Jellyfinでメディアサーバを作ろう

[株式会社ピーアールオー（あったらいいな！を作ります） Advent Calendar 2020](https://qiita.com/advent-calendar/2020/pro-japan1)の19日目になります。

# 前振り
昨年末、会社の忘年会のビンゴでiPadをもらったり、今年初頭に[投げ売りされていたノートPC](https://akiba-pc.watch.impress.co.jp/docs/wakiba/find/1232481.html)を購入したりと、我が家の端末が増えました。
それら端末で作業していると、デスクトップPCに保存している音楽が聴きたくなります。
単純に再生するだけならば、sambaなどを使用してファイルサーバを構築すればよいですが、
ファイルの検索やプレイリストの作成並びに各端末での共有などを考えると、使い勝手があまりよくなさそうです。
かといって、今更iTunesなどの使い方を覚えるのもピンと来ませんでした。
そこで、自分用のメディアサーバを構築し、音楽ファイルやプレイリストを一元管理することにしました。
いろいろ調べているうちによさそうだな、と思ったのが`Jellyfin`です。

# Jellyfinとは
[Jellyfin](https://jellyfin.org/)とは、音楽や動画といった各種メディアの管理と、ストリーミング再生を行えるフリー(GPLv2)のメディアサーバです。

- [公式](https://jellyfin.org/)
- [GitHub](https://github.com/jellyfin/jellyfin)

## 著作権
まず気になるのは**著作権について**です。  

[日本オーディオ協会](https://www.jas-audio.or.jp/network-hometheater/network-guide2_1)
> - ご自身で購入したディスクからリッピングしてください。
> - リッピングしたデータは個人使用しかできません。無断で、売る、貸す、ホームページやSNSに公開する、などを行ってはいけません。

[JASRAC](https://www.jasrac.or.jp/copyright/use/index.html)

> 個人的または家庭内その他これに準ずる限られた範囲内において使用する目的で、使用する本人がコピーする場合、著作権者の許諾を得ずにコピーすることができます。
> ただし、違法にインターネット上にアップロードされたものと知りながら著作物をダウンロードする場合など、私的使用のための複製であっても違法となる場合があります。

素人判断ですが、以下の行為はNGでしょう。
**くれぐれも個人の範囲で使用しなければなりません！**

- 他者(関係問わず)とのアカウントの共有
  - そもそも他者にサービスに触れさせることがよくない
- 自ら購入していないメディアの使用
- 不特定多数を相手にしたメディアの再生

# 構築環境
ちょうど我が家にはラズパイ4で作ったKubernetesクラスタ([k3s](https://k3s.io/))があるので、そこにJellyfinを構築します。
(Jellyfinは現状HA構成が不可能なので、クラスタに乗せるうまみはあまりないです)
構成は以下になります。

|役割|機器名|OS|備考|
|---|---|---|---|
|マスター|Raspberry pi4 8GB|Ubuntu 20.04 LTS||
|ワーカーA|Raspberry pi4 8GB|Ubuntu 20.04 LTS||
|ワーカーB|Raspberry pi4 8GB|Ubuntu 20.04 LTS||
|NAS|Raspberry pi4 4GB|Raspbian10 (buster)|独立したNFSファイルサーバー。外付けのHDDを接続済み。|

ロードバランサにはレイヤー2なMetalLBを使用していて、IngressはNginxです。
まさしく[このドキュメントの構成](https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#a-pure-software-solution-metallb)です。
また、ストレージは外付けHDDを接続したNFSサーバを別途用意して、永続ボリュームを作成します。  
  
なお、[公式ドキュメント](https://jellyfin.org/docs/general/administration/installing.html#docker)にはdocker-compose.yamlのサンプルが置かれています。
試しに起動してみたいという場合には、こちらを使うのがよいでしょう。

# 構築

## 1. ビルド、プッシュ
とてもありがたいことに、aarch64(ARM64)用のイメージが用意されているのでそのまま使用します。
私は[GitHub Container Registry](https://github.blog/2020-09-01-introducing-github-container-registry/)を使用してイメージ管理をしているので、ビルドしてプッシュしています。

```dockerfile:Dockerfile
FROM jellyfin/jellyfin
```

```bash
# docker build . -t ghcr.io/<github-user>/jellyfin:x.y.z
# docker push ghcr.io/<github-user>/jellyfin:x.y.z
```


## 2. 永続ボリューム作成  
[公式のドキュメント](https://jellyfin.org/docs/general/administration/installing.html#docker)を参考に、`media`、`config`、`cache`の3種を設定します。
私の環境では、この3ディレクトリをあらかじめNFSの設定をしておき、`media`には再生したいメディアファイルを格納しておきます。
**全て自炊のファイルとしています！**

### 2.1 media用PV & PVC

```yml:media-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jf-media-pv
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
  namespace: app
spec:
  capacity:
    storage: 2Ti
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.0.10
    path: /mnt/path/jellyfin/media/
  claimRef:
    name: jf-media-pv-claim
    namespace: app
```

```yml:media-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jf-media-pv-claim
  labels:
    app: jellyfin
  namespace: app
  annotations:
    "volume.beta.kubernetes.io/storage-class": "slow"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Ti
```

### 2.2 config用PV & PVC

```yml:config-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jf-config-pv
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
  namespace: app
spec:
  capacity:
    storage: 2Ti
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.0.10
    path: /mnt/path/jellyfin/config/
  claimRef:
    name: jf-config-pv-claim
    namespace: app
```

```yml:config-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jf-config-pv-claim
  labels:
    app: jellyfin
  namespace: app
  annotations:
    "volume.beta.kubernetes.io/storage-class": "slow"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Ti
```

### 2.3 cache用PV & PVC

```yml:cache-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jf-cache-pv
  annotations:
    volume.beta.kubernetes.io/storage-class: "slow"
  namespace: app
spec:
  capacity:
    storage: 2Ti
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 192.168.0.10
    path: /mnt/path/jellyfin/cache/
  claimRef:
    name: jf-cache-pv-claim
    namespace: app
```

```yml:cache-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jf-cache-pv-claim
  labels:
    app: jellyfin
  namespace: app
  annotations:
    "volume.beta.kubernetes.io/storage-class": "slow"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Ti
```

## 3. Deployment定義作成
Deployment定義を作成します。 

```yml:jellyfin-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: jellyfin
  name: jellyfin
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jellyfin
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: jellyfin
    spec:
      containers:
      - image: ghcr.io/<github-user>/jellyfin:x.y.z
        imagePullPolicy: ""
        name: jellyfin
        ports:
        - containerPort: 8096
        volumeMounts:
        - mountPath: /cache
          name: jellyfin-cache-persistent-storage
        - mountPath: /config
          name: jellyfin-config-persistent-storage
        - mountPath: /media
          name: jellyfin-data-persistent-storage
      restartPolicy: Always
      imagePullSecrets:
        - name: github
      volumes:
      - name: jellyfin-cache-persistent-storage
        persistentVolumeClaim:
          claimName: jf-cache-pv-claim
      - name: jellyfin-config-persistent-storage
        persistentVolumeClaim:
          claimName: jf-config-pv-claim
      - name: jellyfin-data-persistent-storage
        persistentVolumeClaim:
          claimName: jf-media-pv-claim
```

## 4. Service定義作成
今回はIngressをhttps終端とし、コンテナにはhttpでアクセスします。
8096でhttpリクエストを受け付けています。

```yml
apiVersion: v1
kind: Service
metadata:
  annotations:
    name: jellyfin
  labels:
    app: jellyfin
  name: jellyfin
  namespace: app
spec:
  ports:
  - name: jellyfin
    port: 8096
    targetPort: 8096
  type: NodePort
  selector:
    app: jellyfin
```

## 5. kustomization作成

作成したyamlを一つにまとめます。
例えば、以下のようなディレクトリ構造の場合。

```bash
# tree
.
├── deployment
│   ├── jellyfin-deployment.yaml
│   └── jellyfin-service.yaml
└── pv
    ├── media-pv.yaml
    ├── media-pvc.yaml
    ├── config-pv.yaml
    ├── config-pvc.yaml
    ├── cache-pv.yaml
    └── cache-pvc.yaml
```

このようなyamlを作成します。

```yml:kustomization.yaml
resources:
    - pv/media-pv.yaml
    - pv/media-pvc.yaml
    - pv/config-pv.yaml
    - pv/config-pvc.yaml
    - pv/cache-pv.yaml
    - pv/cache-pvc.yaml
    - deployment/jellyfin-deployment.yaml
    - deployment/jellyfin-service.yaml
```

最終的に以下のようなファイル構成となります。

```bash
# tree
.
├── deployment
│   ├── jellyfin-deployment.yaml
│   └── jellyfin-service.yaml
├── kustomization.yaml
└── pv
    ├── media-pv.yaml
    ├── media-pvc.yaml
    ├── config-pv.yaml
    ├── config-pvc.yaml
    ├── cache-pv.yaml
    └── cache-pvc.yaml
```

## 6. Jellyfin(pod)をデプロイ
以下のコマンドでデプロイします。

```bash
# kubectl apply -k ./
```

## 7. Ingressをデプロイ
Ingressの設定をデプロイします。

```yml:ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: app-proxy
  namespace: app
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - secretName: servercert
  rules:
    - host: jellyfin.local
      http:
       paths:
       - path:
         backend:
           serviceName: jellyfin
           servicePort: 8096
```

```bash
# kubectl apply -f ingress.yaml
```

これは、例えば以下のような出力の場合、
`192.168.0.100`に`https://jellyfin.local`でアクセスすると、jellyfinにアクセスすることができます。

```
# kubectl -n ingress-nginx get svc
NAME            TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
ingress-nginx   LoadBalancer   10.43.142.98   192.168.0.100   80:31614/TCP,443:30633/TCP   177d
```

あとは、podが上がれば起動完了です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/737cfd9a-9ff2-6a85-a015-146827c63aac.png)

## 動かしてみる
`https://jellyfin.local`でアクセスします。
なお、管理者ユーザの作成等が発生するので、この時点での外部公開は避けたほうがよいでしょう。

## 初期設定

## 1. 表示する優先言語を設定
今のところ日本語はありません。
今回は英語で進めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/3b611287-02e6-fdb6-296b-24efc0f54384.png)

## 2. 管理者ユーザ作成
ユーザ名とパスワードを入力し、管理者ユーザを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/660709de-db02-90a1-a32b-de7b07b0a225.png)

## 3. メディアライブラリの設定
まずは`Add Media Library`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/beed5aab-54a5-8463-8287-ad6c86940824.png)


ダイアログが表示されます。
今回は`Content type`に`Music`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/c031bc6c-e4e4-8ac2-4573-5e6fa410564d.png)


`Folders`の`+`を押し、`/media`を選択します。
このディレクトリがPVで設定したメディア用ディレクトリです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/4010f8f6-a2e8-4138-8001-eb2898e1bbab.png)

正しくPVが設定されていれば、格納した音楽が表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/70655be8-4b51-9651-0958-8b2e0841685c.png)

`OK`を押すと`/media`がライブラリとして追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/f26a5491-67ea-f9ca-f55d-b8a9f269a169.png)

メディアのmetadataの優先言語の設定です。
ひとまず英語で進めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/4e0c0076-4d93-aecd-6f6e-f721076af771.png)

リモート接続の可否を設定します。
デフォルトはチェックONです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/6379aabf-adfb-6df8-9651-a7c5ab558410.png)

以上で初期設定完了です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/8dc1c5bc-d78d-e8f1-f036-3f37b8e2785b.png)

## 4. ログインする
ログイン画面が表示されるので、作成したアカウント/パスワードでログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/439ef9c1-ac00-f925-40bb-693ded19eb66.png)

ログインすると、メディアが表示されます。(これでメディアの再生が可能です)
初回はライブラリのスキャンが発生するため、全てのメディアが表示されるまで少し時間がかかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/ffb1e51e-0ab5-88c0-9015-e6e9567d63e3.png)

## 使ってみて
Jellyfinはまだまだ発展途上です。
以下に使ってみてよかったところ、今後に期待なところを挙げてみます。

### いいところ  
- クライアントのプラットフォームを問わない
- 専用のモバイルアプリが存在
  - まだまだ発展途上のようだが、必要最低限の操作は可能
- 内部のSQLiteでデータ管理するので、別途DBを用意する必要がない
- 公式にDockerfileが用意されているので、構築が容易
- 見た目がしゃれている

### 今後に期待なところ
- SQLiteにしか対応していない
  - ライブラリが巨大化するとパフォーマンスが懸念される  
- HA構成ができない
  - 試したところ、sqliteのファイルロックでぶつかった  
- OIDC/SAMLに対応していない。サポートされているのは以下の二つ
  - ID/パスワード認証
  - LDAP認証
- モバイルアプリはストリーミング再生のみ対応
  - 事前にダウンロードしておくことは不可(非公式クライアントは対応しているのでそちらを推奨)

### クライアントアプリ
非公式アプリですが、どちらも優秀なアプリです。
(事前に楽曲をダウンロードすることでオフライン再生が可能)
- Finamp
  - https://github.com/jmshrv/finamp
- Fintunes
  - https://github.com/leinelissen/jellyfin-audio-player

# 最後に
CDは買いましょう。(置き場所が。。。)
![IMG_20201221_134225011.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531974/ae41dcbe-4acd-ce4c-7b4b-0b501f215877.jpeg)