Merge pull request #97 from cxfksword/10.11

support jellyfin 10.11
This commit is contained in:
cxfksword
2025-10-29 22:03:38 +08:00
committed by GitHub
42 changed files with 579 additions and 113 deletions

148
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,148 @@
# Jellyfin Plugin Danmu - AI 编码助手指南
## 项目概述
一个为 Jellyfin 开发的弹幕自动下载插件支持从多个视频平台B站、优酷、爱奇艺、腾讯视频、芒果TV自动下载和管理中文弹幕。支持 XML 和 ASS 字幕格式。
**核心技术栈**: C# .NET 9.0, Jellyfin Plugin API, ILRepack用于依赖合并
## 架构:多弹幕源设计模式
### 弹幕源系统 (`Jellyfin.Plugin.Danmu/Scrapers/`)
核心架构是一个**可插拔的弹幕源系统**,每个视频平台都实现 `AbstractScraper`
```csharp
// 每个平台Bilibili/、Youku/、Iqiyi/ 等)需要实现:
public abstract class AbstractScraper {
Task<List<ScraperSearchInfo>> Search(BaseItem item);
Task<string?> SearchMediaId(BaseItem item);
Task<ScraperMedia?> GetMedia(BaseItem item, string id);
Task<ScraperEpisode?> GetMediaEpisode(BaseItem item, string id);
Task<ScraperDanmaku?> GetDanmuContent(BaseItem item, string commentId);
}
```
**重要说明**:
- 弹幕源通过 `Plugin.cs` 中的 `IApplicationHost.GetExports<AbstractScraper>()` 自动发现
- 顺序由 `DefaultOrder` 属性和用户配置控制
- 每个弹幕源有唯一的 `ProviderId`(如 `BilibiliID`),用于 Jellyfin 元数据存储
- 参考 `Scrapers/Bilibili/Bilibili.cs` 的实现示例
### 事件驱动的弹幕处理
插件使用 Jellyfin 的媒体库事件和**防抖队列处理**机制:
1. `PluginStartup.cs` 订阅 `ILibraryManager.ItemAdded/ItemUpdated` 事件
2. 事件在 `LibraryManagerEventsHelper` 中排队,使用 10 秒防抖定时器
3. 批量处理:匹配媒体 → 搜索弹幕源 → 下载弹幕
4. 结果保存为 `.xml` 文件,与视频文件同目录(如 `movie.mp4``movie.xml`
**关键类**:
- `LibraryManagerEventsHelper.QueueItem()` - 添加项目到处理队列
- `LibraryManagerEventsHelper.ProcessQueuedMovieEvents()` - 批量处理电影
- `LibraryManagerEventsHelper.ProcessQueuedSeasonEvents()` - 处理电视剧季
## 开发工作流
### 构建项目
```bash
dotnet restore
dotnet publish --configuration=Release Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
```
输出位置: `Jellyfin.Plugin.Danmu/bin/Release/net9.0/Jellyfin.Plugin.Danmu.dll`
**ILRepack 集成**: Release 构建会通过 `ILRepack.targets` 自动将依赖RateLimiter、ComposableAsync、Google.Protobuf、SharpZipLib合并到单个 DLL。这对 Jellyfin 插件部署至关重要。
### 运行测试
```bash
dotnet test --no-restore --verbosity normal
```
测试使用 MSTest 框架和 Moq 进行模拟:
- `*ApiTest.cs` - API 响应解析测试
- `*Test.cs` - 端到端弹幕源测试
- 模拟 `ILibraryManager``IFileSystem` 以实现隔离测试
### 开发环境安装
1. 构建 Release 配置
2. 创建 `danmu/` 文件夹并复制 `Jellyfin.Plugin.Danmu.dll` 到其中
3. 将文件夹移动到 Jellyfin 的 `data/plugins/` 目录
4. 重启 Jellyfin
## 关键模式与约定
### Provider ID 映射
每个弹幕源使用其 `ProviderId` 在 Jellyfin 元数据中存储匹配结果:
```csharp
item.ProviderIds["BilibiliID"] = "123456"; // 季/媒体 ID
// 对于剧集,使用格式:"seasonId_episodeId"
```
**特殊处理**: B站支持 `BV` 视频 ID用户上传内容`av` IDUGC视频以及番剧的 season ID。
### 弹幕 → ASS 转换 (`Core/Danmaku2Ass/`)
`Creater` 类将 XML 弹幕转换为 ASS 字幕,具有:
- 碰撞检测(`Collision.cs`- 使用基于行的跟踪防止重叠
- 显示模式(`Display.cs`- 滚动、顶部锚定、底部锚定
- 可配置的字体、速度、透明度、行数
**重要**: ASS 生成是可选的,通过插件配置 `ToAss = true` 启用。
### API 端点 (`Controllers/DanmuController.cs`)
为外部播放器提供的公共 REST API
- `GET /api/danmu/{id}` - 返回弹幕 URL 元数据
- `GET /api/danmu/{id}/raw` - 下载 XML 弹幕文件
- `GET /api/danmu/search?keyword=` - 跨所有弹幕源搜索
- `GET /api/{site}/danmu/{id}/episodes` - 获取剧集列表
- `GET /api/{site}/danmu/{cid}/download` - 通过评论 ID 下载弹幕
### 配置系统
`PluginConfiguration.cs` 使用 XML 序列化,特殊的 `Scrapers` 属性会:
1. 合并用户配置和新发现的弹幕源
2. 删除已废弃的弹幕源
3. 尊重用户定义的顺序和启用/禁用状态
### 异常处理
- `CanIgnoreException` - 表示预期的失败(如未找到弹幕),不应记录错误日志
- `FrequentlyRequestException` - 视频平台的速率限制异常
## 外部依赖与集成
### Jellyfin Plugin API
- 实现 `ISubtitleProvider` 用于字幕搜索 UI
- `IPluginServiceRegistrator` 用于依赖注入注册
- `BasePlugin<PluginConfiguration>` 用于配置管理
- `IScheduledTask` 用于定期媒体库扫描(`ScheduledTasks/ScanLibraryTask.cs`
### .NET 包
- `RateLimiter` - API 请求节流
- `Google.Protobuf` - B站 API protobuf 响应解析
- `SharpZipLib` - 弹幕数据解压缩
- `ComposableAsync.Core` - 异步工具库
### 平台专用 API
每个弹幕源都有一个 `*Api.cs`(如 `BilibiliApi.cs`)处理:
- HTTP 客户端和重试逻辑
- 响应反序列化JSON/Protobuf
- 速率限制和错误处理
## 命名约定
- 弹幕源:小写名称(如 `bilibili``youku`)用于用户界面显示
- Provider ID驼峰式 + `ID` 后缀(如 `BilibiliID`
- 事件类型:`EventType.Add``EventType.Update``EventType.Force`
- 文件扩展名:`.xml` 用于弹幕,`.ass` 用于字幕
## 测试视频平台 API
使用测试类如 `BilibiliApiTest.cs` 在集成前验证 API 响应。模拟 `ILibraryManager` 以避免单元测试中的 Jellyfin 依赖。
## 发布流程
1. 打标签:`git tag -a v1.2.3 -m "Release notes"`
2. GitHub Actions 自动构建和发布
3. `scripts/generate_manifest.py` 更新插件清单并生成校验和
4. 通过 `CN_DOMAIN` 环境变量支持国内镜像
## 常见陷阱
- **不要在字幕搜索中直接修改 `item.ProviderIds`** - 使用临时 item 副本以避免持久化错误的元数据
- **弹幕源必须在 `ServiceRegistrator.cs` 中注册**才能被发现
- **季匹配需要正确的 `IndexNumber`** 用于多季系列
- **ILRepack 仅在 Release 构建中运行** - Debug 构建有独立的 DLL
- **Jellyfin 的 `LocationType.Virtual`** 项目(没有季文件夹的系列)需要特殊处理

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
env:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
python-version: 3.8
project: Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
artifact: danmu
@@ -43,9 +43,9 @@ jobs:
- name: Build
run: |
dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 ${{ env.project }}
dotnet publish --nologo --no-restore --configuration=Release --framework=net9.0 ${{ env.project }}
mkdir -p artifacts
cp ./Jellyfin.Plugin.Danmu/bin/Release/net8.0/Jellyfin.Plugin.Danmu.dll ./artifacts/
cp ./Jellyfin.Plugin.Danmu/bin/Release/net9.0/Jellyfin.Plugin.Danmu.dll ./artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-dotnet@v3
id: dotnet
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Change default dotnet version
run: |
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json

View File

@@ -5,7 +5,7 @@ on:
tags: ["*"]
env:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
python-version: 3.8
project: Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
artifact: danmu
@@ -50,9 +50,9 @@ jobs:
- name: Build
run: |
dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
dotnet publish --nologo --no-restore --configuration=Release --framework=net9.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
mkdir -p artifacts
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.Danmu/bin/Release/net8.0/Jellyfin.Plugin.Danmu.dll
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.Danmu/bin/Release/net9.0/Jellyfin.Plugin.Danmu.dll
- name: Generate manifest
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
env:

View File

@@ -35,8 +35,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -67,8 +68,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{
@@ -101,8 +103,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -135,8 +138,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -169,8 +173,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -205,8 +210,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{

View File

@@ -27,8 +27,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -61,8 +62,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{

View File

@@ -37,8 +37,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -72,8 +73,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -109,8 +111,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@@ -16,6 +16,7 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using Moq;
@@ -42,8 +43,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -81,7 +83,8 @@ namespace Jellyfin.Plugin.Danmu.Test
mediaSourceManagerStub.Setup(x => x.GetPathProtocol(It.IsAny<string>())).Returns(MediaBrowser.Model.MediaInfo.MediaProtocol.File);
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -116,8 +119,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{
@@ -149,8 +153,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{
@@ -185,8 +190,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -220,8 +226,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{
@@ -256,8 +263,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -295,8 +303,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var mediaSourceManagerStub = new Mock<IMediaSourceManager>();
mediaSourceManagerStub.Setup(x => x.GetPathProtocol(It.IsAny<string>())).Returns(MediaBrowser.Model.MediaInfo.MediaProtocol.File);
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{

View File

@@ -28,8 +28,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -63,7 +64,8 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -100,7 +102,8 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{

View File

@@ -27,8 +27,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -61,8 +62,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -98,8 +100,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{

View File

@@ -37,8 +37,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -71,8 +72,9 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
@@ -107,7 +109,8 @@ namespace Jellyfin.Plugin.Danmu.Test
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var itemRepositoryStub = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(itemRepositoryStub.Object, libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{

View File

@@ -137,6 +137,10 @@ public class ScraperConfigItem
public class DanmuDownloadOption
{
/// <summary>
/// 弹幕自动匹配下载.
/// </summary>
public bool EnableAutoDownload { get; set; } = true;
/// <summary>
/// 检测弹幕数和视频剧集数需要一致才自动下载弹幕.
/// </summary>

View File

@@ -25,16 +25,17 @@
<fieldset class="verticalSection verticalSection-extrabottompadding">
<legend>
<h3>弹幕配置</h3>
<h3>弹幕下载配置</h3>
</legend>
<div class="checkboxList paperList checkboxList-paperList" id="Scrapers" name="Scrapers">
</div>
</fieldset>
<fieldset class="verticalSection verticalSection-extrabottompadding">
<legend>
<h3>弹幕匹配下载配置</h3>
</legend>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableAutoDownload" name="EnableAutoDownload" type="checkbox"
is="emby-checkbox" />
<span>弹幕自动匹配下载</span>
</label>
<div class="fieldDescription">勾选后,有新影片入库会自动匹配下载,不勾选需要自己搜索下载。</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
@@ -46,6 +47,15 @@
</div>
</fieldset>
<fieldset class="verticalSection verticalSection-extrabottompadding">
<legend>
<h3>弹幕源配置</h3>
</legend>
<div class="checkboxList paperList checkboxList-paperList" id="Scrapers" name="Scrapers">
</div>
</fieldset>
<fieldset id="dandanSection" class="verticalSection verticalSection-extrabottompadding" style="display: none;">
<legend>
<h3>弹弹play配置</h3>
@@ -143,6 +153,7 @@
document.querySelector('#AssLineCount').value = config.AssLineCount;
document.querySelector('#AssSpeed').value = config.AssSpeed;
document.querySelector('#EnableAutoDownload').checked = config.DownloadOption.EnableAutoDownload;
document.querySelector('#EnableEpisodeCountSame').checked = config.DownloadOption.EnableEpisodeCountSame;
document.querySelector('#WithRelatedDanmu').checked = config.Dandan.WithRelatedDanmu;
@@ -194,6 +205,7 @@
var download = new Object();
download.EnableEpisodeCountSame = document.querySelector('#EnableEpisodeCountSame').checked;
download.EnableAutoDownload = document.querySelector('#EnableAutoDownload').checked;
config.DownloadOption = download;
var dandan = new Object();

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Danmu</RootNamespace>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -21,8 +21,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Jellyfin.Controller" Version="10.9.0" />
<PackageReference Include="Jellyfin.Model" Version="10.9.0" />
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
<PackageReference Include="RateLimiter" Version="2.2.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>

View File

@@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -27,7 +28,7 @@ public class LibraryManagerEventsHelper : IDisposable
private readonly IMemoryCache _memoryCache;
private readonly MemoryCacheEntryOptions _pendingAddExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
private readonly MemoryCacheEntryOptions _danmuUpdatedExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(24*60) };
private readonly IItemRepository _itemRepository;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<LibraryManagerEventsHelper> _logger;
private readonly Jellyfin.Plugin.Danmu.Core.IFileSystem _fileSystem;
@@ -50,11 +51,12 @@ public class LibraryManagerEventsHelper : IDisposable
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="api">The <see cref="BilibiliApi"/>.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public LibraryManagerEventsHelper(ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager)
public LibraryManagerEventsHelper(IItemRepository itemRepository, ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager)
{
_queuedEvents = new List<LibraryEvent>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
_itemRepository = itemRepository;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<LibraryManagerEventsHelper>();
_fileSystem = fileSystem;
@@ -282,14 +284,8 @@ public class LibraryManagerEventsHelper : IDisposable
var mediaId = await scraper.SearchMediaId(currentItem).ConfigureAwait(false);
if (string.IsNullOrEmpty(mediaId))
{
_logger.LogInformation("[{0}]元数据匹配失败:{1} ({2}),尝试文件匹配", scraper.Name, item.Name, item.ProductionYear);
mediaId = await scraper.SearchMediaIdByFile((Movie)currentItem).ConfigureAwait(false);
if (string.IsNullOrEmpty(mediaId))
{
_logger.LogInformation("[{0}]文件匹配失败:{1}", scraper.Name, currentItem.Path);
continue;
}
_logger.LogInformation("[{0}]元数据匹配失败:{1} ({2})", scraper.Name, item.Name, item.ProductionYear);
continue;
}
var media = await scraper.GetMedia(item, mediaId);
@@ -875,7 +871,7 @@ public class LibraryManagerEventsHelper : IDisposable
item.ProviderIds[pair.Key] = pair.Value;
}
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
_itemRepository.SaveItems(new[] { item }, CancellationToken.None);
}
}
_logger.LogInformation("更新epid到元数据完成。item数{0}", queue.Count);
@@ -989,7 +985,7 @@ public class LibraryManagerEventsHelper : IDisposable
// 保存指定弹幕元数据
item.ProviderIds[providerId] = providerVal;
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
_itemRepository.SaveItems(new[] { item }, CancellationToken.None);
}

View File

@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.Danmu.Model;
using Jellyfin.Plugin.Danmu.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
@@ -20,6 +21,14 @@ namespace Jellyfin.Plugin.Danmu
private readonly LibraryManagerEventsHelper _libraryManagerEventsHelper;
private readonly ILogger<PluginStartup> _logger;
public PluginConfiguration Config
{
get
{
return Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginStartup"/> class.
/// </summary>
@@ -60,6 +69,11 @@ namespace Jellyfin.Plugin.Danmu
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void LibraryManagerItemAdded(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!Config.DownloadOption.EnableAutoDownload)
{
return;
}
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Movie and not Episode and not Series and not Season)
{
@@ -83,6 +97,11 @@ namespace Jellyfin.Plugin.Danmu
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private void LibraryManagerItemUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!Config.DownloadOption.EnableAutoDownload)
{
return;
}
// Don't do anything if it's not a supported media type
if (itemChangeEventArgs.Item is not Movie and not Episode and not Series and not Season)
{

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "https://www.bilibili.com/bangumi/play/ep{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Bilibili.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Bilibili.ScraperProviderId, out var externalId))
{
if (externalId.StartsWith("bv", StringComparison.OrdinalIgnoreCase) || externalId.StartsWith("av", StringComparison.OrdinalIgnoreCase))
{
yield return $"https://www.bilibili.com/{externalId}";
}
else
{
yield return $"https://www.bilibili.com/bangumi/play/ss{externalId}";
}
}
break;
case Episode episode:
if (item.TryGetProviderId(Bilibili.ScraperProviderId, out externalId))
{
yield return $"https://www.bilibili.com/bangumi/play/ep{externalId}";
}
break;
case Movie:
if (item.TryGetProviderId(Bilibili.ScraperProviderId, out externalId))
{
yield return $"https://www.bilibili.com/bangumi/play/ep{externalId}";
}
break;
}
}
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
public string UrlFormatString => "https://www.bilibili.com/bangumi/play/ep{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}

View File

@@ -23,9 +23,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
/// <inheritdoc />
public string UrlFormatString => "https://www.bilibili.com/bangumi/play/ss{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "#";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Dandan.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Dandan.ScraperProviderId, out var externalId))
{
yield return $"https://api.dandanplay.net/api/v2/bangumi/{externalId}";
}
break;
case Episode episode:
if (item.TryGetProviderId(Dandan.ScraperProviderId, out externalId))
{
yield return "#";
}
break;
case Movie:
if (item.TryGetProviderId(Dandan.ScraperProviderId, out externalId))
{
yield return $"https://api.dandanplay.net/api/v2/bangumi/{externalId}";
}
break;
}
}
}

View File

@@ -21,10 +21,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://api.dandanplay.net/api/v2/bangumi/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}

View File

@@ -23,9 +23,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://api.dandanplay.net/api/v2/bangumi/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Iqiyi.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Iqiyi.ScraperProviderId, out var externalId))
{
yield return $"https://www.iqiyi.com/v_{externalId}.html";
}
break;
case Episode episode:
if (item.TryGetProviderId(Iqiyi.ScraperProviderId, out externalId))
{
yield return $"https://www.iqiyi.com/v_{externalId}.html";
}
break;
case Movie:
if (item.TryGetProviderId(Iqiyi.ScraperProviderId, out externalId))
{
yield return $"https://www.iqiyi.com/v_{externalId}.html";
}
break;
}
}
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}

View File

@@ -23,9 +23,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "#";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Mgtv.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Mgtv.ScraperProviderId, out var externalId))
{
yield return $"https://www.mgtv.com/h/{externalId}.html";
}
break;
case Episode episode:
if (item.TryGetProviderId(Mgtv.ScraperProviderId, out externalId))
{
if (episode.Season?.TryGetProviderId(Mgtv.ScraperProviderId, out var seasonExternalId) == true)
{
yield return $"https://www.mgtv.com/b/{seasonExternalId}/{externalId}.html";
}
else
{
yield return "#";
}
}
break;
case Movie:
if (item.TryGetProviderId(Mgtv.ScraperProviderId, out externalId))
{
yield return $"https://www.mgtv.com/h/{externalId}.html";
}
break;
}
}
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.mgtv.com/h/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}

View File

@@ -23,9 +23,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.mgtv.com/h/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "#";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Tencent.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Tencent.ScraperProviderId, out var externalId))
{
yield return $"https://v.qq.com/x/cover/{externalId}.html";
}
break;
case Episode episode:
if (item.TryGetProviderId(Tencent.ScraperProviderId, out externalId))
{
if (episode.Season?.TryGetProviderId(Tencent.ScraperProviderId, out var seasonExternalId) == true)
{
yield return $"https://v.qq.com/x/cover/{seasonExternalId}/{externalId}.html";
}
else
{
yield return "#";
}
}
break;
case Movie:
if (item.TryGetProviderId(Tencent.ScraperProviderId, out externalId))
{
yield return $"https://v.qq.com/x/cover/{externalId}.html";
}
break;
}
}
}

View File

@@ -22,9 +22,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}

View File

@@ -23,9 +23,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}

View File

@@ -21,10 +21,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.ExternalId
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "https://v.youku.com/v_show/id_{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Web;
using System.Collections.Generic;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.ExternalId;
/// <summary>
/// External URLs for Danmu.
/// </summary>
public class ExternalUrlProvider : IExternalUrlProvider
{
/// <inheritdoc/>
public string Name => Youku.ScraperProviderName;
/// <inheritdoc/>
public IEnumerable<string> GetExternalUrls(BaseItem item)
{
switch (item)
{
case Season season:
if (item.TryGetProviderId(Youku.ScraperProviderId, out var externalId))
{
yield return $"https://v.youku.com/v_nextstage/id_{externalId}.html";
}
break;
case Episode episode:
if (item.TryGetProviderId(Youku.ScraperProviderId, out externalId))
{
var decodeExternalId = HttpUtility.UrlDecode(externalId).Replace("||", "==");
yield return $"https://v.youku.com/v_show/id_{decodeExternalId}.html";
}
break;
case Movie:
if (item.TryGetProviderId(Youku.ScraperProviderId, out externalId))
{
yield return $"https://v.youku.com/v_nextstage/id_{externalId}.html";
}
break;
}
}
}

View File

@@ -5,6 +5,7 @@ using Jellyfin.Plugin.Danmu.Scrapers;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.Persistence;
namespace Jellyfin.Plugin.Danmu
{
@@ -27,7 +28,7 @@ namespace Jellyfin.Plugin.Danmu
});
serviceCollection.AddSingleton((ctx) =>
{
return new LibraryManagerEventsHelper(ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<Jellyfin.Plugin.Danmu.Core.IFileSystem>(), ctx.GetRequiredService<ScraperManager>());
return new LibraryManagerEventsHelper(ctx.GetRequiredService<IItemRepository>(), ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<Jellyfin.Plugin.Danmu.Core.IFileSystem>(), ctx.GetRequiredService<ScraperManager>());
});
}
}

View File

@@ -69,7 +69,7 @@ ass格式
1. Clone or download this repository
2. Ensure you have .NET Core SDK 8.0 setup and installed
2. Ensure you have .NET Core SDK 9.0 setup and installed
3. Build plugin with following command.
@@ -83,7 +83,7 @@ dotnet publish --configuration=Release Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Dan
1. Build the plugin
2. Create a folder, like `danmu` and copy `./Jellyfin.Plugin.Danmu/bin/Release/net8.0/Jellyfin.Plugin.Danmu.dll` into it
2. Create a folder, like `danmu` and copy `./Jellyfin.Plugin.Danmu/bin/Release/net9.0/Jellyfin.Plugin.Danmu.dll` into it
3. Move folder `danmu` to jellyfin `data/plugins` folder

View File

@@ -26,7 +26,7 @@ def generate_version(filepath, version, changelog):
return {
'version': f"{version}.0",
'changelog': changelog,
'targetAbi': '10.9.0.0',
'targetAbi': '10.11.0.0',
'sourceUrl': f'https://github.com/cxfksword/jellyfin-plugin-danmu/releases/download/v{version}/danmu_{version}.0.zip',
'checksum': md5sum(filepath),
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S')