技术细节与开发 - MiCat-S/Mi302 GitHub Wiki

繁體中文 | 简体中文 | English

这一页写给想了解内部运作或参与开发的人:请求流程、路径怎么匹配、已实现的 Emby 接口、管理 API、代码结构、怎么运行测试,以及参考过的项目。

请求流程

Mi302 是一个 FastAPI 程序,用 uvicorn 运行,数据存在 SQLite(data/library.db)。

  1. 扫描:把媒体库文件夹里的 .strm、普通视频文件、nfo、图片写进数据库,见 媒体库与扫描。
  2. 浏览:播放器调用 Emby API(/Users/{id}/Views、/Items…),Mi302 从数据库组装出 Emby 的 BaseItemDto。
  3. 播放信息:POST /Items/{id}/PlaybackInfo。strm 项目返回 Protocol=Http、IsRemote=true,只允许直接播放(关闭转码),DirectStreamUrl 指回 /videos/{id}/stream.{格式}。
  4. 串流:播放器请求 /Videos/{id}/stream、/Videos/{id}/original.xxx 或 /Items/{id}/Download:
    • strm 项目:读取 strm 内容,应用路径替换;带 pickcode 就用播放器的 User-Agent 向 115 获取直链,否则可以先跟随上游的重定向;最后返回 302 Location: <真实地址>。
    • 普通视频文件:直接发送文件,支持 Range。
  5. 进度:/Sessions/Playing… 记录续播位置和已看,也用来学习片头片尾,见 片头片尾跳过。

细节见 播放与外网访问。Mi302 对外报告自己是 Emby Server 4.8.11.0。

路径和参数

每个进来的 HTTP 请求先整理路径,再匹配路由:

  • 路径里的英文字母转成小写,其他字符不动(/Persons/Émilie 的 É 保留),所以路径不区分大小写。
  • 去掉开头的 /emby、/mediabrowser,有没有这个前缀都可以。
  • 去掉结尾的 /。
  • 查询参数的名称不区分大小写(ParentId、parentid 都可以)。

其他:

  • 错误响应是纯文本,和 Emby 一样。
  • CORS 全开。
  • FastAPI 自动生成的 API 文档在 /api-docs。

认证

token 依次从这些地方查找:

  1. 请求头 X-Emby-Token、X-MediaBrowser-Token。
  2. 请求头 X-Emby-Authorization(或 Authorization)里的 Token="…",格式是 MediaBrowser Client="…", Device="…", DeviceId="…", Version="…", Token="…"。
  3. 查询参数 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 接口

路径按 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

管理 API

管理页面 /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。
⚠️ **GitHub.com Fallback** ⚠️