阅读本文需要有一定的Linux、Docker等计算机知识,什么?你不懂 Go to study,just do it!

写在前面

本文是基于阿里云盘资源搭建Emby影库,使用其他网盘/本地硬盘搭建大部分也是可行的。尽管阿里审查严格,容易和谐资源。但是耐不住用户数量庞大,资源分享快和全且可以302直链(使用阿里国内CDN来看视频,速度很快)。

说起流媒体平台,国内有优酷、爱奇艺、腾讯视频这’三巨头’,国外则有Netflix、Amazon Prime Video、Disney+、HBO等’大佬’。但不知道你有没有发现,无论是国内还是国外的平台,都或多或少存在一些让人头疼的问题

“国内平台?哎,说起来都是泪啊!会员+广告的双重’折磨’,看个剧还得忍受中途插播的广告,这谁顶得住啊?虽然各大平台在自制内容上砸了不少钱,试图通过高质量的精品内容提升付费订阅的比例,但受限于有限的观众规模、注意力以及严格的内容监管,高质量内容的产出着实不易。更别提国内用户的付费习惯和版权意识还在’襁褓’中呢。”

“再看看国外平台,貌似高大上了不少。用户订阅是主要收入来源,内容制作也更有针对性。虽然产出的内容不一定都达到极高的质量标准,但至少能够满足“标准线”的要求,更不用说其中不乏许多高质量的作品。在欧美,用户为优质内容付费已成为一种长期以来的习惯,而“标准线”以下的剧集则难以在欧美市场获得用户增长。但这’高大上’的背后,可是要付出不菲的订阅费用啊!虽说内容质量有保证,但对于我们的钱包来说,可是不小的负担呢。”

那么,有没有一种既能看到优质内容,又不用花费太多的方法呢?答案就是——Emby!这个’神器’简直就是影视爱好者的福音啊!

Emby就像是一个神奇的百宝箱,里面藏着无数的影视珍宝,等着你去探索。它的自定义程度高得简直让人咋舌,就像是一个超级听话的管家,你想看什么它就能立刻呈上来,简直就是为’挑剔’的你量身定制的。

其次,内容多样性简直爆棚!内容的丰富程度?简直就是一座取之不尽、用之不竭的影视宝库!只要你的网盘/本地硬盘容量够大,几百T甚至PB级的内容都不在话下。从黑白默片、经典老片到最新大片,应有尽有,简直就是一部活生生的电影发展史。

最让人欣喜的是,Emby就像是一个没有广告打扰的私人影院。你可以在沙发上、床上,甚至是在马桶上(别笑,我知道你们有人这么干)尽情享受观影的乐趣。想在哪看就在哪看,爽歪歪!

哦,差点忘了提那令人垂涎的海报墙!精心刮削整理的海报就像是一幅幅艺术品,让你在选片的时候就开始享受视觉盛宴了。”

生命短暂,我用Emby。但别忘了,尊重版权不仅是法律的要求,也是对创作者劳动的尊重。

成品展示

  • 我的豆瓣/IMDb影库-点击进入
  • 99%上榜影片均可在我的网站上观看(天朝特色,容易和谐)
  • 片源大部分为1080P HEVC,服务端不解码,请使用外部播放器
  • 推荐下载使用Potplayer、VLC、MX Player、Emby官方/第三方播放器

毒师镇楼

双榜入选,不同价值观的共同爱好doge

希望让你自由

安装 Alist

官方文档很详细了

配置阿里云盘Open很详细了

注:WebDAV 策略 选择 本地 可以直接通过emby刮削元数据及海报到阿里云盘

安装 Clouddrive2 (付费)

推荐使用Docker安装

CloudDrive下载

Docker安装指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
cloudnas:
image: cloudnas/clouddrive2
container_name: cd2
ports:
- "127.0.0.1:19798:19798"
environment:
- CLOUDDRIVE_HOME=/Config
volumes:
- /mnt:/CloudNAS:shared
- /root/cd2:/Config
devices:
- /dev/fuse:/dev/fuse
restart: unless-stopped
privileged: true

[!WARNING]

clouddrive2免费用户只允许挂载一个

安装 Rclone (免费)

1
2
3
4
5
6
7
8
#安装 fuse3(防止报错,新版Rclone需要)
apt install fuse3
#安装 Rclone
curl https://rclone.org/install.sh | sudo bash
#配置
rclone config
#创建挂载目录(必须自行创建,否则启动rclone报错)
mkdir -p /mnt/yunpan

rclone config 新建配置,选择 webdav 进行配置即可

自行查看下面文章进行配置

群晖 / Linux 挂载阿里云盘实现 Emby 播放,打造属于自己的家庭影院!

挂载命令

1
rclone mount yunpan: /mnt/yunpan --use-mmap --umask 000 --default-permissions --no-check-certificate --allow-other --allow-non-empty --dir-cache-time 4h --cache-dir=/mnt/cache --vfs-cache-mode writes --buffer-size 32M --vfs-read-ahead 32M --vfs-read-chunk-size 64M --vfs-cache-max-size 1G --daemon
  • yunpan: /mnt/yunpan:挂载远程存储的名称及挂载远程存储的本地目录。
  • --use-mmap:使用内存映射加速文件读取。
  • --umask 000:设置文件和目录的默认权限掩码(000 所有用户都有读、写和执行权限)。
  • --default-permissions:使用默认文件权限,忽略远程权限。
  • --no-check-certificate:不检查SSL证书的有效性。
  • --allow-other:允许除挂载所有者外的其他用户访问挂载的文件系统。
  • --allow-non-empty:允许挂载到非空目录。
  • --dir-cache-time 4h:设置目录缓存的有效时间,减少对远程服务器的请求。
  • --vfs-cache-max-age 1h: 设置虚拟文件系统(VFS)缓存中文件的最大存活时间。***
  • --cache-dir=/home/cache:设置缓存目录的位置。
  • --vfs-cache-mode full:设置虚拟文件系统(VFS)缓存模式为完全缓存。
  • --buffer-size 32M:设置每个打开文件的缓冲区大小。
  • --vfs-read-ahead 32M:预读取数据量的大小,提高读取性能。
  • --vfs-read-chunk-size 64M:设置读取数据块的大小。
  • --vfs-read-chunk-size-limit off:设置读取块大小的上限。***
  • --vfs-cache-max-size 1G:设置VFS缓存的最大空间。
  • --low-level-retries 10:设置遇到错误时低级别重试的次数。***
  • --poll-interval 1m:设置轮询检查远程更新的时间间隔。***
  • --config /root/.config/rclone/rclone.conf:指定 rclone 配置文件的位置。
  • --daemon:在后台以守护进程方式运行

添加挂载守护进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 普通单位文件(只能创建一个服务实例),单挂载
cat > /etc/systemd/system/rclone.service <<EOF
[Unit]
Description=Rclone Mount Service
AssertPathIsDirectory=/mnt/yunpan
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/rclone mount yunpan: /mnt/yunpan --use-mmap --umask 000 --default-permissions --no-check-certificate --allow-other --allow-non-empty --dir-cache-time 4h --cache-dir=/mnt/cache --vfs-cache-mode writes --buffer-size 32M --vfs-read-ahead 32M --vfs-read-chunk-size 64M --vfs-cache-max-size 1G
ExecStop=/bin/fusermount -qzu /mnt/yunpan
Restart=on-failure
User=root

[Install]
WantedBy=default.target
EOF

# 启动服务实例成功再启用自动启动
systemctl start rclone && systemctl enable rclone
# 停止服务实例
systemctl stop rclone
# 重启服务实例
systemctl restart rclone
# 查看服务状态
systemctl status rclone
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 模板单位文件(可以创建多个服务实例),多挂载
cat > /etc/systemd/system/rclone@.service <<EOF
[Unit]
Description=Rclone Mount Service for %i
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/rclone mount %i: /mnt/%i --use-mmap --umask 000 --default-permissions --no-check-certificate --allow-other --allow-non-empty --dir-cache-time 4h --cache-dir=/mnt/cache --vfs-cache-mode writes --buffer-size 32M --vfs-read-ahead 32M --vfs-read-chunk-size 64M --vfs-cache-max-size 1G
ExecStop=/bin/fusermount -qzu /mnt/%i
Restart=on-failure
User=root

[Install]
WantedBy=default.target
EOF

# 启动服务实例
systemctl start rclone@挂载的名称
# 使服务实例在系统启动时自动启动
systemctl enable rclone@yunpan
# 停止服务实例
systemctl stop rclone@yunpan
# 重启服务实例
systemctl restart rclone@yunpan
# 查看服务状态
systemctl status rclone@yunpan

安装 Emby

推荐使用Docker安装

amilys美化版(本文采用并推荐)

lovechen开心版(已停止更新)

阿里云盘直链版安装参考文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
emby:
image: amilys/embyserver:beta
container_name: emby
environment:
- UID=0 # The UID to run emby as (default: 2)
- GID=0 # The GID to run emby as (default 2)
- GIDLIST=0 # A comma-separated list of additional GIDs to run emby as (default: 2)
volumes:
- /root/emby:/config # Configuration directory
- /mnt/yunpan:/ali # Media directory
ports:
- 127.0.0.1:8096:8096 # HTTP port
restart: unless-stopped

阿里云盘直链

目前本地 WebDAV策略为兼容性最好的方式,在 emby 中观看视频时,是先从 webdav 下载到本地再推流到 emby 播放的,所以走的是服务器的流量

302 重定向能够获取到阿里云盘的直链,但是无法使用 emby 进行刮削。

如果只是为了方便观看,可以使用 302 重定向模式,然后用 nplayer 挂载 WebDAV,这样走的就是阿里云官方的流量了,4K 不卡

为了方便emby可以直接刮削数据到阿里云盘,采用本地 WebDAV并使用脚本 bpking1/embyExternalUrl/emby2Alist转直链,本文采用安装版Nginx+Emby(Docker)方式部署,如果你不太懂Nginx,项目提供了Docker容器(Nginx+Emby)可以直接部署,参考教程

注:下文为Nginx+Emby(Docker)方式部署,需要有一定的Nginx经验

安装Njs模块

1
2
3
4
5
6
7
8
9
10
11
12
# 如果已经安装了Nginx,添加njs模块
apt update
apt install nginx-module-njs
## 在nginx.conf文件中添加
load_module modules/ngx_http_js_module.so;
## 重启nginx
systemctl restart nginx

# 编译安装Nginx时包含njs模块
./configure --add-module=实际的njs模块源代码路径
make
sudo make install

Nginx配置

下载emby2Alist的nginx目录,根据实际情况更改配置文件,无特殊要求只需要更改以下文件

/nginx/conf.d/constant.js,/nginx/conf.d/emby.conf

/nginx/conf.d/config/constant-mount.js

/nginx/conf.d/includes/http.conf或者/nginx/conf.d/includes/https.conf

/nginx/conf.d/includes/server-group.conf

1
2
# 直链播放日志查看
tail -f -n 10 /var/log/nginx/access.log /var/log/nginx/error.log | grep js:

注:在constant.js文件中有一行配置为const embyMountPath = [“/mnt”];

当Emby是宿主机运行,路径应该为宿主机的路径,当Emby是Docker运行,路径应该为容器内的路径

挂载到alist的目录 /movie
alist admin 用户,播放 高分剧集/火线/火线S01E01.mkv ,请求 /api/fs/get 接口,请求为
{
“password”: “”,
“path”: “/movie/高分剧集/火线/火线S01E01.mkv”
}

rclone 挂载到/mnt/yunpan,emby 播放视频,请求emby接口 /emby/Items/123/PlaybackInfo 获取播放信息

{
“MediaSources”: [
{
// ……
“Path”: “/mnt/yunpan/movie/高分剧集/火线/火线S01E01.mkv”
// ……
}
]
}

当emby是宿主机运行
要想将 “Path”: “/mnt/yunpan/movie/高分剧集/火线/火线S01E01.mkv” 截取成alist接口的path,那么,配置
embyMountPath 配置为 /mnt/yunpan,截取成 /yunpan/movie/高分剧集/火线/火线S01E01.mkv 就是alist的接口参数。

相关逻辑在 emby.js ,如下:

//fetch alist direct link
const alistFilePath = embyRes.replace(embyMountPath, '')

其中,embyRes 是 “/mnt/yunpan/movie/高分剧集/火线/火线S01E01.mkv” ,replace后是 /movie/高分剧集/火线/火线S01E01.mkv

当emby是docker运行,将宿主机 /mnt/yunpan 映射到容器 /ali。

那么 /emby/Items/123/PlaybackInfo 接口返回应该是 “/ali/movie/高分剧集/火线/火线S01E01.mkv”
同理,embyMountPath 配置为 /ali 即可截取正确

Emby配置

  • 在emby设置中将 用户 –> 播放 允许转码关掉
  • 在emby设置中将 转码 –> 启用硬件加速 关掉

解除阿里云盘第三方应用限速限流

阿里云盘上线第三方权益包(1T流量,svip象征性的送10G),权益包和svip是不同的权益,权益包提供在第三方应用使用云盘上传、下载等权益,svip则是提供在官方云盘客户端(如手机移动端、电脑PC端)的使用权益。也就是意味着不购买第三方权益包的用户使用基于开放平台调用阿里云盘的所有第三方应用都会被限速(单线程500KB/s,最大6线程)

如何解决?充钱购买第三方怨种包! 你这不是欺负老实人(svip)吗

svip的权益包含阿里云盘TV的原画观看,这个TV版客户端是不计入第三方流量的,所以只需要获取到TV版的token就能绕过限速限流了。该方案已被阿里修复过一次,换成了加密接口,虽然又被破解了。但是指不定阿里就会再次动手修复,如果那一天到来,也就只能充钱妥协或者使用其他网盘/本地硬盘

懒人一键获取tv token

1.使用python获取到tv token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import base64
import json
import time
from io import BytesIO

import requests
from Crypto.Cipher import AES
from PIL import Image

if __name__ == '__main__':
data = requests.post('http://api.extscreen.com/aliyundrive/qrcode', data={
'scopes': ','.join(["user:base", "file:all:read", "file:all:write"]),
"width": 500,
"height": 500,
}).json()['data']
qr_link = data['qrCodeUrl']
sid = data['sid']
# 两种登录方式都可以
# web登录, 打开链接登录
print(f'https://www.aliyundrive.com/o/oauth/authorize?sid={sid}')
# 手机阿里云盘扫描登录
Image.open(BytesIO(requests.get(qr_link).content)).show()
while True:
time.sleep(3)
status_data = requests.get(f'https://openapi.alipan.com/oauth/qrcode/{sid}/status').json()
status = status_data['status']
if status == 'LoginSuccess':
auth_code = status_data['authCode']
break
# 使用code换refresh_token
# token_data = requests.post('http://api.extscreen.com/aliyundrive/token', data={
# 'code': auth_code,
# }).json()['data']
# refresh_token = token_data['refresh_token']
# # 已有refresh_token, 直接刷新access_token
# token_info = requests.post('http://api.extscreen.com/aliyundrive/token', data={
# 'refresh_token': refresh_token,
# }).json()['data']
# 原接口404,现更新为新的加密接口
key = '^(i/x>>5(ebyhumz*i1wkpk^orIs^Na.'.encode()
# 使用code换refresh_token
data = requests.post('http://api.extscreen.com/aliyundrive/v2/token', data={
'code': auth_code,
}).json()['data']
plain_data = AES.new(key, AES.MODE_CBC, iv=bytes.fromhex(data['iv'])).decrypt(
base64.b64decode(data['ciphertext']))
token_data = json.loads(plain_data[:-plain_data[-1]])
refresh_token = token_data['refresh_token']
print('Refresh token:', refresh_token)
# 已有refresh_token, 直接刷新access_token
data = requests.post('http://api.extscreen.com/aliyundrive/v2/token', data={
'refresh_token': refresh_token,
}).json()['data']
plain_data = AES.new(key, AES.MODE_CBC, iv=bytes.fromhex(data['iv'])).decrypt(
base64.b64decode(data['ciphertext']))
token_data = json.loads(plain_data[:-plain_data[-1]])
access_token = token_data['access_token']
print('Access token:', access_token)

2.使用cloudflare workers部署阿里云TV refresh接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
export default {
async fetch(request, env, ctx) {
/**
* readRequestBody reads in the incoming request body
* @param {Request} request the incoming request to read from
*/
async function readRequestBody(request) {
const contentType = request.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await request.json();
} else {
return null;
}
}

/**
* gatherResponse processes the response from the origin
* @param {Response} response the response from the origin
*/
async function gatherResponse(response) {
const { headers } = response;
const contentType = headers.get("content-type") || "";
if (contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
}

// Handle the refresh token request
if (request.method === "POST" && new URL(request.url).pathname === "/refresh") {
try {
const content = await readRequestBody(request);
if (!content || !content.refresh_token) {
return new Response(JSON.stringify({ error: "Invalid request body" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}

const response = await fetch("http://api.extscreen.com/aliyundrive/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `refresh_token=${content.refresh_token}`,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await gatherResponse(response);

return new Response(JSON.stringify({
token_type: "Bearer",
access_token: data.data.access_token,
refresh_token: data.data.refresh_token,
expires_in: data.data.expires_in
}), {
headers: { "Content-Type": "application/json" },
});
} catch (e) {
return new Response(JSON.stringify({ error: e.message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

// Handle all other requests
return new Response("Not Found", { status: 404 });
},
};

3.将alist中的阿里云盘Oauth令牌链接改为你的workers地址+/refresh,如果使用国内机器部署可以绑定自己的域名,刷新令牌改为用python获取到的tv token

方案来源:

OverlayFS 可选优化

由于元数据和图片放在网盘上,加载速度会比放在本地慢一些

所以我们可以使用OverlayFS将图片和元数据文件储存到本地

OverlayFS(Overlay File System)是一种联合文件系统,它允许将不同目录挂载到同一个虚拟文件系统下,使得多个文件系统的内容可以被同时访问。OverlayFS特别适用于轻量级的容器虚拟化技术,例如Docker和Podman,因为它可以在不复制整个文件系统的情况下,为每个容器提供一个独立的视图。

OverlayFS的工作原理基于下面几个概念:

  1. 底层(Lower Layer):这是只读层,可以包含一个或多个只读目录。这些目录被叠加在一起,形成底层文件系统。
  2. 上层(Upper Layer):这是一个可写层,位于底层之上。所有对文件系统的写操作都会发生在这一层。
  3. 工作目录(Workdir):OverlayFS要求指定一个工作目录,用于存放一些必要的元数据和临时文件。工作目录必须和上层在同一个文件系统上。
  4. 合并层(Merged Layer):这是上层和底层合并后对用户可见的文件系统。当用户查看合并层时,他们可以看到底层的文件和目录,以及上层的任何更改或新增文件。

配置OverlayFS

1
2
# 创建目录
mkdir /mnt/upper /mnt/work /mnt/merge
  • upper:上层目录,可写层
  • work: 工作目录,存储临时文件
  • merge: 合并出的新目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 第一次运行建议加上--dry-run确保指令没有出错,同时可以查看图片和nfo总共的文件大小方便分配空间(仅用于测试命令是否正确,不会真正进行任何文件操作)
rclone copy yunpan:/movie /mnt/upper/movie --include "*.{png,jpg,nfo}" -P -v --dry-run

# 方式一(命令指定筛选规则)
rclone copy yunpan:/movie /mnt/upper/movie --include "*.{png,jpg,nfo}" -P -v --transfers=20

# 方式二(外部文件指定筛选规则)
cat > emby.txt <<EOF
+ *.jpg
+ *.png
+ *.nfo
- *
EOF

rclone copy yunpan:/movie /mnt/upper/movie --filter-from emby.txt -P -v --transfers=20
  • yunpan:/movie: 这是源路径,也就是你rclone配置的路径。
  • /mnt/upper/movie: 这是目的地路径,OverlayFS的上层目录。
  • -P: 这个参数用于显示进度信息,包括当前传输的百分比、速度和剩余时间。
  • -v: 这个参数用于增加命令的冗长输出(verbose),提供更多的详细信息,有助于调试或了解命令的执行情况。
  • --include "*.{png,jpg,nfo}": 这个参数指定一个匹配模式,只有符合该模式的文件才会被复制。
  • --filter-from emby.txt: 这个参数指定一个文件,按照文件的过滤规则复制。
  • --transfers=20: 这个参数用于指定同时进行的最大文件传输数目。
  • --dry-run: 模拟执行命令并展示详细信息,但不会真正地进行任何文件操作。

挂载OverlayFS命令

1
mount -t overlay overlay -o lowerdir=/mnt/yunpan,upperdir=/mnt/upper,workdir=/mnt/work /mnt/merge
  • lowerdir=/mnt/yunpan: 代表底层目录,只读层,进行读操作优先读取上层目录,读不到的才会读这里(比如视频文件)。
  • upperdir=/mnt/upper:代表上层目录,可写层,所有对文件的更改在这个目录中进行。
  • workdir=/mnt/work:代表工作目录,存储临时文件,用于 OverlayFS 的内部需要,必须和 upperdir 在同一个文件系统上。
  • /mnt/merge:合并出的新目录,也就是我们的挂载点,后续对该目录进行访问。

注意

如果使用该方案优化,请将Docker宿主机映射路径改为合并新目录的路径,也就是/mnt/merge,而不是使用rclone挂载路径

Strm文件

strm文件简单来说就是一个文本文件,里面存储的是媒体真实的访问路径(可以是本地绝对路径,也可以是日常见到的http/https的网络资源路径),strm文件在扫库过程中不需要解析文件,因此大大提升了扫库效率。因为网盘都会有风控规则导致无法大批量扫描和刮削视频文件(我仅仅收录豆瓣和IMDb Top 250电影及高分剧集,大概在五六百部视频下未收到风控影响),刮削软件可以把strm文件视作视频文件,根据文件名获取信息,而无需去网盘读取文件,因而不会触发网盘的风控。

相关项目,用法其实也不难,挂载好生成strm文件的目录即可

其他

本文是基于自己的阿里云盘资源搭建Emby影库,如果不想自己查找维护资源或者希望资源多而全的,可搭建小雅Alist,相关教程或资源已在下方

参考引用