🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

追番 Elisoar111/dsh-anime

DeepSeek Harness
Elisoar1119 天前有提交已提供中文文档扫描:含敏感能力

在侧边栏追番:季度新番、放送表、在线播放与下载

30.8/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Elisoar111/dsh-anime
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · platform
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 9 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 124 处,展示前 105 条)
  • 读取本地文件
    index.js:138
    const manifest = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
  • 写入本地文件
    index.js:196
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    index.js:198
    copyFileSync(absolute, target)
  • 读取本地文件
    index.js:208
    entries = readdirSync(dir)
  • 读取本地文件
    index.js:221
    bytes = statSync(full).size
  • 执行外部命令来自配置/变量
    index.js:232
    execFile(process.execPath, ['--check', absolute], { timeout: 20000 }, (error, _stdout, stderr) => {
  • 发起网络请求
    index.js:507
    const response = await fetch(url, {
  • 执行外部命令固定命令
    index.js:1261
    const child = spawn('powershell.exe', ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-File', script], {
  • 发起网络请求
    index.js:1297
    const response = await fetch(target, { headers, signal: AbortSignal.timeout(timeoutMs) })
  • 读取本地文件
    index.js:1530
    settingsState = coerceSettings(JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')))
  • 发起网络请求
    index.js:1748
    const response = await fetch(target, { headers })
  • 发起网络请求
    index.js:1764
    const response = await fetch(target, {
  • 发起网络请求
    index.js:2219
    const response = await fetch(endpoint, {
  • 读取本地文件
    index.js:2847
    const text = readFileSync(absolute, 'utf8')
  • 写入本地文件
    index.js:3641
    copyFileSync(from, absolute)
  • 读取本地文件
    index.js:3646
    const text = readFileSync(absolute, 'utf8')
  • 写入本地文件
    index.js:3674
    writeFileSync(absolute, content, 'utf8')
  • 写入本地文件
    index.js:3677
    copyFileSync(backup, absolute)
  • 执行外部命令固定命令
    index.js:3932
    execFile('explorer', [dir], () => resolvePromise())
  • 发起网络请求
    index.js:4245
    const upstream = await fetch(parsed.toString(), {
  • 发起网络请求
    client.js:822
    const response = await fetch(target, { headers: { accept: 'application/json' } })
  • 发起网络请求
    client.js:1234
    const manifest = await fetch(manifestUrl, { signal, credentials: 'same-origin' }).then((response) => response.json())
  • 发起网络请求
    client.js:1352
    const response = await fetch(url, {
  • 发起网络请求
    client.js:1772
    fetch(src, { method: 'HEAD', signal: controller.signal, credentials: 'same-origin' })
  • 写入剪贴板
    client.js:5397
    await navigator.clipboard.writeText(draft)
  • 发起网络请求
    client.js:6184
    const response = await fetch(path, { cache: 'no-store', headers: { accept: 'application/json' } })
  • 发起网络请求
    client.js:7629
    const response = await fetch(target.url, target.init)
  • 写入剪贴板
    client.js:9200
    navigator.clipboard?.writeText(item.magnet).then(
  • 发起网络请求
    lib/animetosho.js:176
    const response = await fetch(url, {
  • 发起网络请求
    lib/anibt.js:137
    const response = await fetch(`${ANIBT_BASE}${path}`, {
  • 读取本地文件
    lib/torrent-engine.js:128
    const raw = await readFile(CONFIG_FILE, 'utf8')
  • 写入本地文件
    lib/torrent-engine.js:142
    await mkdir(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/torrent-engine.js:143
    await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8')
  • 执行外部命令来自配置/变量
    lib/torrent-engine.js:345
    const child = spawn(ffmpeg, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
  • 读取本地文件
    lib/torrent-engine.js:370
    statSync(command)
  • 读取本地文件
    lib/torrent-engine.js:397
    statSync(file)
  • 执行外部命令来自配置/变量
    lib/torrent-engine.js:455
    const child = spawn(exe, argv, { detached: true, stdio: 'ignore', windowsHide: false })
  • 执行外部命令来自配置/变量
    lib/torrent-engine.js:477
    const child = spawn(player.path, args, { detached: true, stdio: 'ignore', windowsHide: false })
  • 写入本地文件
    lib/torrent-engine.js:519
    await mkdir(cacheDir, { recursive: true })
  • 发起网络请求
    lib/torrent-engine.js:643
    const response = await fetch(`${base}/api/v2/auth/login`, {
  • 发起网络请求
    lib/torrent-engine.js:667
    return fetch(`${base}/api/v2${path}`, {
  • 读取本地文件
    lib/torrent-engine.js:864
    size = statSync(resolved.path).size
  • 读取本地文件
    lib/torrent-engine.js:879
    size = statSync(resolved.path).size
  • 读取本地文件
    lib/mpv.js:93
    files = readdirSync(SHADER_DIR).filter((name) => name.endsWith('.glsl')).sort()
  • 读取本地文件
    lib/mpv.js:111
    statSync(file)
  • 执行外部命令固定命令
    lib/mpv.js:185
    const child = spawn('mpv', args, { detached: false, stdio: 'ignore', windowsHide: false })
  • 发起网络请求
    lib/bilibili.js:58
    const response = await fetch(url, {
  • 读取本地文件
    lib/bilibili.js:599
    if (!existsSync(key)) dashFiles.delete(key)
  • 读取本地文件
    lib/bilibili.js:608
    if (existsSync(target) && statSync(target).size > 0) return target
  • 读取本地文件
    lib/bilibili.js:608
    if (existsSync(target) && statSync(target).size > 0) return target
  • 写入本地文件
    lib/bilibili.js:611
    mkdirSync(dashDir, { recursive: true })
  • 执行外部命令来自配置/变量
    lib/bilibili.js:629
    const child = spawn(ffmpeg, buildArgs(source), { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] })
  • 写入本地文件
    lib/bilibili.js:654
    renameSync(temp, target)
  • 读取本地文件
    lib/bilibili.js:682
    if (existsSync(target) && statSync(target).size > 0) return target
  • 读取本地文件
    lib/bilibili.js:682
    if (existsSync(target) && statSync(target).size > 0) return target
  • 写入本地文件
    lib/bilibili.js:685
    mkdirSync(dashDir, { recursive: true })
  • 执行外部命令来自配置/变量
    lib/bilibili.js:703
    const child = spawn(ffmpeg, argv, { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] })
  • 删除本地文件
    lib/bilibili.js:725
    rmSync(target, { force: true })
  • 读取本地文件
    lib/bilibili.js:756
    size = existsSync(target) ? statSync(target).size : 0
  • 执行外部命令来自配置/变量
    lib/bilibili.js:885
    const child = spawn(ffmpeg, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
  • 发起网络请求
    lib/bilibili.js:935
    upstream = await fetch(play.url, {
  • 发起网络请求
    lib/fqdm.js:35
    const response = await fetch(url, { headers: headers(base), redirect: 'follow' })
  • 发起网络请求
    lib/fqdm.js:380
    const response = await fetch(target, { headers: play.headers })
  • 发起网络请求
    lib/anich.js:215
    const response = await fetch(hostOf(base) + path, { method, headers, body, signal: controller.signal })
  • 发起网络请求
    lib/websource.js:66
    const response = await fetch(url, { headers: headersFor(rule), signal: controller.signal, redirect: 'follow' })
  • 读取本地文件
    lib/websource.js:321
    if (!existsSync(indexPath)) return []
  • 读取本地文件
    lib/websource.js:324
    index = JSON.parse(readFileSync(indexPath, 'utf8'))
  • 读取本地文件
    lib/websource.js:332
    const rule = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null
  • 读取本地文件
    lib/websource.js:332
    const rule = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null
  • 读取本地文件
    lib/websource.js:336
    return readdirSync(dir)
  • 读取本地文件
    lib/websource.js:339
    const rule = JSON.parse(readFileSync(join(dir, file), 'utf8'))
  • 读取本地文件
    lib/settings.js:259
    const raw = await readFile(SETTINGS_FILE, 'utf8')
  • 写入本地文件
    lib/settings.js:273
    await mkdir(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/settings.js:274
    await writeFile(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8')
  • 写入本地文件
    lib/settings.js:280
    await mkdir(STATE_DIR, { recursive: true })
  • 写入本地文件
    lib/settings.js:281
    await writeFile(SETTINGS_FILE, JSON.stringify(defaults, null, 2), 'utf8')
  • 读取本地文件
    lib/downloads.js:48
    await stat(candidate)
  • 读取本地文件
    lib/downloads.js:100
    const raw = await readFile(historyFile(), 'utf8')
  • 写入本地文件
    lib/downloads.js:117
    await mkdir(getSettings().stateDir, { recursive: true })
  • 写入本地文件
    lib/downloads.js:119
    await writeFile(historyFile(), JSON.stringify({ version: 1, jobs: all }, null, 2), 'utf8')
  • 写入本地文件
    lib/downloads.js:170
    await mkdir(folder, { recursive: true })
  • 写入本地文件
    lib/downloads.js:211
    await mkdir(folder, { recursive: true })
  • 执行外部命令来自配置/变量
    lib/downloads.js:260
    child = spawn(ffmpegPath, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
  • 删除本地文件
    lib/downloads.js:337
    await unlink(partial).catch(() => {})
  • 写入本地文件
    lib/downloads.js:344
    await rename(partial, job.path)
  • 读取本地文件
    lib/downloads.js:345
    const info = await stat(job.path)
  • 删除本地文件
    lib/downloads.js:362
    await unlink(partial).catch(() => {})
  • 发起网络请求
    lib/downloads.js:375
    response = await fetch(url, { headers, signal: handshake.signal })
  • 写入本地文件
    lib/downloads.js:397
    const sink = createWriteStream(partial)
  • 删除本地文件
    lib/downloads.js:435
    await unlink(partial).catch(() => undefined)
  • 读取本地文件
    lib/downloads.js:619
    const info = await stat(text)
  • 执行外部命令来自配置/变量
    lib/downloads.js:627
    const child = spawn(explorer, args, { detached: true, stdio: 'ignore' })
  • 读取本地文件
    lib/downloads.js:641
    const info = await stat(folder)
  • 读取本地文件
    lib/downloads.js:656
    const info = await stat(path)
  • 读取本地文件
    lib/accounts.js:81
    if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true })
  • 写入本地文件
    lib/accounts.js:81
    if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true })
  • 读取本地文件
    lib/accounts.js:87
    const stat = statSync(file)
  • 读取本地文件
    lib/accounts.js:98
    cache = normalize(JSON.parse(readFileSync(file, 'utf8')))
  • 写入本地文件
    lib/accounts.js:110
    writeFileSync(temp, JSON.stringify(cache, null, 2), 'utf8')
  • 写入本地文件
    lib/accounts.js:111
    renameSync(temp, file)
  • 发起网络请求
    lib/accounts.js:177
    const response = await fetch(url, { ...options, signal: controller.signal })
  • 读取本地文件
    lib/render-stream.js:38
    statSync(file)
  • 写入本地文件
    lib/render-stream.js:111
    mkdirSync(WORK_DIR, { recursive: true })
  • 写入本地文件
    lib/render-stream.js:113
    writeFileSync(file, subtitleToAss(payload, title), 'utf8')
  • 执行外部命令固定命令
    lib/render-stream.js:166
    const child = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
展开完整介绍(143 字)↓

追番 — DeepSeek Harness 的动漫追踪插件:新番表、放送时间表、搜索、追番列表、详情页、bilibili / FQDM / AniCh 在线观看、种子、下载、OP/ED 主题曲,还有自研 MSE 播放器(PrismFlow),支持 libass 字幕和 Anime4K。

README

由 DeepSeek 最新模型翻译生成
追番 · dsh-anime 中文 · English 在 DeepSeek Harness 里追番的一整页插件:季度新番、放送表、搜索、追番进度、番剧详情、在线播放、种子播放、下载、主题曲。以「侧边栏图标 + 主面板」的形式注册(sidebar.panellist id dsh-anime ⇄ main key dsh-anime),不是浮层、也不是小组件。 An anime tracker as a DeepSeek Harness plugin: seasonal charts, airing schedule, search, watchlist, detail pages, streaming (bilibili / FQDM / AniCh), torrents, a download manager and OP/ED themes — with its own player (PrismFlow: MSE + libass + Anime4K) instead of a bare tag. - 版本:1.0.13(页面构建号 2026-10-25.99) - 运行环境:DeepSeek Harness 桌面版(提供插件宿主与 Web 前端)
展开完整文档(README 剩余 1 节)
追番 · dsh-anime

中文 · English

在 DeepSeek Harness 里追番的一整页插件:季度新番、放送表、搜索、追番进度、番剧详情、在线播放、种子播放、下载、主题曲。以「侧边栏图标 + 主面板」的形式注册(sidebar.panellist id dsh-anime ⇄ main key dsh-anime),不是浮层、也不是小组件。

An anime tracker as a DeepSeek Harness plugin: seasonal charts, airing schedule, search, watchlist, detail pages,
streaming (bilibili / FQDM / AniCh), torrents, a download manager and OP/ED themes — with its own player
(PrismFlow: MSE + libass + Anime4K) instead of a bare tag.

- 版本:1.0.13(页面构建号 2026-10-25.99)
- 运行环境:DeepSeek Harness 桌面版(提供插件宿主与 Web 前端)
- 可选外部依赖:ffmpeg(转码 / 下载合流)、mpv(内嵌或外置播放)、qBittorrent(外部下载客户端)

注意

本插件目前有许多不完善的功能和bug,未来会逐一解决和优化

目录

- 功能
- 安装
- 首次打开:只有一个源是开着的
- 数据源
- 播放
- 追番与详情
- 主题曲(OP / ED)
- 下载
- 账号与隐私
- 设置
- HTTP API
- 架构
- 开发
- 已知限制
- 许可与第三方

功能

| 页面 | 能做什么 |
| 本季新番 / 搜索 | AniList 风格的左侧筛选栏 + 海报网格:季度、年份、排序(热度 / 评分 / 开始时间)、类型、标签、放送状态、格式、评分区间、是否含成人内容;搜索页同一套网格 |
| 放送表 | 未来 7 天的排播,按星期分组,今日高亮;数据随页头选中的番剧库切换 |
| 我的追番 | 本地追番表:集数 +1 / −1、进度条、继续观看;追番卡片直接显示下一话与已看集数 |
| 番剧详情 | 横幅 / 封面 / 评分 / 类型 / 制作组 / 外链 / 预告 / 下一话倒计时;标签页:信息、在线流、种子流、观看顺序、主题曲、角色与关联 |
| 在线流 | 哔哩哔哩、FQDM、AniCh,以及 AnimeFlow 声明式源(src-):搜索 → 匹配季度 → 集数网格 → 播放;真实线路名、死线路自动换线 |
| 种子流 | AniBT / AnimeTosho:集数 + 分辨率 + 只看合集 + 精确标题过滤,带体积 / 集号 / 字幕组;内置 BT 引擎或交给 qBittorrent |
| 片源 | 已安装扩展 + Seanime Marketplace 目录;开关、偏好、以及每张卡右上角的 <> 源码查看 / 编辑 |
| 下载 | 在线流与种子流共用一张任务表(Motrix 风格),进度、速度、暂停、取消、打开目录 |
| 设置 | 播放、桌面播放器、种子流、下载与数据、界面语言等分区 |

页头还有一个番剧库三选一开关(AniList / Bangumi / Animeko),它决定本季新番、放送表、搜索从哪里读。
不做同屏混排——同一部番会带两套 id 出现两次。在线流与种子流与该开关无关。

安装

插件就是一个普通的 DSH 插件包(dsh.bundle.patch + dsh.client.platform = web),装法两种。

方式一:从 GitHub 装(推荐)

把包装进 profile(等价于在 profile 目录里跑 pnpm add)
dsh --profile desktop plugin add "git+https://github.com/Elisoar111/dsh-anime.git"

macOS / Linux 换成你的 profile 名(web / tui / …)
dsh --profile web plugin add "git+https://github.com/Elisoar111/dsh-anime.git"

然后确认 profile 的 package.json 里 dsh.profile.bundles 含这一项——少了它插件不会被加载:

{
"dsh": {
"profile": {
"bundles": ["@local/dsh-anime", "…其它插件"]
}
}
}

重启 DSH(或在插件面板里关 → 开一次),侧边栏出现「追番」。

方式二:本地开发装

git clone https://github.com/Elisoar111/dsh-anime.git
cd dsh-anime
npm install # 运行时只依赖 webtorrent

在 profile 的 package.json 加一条依赖 + 一条 bundle,路径改成你 clone 的位置:

{
"dependencies": {
"@local/dsh-anime": "link:D:/path/to/dsh-anime"
},
"dsh": {
"profile": {
"bundles": ["@local/dsh-anime"]
}
}
}

dsh --profile desktop plugin install # 让 pnpm 建好链接

profile 文件位置:$DSH_HOME/profiles/<profile>/package.json, Windows 默认路径是 %USERPROFILE%\.dsh\profiles\desktop\package.json。

开发时改完 index.js / lib/.js:重启一次插件(关 → 开)即可;改 client.js 还要刷新页面。

可选外部依赖

| 工具 | 用在哪 | 没有会怎样 |
| ffmpeg | 在线流转码(HLS → fMP4)、种子下载合流 | 在线流播不了;种子流只能「用播放器打开」 |
| mpv | 外置播放「用播放器打开」 | 少一个入口,内置播放器不受影响 |
| qBittorrent | 把种子任务交给外部客户端 | 只能用内置 BT 引擎 |

装好之后只有 AniList 是开着的,其它源需要自己到「片源」里打开——见下一节。

首次打开:只有一个源是开着的

九个内置源里除 AniList 外全部 defaultEnabled: false,AnimeFlow 的声明式源(src-)同样默认关闭。
这样新用户可以先把页面跑起来,再按需要开源,而不是一屏十几张卡全是「未配置 / 网络不通」。

打开方式:「片源」→ 找到对应卡片 → 右下角开关。开关状态会写进 $DSH_HOME/state/dsh-anime/settings.json 的 extensions.<id>.enabled,之后就算你改这个源的偏好设置也不会被悄悄改回去(这一点有 dev/check-default-sources.mjs 守着)。

数据源

| 源 | 提供什么 | 网络现状(实测) |
| AniList graphql.anilist.co | 季度新番、热度、放送时间轴、搜索、详情(角色 / 关联 / 制作组 / 预告)、观看顺序 | 可用,1–8 秒 |
| Bangumi api.bgm.tv / 镜像 bgmapi.anibt.net | 每周放送表、中文搜索、按季度搜番、详情 / 集数 / 收藏 | 官方地址连接超时;数据接口默认走 anibt 镜像(可用)。OAuth 登录仍指 bgm.tv,镜像没有代理该端点,所以收藏同步需要自备代理 |
| Animeko api.animeko.org | 季度排期、放送表、搜索、条目详情;封面 static.myani.org | 可用,匿名免 token,1–2 秒;条目 id 与 Bangumi 同一 id 空间 |
| 哔哩哔哩 api.bilibili.com | 在线流(番剧 + 用户投稿)、主题曲第二源 | 可用;无 cookie 会 412 风控,宿主先取 buvid3/buvid4 再请求 |
| FQDM | 在线流(MacCMS 模板站) | 可用;线路表 {name, sid, count},部分线路 CDN 已死(见「死线路自动换线」) |
| AniCh ani.emmmm.eu.org | 在线流(站点解析)+ 索引(季度 / 搜索 / 详情 / 集数) | 可用;解析出的 15 条线路里混着小红书 CDN 的宣传片,需要按域名过滤 |
| AniBT anibt.net | 种子流首选,中文字幕组 | 可用,1–4 秒 |
| AnimeTosho animetosho.org | 种子流兜底(HTML 解析) | 可用,但不少 tracker 已死 |
| AnimeFlow 声明式源 assets/sources/animeflow | src-girigiri / src-gpjda / src-mxdm 等,规则驱动的在线源 | 取决于站点 |
| Seanime Marketplace(jsDelivr 镜像) | 「片源」页列出上游生态的在线流 / 种子流 provider(本次实测 71 + 28 个,随上游清单变化) | GitHub raw 不可达,走 jsDelivr |

所有上游都由宿主代理并缓存(短期 60 秒 / 中期 10 分钟 / 片源目录 6 小时):浏览器直连会撞 CORS 与 Referer 防盗链,宿主侧没有这些限制,也不把上游 URL 暴露给页面。

中文名是怎么补的

AniList 只有罗马字 / 日文 / 英文标题,选它时海报墙天然是英文。宿主做了一层中文名映射:条目自带的 title.native 若是中文(有汉字、无假名)直接用;否则去 Animeko 同一季排期表
(/v1/schedule/season/{id} 与 /v1/schedule/airing)建 name + aliases 索引做精确匹配。
只有精确命中才改名——早先试过「Animeko 搜索取第一条」,把「薬屋のひとりごと」配成了「BanG Dream! 第三季」,比留着英文更糟,已废弃。索引缓存 6 小时;仍没命中的保留原名。

想要 100% 中文,切到 Animeko 数据源(它自带 nameCn)。

播放

PrismFlow(默认引擎)

自研播放器组件:哔哩哔哩的 DASH 视频轨 / 音频轨由页面自己做 MSE 合流,字幕走 libass(真 ASS 渲染,字体 / 字号 / 颜色 / 描边 / 阴影都在设置里改,落成服务端生成的 [V4+ Styles]),画面可选 Anime4K 锐化 / 超分。
不依赖外部进程,也不占独立窗口。

- 设置 → 播放 →「播放引擎」:PrismFlow(默认)或 内嵌 mpv(原生渲染,Anime4K 走 mpv 的 --glsl-shaders-append)。
- 同一套控制栏也用在别处:番剧、种子流、主题曲(OP/ED)播放器都是它,所以预加载段、清晰度菜单、键盘快捷键、画中画的表现一致。

在线流

1. 搜索:/bili/search、/online/search(FQDM / AniCh / src- 走统一抽象)。
2. 集数:/bili/season、/online/episodes。
3. 解析:/online/resolve 内部完成;宿主带 Referer 拉流并透传 Range(206)。
4. 播放:/MSE 读 /online/stream,或转码链路 /torrent/transcode。

死线路自动换线:streamCandidates() 会先问扩展要候选。FQDM 这类 MacCMS 站点的 sid 是每部番一份,
换线必须重写集数 id,不能只换名字(lib/fqdm.js 的 alternates());AniCh 的候选是画质档位。
候选按「用户点的那条 → 其余 → 记忆过能播的」排序,最多试 6 条,能播过的记进 goodLine。
(能播的线路常在列表第 5~11 位,只试三条时经常三条全死。)

广告线路过滤:AniCh 的 /vod/{id}/{ep} 里 全集(全高清-1080P) 那几条实际指向小红书 CDN 的宣传片,宿主按域名丢掉,免得 ffmpeg 报 not in allowed_segment_extensions 或播出一段广告。

清晰度:哔哩哔哩走 qn 阶梯(4K / 1080P+ / 720P / 480P / 360P),换档时把当前秒数通过 startAt 带回去,不回到片头;没有 caption 的源(FQDM / AniCh)档位名直接用站点给的真实线路名,而不是编一个「线路 3」。

种子流

- 默认内置引擎(webtorrent),加入磁力时不能传 announce: [],否则会覆盖磁力自带的 tracker,而本网络 DHT 不可靠,元数据永远拿不到。元数据通常 4–20 秒。
- /torrent/stream?hash=&index=:从 swarm 读文件、支持 Range(206);元数据未就绪立刻返回 409 metadata-pending,不让请求挂死。
- 播不了的编码(HEVC / 10bit / AV1)走 /torrent/transcode,ffmpeg 重封装或转码后再推给播放器。
- 也可切到 qBittorrent(/api/v2,auth/login 取 SID,403 自动重登重放)。
- 「用播放器打开」把流 URL 交给本机 mpv / VLC / PotPlayer(自动探测),并带上当前秒数与标题。

进度条、跳转与 fMP4

在线流的 fMP4 片段没有容器时长,因此这类流的总长显示 --:--、进度条不可拖——这是流本身的限制,不是播放器的 bug。插件对能拿到时长的链路(转码、种子流、哔哩哔哩 DASH 的 duration)会显示真实长度;哔哩哔哩的分段 DASH 没有索引表,拖动 = 换一条从目标秒开始的流(/bili/stream?…&start=秒,宿主用 ffmpeg -ss 起拼),页面把「已跳过多少秒」记在 offset 里,进度条显示 offset + 流内秒数。

拖动时用独立的 scrub 状态,松手才落回真实位置,不会被 timeupdate 抢回去;±85s、←/→、A/D 与进度条共用同一条 seek 路径。ffmpeg 转码流不支持 Range,那条链路上进度条是禁用的。

外部播放器与画质增强

播放器面板里的齿轮会列出本机检测到的 mpv / VLC / PotPlayer,点一下就把流交给它,并带上当前秒数(mpv --start=、VLC --start-time=、PotPlayer /seek=)与本集标题。

- Anime4K:off / 快速 / 均衡 / 高质量,着色器随插件发布在 assets/anime4k/(49 个 .glsl),以 --glsl-shaders-append= 逐条交给 mpv;预设只用 Restore_CNN_ + Upscale_CNN_x2_ + Clamp_Highlights, GAN / UL / VL 在 1080p 实时解码下撑不住。VLC 与 PotPlayer 没有等价机制。
- 自定义命令:设置里填模板即可接管播放器选择,占位符 {url} {start} {title} {shaders}。
- 内嵌 mpv:mpv --wid=<窗口句柄> --geometry=<面板矩形> 直接画在面板区域,再走 --input-ipc-server 把播放 / 暂停 / ±30s / 速度 / 音量 / 位置接回面板(lib/mpv.js)。拿不到窗口句柄时退化成独立窗口。

追番与详情

- 追番表存在浏览器 localStorage(dsh-anime:watchlist:v1),不联网、不占宿主状态。
- 详情页头部:横幅、封面、标题、别名、评分 / 类型 / 放送状态 / 季度 / 集数 / 制作组 / 来源、追番按钮、 AniList 外链、预告、下一话倒计时。
- 观看顺序(anilist/watchorder):按 AniList 的 relations 拓扑排序给出「前作 → 本作 → 续作」链,当前番剧高亮,每一行带封面、类型、集数、年份;点任意一条跳到那部作品的详情,可层层返回。
- 角色与关联:角色卡(声优、性别、年龄、生日、血型、简介)+ 关联作品。

主题曲(OP / ED)

详情页的「主题曲」标签里是来源切换(选中哪个才查哪个),默认 animethemes:

| 来源 | 说明 |
| animethemes.moe | 按 AniList id 取官方 OP/ED 片段(/animethemes/themes)。注意 filter[has][]=resources 这类数组写法是必须的,标量会被判 The filter.resource must be an array |
| 哔哩哔哩 | 复用 bili/videos 搜索 + bili/video 取 cid + bili/stream 代取字节,按视频标题搜「番名 OP」 |

本机 api.animethemes.moe 常被 Cloudflare 挑战(403 / 422 / 500),页面会把真实失败原因原文显示出来,并提供一键切到哔哩哔哩。两个来源的播放器都是同款 Player(不是浏览器原生 ),哔哩哔哩那条带 qn 阶梯,切换清晰度不丢位置。

下载

两种来源共用一张任务表,默认落盘到系统「下载」文件夹,历史写 downloads.json:

- 在线流:播放器右下角下载图标 → /downloads/stream,宿主自己带 Referer/Cookie 抓流,先写 名字.mp4.part 再改名。产物按扩展名交给 ffmpeg(写 .part 时它认不出容器,会报 Unable to find a suitable output format),所以实际是写 .part + 显式 -f mp4。
- 种子流:/downloads/torrent,宿主轮询引擎进度,下完复制进下载目录。
- 两者都有进度、速度、取消(取消时删 .part)、打开所在目录;界面按 Motrix 的任务行 / 进度条 / 状态徽标做的。
- 下载目录三种用法:直接填路径、「浏览…」(宿主优先用 Electron dialog.showOpenDialog,取不到退回 PowerShell + WinForms)、「用默认」。也可以用环境变量 DSH_ANIME_DOWNLOAD_DIR 覆盖。

账号与隐私

| 站点 | 登录方式 | 昵称 / 头像取自 |
| 哔哩哔哩 | 扫码(或粘贴 SESSDATA=... Cookie) | /x/web-interface/nav 的 uname / face |
| AniList | Personal Access Token | Viewer { name avatar { medium } } |
| Bangumi | Access Token | /v0/me 的 username / avatar.large |
| AniCh | 邮箱 + 密码(账号卡内嵌在 AniCh 索引 这个数据源的偏好里,不在全局账号页) | 站点 /ajax_login 后取昵称与头像 |

凭据只写进本机的 $DSH_HOME/state/dsh-anime/accounts.json,页面拿到的永远是掩码 (loggedIn / name / face / masked),不把密钥回传渲染进程;accounts.json 按 mtime 校验再读,两个宿主实例同时存在时不会互相读到旧缓存。仓库里不含任何账号或 token。

设置

左侧分组导航,右侧内容卡,改完点「保存设置」写回 $DSH_HOME/state/dsh-anime/settings.json;「恢复默认」立刻重置并重建 BT 引擎。

| 分区 | 内容 |
| 界面 | 番剧名字用哪种语言(罗马音 / 日语原名 / 中文 / 英语)、插件界面语言(跟随应用 / 简体中文 / English) |
| 视频播放 | 内嵌还是外部播放器、默认清晰度、自动连播、非 MP4 片源怎么处理(自动 / 只重封装 / 必要时转码) |
| 桌面播放器 | 检测到的 mpv / VLC / PotPlayer,以及一条自定义命令模板 |
| 种子流 | 默认片源(自动 → AniBT → AnimeTosho)、上传限速、缓存目录、看完自动清理分钟数 |
| qBittorrent | 地址 / 用户名 / 密码(留空表示保留旧密码) |
| 下载 | 下载目录、完成后保留 .torrent、保留历史 |

名字语言(ui.titleLanguage)四组候选按顺序取第一个非空的,中文模式要求「含简体字标记」——纯汉字的日文标题(如 呪術廻戦 死滅回游 前編)不会被误判成中文,会退化到下一个候选。

界面语言(ui.locale):文案在 client.js 里是 ZH / EN 两份字典,通过
ctx.locale.register(NS, { zh, en }) 交给宿主;auto 时跟随应用。dev/audit-tkeys.mjs 会检查
T('key') 用到的键在两边都存在。

HTTP API

全部挂在 /dsh-anime/api/ 下。上游失败统一回 200 {ok:false, error:{message}}(媒体流用 409), 404 只留给未知路由。写操作同时接受 POST <route> 和 GET <route>?payload=<json> (DSH 桌面壳会把页面的 POST 以没有 body 的 GET 交给宿主)。

| 分组 | 路由 |
| 元数据 | health、anilist/season|trending|airing|search|media|browse|watchorder、animeko/season|airing|search|subject、bangumi/calendar|search|season|subject|episodes|collection(s)|mark、anich/season|search|detail|episodes |
| 在线流 | online/sources|browse|search|episodes|servers|resolve|stream|duration、bili/search|season|episode|stream|videos|video|chapters |
| 种子流 | torrent/search|add|remove|files|stream|transcode|status|list|config|open-player |
| 下载 | downloads、downloads/bili|torrent|stream|cancel|remove|clear|reveal |
| 账号 | accounts、accounts/save|logout|qr|qr/status|code|password|register |
| 扩展 | extensions、extensions/save|toggle|action|source|source-save|source-restore |
| 片源 / 主题曲 | sources、sources/manifest、animethemes/themes |
| 设置 / 播放器 | settings、settings/save|reset|pick-dir、player/open|shaders|shaders/open、player/embed |
| 其他 | stream/cache|stream/cache/clear、client/heartbeat |

架构

package.json dsh.bundle.patch + dsh.client(platform: web) cordis.patch.yml insert: id dsh-anime → name '@local/dsh-anime' index.js Host 半:数据源、代理与缓存、扩展注册表、BT 引擎、下载队列、播放器桥 client.js Client 半:侧边栏图标 + 主面板(React,单文件自带样式与 i18n) lib/anilist.js AniList GraphQL(季度 / 搜索 / 详情 / 观看顺序) lib/bangumi.js Bangumi v0 + 镜像降级 lib/anich.js AniCh 解析 + 索引(登录 / 注册 / 发码 / 线路过滤) lib/fqdm.js FQDM(MacCMS)线路、换线与封面 lib/bilibili.js 哔哩哔哩(搜索 / 季度 / DASH / 字幕 / 分段 seek) lib/wbi.js 哔哩哔哩 WBI 签名 lib/chapters.js 章节 / 看点(pgc 时间轴) lib/anibt.js AniBT 片源(集号与合集识别) lib/animetosho.js AnimeTosho HTML 解析兜底 lib/websource.js AnimeFlow 声明式源(JSON / HTML 规则 → 扩展) lib/html.js HTML 解析(声明式源共用) lib/xpath.js 最小 XPath 实现(声明式源选择器) lib/extensions.js 扩展注册表:manifest、schema 校验、启用状态、调用与超时 lib/torrent-engine.js 内置 webtorrent 引擎 + qBittorrent 客户端 + ffmpeg 转码 + 打开外部播放器 lib/render-stream.js MSE manifest 生成(DASH 分片 / 时长 / 初始分片) lib/mpv.js 内嵌 mpv(wid + JSON IPC) lib/downloads.js 下载队列(在线流 / 种子流共用) lib/accounts.js 账号存储(掩码输出、mtime 校验) lib/settings.js 设置读写与字段校验
locale/{zh,en}.json 插件市场里显示的名称与简介
assets/anime4k/ Anime4K 着色器;assets/vendor/ 内置 libass、jassub、anime4k-webgpu dev/ 开发期脚本(检查、探测、预览),见 dev/README.md docs/status.md 开发日志:每一轮改了什么、拿什么证明、还差什么

路由表是进程级的:宿主不允许同一 exact 路由注册两次,而插件会被反复 apply()(重载、切面板、第二实例)。所以每条路径在进程里只注册一次,注册的是转发器——它每次请求都从进程级表
(globalThis[Symbol.for('dsh-anime.routes')])里取「最新一次 apply 发布的处理器」。
每次 apply 会在日志留一行 routes published: N handlers … build <号>,页头出现「宿主需重启」标签说明 Host 与 Client 的 EXPECTED_BUILD 对不上。

扩展即插件:数据源、种子流、在线流三种扩展走同一个注册表,页面只读 manifest 就能画出卡片、开关和偏好弹窗;关掉的扩展对应路由直接回「已停用」。AnimeFlow 的声明式源也注册成扩展(src-)。

开发

结构检查(不需要 DSH):client.js 能不能被载入、apply() 注册到几个 slot
node dev/load-client.mjs

文案键、CSS 钩子、路由、lib 依赖的静态审计
node dev/audit-tkeys.mjs && node dev/audit-routes.mjs && node dev/audit-libs.mjs

用假 React 走完整组件树,检查某个页面渲染不报错、没有裸 React 元素泄漏
node dev/render-check.mjs anilist 16498

默认源 / 主题曲播放器的回归守卫
node dev/check-default-sources.mjs && node dev/check-theme-player.mjs

需要运行中的宿主的测试(默认 DSH_ANIME_API=http://127.0.0.1:<port>/dsh-anime/api,端口是 DSH Web 的端口):

node dev/test-host.mjs # 全部 Host 路由的端到端冒烟(真 node:http + 假 ctx)
node dev/test-stream.mjs # 哔哩哔哩:搜索 → 季度 → 播放地址 → Range 流
node dev/test-torrent-routes.mjs # 种子:搜索 → 加入 → 元数据 → Range 流(真磁力,几十秒)
node dev/probe-anich.mjs # AniCh:搜索 → 集数 → 线路 → 解析 → 首字节
node dev/test-reload.mjs # apply → dispose → apply,重载后不撞重复路由且由新代码应答

改完代码怎么让它生效:client.js 在 DSH 窗口里按 Ctrl+R 重载;index.js / lib/.js 改的是宿主半,必须在插件市场里把「追番」关一次再打开(或重启 DSH)——只改文件不会热加载。

更多开发工具的分类清单见 dev/README.md,历史决策与逐轮证据见 docs/status.md。

已知限制

- Bangumi OAuth:数据接口能走镜像,但登录端点 bgm.tv/oauth/access_token 没有被镜像代理,需要自备代理或自建反代。
- animethemes.moe:本机常被 Cloudflare 挡住,主题曲默认落到哔哩哔哩。
- 在线流 fMP4 没有容器时长:总长 --:--、进度条不可拖;有 duration 的链路正常。
- 种子 tracker 质量:不少公开 tracker 已死,AnimeTosho 结果里常有 0 做种的条目。
- AniCh 注册需要邮箱验证码,插件只提供入口,不会代发。

许可与第三方

本项目以 MIT 许可发布(见 LICENSE)。

assets/vendor/ 内是第三方构建产物,各自沿用原许可: libass-wasm、jassub、 anime4k-webgpu、Anime4K; assets/anime4k/.glsl 来自 Anime4K(MIT)。BT 引擎是 WebTorrent(MIT)。

数据来自 AniList、Bangumi 番组计划、Animeko、哔哩哔哩 等公开接口;插件只做客户端聚合与播放,不托管任何内容。

界面与交互参考了 Seanime(扩展 / 片源目录 / 播放器交互)与 Motrix(下载列表)。
预览分享卡片长什么样 ↓Elisoar111/dsh-anime
分享 · DeepSeek Harness Hub
Elisoar111/dsh-anime

在侧边栏追番:季度新番、放送表、在线播放与下载

这是你的仓库?认领条目

回到顶部