├── FAQ.md ├── LICENSE ├── README.md ├── _headers ├── alistWebAddExternalUrl ├── README.md ├── alistWebLaunchExternalPlayer.js └── preview │ ├── preview01.png │ └── preview02.png ├── donate ├── Alipay.jpg ├── BTC(SegWit).jpg ├── USDT-Tron (TRC20).jpg └── Wechat.jpg ├── emby2Alist ├── CHANGELOG.md ├── README.md ├── docker │ ├── docker-compose.yml │ └── nginx-emby.syno.json └── nginx │ ├── conf.d │ ├── api │ │ ├── alist-api.js │ │ └── emby-api.js │ ├── common │ │ ├── events.js │ │ ├── live-util.js │ │ ├── periodics.js │ │ ├── url-util.js │ │ └── util.js │ ├── config │ │ ├── constant-common.js │ │ ├── constant-ext.js │ │ ├── constant-mount.js │ │ ├── constant-nginx.js │ │ ├── constant-pro.js │ │ ├── constant-strm.js │ │ ├── constant-symlink.js │ │ └── constant-transcode.js │ ├── constant.js │ ├── custome │ │ └── location │ │ │ └── default.conf │ ├── docs │ │ ├── UA.txt │ │ └── test-data.md │ ├── emby.conf │ ├── emby.js │ ├── exampleConfig │ │ ├── constant-all.js │ │ └── constant-main.js │ ├── includes │ │ ├── http.conf │ │ ├── https.conf │ │ ├── proxy-header.conf │ │ └── server-group.conf │ └── modules │ │ ├── emby-example.js │ │ ├── emby-items.js │ │ ├── emby-live.js │ │ ├── emby-playback-info.js │ │ ├── emby-search.js │ │ ├── emby-system.js │ │ ├── emby-transcode.js │ │ ├── emby-v-media.js │ │ └── ngx-ext.js │ └── nginx.conf ├── embyAddExternalUrl ├── docker-compose.yml └── nginx │ ├── conf.d │ ├── emby.conf │ └── externalUrl.js │ └── nginx.conf ├── embyWebAddExternalUrl ├── README.md ├── embyLaunchPotplayer.js ├── icons │ ├── ExternalPlayer.css │ ├── icon-Copy.webp │ ├── icon-DDPlay.webp │ ├── icon-FigPlayer.webp │ ├── icon-Fileball.webp │ ├── icon-IINA.webp │ ├── icon-MPV.webp │ ├── icon-MXPlayer.webp │ ├── icon-MXPlayerPro.webp │ ├── icon-NPlayer.webp │ ├── icon-OmniPlayer.webp │ ├── icon-PotPlayer.webp │ ├── icon-SenPlayer.webp │ ├── icon-StellarPlayer.webp │ ├── icon-VLC.webp │ ├── icon-infuse.webp │ └── min │ │ ├── icon-Copy.webp │ │ ├── icon-DDPlay.webp │ │ ├── icon-FigPlayer.webp │ │ ├── icon-Fileball.webp │ │ ├── icon-IINA.webp │ │ ├── icon-MPV.webp │ │ ├── icon-MXPlayer.webp │ │ ├── icon-MXPlayerPro.webp │ │ ├── icon-NPlayer.webp │ │ ├── icon-OmniPlayer.webp │ │ ├── icon-PotPlayer.webp │ │ ├── icon-SenPlayer.webp │ │ ├── icon-StellarPlayer.webp │ │ ├── icon-VLC.webp │ │ └── icon-infuse.webp ├── iconsExt.js └── preview │ ├── preview01.png │ └── preview02.png └── plex2Alist ├── CHANGELOG.md ├── README.md ├── docker ├── docker-compose.yml └── nginx-plex.syno.json └── nginx ├── conf.d ├── api │ └── alist-api.js ├── common │ ├── events.js │ ├── periodics.js │ ├── url-util.js │ └── util.js ├── config │ ├── constant-common.js │ ├── constant-ext.js │ ├── constant-mount.js │ ├── constant-pro.js │ ├── constant-strm.js │ ├── constant-symlink.js │ └── constant-transcode.js ├── constant.js ├── custome │ └── location │ │ └── default.conf ├── exampleConfig │ ├── constant-all.js │ └── constant-main.js ├── includes │ ├── http.conf │ ├── https.conf │ └── proxy-header.conf ├── modules │ ├── ngx-ext.js │ └── plex-example.js ├── plex.conf └── plex.js └── nginx.conf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jerry1119 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 主要功能 2 | | 名称 | 功能 | 3 | | - | :- | 4 | | [emby2Alist](./emby2Alist/README.md) | emby/jellyfin 重定向到 alist 直链 | 5 | | [embyAddExternalUrl](./alistWebAddExternalUrl/README.md) | emby/jellyfin 全客户端(除老TV端)添加调用外部播放器按钮 | 6 | | [embyWebAddExternalUrl](./embyWebAddExternalUrl/README.md) | emby/jellyfin/alistWeb 调用外部播放器用户脚本,只支持网页 | 7 | | [plex2Alist](./plex2Alist/README.md) | plex 重定向到 alist 直链 | 8 | 9 | ### 常见问题 10 | [FAQ](./FAQ.md) 11 | 12 | # embyExternalUrl 13 | 14 | ### emby调用外部播放器服务端脚本 15 | 16 | 通过nginx的njs模块运行js脚本,在emby视频的外部链接处添加调用外部播放器链接,所有emby官方客户端可用, 17 | 不支持老 TV 客户端等没有外部媒体数据库链接处的情况,另外需要注意电视端内置的 web view 实现方式的兼容性 18 | 19 | ![](https://raw.githubusercontent.com/bpking1/pics/main/img/Screenshot%202023-02-06%20191721.png) 20 | 21 | 22 | ### 部署方式,任选一种 23 | 24 | ## 一.单独使用方式 25 | 26 | 这里采用的是docker安装,也可以不使用docker,自己安装njs模块 27 | 28 | 先下载脚本: 29 | ```bash 30 | wget https://github.com/bpking1/embyExternalUrl/releases/download/v0.0.1/addExternalUrl.tar.gz && mkdir -p ~/embyExternalUrl && tar -xzvf ./addExternalUrl.tar.gz -C ~/embyExternalUrl && cd ~/embyExternalUrl 31 | ``` 32 | 33 | 然后看情况修改externalUrl.js文件里面的serverAddr 34 | 35 | tags 和 groups是从视频版本中提取的关键字作为外链的名字,不需要就不用改 36 | 37 | emby.conf默认反代emby-server是本机的8096端口,按需修改 38 | 39 | docker-compose.yml默认映射8097端口,按需修改 40 | 41 | 然后启动docker 42 | ``` 43 | docker-compose up -d 44 | ``` 45 | 访问8097端口,在视频信息页面的底部就添加了外部播放器链接 46 | 47 | 日志查看: 48 | ``` 49 | docker logs -f nginx-embyUrl 2>&1 | grep error 50 | ``` 51 | 52 | ## 二.与 emby2Alist 整合并共存 53 | 54 | 1. 将 externalUrl.js 放到 emby2Alist 的 conf.d 下与 emby.js 处于同一级 55 | 56 | 2. 将 emby.conf 中的 ## addExternalUrl SETTINGS ## 之间的内容复制到 emby2Alist 的 emby.conf 中 location / 块的上面 57 | 58 | 3. 将 emby.conf 最上面的 js_import 复制到 emby2Alist 的 emby.conf 相同位置 59 | 60 | 4. 重启 ngixn 或者输入命令 nginx -s reload 重载配置文件,注意此时使用 emby2Alist 的 nginx 对应端口访问 61 | 62 | ### emby调用外部播放器用户脚本,只支持网页: 63 | 64 | [篡改猴地址](https://greasyfork.org/zh-CN/scripts/514529) 65 | 66 | ## 捐赠 67 | 68 | 如果这个项目对你有帮助,欢迎点亮一颗⭐️,假如条件允许,可以请我喝杯咖啡,感谢你对开源精神的认可与支持! 69 | 70 | ### Payments 71 | Alipay: 72 | Wechat: 73 | 74 | BTC(SegWit): 75 | USDT-Tron (TRC20): 76 | 77 | Binance ID/币安 ID: 1041685683 78 | 79 | BTC 80 | 1. Network: BTC(SegWit) 81 | 2. Deposit Address: bc1qvr80l9juwkg94mpe55wafpwwnqtzjfs9tje8zn 82 | 83 | USDT 84 | 1. Network: Tron (TRC20) 85 | 2. Deposit Address: TSsmBGRhtN2AZHSG7WtvYN2UZ3dLdtMpUN 86 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | # 为全部 js 文件设置 2 | /*.js 3 | Content-Type: text/javascript; charset=utf-8 -------------------------------------------------------------------------------- /alistWebAddExternalUrl/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### alist 调用外部播放器用户脚本,支持网页和服务端: 3 | 4 | greasyfork 地址: https://greasyfork.org/zh-CN/scripts/494829 5 | 6 | 按需更改的地方: 7 | 8 | 1.代码内部变量 9 | 10 | ```js 11 | // 是否替换原始外部播放器 12 | const replaceOriginLinks = true; 13 | // 是否使用内置的 Base64 图标 14 | const useInnerIcons = true; 15 | // 移除最后几个冗余的自定义开关 16 | const removeCustomBtns = false; 17 | ``` 18 | 19 | 效果: 20 | 21 | AList V3 22 | ![](https://emby-external-url.7o7o.cc/alistWebAddExternalUrl/preview/preview01.png) 23 | 24 | AList V2 25 | ![](https://emby-external-url.7o7o.cc/alistWebAddExternalUrl/preview/preview02.png) 26 | 27 | 一. 浏览器单独使用方法 28 | 29 | 1. 安装 [Tampermonkey](https://www.tampermonkey.net) 拓展插件, 30 | 2. 进入脚本详情页点击安装 31 | 3. 打开已安装的脚本列表,点击启用按钮,再点击最后边的编辑按钮,选择设置选项卡, 32 | 编辑 包括/排除,去掉 原始匹配 勾选的泛化全域名,在 用户匹配 中添加响应的 alist 域名,不能包含端口号,会被忽略 33 | 34 | 二. 添加到服务端 alist 网站上 35 | 36 | 1. 登录 alist 管理后台 -> 设置 -> 全局 -> 自定义头部,填入脚本地址即可 37 | 38 | ```js 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | #### 其余注意事项请参照 49 | greasyfork 地址: https://greasyfork.org/en/scripts/459297-embylaunchpotplayer 50 | 51 | ### CHANGELOG 52 | 53 | #### 1.1.3 54 | 1. fix(alistWebLaunchExternalPlayer): 提供内部变量移除最后几个冗余的自定义开关 55 | 56 | #### 1.1.2 57 | 1. feat(alistWebLaunchExternalPlayer): 隐藏其他平台播放器开关数据隔离,添加多开Potplayer开关 58 | 59 | #### 1.1.1 60 | 1. 添加几个播放器支持 61 | 2. 默认开启隐藏其他平台播放器图标 62 | 63 | #### 1.1.0 64 | 1. 修复剪切板 API 兼容性 65 | 66 | #### 1.0.9 67 | 1. 修复 Google Chrome Version >= 130 导致的 PotPlayer 拉起播放错误,但注意不要禁用剪切板权限 68 | 2. 意外修复了 PotPlayer 串流的中文标题支持问题 69 | 70 | #### 1.0.8 71 | 1. 修复 mpv-handler 编码错误 72 | 2. 更换 @match 为严格匹配以兼容暴力猴 73 | 74 | #### 1.0.7 75 | 1. 再次修复 URL 编码错误 76 | 77 | #### 1.0.6 78 | 1. 优先使用本地 base64 图标提升加载速度 79 | 80 | #### 1.0.5 81 | 1. 修复 MX 错误的注释内容 82 | 83 | #### 1.0.4 84 | 1. 延迟加载点以适配服务端自定义头部 85 | 86 | #### 1.0.3 87 | 1. 兼容 AList V2 88 | 89 | #### 1.0.2 90 | 1. 降低 token 依赖适配第三方网站 91 | 92 | #### 1.0.1 93 | 1. 修复错误的 URL 双重编码 94 | -------------------------------------------------------------------------------- /alistWebAddExternalUrl/preview/preview01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/alistWebAddExternalUrl/preview/preview01.png -------------------------------------------------------------------------------- /alistWebAddExternalUrl/preview/preview02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/alistWebAddExternalUrl/preview/preview02.png -------------------------------------------------------------------------------- /donate/Alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/donate/Alipay.jpg -------------------------------------------------------------------------------- /donate/BTC(SegWit).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/donate/BTC(SegWit).jpg -------------------------------------------------------------------------------- /donate/USDT-Tron (TRC20).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/donate/USDT-Tron (TRC20).jpg -------------------------------------------------------------------------------- /donate/Wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/donate/Wechat.jpg -------------------------------------------------------------------------------- /emby2Alist/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: emby/jellyfin 重定向到 alist 直链 3 | date: 2021/09/06 22:00:00 4 | --- 5 | 6 | ## 这篇文章的受众: 7 | 写这篇文章默认读者是 emby 用户,使用 rclone 挂载网盘,会使用 docker,因篇幅问题以上软件的使用方法不在文章范围之中,此项目不会对原有的 emby 和rclone 配置造成影响或修改 8 | 9 | ## 原理: 10 | ~~使用[aliyundrive-webdav](https://github.com/messense/aliyundrive-webdav) 项目将阿里盘转为webdav, 再~~ 11 | 使用 rclone 挂载以供 emby 读取 12 | 使用[alist项目](https://github.com/Xhofe/alist) 将阿里盘及别的网盘的文件转为直链,使用 nginx 及其 njs 模块将 emby 视频播放地址劫持到 alist直链 13 | (~~暂时只测试了od,gd和阿里云盘可用,~~ 14 | alist 目前支持好几种网盘,感兴趣的可以测试一下) 15 | 16 | ## 部署方式,任选一种 17 | 18 | ### 一.集成版 docker 一键部署 19 | 20 | 1. 简化配置,拉取镜像映射配置文件即可一键启动。 21 | 22 | 2. 支持 SSL,内置 acme 自动申请证书、定时更新证书。 23 | 24 | 3. 支持重启自动更新,简化更新流程。 25 | 26 | [项目地址](https://github.com/thsrite/MediaLinker?tab=readme-ov-file) 27 | 28 | ### 二.手动在已有的 nginx 环境下部署,步骤基本类似 29 | 30 | #### 1.1 nginx proxy manager(WebUI) 31 | 32 | https://github.com/chen3861229/embyExternalUrl/issues/73#issuecomment-2452921067 33 | 34 | 35 | ### 三.手动 docker 部署 36 | 37 | #### 1.先将配置文件下载到本地 38 | 39 | 注意版本号和文件名 40 | ```bash 41 | wget https://github.com/bpking1/embyExternalUrl/releases/download/v0.0.1/emby2Alist.tar.gz && mkdir -p ~/emby2Alist && tar -xzvf ./emby2Alist.tar.gz -C ~/emby2Alist && cd ~/emby2Alist 42 | ``` 43 | 44 | 此时大致文件结构如下: 45 | ```javascript 46 | ~/emby2Alist 47 | ├── docker // 创建容器脚本文件夹 48 | | ├── docker-compose.yml // docker-compose 脚本,根据自身情况修改 49 | | ├── nginx-emby.syno.json // 群晖 docker 脚本,根据自身情况修改 50 | | └── nginx-jellyfin.syno.json // 群晖 docker 脚本,根据自身情况修改 51 | └── nginx // nginx 配置文件夹 52 | ├── conf.d // nginx 配置文件夹 53 | |  ├── api // JS 脚本文件夹,完全不用改 54 | | ├── cert // SSL 证书文件夹,根据自身情况修改 55 | | ├── common // 通用工具类文件夹,完全不用改 56 | | ├── config // 常量拆分后配置文件,若为 constant-all.js 完全不用改,若为 constant-main.js 则需要更改对应拆分文件 57 | | ├── exampleConfig // 示例 constant 配置文件夹 58 | | ├── includes // 拆分的 conf 文件夹,http 和 https 端口在这改 59 | | ├── constant.js // 常量主配置文件,根据自身情况修改 60 | │   ├── emby-live.js // 直播相关脚本,完全不用改 61 | │   ├── emby-transcode.js // 转码相关脚本,完全不用改 62 | │   ├── emby.conf // emby 配置文件,根据自身情况修改,注意 https 默认被注释掉了 63 | │   └── emby.js // 主脚本,完全不用改 64 | └── nginx.conf // nginx 配置文件,一般不用改 65 | ``` 66 | 67 | #### 2.修改示例配置 68 | 看情况修改 constant.js 中的设置项目,通常来说只需要改 alist 密码 69 | 这里默认 emby 在同一台机器并且使用 8096 端口,~~否则要修改 emby.js和emby.conf中emby的地址~~ 70 | 71 | #### 3.如果不挂载阿里云盘 可以跳过这一步 72 | 修改 docker-compose.yml 中 service.ali-webdav 的 REFRESH_TOKEN 73 | 获取方法参考原项目地址: https://github.com/messense/aliyundrive-webdav 74 | 75 | #### 4.docker 部署的任选以下一种 76 | xxx 为示例目录名,请根据自身情况修改 77 | 78 | ~~前置条件1: 需要手动创建目录~~ 79 | ``` 80 | /xxx/nginx-emby/log 81 | /xxx/nginx-emby/embyCache 82 | ``` 83 | ~~前置条件2: 需要手动移动项目配置文件~~ 84 | ~~将本项目xxx2Alist/nginx/下所有文件移动到/xxx/nginx-emby/config/下面~~ 85 | 86 | #### 4.1 docker-compose 87 | 启动服务: 在 ~/emby2Alist/docker 目录下执行 88 | ```bash 89 | docker-compose up -d 90 | ``` 91 | 查看启动log: 92 | ```bash 93 | docker-compose logs -f 94 | ``` 95 | 如果log有报错,请按照提示信息修改,常见错误可能为 96 | 1. docker端口占用冲突: 修改 docker-comopse 映射端口 97 | 2. webdav 的 refresh token 填写错误 (**如果不挂载阿里云盘则忽略**) 98 | 99 | #### 4.2 群晖 docker 100 | 容器 => 设置 => 导入 => 选择 json 配置文件 => 确认 101 | 102 | #### 5.防火墙配置 103 | 防火墙放行 5244, 8091 ~~和 8080端口~~ 104 | 8080 端口为阿里盘 webdav地址,8091 端口为 emby 转直链端口与默认的 8096 互不影响 105 | 访问 5244 端口,初始密码查看 docker log 能看到 ,根据项目文档 https://github.com/Xhofe/alist 在 Alist 项目后台添加网盘 106 | 注意: 107 | 108 | 1. 添加 od,gd 盘可以直接复制 rclone 配置里面的 clientid , secret , refreshToken,不用再麻烦去重新搞一次了 109 | 2. **不使用阿里云盘可以跳过这步** 110 | alist阿里盘的refreshToken与webdav那个token是不一样的,这里需要的是要不需要referrer请求头的token,详情请参考这个[issue](https://github.com/Xhofe/alist/issues/88) , 可以用这个网页来获取 [阿里云盘 - RefreshToken (cooluc.com)](https://media.cooluc.com/decode_token/) 111 | 3. 盘名建议一致,这样获取直链更快,不一致也可以 112 | 113 | ~~添加的网盘在alist里面的名称需要与 rclone挂载的文件夹名称一样 比如挂载路径为 /mnt/ali 那么盘的名称也要叫 ali~~ 114 | 115 | #### 6.如果不挂载阿里云盘 可以跳过这一步 116 | 配置 rclone,挂载网盘,这里以阿里盘 webdav 为例 117 | 118 | 使用 rclone 挂载 阿里盘 webdav 119 | 第一步name 我这里为 ali 120 | rclone config 选 webdav , 地址为http://localhost:8080 默认用户和密码都为admin 121 | rclone lsf ali: 看一下能否获取到列表 122 | 创建文件夹: 123 | mkdir -p /mnt/ali 注:此挂载文件夹的名字需要与 Alist 中的盘名相同 124 | 挂载: 125 | 126 | ```bash 127 | nohup rclone mount ali: /mnt/ali --umask 0000 --default-permissions --allow-non-empty --allow-other --buffer-size 32M --vfs-read-chunk-size 64M --vfs-read-chunk-size-limit 1G & 128 | ``` 129 | 也可以写成 service 130 | 131 | 132 | #### 7.测试是否成功 133 | 访问 8091 端口打开 emby 测试直链是否生效,查看执行 log 134 | ```bash 135 | docker logs -f -n 10 nginx-emby 2>&1 | grep js: 136 | ``` 137 | 或者直接查看 ../nginx/log 容器映射出来的原始 nginx error.log 业务日志 138 | 8091 端口为走直链端口,原本的 8096 端口 走 emby server 不变 139 | ~~直链播放不支持转码,转码的话只能走emby server~~ 140 | 所以最好 在 emby 设置中将 播放 --> 视频 --> 互联网质量 设置为最高, 141 | ~~并且将用户的转码权限关掉,确保走直链,~~ 142 | web 端各大浏览器对音频和视频编码支持情况不一,碰到不支持的情况 emby 会强制走转码而不会走直链 143 | 144 | ## 已知问题: 145 | 1. emby web 播放时如果需要使用内封的字幕,实际上是需要 embyServer 在后台用 ffmpeg 去提取的,~~ffmpeg要读取整个视频文件才能获取所有的字幕流,相当于几乎整个视频文件都要通过rclone下载,并且消耗cpu资源,对于比较大的视频文件是不现实的,所以web端建议使用外挂字幕~~,从头读取到字幕流位置截止,大概占文件大小的40%. 只有修改版 emby 客户端调用 MX Player 会同时传递所有外挂字幕,其余方式包括串流地址不支持外挂字幕加载,需要手动下载字幕文件并选择装载 146 | 2. ~~google Drive由于api的限制直链只能通过server中转,所以还是建议在cf上搭建goindex来获取直链 ,如何给到emby请参考 这篇[文章](https://blog.738888.xyz/2021/09/09/emby%E6%8C%82%E8%BD%BD%E7%BD%91%E7%9B%98%E8%BD%AC%E7%9B%B4%E9%93%BE%E6%92%AD%E6%94%BE/)结尾,另外一种方法是给alist添加cf worker中转gd的支持,有待研究~~ 147 | alist 新版已经支持 cf worker 代理 gd 下载了,详情参考 alist 文档 148 | 3. 可能会有其他问题,请留言 149 | -------------------------------------------------------------------------------- /emby2Alist/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | service.nginx-emby: 4 | image: nginx:1.27.1 5 | container_name: nginx-emby 6 | # 需要 Docker Swarm 模式 7 | # deploy: 8 | # resources: 9 | # limits: 10 | # cpus: '0.50' 11 | # memory: 128M 12 | # 更改为默认host网络,纠正流量标识,直接占用宿主机端口,docker层会禁用端口映射 13 | network_mode: host 14 | # 如果需要使用bridge(桥接)网络,请取消ports(端口映射)注释,并注释network_mode 15 | # 端口映射规则为,宿主机端口:容器内部端口 16 | # ports: 17 | # - 8091:8091 18 | # - 8095:8095 19 | # 如果遇到海报全部裂开或日志中 permission denied,以下二选一 20 | # 还不行的话,自己给宿主机 embyCache 读写权限 21 | # privileged: true 22 | # entrypoint: | 23 | # /bin/sh -c "chmod -R 777 /var/cache/nginx/emby" 24 | volumes: 25 | - ../nginx/nginx.conf:/etc/nginx/nginx.conf 26 | - ../nginx/conf.d:/etc/nginx/conf.d 27 | - ../nginx/embyCache:/var/cache/nginx/emby 28 | - ../nginx/log:/var/log/nginx 29 | restart: always 30 | # depends_on: 31 | # - service.ali 32 | 33 | # service.ali: 34 | # image: xhofe/alist:latest 35 | # container_name: alist 36 | # ports: 37 | # - 5244:5244 38 | # volumes: 39 | # - ../alist:/opt/alist/data 40 | # restart: always 41 | 42 | # service.ali-webdav: 43 | # image: messense/aliyundrive-webdav 44 | # container_name: ali-webdav 45 | # ports: 46 | # - 8080:8080 47 | # volumes: 48 | # - ./aliyundrive-webdav/:/etc/aliyundrive-webdav/ 49 | # environment: 50 | # - REFRESH_TOKEN=1111111111111aaaaaaaaaa 51 | # - WEBDAV_AUTH_USER=admin 52 | # - WEBDAV_AUTH_PASSWORD=admin 53 | # restart: always 54 | -------------------------------------------------------------------------------- /emby2Alist/docker/nginx-emby.syno.json: -------------------------------------------------------------------------------- 1 | { 2 | "CapAdd" : null, 3 | "CapDrop" : null, 4 | "cmd" : "nginx -g daemon\\ off\\;", 5 | "cpu_priority" : 50, 6 | "enable_publish_all_ports" : false, 7 | "enable_restart_policy" : true, 8 | "enabled" : false, 9 | "entrypoint_default" : "/docker-entrypoint.sh", 10 | "env_variables" : [ 11 | { 12 | "key" : "TZ", 13 | "value" : "Asia/Shanghai" 14 | } 15 | ], 16 | "exporting" : false, 17 | "image" : "nginx:1.27.1", 18 | "is_ddsm" : false, 19 | "is_package" : false, 20 | "links" : [], 21 | // 128MB 22 | "memory_limit" : 134217728, 23 | "name" : "nginx-emby", 24 | // 更改为默认host网络,纠正流量标识,直接占用宿主机端口,docker层会禁用端口映射 25 | "network_mode" : "host", 26 | // "network_mode" : "bridge", 27 | // 如果需要使用bridge(桥接)网络,请取消ports(端口映射)注释,并修改network_mode为bridge 28 | // 端口映射规则为,host_port(宿主机端口),container_port(容器内部端口) 29 | "port_bindings" : [ 30 | // { 31 | // "host_port" : 8091, 32 | // "container_port" : 8091, 33 | // "type" : "tcp" 34 | // }, 35 | // { 36 | // "host_port" : 8095, 37 | // "container_port" : 8095, 38 | // "type" : "tcp" 39 | // } 40 | ], 41 | "privileged" : false, 42 | "shortcut" : { 43 | "enable_shortcut" : false, 44 | "enable_status_page" : false, 45 | "enable_web_page" : false, 46 | "web_page_url" : "" 47 | }, 48 | // "use_host_network" : true, 49 | "volume_bindings" : [ 50 | { 51 | "host_volume_file" : "/docker/nginx-emby/log", 52 | "mount_point" : "/var/log/nginx", 53 | "type" : "rw" 54 | }, 55 | { 56 | "host_volume_file" : "/docker/nginx-emby/embyCache", 57 | "mount_point" : "/var/cache/nginx/emby", 58 | "type" : "rw" 59 | }, 60 | { 61 | "host_volume_file" : "/docker/nginx-emby/config/conf.d", 62 | "mount_point" : "/etc/nginx/conf.d", 63 | "type" : "rw" 64 | }, 65 | { 66 | "host_volume_file" : "/docker/nginx-emby/config/nginx.conf", 67 | "mount_point" : "/etc/nginx/nginx.conf", 68 | "type" : "rw" 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/api/alist-api.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-04-16 3 | 4 | // import util from "../common/util.js"; 5 | import urlUtil from "../common/url-util.js"; 6 | 7 | const API_ENUM = { 8 | fsGet: "/api/fs/get", 9 | fsLink: "/api/fs/link", 10 | fsList: "/api/fs/list", 11 | }; 12 | 13 | async function fetchAuth(url, username, password) { 14 | const body = { 15 | username: username, 16 | password: password, 17 | }; 18 | try { 19 | const response = await ngx.fetch(url, { 20 | method: "POST", 21 | max_response_body_size: 1024, 22 | body: JSON.stringify(body), 23 | }); 24 | if (response.ok) { 25 | const result = await response.json(); 26 | if (!result) { 27 | return `error: alist_auth_api response is null`; 28 | } 29 | if (result.message == "success") { 30 | return result.data.token; 31 | } 32 | return `error500: alist_auth_api ${result.code} ${result.message}`; 33 | } else { 34 | return `error: alist_auth_api ${response.status} ${response.statusText}`; 35 | } 36 | } catch (error) { 37 | return `error: alist_auth_api filed ${error}`; 38 | } 39 | } 40 | 41 | async function fetchPath(url, filePath, token, ua) { 42 | const requestBody = { 43 | path: filePath, 44 | password: "", 45 | }; 46 | try { 47 | const urlParts = urlUtil.parseUrl(url); 48 | const hostValue = `${urlParts.host}:${urlParts.port}`; 49 | ngx.log(ngx.WARN, `fetchAlistPath add Host: ${hostValue}`); 50 | const response = await ngx.fetch(url, { 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json;charset=utf-8", 54 | Authorization: token, 55 | "User-Agent": ua, 56 | Host: hostValue, 57 | }, 58 | max_response_body_size: 65535, 59 | body: JSON.stringify(requestBody), 60 | }); 61 | if (response.ok) { 62 | const result = await response.json(); 63 | if (!result) { 64 | return `error: alist_path_api response is null`; 65 | } 66 | if (result.message == "success") { 67 | return result; 68 | } 69 | if (result.code == 403) { 70 | return `error403: alist_path_api ${result.message}`; 71 | } 72 | return `error500: alist_path_api ${result.code} ${result.message}`; 73 | } else { 74 | return `error: alist_path_api ${response.status} ${response.statusText}`; 75 | } 76 | } catch (error) { 77 | return `error: alist_path_api fetchAlistFiled ${error}`; 78 | } 79 | } 80 | 81 | export default { 82 | API_ENUM, 83 | fetchAuth, 84 | fetchPath, 85 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/api/emby-api.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-03-31 3 | 4 | import config from "../constant.js"; 5 | import urlUtil from "../common/url-util.js"; 6 | 7 | const PLAY_METHOD_ENUM = { 8 | DirectPlay: "DirectPlay", 9 | DirectStream: "DirectStream", 10 | Transcode: "Transcode" 11 | }; 12 | 13 | async function fetchNotificationsAdmin(Name, Description) { 14 | const body = { 15 | Name: Name, 16 | Description: Description 17 | } 18 | try { 19 | ngx.fetch(`${config.embyHost}/Notifications/Admin?api_key=${config.embyApiKey}`, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json;charset=utf-8" 23 | }, 24 | body: JSON.stringify(body), 25 | }).then(res => { 26 | if (res.ok) { 27 | ngx.log(ngx.WARN, `success: fetchNotificationsAdmin: ${JSON.stringify(body)}`); 28 | } else { 29 | ngx.log(ngx.ERR, `error: fetchNotificationsAdmin: ${res.status} ${res.statusText}`); 30 | } 31 | }); 32 | } catch (error) { 33 | ngx.log(ngx.ERR, `error: fetchNotificationsAdmin: ${error}`); 34 | } 35 | } 36 | 37 | async function fetchSessionsMessage(Id, Header, Text, TimeoutMs) { 38 | const body = { 39 | Header: Header, 40 | Text: Text, 41 | TimeoutMs: TimeoutMs, 42 | } 43 | try { 44 | ngx.fetch(`${config.embyHost}/Sessions/${Id}/Message?api_key=${config.embyApiKey}`, { 45 | method: "POST", 46 | headers: { 47 | "Content-Type": "application/json;charset=utf-8" 48 | }, 49 | body: JSON.stringify(body), 50 | }).then(res => { 51 | if (res.ok) { 52 | ngx.log(ngx.WARN, `success: fetchSessionsMessage: ${JSON.stringify(body)}`); 53 | } else { 54 | ngx.log(ngx.ERR, `error: fetchSessionsMessage: ${res.status} ${res.statusText}`); 55 | } 56 | }); 57 | } catch (error) { 58 | ngx.log(ngx.ERR, `error: fetchSessionsMessage: ${error}`); 59 | } 60 | } 61 | 62 | async function fetchSessions(host, apiKey, queryParams) { 63 | if (!host || !apiKey || !queryParams) { 64 | ngx.log(ngx.ERR, `error: fetchSessions: params is required`); 65 | return; 66 | } 67 | let url = `${host}/Sessions?api_key=${apiKey}`; 68 | for (const key in queryParams) { 69 | url = urlUtil.appendUrlArg(url, key, queryParams[key]); 70 | } 71 | return ngx.fetch(url, { 72 | method: "GET", 73 | headers: { 74 | "Content-Type": "application/json;charset=utf-8", 75 | "Accept-Encoding": "", 76 | }, 77 | }); 78 | } 79 | 80 | async function fetchPlaybackInfo(itemId) { 81 | return ngx.fetch(`${config.embyHost}/Items/${itemId}/PlaybackInfo?api_key=${config.embyApiKey}`, { 82 | method: "POST", 83 | headers: { 84 | "Content-Type": "application/json;charset=utf-8" 85 | } 86 | }); 87 | } 88 | 89 | async function fetchItems(host, apiKey, queryParams) { 90 | if (!host || !apiKey || !queryParams) { 91 | ngx.log(ngx.ERR, `error: fetchItems: params is required`); 92 | return; 93 | } 94 | let url = `${host}/Items?api_key=${apiKey}`; 95 | for (const key in queryParams) { 96 | url = urlUtil.appendUrlArg(url, key, queryParams[key]); 97 | } 98 | ngx.log(ngx.WARN, `warn: fetchItems url: ${url}`); 99 | return ngx.fetch(url, { 100 | method: "GET", 101 | headers: { 102 | "Content-Type": "application/json;charset=utf-8", 103 | "Accept-Encoding": "", 104 | } 105 | }); 106 | } 107 | 108 | async function fetchVideosActiveEncodingsDelete(host, apiKey, queryParams) { 109 | if (!host || !apiKey || !queryParams) { 110 | ngx.log(ngx.ERR, `error: fetchVideosActiveEncodingsDelete: params is required`); 111 | return; 112 | } 113 | let url = `${host}/Videos/ActiveEncodings?api_key=${apiKey}`; 114 | for (const key in queryParams) { 115 | url = urlUtil.appendUrlArg(url, key, queryParams[key]); 116 | } 117 | ngx.log(ngx.WARN, `warn: fetchVideosActiveEncodingsDelete url: ${url}`); 118 | return ngx.fetch(url, { 119 | method: "DELETE", 120 | headers: { 121 | "Content-Type": "application/json;charset=utf-8", 122 | "Accept-Encoding": "", 123 | } 124 | }); 125 | } 126 | 127 | async function fetchBaseHtmlPlayer(host, queryParams) { 128 | let url = `${host}/web/modules/htmlvideoplayer/basehtmlplayer.js`; 129 | for (const key in queryParams) { 130 | url = urlUtil.appendUrlArg(url, key, queryParams[key]); 131 | if (key === "v") { 132 | ngx.log(ngx.WARN, `fetchBaseHtmlPlayer version: ${queryParams[key]}`); 133 | } 134 | } 135 | ngx.log(ngx.WARN, `fetchBaseHtmlPlayer url: ${url}`); 136 | return ngx.fetch(url); 137 | } 138 | 139 | export default { 140 | PLAY_METHOD_ENUM, 141 | fetchNotificationsAdmin, 142 | fetchSessionsMessage, 143 | fetchSessions, 144 | fetchPlaybackInfo, 145 | fetchItems, 146 | fetchVideosActiveEncodingsDelete, 147 | fetchBaseHtmlPlayer, 148 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/common/events.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-04-19 3 | // NJS events 4 | 5 | function njsOnExit(mark, callbacks, r) { 6 | njsOnBeginNotice(mark); 7 | const eventName = "exit"; 8 | if (callbacks && Array.isArray(callbacks)) { 9 | callbacks.map(callback => { 10 | njsOn(eventName, callback); 11 | }); 12 | } 13 | njsOn(eventName, () => { 14 | njsOnExitNotice(mark); 15 | }); 16 | } 17 | 18 | function njsOn(eventName, callback) { 19 | njs.on(eventName, callback); 20 | } 21 | 22 | function njsOnBeginNotice(mark) { 23 | ngx.log(ngx.WARN, `=== ${mark}, the NJS VM is beginning ===`); 24 | } 25 | 26 | function njsOnExitNotice(mark) { 27 | ngx.log(ngx.WARN, `=== ${mark}, the NJS VM is destroyed ===`); 28 | } 29 | 30 | export default { 31 | njsOnExit, 32 | njsOn, 33 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/common/live-util.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-08-16 3 | 4 | const SUBS_CODEC_ENUM = { 5 | srt: "srt", 6 | ass: "ass", 7 | ssa: "ssa", 8 | subrip: "subrip", 9 | webvtt: "webvtt", 10 | }; 11 | 12 | // extract audio, video and subtitles 13 | function parseM3U8(content) { 14 | const lines = content.split('\n').map(line => line.trim()).filter(line => line); 15 | const streams = []; 16 | const audios = []; 17 | const subtitles = []; 18 | 19 | for (let i = 0; i < lines.length; i++) { 20 | if (lines[i].startsWith('#EXT-X-STREAM-INF')) { 21 | const bandwidthMatch = lines[i].match(/BANDWIDTH=(\d+)/); 22 | const resolutionMatch = lines[i].match(/RESOLUTION=\d+x(\d+)/); 23 | const nameMatch = lines[i].match(/NAME="([^"]+)"/); 24 | const url = lines[i + 1]; 25 | 26 | if (bandwidthMatch && resolutionMatch && nameMatch && url) { 27 | // const bandwidth = bandwidthMatch[1]; 28 | const bandwidth = parseInt(bandwidthMatch[1]) || 0; 29 | const resolution = resolutionMatch[1]; 30 | const name = nameMatch[1]; 31 | streams.push({ 32 | bandwidth, 33 | resolution, 34 | url, 35 | quality: name 36 | }); 37 | } 38 | } else if (lines[i].startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) { 39 | const nameMatch = lines[i].match(/NAME="([^"]+)"/); 40 | const languageMatch = lines[i].match(/LANGUAGE="([^"]+)"/); 41 | const defaultMatch = lines[i].match(/DEFAULT=(YES|NO)/); 42 | const uriMatch = lines[i].match(/URI="([^"]+)"/); 43 | 44 | if (nameMatch && languageMatch && defaultMatch && uriMatch) { 45 | const name = nameMatch[1]; 46 | const language = languageMatch[1]; 47 | const isDefault = defaultMatch[1] === 'YES'; 48 | const url = uriMatch[1]; 49 | audios.push({ 50 | name, 51 | language, 52 | isDefault, 53 | url 54 | }); 55 | } 56 | } 57 | } 58 | 59 | return { streams, audios, subtitles }; 60 | } 61 | 62 | function subCodecConvert(data, sourceCodec, targetCodec) { 63 | if (!targetCodec) { targetCodec = SUBS_CODEC_ENUM.webvtt; } 64 | let rvt = ""; 65 | if (sourceCodec === SUBS_CODEC_ENUM.srt && targetCodec === SUBS_CODEC_ENUM.webvtt) { 66 | rvt = srt2webvtt(data); 67 | } 68 | return rvt; 69 | } 70 | 71 | // copy from https://github.com/silviapfeiffer/silviapfeiffer.github.io/blob/master/index.html 72 | // It's too complicated, it can be optimized 73 | function srt2webvtt(data) { 74 | // remove dos newlines, trim white space start and end 75 | const srt = data.replace(/\r+/g, '').replace(/^\s+|\s+$/g, ''); 76 | // get cues 77 | const cuelist = srt.split('\n\n'); 78 | let rvt = ""; 79 | if (cuelist.length > 0) { 80 | rvt += "WEBVTT\n\n"; 81 | for (let i = 0; i < cuelist.length; i = i + 1) { 82 | rvt += convertSrtCue(cuelist[i]); 83 | } 84 | } 85 | return rvt; 86 | } 87 | 88 | function convertSrtCue(caption) { 89 | let cue = ""; 90 | const s = caption.split(/\n/); 91 | // concatenate muilt-line string separated in array into one 92 | while (s.length > 3) { 93 | for (let i = 3; i < s.length; i++) { 94 | s[2] += "\n" + s[i] 95 | } 96 | s.splice(3, s.length - 3); 97 | } 98 | let line = 0; 99 | // detect identifier 100 | if (!s[0].match(/\d+:\d+:\d+/) && s[1].match(/\d+:\d+:\d+/)) { 101 | cue += s[0].match(/\w+/) + "\n"; 102 | line += 1; 103 | } 104 | // get time strings 105 | if (s[line].match(/\d+:\d+:\d+/)) { 106 | // convert time string 107 | const m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); 108 | if (m) { 109 | cue += m[1]+":"+m[2]+":"+m[3]+"."+m[4]+" --> " 110 | +m[5]+":"+m[6]+":"+m[7]+"."+m[8]+"\n"; 111 | line += 1; 112 | } else { 113 | // Unrecognized timestring 114 | return ""; 115 | } 116 | } else { 117 | // file format error or comment lines 118 | return ""; 119 | } 120 | // get cue text 121 | if (s[line]) { 122 | cue += s[line] + "\n\n"; 123 | } 124 | return cue; 125 | } 126 | 127 | export default { 128 | SUBS_CODEC_ENUM, 129 | parseM3U8, 130 | subCodecConvert, 131 | } -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/common/periodics.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-04-12 3 | // global schedule task 4 | 5 | // import config from "./constant.js"; 6 | // import util from "./util.js"; 7 | 8 | const fs = require("fs"); 9 | 10 | async function logHandler(s) { 11 | const errorLogPath = ngx.error_log_path; 12 | if (!fs.existsSync(errorLogPath)) { 13 | return; 14 | } 15 | const timeLocal = s.variables["time_local"]; 16 | // const dateLocal = s.variables["date_local"]; 17 | fs.writeFileSync(errorLogPath, ""); 18 | ngx.log(ngx.WARN, `cleared by periodics.logHandler`); 19 | 20 | const accessLogPath = errorLogPath.replace("error", "access"); 21 | if (fs.existsSync(accessLogPath)) { 22 | fs.writeFileSync(accessLogPath, `${timeLocal}: js: cleared by periodics.logHandler\n`); 23 | } 24 | } 25 | 26 | export default { 27 | logHandler, 28 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/common/url-util.js: -------------------------------------------------------------------------------- 1 | // @author: Ambitious 2 | // @date: 2023-09-04 3 | 4 | function proxyUri(uri) { 5 | return `/proxy${uri}`; 6 | } 7 | 8 | function appendUrlArg(u, k, v) { 9 | if (u.includes(k)) { 10 | return u; 11 | } 12 | return u + (u.includes("?") ? "&" : "?") + `${k}=${v}`; 13 | } 14 | 15 | function generateUrl(r, host, uri, skipKeys) { 16 | skipKeys = skipKeys ?? []; 17 | let url = host + uri; 18 | let isFirst = true; 19 | for (const key in r.args) { 20 | if (skipKeys.includes(key)) { 21 | continue; 22 | } 23 | url += isFirst ? "?" : "&"; 24 | url += `${key}=${r.args[key]}`; 25 | isFirst = false; 26 | } 27 | return url; 28 | } 29 | 30 | function addDefaultApiKey(r, u) { 31 | let url = u; 32 | if (!url.includes("api_key") && !url.includes("X-Emby-Token")) { 33 | url = appendUrlArg(url, "api_key", getDefaultApiKey(r.args)); 34 | } 35 | return url; 36 | } 37 | 38 | function getCurrentRequestUrl(r) { 39 | return addDefaultApiKey(r, generateUrl(r, getCurrentRequestUrlPrefix(r), r.uri)); 40 | } 41 | 42 | function getCurrentRequestUrlPrefix(r) { 43 | return `${r.variables.scheme}://${r.headersIn["Host"]}`; 44 | } 45 | 46 | function getDefaultApiKey(rArgs) { 47 | // emby old TV client only use config.embyApiKey 48 | const embyApiKey = config.embyApiKey; 49 | if (!rArgs) { return embyApiKey; } 50 | return rArgs["X-Emby-Token"] ?? (rArgs.api_key ?? embyApiKey); 51 | } 52 | 53 | function getDeviceId(r) { 54 | const rArgs = r.args; 55 | let deviceId = rArgs["X-Emby-Device-Id"]; 56 | // jellyfin and old emby tv clients use DeviceId/deviceId 57 | if (!deviceId) { 58 | deviceId = rArgs["DeviceId"] || rArgs["deviceId"]; 59 | } 60 | if (!deviceId) { 61 | deviceId = r.headersIn["DeviceId"]; 62 | } 63 | return deviceId; 64 | } 65 | 66 | function getMediaSourceId(rArgs) { 67 | return rArgs.MediaSourceId ? rArgs.MediaSourceId : rArgs.mediaSourceId; 68 | } 69 | 70 | // r only is PlaybackInfo 71 | function generateDirectStreamUrl(r, mediaSourceId, resourceKey) { 72 | if (!resourceKey) { resourceKey = "stream."; } 73 | let directStreamUrl = addDefaultApiKey( 74 | r, 75 | generateUrl(r, "", r.uri, ["StartTimeTicks"]) 76 | // official clients hava /emby web context path, like fileball not hava, both worked 77 | .replace(/^.*\/items/i, "/videos") 78 | .replace("PlaybackInfo", resourceKey) 79 | ); 80 | directStreamUrl = appendUrlArg( 81 | directStreamUrl, 82 | "MediaSourceId", 83 | mediaSourceId 84 | ); 85 | directStreamUrl = appendUrlArg( 86 | directStreamUrl, 87 | "Static", 88 | "true" 89 | ); 90 | return directStreamUrl; 91 | } 92 | 93 | /** 94 | * 1.CloudDrive with params 95 | * http://mydomain:19798/static/http/mydomain:19798/False//AList/xxx.mkv?aaa=bbb 96 | * 2.AList with params 97 | * http://mydomain:5244/d/AList/xxx.mkv?aaa=bbb 98 | * see: https://regex101.com/r/Gd3JUH/2 99 | * @param {String} url full url 100 | * @returns "/AList/xxx.mkv" or "AList/xxx.mkv" or "" 101 | */ 102 | function getFilePathPart(url) { 103 | const matches = url.match(/(?:\/False\/|\/d\/)(.*?)(?:\?|$)/); 104 | return matches ? matches[1] : ""; 105 | } 106 | 107 | /** 108 | * Parses the URL and returns an object with various components. 109 | * @param {string} url The URL string to parse. 110 | * @returns {Object} An object containing protocol, username, password, host, port, pathname, search, and hash. 111 | */ 112 | function parseUrl(url) { 113 | const regex = /^(?:(\w+)?:\/\/)?(?:(\w+):(\w+)@)?(?:www\.)?([^:\/\n?#]+)(?::(\d+))?(\/[^?\n]*)?(\?[^#\n]*)?(#.*)?$/i; 114 | const match = url.match(regex); 115 | if (match) { 116 | const protocol = match[1] || 'http'; 117 | const username = match[2] || ''; 118 | const password = match[3] || ''; 119 | const host = match[4]; 120 | const port = match[5] || ''; 121 | const pathname = match[6] || ''; 122 | const search = match[7] || ''; 123 | const hash = match[8] || ''; 124 | const fullProtocol = `${protocol}:`; 125 | const fullPort = port || (fullProtocol === 'https:' ? '443' : '80'); 126 | return { 127 | protocol: fullProtocol, 128 | username, 129 | password, 130 | host, 131 | port: fullPort, 132 | pathname, 133 | search, 134 | hash 135 | }; 136 | } 137 | return null; 138 | } 139 | 140 | function getRealIp(r) { 141 | const headers = r.headersIn; 142 | const ip = headers["X-Forwarded-For"] || 143 | headers["X-Real-IP"] || 144 | headers["Proxy-Client-IP"] || 145 | headers["Proxy-Client-IP"] || 146 | headers["WL-Proxy-Client-IP"] || 147 | headers["HTTP_CLIENT_IP"] || 148 | headers["HTTP_X_FORWARDED_FOR"] || 149 | r.variables.remote_addr; 150 | return ip; 151 | } 152 | 153 | export default { 154 | proxyUri, 155 | appendUrlArg, 156 | generateUrl, 157 | addDefaultApiKey, 158 | getCurrentRequestUrl, 159 | getCurrentRequestUrlPrefix, 160 | getDefaultApiKey, 161 | getDeviceId, 162 | getMediaSourceId, 163 | generateDirectStreamUrl, 164 | getFilePathPart, 165 | parseUrl, 166 | getRealIp, 167 | } -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-common.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,程序内部使用的公共常量 3 | 4 | // 字符串头,用于特殊匹配判断 5 | const strHead = { 6 | lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头 7 | xEmbyClients: { 8 | seekBug: ["Emby for iOS"], 9 | }, 10 | xUAs: { 11 | seekBug: ["Infuse", "VidHub", "SenPlayer"], 12 | clientsPC: ["EmbyTheater"], 13 | clients3rdParty: ["Fileball", "Infuse", "SenPlayer", "VidHub"], 14 | player3rdParty: ["dandanplay", "VLC", "MXPlayer", "PotPlayer"], 15 | blockDownload: ["Infuse-Download"], 16 | infuse: { 17 | direct: "Infuse-Direct", 18 | download: "Infuse-Download", 19 | }, 20 | // 安卓与 TV 客户端不太好区分,浏览器 UA 关键字也有交叉重叠,请使用 xEmbyClients 参数或使用正则 21 | }, 22 | "115": ["115.com", "115cdn.net"], 23 | ali: ["aliyundrive.net"], 24 | userIds: { 25 | mediaPathMappingGroup01: ["ac0d220d548f43bbb73cf9b44b2ddf0e"], 26 | allowInteractiveSearch: [], 27 | }, 28 | filePaths: { 29 | mediaMountPath: [], 30 | redirectStrmLastLinkRule: [], 31 | mediaPathMappingGroup01: [], 32 | }, 33 | }; 34 | 35 | // 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配) 36 | // 参数2: 匹配类型或来源(字符串参数类型),默认为 "filePath": 本地文件为路径,strm 为远程链接 37 | // ,有分组时不可省略填写,可为表达式 38 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 39 | // ,分组时建议写 "startsWith" 这样的字符串,方便日志中排错 40 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 41 | const ruleRef = { 42 | // 这个 key 值仅仅只是代码中引用的可读性标识,需见名知意,可自定义 43 | // mediaPathMappingGroup01: [ 44 | // ["mediaPathMappingGroup01", "filePath", "startsWith", strHead.filePaths.mediaPathMappingGroup01], // 目标地址 45 | // ["mediaPathMappingGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 46 | // ["mediaPathMappingGroup01", "r.args.UserId", "startsWith", strHead.userIds.mediaPathMappingGroup01], 47 | // ], 48 | // directHlsEnable: [ 49 | // // 此条规则代表大于等于 4Mbps 码率时生效,XMedia 为固定值,平方使用双星号表示 50 | // ["directHlsEnable", "r.XMedia.Bitrate", ">=", 4 * 1024 ** 2], 51 | // ["directHlsEnable", "r.args.UserId", "==", "ac0d220d548f43bbb73cf9b44b2ddf0e"], 52 | // ] 53 | }; 54 | 55 | export default { 56 | strHead, 57 | ruleRef, 58 | } 59 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-ext.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | 3 | const strHead = commonConfig.strHead; 4 | const ruleRef = commonConfig.ruleRef; 5 | 6 | // 选填项,特定平台功能,用不到保持默认即可 7 | 8 | // 图片缓存策略,包括主页、详情页、图片库的原图,路由器 nginx 请手动调小 conf 中 proxy_cache_path 的 max_size 9 | // 0: 不同尺寸设备共用一份缓存,先访问先缓存,空间占用最小但存在小屏先缓存大屏看的图片模糊问题 10 | // 1: 不同尺寸设备分开缓存,空间占用适中,命中率低下,但契合 emby 的图片缩放处理 11 | // 2: 不同尺寸设备共用一份缓存,空间占用最大,移除 emby 的缩放参数,直接原图高清显示 12 | // 3: 关闭 nginx 缓存功能,已缓存文件不做处理 13 | const imageCachePolicy = 0; 14 | 15 | // 对接 emby 通知管理员设置,目前只发送是否直链成功和屏蔽详情,依赖 emby/jellyfin 的 webhook 配置并勾选外部通知 16 | const embyNotificationsAdmin = { 17 | enable: false, 18 | includeUrl: false, // 链接太长,默认关闭 19 | name: "【emby2Alist】", 20 | }; 21 | 22 | // 对接 emby 设备控制推送通知消息,目前只发送是否直链成功,此处为统一开关,范围为所有的客户端,通知目标只为当前播放的设备 23 | const embyRedirectSendMessage = { 24 | enable: false, 25 | header: "【emby2Alist】", 26 | timeoutMs: -1, // 消息通知弹窗持续毫秒值 27 | }; 28 | 29 | // 按路径匹配规则隐藏部分接口返回的 items 30 | // 参数1: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 31 | // 参数2: 匹配目标,对象为 Item.Path 32 | // 参数3: 0: 默认同时过滤下列所有类型接口, 1: 只隐藏[搜索建议(不会过滤搜索接口)]接口, 33 | // 2: 只隐藏[更多类似(若当前浏览项目位于规则中,将跳过隐藏)]接口, 3: 只隐藏第三方使用的[海报推荐]接口 34 | // 4: 只隐藏[首页最新项目]接口, 35 | const itemHiddenRule = [ 36 | // [0, "/mnt/sda1"], 37 | // [1, ".mp3", 1], 38 | // [2, "Google", 2], 39 | // [3, /private/ig], 40 | ]; 41 | 42 | // 串流配置 43 | const streamConfig = { 44 | // 默认不启用,因违反 HTTP 规范,链接中携带未编译中文,可能存在兼容性问题,如发现串流访问失败,请关闭此选项, 45 | // !!! 谨慎开启,启用后将修改直接串流链接为真实文件名,方便第三方播放器友好显示和匹配, 46 | // 该选项只对 emby 有用, jellyfin 为前端自行拼接的 47 | useRealFileName: false, 48 | }; 49 | 50 | // 搜索接口增强配置 51 | const searchConfig = { 52 | // 开启脚本的部分交互性功能 53 | interactiveEnable: false, 54 | // 快速交互,启用后将根据指令头匹配,直接返回虚拟搜索结果,不经过回源查询,优化搜索栏失焦的自动搜索 55 | interactiveFast: false, 56 | // 限定交互性功能的隔离,取值来源为带参数的 request_uri 字符串 57 | // 不带协议与域名,仅作包含匹配,多个值为或的关系,未定义或空数组为不隔离 58 | //interactiveEnableRule: [ 59 | // "ac0d220d548f43bbb73cf9b44b2ddf0e", // request_uri path level userId 60 | // "2d427412-43e1-49e4-a1db-fa17c04d49db", // X-Emby-Device-Id 61 | //], 62 | }; 63 | 64 | // 115网盘 web cookie, 会覆盖从 alist 获取到的 cookie 65 | const webCookie115 = ""; 66 | // 网盘转码直链配置,当前仅支持 115(必填 webCookie115) 和 emby 挂载媒体环境 67 | const directHlsConfig = { 68 | enable: false, 69 | // 仅在首次占位未获取清晰度时,默认播放最小,开启后默认播放最大,版本缓存有效期内客户端自行选择 70 | defaultPlayMax: false, 71 | // 启用规则,仅在 enable = true 时生效 72 | enableRule: ruleRef.directHlsEnable ?? [], 73 | }; 74 | 75 | // PlaybackInfo 接口的一些增强配置 76 | const playbackInfoConfig = { 77 | enabled: true, 78 | // 根据规则组指定播放源排序规则(与 redirectStrmLastLinkRule 配置类似,但必须设置分组名) 79 | // sourcesSortRules 为旧版兼容排序规则,同时做为未匹配的默认排序规则,不要使用这个组名 80 | // 匹配规则越靠前优先级越高 81 | // 参数1: 分组名,组内为与关系(全部匹配),排序规则 key 需要与分组名相同 82 | // 参数2: 匹配类型或来源(字符串参数类型),不支持 filePath 和 alistRes 变量 83 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 84 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 85 | sourcesSortFitRule: [ 86 | // ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 客户端为内网 87 | // ["useGroup01", "r.args.X-Emby-Client", "startsWith", strHead.xEmbyClients.seekBug], // Emby 客户端类型 88 | // ["useGroup02", "r.variables.remote_addr", "startsWith:not", strHead.lanIp[0]], // 公网 89 | // ["useGroup02", "r.variables.remote_addr", "startsWith:not", strHead.lanIp[3]], // 公网 90 | // ["useGroup03", "r.args.X-Emby-Client", 2, "Emby Web"], // Emby 客户端类型为浏览器 91 | // ["useGroup03", "r.headersIn.user-agent", 2, "Chrome"], // 通用客户端 UA 标识 92 | ], 93 | // 多版本播放源排序规则,对接口数据 MediaSources 数组进行排序,优先级从上至下,数组内从左至右,支持正则表达式 94 | // key 使用"."进行层级,分割后的键按层级从 MediaSources 获取,根据分割键获取下一层值时若对象为数组,则过滤[Type === 分割键]的第一行数据 95 | // (如: "MediaStreams.Video.Height"规则中 MediaSources.MediaStreams 值为数组,则取数组中[Type === "Video"]的对象的 Height 值) 96 | // ":length"为关键字,用于数组长度排序 97 | // value 只有三种类型, "asc": 正序, "desc": 倒序, 字符串/正则混合数组: 指定按关键字顺序排序 98 | // 非正则情况下, value 不区分大小写(简化书写), 只有正则区分大小写 99 | sourcesSortRules: { 100 | // "Path": ["1080p", "720p", "480p", "hevc", "h265", "h264"], // 按原文件名路径关键字排序 101 | // "Path": [/^\d{4}p$/g, /^\d{3}p$/g, "hevc", "h265", "h264"], // 正则匹配 4 位数字排在 3 位数字前面并忽略大小写 102 | // "MediaStreams.Video.Height": "desc", // 按视频高度倒序 103 | // "MediaStreams.Video.Codec": ["AV1", "HEVC", "H264"], // 按视频编码排序 104 | // "MediaStreams.Subtitle:length": "desc", // 更多字幕的排在前面 105 | // "MediaStreams.Video.BitRate": "asc", // 码率正序 106 | }, 107 | // useGroup01: { 108 | // "MediaStreams.Video.Width": "desc", 109 | // "MediaStreams.Video.ExtendedVideoSubType": ["DoviProfile5", "DoviProfile8", "Hdr10", "DoviProfile7", "None"], // 视频编码子类型 110 | // "MediaStreams.Video.BitRate": "desc", // 码率倒序 111 | // "MediaStreams.Video.RealFrameRate": "desc", // 帧率倒序 112 | // }, 113 | // useGroup02: { 114 | // "MediaStreams.Video.BitRate": "asc", 115 | // "MediaStreams.Video.RealFrameRate": "asc", 116 | // }, 117 | // useGroup03: { 118 | // "MediaStreams.Video.VideoRange": ["SDR"], // 视频动态范围 119 | // }, 120 | } 121 | 122 | export default { 123 | imageCachePolicy, 124 | embyNotificationsAdmin, 125 | embyRedirectSendMessage, 126 | itemHiddenRule, 127 | streamConfig, 128 | searchConfig, 129 | webCookie115, 130 | directHlsConfig, 131 | playbackInfoConfig, 132 | } 133 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-mount.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | 3 | const strHead = commonConfig.strHead; 4 | 5 | // 选填项,用不到保持默认即可 6 | 7 | // rclone/CD2 挂载的 alist 文件配置,根据实际情况修改下面的设置 8 | // 访问宿主机上 5244 端口的 alist 地址, 要注意 iptables 给容器放行端口 9 | const alistAddr = "http://172.17.0.1:5244"; 10 | 11 | // alist token, 在 alist 后台查看 12 | const alistToken = "alsit-123456"; 13 | 14 | // alist 是否启用了 sign 15 | const alistSignEnable = false; 16 | 17 | // alist 中设置的直链过期时间,以小时为单位,严格对照 alist 设置 => 全局 => 直链有效期 18 | const alistSignExpireTime = 12; 19 | 20 | // alist 公网地址,用于需要 alist server 代理流量的情况,按需填写 21 | const alistPublicAddr = "http://youralist.com:5244"; 22 | 23 | // 指定客户端自己请求并获取 alist 直链的规则,代码优先级在 redirectStrmLastLinkRule 之后 24 | // 特殊情况使用,则此处必须使用域名且公网畅通,用不着请保持默认 25 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 26 | // 参数?.2: 匹配类型或来源(字符串参数类型),优先级高"filePath": 文件路径(Item.Path),默认为"alistRes": alist 返回的链接 raw_url 27 | // ,有分组时不可省略填写,可为表达式,然后下面参数序号-1 28 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 29 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 30 | // 参数5: 指定转发给客户端的 alist 的 host 前缀,兼容 sign 参数 31 | const clientSelfAlistRule = [ 32 | // Infuse 客户端对于 115 的进度条拖动可能依赖于此 33 | // 如果 nginx 为 https,则此 alist 也必须 https,浏览器行为客户端会阻止非 https 请求 34 | [2, strHead["115"], alistPublicAddr], 35 | // [2, strHead.ali, alistPublicAddr], 36 | // 优先使用 filePath,可省去一次查询 alist,如驱动为 alias,则应使用 alistRes 37 | // ["115-local", "filePath", 0, "/mnt/115", alistPublicAddr], 38 | // ["115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 39 | // ["115-alist", "alistRes", 2, strHead["115"], alistPublicAddr], 40 | // ["115-alist", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], 41 | ]; 42 | 43 | // 响应重定向链接前是否检测有效性,无效链接时转给媒体服务器回源中转处理 44 | const redirectCheckEnable = false; 45 | 46 | // 媒体服务/alist 查询失败后是否使用原始链接回源中转流量处理,如无效则直接返回 500 47 | const fallbackUseOriginal = true; 48 | 49 | export default { 50 | alistAddr, 51 | alistToken, 52 | alistSignEnable, 53 | alistSignExpireTime, 54 | alistPublicAddr, 55 | clientSelfAlistRule, 56 | redirectCheckEnable, 57 | fallbackUseOriginal, 58 | } 59 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-nginx.js: -------------------------------------------------------------------------------- 1 | // import embySearch from "../modules/emby-search.js"; // this is cycle import, unhandled promise rejection: ReferenceError: cannot access variable before initialization 2 | 3 | // 选填项,用不到保持默认即可 4 | 5 | const nginxConfig = { 6 | // 禁用上游服务的 docs 页面 7 | disableDocs: true, 8 | }; 9 | 10 | // for js_set 11 | function getDisableDocs(r) { 12 | const value = nginxConfig.disableDocs 13 | && !ngx.shared["tmpDict"].get("opendocs"); 14 | // r.log(`getDisableDocs: ${value}`); 15 | return value; 16 | } 17 | 18 | export default { 19 | nginxConfig, 20 | getDisableDocs, 21 | } 22 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-pro.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | import mountConfig from "./constant-mount.js"; 3 | 4 | const strHead = commonConfig.strHead; 5 | const ruleRef = commonConfig.ruleRef; 6 | const alistPublicAddr = mountConfig.alistPublicAddr; 7 | 8 | // 选填项,高级配置,用不到保持默认即可 9 | 10 | // 重定向/直链开关配置,关闭的效果为还原为严格反代逻辑,即中转上游源服务流量 11 | // 此为粗颗粒度控制,优先级最高,细颗粒控制依旧使用路由规则管理 12 | const redirectConfig = { 13 | enable: true, // 允许直链的总开关,false 等同覆盖下列所有为 false 14 | // 允许电视直播直链,关闭后特例将忽略 transcodeConfig.enable 值,因直播 m3u 特殊表现为转码播放,实际并未占用服务端硬件转码 15 | enableVideoLivePlay: true, 16 | enableVideoStreamPlay: true, // 允许视频串流播放直链 17 | enableAudioStreamPlay: true, // 允许音频串流播放直链 18 | enableItemsDownload: true, // 允许网页下载项目直链 19 | enableSyncDownload: true, // 允许官方客户端下载项目直链 20 | }; 21 | 22 | // 路由缓存配置 23 | const routeCacheConfig = { 24 | // 总开关,是否开启路由缓存,此为一级缓存,添加阶段为 redirect 和 proxy 之前 25 | // 短时间内同客户端访问相同资源不会再做判断和请求 alist,有限的防抖措施,出现问题可以关闭此选项 26 | enable: true, 27 | // 二级缓存开关,仅针对直链,添加阶段为进入单集详情页,clientSelfAlistRule 中的和首页直接播放的不生效 28 | // 非 web 端且限 UA 的不建议使用,效率太低,因部分客户端详情页 UA 和播放器 UA 存在不同的情况 29 | enableL2: false, 30 | // 缓存键表达式,默认值好处是命中范围大,但会导致 routeRule 中针对设备的规则失效,多个变量可自行组合修改,冒号分隔 31 | // 注意 jellyfin 是小写开头 mediaSourceId 32 | keyExpression: "r.uri:r.args.MediaSourceId", // "r.uri:r.args.MediaSourceId:r.args.X-Emby-Device-Id" 33 | }; 34 | 35 | // 路由规则,注意顺序是从上至下匹配,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内 36 | // 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1 37 | // "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链 302, 38 | // "transcode": 转码,稍微有些歧义,大部分情况等同于"proxy",这里只是不做转码参数修改,具体是否转码由 emby 客户端自己判断上报或客户端手动切换码率控制, 39 | // "block": 屏蔽媒体播放和下载, "blockDownload": 只屏蔽下载, "blockPlay": 只屏蔽播放, 40 | // 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 41 | // 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接 42 | // 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 43 | // 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 44 | const routeRule = [ 45 | // ["filePath", 0, "/mnt/sda1"], 46 | // ["filePath", 1, ".mp3"], 47 | // ["filePath", 2, "Google"], 48 | // ["alistRes", 2, "/NAS/"], // 例如使用 alias 聚合了 nas 本地文件,可能会存在卡顿或花屏 49 | // ["filePath", 3, /private/ig], 50 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 51 | // ["r.variables.remote_addr", 0, strHead.lanIp], 52 | // ["r.headersIn.User-Agent", 2, "IE"], // 请求头参数,客户端UA 53 | // ["r.args.X-Emby-Device-Id", 0, "d4f30461-ec5c-488d-b04a-783e6f419eb1"], // 链接入参,设备id 54 | // ["r.args.X-Emby-Device-Name", 0, "Microsoft Edge Windows"], // 链接入参,设备名称 55 | // ["r.args.UserId", 0, "ac0d220d548f43bbb73cf9b44b2ddf0e"], // 链接入参,用户id 56 | // 注意非"proxy"无法使用"alistRes"条件,因为没有获取 alist 直链的过程 57 | // ["proxy", "filePath", 0, "/mnt/sda1"], 58 | // ["proxy", "直播走中转01", "r.XMedia.IsInfiniteStream", "===", true], 59 | // ["redirect", "filePath", 0, "/mnt/sda2"], 60 | // ["transcode", "filePath", 0, "/mnt/sda3"], 61 | // ["transcode", "115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.XXX], 62 | // ["transcode", "115-local", "filePath", 0, "/mnt/115"], 63 | // ["block", "filePath", 0, "/mnt/sda4"], 64 | 65 | // 高级分组规则,XMedia 为固定值,等于 Emby.MediaSources 数组中的单个目标对象 66 | // 注意设备id具有唯一性,不会跟随切换用户变更,取值参考 /PlaybackInfo 接口的出入参数 67 | // r.XMedia.MediaStreams 数组比较特殊,路由规则暂未做关键词抽取,但目前匹配视频流规则够用,多音频/字幕流不太好写规则 68 | // r.XMedia.MediaStreams.0 一般为视频流对象,不支持 r.XMedia.MediaStreams[0] 写法 69 | 70 | // 此条规则代表大于等于 3Mbps 码率的允许转码,平方使用双星号表示,无意义加减仅为示例,注意 emby/jellyfin 码率为 bps 单位 71 | // ["transcode", "高码率允许转码01", "r.XMedia.Bitrate", ">=", 3 * 1000 ** 2 + (1 * 1000 ** 2) - (1 * 1000 ** 2)], 72 | // 可选规则,结合上条规则做分组,同时满足才能生效,否则继续向下匹配 73 | // ["transcode", "高码率允许转码01", "r.args.X-Emby-Device-Id", "===", ["设备id01", "设备id02"]], 74 | // 此条规则代表 4K 分辨率的允许转码,但假如设备自身上报和上游决定走转码,不满足的也会转码,遵守上游倾向为播放成功率考虑 75 | // ["transcode", "高分辨率允许转码01", "r.XMedia.MediaStreams.0.DisplayTitle", "includes", "4K"], 76 | // 可选替换上条规则,更精确的分辨率规则,例如 21:9 视频,或某些 2.5 K 视频等不在标准分辨率划分内的 77 | // ["transcode", "高分辨率允许转码01", "r.XMedia.MediaStreams.0.Width", ">=", 4320], 78 | // 精确屏蔽指定功能,注意同样是整体规则都不匹配默认走"redirect",即不屏蔽,建议只用下方一条,太复杂的话需要自行测试 79 | // ["blockDownload", "屏蔽下载01", "r.headersIn.User-Agent", "includes", strHead.xUAs.blockDownload], 80 | // 非必须,该分组内细分为用户 id 白名单,结合上面一条代表 "屏蔽指定标识客户端的非指定用户的下载" 81 | // ["blockDownload", "屏蔽下载01", "r.args.UserId", "startsWith:not", ["用户id01", "用户id02"]], 82 | // 非必须,该分组内细分为入库路径黑名单,结合上面两条代表 "屏蔽指定标识客户端的非指定用户的指定入库路径的下载" 83 | // ["blockDownload", "屏蔽下载01", "filePath", "startsWith", ["/mnt/115"]], 84 | ]; 85 | 86 | // 路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍,不要有重叠,注意 /mnt 会先被移除掉了 87 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 88 | // 参数1: 0: 默认做字符串替换 replace 一次, 1: 前插, 2: 尾插, 3: replaceAll 替换全部 89 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 90 | // 参数3: 来源, 参数4: 目标 91 | const mediaPathMapping = [ 92 | // [0, 0, "/aliyun-01", "/aliyun-02"], 93 | // [0, 2, "http:", "https:"], 94 | // [0, 2, ":5244", "/alist"], 95 | // [0, 0, "D:", "F:"], 96 | // [0, 0, /blue/g, "red"], // 此处正则不要加引号 97 | // [1, 1, `${alistPublicAddr}/d`], 98 | // [2, 2, "?xxx"], 99 | // 此条是一个规则变量引用,方便将规则汇合到同一处进行管理 100 | // [ruleRef.mediaPathMappingGroup01, 0, 0, "/aliyun-01", "/aliyun-02"], 101 | // 路径映射多条规则会从上至下依次执行,如下有同一个业务关系集的,注意带上区间的闭合条件,不然会被后续重复替换会覆盖 102 | // 以下是按码率条件进行路径映射,全用户设备强制,区分用户和设备可再精确添加条件 103 | // [[["4K 目录映射到 1080P 目录", "r.XMedia.Bitrate", ">", 10 * 1000 ** 2], 104 | // ], 0, 0, "/4K/", "/1080P/"], 105 | // [[["1080P 目录映射到 720P 目录", "r.XMedia.Bitrate", ">", 6 * 1000 ** 2], 106 | // ["1080P 目录映射到 720P 目录", "r.XMedia.Bitrate", "<=", 10 * 1000 ** 2], 107 | // ], 0, 0, "/1080P/", "/720P/"], 108 | // [[["720P 目录映射到 480P 目录", "r.XMedia.Bitrate", ">", 3 * 1000 ** 2], 109 | // ["720P 目录映射到 480P 目录", "r.XMedia.Bitrate", "<=", 6 * 1000 ** 2], 110 | // ], 0, 0, "/720P/", "/480P/"], 111 | ]; 112 | 113 | // 仅针对 alist 返回的 raw_url 进行路径映射,优先级在 mediaPathMapping 和 clientSelfAlistRule 后,使用方法一样 114 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 115 | // 参数1: 0: 默认做字符串替换replace一次, 1: 前插, 2: 尾插, 3: replaceAll替换全部 116 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 117 | // 参数3: 来源, 参数4: 目标 118 | const alistRawUrlMapping = [ 119 | // [0, 0, "/alias/movies", "/aliyun-01"], 120 | ]; 121 | 122 | export default { 123 | redirectConfig, 124 | routeCacheConfig, 125 | routeRule, 126 | mediaPathMapping, 127 | alistRawUrlMapping, 128 | } 129 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-strm.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | import mountConfig from "./constant-mount.js"; 3 | 4 | const strHead = commonConfig.strHead; 5 | const alistAddr = mountConfig.alistAddr; 6 | const alistToken = mountConfig.alistToken; 7 | const alistSignExpireTime = mountConfig.alistSignExpireTime; 8 | 9 | // 只使用 strm 文件配置模板,即标准 strm 内部只有远程链接,不存在/开头的相对路径 10 | // 不需要挂载功能,不显示依赖 alist,strm 内部为任意直链 11 | 12 | // 选填项,用不到保持默认即可 13 | 14 | // 指定是否转发由 njs 获取 strm/远程链接 重定向后直链地址的规则,例如 strm/远程链接 内部为局域网 ip 或链接需要验证 15 | // 匹配来源为入库媒体的文件路径 16 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 17 | // 参数?.2: 匹配类型或来源(字符串参数类型),默认为 "filePath": mediaPathMapping 映射后的 strm/远程链接 内部链接 18 | // ,有分组时不可省略填写,可为表达式 19 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 20 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 21 | const redirectStrmLastLinkRule = [ 22 | [0, strHead.lanIp.map(s => "http://" + s)], 23 | // [0, alistAddr], 24 | // [0, "http:"], 25 | // 参数5: 请求验证类型,当前 alistAddr 不需要此参数 26 | // 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime 27 | // [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`], 28 | // useGroup01 同时满足才命中 29 | // ["useGroup01", "filePath", "startsWith", strHead.lanIp.map(s => "http://" + s)], // 目标地址 30 | // ["useGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 31 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 32 | // ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网 33 | ]; 34 | 35 | export default { 36 | redirectStrmLastLinkRule, 37 | } 38 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-symlink.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,用不到保持默认即可 3 | 4 | // 指定需要获取符号链接真实路径的规则,优先级在 mediaMountPath 和 routeRule 之间 5 | // 注意前提条件是此程序或容器必须挂载或具有对应目录的读取权限,否则将跳过处理,回源中转 6 | // 此参数仅在软链接后的文件名和原始文件名不一致或路径差异较大时使用,其余情况建议用 mediaPathMapping 7 | // 参数1: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 8 | // 参数2: 匹配目标,对象为媒体服务入库的文件路径(Item.Path) 9 | const symlinkRule = [ 10 | // [0, "/mnt/sda1"], 11 | ]; 12 | 13 | export default { 14 | symlinkRule, 15 | } 16 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/config/constant-transcode.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,用不到保持默认即可 3 | 4 | // 转码配置,默认 false,将按之前逻辑禁止转码处理并移除转码选项参数,与服务端允许转码配置有相关性 5 | const transcodeConfig = { 6 | enable: false, // 此大多数情况下为允许转码的总开关 7 | enableStrmTranscode: false, // 默认禁用 strm 的转码,体验很差,仅供调试使用 8 | type: "distributed-media-server", // 负载类型,可选值, ["nginx", "distributed-media-server"] 9 | maxNum: 3, // 单机最大转码数量,有助于加速轮询, 参数暂无作用,接口无法查询转码情况,忽略此参数 10 | redirectTransOptEnable: true, // 是否保留码率选择,不保留官方客户端将无法手动切换至转码 11 | targetItemMatchFallback: "redirect", // 目标服务媒体匹配失败后的降级后路由措施,可选值, ["redirect", "proxy"] 12 | // 如果只需要当前服务转码,enable 改为 true,server 改为下边的空数组 13 | server: [], 14 | // !!!实验功能,主库和所有从库给用户开启[播放-如有必要,在媒体播放期间允许视频转码]+[倒数7行-允许媒体转换] 15 | // type: "nginx", nginx 负载均衡,好处是使用简单且内置均衡参数选择,缺点是流量全部经过此服务器, 16 | // 且使用条件很苛刻,转码服务组中的媒体 id 需要和主媒体库中 id 一致,自行寻找实现主从同步,完全同步后,ApiKey 也是一致的 17 | // type: "distributed-media-server", 分布式媒体服务负载均衡(暂未实现均衡),优先利用 302 真正实现流量的 LB,且灵活, 18 | // 不区分主从,当前访问服务即为主库,可 emby/jellyfin 混搭,挂载路径可以不一致,但要求库中的标题和语种一致且原始文件名一致 19 | // 负载的服务组,需要分离转码时才使用,注意下列 host 必须全部为公网地址,会 302 给客户端访问,若参与负载下边手动添加 20 | // server: [ 21 | // { 22 | // type: "emby", 23 | // host: "http://yourdomain.com:8096", 24 | // apiKey: "f839390f50a648fd92108bc11ca6730a", 25 | // }, 26 | // { 27 | // type: "jellyfin", 28 | // host: "http://yourdomain.com:8097", 29 | // apiKey: "f839390f50a648fd92108bc11ca6730a", 30 | // }, 31 | // ] 32 | }; 33 | 34 | export default { 35 | transcodeConfig, 36 | } 37 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/constant.js: -------------------------------------------------------------------------------- 1 | // 如果使用拆分配置,请注意填写 config 下使用到的配置文件 2 | 3 | import commonConfig from "./config/constant-common.js"; 4 | import mountConfig from "./config/constant-mount.js"; 5 | import proConfig from "./config/constant-pro.js"; 6 | import symlinkConfig from "./config/constant-symlink.js"; 7 | import strmConfig from "./config/constant-strm.js"; 8 | import transcodeConfig from "./config/constant-transcode.js"; 9 | import extConfig from "./config/constant-ext.js"; 10 | import nginxConfig from "./config/constant-nginx.js"; 11 | 12 | // 必填项,根据实际情况修改下面的设置 13 | 14 | // 这里默认 emby/jellyfin 的地址是宿主机,要注意 iptables 给容器放行端口 15 | const embyHost = "http://172.17.0.1:8096"; 16 | 17 | // emby/jellyfin api key, 在 emby/jellyfin 后台设置 18 | const embyApiKey = "f839390f50a648fd92108bc11ca6730a"; 19 | 20 | // 挂载工具 rclone/CD2 多出来的挂载目录, 例如将 od,gd 挂载到 /mnt 目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt 21 | // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 emby 处理 22 | // 如果没有挂载,全部使用 strm 文件,此项填[""],必须要是数组 23 | const mediaMountPath = ["/mnt"]; 24 | 25 | // for js_set 26 | function getEmbyHost(r) { 27 | return embyHost; 28 | } 29 | function getTranscodeEnable(r) { 30 | return transcodeConfig.transcodeConfig.enable; 31 | } 32 | function getTranscodeType(r) { 33 | return transcodeConfig.transcodeConfig.type; 34 | } 35 | function getImageCachePolicy(r) { 36 | return extConfig.imageCachePolicy; 37 | } 38 | 39 | function getUsersItemsLatestFilterEnable(r) { 40 | return extConfig.itemHiddenRule.some(rule => !rule[2] || rule[2] == 0 || rule[2] == 4); 41 | } 42 | 43 | export default { 44 | embyHost, 45 | embyApiKey, 46 | mediaMountPath, 47 | strHead: commonConfig.strHead, 48 | 49 | alistAddr: mountConfig.alistAddr, 50 | alistToken: mountConfig.alistToken, 51 | alistSignEnable: mountConfig.alistSignEnable, 52 | alistSignExpireTime: mountConfig.alistSignExpireTime, 53 | alistPublicAddr: mountConfig.alistPublicAddr, 54 | clientSelfAlistRule: mountConfig.clientSelfAlistRule, 55 | redirectCheckEnable: mountConfig.redirectCheckEnable, 56 | fallbackUseOriginal: mountConfig.fallbackUseOriginal, 57 | 58 | redirectConfig: proConfig.redirectConfig, 59 | routeCacheConfig: proConfig.routeCacheConfig, 60 | routeRule: proConfig.routeRule, 61 | mediaPathMapping: proConfig.mediaPathMapping, 62 | alistRawUrlMapping: proConfig.alistRawUrlMapping, 63 | 64 | symlinkRule: symlinkConfig.symlinkRule, 65 | redirectStrmLastLinkRule: strmConfig.redirectStrmLastLinkRule, 66 | transcodeConfig: transcodeConfig.transcodeConfig, 67 | 68 | embyNotificationsAdmin: extConfig.embyNotificationsAdmin, 69 | embyRedirectSendMessage: extConfig.embyRedirectSendMessage, 70 | itemHiddenRule: extConfig.itemHiddenRule, 71 | streamConfig: extConfig.streamConfig, 72 | searchConfig: extConfig.searchConfig, 73 | webCookie115: extConfig.webCookie115, 74 | directHlsConfig: extConfig.directHlsConfig, 75 | playbackInfoConfig: extConfig.playbackInfoConfig, 76 | 77 | getEmbyHost, 78 | getTranscodeEnable, 79 | getTranscodeType, 80 | getImageCachePolicy, 81 | getUsersItemsLatestFilterEnable, 82 | 83 | nginxConfig: nginxConfig.nginxConfig, 84 | getDisableDocs: nginxConfig.getDisableDocs, 85 | } 86 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/custome/location/default.conf: -------------------------------------------------------------------------------- 1 | 2 | # 1.location 默认为精确匹配,优先级最高; 3 | # 2."^~": 前缀匹配,不支持正则,优先级高于正则匹配,低于精确匹配; 4 | # 3."~": 区分大小写路径正则匹配,优先级最低; 5 | # 4."~*": 不区分大小写路径正则匹配,优先级最低; 6 | # 5.请求路径带 / 结尾: 匹配目录,自动追加斜杠; 不带 / 结尾: 匹配请求或文件或目录,不自动追加斜杠; 7 | 8 | # location ~* /Sync/JobItems/(.*)/File { 9 | # } 10 | 11 | # 切断客户端与上游服务器的 304 缓存协商 12 | # location ~* \.(js|css)$ { 13 | # proxy_set_header Cache-Control "no-cache"; 14 | # add_header X-Mark "forced Cache-Control no-cache"; 15 | # proxy_pass $emby; 16 | # } -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/docs/UA.txt: -------------------------------------------------------------------------------- 1 | // Warnning 2 | 01. undefined 3 | 4 | // Android Mobile Clients 5 | 01. Emby/3.2.32-17.41 (Linux;Android 14) ExoPlayerLib/2.13.2 6 | 02. Emby/3.2.32-17.24 (Linux;Android 13) ExoPlayerLib/2.13.2 7 | 03. libmpv 8 | 9 | // Android TV Clients 10 | 01. Emby/2.0.95g (Linux;Android 9) ExoPlayerLib/2.18.7 11 | 12 | // Windows PC Clients 13 | 01. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) EmbyTheater/3.0.20-3.0 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36 14 | 15 | // 3rd party Clients 16 | 01. Fileball/238 CFNetwork/1410.0.3 Darwin/22.6.0 17 | 02. Infuse-Direct/7.8 18 | 03. Infuse-Download/8.0.2 19 | 04. SenPlayer/4.0.8 20 | 05. VidHub/1.7.6 21 | 22 | // 3rd party Player 23 | 01. dandanplay/android 4.1.0 24 | 02. VLC/3.0.18 LibVLC/3.0.18 25 | 03. VLC/4.0.0-dev LibVLC/4.0.0-dev 26 | 04. MXPlayer/1.68.4 (Linux; Android 14; zh-CN; 2311DRK48C Build/UP1A.230905.011) 27 | 05. (Windows NT 10.0; Win64; x64) PotPlayer/23.7.7 28 | 29 | // Web Browsers 30 | 01. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0 31 | 02. Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0 32 | 03. AppleCoreMedia/1.0.0.20G81 (iPad; U; CPU OS 16_6_1 like Mac OS X; zh_cn) 33 | 04. AppleCoreMedia/1.0.0.21F90 (iPhone; U; CPU OS 17_5_1 like Mac OS X; zh_cn) 34 | 05. Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro Build/RP1A.200720.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/045913 Mobile Safari/537.36 V1_AND_SQ_8.8.68_2538_YYB_D A_8086800 QQ/8.8.68.7265 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/76 SimpleUISwitch/1 QQTheme/2971 InMagicWin/0 StudyMode/0 CurrentMode/1 CurrentFontScale/1.0 GlobalDensityScale/0.9818182 AppId/537112567 Edg/98.0.4758.102 -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/docs/test-data.md: -------------------------------------------------------------------------------- 1 | 2 | # emby-v-media.js 3 | 4 | #### 1.fetch115Hls masterPlaylistText 5 | ```log 6 | #EXTM3U 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1800000,RESOLUTION=1280x720,AUDIO="Audio-Group",NAME="HD" 8 | https: //cpats01.115.com/2032827273f2509ec953cdcdf6bb2643/66BDE8C8/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_1280.m3u8?u=361327067&se=u,ua&s=104857600&ck= 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000,RESOLUTION=1920x1080,AUDIO="Audio-Group",NAME="UD" 10 | https: //cpats01.115.com/7b9da42ee9b6ef6d88b9cdc216b52f4a/66BDE8C8/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_1920.m3u8?u=361327067&se=u,ua&s=157286400&ck= 11 | ``` 12 | 13 | #### 2.fetch115Hls function result 14 | ```json 15 | { 16 | "streams": [ 17 | { 18 | "resolution": "720", 19 | "url": "https://cpats01.115.com/2809f2d1eef77d71beab670be626d566/66BE4AC2/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_1280.m3u8?u=361327067&se=u,ua&s=104857600&ck=", 20 | "quality": "HD" 21 | }, 22 | { 23 | "resolution": "1080", 24 | "url": "https://cpats01.115.com/b3a68e1971bd46c58725c86228fde500/66BE4AC2/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_1920.m3u8?u=361327067&se=u,ua&s=157286400&ck=", 25 | "quality": "UD" 26 | } 27 | ], 28 | "audios": [], 29 | "subtitles": [ 30 | { 31 | "sid": "41f2287f2634e9d8762104691de0c974builtin", 32 | "language": "", 33 | "title": "[内置字幕]简体", 34 | "url": "http://cpats01.115.com/43579af238305bcbec6af0df14d71e71/66BE4AC2/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_0.srt?u=0&t=33db561f2eca147c51b77f3b8dc2f18f", 35 | "type": "srt" 36 | }, 37 | { 38 | "sid": "f2ac38e1122f83fa13f378abba2bc185builtin", 39 | "language": "", 40 | "title": "[内置字幕]繁体", 41 | "url": "http://cpats01.115.com/716ea709a65a436c7ad9c0eaecbb2aa8/66BE4AC2/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148/6CC0BBC7CEFCD1AA339F9CE64BE82E4249321148_1.srt?u=0&t=33db561f2eca147c51b77f3b8dc2f18f", 42 | "type": "srt" 43 | } 44 | ], 45 | } 46 | ``` 47 | 48 | #### 3.fetch115Hls subtitle API result 49 | ```json 50 | { 51 | "state": true, 52 | "error": "", 53 | "errNo": 0, 54 | "data": { 55 | "autoload": { 56 | "sid": "18df1b2ceef91541d3bc82c7a10c4249builtin", 57 | "language": "", 58 | "title": "[内置字幕]字幕1", 59 | "url": "http://cpats01.115.com/397c53d085d9804be5a35f60e10871d6/66BF8DB2/83C82F4C00A1349F53298119F408F6FAA6C5D678/83C82F4C00A1349F53298119F408F6FAA6C5D678_0.ass?u=0&t=33db561f2eca147c51b77f3b8dc2f18f", 60 | "type": "ass" 61 | }, 62 | "list": [ 63 | { 64 | "sid": "18df1b2ceef91541d3bc82c7a10c4249builtin", 65 | "language": "", 66 | "title": "[内置字幕]字幕1", 67 | "url": "http://cpats01.115.com/397c53d085d9804be5a35f60e10871d6/66BF8DB2/83C82F4C00A1349F53298119F408F6FAA6C5D678/83C82F4C00A1349F53298119F408F6FAA6C5D678_0.ass?u=0&t=33db561f2eca147c51b77f3b8dc2f18f", 68 | "type": "ass" 69 | }, 70 | { 71 | "sid": "5ac9fb583be99c63415862771e2080fabuiltin", 72 | "language": "", 73 | "title": "[内置字幕]字幕2", 74 | "url": "http://cpats01.115.com/b0e55d33463c239e9d5bd2049ac2ccd1/66BF8DB2/83C82F4C00A1349F53298119F408F6FAA6C5D678/83C82F4C00A1349F53298119F408F6FAA6C5D678_1.ass?u=0&t=33db561f2eca147c51b77f3b8dc2f18f", 75 | "type": "ass" 76 | } 77 | ] 78 | } 79 | } 80 | ``` 81 | 82 | #### 4.emby local media External subtitle type item 83 | ```json 84 | { 85 | "Protocol": "File", 86 | "Id": "442550c9fb8501bf2e8d317495a04d26", 87 | "Path": "/AList/alias/xxx - S01E01 - 第1集.mkv", 88 | "Type": "Default", 89 | "Container": "mkv", 90 | "Size": 1091315571, 91 | "Name": "xxx - S01E01 - 第1集", 92 | "IsRemote": false, 93 | "HasMixedProtocols": false, 94 | "RunTimeTicks": 14119520000, 95 | "SupportsTranscoding": false, 96 | "SupportsDirectStream": true, 97 | "SupportsDirectPlay": true, 98 | "IsInfiniteStream": false, 99 | "RequiresOpening": false, 100 | "RequiresClosing": false, 101 | "RequiresLooping": false, 102 | "SupportsProbing": false, 103 | "MediaStreams": [ 104 | { 105 | "Codec": "hevc", 106 | "TimeBase": "1/1000", 107 | "VideoRange": "SDR", 108 | "DisplayTitle": "1080p HEVC", 109 | "IsInterlaced": false, 110 | "BitRate": 6183301, 111 | "BitDepth": 10, 112 | "RefFrames": 1, 113 | "IsDefault": true, 114 | "IsForced": false, 115 | "IsHearingImpaired": false, 116 | "Height": 1080, 117 | "Width": 1920, 118 | "AverageFrameRate": 23.976025, 119 | "RealFrameRate": 23.976025, 120 | "Profile": "Main 10", 121 | "Type": "Video", 122 | "AspectRatio": "16:9", 123 | "Index": 0, 124 | "IsExternal": false, 125 | "IsTextSubtitleStream": false, 126 | "SupportsExternalStream": false, 127 | "Protocol": "File", 128 | "PixelFormat": "yuv420p10le", 129 | "Level": 120, 130 | "IsAnamorphic": false, 131 | "ExtendedVideoType": "None", 132 | "ExtendedVideoSubType": "None", 133 | "ExtendedVideoSubTypeDescription": "None", 134 | "AttachmentSize": 0 135 | }, 136 | { 137 | "Codec": "flac", 138 | "TimeBase": "1/1000", 139 | "DisplayTitle": "FLAC stereo (默认)", 140 | "IsInterlaced": false, 141 | "ChannelLayout": "stereo", 142 | "BitDepth": 24, 143 | "Channels": 2, 144 | "SampleRate": 48000, 145 | "IsDefault": true, 146 | "IsForced": false, 147 | "IsHearingImpaired": false, 148 | "Type": "Audio", 149 | "Index": 1, 150 | "IsExternal": false, 151 | "IsTextSubtitleStream": false, 152 | "SupportsExternalStream": false, 153 | "Protocol": "File", 154 | "ExtendedVideoType": "None", 155 | "ExtendedVideoSubType": "None", 156 | "ExtendedVideoSubTypeDescription": "None", 157 | "AttachmentSize": 0 158 | }, 159 | { 160 | "Codec": "ass", 161 | "Language": "chs", 162 | "DisplayTitle": "Chinese Simplified (ASS)", 163 | "DisplayLanguage": "Chinese Simplified", 164 | "IsInterlaced": false, 165 | "IsDefault": false, 166 | "IsForced": false, 167 | "IsHearingImpaired": false, 168 | "Type": "Subtitle", 169 | "Index": 2, 170 | "IsExternal": true, 171 | "DeliveryMethod": "External", 172 | "DeliveryUrl": "/Videos/284773/442550c9fb8501bf2e8d317495a04d26/Subtitles/2/0/Stream.ass?api_key=xxx", 173 | "IsExternalUrl": false, 174 | "IsTextSubtitleStream": true, 175 | "SupportsExternalStream": true, 176 | "Path": "/AList/alias/xxx - S01E01 - 第1集.chi.zh-cn.ass", 177 | "Protocol": "File", 178 | "ExtendedVideoType": "None", 179 | "ExtendedVideoSubType": "None", 180 | "ExtendedVideoSubTypeDescription": "None", 181 | "AttachmentSize": 0 182 | }, 183 | { 184 | "Codec": "ass", 185 | "Language": "cht", 186 | "DisplayTitle": "Chinese Traditional (ASS)", 187 | "DisplayLanguage": "Chinese Traditional", 188 | "IsInterlaced": false, 189 | "IsDefault": false, 190 | "IsForced": false, 191 | "IsHearingImpaired": false, 192 | "Type": "Subtitle", 193 | "Index": 3, 194 | "IsExternal": true, 195 | "DeliveryMethod": "External", 196 | "DeliveryUrl": "/Videos/284773/442550c9fb8501bf2e8d317495a04d26/Subtitles/3/0/Stream.ass?api_key=xxx", 197 | "IsExternalUrl": false, 198 | "IsTextSubtitleStream": true, 199 | "SupportsExternalStream": true, 200 | "Path": "/AList/alias/xxx - S01E01 - 第1集.zh-tw.ass", 201 | "Protocol": "File", 202 | "ExtendedVideoType": "None", 203 | "ExtendedVideoSubType": "None", 204 | "ExtendedVideoSubTypeDescription": "None", 205 | "AttachmentSize": 0 206 | } 207 | ], 208 | "Formats": [], 209 | "Bitrate": 6183301, 210 | "RequiredHttpHeaders": {}, 211 | "DirectStreamUrl": "/videos/284773/stream/xxx-%20S01E01%20-%20%E7%AC%AC1%E9%9B%86.mkv?UserId=ac0d220d548f43bbb73cf9b44b2ddf0e&IsPlayback=false&AutoOpenLiveStream=false&MaxStreamingBitrate=200000000&X-Emby-Client=Emby%20Web&X-Emby-Device-Name=Microsoft%20Edge%20Windows&X-Emby-Device-Id=2d427412-43e1-49e4-a1db-fa17c04d49db&X-Emby-Client-Version=4.8.8.0&X-Emby-Token=xxx&X-Emby-Language=zh-cn&reqformat=json&MediaSourceId=442550c9fb8501bf2e8d317495a04d26&PlaySessionId=51c79c1d3e504cc6a8cf2688700963fa&Static=true", 212 | "AddApiKeyToDirectStreamUrl": false, 213 | "ReadAtNativeFramerate": false, 214 | "DefaultAudioStreamIndex": 1, 215 | "DefaultSubtitleStreamIndex": 2, 216 | "ItemId": "284773", 217 | "XRouteMode": "redirect", 218 | "XOriginDirectStreamUrl": "/videos/284773/original.mkv?DeviceId=2d427412-43e1-49e4-a1db-fa17c04d49db&MediaSourceId=442550c9fb8501bf2e8d317495a04d26&PlaySessionId=51c79c1d3e504cc6a8cf2688700963fa&api_key=xxx", 219 | "XModifyDirectStreamUrlSuccess": true 220 | } 221 | ``` -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/exampleConfig/constant-main.js: -------------------------------------------------------------------------------- 1 | // 这个总配置拆分主体文件只是备份,生效需要放置在 conf.d 下,且重命名为 constant.js 2 | // 如果使用拆分配置,请注意填写 config 下的使用到的配置文件 3 | 4 | import commonConfig from "./config/constant-common.js"; 5 | import mountConfig from "./config/constant-mount.js"; 6 | import proConfig from "./config/constant-pro.js"; 7 | import symlinkConfig from "./config/constant-symlink.js"; 8 | import strmConfig from "./config/constant-strm.js"; 9 | import transcodeConfig from "./config/constant-transcode.js"; 10 | import extConfig from "./config/constant-ext.js"; 11 | import nginxConfig from "./config/constant-nginx.js"; 12 | 13 | // 必填项,根据实际情况修改下面的设置 14 | 15 | // 这里默认 emby/jellyfin 的地址是宿主机,要注意 iptables 给容器放行端口 16 | const embyHost = "http://172.17.0.1:8096"; 17 | 18 | // emby/jellyfin api key, 在 emby/jellyfin 后台设置 19 | const embyApiKey = "f839390f50a648fd92108bc11ca6730a"; 20 | 21 | // 挂载工具 rclone/CD2 多出来的挂载目录, 例如将 od,gd 挂载到 /mnt 目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt 22 | // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 emby 处理 23 | // 如果没有挂载,全部使用 strm 文件,此项填[""],必须要是数组 24 | const mediaMountPath = ["/mnt"]; 25 | 26 | // for js_set 27 | function getEmbyHost(r) { 28 | return embyHost; 29 | } 30 | function getTranscodeEnable(r) { 31 | return transcodeConfig.transcodeConfig.enable; 32 | } 33 | function getTranscodeType(r) { 34 | return transcodeConfig.transcodeConfig.type; 35 | } 36 | function getImageCachePolicy(r) { 37 | return extConfig.imageCachePolicy; 38 | } 39 | 40 | function getUsersItemsLatestFilterEnable(r) { 41 | return extConfig.itemHiddenRule.some(rule => !rule[2] || rule[2] == 0 || rule[2] == 4); 42 | } 43 | 44 | export default { 45 | embyHost, 46 | embyApiKey, 47 | mediaMountPath, 48 | strHead: commonConfig.strHead, 49 | 50 | alistAddr: mountConfig.alistAddr, 51 | alistToken: mountConfig.alistToken, 52 | alistSignEnable: mountConfig.alistSignEnable, 53 | alistSignExpireTime: mountConfig.alistSignExpireTime, 54 | alistPublicAddr: mountConfig.alistPublicAddr, 55 | clientSelfAlistRule: mountConfig.clientSelfAlistRule, 56 | redirectCheckEnable: mountConfig.redirectCheckEnable, 57 | fallbackUseOriginal: mountConfig.fallbackUseOriginal, 58 | 59 | routeCacheConfig: proConfig.routeCacheConfig, 60 | routeRule: proConfig.routeRule, 61 | mediaPathMapping: proConfig.mediaPathMapping, 62 | alistRawUrlMapping: proConfig.alistRawUrlMapping, 63 | 64 | symlinkRule: symlinkConfig.symlinkRule, 65 | redirectStrmLastLinkRule: strmConfig.redirectStrmLastLinkRule, 66 | transcodeConfig: transcodeConfig.transcodeConfig, 67 | 68 | embyNotificationsAdmin: extConfig.embyNotificationsAdmin, 69 | embyRedirectSendMessage: extConfig.embyRedirectSendMessage, 70 | itemHiddenRule: extConfig.itemHiddenRule, 71 | streamConfig: extConfig.streamConfig, 72 | searchConfig: extConfig.searchConfig, 73 | webCookie115: extConfig.webCookie115, 74 | directHlsConfig: extConfig.directHlsConfig, 75 | playbackInfoConfig: extConfig.playbackInfoConfig, 76 | 77 | getEmbyHost, 78 | getTranscodeEnable, 79 | getTranscodeType, 80 | getImageCachePolicy, 81 | getUsersItemsLatestFilterEnable, 82 | 83 | nginxConfig: nginxConfig.nginxConfig, 84 | getDisableDocs: nginxConfig.getDisableDocs, 85 | } 86 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/includes/http.conf: -------------------------------------------------------------------------------- 1 | server_name default; 2 | listen 8091; ## Listens on port IPv4 3 | listen [::]:8091; # Listens on port IPv6 -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/includes/https.conf: -------------------------------------------------------------------------------- 1 | server_name default; 2 | ## SSL SETTINGS ## 3 | listen [::]:8095 ssl; ## Listens on port IPv6 with ssl enabled 4 | listen 8095 ssl; ## Listens on port IPv4 with ssl enabled 5 | # listen 8095 quic reuseport; ## http3 enabled 6 | http2 on; ## since nginx 1.25.1, the "listen ... http" directive is deprecated 7 | add_header Alt-Svc 'h3=":$server_port"; ma=86400'; ## http3 enabled 8 | ssl_session_timeout 30m; 9 | ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; 10 | ssl_certificate /etc/nginx/conf.d/cert/fullchain.pem; ## Location of your public PEM file. 11 | ssl_certificate_key /etc/nginx/conf.d/cert/privkey.key; ## Location of your private PEM file. 12 | ssl_session_cache shared:SSL:10m; 13 | error_page 497 =307 https://$host:$server_port$request_uri; ## if http and https use same port, Redirects http:// to https:// 14 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/includes/proxy-header.conf: -------------------------------------------------------------------------------- 1 | #proxy_set_header X-Real-IP $http_CF_Connecting_IP; 2 | ## if you use cloudflare un-comment this line and comment out above line. 3 | proxy_set_header Host $host; 4 | proxy_set_header X-Real-IP $remote_addr; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | proxy_set_header X-Forwarded-Proto $scheme; 7 | proxy_set_header X-Forwarded-Protocol $scheme; 8 | proxy_set_header X-Forwarded-Host $http_host; 9 | proxy_set_header Range $http_range; 10 | proxy_set_header If-Range $http_if_range; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/includes/server-group.conf: -------------------------------------------------------------------------------- 1 | upstream transcode_group { 2 | least_conn; 3 | server 172.17.0.1:8096; 4 | # server 172.17.0.2:8096; 5 | # server 172.17.0.3:8096; 6 | } -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-example.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import events from "../common/events.js"; 7 | import emby from "../emby.js"; 8 | import embyApi from "../api/emby-api.js"; 9 | 10 | async function example() { 11 | return "Hello Word!"; 12 | } 13 | 14 | export default { 15 | example, 16 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-items.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import urlUtil from "../common/url-util.js"; 7 | import events from "../common/events.js"; 8 | import emby from "../emby.js"; 9 | // import embyApi from "../api/emby-api.js"; 10 | 11 | async function itemsFilter(r) { 12 | events.njsOnExit(`itemsFilter: ${r.uri}`); 13 | 14 | const subRes = await subrequestForPath(r); 15 | const body = subRes.body; 16 | const subR = subRes.subR; 17 | const itemHiddenRule = config.itemHiddenRule.filter(rule => rule[2] != 4); 18 | if (itemHiddenRule && itemHiddenRule.length > 0) { 19 | const apiType = r.variables.apiType; 20 | r.warn(`itemsFilter apiType: ${apiType}`); 21 | let mainItemPath; 22 | if (apiType == "itemSimilar") { 23 | // fetch mount emby/jellyfin file path 24 | const itemInfo = util.getItemInfo(r); 25 | r.warn(`itemSimilarInfoUri: ${itemInfo.itemInfoUri}`); 26 | const embyRes = await util.cost(emby.fetchEmbyFilePath, 27 | itemInfo.itemInfoUri, 28 | itemInfo.itemId, 29 | itemInfo.Etag, 30 | itemInfo.mediaSourceId 31 | ); 32 | mainItemPath = embyRes.path; 33 | r.warn(`mainItemPath: ${mainItemPath}`); 34 | } 35 | 36 | const beforeLength = body.Items.length; 37 | let itemHiddenCount = 0; 38 | if (body.Items) { 39 | body.Items = body.Items.filter(item => { 40 | if (!item.Path) { 41 | return true; 42 | } 43 | const flag = !itemHiddenRule.some(rule => { 44 | if ((!rule[2] || rule[2] == 0 || rule[2] == 2) && !!mainItemPath 45 | && util.strMatches(rule[0], mainItemPath, rule[1])) { 46 | return false; 47 | } 48 | if (apiType == "searchSuggest" && rule[2] == 2) { 49 | return false; 50 | } 51 | if (apiType == "backdropSuggest" && rule[2] == 3) { 52 | return false; 53 | } 54 | // 4: 只隐藏[类型风格]接口,这个暂时分页有 bug,被隐藏掉的项会有个空的海报,第一页后的 StartIndex 需要减去 itemHiddenCount 55 | // 且最重要是无法得知当前浏览项目,会误伤导致接口返回[],不建议实现该功能 56 | // if (apiType == "genreSearch" && rule[2] == 4) { 57 | // return false; 58 | // } 59 | if (apiType == "itemSimilar" && rule[2] == 1) { 60 | return false; 61 | } 62 | if (util.strMatches(rule[0], item.Path, rule[1])) { 63 | r.warn(`itemPath hit itemHiddenRule: ${item.Path}`); 64 | itemHiddenCount++; 65 | return true; 66 | } 67 | }); 68 | delete item.Path; 69 | return flag; 70 | }); 71 | } 72 | const logLevel = itemHiddenCount > 0 ? ngx.WARN : ngx.INFO; 73 | ngx.log(logLevel, `itemsFilter before: ${beforeLength}`); 74 | ngx.log(logLevel, `itemsFilter after: ${body.Items.length}`); 75 | if (body.TotalRecordCount) { 76 | body.TotalRecordCount -= itemHiddenCount; 77 | ngx.log(logLevel, `itemsFilter TotalRecordCount: ${body.TotalRecordCount}`); 78 | } 79 | } 80 | 81 | util.copyHeaders(subR.headersOut, r.headersOut); 82 | return r.return(200, JSON.stringify(body)); 83 | } 84 | 85 | async function usersItemsLatestFilter(r) { 86 | events.njsOnExit(`usersItemsLatestFilter: ${r.uri}`); 87 | 88 | const subRes = await subrequestForPath(r); 89 | let body = subRes.body; 90 | const subR = subRes.subR; 91 | const itemHiddenRule = config.itemHiddenRule.filter(rule => !rule[2] || rule[2] == 0 || rule[2] == 4); 92 | if (itemHiddenRule && itemHiddenRule.length > 0 && Array.isArray(body)) { 93 | const beforeLength = body.length; 94 | body = body.filter(item => { 95 | if (!item.Path) { 96 | return true; 97 | } 98 | const flag = !itemHiddenRule.some(rule => { 99 | if (util.strMatches(rule[0], item.Path, rule[1])) { 100 | r.warn(`itemPath hit itemHiddenRule: ${item.Path}`); 101 | return true; 102 | } 103 | }); 104 | delete item.Path; 105 | return flag; 106 | }); 107 | const itemHiddenCount = beforeLength - body.length; 108 | const logLevel = itemHiddenCount > 0 ? ngx.WARN : ngx.INFO; 109 | ngx.log(logLevel, `usersItemsLatestFilter before: ${beforeLength}`); 110 | ngx.log(logLevel, `usersItemsLatestFilter after: ${body.length}`); 111 | ngx.log(logLevel, `usersItemsLatestFilter itemHiddenCount: ${itemHiddenCount}`); 112 | } 113 | 114 | util.copyHeaders(subR.headersOut, r.headersOut); 115 | return r.return(200, JSON.stringify(body)); 116 | } 117 | 118 | async function subrequestForPath(r) { 119 | r.variables.request_uri += "&Fields=Path"; 120 | // urlUtil.appendUrlArg(r.variables.request_uri, "Fields", "Path"); 121 | const subR = await r.subrequest(urlUtil.proxyUri(r.uri), { 122 | method: r.method, 123 | }); 124 | if (subR.status === 200) { 125 | const body = JSON.parse(subR.responseText); 126 | return { body, subR }; 127 | } else { 128 | r.warn(`${r.uri} subrequest failed, status: ${subR.status}`); 129 | return emby.internalRedirect(r); 130 | } 131 | } 132 | 133 | export default { 134 | itemsFilter, 135 | usersItemsLatestFilter, 136 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-live.js: -------------------------------------------------------------------------------- 1 | // @author: Ambitious 2 | // @date: 2023-09-04 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import events from "../common/events.js"; 7 | import Emby from "../emby.js"; 8 | import embyVMedia from "../modules/emby-v-media.js"; 9 | 10 | async function directLive(r) { 11 | events.njsOnExit(`directLive: ${r.uri}`); 12 | 13 | // check virtualMediaSources, versionDict cache range > routeLXDict, so ignore routeLXDict 14 | const vMediaUrl = await embyVMedia.getUrlByVMediaSources(r); 15 | // if (vMediaUrl === 1) { 16 | // ngx.log(ngx.WARN, `fetchHlsWithCache success, but pre IsPlayback true not ready Streams Array`); 17 | // return r.return(500, "need retry playback"); 18 | // // return r.return(449, "need retry playback"); 19 | // } else 20 | if (vMediaUrl) { 21 | return Emby.redirect(r, vMediaUrl); 22 | } 23 | 24 | if (!Emby.allowRedirect(r)) { 25 | return Emby.internalRedirect(r); 26 | } 27 | 28 | const embyHost = config.embyHost; 29 | const itemInfo = util.getItemInfo(r); 30 | // 1 get the ItemId 31 | const itemId = itemInfo.itemId; 32 | // 2 get Item's PlayBackInfo 33 | // 3 get the live-tv direct m3u8 url 34 | const itemInfoUri = `${embyHost}/Items/${itemId}/PlaybackInfo?api_key=${itemInfo.api_key}&AutoOpenLiveStream=true`; 35 | r.warn(`directLive itemInfoUri: ${itemInfoUri}`); 36 | const response = await ngx.fetch(itemInfoUri, { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json;charset=utf-8" 40 | } 41 | }); 42 | if (!response.ok) { 43 | r.error(response.statusText); 44 | return Emby.internalRedirect(r); 45 | } 46 | const body = await response.json(); 47 | if (!body.MediaSources || body.MediaSources.length === 0) { 48 | r.error('no media source found'); 49 | return Emby.internalRedirect(r); 50 | } 51 | if (!body.MediaSources[0].IsRemote) { 52 | // not a remote link 53 | return Emby.redirect2Pan(r); 54 | } 55 | // 5 execute redirect 56 | Emby.redirect(r, body.MediaSources[0].Path); 57 | } 58 | 59 | export default { directLive }; 60 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-playback-info.js: -------------------------------------------------------------------------------- 1 | // @author: ykchenc 2 | // @date: 2024-10-18 3 | 4 | function sourcesSort(mediaSources, rules) { 5 | return mediaSources.sort((a, b) => { 6 | for (let key in rules) { 7 | let ruleVal = rules[key]; 8 | let aVal = getNestedValue(a, key); 9 | let bVal = getNestedValue(b, key); 10 | if (aVal === undefined) { 11 | if (bVal === undefined) continue; 12 | return 1; 13 | } 14 | if (bVal === undefined) return -1; 15 | if (typeof aVal === 'string') aVal = aVal.toLowerCase(); 16 | if (typeof bVal === 'string') bVal = bVal.toLowerCase(); 17 | if (Array.isArray(ruleVal)) { 18 | for (let i in ruleVal) { 19 | let rule = ruleVal[i]; 20 | const hasRuleA = rule instanceof RegExp ? aVal.match(rule) : aVal.includes(rule.toLowerCase()); 21 | const hasRuleB = rule instanceof RegExp ? bVal.match(rule) : bVal.includes(rule.toLowerCase()); 22 | if (hasRuleA && !hasRuleB) return -1; 23 | if (!hasRuleA && hasRuleB) return 1; 24 | } 25 | } else { 26 | if (aVal < bVal) { return ruleVal === 'asc' ? -1 : 1; } 27 | if (aVal > bVal) { return ruleVal === 'asc' ? 1 : -1; } 28 | } 29 | } 30 | return 0; 31 | }); 32 | } 33 | function getNestedValue(obj, path) { 34 | let current = obj; 35 | const keys = path.split('.'); 36 | for (let i in keys) { 37 | let key = keys[i]; 38 | if (key.includes(':length')) { 39 | const type = key.split(':')[0]; 40 | return (current || []).filter(stream => stream.Type === type).length; 41 | } 42 | if (Array.isArray(current)) { 43 | current = current.find(item => item.Type === key); 44 | if (current === undefined) { 45 | return undefined; 46 | } 47 | } else if (current && current.hasOwnProperty(key)) { 48 | current = current[key]; 49 | } else { 50 | return undefined; 51 | } 52 | } 53 | return current; 54 | } 55 | 56 | export default { 57 | sourcesSort, 58 | }; 59 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-search.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import events from "../common/events.js"; 7 | import emby from "../emby.js"; 8 | // import embyApi from "../api/emby-api.js"; 9 | 10 | const ARGS = { 11 | dictName: "tmpDict", 12 | itemsType: "Episode", 13 | } 14 | 15 | const DIRECTIVE_SPLIT_ENUM = { 16 | directive: { key: "/", desc: "指令分隔符" }, 17 | valueSplit: { key: "=", desc: "参数分隔符" }, 18 | timeSplit: { key: ":", desc: "有效秒数分隔符" }, 19 | } 20 | 21 | const DICT_ZONE_EMUN = { 22 | transcodeDict: { key: "transcodeDict", desc: "转码链接缓存字典" }, 23 | routeL1Dict: { key: "routeL1Dict", desc: "一级直链缓存字典" }, 24 | routeL2Dict: { key: "routeL2Dict", desc: "二级直链缓存字典" }, 25 | // routeL3Dict: { key: "routeL3Dict", desc: "三级直链缓存字典" }, 26 | idemDict: { key: "idemDict", desc: "防抖缓存字典" }, 27 | tmpDict: { key: "tmpDict", desc: "内部用临时变量缓存字典" }, 28 | versionDict: { key: "versionDict", desc: "虚拟多版本缓存字典" }, 29 | } 30 | 31 | const DIRECTIVE_KEY_ENUM = { 32 | help: { key: "help", desc: "帮助" }, 33 | skipImageCache: { key: "skipImageCache", desc: "临时跳过 nginx 图片缓存" }, 34 | opendocs: { key: "opendocs", desc: "临时允许访问上游 docs" }, 35 | showDictZoneState: { key: "showDictZoneState", desc: "显示路由缓存/直链缓存数量" }, 36 | clearDictZone: { key: "clearDictZone", desc: "清空路由缓存/直链缓存" }, 37 | } 38 | 39 | async function searchHandle(r) { 40 | const searchConfig = config.searchConfig; 41 | const enable = searchConfig && searchConfig.interactiveEnable; 42 | if (!enable) { 43 | r.variables.request_uri += `&${util.ARGS.useProxyKey}=1`; 44 | return emby.internalRedirectExpect(r, r.variables.request_uri); 45 | } 46 | let hitFlag = true; 47 | if (searchConfig.interactiveEnableRule && searchConfig.interactiveEnableRule.length > 0) { 48 | hitFlag = searchConfig.interactiveEnableRule.some(rule => r.variables.request_uri.includes(rule)); 49 | } 50 | if (!hitFlag) { 51 | r.variables.request_uri += `&${util.ARGS.useProxyKey}=1`; 52 | return emby.internalRedirectExpect(r, r.variables.request_uri); 53 | } 54 | 55 | events.njsOnExit(`searchHandle: ${r.uri}`); 56 | 57 | const searchTerm = r.args.SearchTerm; 58 | r.headersOut['Content-Type'] = 'application/json; charset=utf-8'; 59 | if (searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM.help.key)) { 60 | r.return(200, handleHelp(r)); 61 | } else if (searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM.skipImageCache.key)) { 62 | r.return(200, handleWithTimeout(r, searchTerm, DIRECTIVE_KEY_ENUM.skipImageCache.key)); 63 | } else if (searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM.opendocs.key)) { 64 | r.return(200, handleWithTimeout(r, searchTerm, DIRECTIVE_KEY_ENUM.opendocs.key)); 65 | } else if (searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM.showDictZoneState.key)) { 66 | r.return(200, handleShowDictZoneStat(r, searchTerm)); 67 | } else if (searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM.clearDictZone.key)) { 68 | r.return(200, handleClearDictZone(r, searchTerm)); 69 | } else if (searchConfig.interactiveFast && searchTerm.startsWith(DIRECTIVE_SPLIT_ENUM.directive.key)) { 70 | r.return(200, handleHelp(r)); 71 | } else { 72 | r.variables.request_uri += `&${util.ARGS.useProxyKey}=1`; 73 | return emby.internalRedirectExpect(r, r.variables.request_uri); 74 | } 75 | } 76 | 77 | function handleHelp(r) { 78 | const items1 = Object.keys(DIRECTIVE_KEY_ENUM).map(k => { 79 | return { 80 | Name: DIRECTIVE_KEY_ENUM[k].desc, // sub title 81 | SeriesName: DIRECTIVE_SPLIT_ENUM.directive.key + DIRECTIVE_KEY_ENUM[k].key, // main title 82 | Type: ARGS.itemsType, 83 | }; 84 | }); 85 | const items2 = Object.keys(DIRECTIVE_SPLIT_ENUM).map(k => { 86 | return { 87 | Name: DIRECTIVE_SPLIT_ENUM[k].desc, // sub title 88 | SeriesName: DIRECTIVE_SPLIT_ENUM[k].key, // main title 89 | Type: ARGS.itemsType, 90 | }; 91 | }); 92 | const items3 = Object.keys(DICT_ZONE_EMUN).map(k => { 93 | return { 94 | Name: DICT_ZONE_EMUN[k].desc, // sub title 95 | SeriesName: DICT_ZONE_EMUN[k].key, // main title 96 | Type: ARGS.itemsType, 97 | }; 98 | }); 99 | const items = items1.concat(items2).concat(items3); 100 | return JSON.stringify({ 101 | Items: items, 102 | TotalRecordCount: 0 103 | }); 104 | } 105 | 106 | /** 107 | * handleWithTimeout 108 | * @param {Object} r nginx objects, HTTP Request 109 | * @param {String} searchTerm 110 | * @param {String} directiveKey not with / 111 | * @returns 112 | */ 113 | function handleWithTimeout(r, searchTerm, directiveKey) { 114 | let msg; 115 | let timeoutS = parseInt(searchTerm.split(DIRECTIVE_SPLIT_ENUM.timeSplit.key)[1]); 116 | r.warn(`${directiveKey} input timeoutS: ${timeoutS}`); 117 | if (isNaN(timeoutS)) { 118 | timeoutS = 60; 119 | r.warn(`${directiveKey} handle default timeoutS: ${timeoutS}`); 120 | } 121 | if (timeoutS < 1) { 122 | msg = ngx.shared[ARGS.dictName].delete(directiveKey) ? "CloseSuccess" : "CloseFail"; 123 | } else { 124 | const msgIndex = util.dictAdd(ARGS.dictName, directiveKey, "1", timeoutS ? timeoutS * 1000 : -1); 125 | msg = ["AddFail", "NotExpire", `With60Seconds`, `With${timeoutS}Seconds`][msgIndex + 1]; 126 | } 127 | r.warn(`${directiveKey} msg: ${msg}`); 128 | return JSON.stringify({ 129 | Items: [{ 130 | Name: searchTerm, 131 | SeriesName: msg, 132 | Type: ARGS.itemsType, 133 | }], 134 | TotalRecordCount: 0 135 | }); 136 | } 137 | 138 | function handleShowDictZoneStat(r, searchTerm) { 139 | const items = Object.values(DICT_ZONE_EMUN).map(value => { 140 | return { 141 | Name: `${value.key}(${value.desc})`, // sub title 142 | SeriesName: String(ngx.shared[value.key].size()), // main title 143 | Type: ARGS.itemsType, 144 | }; 145 | }); 146 | return JSON.stringify({ 147 | Items: items, 148 | TotalRecordCount: 0 149 | }); 150 | } 151 | 152 | function handleClearDictZone(r, searchTerm) { 153 | let msg; 154 | const value = searchTerm.split(DIRECTIVE_SPLIT_ENUM.valueSplit.key)[1]; 155 | if (value) { 156 | if (value.includes("route")) { 157 | ngx.shared[value].clear(); 158 | msg = `clear ${success ? "success" : "fail"}: ${value}`; 159 | r.warn(`handleClearDictZone: ${msg}`); 160 | } else { 161 | msg = `not allow: ${value}`; 162 | r.warn(`handleClearDictZone: ${msg}`); 163 | } 164 | } else { 165 | ngx.shared[DICT_ZONE_EMUN.routeL1Dict.key].clear(); 166 | ngx.shared[DICT_ZONE_EMUN.routeL2Dict.key].clear(); 167 | msg = `clear routeDict success, skip others`; 168 | r.warn(`handleClearDictZone: ${msg}`); 169 | } 170 | return JSON.stringify({ 171 | Items: [{ 172 | Name: searchTerm, 173 | SeriesName: msg, 174 | Type: ARGS.itemsType, 175 | }], 176 | TotalRecordCount: 0 177 | }); 178 | } 179 | 180 | // for js_set 181 | function getNocache(r) { 182 | const value = ngx.shared[ARGS.dictName].get(DIRECTIVE_KEY_ENUM.skipImageCache.key) ?? ""; 183 | // r.log(`getNocache: ${value}`); 184 | return value; 185 | } 186 | 187 | export default { 188 | searchHandle, 189 | getNocache, 190 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-system.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import urlUtil from "../common/url-util.js"; 7 | import events from "../common/events.js"; 8 | import emby from "../emby.js"; 9 | // import embyApi from "../api/emby-api.js"; 10 | 11 | async function systemInfoHandler(r) { 12 | events.njsOnExit(`systemInfoHandler: ${r.uri}`); 13 | 14 | // /emby/System/Info?api_key=xxx is important,access token(api_key) is invalid,clients redirects itself to the login page 15 | // r.variables.request_uri = r.variables.request_uri.replace(r.args.api_key, config.embyApiKey); 16 | const subR = await r.subrequest(urlUtil.proxyUri(r.uri), { 17 | method: r.method, 18 | }); 19 | let body; 20 | if (subR.status === 200) { 21 | body = JSON.parse(subR.responseText); 22 | } else { 23 | r.warn(`systemInfoHandler subrequest failed, status: ${subR.status}`); 24 | return emby.internalRedirect(r); 25 | } 26 | const currentPort = parseInt(r.variables.server_port); 27 | const originPort = parseInt(body.WebSocketPortNumber); 28 | body.WebSocketPortNumber = currentPort; 29 | if (body.HttpServerPortNumber) { 30 | body.HttpServerPortNumber = currentPort; 31 | } 32 | if (body.LocalAddresses) { 33 | body.LocalAddresses.forEach((s, i, arr) => { 34 | arr[i] = s.replace(originPort, currentPort); 35 | }); 36 | } 37 | if (body.RemoteAddresses) { 38 | body.RemoteAddresses.forEach((s, i, arr) => { 39 | arr[i] = s.replace(originPort, currentPort); 40 | }); 41 | } 42 | // old clients 43 | if (body.LocalAddress) { 44 | body.LocalAddress = body.LocalAddress.replace(originPort, currentPort); 45 | } 46 | if (body.WanAddress) { 47 | body.WanAddress = body.WanAddress.replace(originPort, currentPort); 48 | } 49 | util.copyHeaders(subR.headersOut, r.headersOut); 50 | return r.return(200, JSON.stringify(body)); 51 | } 52 | 53 | export default { 54 | systemInfoHandler, 55 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/emby-transcode.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-02-07 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import events from "../common/events.js"; 7 | import emby from "../emby.js"; 8 | import embyApi from "../api/emby-api.js"; 9 | 10 | import qs from "querystring"; 11 | 12 | let keys = { 13 | idKey: "Id", 14 | filePathKey: "Path", 15 | mediaSourcesKey: "MediaSources", 16 | } 17 | 18 | async function transcodeBalance(r) { 19 | if (!checkEnable(r)) { 20 | return emby.internalRedirectExpect(r); 21 | } 22 | events.njsOnExit(`transcodeBalance: ${r.uri}`); 23 | const ua = r.headersIn["User-Agent"]; 24 | 25 | // const routeInternalDictKey = `${r.args.MediaSourceId}:${urlUtil.getDeviceId(r)}`; 26 | 27 | // getCurrentItemInfo 28 | const currentItem = await getCurrentItemInfo(r); 29 | if (!currentItem) { 30 | return emby.internalRedirectExpect(r); 31 | } 32 | 33 | // add Expression Context to r 34 | r[util.ARGS.rXMediaKey] = currentItem; 35 | ngx.log(ngx.WARN, `add emby/jellyfin currentItem to r: ${JSON.stringify(r[util.ARGS.rXMediaKey])}`); 36 | // routeRule, diff of PlaybackInfo routeRule, prevent bypass so rejudge 37 | const notLocal = util.checkIsStrmByPath(currentItem.path); 38 | const routeMode = util.getRouteMode(r, currentItem.path, false, notLocal); 39 | const apiType = r.variables.apiType ?? ""; 40 | r.warn(`getRouteMode: ${routeMode}, apiType: ${apiType}`); 41 | if (util.ROUTE_ENUM.proxy === routeMode) { 42 | return emby.internalRedirectExpect(r); 43 | } else if ((routeMode === util.ROUTE_ENUM.block) 44 | || (routeMode === util.ROUTE_ENUM.blockDownload && apiType.endsWith("Download")) 45 | || (routeMode === util.ROUTE_ENUM.blockPlay && apiType.endsWith("Play")) 46 | // Infuse use VideoStreamPlay to download, UA diff, ignore apiType 47 | || (routeMode === util.ROUTE_ENUM.blockDownload && ua.includes("Infuse")) 48 | ) { 49 | return emby.blocked(r); 50 | } 51 | 52 | // swich transcode opt, skip modify 53 | // if (r.args.StartTimeTicks === "0") { 54 | // // routeRule 55 | // const notLocal = util.checkIsStrmByPath(currentItem.path); 56 | // const routeMode = util.getRouteMode(r, currentItem.path, false, notLocal); 57 | // if (util.ROUTE_ENUM.proxy === routeMode) { 58 | // return emby.internalRedirectExpect(r); 59 | // // not need route, clients will self select 60 | // // } else if (util.ROUTE_ENUM.redirect === routeMode) { 61 | // // // this maybe not support, because player is already inited to HLS 62 | // // return emby.redirect2Pan(r); 63 | // } else if (util.ROUTE_ENUM.block === routeMode) { 64 | // return emby.blocked(r); 65 | // } 66 | // } 67 | 68 | // check transcode load 69 | let transServer = await getTransServer(r); 70 | if (!transServer) { 71 | return emby.internalRedirectExpect(r); 72 | } 73 | 74 | // media item match 75 | const targetItem = await mediaItemMatch(r, currentItem, transServer, keys); 76 | if (!targetItem) { 77 | const targetItemMatchFallback = config.transcodeConfig.targetItemMatchFallback; 78 | r.warn(`targetItemMatchFallback: ${targetItemMatchFallback}`); 79 | if (util.ROUTE_ENUM.proxy == targetItemMatchFallback) { 80 | return emby.internalRedirectExpect(r); 81 | } else { 82 | // util.dictAdd("routeInternalDict", routeInternalDictKey, util.ROUTE_ENUM.redirect); 83 | return r.return(449, "need retry playback"); 84 | // return emby.redirect2Pan(r); 85 | } 86 | } 87 | const targetMediaId = targetItem.source ? targetItem.source[keys.idKey] : targetItem.item[keys.idKey]; 88 | r.warn(`media item match success target server item id: ${targetMediaId}`); 89 | 90 | // build target server url 91 | // redirect to target server 92 | emby.redirect(r, buildTransServerUrl(r, transServer, targetMediaId)); 93 | 94 | // async add cache 95 | util.dictAdd("transcodeDict", r.args["PlaySessionId"], JSON.stringify({ 96 | DeviceId: r.args["DeviceId"], 97 | Server: transServer, 98 | TargetItemId: targetItem.item[keys.idKey], 99 | TargetItemSourceId: targetItem.source ? targetItem.source[keys.idKey] : "", 100 | })); 101 | return; 102 | } 103 | 104 | function checkEnable(r) { 105 | let flag = false; 106 | const transcodeConfig = config.transcodeConfig; 107 | if (transcodeConfig && transcodeConfig.enable 108 | && transcodeConfig.type === "distributed-media-server" 109 | && transcodeConfig.server && transcodeConfig.server.length > 0) { 110 | flag = true; 111 | } 112 | return flag; 113 | } 114 | 115 | async function getTransServer(r) { 116 | const transcodeConfig = config.transcodeConfig; 117 | let serverArr = transcodeConfig.server; 118 | if (!serverArr || (serverArr && serverArr.length === 0)) { 119 | return r.warn(`no transServer, will use current server transcode`); 120 | } 121 | const maxNum = transcodeConfig.maxNum; 122 | let target; 123 | let serverTmp; 124 | let transSessions; 125 | let start = Date.now(); 126 | for (let i = 0; i < serverArr.length; i++) { 127 | serverTmp = serverArr[i]; 128 | try { 129 | transSessions = await embyApi.fetchSessions(serverTmp.host, serverTmp.apiKey, {IsPlaying: true}); 130 | } catch (error) { 131 | r.warn(`fetchSessions: ${error}, skip this server: ${serverTmp.host}`); 132 | continue; 133 | } 134 | r.warn(`fetchSessions res.status: ${transSessions.status}`); 135 | transSessions = await transSessions.json(); 136 | r.log(`fetchSessions res: ${JSON.stringify(transSessions)}`); 137 | transSessions = transSessions.filter(s => embyApi.PLAY_METHOD_ENUM.Transcode == s.PlayState.PlayMethod); 138 | serverTmp.transcodeNum = transSessions.length; 139 | if (transSessions.length > maxNum) { 140 | r.warn(`hit maxNum, skip this server: ${serverTmp.host}`); 141 | continue; 142 | } 143 | target = serverTmp; 144 | } 145 | if (!target) { 146 | r.warn(`all server overload, will use least transcode`); 147 | target = serverArr.sort((a, b) => a.transcodeNum - b.transcodeNum)[0]; 148 | } 149 | let end = Date.now(); 150 | r.warn(`cost ${end - start}ms, find target server: ${target.host}`); 151 | if (target.host == config.embyHost) { 152 | r.warn(`find target server same as currentServer`); 153 | return emby.internalRedirectExpect(r); 154 | } 155 | return target; 156 | } 157 | 158 | async function getCurrentItemInfo(r) { 159 | const isEmby = !!config.embyHost; 160 | if (!isEmby) { 161 | return r.error(`not supported media server type`); 162 | } 163 | 164 | let rvt = { 165 | notLocal: false, 166 | itemName: "", 167 | path: "", 168 | itemId: "", 169 | mediaSourceId: "", 170 | } 171 | let mediaServerRes; 172 | if (isEmby) { 173 | const itemInfo = util.getItemInfo(r); 174 | mediaServerRes = await util.cost(emby.fetchEmbyFilePath, 175 | itemInfo.itemInfoUri, 176 | itemInfo.itemId, 177 | itemInfo.Etag, 178 | itemInfo.mediaSourceId 179 | ); 180 | r.warn(`fetchEmbyFilePath mediaServerRes: ${JSON.stringify(mediaServerRes)}`); 181 | if (mediaServerRes.message.startsWith("error") || !mediaServerRes.itemName || !mediaServerRes.path) { 182 | return r.error(mediaServerRes.message); 183 | } 184 | rvt.notLocal = mediaServerRes.notLocal; 185 | rvt.itemName = mediaServerRes.itemName; 186 | rvt.path = mediaServerRes.path; 187 | rvt.itemId = itemInfo.itemId; 188 | rvt.mediaSourceId = itemInfo.mediaSourceId; 189 | } else {} 190 | r.log(`mediaServerRes: ${JSON.stringify(mediaServerRes)}`); 191 | r.warn(`getCurrentItemInfo: ${JSON.stringify(rvt)}`); 192 | return rvt; 193 | } 194 | 195 | async function mediaItemMatch(r, currentItem, transServer, keys) { 196 | const isEmby = transServer.type == "emby" || transServer.type == "jellyfin"; 197 | if (!isEmby) { 198 | return r.error(`not supported media server type`); 199 | } 200 | 201 | let targetRes; 202 | let targetItems; 203 | if (isEmby) { 204 | try { 205 | targetRes = await util.cost(embyApi.fetchItems, 206 | transServer.host, 207 | transServer.apiKey, 208 | { 209 | SearchTerm: encodeURI(currentItem.itemName), 210 | Limit: 10, 211 | Recursive: true, 212 | Fields: "ProviderIds,Path,MediaSources", 213 | } 214 | ); 215 | } catch (error) { 216 | return r.error(`media item match fetchItems: ${error}`); 217 | } 218 | r.warn(`media item match targetRes.status: ${targetRes.status}`); 219 | targetRes = await targetRes.json(); 220 | targetItems = targetRes.Items; 221 | } else { 222 | // try { 223 | // targetRes = await util.cost(embyApi.fetchItems, 224 | // transServer.host, 225 | // transServer.apiKey, 226 | // { 227 | // SearchTerm: encodeURI(currentItem.itemName), 228 | // Limit: 10, 229 | // Recursive: true, 230 | // Fields: "ProviderIds,Path,MediaSources", 231 | // } 232 | // ); 233 | // } catch (error) { 234 | // r.error(`media item match fetchItems: ${error}`); 235 | // return emby.internalRedirectExpect(r); 236 | // } 237 | // targetRes = await targetRes.json(); 238 | // targetItems = targetRes.Items; 239 | // keys.filePathKey = ""; 240 | // keys.mediaSourcesKey = ""; 241 | } 242 | 243 | r.warn(`media item match fetchItems: ${JSON.stringify(targetItems)}`); 244 | if (targetItems.length < 1) { 245 | return r.error(`media item match not found`); 246 | } 247 | 248 | const currentFileName = currentItem.path.split("/").pop(); 249 | let fileNameTmp; 250 | let targetItem; // mutiple versions parent item 251 | let targetItemSource; // mutiple versions detail item 252 | targetItems.map(item => { 253 | fileNameTmp = item[keys.filePathKey].split("/").pop(); 254 | if (fileNameTmp == currentFileName) { 255 | targetItem = item; 256 | return; 257 | } 258 | item[keys.mediaSourcesKey].map(source => { 259 | fileNameTmp = source[keys.filePathKey].split("/").pop(); 260 | if (fileNameTmp == currentFileName) { 261 | targetItem = item; 262 | targetItemSource = source; 263 | return; 264 | } 265 | }); 266 | }); 267 | r.warn(`media item match targetItem: ${JSON.stringify(targetItem)}`); 268 | if (!targetItem || (targetItem && !targetItem.Id)) { 269 | return r.error(`media item match not found`); 270 | } 271 | 272 | return { item: targetItem, itemSource: targetItemSource, keys: keys }; 273 | } 274 | 275 | function buildTransServerUrl(r, transServer, targetMediaId) { 276 | const isEmby = transServer.type == "emby" || transServer.type == "jellyfin"; 277 | if (!isEmby) { 278 | return r.error(`not supported media server type`); 279 | } 280 | 281 | // let oriArgs = r.variables.args; 282 | r.warn(`original args: ${r.variables.args}`); 283 | let rArgs = r.args; 284 | let baseUrl; 285 | if (isEmby) { 286 | for (let k in rArgs) { 287 | // k == "DeviceId" 288 | if (k == "api_key" || k == "MediaSourceId" || k == "TranscodeReasons") { 289 | const newK = "ori_" + k; 290 | rArgs[newK] = rArgs[k]; 291 | delete rArgs[k]; 292 | } 293 | } 294 | if (transServer.type == "jellyfin") { 295 | // jellyfin, MediaSourceId, The mediaSourceId field is required 296 | rArgs["MediaSourceId"] = targetMediaId; 297 | // jellyfin, StartTimeTicks, Error processing request 298 | // let oriVal = rArgs["StartTimeTicks"]; 299 | // if (oriVal) { 300 | // rArgs["ori_StartTimeTicks"] = oriVal; 301 | delete rArgs["StartTimeTicks"]; 302 | // rArgs["runtimeTicks"] = oriVal; 303 | // } 304 | } 305 | rArgs["api_key"] = transServer.apiKey; 306 | baseUrl = `${transServer.host}/Videos/${targetMediaId}/master.m3u8`; 307 | } else { 308 | // params mapping 309 | // rArgs["api_key"] = transServer.apiKey; 310 | // baseUrl = `${transServer.host}/Videos/${targetMediaId}/master.m3u8`; 311 | } 312 | 313 | // important, avoid dead loops 314 | rArgs[util.ARGS.useProxyKey] = "1"; 315 | let args = qs.stringify(rArgs); 316 | r.warn(`modify args: ${args}`); 317 | 318 | return `${baseUrl}?${args}`; 319 | } 320 | 321 | async function syncDelete(r) { 322 | if (!checkEnable(r)) { 323 | return emby.internalRedirectExpect(r); 324 | } 325 | events.njsOnExit(`syncDelete: ${r.uri}`); 326 | 327 | const uri = r.uri; 328 | let rArgs = r.args; 329 | // Not Expect, this playSessionId on switch video bitrate will always be old value 330 | const playSessionId = rArgs["PlaySessionId"]; 331 | r.warn(`syncDelete transcodeDict key: ${playSessionId}`); 332 | const cachedStr = ngx.shared.transcodeDict.get(playSessionId); 333 | if (!cachedStr) { 334 | r.log(`syncDelete playSession not exist, skip, ${uri}`); 335 | return emby.internalRedirectExpect(r); 336 | } 337 | const cacheObj = JSON.parse(cachedStr); 338 | if (!cacheObj) { 339 | r.warn(`syncDelete cacheObj not exist, skip, ${uri}`); 340 | return emby.internalRedirectExpect(r); 341 | } 342 | const server = cacheObj.Server; 343 | if (!server || (server && !server.host)) { 344 | r.warn(`syncDelete targetServer not exist, skip, ${uri}`); 345 | return emby.internalRedirectExpect(r); 346 | } 347 | let res; 348 | try { 349 | res = await embyApi.fetchVideosActiveEncodingsDelete(server.host, server.apiKey, { 350 | DeviceId: cacheObj.DeviceId, 351 | PlaySessionId: playSessionId 352 | }); 353 | } catch (error) { 354 | r.warn(`fetchVideosActiveEncodingsDelete: ${error}, skip, ${uri}`); 355 | return emby.internalRedirectExpect(r); 356 | } 357 | if (res && res.ok) { 358 | r.warn(`syncDelete success: ${server.host}`); 359 | } 360 | // After redirect, a new njs VM is started in the target location, the VM in the original location is stopped 361 | return emby.internalRedirectExpect(r); 362 | } 363 | 364 | async function syncPlayState(r) { 365 | if (!checkEnable(r)) { 366 | return emby.internalRedirectExpect(r); 367 | } 368 | events.njsOnExit(`syncPlayState: ${r.uri}`); 369 | 370 | const uri = r.uri; 371 | if (!r.requestText) { 372 | r.warn(`syncPlayState requestText not exist, skip, ${uri}`); 373 | return emby.internalRedirectExpect(r); 374 | } 375 | const reqBody = JSON.parse(r.requestText); 376 | // Expect, this playSessionId is always current 377 | const playSessionId = reqBody["PlaySessionId"]; 378 | r.warn(`syncPlayState transcodeDict key: ${playSessionId}`); 379 | const cachedStr = ngx.shared.transcodeDict.get(playSessionId); 380 | if (!cachedStr) { 381 | r.log(`syncPlayState playSession not exist, skip, ${uri}`); 382 | return emby.internalRedirectExpect(r); 383 | } 384 | const cacheObj = JSON.parse(cachedStr); 385 | if (!cacheObj) { 386 | r.warn(`syncPlayState cacheObj not exist, skip, ${uri}`); 387 | return emby.internalRedirectExpect(r); 388 | } 389 | const server = cacheObj.Server; 390 | if (!server || (server && !server.host)) { 391 | r.warn(`syncPlayState targetServer not exist, skip, ${uri}`); 392 | return emby.internalRedirectExpect(r); 393 | } 394 | reqBody["ItemId"] = cacheObj.TargetItemId; 395 | reqBody["MediaSourceId"] = cacheObj.TargetItemSourceId; 396 | reqBody["PlayMethod"] = embyApi.PLAY_METHOD_ENUM.Transcode; 397 | let rArgs = r.args; 398 | if (server.type == "jellyfin") { 399 | delete rArgs["X-Emby-Token"]; 400 | } 401 | rArgs["api_key"] = server.apiKey; 402 | let url = `${server.host}${r.uri}?${qs.stringify(rArgs)}`; 403 | r.warn(`syncPlayState fetchUrl: ${url}`); 404 | r.warn(`syncPlayState fetchBody: ${JSON.stringify(reqBody)}`); 405 | ngx.fetch(url, { 406 | method: r.method, 407 | headers: { 408 | "User-Agent": r.headersIn["User-Agent"], 409 | "Content-Type": "application/json" 410 | }, 411 | body: JSON.stringify(reqBody), 412 | }).then(res => { 413 | r.warn(`syncPlayState fetch res.status: ${res.status}`); 414 | if (res.ok || res.status === 204) { 415 | r.warn(`syncPlayState success: ${server.host}`); 416 | } 417 | }); 418 | return emby.internalRedirectExpect(r); 419 | } 420 | 421 | export default { 422 | transcodeBalance, 423 | syncDelete, 424 | syncPlayState, 425 | }; 426 | -------------------------------------------------------------------------------- /emby2Alist/nginx/conf.d/modules/ngx-ext.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import urlUtil from "../common/url-util.js"; 7 | // import events from "../common/events.js"; 8 | 9 | /** 10 | * fetchLastLink, actually this just once request,currently sufficient 11 | * @param {String} oriLink eg: "https://alist/d/file.xxx" or "http(s)://xxx" 12 | * @param {String} authType eg: "sign" 13 | * @param {String} authInfo eg: "sign:token:expireTime" 14 | * @param {String} ua 15 | * @returns redirect after link 16 | */ 17 | async function fetchLastLink(oriLink, authType, authInfo, ua) { 18 | // this is for multiple instances alist add sign 19 | if (authType && authType === "sign" && authInfo) { 20 | const arr = authInfo.split(":"); 21 | oriLink = util.addAlistSign(oriLink, arr[0], parseInt(arr[1])); 22 | } 23 | // this is for current alist add sign 24 | if (config.alistSignEnable) { 25 | oriLink = util.addAlistSign(oriLink, config.alistToken, config.alistSignExpireTime); 26 | } 27 | const url = encodeURI(oriLink); 28 | const urlParts = urlUtil.parseUrl(url); 29 | const hostValue = `${urlParts.host}:${urlParts.port}`; 30 | ngx.log(ngx.WARN, `fetchLastLink add Host: ${hostValue}`); 31 | try { 32 | // fetch Api ignore nginx locations,ngx.ferch,redirects are not handled 33 | const response = await ngx.fetch(url, { 34 | method: "HEAD", 35 | headers: { 36 | "User-Agent": ua, 37 | Host: hostValue, 38 | }, 39 | max_response_body_size: 1024 40 | }); 41 | const contentType = response.headers["Content-Type"]; 42 | ngx.log(ngx.WARN, `fetchLastLink response.status: ${response.status}, contentType: ${contentType}`); 43 | // response.redirected api error return false 44 | if ((response.status > 300 && response.status < 309) || response.status == 403) { 45 | // if handle really LastLink, modify here to recursive and return link on status 200 46 | return response.headers["Location"]; 47 | } else if (response.status === 200) { 48 | // alist 401 but return 200 status code 49 | if (contentType.includes("application/json")) { 50 | throw new Error("alist maybe return 401, check your alist sign or auth settings"); 51 | } 52 | throw new Error("not expected result"); 53 | } else { 54 | throw new Error(`${response.status} ${response.statusText}`); 55 | } 56 | } catch (error) { 57 | ngx.log(ngx.ERR, `error: fetchLastLink: ${error}`); 58 | } 59 | } 60 | 61 | function lastLinkFailback(url) { 62 | if (!url) { 63 | return url; 64 | } 65 | let rvt = alistLinkFailback(url); 66 | return rvt; 67 | } 68 | 69 | function alistLinkFailback(url) { 70 | let rvt = url; 71 | const alistAddr = config.alistAddr; 72 | const alistPublicAddr = config.alistPublicAddr; 73 | let uri = url.replace(alistAddr, ""); 74 | if (alistAddr && url.startsWith(alistAddr) && !uri.startsWith("/d/")) { 75 | rvt = `${alistAddr}/d${uri}`; 76 | ngx.log(ngx.WARN, `hit alistLinkFailback, add /d: ${rvt}`); 77 | return rvt; 78 | } 79 | uri = url.replace(alistPublicAddr, ""); 80 | if (alistPublicAddr && url.startsWith(alistPublicAddr) && !uri.startsWith("/d/")) { 81 | rvt = `${alistPublicAddr}/d${uri}`; 82 | ngx.log(ngx.WARN, `hit alistLinkFailback, add /d: ${rvt}`); 83 | return rvt; 84 | } 85 | return rvt; 86 | } 87 | 88 | async function linkCheck(url, ua, onlyRedirect) { 89 | const urlParts = urlUtil.parseUrl(url); 90 | const hostValue = `${urlParts.host}:${urlParts.port}`; 91 | ngx.log(ngx.WARN, `linkCheck add Host: ${hostValue}`); 92 | try { 93 | // fetch Api ignore nginx locations,ngx.ferch,redirects are not handled 94 | const response = await ngx.fetch(url, { 95 | method: "HEAD", 96 | headers: { 97 | "User-Agent": ua, 98 | Host: hostValue, 99 | }, 100 | max_response_body_size: 1024 101 | }); 102 | const contentType = response.headers["Content-Type"]; 103 | ngx.log(ngx.WARN, `linkCheck response.status: ${response.status}, contentType: ${contentType}`); 104 | // response.redirected api error return false 105 | if (response.status > 300 && response.status < 309) { 106 | return true; 107 | } else if (response.status === 200) { 108 | // maybe alist 401 but return 200 status code 109 | if (contentType.includes("application/json")) { 110 | throw new Error("linkCheck alist maybe return 401, check your alist sign or auth settings"); 111 | } 112 | if (onlyRedirect) { 113 | throw new Error("not expected result, onlyRedirect true but status 200"); 114 | } 115 | return true; 116 | } else { 117 | throw new Error(`${response.status} ${response.statusText}`); 118 | } 119 | } catch (error) { 120 | ngx.log(ngx.ERR, `error: linkCheck: ${error}`); 121 | } 122 | } 123 | 124 | // NJS log level only have: info, warn, error 125 | // nginx log level settings emby2Alist/nginx/nginx.conf error_log /var/log/nginx/error.log notice; 126 | 127 | function info(message) { 128 | ngx.log(ngx.INFO, message); 129 | } 130 | 131 | function warn(message) { 132 | ngx.log(ngx.WARN, message); 133 | } 134 | 135 | function error(message) { 136 | ngx.log(ngx.ERR, message); 137 | } 138 | 139 | function log(level, message) { 140 | ngx.log(level, message); 141 | } 142 | 143 | export default { 144 | fetchLastLink, 145 | lastLinkFailback, 146 | linkCheck, 147 | info, 148 | warn, 149 | error, 150 | log, 151 | }; -------------------------------------------------------------------------------- /emby2Alist/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_js_module.so; 2 | #load_module modules/ngx_stream_js_module.so; # don't need this 3 | 4 | user root; 5 | worker_processes auto; 6 | 7 | error_log /var/log/nginx/error.log notice; 8 | pid /var/run/nginx.pid; 9 | # error_log syslog:server=192.168.31.200,tag=error_log notice; 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | http { 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | # access_log syslog:server=192.168.31.200,tag=access_log combined; 25 | 26 | sendfile on; 27 | #tcp_nopush on; 28 | 29 | keepalive_timeout 65; 30 | 31 | #gzip on; 32 | 33 | include /etc/nginx/conf.d/*.conf; 34 | } 35 | -------------------------------------------------------------------------------- /embyAddExternalUrl/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | service.nginx: 6 | image: nginx:latest 7 | container_name: nginx-embyUrl 8 | ports: 9 | - 8097:80 10 | volumes: 11 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 12 | - ./nginx/conf.d:/etc/nginx/conf.d 13 | restart: always -------------------------------------------------------------------------------- /embyAddExternalUrl/nginx/conf.d/emby.conf: -------------------------------------------------------------------------------- 1 | # Load the njs script 2 | js_path /etc/nginx/conf.d/; 3 | js_import addExternalUrl from externalUrl.js; 4 | 5 | server{ 6 | gzip on; 7 | listen 80; 8 | server_name default; 9 | set $emby http://172.17.0.1:8096; #emby address 10 | 11 | # Proxy sockets traffic for jellyfin-mpv-shim and webClient 12 | location ~ /(socket|embywebsocket) { 13 | # Proxy Emby Websockets traffic 14 | proxy_pass $emby; 15 | proxy_http_version 1.1; 16 | proxy_set_header Upgrade $http_upgrade; 17 | proxy_set_header Connection "upgrade"; 18 | proxy_set_header Host $host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header X-Forwarded-Proto $scheme; 22 | proxy_set_header X-Forwarded-Protocol $scheme; 23 | proxy_set_header X-Forwarded-Host $http_host; 24 | } 25 | 26 | ## addExternalUrl SETTINGS ## 27 | location ~* /Users/(.*)/Items/(.*)$ { 28 | proxy_buffering off; 29 | js_body_filter addExternalUrl.addExternalUrl buffer_type=string; 30 | proxy_pass $emby; 31 | proxy_pass_request_body off; 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection "upgrade"; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header X-Forwarded-Proto $scheme; 39 | proxy_set_header X-Forwarded-Protocol $scheme; 40 | proxy_set_header X-Forwarded-Host $http_host; 41 | proxy_set_header Accept-Encoding "identity"; 42 | proxy_set_header X-Original-URI $request_uri; 43 | js_header_filter addExternalUrl.HeaderFilter; 44 | } 45 | 46 | location ~* /redirect2external { 47 | js_content addExternalUrl.redirectUrl; 48 | } 49 | ## addExternalUrl SETTINGS ## 50 | 51 | location / { 52 | # Proxy main Emby traffic 53 | proxy_pass $emby; 54 | proxy_set_header Host $host; 55 | proxy_set_header X-Real-IP $remote_addr; 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | proxy_set_header X-Forwarded-Proto $scheme; 58 | proxy_set_header X-Forwarded-Protocol $scheme; 59 | proxy_set_header X-Forwarded-Host $http_host; 60 | } 61 | } -------------------------------------------------------------------------------- /embyAddExternalUrl/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_js_module.so; 2 | 3 | user nginx; 4 | worker_processes auto; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | include /etc/nginx/conf.d/*.conf; 32 | } 33 | -------------------------------------------------------------------------------- /embyWebAddExternalUrl/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### emby 调用外部播放器用户脚本,支持网页和服务端: 3 | 4 | ### 现用户脚本更新地址 5 | https://greasyfork.org/zh-CN/scripts/514529 6 | 7 | 8 | ### 原作者(bpking1)提示信息 9 | 10 | 1. 添加mpv player, 桌面端需要使用这个项目进行设置 https://github.com/akiirui/mpv-handler 11 | 2. 请使用Potplayer官方最新版,目前的版本号是230208,影片标题中文表现为乱码,需要等potplayer官方更新才行 12 | 3. PotPlayer可以调用外挂字幕,未选中外挂字幕的时候默认会尝试加载中文外挂字幕 13 | 4. 取消直链网盘播放按钮,直链需求可以在emby_server解决,请参考 [这篇文章](https://blog.738888.xyz/posts/emby_jellyfin_to_alist_directlink) 14 | 5. potPlayer调用不生效的情况通常是注册表没加上,请重新安装pot官网最新版 15 | 6. 需要多开potPlayer的话,将脚本第186行左右的 /current 删除即可 16 | 7. 推荐直接将js脚本部署到emby_server,这样就不需要油猴了: 以linux为例,在/opt/emby-server/system/dashboard-ui目录下新建externalPlayer.js文件,将本脚本内容复制到里面,然后在当前目录下的index.html的 /body上面引入脚本即可 17 | 8. 提示信息引用至原地址1: https://greasyfork.org/zh-CN/scripts/459297-embylaunchpotplayer 18 | 9. 原脚本的账号无法登陆了,以后在这个地址更新,原地址2 https://greasyfork.org/en/scripts/406811-embylaunchpotplayer ,github地址: https://github.com/bpking1/embyExternalUrl 19 | 20 | ### 部署方式,任选一种 21 | 22 | ## 一.原生部署到服务端上(推荐) 23 | 1. 优点是不依赖其他插件,例如: 油猴/篡改猴, 所有 Web 端共享加载插件,缺点是用户无法手动禁用插件,且非 Web 端不生效 24 | 2. 修改服务端的`../emby-server/system/dashboard-ui/index.html`最下方,/body 标签上,``这行的下方添加, 25 | ```js 26 | ... 27 | 28 | 29 | ``` 30 | 3. 客户端浏览器刷新页面或清空缓存生效 31 | 32 | ## 二.服务端插件用户脚本管理器部署方式(推荐) 33 | 1. 优点是多端统一共用,支持强制启用,手动启用或禁用插件,更为灵活与现代化,缺点是依赖第三方用户脚本管理器,属于 emby 插件,也可添加其它类型脚本并快捷管理,但同样需要服务端与客户端配合使用[CustomCssJS](https://github.com/Shurelol/Emby.CustomCssJS),服务端改一次,客户端修改集成可手动参考三,不想自己改的可直接使用第三方魔改增强版已内置 **CustomCssJS** 集成的即可,缺点为无 iOS 端的已修改版 34 | 35 | ## 三.浏览器用户脚本管理器部署方式 36 | 1. 优点是最传统且符合习惯,缺点是甚至每个浏览器都需要装用户脚本管理器 37 | 38 | ## 四.其他部署方式和各客户端集成方式 39 | 1. 参考: 40 | https://github.com/chen3861229/dd-danmaku#%E5%AE%89%E8%A3%85 41 | 42 | ## 按需更改的地方 43 | 44 | 1.代码内部变量 45 | 46 | ```js 47 | const iconConfig = { 48 | // 图标来源,以下三选一,注释为只留一个,3 的优先级最高 49 | // 1.add icons from jsdelivr, network 50 | baseUrl: "https://emby-external-url.7o7o.cc/embyWebAddExternalUrl/icons", 51 | // baseUrl: "https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@main/embyWebAddExternalUrl/icons", 52 | // 2.server local icons, same as /emby-server/system/dashboard-ui/icons 53 | // baseUrl: "icons", 54 | // 3.add icons from Base64, script inner, this script size 22.5KB to 74KB, 55 | // 自行复制 ./iconsExt.js 内容到此脚本的 getIconsExt 中 56 | // 移除最后几个冗余的自定义开关 57 | removeCustomBtns: false, 58 | }; 59 | // 启用后将修改直接串流链接为真实文件名,方便第三方播放器友好显示和匹配, 60 | // 默认不启用,强依赖 nginx-emby2Alist location two rewrite,如发现原始链接播放失败,请关闭此选项 61 | const useRealFileName = false; 62 | ``` 63 | 64 | 效果: 65 | 66 | Emby Web, iconOnly: false 67 | ![image](https://emby-external-url.7o7o.cc/embyWebAddExternalUrl/preview/preview01.png) 68 | 69 | Emby Web, iconOnly: true 70 | ![image](https://emby-external-url.7o7o.cc/embyWebAddExternalUrl/preview/preview02.png) 71 | 72 | ### CHANGELOG 73 | 74 | #### 1.1.21 75 | 1. fix(embyLaunchPotplayer): 修复上个版本引入的 jellyfin 下的误改,并修复图标 76 | 77 | #### 1.1.20 78 | 1. fix(embyLaunchPotplayer): 适配 4.9.0.40 79 | 2. refactor(embyLaunchPotplayer): 抽取 selectors 80 | 81 | #### 1.1.19 82 | 1. fix(embyLaunchPotplayer): 提供内部变量移除最后几个冗余的自定义开关并添加图标 83 | 2. feat(embyLaunchPotplayer): 提供 strm 播放链接直通开关 84 | 85 | #### 1.1.18 86 | 1. feat(embyLaunchPotplayer): 添加多开 Potplayer 开关 87 | 88 | #### 1.1.17 89 | 1. 优化图标/文字模式,隐藏其他平台播放器,两个开关的数据隔离 90 | 91 | #### 1.1.16 92 | 1. 补充丢失的 icon-MXPlayerPro 93 | 94 | #### 1.1.15 95 | 1. 添加几个播放器支持 96 | 2. 默认开启隐藏其他平台播放器图标 97 | 98 | #### 1.1.14 99 | 1. 修复剪切板 API 兼容性 100 | 101 | #### 1.1.13 102 | 1. 修复 Google Chrome Version >= 130 导致的 PotPlayer 拉起播放错误,但注意不要禁用剪切板权限 103 | 2. 意外修复了 PotPlayer 串流的中文标题支持问题 104 | 3. 更换为现代方式写入剪切板 API 以支持火狐,且可能会导致老旧浏览器无法复制 105 | 106 | #### 1.1.12 107 | 1. 更换默认的网络图标 CDN 为 Cloudflare Pages 地址,以改善中国移动宽带的体验 108 | 2. 更换 @match 为严格匹配以兼容暴力猴 109 | 110 | #### 1.1.11 111 | 1. 播放链接添加 DeviceId 参数 112 | 113 | #### 1.1.10 114 | 1. 修复 mpv-handler 编码错误 115 | 116 | #### 1.1.9 117 | 1. 修复非管理员账号 ddplay 的无 filePath 错误 118 | 119 | #### 1.1.8 120 | 1. 修复自定义的串流和下载地址 121 | 122 | #### 1.1.7 123 | 1. 添加 iconOnly 设置 124 | 2. 兼容Jellyfin 10.9.6 + 125 | 126 | #### 1.1.6 127 | 1. 重构 html 字符串为 js 对象方式,方便排错 128 | 2. 修复未定义变量 129 | 3. 复制并填写了 getIconsExt 函数时优先使用本地图标提升加载速度 130 | 4. 修复 live 文件名 131 | 132 | #### 1.1.5 133 | 1. 修复复制链接按钮 134 | 135 | #### 1.1.4 136 | 1. 兼容 jellfin 10.8.13 137 | 2. stream 提供 useRealFileName 开关 138 | 3. 修复无当前集情况 139 | 4. 调整 JellyfinWebMobileCss 上的样式 140 | 141 | #### 1.1.3 142 | 1. 同步 emby2Alist 中对 stream.ext 的修改 143 | 2. 修复错误的 URL 双重编码 144 | 145 | #### 1.1.2 146 | 1. 兼容播放列表页 147 | 2. 修正一个造成当前集无法播放的 bug 148 | 3. 兼容直播详情页 149 | 4. 添加弹弹 play 和 base64 图标方式 150 | 5. 兼容 jellfin 151 | 6. 修复直播页面 bug 152 | 153 | #### 1.1.1 154 | 兼容首次没有音视频信息加载(STRM) 155 | -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/ExternalPlayer.css: -------------------------------------------------------------------------------- 1 | @media not all and (pointer: fine) { 2 | .ExternalPlayer { 3 | display: none 4 | } 5 | } 6 | 7 | .topDetailsContainer:not(.topDetailsContainer-withbackdrop) .ExternalPlayButton { 8 | display: none 9 | } 10 | 11 | .ExternalPlayButtonStyle.emby-button { 12 | border-radius: 5em 13 | } 14 | 15 | .ExternalPlayer-button-text { 16 | font-size: .8em 17 | } 18 | 19 | .icon-PotPlayer { 20 | background: url(../ExternalPlayer/icon-PotPlayer.webp)no-repeat; 21 | background-size: 100% 100%; 22 | font-size: 1.4em 23 | } 24 | 25 | .icon-MXPlayer { 26 | background: url(../ExternalPlayer/icon-MXPlayer.webp)no-repeat; 27 | background-size: 100% 100%; 28 | font-size: 1.4em 29 | } 30 | 31 | .icon-NPlayer { 32 | background: url(../ExternalPlayer/icon-NPlayer.webp)no-repeat; 33 | background-size: 100% 100%; 34 | font-size: 1.3em 35 | } 36 | 37 | .icon-IINA { 38 | background: url(../ExternalPlayer/icon-IINA.webp)no-repeat; 39 | background-size: 100% 100%; 40 | font-size: 1.4em 41 | } 42 | 43 | .icon-VLC { 44 | background: url(../ExternalPlayer/icon-VLC.webp)no-repeat; 45 | background-size: 100% 100%; 46 | font-size: 1.3em 47 | } 48 | 49 | .icon-infuse { 50 | background: url(../ExternalPlayer/icon-infuse.webp)no-repeat; 51 | background-size: 100% 100%; 52 | font-size: 1.4em 53 | } 54 | 55 | .icon-StellarPlayer { 56 | background: url(../ExternalPlayer/icon-StellarPlayer.webp)no-repeat; 57 | background-size: 100% 100%; 58 | font-size: 1.4em 59 | } 60 | 61 | .icon-MPV { 62 | background: url(../ExternalPlayer/icon-MPV.webp)no-repeat; 63 | background-size: 100% 100%; 64 | font-size: 1.4em 65 | } 66 | 67 | .icon-DDPlay { 68 | background: url(../ExternalPlayer/icon-DDPlay.webp)no-repeat; 69 | background-size: 100% 100%; 70 | font-size: 1.4em 71 | } 72 | 73 | .icon-Copy { 74 | background: url(../ExternalPlayer/icon-Copy.webp)no-repeat; 75 | background-size: 100% 100%; 76 | font-size: 1.4em 77 | } 78 | -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-Copy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-Copy.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-DDPlay.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-DDPlay.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-FigPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-FigPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-Fileball.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-Fileball.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-IINA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-IINA.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-MPV.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-MPV.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-MXPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-MXPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-MXPlayerPro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-MXPlayerPro.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-NPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-NPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-OmniPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-OmniPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-PotPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-PotPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-SenPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-SenPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-StellarPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-StellarPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-VLC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-VLC.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/icon-infuse.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/icon-infuse.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-Copy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-Copy.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-DDPlay.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-DDPlay.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-FigPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-FigPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-Fileball.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-Fileball.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-IINA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-IINA.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-MPV.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-MPV.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-MXPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-MXPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-MXPlayerPro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-MXPlayerPro.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-NPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-NPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-OmniPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-OmniPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-PotPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-PotPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-SenPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-SenPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-StellarPlayer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-StellarPlayer.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-VLC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-VLC.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/icons/min/icon-infuse.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/icons/min/icon-infuse.webp -------------------------------------------------------------------------------- /embyWebAddExternalUrl/preview/preview01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/preview/preview01.png -------------------------------------------------------------------------------- /embyWebAddExternalUrl/preview/preview02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chen3861229/embyExternalUrl/4ad0c67b65267269e8601dbb4ad8803ca068aa0d/embyWebAddExternalUrl/preview/preview02.png -------------------------------------------------------------------------------- /plex2Alist/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### 文章更新记录 3 | 4 | #### 2025-03-10 5 | 6 | 1.fix(emby2Alist&plex2Alist): 修复 include 错误 7 | 8 | 9 | #### 2025-02-24 10 | 11 | 1.fix(emby2Alist&plex2Alist): 修复直链缓存 12 | 13 | #### 2024-12-28 14 | 15 | 1.fix(emby2Alist&plex2Alist): 修复 getClientSelfAlistLink 编码 16 | 17 | 2.fix(emby2Alist&plex2Alist): 修改内网IP的识别通过header中的X-Real-IP、Proxy-Client-IP等实现 18 | 19 | 3.fix(emby2Alist&plex2Alist): 修复错误的 routeRule 分组规则 20 | 21 | #### 2024-12-08 22 | 23 | 1.fix(emby2Alist&plex2Alist): 回退部分 encodeURI 24 | 25 | 2.refactor(emby2Alist&plex2Alist): 耗时打印添加关键字 26 | 27 | #### 2024-12-07 28 | 29 | 1.feat(emby2Alist&plex2Alist): 新增 redirectConfig 参数 30 | 31 | 2.fix(emby2Alist&plex2Alist): 统一 encodeURIComponent 32 | 33 | 3.fix(emby2Alist&plex2Alist): 区分远程链接的解码范围 34 | 35 | #### 2024-11-27 36 | 37 | 1.refactor(emby2Alist&plex2Alist): 优化日志打印开始行,以区分原始 error.log 内容和 njs 日志分隔 38 | 39 | 2.feat(plex2Alist): 解决上游服务双重响应头问题,plex 自身内部不规范,与此脚本无关 40 | 41 | #### 2024-11-26 42 | 43 | 1.feat(emby2Alist&plex2Alist): fallbackUseOriginal 44 | 45 | #### 2024-11-24 46 | 47 | 1.refactor(emby2Alist&plex2Alist): 优化 strMatches 的日志打印量 48 | 49 | #### 2024-11-21 50 | 51 | 1.fix(emby2Alist&plex2Alist): 增强 addAlistSign 特定情况覆盖 52 | 53 | #### 2024-11-20 54 | 55 | 1.feat(emby2Alist&plex2Alist): alistRawUrlMapping 56 | 57 | 2.fix(emby2Alist&plex2Alist): 修复 blockDownload 实现,支持屏蔽 Infuse 下载 58 | 59 | #### 2024-11-17 60 | 61 | 1.强制 Infuse 支持 strm 播放,当然这是实验性质的,因为修改了文件名后缀,绕过了白名单检测, 62 | 理论上任何程序都应该是实时取二进制文件头/魔术头判断文件类型,实测播放器应该都是这样工作的, 63 | 假如是使用 FFmpeg 的话,都是可以播放成功的 64 | 65 | 1.1 加上很久之前修改了 strm 的在 plex 的 Media/Part 对象的 container 字段填充了一个默认值 mp4 进行欺骗支持, 66 | 以上不区分客户端都进行了修改,此次仅针对 Infuse 客户端,强制修改了 Part 对象的 file 字段/文件地址,添加了 .mp4 后缀进行欺骗支持, 67 | 所以在媒体详情或日志或报文中发现 strm 的这两个字段信息有误的话,是预期修改结果,请无视 68 | 69 | 1.2 对比没有修改这两个字段进行欺骗的情况下,原本 plex 对于 strm 播放的支持不够完善,具体为 70 | Web 端和官方 plex 客户端响应了 307 重定向(和 302 差不多意思),但是重定向地址为了安全考虑强制加了服务器的域名前缀,导致无法正常播放, 71 | 而响应给第三方播放器的重定向地址是正确的,理论上除 Infuse 外,其他第三方播放原生支持 strm 播放,不需要此反代修改 72 | 73 | 1.3 没欺骗 Infuse 前,这边简单测试 HamHub: 1.3.9(267) 是正常播放 strm 的,欺骗后 Infuse-Direct/8.0.3 也是支持了, 74 | 具体测试媒体为 mkv 1080p HEVC,文件名为 xxx.strm,只针对 Infuse 返回的 Part 对象的 file 字段/文件地址为, 75 | xxx.strm.mp4, Infuse 中媒体的内封字幕也是兼容的可以切换 76 | 77 | 1.4 唯一缺点是开始播放会比较慢,大概 5-8 秒,这个没办法,plex 相对 emby 没有客户端首次播放 strm 后上报补充媒体信息的功能 78 | 79 | 1.5 另外测试发现 Infuse 对于 emby 是没有屏蔽 strm 文件的详情页的,只对 plex 禁用了,原因未知 80 | 81 | #### 2024-11-11 82 | 83 | 1.115 最近风控比较严格, alist 驱动上前端能填写的最小值 1 已经无法匹配远端限流熔断策略, 84 | 根据 alist issues 与 PR 得知网页描述单位错误,目前需要设置为 0.5 速率才能满足需求, 85 | 即流速为 2 秒 1 个请求,建议尽快更改为小数避免风控损失 86 | 87 | 2.【限制所有 api 请求速率(1r/[limit_rate]s)】目前正确为 ([limit_rate]r/1s), 0.1 意味 10 秒 1 个请求 88 | 89 | 3.更改为小数的方法,停止 alist => 下载 data.db 数据库文件 90 | => 使用 [navicat-premium-lite](https://www.navicat.com.cn/products#navicat-premium-lite) 数据库管理工具免费版打开 91 | => navicat 中双击打开 data.db 的 main 数据库 => 表 => 右侧 x_storages 双击 92 | => 通过 drive 字段将鼠标点击到 115 Cloud 上 => 点击右下角倒数第二行的表单视图 93 | => addition 字段 => json 字段的 "limit_rate" 属性并修改 : 后的数值 94 | => ctrl + s 保存/点击左下角的对勾应用更改 => 关闭 data.db 连接 95 | => 更名原始的 data.db 为 data.db.bak 进行备份 96 | => 将修改后的 data.db 传输到服务端 => 重新启动 alist 即可 97 | 98 | #### 2024-11-02 99 | 100 | 1.精确带参数的 strm 路径截取解码 101 | 102 | #### 2024-10-29 103 | 104 | 1.修复带参数的 strm 路径截取编码 105 | 106 | #### 2024-10-22 107 | 108 | 1.测试最新的稳定版 nginx:1.27.2 发行版,内置的 njs:0.8.6 可能存在内存泄漏问题,导致启动时内存从之前的占用 30MB 暴增至 333MB, 109 | 且 nginx -s reload 时新开的 worker 线程导致总占用内存占用进一步暴涨到 1GB,导致重载命令卡死和系统内存耗尽机器卡死 110 | 111 | 2.目前固定版本为 nginx:1.27.1 待 nginx 官方修复后再改回 nginx:latest 镜像, 112 | 为防止意外内存泄漏卡死机器,docker 脚本已添加资源限制,默认 memory: 128M 也可 memory: 256M 113 | 114 | #### 2024-10-13 115 | 116 | 1.修复变量未初始化的日志输出,此问题不影响任何功能 117 | https://github.com/nginx/njs/issues/668 118 | 119 | #### 2024-08-29 120 | 121 | 1.修复 mediaPathMapping 中的 r 上下文变量传递 122 | 123 | #### 2024-08-28 124 | 125 | 1.移除错误的 proxy_buffering off; 设置,对于大文件只会降低磁盘临时文件的占用,但会增加磁盘IO,进而导致 CPU 占用率增加 126 | 127 | #### 2024-08-22 128 | 129 | 1.plex 路由规则添加了 r.XMedia 对象,以支持码率/媒体时长/媒体大小...之类的判断,特别需要注意原始数值的单位和 emby 不同 130 | 131 | #### 2024-08-14 132 | 133 | 1.升级全体匹配表达式的匹配符类型以增加数值比较 134 | 135 | #### 2024-08-04 136 | 137 | 1.为 mediaPathMapping 添加生效分组规则,需要注意假如规则中有 UserId 或 X-Emby-Device-Id 之类缩小了范围的, 138 | 此时 routeCacheConfig.keyExpression 路由缓存表达式也需酌情缩小范围,区别为直链缓存范围大命中率高,默认仅按媒体版本区分, 139 | 不缩小范围会导致头一个设备访问添加了直链缓存后,后续该相同版本的媒体将跳过 mediaPathMapping 处理直接返回缓存的直链 140 | 141 | #### 2024-07-25 142 | 143 | 1.修复 ngx.fetch API 没传递 Host 头导致的端口丢失 144 | 145 | #### 2024-07-18 146 | 147 | 1.修复 routeRule 分组规则的错误判断 148 | 149 | #### 2024-07-15 150 | 151 | 1.clientSelfAlistRule 添加分组,修复 redirectStrmLastLinkRule 分组 152 | 153 | 2.升级 util.strMatches 以支持字符串形式指定取反操作,不影响历史的数字类型参数,但后续建议使用字符串形式以增强可读性 154 | 155 | #### 2024-07-14 156 | 157 | 1.redirectStrmLastLinkRule 添加分组 158 | 159 | #### 2024-07-09 160 | 161 | 1.去除 mediaMountPath empty 情况的默认走源服务中转,具有二义性,无法区分是未填写还是需要 mediaPathMapping 手动处理, 162 | 可选优化方向为对比路径映射前后的是否相同,来进一步区分,但仍旧存在未知性,可能不符合实际情况, 163 | 故回退到之前的需要 routeRule 路由规则手动指定比较稳妥 164 | 165 | #### 2024-06-01 166 | 167 | 1.修复 L2 缓存只存不取 bug 168 | 169 | #### 2024-05-30 170 | 171 | 1.同步 emby 代码实现 plex 的 L2 缓存功能 172 | 173 | #### 2024-05-26 174 | 175 | 1.完善 plexApiHandler 的责任链模式,方便后续扩展 176 | 177 | #### 2024-05-21 178 | 179 | 1.拆分配置文件,根据自身倾向选择 exampleConfig 中的配置并参照注释 180 | 181 | #### 2024-05-19 182 | 183 | 1.提供获取软连接真实路径的配置项,前提条件是此程序或容器必须挂载或具有对应目录的读取权限,否则将跳过处理,不生效, 184 | 只做了简单的测试,可能暂时存在和非本地文件路径走代理判断稍微有些冲突,自行尝试配置 mediaMountPath 或 routeRule 185 | 186 | #### 2024-05-11 187 | 188 | 1.修复错误的 proxy_buffering off 层级导致的缓存失效 189 | 190 | #### 2024-05-10 191 | 192 | 1.放开 plex 的原始转码功能,这里只是不做修改,交给原始服务中转处理,具体是否转码由 plex 客户端自己判断上报的, 193 | 且 web 端八成都会上报为转码,多半是只支持 AAC 的音频,FLAC 和 DTS 均会转码, 194 | ~~侧重直链成功率的不建议开启此选项~~, 195 | 侧重播放成功率的可以开启, 196 | ~~因 plex 比较封闭,目前客户端自身上报为转码,可能没办法切换回直链,~~ 197 | 需要自己尝试调节客户端网络质量,经热心网友协助测试,客户端可以在播放设置 -> 质量 -> 原始质量,来手动切换回直链, 198 | 目前 plex 对比 emby 的默认播放形式暂时存在不一致情况, plex 默认为直链, emby 客户端会自行记忆全局的上次播放质量, 199 | 不打算更改各客户端的行为,以官方客户端行为为准 200 | 201 | #### 2024-05-05 202 | 203 | 1.还原 115 的链接判断为二级域名,三级域名为 CDN 负载均衡会动态变化, 204 | 最好也不要用 alist get 接口中的 provider 字段,使用了 alias 情况下,provider 为 alias 而非 115 Cloud 205 | 206 | 2.拆分 conf 配置,http 和 https 不要共用相同端口,恢复 plex 默认为 http 配置,IOS 客户端依赖 https,自行配置端口和证书 207 | 208 | #### 2024-04-27 209 | 210 | 1.升级路由缓存配置,缓存的key值可自定义表达式 211 | 212 | #### 2024-04-21 213 | 214 | 1.还原以兼容115部分客户端拖动进度条bug 215 | 216 | #### 2024-04-20 217 | 218 | 1.升级禁用直链规则为路由规则,内部判断变得十分复杂,有一些历史遗留和可能存在一些优先级问题,请自行测试 219 | 220 | #### 2024-04-19 221 | 222 | 1.升级分组禁用直链规则,添加NJS事件日志,重构拆分转码方法 223 | 224 | 2.经热心网友协助测试,发现拖动进度条黑屏属于emby的ios官方客户端和infuse播放115独有的问题,阿里云盘不存在此问题, 225 | 故升级了直链禁用规则,复现测试可以将115直链放到IDM等多线程下载器中,注意设置为获取直链时的UA,即使是115年费会员, 226 | 超过2个线程加载,就会导致整个下载进度卡住,时断时续,猜测这两个客户端默认使用了并发分块加载, 227 | 如果有相关设置的话(一般没有,是播放器内核自己的行为),自行设置为1,或者放开禁用直链规则 228 | 229 | 3.115还有一个问题,很久之前做文件迁移时发现的,一个账号不论多少客户端总共的下载进程最大10个,且有日传输量限制, 230 | 这个具体多少流量不清楚,复现测试为浏览器开10个任务,官方客户端下载文件夹只算一个任务,就会发现其余客户端无法播放和下载了 231 | 232 | #### 2024-04-13 233 | 234 | 1.注意需要保证njs >= 0.8.0,直接nginx:latest即可, 235 | 加入防抖措施,添加内外部重定向地址缓存,以兼容部分客户端不遵循30X重定向规范导致的短时过多重复请求穿透到alist, 236 | 例如emby安卓客户端,infuse,表现形式为每次拖动进度条都会重复请求emby原始串流地址,忽略上次拿到的重定向后地址, 237 | Web端没有此问题,但是播放nas本地视频也会多次请求,故强制兼容解决, 238 | 默认按阿里云盘的直链最大有效时间15分钟,请勿随意更改此时间 239 | 240 | #### 2024-04-12 241 | 242 | 1.添加定时任务默认7天自动清空nginx日志,请结合日志重要程度和硬盘占用情况自行调整为合适间隔,建议不要改为小于1天以免影响性能, 243 | 使用条件为没有更改过默认日志的路径和名称,且需要更新最新版本njs 244 | ``` 245 | /var/log/nginx/error.log 246 | /var/log/nginx/access.log 247 | ``` 248 | 249 | 2.添加限流配置示例,只对302之前的请求生效,302后是直连第三方服务器,无法进行控制 250 | 251 | #### 2024-04-11 252 | 253 | 1.添加nginx对接日志中心示例配置,可以和原xxx_log共存,如有需要,打开注释并修改为自己的ip和端口即可 254 | 发送日志到syslog,默认使用UDP的514端口,群晖=>日志中心=>接收日志=>新增=>名称随意,保持默认的BSD格式,UDP,514 255 | 256 | #### 2024-04-08 257 | 258 | 1.增强禁用直链的规则配置,docker环境需要注意此参数客户端地址($remote_addr),nginx容器网络必须为host模式,不然此变量全部为内网ip,判断无效 259 | 260 | #### 2024-04-05 261 | 262 | 如何避免媒体服务器频繁进行整库扫描刮削,导致内存占用飙升且影响性能,并范围太大会触发网盘的熔断机制 263 | 264 | 1.部分网盘熔断恢复周期,期间会全部拒绝服务,阿里云盘大概为30分钟到1小时,115网盘则为1-2小时 265 | 266 | 2.简单方案,发现整库扫描情况立即重启媒体服务,会消停几小时,但治标不治本,之后还会继续重试,控制台和计划任务里不会显示,无法强制停止 267 | 268 | 3.alist一定要开启115网盘驱动的限制速率,它的限制比较严格,一扫库必定熔断,默认为2【限制所有 api 请求速率(1r/[limit_rate]s)】, 269 | 意味着 QPS **速率** 为 2 ,即 1 秒处理 2 个请求,忽略(1r/[limit_rate]s)这个描述,是错误的, 270 | ~~意味着2秒内只处理1个请求,个人设置为1也是没问题的,此为最小值~~,为0是没限制,缺点是整库扫持续时间会达到一周,但是不会触发熔断,比较安全, 271 | 阿里网盘的熔断机制相对宽松很多,除非一次性文件太多,但是截止当前,alist并没有当前网盘的限流参数 272 | 273 | 4.最简单,全部使用strm文件解决,没有播放的情况下默认不会进行刮削,emby对此兼容性较好,只在第一次播放后会将媒体流参数信息存入数据库, 274 | 下次播放将跳过分析过程,表现形式为秒播,pelx对此无法跳过分析过程,播放开始会长达6-8秒等待,以上为官方客户端,第三方客户端不会有分析过程 275 | 276 | 以下为猜想未经测试,仅供记录 277 | 278 | 1.限流可以放在nginx反代配置,流量路径为 nginx-媒体服务 => 媒体服务 => cd2/rclone => nginx-alist => alist 279 | 280 | 2.媒体库触发强制自动扫库的根本原因为媒体文件的最后修改时间 > 入库时的最后修改时间,有条件可以在nginx-alist这层做反代返回假的最后修改时间, 281 | 或者等待alist添加自定义配置最后修改时间功能,截止目前,并没有相关issue 282 | 283 | 3.从挂载层解决,查询rclone官方文档得知有缓存参数,此测试结果为会在自定义配置的缓存目录同目录结构下,优先生成一个缓存文件, 284 | 缓存文件属性显示大小等于原始文件,但是实际占用大小只为读取的文件大小,例如1G文件,被刮削视频头后,大概实际只占用40%,完全没读取过,只占用0KB, 285 | 但是此缓存文件最后修改时间为最后一次读写时间,文档中没有自定义配置的参数,如果想要Web控制台,可以再套一层cd2 286 | 287 | #### 2024-04-04 288 | 289 | strm的plex官方客户端播放,因为它自身没有刮削stream信息,例如视频流、音频流解码器,所以播放时存在一个分析过程, 290 | 浏览器控制台可以看到分析过程和结果,根据网速决定,至少也要5-8秒, 291 | 和emby不同,emby第一次播放后就会将分析后的媒体信息存入数据库,详情页下方可以看到,所以第二次播放后就是秒播放 292 | 293 | ...过度想象了,局域网环境直接用ip:nginx端口就行了,不会经过xxx.plex.direct:32400,这是plex对于域名https的局域网兼容 294 | 295 | vpn局域网模拟测试了下,无解,plex这点强制性太高,在服务启动时候时候就会把局域网和公网ip还有自定义服务器访问 URL上传到plex.tv, 296 | 同时也上传了geoip服务器的地理位置信息。。。(这个查询说明,默认只有管理员可以看到此信息,不用过度担心) 297 | 298 | 全局刷新页面可以看到 https://clients.plex.tv/api/v2/resources 299 | 这个接口调用,局域网的优先级是最高的,只要联通,详情页就不会请求当前访问的域名或ip了,本来我的环境是docker plex host网络,宿主机局域网段是192.168.xxx, 改为了bridge网络,甚至映射了32400到22400, 结果还是发现plex自己请求172.xxx的docker桥接网段且用了22400,因为群晖的docker有些残废,无法测试container网络模式。 300 | 301 | plex部分接口优先使用局域网,本意是好的,提升网络反应和增加服务容错,它本身貌似没有提供相关参数关闭这个行为,本来我以为的关闭本地网络发现和关闭中转可以禁用此行为,但是局域网下并没有生效,只能自行寻找偏方强行禁用了。 302 | 以下是猜测,任选一个应该可以解决,但是没有实际测试过 303 | 304 | 服务端方向 305 | 306 | 1.较难,想办法将plex服务和其他局域网设备严格网络隔离,局域网找不到就会走自定义url了,隔离方式可以试试docker的container指定nginx的容器名称,或者添加防火墙规则,限制只有nginx可以访问plex 307 | 308 | 2.很难,基本无法阻止,尝试阻止plex上传局域网ip或者改写报文内容将 309 | https://clients.plex.tv/api/v2/resources 310 | ~~以达到将自己域名connections.local = true,~~ 311 | 这是客户端的查询接口,需要自己扒服务端plex与plex.tv上报的接口 312 | 313 | 客户端方向 314 | 315 | 1.较简单,使用路由器的访客wifi内网隔离功能,或者连接旁路由的wifi,以达到隔离plex局域网访问 316 | 317 | 2.较简单,使用代理软件的不同路由模式尝试,或者更改客户机的防火墙规则禁止掉plex的局域网访问 318 | 319 | 3.较简单,plex安卓官方客户端高级设置里可以自定义连接ip和端口,可以留空ip只填nginx的短裤,注意ios没有这个选项 320 | 321 | 4.很简单,使用公网访问自己plex,避免局域网的使用情况,确保nginx的处理生效 322 | 323 | 5.最简单,ios平台使用第三方客户端,原本plex对于strm的支持就是只能远程直链的重定向下载,响应的是301,而部分第三方客户端播放可能使用的下载接口,支持情况不统一 324 | 可以断定plex就是把这个活丢出去了,自身客户端并不想支持播放,因为还有bug,体验不是很好,如果媒体库全部为远程strm直链,且客户端全部为第三方,其实用不着部署nginx了 325 | 326 | 测试已通过 327 | 328 | 1.局域网环境下的Web浏览器添加拓展解决,并更新最新代码 329 | ![e6cb4294e72b3140870f42da1bb966bb](https://github.com/bpking1/embyExternalUrl/assets/42368856/96e1a512-2021-4aff-83dc-bc1424d6e0db) 330 | ![image](https://github.com/bpking1/embyExternalUrl/assets/42368856/ab71279e-296c-4845-af69-19738b25aae2) 331 | 332 | #### 2024-03-30 333 | 334 | 1.增强路径映射功能,区分来源类型 335 | 336 | 2.plex的strm两种都属于强行支持,都可能存在播放记录和回传bug,根本在于pelx自身播放没有处理strm的逻辑 337 | 338 | 3.添加默认可选参数以支持issue建议的指定strm规则获取重定向后的地址进行转发,兼容局域网的strm链接 339 | 340 | 4.测试发现plex非常容易擅自使用中转域名,即使设置里中转已关闭并且指定了plex发现网络的自定义链接,例如用梯子的时候,虽然选择的绕过局域网和大陆,但是PlexWeb马上会变更使用["https://192-168-31-200.{uuid}.plex.direct:32400/library/metadata/180"]这样的链接导致绕过了nginx的处理 341 | 342 | 4.1.建议直链不正常的时候关注下当前请求的url,确认api有经过nginx的处理 343 | 344 | 5.感谢 @sgpublic 提供的 alist sign 计算方案 345 | ~~5.alist >= 3.20的默认对直链开启了sign全部参数,属于额外验证,不接受token验证,~~ 346 | ~~如果要兼容,性能会很差,需要多用token请求一次alist获取到直链和sign参数,解决方案两种~~ 347 | 348 | ~~5.1.用/开头的路径,这样会用alistToken走fsGet接口一次获取最终直链返回,缺点是官方客户端字幕流不正常且播放记录不准确或者没有~~ 349 | 350 | ~~5.2.nginx请求的alist建议关闭设置-全局-签名所有,将此alist部署为和nginx同一局域网,接口响应也会快很多,通常在200ms-2000ms之间,跨网络会更慢~~ 351 | ~~如果对直链安全有介意,去掉此alist的公网端口映射,只在局域网使用,公网使用另行部署一个开启sign全部的alist~~ 352 | 353 | #### 2024-03-28 354 | 355 | 1.添加基本的配置示例文件,若符合需求,更改内容并删除文件名后缀,复制文件到上一级目录覆盖原始文件即可,\emby2Alist\nginx\conf.d\constant.js 356 | 357 | #### 2024-03-26 358 | 359 | 1.强制支持了strm的播放,目前存在的问题为Web端不支持内封字幕显示 360 | 361 | #### 2024-03-25 362 | 363 | 1.修复了plex对strm文件下载的重定向,内部远程链接的plex源服务自身会响应301支持,添加了内部填写相对路径的情况 364 | 365 | 2.~~目前的情况是plex官方客户端不支持播放~~ 366 | Web端可以直链下载,从之前issus的反馈推断第三方客户端播放用的是下载的api,故理论上应该支持直链 367 | 368 | 3.理论上官方客户端直链下载后应该可以播放,但是没通行证,暂无法测试 369 | 370 | #### 2024-03-18 371 | 372 | 1.优化请求alist的115直链获取逻辑,透传UA,减少一次302过程,以兼容媒体服务器https而alist为http默认被浏览器客户端强制改写导致的错误 373 | 374 | 2.~~115直链不在需要clientSelfAlistRule参数,~~ 375 | 但保留处理逻辑,有特殊需要可自定义配置 376 | 377 | #### 2024-03-13 378 | 379 | 1.媒体服务器https后,如果alist没有https,且相同域名情况,http链接会被浏览器默认强制改写为https,导致115的处理的第一次302会失败 380 | 381 | 2.地址栏左边-此网站权限-不安全的内容-允许,或者浏览器设置-Cookie 和网站权限-不安全的内容-允许 382 | 383 | 3.~~非浏览器不存在此问题,~~ 384 | 例如第三方播放器,默认不会阻止,也可将alist套上证书解决此问题 385 | 386 | #### 2024-03-10 387 | 388 | 1.测试并修复本地视频兼容问题,意外发现http2对本地原始链接的视频在部分跨宽带网络阻断有帮助(电信->联通),如有相同情况请开启http2或者http3 389 | 390 | #### 2024-03-07 391 | 392 | 1.测试并添加https相关示例,plex情况特殊,服务端必须设置-网络-自定义服务器访问 URL: [https://自己的域名:自己的端口号],以发布到plex.tv发现服务器,客户端保持默认设置,不用更改高级设置-自定义ip和端口,高级设置-允许非加密连接-永不,如果plex源服务器没有配证书(也不用配),同时不要更改nginx的proxy_pass值,也就是constant.js中的plexHost值,保持[http://]开头 393 | ![image](https://github.com/chen3861229/embyExternalUrl/assets/42368856/5efd2df2-33ba-4dc5-9aff-707f25022a9d) 394 | ![image-2](https://github.com/chen3861229/embyExternalUrl/assets/42368856/0431fcc9-9c08-4ccb-af88-71614df5b9c1) 395 | 396 | 2.Web端还会提示部分内容不安全,无视即可,是plex的设计问题,可自行查找xxx.plex.direct:端口号,关键字解决 397 | 398 | #### 2024-03-03 399 | 1.更换part连接的路径匹配实现,以彻底解决精度问题,同时将挂载路径缓存至客户端,使用到了xml包,此版本需要njs>=0.7.10 400 | 401 | #### 2024-02-29 402 | 403 | 1.修复剧集页面无法打开的bug 404 | 405 | #### 2024-01-19 406 | 407 | 1.提高web的直链下载匹配精度,使用的nginx的新特性共享变量,缓存了/library/metadata接口中part链接数据,此版本需要njs>=0.8.0,直接nginx:latest即可 408 | 409 | ~~2.目前的限制是第三方客户端注意必须进一次详情页,再点播放,不要直接点封面图上的播放按钮,下载和第三方客户端不进详情页播放特殊处理的,链接类似/library/parts/81327/1708863299/file.mp4,这个plex没提供直接的api查询出挂载路径,所以这边的处理是用进详情页的数据在nginx共享变量中缓存的,如果缓存没查到,就会用文件名调用plex的接口查询,和首页的查询接口同一个,所以对文件名带-的这种查询不出来,因为plex入库的时候会去掉-~~ 410 | 411 | #### 2024-01-10 412 | 413 | 1.兼容新版alist默认签名所有115直链 414 | 415 | #### 2024-01-09 416 | 417 | ~~1.为解决part链接无法获取挂载路径问题,加入偏移量计算,需要大致估算多版本视频的套数,用于计算metadata和part(media)的自增id偏差数~~ 418 | 419 | #### 2023-11-04 420 | 421 | 1.参考博客(https://blog.738888.xyz/posts/plex_to_alist_directlink)中的plex直链功能并参考之前的njs实现, 422 | ~~实验性(bug)性质支持了直链播放,~~ 423 | 因为只找到社区维护的简易API(https://plex-docs.vercel.app), 424 | ~~所以实现方式比较别扭,目前存在多版本视频不能直链下载的bug。~~ 425 | 426 | ~~2.Plex要直链播放只需要设置远程访问-手动设置公开端口为nginx监听端口,避免客户端优先直连32400,如果路由器做了32400端口转发,不影响外部访问。~~ 427 | 428 | 2.Plex要直链播放需要避免客户端优先直连 32400, 429 | ~~去掉路由器的 32400 端口转发~~, 430 | 添加 nginx 的端口转发即可 431 | ![image](https://github.com/bpking1/embyExternalUrl/assets/42368856/9abc036a-72db-4434-9be7-1f31c2686bb2) 432 | -------------------------------------------------------------------------------- /plex2Alist/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: plex 重定向到 alist 直链 3 | date: 2023/11/04 22:00:00 4 | --- 5 | 6 | ## 使用步骤: 7 | 8 | ### 1.先将配置文件下载到本地 9 | 10 | 此时大致文件结构如下: 11 | ```javascript 12 | ~/plex2Alist 13 | ├── docker // 创建容器脚本文件夹 14 | | ├── docker-compose.yml // docker-compose 脚本,根据自身情况修改 15 | | ├── nginx-plex.syno.json // 群晖 docker 脚本,根据自身情况修改 16 | └── nginx // nginx 配置文件夹 17 | ├── conf.d // nginx 配置文件夹 18 | |  ├── api // JS 脚本文件夹,完全不用改 19 | | ├── cert // SSL 证书文件夹,根据自身情况修改 20 | | ├── common // 通用工具类文件夹,完全不用改 21 | | ├── config // 常量拆分后配置文件,若为 constant-all.js 完全不用改,若为 constant-main.js 则需要更改对应拆分文件 22 | | ├── exampleConfig // 示例 constant 配置文件夹 23 | | ├── includes // 拆分的 conf 文件夹,http 和 https 端口在这改 24 | | ├── constant.js // 常量主配置文件,根据自身情况修改 25 | │   ├── plex.conf // plex 配置文件,根据自身情况修改,注意 https 默认被注释掉了 26 | │   └── plex.js // 主脚本,完全不用改 27 | └── nginx.conf // nginx 配置文件,一般不用改 28 | ``` 29 | 30 | ### 2. 31 | 看情况修改 constant.js 中的设置项目,通常来说只需要改 alist 密码 32 | 这里默认 plex 在同一台机器并且使用 32400 端口, 33 | 34 | ### 3. docker部署的任选以下一种 35 | ### 3.1 - docker-compose 36 | 启动服务: 在 ~/plex2Alist/docker 目录下执行 37 | ```bash 38 | docker-compose up -d 39 | ``` 40 | 查看启动log: 41 | ```bash 42 | docker-compose logs -f 43 | ``` 44 | 如果log有报错,请按照提示信息修改 ,常见错误可能为 45 | 1. docker 端口占用冲突: 修改 docker-comopse 映射端口 46 | 47 | ### 3.2 - 群晖docker 48 | 容器=>设置=>导入=>选择json配置文件=>确认 49 | 50 | ### 4.1 http 51 | 防火墙放行 http(当前默认) 的 8091 端口和 https(需要自己配置证书并启用) 的 8095 为 plex 转直链端口与默认的 32400 互不影响 52 | 访问 alist,查看 token,管理=>设置=>其他=>令牌,根据项目文档 https://github.com/Xhofe/alist 在Alist项目后台添加网盘 53 | 54 | ### 4.2 https 55 | 4.2.1 更改`plex2Alist/nginx/conf.d/plex.conf`第 25-26 行,注释默认的 http 访问方式,打开 https 的访问 56 | ```js 57 | ## Include the http and https configs, better don't use same port 58 | # include /etc/nginx/conf.d/includes/http.conf; 59 | include /etc/nginx/conf.d/includes/https.conf; 60 | ``` 61 | 4.2.2 注意下`plex2Alist/nginx/conf.d/includes/https.conf`中第 12-13 行的,证书文件,证书密钥的路径位置与文件名 62 | ```js 63 | ssl_certificate /etc/nginx/conf.d/cert/fullchain.pem; ## Location of your public PEM file. 64 | ssl_certificate_key /etc/nginx/conf.d/cert/privkey.key; ## Location of your private PEM file. 65 | ``` 66 | 4.2.3 假如不更改默认的证书路径和文件名,直接将证书文件,证书密钥,改为上面的默认文件名,然后放置到`plex2Alist/nginx/conf.d/cert`目录下即可 67 | 68 | 4.2.4 配置文件更改完成后,需要终端执行`nginx -s reload`或直接重启 nginx 服务刷新配置文件生效 69 | 70 | 4.2.5 此时通过默认 https 脚本中默认的`8095`端口访问,`http.conf`中的`8091`端口已经被释放无法访问,假如是`Docker`环境,需要检查下容器是否正确映射出了`8095`到`宿主机`上,假如`路由器需要做端口转发(内部端口/宿主机 8095 到外部端口 8095)`开放在公网上,也需要检查下路由器中的设置 71 | 72 | ### 5. plex 服务端控制台设置 73 | 1.设置远程访问-手动设置公开端口为 nginx 的 8091 端口,避免客户端优先直连 32400,如果路由器做了 32400 端口转发,不影响外部访问 74 | 2.plex情况特殊,服务端必须设置-网络-自定义服务器访问 URL: [https://自己的域名:自己的端口号],以发布到 plex.tv 发现服务器, 75 | 客户端保持默认设置,不用更改高级设置-自定义ip和端口,高级设置-允许非加密连接-永不,如果plex源服务器没有配证书(也不用配),同时不要更改nginx的proxy_pass 值,也就是 constant.js 中的 plexHost 值,保持[http://]开头 76 | 77 | ### 6. plex 客户端设置 78 | 关闭自动质量调节,所有串流改为最高质量或原始,根据自身流量套餐情况选择关闭数据流量下的带宽限制,如限制为 WIFI 网络或关闭允许低码率质量 79 | 80 | ### 7. 测试是否成功 81 | 访问 8091 端口打开 plex 测试直链是否生效,查看执行 log 82 | ```bash 83 | docker logs -f -n 10 nginx-plex 2>&1 | grep js: 84 | ``` 85 | 或者直接查看 ../nginx/log 容器映射出来的原始 nginx error.log 业务日志 86 | 8091 端口为走直链端口 , 原本的 32400 端口 走 plex server 不变 87 | 最好在 plex 设置中将所有 互联网质量 设置为最高, 88 | web 端各大浏览器对音频和视频编码支持情况不一,碰到不支持的情况 plex 会强制走转码而不会走直链 89 | 90 | ### 8. 补充说明 91 | 鉴于 plex 除了 web 外各客户端对于不安全内容 http 默认都是禁止状态,故强烈建议配置域名和证书, 92 | 具体查看 plex2Alist/nginx/conf.d/includes/https.conf 文件,注意端口为 8095,且需要放开 93 | plex2Alist/nginx/conf.d/plex.conf 中的注释 # include /etc/nginx/conf.d/includes/https.conf; 94 | 95 | ## 已知问题: 96 | 1.PlexWeb 自身存在很多问题,不支持 DTS 的直接播放,不支持所有非内嵌字幕的直接播放,使用起来就是已经直连云盘在接收数据了,但是 Web 播放器卡住不动,页签内存过载就直接白屏了,Web 只支持简单格式的内嵌字幕视频,解决方案为使用对应平台的客户端,经测试,安卓客户端是没问题的。 97 | ![47dc8412c5d0b46aac999d4c3aae36ae](https://github.com/bpking1/embyExternalUrl/assets/42368856/625731e4-a8b9-46f9-b511-96aba4498485) 98 | ![baddd39444f9ca6f2069209c2f73f9d](https://github.com/bpking1/embyExternalUrl/assets/42368856/47be6ced-e630-460b-9a7d-bfdedc795907) 99 | ![cf1b5edc12c2fd2b52eed17ab3afc85](https://github.com/bpking1/embyExternalUrl/assets/42368856/8bd44d0e-3761-4dc8-b86e-fa3baf8163b6) 100 | 101 | 2.各客户端对非默认端口的支持不同 102 | - 例如安卓客户端打开客户端高级设置-允许非加密连接-始终,服务端自定义了网络发现地址的不用填写服务器ip 103 | - IOS 客户端强制禁止非 https 连接,只能给 nginx 配域名和证书,conf示例里有配置注释 104 | 105 | ~~3.经测试plex对strm文件仅限于显示元信息,不具备播放条件~~ 106 | 107 | 4.可能会有其他问题,请留言 108 | -------------------------------------------------------------------------------- /plex2Alist/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | service.nginx-plex: 4 | image: nginx:1.27.1 5 | container_name: nginx-plex 6 | # 需要 Docker Swarm 模式 7 | # deploy: 8 | # resources: 9 | # limits: 10 | # cpus: '0.50' 11 | # memory: 128M 12 | # 更改为默认host网络,纠正流量标识,直接占用宿主机端口,docker层会禁用端口映射 13 | network_mode: host 14 | # 如果需要使用bridge(桥接)网络,请取消ports(端口映射)注释,并注释network_mode 15 | # 端口映射规则为,宿主机端口:容器内部端口 16 | # ports: 17 | # - 8091:8091 18 | # - 8095:8095 19 | # privileged: true 20 | volumes: 21 | - ../nginx/nginx.conf:/etc/nginx/nginx.conf 22 | - ../nginx/conf.d:/etc/nginx/conf.d 23 | - ../nginx/log:/var/log/nginx 24 | restart: always 25 | # depends_on: 26 | # - service.ali 27 | 28 | # service.ali: 29 | # image: xhofe/alist:latest 30 | # container_name: alist 31 | # ports: 32 | # - 5244:5244 33 | # volumes: 34 | # - ./alist:/opt/alist/data 35 | # restart: always 36 | 37 | # service.ali-webdav: 38 | # image: messense/aliyundrive-webdav 39 | # container_name: ali-webdav 40 | # ports: 41 | # - 8080:8080 42 | # volumes: 43 | # - ./aliyundrive-webdav/:/etc/aliyundrive-webdav/ 44 | # environment: 45 | # - REFRESH_TOKEN=1111111111111aaaaaaaaaa 46 | # - WEBDAV_AUTH_USER=admin 47 | # - WEBDAV_AUTH_PASSWORD=admin 48 | # restart: always 49 | -------------------------------------------------------------------------------- /plex2Alist/docker/nginx-plex.syno.json: -------------------------------------------------------------------------------- 1 | { 2 | "CapAdd" : null, 3 | "CapDrop" : null, 4 | "cmd" : "nginx -g daemon\\ off\\;", 5 | "cpu_priority" : 50, 6 | "enable_publish_all_ports" : false, 7 | "enable_restart_policy" : true, 8 | "enabled" : false, 9 | "entrypoint_default" : "/docker-entrypoint.sh", 10 | "env_variables" : [ 11 | { 12 | "key" : "TZ", 13 | "value" : "Asia/Shanghai" 14 | } 15 | ], 16 | "exporting" : false, 17 | "image" : "nginx:1.27.1", 18 | "is_ddsm" : false, 19 | "is_package" : false, 20 | "links" : [], 21 | // 128MB 22 | "memory_limit" : 134217728, 23 | "name" : "nginx-plex", 24 | // 更改为默认host网络,纠正流量标识,直接占用宿主机端口,docker层会禁用端口映射 25 | "network_mode" : "host", 26 | // "network_mode" : "bridge", 27 | // 如果需要使用bridge(桥接)网络,请取消ports(端口映射)注释,并修改network_mode为bridge 28 | // 端口映射规则为,host_port(宿主机端口),container_port(容器内部端口) 29 | "port_bindings" : [ 30 | // { 31 | // "host_port" : 8091, 32 | // "container_port" : 8091, 33 | // "type" : "tcp" 34 | // }, 35 | // { 36 | // "host_port" : 8095, 37 | // "container_port" : 8095, 38 | // "type" : "tcp" 39 | // } 40 | ], 41 | "privileged" : false, 42 | "shortcut" : { 43 | "enable_shortcut" : false, 44 | "enable_status_page" : false, 45 | "enable_web_page" : false, 46 | "web_page_url" : "" 47 | }, 48 | // "use_host_network" : true, 49 | "volume_bindings" : [ 50 | { 51 | "host_volume_file" : "/docker/nginx-plex/log", 52 | "mount_point" : "/var/log/nginx", 53 | "type" : "rw" 54 | }, 55 | { 56 | "host_volume_file" : "/docker/nginx-plex/config/conf.d", 57 | "mount_point" : "/etc/nginx/conf.d", 58 | "type" : "rw" 59 | }, 60 | { 61 | "host_volume_file" : "/docker/nginx-plex/config/nginx.conf", 62 | "mount_point" : "/etc/nginx/nginx.conf", 63 | "type" : "rw" 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/api/alist-api.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-04-16 3 | 4 | // import util from "../common/util.js"; 5 | import urlUtil from "../common/url-util.js"; 6 | 7 | const API_ENUM = { 8 | fsGet: "/api/fs/get", 9 | fsLink: "/api/fs/link", 10 | fsList: "/api/fs/list", 11 | }; 12 | 13 | async function fetchAuth(url, username, password) { 14 | const body = { 15 | username: username, 16 | password: password, 17 | }; 18 | try { 19 | const response = await ngx.fetch(url, { 20 | method: "POST", 21 | max_response_body_size: 1024, 22 | body: JSON.stringify(body), 23 | }); 24 | if (response.ok) { 25 | const result = await response.json(); 26 | if (!result) { 27 | return `error: alist_auth_api response is null`; 28 | } 29 | if (result.message == "success") { 30 | return result.data.token; 31 | } 32 | return `error500: alist_auth_api ${result.code} ${result.message}`; 33 | } else { 34 | return `error: alist_auth_api ${response.status} ${response.statusText}`; 35 | } 36 | } catch (error) { 37 | return `error: alist_auth_api filed ${error}`; 38 | } 39 | } 40 | 41 | async function fetchPath(url, filePath, token, ua) { 42 | const requestBody = { 43 | path: filePath, 44 | password: "", 45 | }; 46 | try { 47 | const urlParts = urlUtil.parseUrl(url); 48 | const hostValue = `${urlParts.host}:${urlParts.port}`; 49 | ngx.log(ngx.WARN, `fetchAlistPath add Host: ${hostValue}`); 50 | const response = await ngx.fetch(url, { 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json;charset=utf-8", 54 | Authorization: token, 55 | "User-Agent": ua, 56 | Host: hostValue, 57 | }, 58 | max_response_body_size: 65535, 59 | body: JSON.stringify(requestBody), 60 | }); 61 | if (response.ok) { 62 | const result = await response.json(); 63 | if (!result) { 64 | return `error: alist_path_api response is null`; 65 | } 66 | if (result.message == "success") { 67 | return result; 68 | } 69 | if (result.code == 403) { 70 | return `error403: alist_path_api ${result.message}`; 71 | } 72 | return `error500: alist_path_api ${result.code} ${result.message}`; 73 | } else { 74 | return `error: alist_path_api ${response.status} ${response.statusText}`; 75 | } 76 | } catch (error) { 77 | return `error: alist_path_api fetchAlistFiled ${error}`; 78 | } 79 | } 80 | 81 | export default { 82 | API_ENUM, 83 | fetchAuth, 84 | fetchPath, 85 | }; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/common/events.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-04-19 3 | // NJS events 4 | 5 | function njsOnExit(mark, callbacks, r) { 6 | njsOnBeginNotice(mark); 7 | const eventName = "exit"; 8 | if (callbacks && Array.isArray(callbacks)) { 9 | callbacks.map(callback => { 10 | njsOn(eventName, callback); 11 | }); 12 | } 13 | njsOn(eventName, () => { 14 | njsOnExitNotice(mark); 15 | }); 16 | } 17 | 18 | function njsOn(eventName, callback) { 19 | njs.on(eventName, callback); 20 | } 21 | 22 | function njsOnBeginNotice(mark) { 23 | ngx.log(ngx.WARN, `=== ${mark}, the NJS VM is beginning ===`); 24 | } 25 | 26 | function njsOnExitNotice(mark) { 27 | ngx.log(ngx.WARN, `=== ${mark}, the NJS VM is destroyed ===`); 28 | } 29 | 30 | export default { 31 | njsOnExit, 32 | njsOn, 33 | }; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/common/periodics.js: -------------------------------------------------------------------------------- 1 | // @author: Chen3861229 2 | // @date: 2024-04-12 3 | // global schedule task 4 | 5 | // import config from "./constant.js"; 6 | // import util from "./util.js"; 7 | 8 | const fs = require("fs"); 9 | 10 | async function logHandler(s) { 11 | const errorLogPath = ngx.error_log_path; 12 | if (!fs.existsSync(errorLogPath)) { 13 | return; 14 | } 15 | const timeLocal = s.variables["time_local"]; 16 | // const dateLocal = s.variables["date_local"]; 17 | fs.writeFileSync(errorLogPath, ""); 18 | ngx.log(ngx.WARN, `cleared by periodics.logHandler`); 19 | 20 | const accessLogPath = errorLogPath.replace("error", "access"); 21 | if (fs.existsSync(accessLogPath)) { 22 | fs.writeFileSync(accessLogPath, `${timeLocal}: js: cleared by periodics.logHandler\n`); 23 | } 24 | } 25 | 26 | export default { 27 | logHandler, 28 | }; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/common/url-util.js: -------------------------------------------------------------------------------- 1 | // @author: Ambitious 2 | // @date: 2023-09-04 3 | 4 | function proxyUri(uri) { 5 | return `/proxy${uri}`; 6 | } 7 | 8 | function appendUrlArg(u, k, v) { 9 | if (u.includes(k)) { 10 | return u; 11 | } 12 | return u + (u.includes("?") ? "&" : "?") + `${k}=${v}`; 13 | } 14 | 15 | function generateUrl(r, host, uri, skipKeys) { 16 | skipKeys = skipKeys ?? []; 17 | let url = host + uri; 18 | let isFirst = true; 19 | for (const key in r.args) { 20 | if (skipKeys.includes(key)) { 21 | continue; 22 | } 23 | url += isFirst ? "?" : "&"; 24 | url += `${key}=${r.args[key]}`; 25 | isFirst = false; 26 | } 27 | return url; 28 | } 29 | 30 | function getCurrentRequestUrlPrefix(r) { 31 | return `${r.variables.scheme}://${r.headersIn["Host"]}`; 32 | } 33 | 34 | /** 35 | * 1.CloudDrive with params 36 | * http://mydomain:19798/static/http/mydomain:19798/False//AList/xxx.mkv?aaa=bbb 37 | * 2.AList with params 38 | * http://mydomain:5244/d/AList/xxx.mkv?aaa=bbb 39 | * see: https://regex101.com/r/Gd3JUH/2 40 | * @param {String} url full url 41 | * @returns "/AList/xxx.mkv" or "AList/xxx.mkv" or "" 42 | */ 43 | function getFilePathPart(url) { 44 | const matches = url.match(/(?:\/False\/|\/d\/)(.*?)(?:\?|$)/); 45 | return matches ? matches[1] : ""; 46 | } 47 | 48 | /** 49 | * Parses the URL and returns an object with various components. 50 | * @param {string} url The URL string to parse. 51 | * @returns {Object} An object containing protocol, username, password, host, port, pathname, search, and hash. 52 | */ 53 | function parseUrl(url) { 54 | const regex = /^(?:(\w+)?:\/\/)?(?:(\w+):(\w+)@)?(?:www\.)?([^:\/\n?#]+)(?::(\d+))?(\/[^?\n]*)?(\?[^#\n]*)?(#.*)?$/i; 55 | const match = url.match(regex); 56 | if (match) { 57 | const protocol = match[1] || 'http'; 58 | const username = match[2] || ''; 59 | const password = match[3] || ''; 60 | const host = match[4]; 61 | const port = match[5] || ''; 62 | const pathname = match[6] || ''; 63 | const search = match[7] || ''; 64 | const hash = match[8] || ''; 65 | const fullProtocol = `${protocol}:`; 66 | const fullPort = port || (fullProtocol === 'https:' ? '443' : '80'); 67 | return { 68 | protocol: fullProtocol, 69 | username, 70 | password, 71 | host, 72 | port: fullPort, 73 | pathname, 74 | search, 75 | hash 76 | }; 77 | } 78 | return null; 79 | } 80 | 81 | function getRealIp(r) { 82 | const headers = r.headersIn; 83 | const ip = headers["X-Forwarded-For"] || 84 | headers["X-Real-IP"] || 85 | headers["Proxy-Client-IP"] || 86 | headers["Proxy-Client-IP"] || 87 | headers["WL-Proxy-Client-IP"] || 88 | headers["HTTP_CLIENT_IP"] || 89 | headers["HTTP_X_FORWARDED_FOR"] || 90 | r.variables.remote_addr; 91 | return ip; 92 | } 93 | 94 | export default { 95 | proxyUri, 96 | appendUrlArg, 97 | generateUrl, 98 | getCurrentRequestUrlPrefix, 99 | getFilePathPart, 100 | parseUrl, 101 | getRealIp, 102 | } -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-common.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,程序内部使用的公共常量 3 | 4 | // 字符串头,用于特殊匹配判断 5 | const strHead = { 6 | lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头 7 | xEmbyClients: { 8 | seekBug: ["Emby for iOS"], 9 | }, 10 | xUAs: { 11 | seekBug: ["Infuse", "VidHub", "SenPlayer"], 12 | clientsPC: ["EmbyTheater"], 13 | clients3rdParty: ["Fileball", "Infuse", "SenPlayer", "VidHub"], 14 | player3rdParty: ["dandanplay", "VLC", "MXPlayer", "PotPlayer"], 15 | blockDownload: ["Infuse-Download"], 16 | infuse: { 17 | direct: "Infuse-Direct", 18 | download: "Infuse-Download", 19 | }, 20 | // 安卓与 TV 客户端不太好区分,浏览器 UA 关键字也有交叉重叠,请使用 xEmbyClients 参数或使用正则 21 | }, 22 | "115": ["115.com", "115cdn.net"], 23 | ali: ["aliyundrive.net"], 24 | userIds: { 25 | mediaPathMappingGroup01: ["ac0d220d548f43bbb73cf9b44b2ddf0e"], 26 | allowInteractiveSearch: [], 27 | }, 28 | filePaths: { 29 | mediaMountPath: [], 30 | redirectStrmLastLinkRule: [], 31 | mediaPathMappingGroup01: [], 32 | }, 33 | }; 34 | 35 | // 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配) 36 | // 参数2: 匹配类型或来源(字符串参数类型),默认为 "filePath": 本地文件为路径,strm 为远程链接 37 | // ,有分组时不可省略填写,可为表达式 38 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 39 | // ,分组时建议写 "startsWith" 这样的字符串,方便日志中排错 40 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 41 | const ruleRef = { 42 | // 这个 key 值仅仅只是代码中引用的可读性标识,需见名知意,可自定义 43 | // mediaPathMappingGroup01: [ 44 | // ["mediaPathMappingGroup01", "filePath", "startsWith", strHead.filePaths.mediaPathMappingGroup01], // 目标地址 45 | // ["mediaPathMappingGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 46 | // ["mediaPathMappingGroup01", "r.args.UserId", "startsWith", strHead.userIds.mediaPathMappingGroup01], 47 | // ], 48 | }; 49 | 50 | export default { 51 | strHead, 52 | ruleRef, 53 | } 54 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-ext.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,特定平台功能,用不到保持默认即可 3 | 4 | export default { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-mount.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | 3 | const strHead = commonConfig.strHead; 4 | 5 | // 选填项,用不到保持默认即可 6 | 7 | // rclone/CD2 挂载的 alist 文件配置,根据实际情况修改下面的设置 8 | // 访问宿主机上 5244 端口的 alist 地址, 要注意 iptables 给容器放行端口 9 | const alistAddr = "http://172.17.0.1:5244"; 10 | 11 | // alist token, 在 alist 后台查看 12 | const alistToken = "alsit-123456"; 13 | 14 | // alist 是否启用了 sign 15 | const alistSignEnable = false; 16 | 17 | // alist 中设置的直链过期时间,以小时为单位,严格对照 alist 设置 => 全局 => 直链有效期 18 | const alistSignExpireTime = 12; 19 | 20 | // alist 公网地址,用于需要 alist server 代理流量的情况,按需填写 21 | const alistPublicAddr = "http://youralist.com:5244"; 22 | 23 | // 指定客户端自己请求并获取 alist 直链的规则,代码优先级在 redirectStrmLastLinkRule 之后 24 | // 特殊情况使用,则此处必须使用域名且公网畅通,用不着请保持默认 25 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 26 | // 参数?.2: 匹配类型或来源(字符串参数类型),优先级高"filePath": 文件路径(Item.Path),默认为"alistRes": alist 返回的链接 raw_url 27 | // ,有分组时不可省略填写,可为表达式,然后下面参数序号-1 28 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 29 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 30 | // 参数5: 指定转发给客户端的 alist 的 host 前缀,兼容 sign 参数 31 | const clientSelfAlistRule = [ 32 | // Infuse 客户端对于 115 的进度条拖动可能依赖于此 33 | // 如果 nginx 为 https,则此 alist 也必须 https,浏览器行为客户端会阻止非 https 请求 34 | [2, strHead["115"], alistPublicAddr], 35 | // [2, strHead.ali, alistPublicAddr], 36 | // 优先使用 filePath,可省去一次查询 alist,如驱动为 alias,则应使用 alistRes 37 | // ["115-local", "filePath", 0, "/mnt/115", alistPublicAddr], 38 | // ["115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 39 | // ["115-alist", "alistRes", 2, strHead["115"], alistPublicAddr], 40 | // ["115-alist", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], 41 | ]; 42 | 43 | // 响应重定向链接前是否检测有效性,无效链接时转给媒体服务器回源中转处理 44 | const redirectCheckEnable = false; 45 | 46 | // 媒体服务/alist 查询失败后是否使用原始链接回源中转流量处理,如无效则直接返回 500 47 | const fallbackUseOriginal = true; 48 | 49 | export default { 50 | alistAddr, 51 | alistToken, 52 | alistSignEnable, 53 | alistSignExpireTime, 54 | alistPublicAddr, 55 | clientSelfAlistRule, 56 | redirectCheckEnable, 57 | fallbackUseOriginal, 58 | } 59 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-pro.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | import mountConfig from "./constant-mount.js"; 3 | 4 | const strHead = commonConfig.strHead; 5 | const ruleRef = commonConfig.ruleRef; 6 | const alistPublicAddr = mountConfig.alistPublicAddr; 7 | 8 | // 选填项,高级配置,用不到保持默认即可 9 | 10 | // 重定向/直链开关配置,关闭的效果为还原为严格反代逻辑,即中转上游源服务流量 11 | // 此为粗颗粒度控制,优先级最高,细颗粒控制依旧使用路由规则管理 12 | const redirectConfig = { 13 | enable: true, // 允许直链的总开关,false 等同覆盖下列所有为 false 14 | enablePartStreamPlayOrDownload: true, // 允许串流播放或下载的直链 15 | enableVideoTranscodePlay: true, // 允许转码播放的直链 16 | }; 17 | 18 | // 路由缓存配置 19 | const routeCacheConfig = { 20 | // 总开关,是否开启路由缓存,此为一级缓存,添加阶段为 redirect 和 proxy 之前 21 | // 短时间内同客户端访问相同资源不会再做判断和请求 alist,有限的防抖措施,出现问题可以关闭此选项 22 | enable: true, 23 | // 二级缓存开关,仅针对直链,添加阶段为进入单集详情页,clientSelfAlistRule 中的和首页直接播放的不生效 24 | // 非 web 端且限 UA 的不建议使用,生效率太低,因部分客户端详情页 UA 和播放器 UA 存在不同的情况 25 | enableL2: false, 26 | // 缓存键表达式,默认值好处是命中范围大,但会导致 routeRule 中针对设备的规则失效,多个变量可自行组合修改,冒号分隔 27 | keyExpression: "r.uri:r.args.path:r.args.mediaIndex:r.args.partIndex", //"xxx:r.args.X-Plex-Client-Identifier" 28 | }; 29 | 30 | // 路由规则,注意顺序是从上至下匹配,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内 31 | // 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1 32 | // "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链302, "block": 屏蔽媒体播放和下载 33 | // pelx 不需要 "transcode", 可用 "proxy" 代替,稍微有些歧义,这里只是不做修改,交给原始服务中转处理,具体是否转码由 plex 客户端自己判断上报的 34 | // "block": 屏蔽媒体播放和下载, "blockDownload": 只屏蔽下载, "blockPlay": 只屏蔽播放, 35 | // 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 36 | // 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接 37 | // 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 38 | // 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 39 | const routeRule = [ 40 | // ["filePath", 0, "/mnt/sda1"], 41 | // ["filePath", 1, ".mp3"], 42 | // ["filePath", 2, "Google"], 43 | // ["alistRes", 2, "/NAS/"], // 例如使用 alias 聚合了 nas 本地文件,可能会存在卡顿或花屏 44 | // ["filePath", 3, /private/ig], 45 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 46 | // ["r.variables.remote_addr", 0, strHead.lanIp], 47 | // ["r.headersIn.User-Agent", 2, "IE"], // 请求头参数,客户端UA 48 | 49 | // 可参照 Emby 配置文件,但受限于脚本对于 Plex 获取文件路径的缓存实现,目前可能 XMedia 对象为空,只能使用接口入参 50 | // 注意设备id具有唯一性,不会跟随切换用户变更,Plex 接口入参不存在用户id,取值参考 /library/metadata/xxx 接口的出入参数 51 | // 高级分组规则,XMedia 为固定值,等于 Plex.MediaContainer.Metadata[0].Media[目标索引] 对象 52 | // r.XMedia.Part.0.Stream 数组比较特殊,路由规则暂未做关键词抽取,但目前匹配视频流规则够用,多音频/字幕流不太好写规则 53 | // r.XMedia.Part.0.Stream.0 一般为视频流对象,不支持 r.XMedia.Part[0].Stream[0] 写法 54 | 55 | // 此条规则代表大于等于 3Mbps 码率的允许转码,平方使用双星号表示,无意义加减仅为示例,注意 plex 码率为 kbps 单位 56 | // ["transcode", "高码率允许转码01", "r.XMedia.bitrate", ">=", 3 * 1000 + (1 * 1000) - (1 * 1000)], 57 | // 可选规则,结合上条规则做分组,同时满足才能生效,否则继续向下匹配 58 | // ["transcode", "高码率允许转码01", "r.args.X-Plex-Client-Identifier", "===", ["设备id01", "设备id02"]], 59 | // 此条规则代表 4K 分辨率的允许转码,但假如设备自身上报和上游决定走转码,不满足的也会转码,遵守上游倾向为播放成功率考虑 60 | // ["transcode", "高分辨率允许转码01", "r.XMedia.Part.0.Stream.0.displayTitle", "includes", "4K"], 61 | // 可选替换上条规则,更精确的分辨率规则,例如 21:9 视频,或某些 2.5 K 视频等不在标准分辨率划分内的 62 | // ["transcode", "高分辨率允许转码01", "r.XMedia.Part.0.Stream.0.width", ">=", 4320], 63 | // 精确屏蔽指定功能,注意同样是整体规则都不匹配默认走"redirect",即不屏蔽,建议只用下方一条,太复杂的话需要自行测试 64 | // ["blockDownload", "屏蔽下载01", "r.headersIn.User-Agent", "includes", strHead.xUAs.blockDownload], 65 | // 非必须,该分组内细分为入库路径黑名单,结合上面两条代表 "屏蔽指定标识客户端的非指定用户的指定入库路径的下载" 66 | // ["blockDownload", "屏蔽下载01", "filePath", "startsWith", ["/mnt/115"]], 67 | ]; 68 | 69 | // 路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍,不要有重叠,注意 /mnt 会先被移除掉了 70 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 71 | // 参数1: 0: 默认做字符串替换 replace 一次, 1: 前插, 2: 尾插, 3: replaceAll 替换全部 72 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 73 | // 参数3: 来源, 参数4: 目标 74 | const mediaPathMapping = [ 75 | // [0, 0, "/aliyun-01", "/aliyun-02"], 76 | // [0, 2, "http:", "https:"], 77 | // [0, 2, ":5244", "/alist"], 78 | // [0, 0, "D:", "F:"], 79 | // [0, 0, /blue/g, "red"], // 此处正则不要加引号 80 | // [1, 1, `${alistPublicAddr}/d`], 81 | // [2, 2, "?xxx"], 82 | // 此条是一个规则变量引用,方便将规则汇合到同一处进行管理 83 | // [ruleRef.mediaPathMappingGroup01, 0, 0, "/aliyun-01", "/aliyun-02"], 84 | // 路径映射多条规则会从上至下依次执行,如下有同一个业务关系集的,注意带上区间的闭合条件,不然会被后续重复替换会覆盖 85 | // 以下是按码率条件进行路径映射,全用户设备强制,区分用户和设备可再精确添加条件 86 | // [[["4K 目录映射到 1080P 目录", "r.XMedia.bitrate", ">", 10 * 1000], 87 | // ], 0, 0, "/4K/", "/1080P/"], 88 | // [[["1080P 目录映射到 720P 目录", "r.XMedia.bitrate", ">", 6 * 1000], 89 | // ["1080P 目录映射到 720P 目录", "r.XMedia.bitrate", "<=", 10 * 1000], 90 | // ], 0, 0, "/1080P/", "/720P/"], 91 | // [[["720P 目录映射到 480P 目录", "r.XMedia.bitrate", ">", 3 * 1000], 92 | // ["720P 目录映射到 480P 目录", "r.XMedia.bitrate", "<=", 6 * 1000], 93 | // ], 0, 0, "/720P/", "/480P/"], 94 | ]; 95 | 96 | // 仅针对 alist 返回的 raw_url 进行路径映射,优先级在 mediaPathMapping 和 clientSelfAlistRule 后,使用方法一样 97 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 98 | // 参数1: 0: 默认做字符串替换replace一次, 1: 前插, 2: 尾插, 3: replaceAll替换全部 99 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 100 | // 参数3: 来源, 参数4: 目标 101 | const alistRawUrlMapping = [ 102 | // [0, 0, "/alias/movies", "/aliyun-01"], 103 | ]; 104 | 105 | export default { 106 | redirectConfig, 107 | routeCacheConfig, 108 | routeRule, 109 | mediaPathMapping, 110 | alistRawUrlMapping, 111 | } 112 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-strm.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "./constant-common.js"; 2 | import mountConfig from "./constant-mount.js"; 3 | 4 | const strHead = commonConfig.strHead; 5 | const alistAddr = mountConfig.alistAddr; 6 | const alistToken = mountConfig.alistToken; 7 | const alistSignExpireTime = mountConfig.alistSignExpireTime; 8 | 9 | // 只使用 strm 文件配置模板,即标准 strm 内部只有远程链接,不存在/开头的相对路径 10 | // 不需要挂载功能,不显示依赖 alist,strm 内部为任意直链 11 | 12 | // 选填项,用不到保持默认即可 13 | 14 | // 指定是否转发由 njs 获取 strm/远程链接 重定向后直链地址的规则,例如 strm/远程链接 内部为局域网 ip 或链接需要验证 15 | // 匹配来源为入库媒体的文件路径 16 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 17 | // 参数?.2: 匹配类型或来源(字符串参数类型),默认为 "filePath": mediaPathMapping 映射后的 strm/远程链接 内部链接 18 | // ,有分组时不可省略填写,可为表达式 19 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 20 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 21 | const redirectStrmLastLinkRule = [ 22 | [0, strHead.lanIp.map(s => "http://" + s)], 23 | // [0, alistAddr], 24 | // [0, "http:"], 25 | // 参数5: 请求验证类型,当前 alistAddr 不需要此参数 26 | // 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime 27 | // [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`], 28 | // useGroup01 同时满足才命中 29 | // ["useGroup01", "filePath", "startsWith", ["https://youdomain.xxx.com:88"]], // 目标地址 30 | // ["useGroup01", "r.headersIn.User-Agent", "startsWith:not", ["Infuse"]], // 链接入参,客户端类型 31 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 32 | // ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网 33 | ]; 34 | 35 | export default { 36 | redirectStrmLastLinkRule, 37 | } 38 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-symlink.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,用不到保持默认即可 3 | 4 | // 指定需要获取符号链接真实路径的规则,优先级在 mediaMountPath 和 routeRule 之间 5 | // 注意前提条件是此程序或容器必须挂载或具有对应目录的读取权限,否则将跳过处理,回源中转 6 | // 此参数仅在软链接后的文件名和原始文件名不一致或路径差异较大时使用,其余情况建议用 mediaPathMapping 7 | // 参数1: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 8 | // 参数2: 匹配目标,对象为媒体服务入库的文件路径(Item.Path) 9 | const symlinkRule = [ 10 | // [0, "/mnt/sda1"], 11 | ]; 12 | 13 | export default { 14 | symlinkRule, 15 | } 16 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/config/constant-transcode.js: -------------------------------------------------------------------------------- 1 | 2 | // 选填项,用不到保持默认即可 3 | 4 | // 转码配置,默认 false,将按之前逻辑强制直接播放 5 | // plex 只能用自身服务转码,只有下面一个参数,多填写没用 6 | const transcodeConfig = { 7 | enable: false, // 此为允许转码的总开关 8 | }; 9 | 10 | export default { 11 | transcodeConfig, 12 | } 13 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/constant.js: -------------------------------------------------------------------------------- 1 | // 如果使用拆分配置,请注意填写 config 下的使用到的配置文件 2 | 3 | import commonConfig from "./config/constant-common.js"; 4 | import mountConfig from "./config/constant-mount.js"; 5 | import proConfig from "./config/constant-pro.js"; 6 | import symlinkConfig from "./config/constant-symlink.js"; 7 | import strmConfig from "./config/constant-strm.js"; 8 | import transcodeConfig from "./config/constant-transcode.js"; 9 | // import extConfig from "./config/constant-ext.js"; 10 | 11 | // 必填项,根据实际情况修改下面的设置 12 | 13 | // 这里默认 plex 的地址是宿主机,要注意 iptables 给容器放行端口 14 | const plexHost = "http://172.17.0.1:32400"; 15 | 16 | // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt 17 | // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 plex 处理 18 | const mediaMountPath = ["/mnt"]; 19 | 20 | // for js_set 21 | function getPlexHost(r) { 22 | return plexHost; 23 | } 24 | function getTranscodeEnable(r) { 25 | return transcodeConfig.transcodeConfig.enable; 26 | } 27 | 28 | export default { 29 | plexHost, 30 | mediaMountPath, 31 | strHead: commonConfig.strHead, 32 | 33 | alistAddr: mountConfig.alistAddr, 34 | alistToken: mountConfig.alistToken, 35 | alistSignEnable: mountConfig.alistSignEnable, 36 | alistSignExpireTime: mountConfig.alistSignExpireTime, 37 | alistPublicAddr: mountConfig.alistPublicAddr, 38 | clientSelfAlistRule: mountConfig.clientSelfAlistRule, 39 | redirectCheckEnable: mountConfig.redirectCheckEnable, 40 | fallbackUseOriginal: mountConfig.fallbackUseOriginal, 41 | 42 | redirectConfig: proConfig.redirectConfig, 43 | routeCacheConfig: proConfig.routeCacheConfig, 44 | routeRule: proConfig.routeRule, 45 | mediaPathMapping: proConfig.mediaPathMapping, 46 | alistRawUrlMapping: proConfig.alistRawUrlMapping, 47 | 48 | symlinkRule: symlinkConfig.symlinkRule, 49 | redirectStrmLastLinkRule: strmConfig.redirectStrmLastLinkRule, 50 | transcodeConfig: transcodeConfig.transcodeConfig, 51 | 52 | getPlexHost, 53 | getTranscodeEnable, 54 | } 55 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/custome/location/default.conf: -------------------------------------------------------------------------------- 1 | 2 | # 1.location 默认为精确匹配,优先级最高; 3 | # 2."^~": 前缀匹配,不支持正则,优先级高于正则匹配,低于精确匹配; 4 | # 3."~": 区分大小写路径正则匹配,优先级最低; 5 | # 4."~*": 不区分大小写路径正则匹配,优先级最低; 6 | # 5.请求路径带 / 结尾: 匹配目录,自动追加斜杠; 不带 / 结尾: 匹配请求或文件或目录,不自动追加斜杠; 7 | 8 | # location ~* /Sync/JobItems/(.*)/File { 9 | # } 10 | 11 | # 切断客户端与上游服务器的 304 缓存协商 12 | # location ~* \.(js|css)$ { 13 | # proxy_set_header Cache-Control "no-cache"; 14 | # add_header X-Mark "forced Cache-Control no-cache"; 15 | # proxy_pass $emby; 16 | # } -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/exampleConfig/constant-all.js: -------------------------------------------------------------------------------- 1 | // 这个总配置单体文件只是备份,生效需要放置在 conf.d 下,且重命名为 constant.js 2 | // 如果使用这个全量总配置文件,忽略 config 下的所有文件,以这个文件为准 3 | 4 | // 全量配置,媒体库混合,本地文件 + rclone/CD2 挂载的 alist 文件 + strm文件 + 软链接(路径和文件名不一致) 5 | // export constant allocation 6 | 7 | // 必填项,根据实际情况修改下面的设置 8 | 9 | // 这里默认 plex 的地址是宿主机,要注意 iptables 给容器放行端口 10 | const plexHost = "http://172.17.0.1:32400"; 11 | 12 | // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt 13 | // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 plex 处理 14 | const mediaMountPath = ["/mnt"]; 15 | 16 | // 访问宿主机上 5244 端口的 alist 地址, 要注意 iptables 给容器放行端口 17 | const alistAddr = "http://172.17.0.1:5244"; 18 | 19 | // alist token, 在 alist 后台查看 20 | const alistToken = "alsit-123456"; 21 | 22 | // alist 是否启用了 sign 23 | const alistSignEnable = false; 24 | 25 | // alist 中设置的直链过期时间,以小时为单位,严格对照 alist 设置 => 全局 => 直链有效期 26 | const alistSignExpireTime = 12; 27 | 28 | // 选填项,用不到保持默认即可 29 | 30 | // alist 公网地址, 用于需要 alist server 代理流量的情况, 按需填写 31 | const alistPublicAddr = "http://youralist.com:5244"; 32 | 33 | // 字符串头,用于特殊匹配判断 34 | const strHead = { 35 | lanIp: ["172.", "10.", "192.", "[fd00:"], // 局域网ip头 36 | xEmbyClients: { 37 | seekBug: ["Emby for iOS"], 38 | }, 39 | xUAs: { 40 | seekBug: ["Infuse", "VidHub", "SenPlayer"], 41 | clientsPC: ["EmbyTheater"], 42 | clients3rdParty: ["Fileball", "Infuse", "SenPlayer", "VidHub"], 43 | player3rdParty: ["dandanplay", "VLC", "MXPlayer", "PotPlayer"], 44 | blockDownload: ["Infuse-Download"], 45 | infuse: { 46 | direct: "Infuse-Direct", 47 | download: "Infuse-Download", 48 | }, 49 | // 安卓与 TV 客户端不太好区分,浏览器 UA 关键字也有交叉重叠,请使用 xEmbyClients 参数或使用正则 50 | }, 51 | "115": ["115.com", "115cdn.net"], 52 | ali: ["aliyundrive.net"], 53 | userIds: { 54 | mediaPathMappingGroup01: ["ac0d220d548f43bbb73cf9b44b2ddf0e"], 55 | allowInteractiveSearch: [], 56 | }, 57 | filePaths: { 58 | mediaMountPath: [], 59 | redirectStrmLastLinkRule: [], 60 | mediaPathMappingGroup01: [], 61 | }, 62 | }; 63 | 64 | // 参数1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配) 65 | // 参数2: 匹配类型或来源(字符串参数类型),默认为 "filePath": 本地文件为路径,strm 为远程链接 66 | // ,有分组时不可省略填写,可为表达式 67 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 68 | // ,分组时建议写 "startsWith" 这样的字符串,方便日志中排错 69 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 70 | const ruleRef = { 71 | // 这个 key 值仅仅只是代码中引用的可读性标识,需见名知意,可自定义 72 | // mediaPathMappingGroup01: [ 73 | // ["mediaPathMappingGroup01", "filePath", "startsWith", strHead.filePaths.mediaPathMappingGroup01], // 目标地址 74 | // ["mediaPathMappingGroup01", "r.args.X-Emby-Client", "startsWith:not", strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 75 | // ["mediaPathMappingGroup01", "r.args.UserId", "startsWith", strHead.userIds.mediaPathMappingGroup01], 76 | // ], 77 | }; 78 | 79 | // 重定向/直链开关配置,关闭的效果为还原为严格反代逻辑,即中转上游源服务流量 80 | // 此为粗颗粒度控制,优先级最高,细颗粒控制依旧使用路由规则管理 81 | const redirectConfig = { 82 | enable: true, // 允许直链的总开关,false 等同覆盖下列所有为 false 83 | enablePartStreamPlayOrDownload: true, // 允许串流播放或下载的直链 84 | enableVideoTranscodePlay: true, // 允许转码播放的直链 85 | }; 86 | 87 | // 路由缓存配置 88 | const routeCacheConfig = { 89 | // 总开关,是否开启路由缓存,此为一级缓存,添加阶段为 redirect 和 proxy 之前 90 | // 短时间内同客户端访问相同资源不会再做判断和请求 alist,有限的防抖措施,出现问题可以关闭此选项 91 | enable: true, 92 | // 二级缓存开关,仅针对直链,添加阶段为进入单集详情页,clientSelfAlistRule 中的和首页直接播放的不生效 93 | enableL2: false, 94 | // 缓存键表达式,默认值好处是命中范围大,但会导致 routeRule 中针对设备的规则失效,多个变量可自行组合修改,冒号分隔 95 | keyExpression: "r.uri:r.args.path:r.args.mediaIndex:r.args.partIndex", //"xxx:r.args.X-Plex-Client-Identifier" 96 | }; 97 | 98 | // 指定需要获取符号链接真实路径的规则,优先级在 mediaMountPath 和 routeRule 之间 99 | // 注意前提条件是此程序或容器必须挂载或具有对应目录的读取权限,否则将跳过处理,回源中转 100 | // 此参数仅在软链接后的文件名和原始文件名不一致或路径差异较大时使用,其余情况建议用 mediaPathMapping 101 | // 参数1: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 102 | // 参数2: 匹配目标,对象为媒体服务入库的文件路径(Item.Path) 103 | const symlinkRule = [ 104 | // [0, "/mnt/sda1"], 105 | ]; 106 | 107 | // 路由规则,注意顺序是从上至下匹配,千万注意规则不要重叠,不然排错十分困难,字幕和图片走了缓存,不在此规则内 108 | // 参数1: 指定处理模式,单规则的默认值为"proxy",但是注意整体规则都不匹配默认值为"redirect",然后下面参数序号-1 109 | // "proxy": 原始媒体服务器处理(中转流量), "redirect": 直链302, "block": 屏蔽媒体播放和下载 110 | // pelx 不需要 "transcode", 可用 "proxy" 代替,稍微有些歧义,这里只是不做修改,交给原始服务中转处理,具体是否转码由 plex 客户端自己判断上报的 111 | // 参数2: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 112 | // 参数3: 匹配类型或来源(字符串参数类型) "filePath": 文件路径(Item.Path), "alistRes": alist返回的链接 113 | // 参数4: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 114 | // 参数5: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 115 | const routeRule = [ 116 | // ["filePath", 0, "/mnt/sda1"], 117 | // ["filePath", 1, ".mp3"], 118 | // ["filePath", 2, "Google"], 119 | // ["alistRes", 2, "/NAS/"], // 例如使用 alias 聚合了 nas 本地文件,可能会存在卡顿或花屏 120 | // ["filePath", 3, /private/ig], 121 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 122 | // ["r.variables.remote_addr", 0, strHead.lanIp], 123 | // ["r.headersIn.User-Agent", 2, "IE"], // 请求头参数,客户端UA 124 | // ["r.args.X-Emby-Device-Id", 0, "d4f30461-ec5c-488d-b04a-783e6f419eb1"], // 链接入参,设备id 125 | // ["r.args.X-Emby-Device-Name", 0, "Microsoft Edge Windows"], // 链接入参,设备名称 126 | // ["r.args.UserId", 0, "ac0d220d548f43bbb73cf9b44b2ddf0e"], // 链接入参,用户id 127 | // 注意非"proxy"无法使用"alistRes"条件,因为没有获取 alist 直链的过程 128 | // ["proxy", "filePath", 0, "/mnt/sda1"], 129 | // ["redirect", "filePath", 0, "/mnt/sda2"], 130 | // ["block", "filePath", 0, "/mnt/sda4"], 131 | 132 | // 可参照 Emby 配置文件,但受限于脚本对于 Plex 获取文件路径的缓存实现,目前可能 XMedia 对象为空,只能使用接口入参 133 | // 注意设备id具有唯一性,不会跟随切换用户变更,Plex 接口入参不存在用户id,取值参考 /library/metadata/xxx 接口的出入参数 134 | // 高级分组规则,XMedia 为固定值,等于 Plex.MediaContainer.Metadata[0].Media[目标索引] 对象 135 | // r.XMedia.Part.0.Stream 数组比较特殊,路由规则暂未做关键词抽取,但目前匹配视频流规则够用,多音频/字幕流不太好写规则 136 | // r.XMedia.Part.0.Stream.0 一般为视频流对象,不支持 r.XMedia.Part[0].Stream[0] 写法 137 | 138 | // 此条规则代表大于等于 3Mbps 码率的允许转码,平方使用双星号表示,无意义加减仅为示例,注意 plex 码率为 kbps 单位 139 | // ["transcode", "高码率允许转码01", "r.XMedia.bitrate", ">=", 3 * 1000 + (1 * 1000) - (1 * 1000)], 140 | // 可选规则,结合上条规则做分组,同时满足才能生效,否则继续向下匹配 141 | // ["transcode", "高码率允许转码01", "r.args.X-Plex-Client-Identifier", "===", ["设备id01", "设备id02"]], 142 | // 此条规则代表 4K 分辨率的允许转码,但假如设备自身上报和上游决定走转码,不满足的也会转码,遵守上游倾向为播放成功率考虑 143 | // ["transcode", "高分辨率允许转码01", "r.XMedia.Part.0.Stream.0.displayTitle", "includes", "4K"], 144 | // 可选替换上条规则,更精确的分辨率规则,例如 21:9 视频,或某些 2.5 K 视频等不在标准分辨率划分内的 145 | // ["transcode", "高分辨率允许转码01", "r.XMedia.Part.0.Stream.0.width", ">=", 4320], 146 | // 精确屏蔽指定功能,注意同样是整体规则都不匹配默认走"redirect",即不屏蔽,建议只用下方一条,太复杂的话需要自行测试 147 | // ["blockDownload", "屏蔽下载01", "r.headersIn.User-Agent", "includes", strHead.xUAs.blockDownload], 148 | // 非必须,该分组内细分为入库路径黑名单,结合上面两条代表 "屏蔽指定标识客户端的非指定用户的指定入库路径的下载" 149 | // ["blockDownload", "屏蔽下载01", "filePath", "startsWith", ["/mnt/115"]], 150 | ]; 151 | 152 | // 路径映射,会在 mediaMountPath 之后从上到下依次全部替换一遍,不要有重叠,注意 /mnt 会先被移除掉了 153 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 154 | // 参数1: 0: 默认做字符串替换 replace 一次, 1: 前插, 2: 尾插, 3: replaceAll 替换全部 155 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 156 | // 参数3: 来源, 参数4: 目标 157 | const mediaPathMapping = [ 158 | // [0, 0, "/aliyun-01", "/aliyun-02"], 159 | // [0, 2, "http:", "https:"], 160 | // [0, 2, ":5244", "/alist"], 161 | // [0, 0, "D:", "F:"], 162 | // [0, 0, /blue/g, "red"], // 此处正则不要加引号 163 | // [1, 1, `${alistPublicAddr}/d`], 164 | // [2, 2, "?xxx"], 165 | // 此条是一个规则变量引用,方便将规则汇合到同一处进行管理 166 | // [ruleRef.mediaPathMappingGroup01, 0, 0, "/aliyun-01", "/aliyun-02"], 167 | // 路径映射多条规则会从上至下依次执行,如下有同一个业务关系集的,注意带上区间的闭合条件,不然会被后续重复替换会覆盖 168 | // 以下是按码率条件进行路径映射,全用户设备强制,区分用户和设备可再精确添加条件 169 | // [[["4K 目录映射到 1080P 目录", "r.XMedia.bitrate", ">", 10 * 1000], 170 | // ], 0, 0, "/4K/", "/1080P/"], 171 | // [[["1080P 目录映射到 720P 目录", "r.XMedia.bitrate", ">", 6 * 1000], 172 | // ["1080P 目录映射到 720P 目录", "r.XMedia.bitrate", "<=", 10 * 1000], 173 | // ], 0, 0, "/1080P/", "/720P/"], 174 | // [[["720P 目录映射到 480P 目录", "r.XMedia.bitrate", ">", 3 * 1000], 175 | // ["720P 目录映射到 480P 目录", "r.XMedia.bitrate", "<=", 6 * 1000], 176 | // ], 0, 0, "/720P/", "/480P/"], 177 | ]; 178 | 179 | // 仅针对 alist 返回的 raw_url 进行路径映射,优先级在 mediaPathMapping 和 clientSelfAlistRule 后,使用方法一样 180 | // 参数?.1: 生效规则三维数组,有时下列参数序号加一,优先级在参数2之后,需同时满足,多个组是或关系(任一匹配) 181 | // 参数1: 0: 默认做字符串替换replace一次, 1: 前插, 2: 尾插, 3: replaceAll替换全部 182 | // 参数2: 0: 默认只处理本地路径且不为 strm, 1: 只处理 strm 内部为/开头的相对路径, 2: 只处理 strm 内部为远程链接的, 3: 全部处理 183 | // 参数3: 来源, 参数4: 目标 184 | const alistRawUrlMapping = [ 185 | // [0, 0, "/alias/movies", "/aliyun-01"], 186 | ]; 187 | 188 | // 指定是否转发由 njs 获取 strm/远程链接 重定向后直链地址的规则,例如 strm/远程链接 内部为局域网 ip 或链接需要验证 189 | // 匹配来源为入库媒体的文件路径 190 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 191 | // 参数?.2: 匹配类型或来源(字符串参数类型),默认为 "filePath": mediaPathMapping 映射后的 strm/远程链接 内部链接 192 | // ,有分组时不可省略填写,可为表达式 193 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 194 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 195 | const redirectStrmLastLinkRule = [ 196 | [0, strHead.lanIp.map(s => "http://" + s)], 197 | // [0, alistAddr], 198 | // [0, "http:"], 199 | // 参数5: 请求验证类型,当前 alistAddr 不需要此参数 200 | // 参数6: 当前 alistAddr 不需要此参数,alistSignExpireTime 201 | // [3, "http://otheralist1.com", "sign", `${alistToken}:${alistSignExpireTime}`], 202 | // useGroup01 同时满足才命中 203 | // ["useGroup01", "filePath", "startsWith", ["https://youdomain.xxx.com:88"]], // 目标地址 204 | // ["useGroup01", "r.headersIn.User-Agent", "startsWith:not", ["Infuse"]], // 链接入参,客户端类型 205 | // docker 注意必须为 host 模式,不然此变量全部为内网ip,判断无效,nginx 内置变量不带$,客户端地址($remote_addr) 206 | // ["useGroup01", "r.variables.remote_addr", 0, strHead.lanIp], // 远程客户端为内网 207 | ]; 208 | 209 | // 指定客户端自己请求并获取 alist 直链的规则,代码优先级在 redirectStrmLastLinkRule 之后 210 | // 特殊情况使用,则此处必须使用域名且公网畅通,用不着请保持默认 211 | // 参数?.1: 分组名,组内为与关系(全部匹配),多个组和没有分组的规则是或关系(任一匹配),然后下面参数序号-1 212 | // 参数?.2: 匹配类型或来源(字符串参数类型),优先级高"filePath": 文件路径(Item.Path),默认为"alistRes": alist 返回的链接 raw_url 213 | // ,有分组时不可省略填写,可为表达式,然后下面参数序号-1 214 | // 参数3: 0: startsWith(str), 1: endsWith(str), 2: includes(str), 3: match(/ain/g) 215 | // 参数4: 匹配目标,为数组的多个参数时,数组内为或关系(任一匹配) 216 | // 参数5: 指定转发给客户端的 alist 的 host 前缀,兼容 sign 参数 217 | const clientSelfAlistRule = [ 218 | // Infuse 客户端对于 115 的进度条拖动可能依赖于此 219 | // 如果 nginx 为 https,则此 alist 也必须 https,浏览器行为客户端会阻止非 https 请求 220 | [2, strHead["115"], alistPublicAddr], 221 | // [2, strHead.ali, alistPublicAddr], 222 | // 优先使用 filePath,可省去一次查询 alist,如驱动为 alias,则应使用 alistRes 223 | // ["115-local", "filePath", 0, "/mnt/115", alistPublicAddr], 224 | // ["115-local", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], // 链接入参,客户端类型 225 | // ["115-alist", "alistRes", 2, strHead["115"], alistPublicAddr], 226 | // ["115-alist", "r.args.X-Emby-Client", 0, strHead.xEmbyClients.seekBug], 227 | ]; 228 | 229 | // 响应重定向链接前是否检测有效性,无效链接时转给媒体服务器回源中转处理 230 | const redirectCheckEnable = false; 231 | 232 | // 媒体服务/alist 查询失败后是否使用原始链接回源中转流量处理,如无效则直接返回 500 233 | const fallbackUseOriginal = true; 234 | 235 | // 转码配置,默认 false,将按之前逻辑强制直接播放 236 | // plex 只能用自身服务转码,只有下面一个参数,多填写没用 237 | const transcodeConfig = { 238 | enable: false, // 此为允许转码的总开关 239 | }; 240 | 241 | // for js_set 242 | function getPlexHost(r) { 243 | return plexHost; 244 | } 245 | function getTranscodeEnable(r) { 246 | return transcodeConfig.enable; 247 | } 248 | 249 | export default { 250 | plexHost, 251 | mediaMountPath, 252 | redirectConfig, 253 | routeCacheConfig, 254 | symlinkRule, 255 | routeRule, 256 | alistAddr, 257 | alistToken, 258 | alistSignEnable, 259 | alistSignExpireTime, 260 | alistPublicAddr, 261 | strHead, 262 | clientSelfAlistRule, 263 | redirectCheckEnable, 264 | fallbackUseOriginal, 265 | mediaPathMapping, 266 | alistRawUrlMapping, 267 | redirectStrmLastLinkRule, 268 | transcodeConfig, 269 | getPlexHost, 270 | getTranscodeEnable, 271 | } 272 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/exampleConfig/constant-main.js: -------------------------------------------------------------------------------- 1 | // 这个总配置拆分主体文件只是备份,生效需要放置在 conf.d 下,且重命名为 constant.js 2 | // 如果使用拆分配置,请注意填写 config 下的使用到的配置文件 3 | 4 | import commonConfig from "./config/constant-common.js"; 5 | import mountConfig from "./config/constant-mount.js"; 6 | import proConfig from "./config/constant-pro.js"; 7 | import symlinkConfig from "./config/constant-symlink.js"; 8 | import strmConfig from "./config/constant-strm.js"; 9 | import transcodeConfig from "./config/constant-transcode.js"; 10 | // import extConfig from "./config/constant-ext.js"; 11 | 12 | // 必填项,根据实际情况修改下面的设置 13 | 14 | // 这里默认 plex 的地址是宿主机,要注意 iptables 给容器放行端口 15 | const plexHost = "http://172.17.0.1:32400"; 16 | 17 | // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt 18 | // 通常配置一个远程挂载根路径就够了,默认非此路径开头文件将转给原始 plex 处理 19 | const mediaMountPath = ["/mnt"]; 20 | 21 | // for js_set 22 | function getPlexHost(r) { 23 | return plexHost; 24 | } 25 | function getTranscodeEnable(r) { 26 | return transcodeConfig.transcodeConfig.enable; 27 | } 28 | 29 | export default { 30 | plexHost, 31 | mediaMountPath, 32 | strHead: commonConfig.strHead, 33 | 34 | alistAddr: mountConfig.alistAddr, 35 | alistToken: mountConfig.alistToken, 36 | alistSignEnable: mountConfig.alistSignEnable, 37 | alistSignExpireTime: mountConfig.alistSignExpireTime, 38 | alistPublicAddr: mountConfig.alistPublicAddr, 39 | clientSelfAlistRule: mountConfig.clientSelfAlistRule, 40 | redirectCheckEnable: mountConfig.redirectCheckEnable, 41 | fallbackUseOriginal: mountConfig.fallbackUseOriginal, 42 | 43 | redirectConfig: proConfig.redirectConfig, 44 | routeCacheConfig: proConfig.routeCacheConfig, 45 | routeRule: proConfig.routeRule, 46 | mediaPathMapping: proConfig.mediaPathMapping, 47 | alistRawUrlMapping: proConfig.alistRawUrlMapping, 48 | 49 | symlinkRule: symlinkConfig.symlinkRule, 50 | redirectStrmLastLinkRule: strmConfig.redirectStrmLastLinkRule, 51 | transcodeConfig: transcodeConfig.transcodeConfig, 52 | 53 | getPlexHost, 54 | getTranscodeEnable, 55 | } 56 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/includes/http.conf: -------------------------------------------------------------------------------- 1 | server_name default; 2 | listen 8091; ## Listens on port IPv4 3 | listen [::]:8091; # Listens on port IPv6 -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/includes/https.conf: -------------------------------------------------------------------------------- 1 | server_name default; 2 | ## SSL SETTINGS ## 3 | listen [::]:8095 ssl; ## Listens on port IPv6 with ssl enabled 4 | listen 8095 ssl; ## Listens on port IPv4 with ssl enabled 5 | # listen 32411 ssl; 6 | # listen 8095 quic reuseport; ## http3 enabled 7 | # listen 32411 quic reuseport; 8 | http2 on; ## since nginx 1.25.1, the "listen ... http" directive is deprecated 9 | add_header Alt-Svc 'h3=":$server_port"; ma=86400'; ## http3 enabled 10 | ssl_session_timeout 30m; 11 | ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; 12 | ssl_certificate /etc/nginx/conf.d/cert/fullchain.pem; ## Location of your public PEM file. 13 | ssl_certificate_key /etc/nginx/conf.d/cert/privkey.key; ## Location of your private PEM file. 14 | ssl_session_cache shared:SSL:10m; 15 | error_page 497 =307 https://$host:$server_port$request_uri; ## if http and https use same port, Redirects http:// to https:// 16 | -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/includes/proxy-header.conf: -------------------------------------------------------------------------------- 1 | #proxy_set_header X-Real-IP $http_CF_Connecting_IP; 2 | ## if you use cloudflare un-comment this line and comment out above line. 3 | proxy_set_header Host $host; 4 | proxy_set_header X-Real-IP $remote_addr; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | proxy_set_header X-Forwarded-Proto $scheme; 7 | proxy_set_header X-Forwarded-Protocol $scheme; 8 | proxy_set_header X-Forwarded-Host $http_host; 9 | proxy_set_header Range $http_range; 10 | proxy_set_header If-Range $http_if_range; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/modules/ngx-ext.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import urlUtil from "../common/url-util.js"; 7 | // import events from "../common/events.js"; 8 | 9 | /** 10 | * fetchLastLink, actually this just once request,currently sufficient 11 | * @param {String} oriLink eg: "https://alist/d/file.xxx" or "http(s)://xxx" 12 | * @param {String} authType eg: "sign" 13 | * @param {String} authInfo eg: "sign:token:expireTime" 14 | * @param {String} ua 15 | * @returns redirect after link 16 | */ 17 | async function fetchLastLink(oriLink, authType, authInfo, ua) { 18 | // this is for multiple instances alist add sign 19 | if (authType && authType === "sign" && authInfo) { 20 | const arr = authInfo.split(":"); 21 | oriLink = util.addAlistSign(oriLink, arr[0], parseInt(arr[1])); 22 | } 23 | // this is for current alist add sign 24 | if (config.alistSignEnable) { 25 | oriLink = util.addAlistSign(oriLink, config.alistToken, config.alistSignExpireTime); 26 | } 27 | const url = encodeURI(oriLink); 28 | const urlParts = urlUtil.parseUrl(url); 29 | const hostValue = `${urlParts.host}:${urlParts.port}`; 30 | ngx.log(ngx.WARN, `fetchLastLink add Host: ${hostValue}`); 31 | try { 32 | // fetch Api ignore nginx locations,ngx.ferch,redirects are not handled 33 | const response = await ngx.fetch(url, { 34 | method: "HEAD", 35 | headers: { 36 | "User-Agent": ua, 37 | Host: hostValue, 38 | }, 39 | max_response_body_size: 1024 40 | }); 41 | const contentType = response.headers["Content-Type"]; 42 | ngx.log(ngx.WARN, `fetchLastLink response.status: ${response.status}, contentType: ${contentType}`); 43 | // response.redirected api error return false 44 | if ((response.status > 300 && response.status < 309) || response.status == 403) { 45 | // if handle really LastLink, modify here to recursive and return link on status 200 46 | return response.headers["Location"]; 47 | } else if (response.status === 200) { 48 | // alist 401 but return 200 status code 49 | if (contentType.includes("application/json")) { 50 | throw new Error("alist maybe return 401, check your alist sign or auth settings"); 51 | } 52 | throw new Error("not expected result"); 53 | } else { 54 | throw new Error(`${response.status} ${response.statusText}`); 55 | } 56 | } catch (error) { 57 | ngx.log(ngx.ERR, `error: fetchLastLink: ${error}`); 58 | } 59 | } 60 | 61 | function lastLinkFailback(url) { 62 | if (!url) { 63 | return url; 64 | } 65 | let rvt = alistLinkFailback(url); 66 | return rvt; 67 | } 68 | 69 | function alistLinkFailback(url) { 70 | let rvt = url; 71 | const alistAddr = config.alistAddr; 72 | const alistPublicAddr = config.alistPublicAddr; 73 | let uri = url.replace(alistAddr, ""); 74 | if (alistAddr && url.startsWith(alistAddr) && !uri.startsWith("/d/")) { 75 | rvt = `${alistAddr}/d${uri}`; 76 | ngx.log(ngx.WARN, `hit alistLinkFailback, add /d: ${rvt}`); 77 | return rvt; 78 | } 79 | uri = url.replace(alistPublicAddr, ""); 80 | if (alistPublicAddr && url.startsWith(alistPublicAddr) && !uri.startsWith("/d/")) { 81 | rvt = `${alistPublicAddr}/d${uri}`; 82 | ngx.log(ngx.WARN, `hit alistLinkFailback, add /d: ${rvt}`); 83 | return rvt; 84 | } 85 | return rvt; 86 | } 87 | 88 | async function linkCheck(url, ua, onlyRedirect) { 89 | const urlParts = urlUtil.parseUrl(url); 90 | const hostValue = `${urlParts.host}:${urlParts.port}`; 91 | ngx.log(ngx.WARN, `linkCheck add Host: ${hostValue}`); 92 | try { 93 | // fetch Api ignore nginx locations,ngx.ferch,redirects are not handled 94 | const response = await ngx.fetch(url, { 95 | method: "HEAD", 96 | headers: { 97 | "User-Agent": ua, 98 | Host: hostValue, 99 | }, 100 | max_response_body_size: 1024 101 | }); 102 | const contentType = response.headers["Content-Type"]; 103 | ngx.log(ngx.WARN, `linkCheck response.status: ${response.status}, contentType: ${contentType}`); 104 | // response.redirected api error return false 105 | if (response.status > 300 && response.status < 309) { 106 | return true; 107 | } else if (response.status === 200) { 108 | // maybe alist 401 but return 200 status code 109 | if (contentType.includes("application/json")) { 110 | throw new Error("linkCheck alist maybe return 401, check your alist sign or auth settings"); 111 | } 112 | if (onlyRedirect) { 113 | throw new Error("not expected result, onlyRedirect true but status 200"); 114 | } 115 | return true; 116 | } else { 117 | throw new Error(`${response.status} ${response.statusText}`); 118 | } 119 | } catch (error) { 120 | ngx.log(ngx.ERR, `error: linkCheck: ${error}`); 121 | } 122 | } 123 | 124 | // NJS log level only have: info, warn, error 125 | // nginx log level settings emby2Alist/nginx/nginx.conf error_log /var/log/nginx/error.log notice; 126 | 127 | function info(message) { 128 | ngx.log(ngx.INFO, message); 129 | } 130 | 131 | function warn(message) { 132 | ngx.log(ngx.WARN, message); 133 | } 134 | 135 | function error(message) { 136 | ngx.log(ngx.ERR, message); 137 | } 138 | 139 | function log(level, message) { 140 | ngx.log(level, message); 141 | } 142 | 143 | export default { 144 | fetchLastLink, 145 | lastLinkFailback, 146 | linkCheck, 147 | info, 148 | warn, 149 | error, 150 | log, 151 | }; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/modules/plex-example.js: -------------------------------------------------------------------------------- 1 | // @author: chen3861229 2 | // @date: 2024-07-13 3 | 4 | import config from "../constant.js"; 5 | import util from "../common/util.js"; 6 | import events from "../common/events.js"; 7 | import plex from "../plex.js"; 8 | 9 | async function example() { 10 | return "Hello Word!"; 11 | } 12 | 13 | export default { 14 | example, 15 | }; -------------------------------------------------------------------------------- /plex2Alist/nginx/conf.d/plex.conf: -------------------------------------------------------------------------------- 1 | # 此文件只需要更改 periodics 相关配置,和 includes 下 listen, SSL 配置 2 | # Load the njs script 3 | js_path /etc/nginx/conf.d/; 4 | js_import config from constant.js; 5 | js_import plex2Pan from plex.js; 6 | js_import periodics from common/periodics.js; 7 | 8 | # Memory Cache, workers shared values, since njs 0.8.0 9 | js_shared_dict_zone zone=partInfoDict:4M timeout=1d evict; 10 | js_shared_dict_zone zone=routeL1Dict:8M timeout=15m evict; 11 | js_shared_dict_zone zone=routeL2Dict:16M timeout=15m evict; 12 | # js_shared_dict_zone zone=routeL3Dict:32M timeout=15m evict; 13 | 14 | ## ReqLimit, the processing rate of requests coming from a single IP address 15 | # limit_req_zone $binary_remote_addr zone=one:1m rate=1r/s; 16 | 17 | ## ConnLimit, the number of connections from a single IP addres 18 | # limit_conn_zone $binary_remote_addr zone=one:1m; 19 | 20 | ## Start of actual server blocks 21 | server { 22 | js_set $plex config.getPlexHost; #plex address 23 | 24 | ## Include the http and https configs, better don't use same port 25 | include /etc/nginx/conf.d/includes/http.conf; 26 | # include /etc/nginx/conf.d/includes/https.conf; 27 | 28 | ## Include the proxy headers 29 | include /etc/nginx/conf.d/includes/proxy-header.conf; 30 | 31 | proxy_http_version 1.1; 32 | ## js_fetch SETTINGS 33 | resolver 8.8.8.8 114.114.114.114 [2001:4860:4860::8888] [2400:3200::1]; 34 | js_fetch_verify off; # internal use only, off this fetch https:// 35 | ## hide nginx version info 36 | server_tokens off; 37 | ## Compresses the content to the client, speeds up client browsing. 38 | gzip on; 39 | gzip_disable "msie6"; 40 | gzip_comp_level 6; 41 | gzip_min_length 1100; 42 | gzip_buffers 16 8k; 43 | gzip_proxied any; 44 | gzip_types 45 | text/plain 46 | text/css 47 | text/js 48 | text/xml 49 | text/javascript 50 | application/javascript 51 | application/x-javascript 52 | application/json 53 | application/xml 54 | application/rss+xml 55 | image/svg+xml; 56 | ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. 57 | client_max_body_size 20M; 58 | # This default is either 4K or 8K, depending on a platform 59 | # Plex sections API maybe be too large, 200k not enough 60 | subrequest_output_buffer_size 4M; 61 | # # Security / XSS Mitigation Headers 62 | # add_header X-Frame-Options "SAMEORIGIN"; 63 | # add_header X-XSS-Protection "1; mode=block"; 64 | # add_header X-Content-Type-Options "nosniff"; 65 | # aliDrive direct stream need no-referrer 66 | add_header 'Referrer-Policy' 'no-referrer'; 67 | 68 | set $apiType ""; 69 | 70 | # PlexMediaServer Start 71 | # Proxy sockets traffic for webClient 72 | location ~* /(websockets) { 73 | # Proxy plex Websockets traffic 74 | proxy_pass $plex; 75 | ## WEBSOCKET SETTINGS ## Used to pass two way real time info to and from emby and the client. 76 | proxy_http_version 1.1; 77 | proxy_set_header Upgrade $http_upgrade; 78 | proxy_set_header Connection $http_connection; 79 | proxy_connect_timeout 1h; 80 | proxy_send_timeout 1h; 81 | proxy_read_timeout 1h; 82 | tcp_nodelay on; ## Sends data as fast as it can not buffering large chunks, saves about 200ms per request. 83 | } 84 | 85 | # internal redirect 86 | location ~ ^(.*)/proxy(/.*)$ { 87 | internal; # internal use only 88 | gunzip on; # Jellyfin/Plex has gzip,need this,Emby no gzip but compatible 89 | proxy_set_header Accept-Encoding ""; # subrequest need this 90 | client_body_in_file_only clean; 91 | rewrite ^(.*)/proxy(/.*)$ $1$2 break; 92 | proxy_pass $plex$request_uri; # Emby/Plex need $request_uri,Jellyfin not need but compatible 93 | proxy_pass_request_body on; 94 | proxy_pass_request_headers on; 95 | add_header X-Proxy-Success true; # for debug 96 | } 97 | # PlexApiHandler, this REG see: https://regex101.com/r/spMXKP/3 98 | location ~* ^\/(hubs|library|status)\/(sections\/\d+($|\/all)|metadata\/\d+($|\/children)|continueWatching|promoted$|sessions$) { 99 | proxy_set_header Accept-Encoding ""; 100 | add_header X-Modify-Success true; 101 | js_content plex2Pan.plexApiHandler; 102 | } 103 | # like emby playbackinfo,but response is xml 104 | location ~* /video/:/transcode/universal/decision { 105 | set $apiType "TranscodeUniversalDecision"; 106 | proxy_set_header Accept-Encoding ""; 107 | # set $isDecision 1; 108 | add_header X-Cache-PartInfo-Success true; 109 | js_content plex2Pan.plexApiHandler; 110 | } 111 | # Mobile Clients only support text/xml 112 | location ~* /playQueues { 113 | proxy_set_header Accept-Encoding ""; 114 | add_header X-Modify-Success true; 115 | js_content plex2Pan.plexApiHandler; 116 | } 117 | 118 | # Redirect the stream to njs 119 | location ~* /library/parts/(\d+)/(\d+)/file { 120 | set $apiType "PartStreamPlayOrDownload"; 121 | # limit_req zone=one; 122 | # Cache alist direct link 123 | add_header Cache-Control max-age=3600; 124 | # proxy_pass $goalist; 125 | js_content plex2Pan.redirect2Pan; 126 | } 127 | # transcode, dash scheme, mpd file 128 | location ~* /video/:/transcode/universal/start { 129 | set $apiType "VideoTranscodePlay"; 130 | # limit_req zone=one; 131 | # Cache alist direct link 132 | add_header Cache-Control max-age=3600; 133 | # proxy_pass $goalist; 134 | js_content plex2Pan.redirect2Pan; 135 | } 136 | # transcode, m4s, this is expected, not redirect2Pan 137 | location ~* /video/:/transcode/universal/session { 138 | proxy_pass $plex; 139 | } 140 | # other streams like subtitles 141 | location ~* /library/streams/(\d+)$ { 142 | proxy_pass $plex; 143 | proxy_set_header Range ""; 144 | add_header X-Message 'fix upstream error removed headersIn["Range"]: $http_range'; # for debug 145 | } 146 | # PlexMediaServer End 147 | 148 | # customeLocations must before default location / 149 | include conf.d/custome/location/*.conf; 150 | 151 | location / { 152 | # Proxy main plex traffic 153 | proxy_pass $plex; 154 | # client_max_body_size 1000M; ## Allows for mobile device large photo uploads. 155 | 156 | ## ADDITIONAL SECURITY SETTINGS ## 157 | ## Optional settings to improve security ## 158 | ## add these after you have completed your testing and ssl setup ## 159 | ## NOTICE: For the Strict-Transport-Security setting below, I would recommend ramping up to this value ## 160 | ## See https://hstspreload.org/ read through the "Deployment Recommendations" section first! ## 161 | # add_header 'Referrer-Policy' 'origin-when-cross-origin'; 162 | # add_header Strict-Transport-Security "max-age=15552000; preload" always; 163 | # add_header X-Frame-Options "SAMEORIGIN" always; 164 | # add_header X-Content-Type-Options "nosniff" always; 165 | # add_header X-XSS-Protection "1; mode=block" always; 166 | } 167 | 168 | location @root { 169 | # Proxy main plex traffic 170 | proxy_pass $plex; 171 | } 172 | 173 | # global schedule task, since njs 0.8.1 174 | # location @periodics { 175 | # # to be run at 7 day intervals in worker process 0 176 | # js_periodic periodics.logHandler interval=7d; 177 | # } 178 | } 179 | -------------------------------------------------------------------------------- /plex2Alist/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module modules/ngx_http_js_module.so; 2 | #load_module modules/ngx_stream_js_module.so; # don't need this 3 | 4 | user root; 5 | worker_processes auto; 6 | 7 | error_log /var/log/nginx/error.log notice; 8 | pid /var/run/nginx.pid; 9 | # error_log syslog:server=192.168.31.200,tag=error_log notice; 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | http { 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | # access_log syslog:server=192.168.31.200,tag=access_log combined; 25 | 26 | sendfile on; 27 | #tcp_nopush on; 28 | 29 | keepalive_timeout 65; 30 | 31 | #gzip on; 32 | 33 | include /etc/nginx/conf.d/*.conf; 34 | } 35 | --------------------------------------------------------------------------------