Plex

使用Plex & Docker搭建自己的媒体服务器

URL Source: https://blog.hinatarin.com/2021/04/21/set-up-your-own-media-server-with-plex-and-docker/index.html

Plex是Plex,Inc.制造的全球流媒体服务和客户端服务器媒体播放器平台。PlexMedia Server整理用户收藏和在线服务中的视频,音频和照片,并将其流式传输到播放器。

尽管提供多种媒体服务, 我这里的需求只是流媒体播放我的本地音乐集还有R音声作品而已,所以关于后面的媒体配置相关都是关于音乐 的, 使用 Dockerlinuxserver 提供的 镜像 搭建Plex Server非常简单

部分截图:

Music Library

Music Library

image-20210420121417478

Music Album

image-20210420121148416

Playing Music

image-20210420150223086

Music Artist

相比与同类型的开源选择_Jellyfin_, 界面更加漂亮,依赖于大公司的技术使用体验也更加好,接下来我们将要使用 Docker 搭建自己的服务

你需要准备的东西有

  • NAS设备(树莓派4B即可) + 公网IP/内网穿透 / 土豪直接买云服务器

搭建Plex Server


新建文件夹plex, 创建docker-compose.yml

mkdir plex && cd plex  
touch docker-compose.yml  

1- 写入下面的配置

version: "2.1"  
services:  
  plex:  
    image: linuxserver/plex  
    container\_name: plex  
    environment:  
      \- PUID=1000  
      \- PGID=1000  
      \- VERSION=docker  
      \- PLEX\_CLAIM=your-claim  
    volumes:  
      \- ./config:/config  
      \- /path/to/your/music/library:/musics:ro  
    restart: unless-stopped  
    mem\_limit: 700m  
    memswap\_limit: 2000m  
    devices:  
      \- /dev/dri:/dev/dri   
    ports:  
      \- 32400:32400  

2- 需要注意的几点:

  • PUIDPGID用于配置容器内进程的UIDGID, 全都设置为0表示以root用户运行,如果你这里不是很明白的话可以无脑设置为0以避免部分权限问题

  • mem_limitmemswap_limit属于可选项, 如果你(我)使用 2GB 内存的树莓派你可能会担心超出内存而出现的问题(一般出现在扫描媒体时, 日常占用并不大, 2GB内存足矣),如果你的内存充足的话,完全可以忽略这两个配置项

  • PLEX_CLAIM环境变量用于认证自己的服务器,也是可选, 你可以从 这里 获取(注意需要可用的plex账号), 另外claim的有效期一般只有 4 分钟 ,如果服务器网络不佳,建议先通过执行docker-compose pull拉取镜像之后再获取,防止过期(虽说过期后再重新claim也行)

  • /path/to/your/music/library:/musics:ro 将自己本地的音乐库映射到plex container的/musics目录, 并且只读(roread only),你可以将自己用nextcloudsyncthing同步过来的曲目映射到这里

执行docker-compose up然后静待服务器启动完成, 启动完成后可以访问ip:32400/web进入web界面

检查你的远程访问有没有正确开启, 成功的示例如下

image-20210421114615816

由于我端口映射并非标准的32400端口, 所以这里手动指定了一下自己的端口

如果已经成功了的话, 你就可以在plex官网看到自己的 server 了, 接下来你可以按照提示设置自己的媒体库, 至于音乐库相关的高级选项可以参考下面的解释

image-20210421114937537

移动端示例截图

Albums Playing Music

音乐相关高级配置解释


扫描器和代理这两个选项不用考虑, 默认就行, 其中Plex Music是最近几个版本更新的扫描器, 性能更好,扫描速度明显快于旧的Plex Music Scanner

image-20210421113102215

从网易云下载的曲子一般会自带正确的ID3tag, 我们这里可以直接选中Prefer local metadata , 让扫描器优先使用自带的tag.

Artist Bios 歌手信息。推荐保持选中, 我这里没有选中是因为我歌单里面太多冷门优秀歌手/作曲比如 Acyanxi, plex拉取不到这些歌手/作曲的信息,导致后面只有一小部分歌手有封面而多数都没有, 强迫症的我直接把这个选项给关了。

Album Reviews and Critic Ratings Popular Tracks Concerts 歌曲评价相关,热门曲目和演唱会信息 我个人认为没有必要没有打开, 你可以根据自己的情况选择, 我没有打开, 毕竟热门的并不代表适合自己, 演唱会又不能去(没钱买机票), 我觉得好听的就是最好的, 看xx评价

Genres 注意网易云给的 tag 中的Genres都是空的, 如果你认为音乐风格对你来说有必要可以保持默认的Plex Music选项,虽然并不保证一定能得到

Album Art专辑封面, 网易云提供了, 所以我直接选择了Local Files Only

Scanning

Scanning

1- NCM 文件批量解码

import os  
from ncmdump import dump  
from pathlib import Path  
  
source\_dir = Path(r"F:\\Musics\\netease")   
  
  
def output\_path(input\_path: Path, metadata) -\> str:  
    return (input\_path.parent /  
            (input\_path.stem + "." + metadata\['format'\])).\_\_str\_\_()  
  
  
if \_\_name\_\_ == '\_\_main\_\_':  
    for file in source\_dir.iterdir():  
        if file.name.endswith('.ncm'):  
            print(str(file))  
  
            dump(file, output\_path, skip=True)  
            os.remove(file)  
  

ncmdump模块来自https://github.com/nondanee/ncmdump, 你可以通过执行下面的命令安装依赖和ncmdump

pip install pycryptodome mutagen  
pip install git+https://github.com/nondanee/ncmdump.git  

2- 删除重复文件

网易云音乐下载的曲目有可能会出现重复下载的情况, 一般情况下重复下载的曲目文件名称有比较明显的特征,例如XX - XXX (1).mp3, 可以通过这个特征删除掉重复的下载的曲目

from pathlib import Path  
import re  
import os  
  
source\_dir = Path(r"F:\\Musics\\netease")   
target = re.compile(r"^.+\\(\[1-9\]\\)\\.(mp3|ncm)$")  
  
if \_\_name\_\_ == '\_\_main\_\_':  
    for file in source\_dir.iterdir():  
        if not file.name.endswith((".mp3", "ncm")):  
            continue  
  
        if target.match(file.name):  
            print(file)  
            os.remove(file)  

3- 目录转移(Plex推荐格式)

plex推荐了音乐相关的目录格式为

Music/ArtistName/AlbumName/TrackNumber - TrackName.ext  

例如

/Music  
   /Pink Floyd  
      /Wish You Were Here  
         01 - Shine On You Crazy Diamond (Parts I-V).m4a  
         02 - Welcome to the Machine.mp3  
         03 - Have a Cigar.mp3  
   /Foo Fighters  
      /One By One  
      /There is Nothing Left to Lose  
   /U2  
      /Joshua Tree  

如果一首曲子存在多个artists, plex推荐使用 Various Artists作为ArtistName, 搞不清理由,由于我下载的曲目太多是多艺术家创作的, 都放在这样的文件夹里让我感觉很乱,因此并没有遵守这个推荐规则

为了后面新增曲目时能快速找到哪些是新增的曲目, 我这里随便使用了sqlite保存了源文件目录的位置

from pathlib import Path  
from mutagen.id3 import ID3  
import shutil  
import sqlite3  
  
source\_dir = Path(r"F:\\Musics\\netease")   
target\_dir = Path(r"F:\\nextcloud\\Musics\\NETEASE")   
db\_path = 'music.db'  
  
def safe\_name(name: str) -\> str:  
    """this function is awful and if you got a better one, just tell me  
    """  
    return name.replace("/", "/").replace(":", ":").replace("\>", "❯").replace(  
        "|", " ").replace("<",  
                          "❮").replace("\\\\", " ").replace("?", "?").replace(  
                              '"', "'").replace("\*", "*").strip().strip('.')  
  
  
if \_\_name\_\_ == '\_\_main\_\_':  
    con = sqlite3.connect(db\_path)  
    cur = con.cursor()  
  
      
    cur.execute('''CREATE TABLE IF NOT EXISTS musics  
                (path text NOT NULL PRIMARY KEY)''')  
  
    for source\_file in source\_dir.iterdir():  
        if source\_file.name.endswith(".ncm"):  
            continue  
  
        source\_file = source\_file.absolute()  
        cur.execute("SELECT \* FROM musics WHERE path = ?", \[str(source\_file)\])  
        if cur.fetchone():   
            continue  
  
        metadata = ID3(source\_file)  
        artist = source\_file.name.split('-')\[0\].strip().split(',')\[0\]  
        try:  
            album = safe\_name(metadata.get("TALB").text\[0\])  
        except AttributeError:  
            print(source\_file, "has no album, just skipped")  
            continue  
  
        target\_file: Path = target\_dir / artist / album / source\_file.name  
  
        if target\_file.exists():  
              
            cur.execute("INSERT INTO musics VALUES (?)", \[str(source\_file)\])  
            con.commit()  
            continue  
  
        if not target\_file.parent.exists():  
            target\_file.parent.mkdir(parents=True, exist\_ok=True)  
  
        shutil.copy(source\_file, target\_file)  
  
          
        cur.execute("INSERT INTO musics VALUES (?)", \[str(source\_file)\])  
  
        con.commit()  
  
    con.close()