技术细节与开发 - MiCat-S/Mi302 GitHub Wiki
这一页写给想了解内部运作或参与开发的人:请求流程、路径怎么匹配、已实现的 Emby 接口、管理 API、代码结构、怎么运行测试,以及参考过的项目。
Mi302 是一个 FastAPI 程序,用 uvicorn 运行,数据存在 SQLite(data/library.db)。
-
扫描:把媒体库文件夹里的
.strm、普通视频文件、nfo、图片写进数据库,见 媒体库与扫描。 -
浏览:播放器调用 Emby API(
/Users/{id}/Views、/Items…),Mi302 从数据库组装出 Emby 的BaseItemDto。 -
播放信息:
POST /Items/{id}/PlaybackInfo。strm 项目返回Protocol=Http、IsRemote=true,只允许直接播放(关闭转码),DirectStreamUrl指回/videos/{id}/stream.{格式}。 -
串流:播放器请求
/Videos/{id}/stream、/Videos/{id}/original.xxx或/Items/{id}/Download:- strm 项目:读取 strm 内容,应用路径替换;带 pickcode 就用播放器的 User-Agent 向 115 获取直链,否则可以先跟随上游的重定向;最后返回
302 Location: <真实地址>。 - 普通视频文件:直接发送文件,支持 Range。
- strm 项目:读取 strm 内容,应用路径替换;带 pickcode 就用播放器的 User-Agent 向 115 获取直链,否则可以先跟随上游的重定向;最后返回
-
进度:
/Sessions/Playing…记录续播位置和已看,也用来学习片头片尾,见 片头片尾跳过。
细节见 播放与外网访问。Mi302 对外报告自己是 Emby Server 4.8.11.0。
每个进来的 HTTP 请求先整理路径,再匹配路由:
- 路径里的英文字母转成小写,其他字符不动(
/Persons/Émilie的É保留),所以路径不区分大小写。 - 去掉开头的
/emby、/mediabrowser,有没有这个前缀都可以。 - 去掉结尾的
/。 - 查询参数的名称不区分大小写(
ParentId、parentid都可以)。
其他:
- 错误响应是纯文本,和 Emby 一样。
- CORS 全开。
- FastAPI 自动生成的 API 文档在
/api-docs。
token 依次从这些地方查找:
- 请求头
X-Emby-Token、X-MediaBrowser-Token。 - 请求头
X-Emby-Authorization(或Authorization)里的Token="…",格式是MediaBrowser Client="…", Device="…", DeviceId="…", Version="…", Token="…"。 - 查询参数
api_key、X-Emby-Token、ApiKey、X-MediaBrowser-Token。
token 由 POST /Users/AuthenticateByName 签发。同一个用户在同一台设备(DeviceId)重新登录时,旧的 token 作废。API 密钥(配置文件的 api_keys,或在网页上创建的)以管理员的身份运行(按名称排序的第一个管理员),给 MoviePilot 这类程序用;它不能调用 /web/api/… 和 /p115/…。
路径里有用户(/Users/{id}/…)或带 UserId 参数时,读写的是那个人的观看记录、续播点和收藏。普通用户只能指定自己,指定别人返回 403;管理员和 API 密钥可以指定别人,找不到那个人返回 404。id 不区分大小写,也接受带连字符的 GUID 写法。没指定就是登录的人。
路径按 Emby 的写法列出,实际上不区分大小写。除了标注“不用登录”的,都要带 token。
| 接口 | 说明 |
|---|---|
GET /System/Info/Public |
服务器名称、版本、id;不用登录 |
GET /System/Info |
完整服务器信息 |
GET, POST /System/Ping |
返回 Emby Server;不用登录 |
GET /System/Endpoint |
固定返回“在局域网内”;不用登录 |
GET /Branding/Configuration |
空的品牌设置;不用登录 |
GET /Users/Public |
登录界面的用户列表(server.public_users 关掉时为空);不用登录 |
POST /Users/AuthenticateByName |
登录,JSON 或表单,密码字段是 Pw 或 Password;不用登录 |
GET /Users |
所有用户;需要管理员 |
GET /Users/Query |
同上,包成 {Items, TotalRecordCount}(MoviePilot 用它统计用户数);需要管理员 |
GET /Users/{id} |
用户数据;只能查自己,管理员可以查别人 |
POST /Sessions/Logout |
退出登录,作废这个 token |
POST /Sessions/Capabilities、/Sessions/Capabilities/Full
|
接受但不处理 |
GET /Sessions |
空列表 |
GET, POST /DisplayPreferences/{id} |
固定的默认值,不保存;不用登录 |
GET /Users/{id}/GroupingOptions、/Plugins、/Localization/{kind}
|
空列表;不用登录 |
GET /Items/{id}/RemoteImages |
始终为空(Mi302 不向外查图片,MoviePilot 会改用 /Items/{id}/Images/{类型});不用登录 |
WebSocket /embywebsocket
|
只响应 KeepAlive |
| 接口 | 说明 |
|---|---|
GET /Users/{id}/Views、/Library/MediaFolders
|
媒体库列表 |
GET /Library/VirtualFolders、/Library/VirtualFolders/Query、/Library/SelectableMediaFolders
|
媒体库和文件夹路径(MoviePilot 用) |
POST /Library/Refresh |
全部重新扫描;需要管理员 |
POST /Library/Media/Updated |
只扫描通知里的路径;需要管理员 |
POST /Items/{id}/Refresh |
只重扫这一项所在的剧或电影;需要管理员 |
GET /Users/{id}/Items、/Items
|
查询项目,参数见下文 |
GET /Users/{id}/Items/{itemId}、/Items/{id}
|
单个项目的完整数据,含 MediaSources、People、Chapters;id 是人物 id 时返回人物 |
GET /Users/{id}/Items/Latest |
最近添加(Limit、ParentId、IncludeItemTypes;请求集时以剧为单位) |
GET /Users/{id}/Items/Resume |
继续观看 |
GET /Items/{id}/Ancestors |
上级项目 |
GET /Items/Counts |
电影、剧集、集的数量 |
GET /Shows/{id}/Seasons |
季 |
GET /Shows/{id}/Episodes |
集(SeasonId、Season、StartIndex、Limit、Fields) |
GET /Shows/NextUp |
接着看(SeriesId、Limit) |
GET /Genres |
所有类型 |
GET /Persons、/Persons/{name}
|
搜索人物、按名称查找人物(原名、中文名都识别) |
GET /Persons/{name}/Images/{type} |
人物照片,302 到 nfo 里的地址;不用登录 |
POST /Users/{id}/PlayedItems/{itemId};DELETE(或 POST …/Delete) |
标记已看、未看;对剧或季就是下面所有集 |
POST /Users/{id}/FavoriteItems/{itemId};DELETE(或 POST …/Delete) |
加入、移除收藏 |
/Items 的查询参数:ParentId、Recursive、IncludeItemTypes(含 Person 时同时搜索人物)、ExcludeItemTypes、Ids、PersonIds、Person、IsFolder、SearchTerm、NameStartsWith、Years、Genres、Filters(IsPlayed、IsUnplayed、IsFavorite、IsResumable)、IsPlayed、IsFavorite、SortBy、SortOrder、StartIndex、Limit、Fields(MediaSources、People、Chapters)、UserId。
StartIndex、Limit 是负数或无法识别时当作没给。/Items 和 /Shows/{id}/Episodes 没给 Limit 就是全部,和 Emby 一样;最新、继续观看、接着看默认 20 项,人物搜索默认 50 项,这几个最多 500 项。Limit=0 只返回总数。
SortBy 支持:SortName、Name、DateCreated、DateLastContentAdded、PremiereDate、ProductionYear、CommunityRating、CriticRating、Runtime、Random、DatePlayed、PlayCount、IndexNumber、ParentIndexNumber、AiredEpisodeOrder。
返回空结果的接口(让播放器不报错):/Users/{id}/Items/{id}/Intros、/Items/{id}/Similar、/Movies/{id}/Similar、/Shows/{id}/Similar、/Videos/{id}/AdditionalParts、/Items/{id}/CriticReviews、/Users/{id}/Items/{id}/LocalTrailers、/Users/{id}/Items/{id}/SpecialFeatures、/Items/{id}/SpecialFeatures、/Items/{id}/ThemeMedia。
| 接口 | 说明 |
|---|---|
GET /Items/{id}/Images/{type}[/{index}] |
读取图片,type 是 Primary、Backdrop、Thumb、Logo、Art;不用登录 |
POST /Items/{id}/Images/{type}[/{index}] |
上传图片(base64 文本或图片文件本身,最大 30 MB),保存在 data/images/;需要管理员 |
DELETE /Items/{id}/Images/{type}[/{index}] |
删除上传的图片,改回文件夹里的图;需要管理员 |
GET /Users/{id}/Images/{type} |
用户头像,一律 404 |
| 接口 | 说明 |
|---|---|
GET, POST /Items/{id}/PlaybackInfo |
媒体源 |
GET, HEAD /Videos/{id}/{name} |
串流(stream、stream.mkv、original.mkv…);redirect.require_auth 开着时需要登录 |
GET, HEAD /Items/{id}/Download、/Items/{id}/File
|
下载;一律需要登录,server.allow_download 关掉时返回 403 |
POST /Sessions/Playing、/Sessions/Playing/Progress、/Sessions/Playing/Stopped
|
播放进度 |
POST /Sessions/Playing/Ping |
接受但不处理 |
POST /Users/{id}/PlayingItems/{itemId}、…/Progress;DELETE(或 POST …/Delete) |
旧版的播放进度上报 |
| 接口 | 说明 |
|---|---|
GET /Episode/{id}/IntroTimestamps(也识别 /v1)、GET /Episode/{id}/Timestamps
|
Jellyfin Intro Skipper 插件格式的片头片尾 |
GET /MediaSegments/{id} |
Jellyfin 10.10 的媒体片段 |
GET, HEAD /d/{pickcode}[.扩展名][/文件名] |
Mi302 生成的 strm 用的短链接,302 到 115 直链;不用登录 |
GET, HEAD /p115/redirect?pickcode=…、/api/v1/plugin/p115strmhelper/redirect_url?pickcode=…
|
兼容其他工具的 strm;不用登录 |
OPTIONS, PROPFIND, GET, HEAD /dav/{115 路径} |
只读的 WebDAV(webdav.enabled):PROPFIND 列文件夹(Depth 最多 1),GET 文件 302 到 115 直链,HEAD 只返回大小;要 HTTP Basic 登录,写入的方法返回 405 |
管理页面 /web 用的 API。115 的文件、文件夹 id 有 19 位,超过 JavaScript 能精确表示的整数,返回时一律是字符串;传进来时字符串、数字都接受。除了 /web/api/setup,都要用管理员账号登录后的 token(放在 X-Emby-Token 请求头),API 密钥不行。请求和响应都是 JSON。
| 路径 | 用途 |
|---|---|
GET, POST /web/api/setup |
是否需要创建第一个管理员;创建它(只在还没有任何账号时可用) |
GET, PUT /web/api/settings |
读取、保存网页上的设置(写进配置文件) |
GET, POST /web/api/scan |
扫描进度和各媒体库的项目数;开始扫描:{"library": 名称}、{"path": 路径},都没有就扫描全部;没在扫描时 last 是上一次扫描(范围、处理了几个、是否点了停止) |
GET /web/api/libraries/userdata |
这个媒体库(name)有几条观看记录、是几个用户的(删除媒体库的确认框用) |
GET, POST /web/api/users;PUT, DELETE /web/api/users/{id}
|
用户 |
GET /web/api/browse、/web/api/115/browse
|
选择服务器上的文件夹、选择 115 目录 |
GET /web/api/115/offline、POST /web/api/115/offline、POST /web/api/115/offline/delete、POST /web/api/115/offline/clear
|
115 云下载:任务列表(page,附这个月还能加几个);加任务(urls 一行一个、folder 存到的 115 路径,空的用 115 默认);删任务(hashes,files 连下载好的文件一起删);清除已完成或已失败的记录(what:done/failed) |
POST /web/api/115/offline/retry |
重新加入失败的云下载任务(hash、url、folder_id):先删那一条失败的记录(不删文件),再加回原来的文件夹 |
POST /web/api/115/share/read、POST /web/api/115/share/start、GET /web/api/115/share/status、POST /web/api/115/share/stop
|
转存 115 分享(要用 cookie 登录):读分享(text 一行一个链接;返回每个分享的标题、大小、最上层的项目,读不到的在 error 写 115 的原文);在后台转存(shares:[{code, receive_code, title, ids}],ids 是字符串;folder 存到的 115 文件夹;target:path 整理到 target_path,空的先不整理);进度;停止(做完手上这一个分享) |
POST /web/api/115/rapid/read、POST /web/api/115/rapid/start、GET /web/api/115/rapid/status、POST /web/api/115/rapid/stop
|
从阿里云盘秒传到 115(要用 cookie 登录 115、登录阿里云盘):读取(source 阿里云盘路径、media_only 只要视频和字幕;返回几个视频、字幕、总大小、没有 SHA1 的、超过 115 单文件上限的、前 200 个);在后台秒传(source、folder 存到的 115 文件夹、target:path 整理到 target_path,空的先不整理、media_only);进度和每一个的结果;停止(做完手上这一个) |
GET /web/api/aliyun/status、POST /web/api/aliyun/token、POST /web/api/aliyun/logout、GET /web/api/aliyun/dirs
|
阿里云盘:账号状态;登录(refresh_token,失败时换回原本的);登出;选文件夹(path 这一层的子文件夹和文件数,空的是“资源库”“备份盘”) |
GET /web/api/115/recyclebin、POST /web/api/115/recyclebin/clean
|
115 回收站的一页(offset、limit);清空(永久删除,要 {"confirm": "清空"},cookie 通道再带 password 安全密钥) |
GET /web/api/deleted |
Mi302 送进 115 回收站的记录,新的在前面(source 只看一种来源、offset、limit);返回 items、total、sources
|
GET /web/api/115/files |
浏览 115:文件夹(cid、path,或只给 path)的子文件夹和文件,视频附上在媒体库里的样子(lib);文件一次最多 1000 个,offset 取下一批 |
POST /web/api/115/delete |
浏览 115 里勾的删掉(parent 当前看的文件夹 id、ids):先列一次那个文件夹,只删真的在里面的,移到 115 回收站;同步目录里的本地 strm 和媒体库跟着拿掉 |
GET /web/api/115/organize、POST /web/api/115/organize/check、GET /web/api/115/organize/job
|
整理 115 网盘:要整理的文件夹(问过 MoviePilot 名称不一样的、集号不对的;q、kind 是 series/movie/episodes/held(先不整理的)、offset、limit;浏览 115 加进来的在 pinned;每一个的 held 是有没有标记先不整理),附后台检查进度 job、还没问过的 unchecked;在后台问 MoviePilot(refresh 连问过的也重问);整理任务的进度 |
POST /web/api/115/organize/folder、DELETE /web/api/115/organize/folder/{id}
|
浏览 115 里挑的文件夹(cid、path)固定在清单最上面;拿掉 |
POST /web/api/115/organize/preview、POST /web/api/115/organize/recommend、POST /web/api/115/organize/execute
|
预览一个文件夹(id;每一部分可以指定 parts: {部分: {type, tmdbid, season, format}};target 是 parent/auto/path 加 target_path),返回预览代码 token;一部分的集数定位(id、part,先请 MoviePilot 推荐);按预览代码执行(tokens,cleanup 整理完把没有视频留下的来源文件夹移到回收站) |
POST /web/api/115/organize/all、POST /web/api/115/organize/all/stop、GET /web/api/115/organize/all
|
全部整理:清单上符合 q、kind 的(加上浏览 115 固定上来的)在后台一个一个预览,没问题(预览的 review 是空的)就按预览整理,其他跳过并记原因(target、target_path、cleanup;标记了先不整理的、一次要送超过 max_videos 支视频的不做,默认 300,0 = 不限;MoviePilot 连不上时等它恢复,最多 15 分钟);做完手上这一个就停;进度和跳过、失败的明细(明细最多留 2000 个,results_total 是全部的数量,truncated 表示有没有留不下的;进度也在 GET /web/api/115/organize/job 的 batch) |
POST /web/api/115/organize/hold |
先不整理(id、hold):全部整理时跳过、排到清单后面;返回那一个 |
GET /web/api/115/organize/episodes、POST /web/api/115/organize/delete
|
删除:一部剧的每一集和剧集文件夹(series,集号不对的标 problem);删勾选的集(series_id、file_ids、remove_folder),或清单上的一个整个删(id),都移到 115 回收站,本地 strm 和媒体库跟着拿掉;{"ids": [...]} 勾选的在后台一个一个删,进度看 organize/job 的 deleting
|
GET /web/api/server、POST /web/api/server/check、POST /web/api/server/update、POST /web/api/server/restart
|
版本和上次检查更新的结果(boot 每次启动都不同)、立即检查、更新到最新版(后台执行,进度看 job)、重启 |
GET /web/api/libraries/suggest?path= |
批量新增媒体库:列出子文件夹,附上建议的类型和理由、视频数(complete 表示数完了)、是否已在某个媒体库里(used)、要不要默认勾选;partial 表示有文件夹没看完 |
POST /web/api/moviepilot/test、GET /web/api/moviepilot/status、POST /web/api/moviepilot/scrape
|
MoviePilot 测试连接、状态、刮削缺少数据的项目 |
GET /web/api/moviepilot/library-dirs |
MoviePilot 目录设置里存储是 115 的媒体库目录(dirs 每一项有 name、path、type;enabled 是设置了 MoviePilot 没有),给转存分享的“整理到”;没设置、读不到时 dirs 是空的、error 写原因 |
GET /web/api/series、POST /web/api/moviepilot/fill
|
剧集和集号空缺列表;补全缺集 |
GET /web/api/moviepilot/subscriptions、POST /web/api/moviepilot/subscriptions/clear、GET /web/api/moviepilot/subscriptions/unsubscribed
|
MoviePilot 现在有几个订阅;取消所有订阅({"confirm": "取消訂閱"});最近 5 次取消前的订阅列表(backups) |
GET /web/api/intro/status、POST /web/api/intro/clear
|
片头片尾({"season_id": id} 只清除一季学到的记录) |
GET /web/api/intro/seasons?q=、PUT /web/api/intro/seasons/{季 id}
|
片头片尾的季列表和搜索;手动设置一季(all_seasons 应用到整部剧) |
GET /web/api/people/status、POST /web/api/people/resolve
|
演职人员中文名 |
GET, POST /web/api/backups;GET /web/api/backups/{name}
|
备份列表、立即备份、下载备份 |
GET /web/api/mediainfo/status、GET /web/api/mediainfo/titles、POST /web/api/mediainfo/probe
|
媒体信息:状态;还缺媒体信息的片名(可筛选、排序);提取(可带筛选、limit、ids) |
POST /web/api/mediainfo/probe/limit、POST /web/api/mediainfo/stop
|
提取中修改这一批最多几个({"limit": N});停止这一批 |
GET /web/api/logs、/web/api/logs/download
|
日志、下载日志文件 |
GET, POST /web/api/apikeys;DELETE /web/api/apikeys/{key}
|
API 密钥 |
GET /p115/status |
115 账号状态 |
POST /p115/qrcode、GET /p115/qrcode/status、POST /p115/cookies、POST /p115/logout
|
扫码登录、粘贴 cookie、退出登录 |
POST /p115/open/qrcode、GET /p115/open/qrcode/status、POST /p115/open/logout
|
115 开放平台授权 |
POST /p115/strm/sync?mode=incremental(不带就是全量)、GET /p115/strm/status、PUT /p115/strm/tasks
|
同步、同步状态、同步任务 |
POST /p115/strm/rewrite、POST /p115/strm/rewrite/stop
|
把现有的 strm 改成当前的服务器地址(只动本地文件);停止(改好的留着) |
GET /web/api/dupes、POST /web/api/dupes/scan、GET /web/api/dupes/groups、GET /web/api/dupes/shows、POST /web/api/dupes/delete、GET /web/api/dupes/log
|
115 重复文件:状态、找重复(paths)、分组列表、删除(overrides、sha1+size 只删一组、dry_run 只试算);kind=versions 是不同版本(grp 只删一组、show 只删一部、use_suggestions 按建议;网页的列表用 GET /web/api/dupes/shows?kind=versions,同一部剧合成一行,展开时 groups?kind=versions&show=…;完全相同一样用 shows?kind=exact、groups?show=…,删除带 show 只删那一部)、删除记录;kind=big 是大文件(min_size 字节、type 是 movie/episode/none 筛选,删除时 use_suggestions 是符合条件的全部,不必留一份);POST /web/api/dupes/prefer 切换不同版本建议保留的分辨率(prefer 是 1080、2160 或 highest) |
POST /web/api/scan/stop、POST /p115/strm/stop、POST /web/api/dupes/stop、POST /web/api/moviepilot/stop、POST /web/api/115/organize/check/stop、POST /web/api/115/organize/execute/stop、POST /web/api/115/organize/delete/stop
|
停止扫描、115 同步、找重复/删重复、MoviePilot 刮削、补全缺集或取消订阅(what 是 scrape、fill 或 unsubscribe)、整理 115 网盘的检查、执行、删除勾选的:都在安全的地方停下,没做完的下次再做;返回 stopped(没在跑是 false) |
GET /web 是管理页面本身,/web/115 跳转到 /web#115。
embyserver/ 下的每个模块:
| 模块 | 内容 |
|---|---|
__main__.py |
命令行入口:python -m embyserver [-c config.yaml] [--scan] [--sync-115 [incremental]] [--reset-password 账号 新密码]
|
app.py |
组装 FastAPI 程序、整理路径、启动后台任务 |
auth.py |
用户、token、Emby 认证请求头 |
backup.py |
每天备份数据库和配置文件 |
browse115.py |
浏览 115:列文件夹,视频对照媒体库 |
config.py |
配置文件的数据结构和读取 |
config_file.py |
把设置写回 config.yaml
|
settings.py |
网页上可改的设置:校验、应用、和配置文件同步 |
db.py |
SQLite 数据表和访问 |
dupes.py |
115 上的重复文件:完全相同、不同版本、建议保留哪一份;大文件 |
dto.py |
数据库行转成 Emby 的 BaseItemDto、UserDto、MediaSourceInfo
|
filetypes.py |
共用的扩展名列表 |
organize115.py |
整理 115 网盘:问 MoviePilot 每个文件夹整理后叫什么(结果记在 organize_checks),整个文件夹交给它预览 |
http_util.py |
调用外部服务的 httpx Client,把连接错误转成看得懂的原因 |
intro.py |
从播放行为学习片头片尾 |
library_suggest.py |
批量添加媒体库:猜子文件夹是电影还是剧集 |
logs.py |
日志:终端、日志文件、内存,遮盖凭证 |
mediainfo.py |
媒体信息:ffprobe 结果和 X-mediainfo.json 转成 Emby 格式 |
prober.py |
用 ffprobe 探测 strm 指向的视频 |
moviepilot.py |
刮削、补全缺集交给 MoviePilot |
moviepilot-plugin/ |
MoviePilot V3 插件“Mi302 整理助手”(package.v3.json、plugins.v3/mi302organizer/):按 MoviePilot 的规则算名字(naming.py),用 MoviePilot 的 115 授权按 Mi302 发来的清单批量改名,见 MoviePilot 集成
|
p115.py |
115:扫码登录、获取直链、列目录、生活事件、熔断 |
p115_open.py |
115 开放平台通道 |
probe_select.py |
媒体信息:挑要提取的片名(筛选、排序) |
people.py |
演职人员、中文名、类型中文化 |
redirect.py |
解析 strm 真实地址、路径替换、重定向缓存 |
reorganize.py |
整理的执行(按预览代码发送给 MoviePilot、清掉搬空的旧文件夹、之后的增量同步)和删除;集数定位模板(MoviePilot 推荐不出来时的备用) |
scanner.py |
媒体库扫描 |
share115.py |
转存 115 分享:读分享、转存到暂存子文件夹、等 115 做完,加进“整理 115 网盘”交给 MoviePilot 整理 |
rapid115.py |
从阿里云盘秒传到 115:115 的上传初始化(加密、二次验证),整批秒传,完成的文件夹加进“整理 115 网盘” |
aliyun.py |
阿里云盘开放平台:登录和换 token、列目录、取下载地址、读一段 |
strm_sync.py |
从 115 生成 strm、下载元数据 |
textutil.py |
拼音排序、拼音搜索、繁简转换 |
updater.py |
网页上的检查更新、更新、重启 |
routes/system.py |
System、Users、Sessions 等接口 |
routes/items.py |
媒体库、项目查询、用户数据、图片 |
routes/playback.py |
PlaybackInfo、串流、进度上报、片头片尾接口 |
routes/p115.py |
115 登录和同步的管理 API、pickcode 短链接 |
routes/web/ |
管理页面和它的 API,按页面分成 setup、scan、intro、moviepilot、organize、p115、server 几个文件 |
routes/common.py |
路由共用的小工具 |
web/admin.html |
管理页面(单个文件,HTML、CSS、JavaScript 都在里面) |
数据库 library.db 的数据表:meta(服务器 id、115 登录状态、同步进度等)、users、tokens、items、user_data(观看记录、收藏)、p115_index(115 文件 id 对应本地路径)、media_info、people、person_names、intro_obs、mp_no_image。
需要 Python 3.10 以上。在 venv 里:
pip install -r requirements.txt pytest ruff
python -m pytest
ruff check embyserver tests测试也会用 node --check 检查管理页面的 JavaScript 语法(没有 node 就跳过),并检查文档:Wiki 的链接和锚点都找得到、每一页都在侧栏、三种语言的配置文件参考都写到每一个键。
改了 install.sh 之后:
shellcheck install.sh-
DDSRem-Dev/MoviePilot-Plugins:302 播放流程参考
embyreverseproxy插件,区别是 Mi302 背后不需要真的 Emby,Emby API 自己实现;115 扫码登录和获取直链的流程参考p115strmhelper。 -
StrmAssistant(Emby 神医助手):
X-mediainfo.json沿用它“媒体信息持久化”的格式;片头片尾的学习方法参考它的“片头探测 - 播放行为”。 -
xiao-vvv/emby-mediainfo(MIT 许可):ffprobe 到 Emby 字段的对照、探测的限速和 115 熔断规则改写自这个项目,版权声明保留在
embyserver/mediainfo.py。