# Jellyfin 真的好用吗？

我的结论是：实则不然。但 DIY 这一块本就是矮个子拔高个，也没那么多平替可以挑。

> [!important]
> - 本文字里行间夹带着作者的私货，行文并不严谨，谨慎阅读。
> - 本文有关的折腾流程时间跨度较长，经验之谈不说没有，也只能说碎得跟渣一样。~~所以不是笔记而是大作文。~~

## 硬件加速，硬件呢

我是 Linux Docker 部署的。由于因特耳的核显驱动闭源，**官版 Docker 容器搞起来后还需要`exec -it`进去装 Intel 驱动**。即便是`nyanmisaka`版开箱即用 Jellyfin 镜像，也需要**手动映射渲染节点**（他们就没设置过`render`用户组，若要**映射整个`/dev/dri`后续就需要进去`groupadd`**）。

::: details docker-compose.yml
See [AgxCOy@liteyuki/agserver.svc](https://git.liteyuki.org/AgxCOy/agserver.svc).
```yaml
services:
  jellyfin:
    image: nyanmisaka/jellyfin:latest
    container_name: jellyfin
    network_mode: 'host'
    volumes:
      - /var/lib/jellyfin:/config
      - jellyfin-cache:/cache
      - /media:/media
      # - /dev/dri/renderD128:/dev/dri/renderD128
      - type: bind
        source: /usr/share/fonts/opentype
        target: /usr/local/share/fonts/custom
        read_only: true
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128
      #- /dev/dri/card0:/dev/dri/card0
    restart: 'unless-stopped'
    # Optional - alternative address used for autodiscovery
    # environment:
    #   - JELLYFIN_PublishedServerUrl=
    extra_hosts:
      - 'host.docker.internal:host-gateway'
volumes:
  jellyfin-cache:
    driver: local
```
:::

## 媒体库组织

我还是更习惯树形文件系统，媒体库还是太难为我了。那样的话，事情或许也很简单——许多成品 NAS 都有自带视频、相册之类的浏览功能。但很遗憾，自己装的 Ubuntu Server 自然什么都没有。

我针对的还是 Jellyfin “神奇”的资源组织方式。就像[这篇《基于 Jellyfin 和音流的 nas 影音库搭建及踩坑》](https://sspai.com/post/90896)所说，“为什么不直接读取音乐标签进行专辑聚合”呢？

### 元数据刮削：一笔糊涂账

然而事实上，即使真的能做到自动专辑聚合，元数据本身的刻录和查询也是一片混乱。上面援引的博文推荐 MusicTag 这个工具，但同是从网易云那刮削，下载下来本是《紫罗兰永恒花园 OST》的曲目能给它识别成《凹凸世界五周年》，也是给我看笑了。更别说国外的 MusicBrainZ 数据库有相当一部分冷门曲目并没有收录（尽管 Spotify 等平台能够找到）。

![误刮削](./musictag-misfetch.webp)

::: tip 音乐库最简方案
文档这么说：我管你怎么整理，我只知道**是同一张专辑就该塞在同一个文件夹**。

那么事情也好办，就按专辑名来呗。好比如：`music/{album or "[standalone]"}/{artist1,artist2} - {title}`。

- 为什么要`or "[standalone]"`？
  - 不单独收纳**没有专辑**的单曲，结果就会被淹没在大几百首、好几页的“歌曲”页面。

- 要是有专辑重名了（比如两张专都叫`Fragrance`）？
  - 没招，默认合并（甚至可以看到重复的曲号，都是第六首）。只能手工分离（`Fragrance - SoundzImage`和`Fragrance - Hatsune Miku`）。
:::

但比起前面所述的国内外两桌草台班子，更令我恼火的当属大把大把的、查无此人的“黑户”。怎么来的？音声、网文、3D 动画。

> 我的整理方式和 acgdb 类似：3D 区、音声区（作者分发、油管录播、DLsite 正作）、书目区（漫画和文集）、写真区、插画区，此外新增音乐区。

3D 区无论玩恋活还是 MMD，既有图集又有视频，无法按媒体类型区分。即便真要分，它算电视节目？电影？MV？分不明白。

音声区大量的音频资源，摆烂一点直接用电子书库、按文件夹访查也不是不行，但显示出来反倒是专辑名（或者说 DLsite、系列作品名）更显眼，而非每一集的标题；若要用音乐库收纳，就得改造一番目录树适配它那个“一个专辑只有一个目录”的原则，工作量也不小。

至于书目区，正经出版物想获取元数据并不难。相对的，“不正经”读物呢？网文可以依靠豆瓣之流；同人文只要作者有公开发布，留心寻找也是能记下元数据的（同人圈子对打 Tag 这件事很看重）；那么只剩下来源不正经、题材也不正经的“那种”作品了。它们流通的地方通常都是论坛这种早期网络社区，只知道个标题（很难说发帖人就是作者，万一是二道贩子呢？甚至文件名也不一定就是完整标题）。What can I say?

偏偏这种不在台面上的“资源”我收纳了不少，哪怕有自动化工具，后续校对也是个体力活，何况很多资源只能一件件手工入库。饶了我吧。

### 电子书：吃力不讨好

小说漫画，或者说书籍媒体库，只能说真搭一个私人图书馆大概也和 Jellyfin 别无二致——满满的工作量。可能甚至有过之而不及。

> 不禁想到米哈游创始人蔡浩宇的“快乐守恒定律”：你做的过程中如果很轻松快乐，那么你做出来的结果未必能给人带来快乐。  
> 媒体库编排是不是也要“苦其心志、劳其筋骨”，才能有好的观影体验呢？但话又说回来，这种家庭影院通常都是自己管理自己用，费那么大功夫组织这些文件，观赏的体验又能改善多少呢？

我相信大多数\*并不在乎资源来源\*的读者去找网络上流通的小说肯定优先去找 txt、doc(x) 这种容易打开的格式。但现有的私人图书馆，无论 Jellyfin 这种顺手实现的，还是 Kavita 这种专用的，**都不支持这些格式**。Calibre 也许可以，但**书目的元数据就成了另一桩麻烦事**。

::: tip 最简方案
那当然是通通转换成 pdf，单纯当作文件管理器那样访问文件夹、文件最简单。**但 pdf 对移动端并不友好，文字内容并不能自适应，更多还是用于漫画**。
:::

进阶一点的方案主要围绕 epub 电子书，毕竟漫画这块 pdf、epub、`.cb*`压缩包三者的页面版式不可能差太多，翻页起来其实没什么区别。

::: info 压缩包漫画
漫画本质上就是一页页的画嘛。许多网站本就提供试阅，给这些图片打个包也就是顺手的事。像我这样存储空间拮据的（主要还是[协议不通用](recents-11172025.md#硬件购置)），只能是哪种最节省空间就选哪种咯。

Jellyfin 本身支持`.zip` `.rar` `.7z`。如果你打算另起炉灶，但选配的服务不支持这样通用的后缀名，也可以改变后缀：
`.cbz`即 zip、`.cbr`即 rar、`.cb7`即 7z、`.cbt`即 tar。

至于元数据，Jellyfin 文档指出**只有 epub 书籍才允许元数据集成在文件里**，对于这种漫画压缩包，需要另写`ComicInfo.xml`。也就是说，**每一本、系列的每一话，都需要单独的文件夹放置元数据和本体**。
:::

现有的 epub 编辑器并没有像写哔哩哔哩专栏（或者像 Word 文档）那样自动编纂页面的能力，操作起来更像是手搓 Web 三剑客（HTML、CSS、JavaScript）。所以，**图方便的话更建议在线转换**`.txt` `.doc(x)`文档。

::: details EPUB 内容物
epub 的 mimetype 通常标`application/epub+zip`，说明其本质是压缩包。解压出来通常分这么几块：
- `mimetype`：资源类型标识，内容如上。
- `META-INF/container.xml`：标明电子书（OEBPS 包）的根目录。
- `OEBPS/`
  - `Images/cover.*`：封面
  - `Styles/*.css`：网页排版
  - `Text/*.*htm*`：反正必须是 HTML。
  - `content.opf`：电子书的元数据、内容物信息。
  - `nav.*htm*` `nav.opf`：电子书的目录。前者为 epub3 标准，后者为 epub2 标准，通常在`content.opf`里特别标注。
  - `Fonts/` `Audio/` ...：其他内容。

所以我称编辑 epub 这件事为“手搓网站”，因为本来就需要按章回编排 Web 内容，这 OEBPS 的目录树也很有网站的味。
:::

::: warning 书目目录索引
和 Calibre 等阅读器不同，Jellyfin 对目录的跳转**以 OEBPS 包为基准**，而不是以目录文件为基准。如果发现 Jellyfin 里**点目录里的章标题没有反应**，你需要用 Sigil 等编辑器将`nav.*`移动到`OEBPS/`目录下。
:::

::: note 文件组织
实测结论是：可以不像漫画那样遵循官方文档“一书一目录”的要求，但**应尽量避免“一目录只有一 epub”的情形**。像以下情形 Jellyfin 会误判：

- Books
  - 正经的
    - 《时间简史》.epub
    - 毛泽东
      - 《实践论》.pdf
      - 《矛盾论》.pdf
    - 《葬送的芙莉莲》.pdf
  - 不正经的

扫描结果是：“不正经的”和《时间简史》。其余读物无法直接访问（可能仍能搜索到）。
:::

## 穿透与反向代理

如今连网易云也改得不知为何物了，于是我随身听的需求便也转向 Jellyfin。最简单的方法可以是 ZeroTier，但安卓有“同一时间只允许一个 VPN”的限制，所以在群友指导下搞了内网穿透（FRP）。

此后，我遇到了子网划分的问题。起初这个问题并不起眼，直到我在户外同时尝试走 ZeroTier 和穿透，我才发现 frp 连上之后似乎更像是在本地回环，而 Jellyfin 似乎**对本地回环不设限速**。

> ~~写着写着总会变成报流水账。~~

AI 对此的解法是在 frp 这里记录下真实 IP。我选配的服务商支持 Proxy Protocol，也更推荐这么实现。但在实地测试后，发现 Jellyfin 并不直接支持 Proxy Protocol，那么就只能在 frpc 跟 Jellyfin 之间多加一层 nginx 做反向代理了（frps 显然我是动不了的）。

方法很简单，frp 侧仍旧启用 Proxy Protocol，在 nginx 里剥离真实 ip。我是在 Ubuntu Server 上安装的 nginx，据称已集成`real-ip`模块。那么编辑`/etc/nginx/sites-enabled/default`（我是图方便直接原地开刀了）：

```
server {
  listen 8097 proxy_protocol;
  listen [::]:8097 proxy_protocol;
  set_real_ip_from 127.0.0.1;
  real_ip_header	proxy_protocol;

  server_name _;

  add_header X-Content-Type-Options "nosniff";

  location / {
    proxy_pass http://127.0.0.1:8096;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Protocol $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_buffering off;
  }

  location /socket {
    proxy_pass http://127.0.0.1:8096;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Protocol $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
  }

  # Jellyfin 推荐给真实客户端信息“打码”
  access_log /var/log/nginx/access.log stripsecrets;
}

# 为了解说方便把 http 块里的全局“打码”配置拉下来叻（本文件本来就嵌套在 http 块）。
# 不清楚先后顺序有没有影响。
log_format stripsecrets '$remote_addr $host - $remote_user [$time_local] '
                    '"$secretfilter" $status $body_bytes_sent '
                    '$request_length $request_time $upstream_response_time '
                    '"$http_referer" "$http_user_agent"';

map $request $secretfilter {
    ~*^(?<prefix1>.*[\?&]api_key=)([^&]*)(?<suffix1>.*)$  "${prefix1}***$suffix1";
    ~*^(?<prefix1>.*[\?&]ApiKey=)([^&]*)(?<suffix1>.*)$  "${prefix1}***$suffix1";
    default                                               $request;
}
```
