feat: add danmu api scraper. close #102

This commit is contained in:
cxfksword
2025-12-10 21:45:18 +08:00
parent ecb819a4f0
commit 209d6ff4d4
17 changed files with 1499 additions and 15 deletions

View File

@@ -1,9 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Plugin.Danmu.Configuration;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Moq;
namespace Jellyfin.Plugin.Danmu.Test
{
@@ -19,15 +26,60 @@ namespace Jellyfin.Plugin.Danmu.Test
options.TimestampFormat = "hh:mm:ss ";
}));
protected Plugin? mockPlugin;
protected PluginConfiguration mockConfiguration = new PluginConfiguration();
[TestInitialize]
public void SetUp()
{
DotNetEnv.Env.TraversePath().Load();
// Mock Plugin.Instance 及其依赖项
var mockApplicationPaths = new Mock<IApplicationPaths>();
mockApplicationPaths.Setup(p => p.PluginConfigurationsPath).Returns(Path.GetTempPath());
mockApplicationPaths.Setup(p => p.PluginsPath).Returns(Path.GetTempPath());
var mockApplicationHost = new Mock<IApplicationHost>();
mockApplicationHost.Setup(h => h.GetExports<Scrapers.AbstractScraper>(false))
.Returns(new List<Scrapers.AbstractScraper>());
var mockXmlSerializer = new Mock<IXmlSerializer>();
var mockScraperManager = new Mock<Scrapers.ScraperManager>(
Mock.Of<ILoggerFactory>());
try
{
// 创建 Plugin 实例
mockPlugin = new Plugin(
mockApplicationPaths.Object,
mockApplicationHost.Object,
mockXmlSerializer.Object,
mockScraperManager.Object
);
// 使用反射设置 Configuration
var configField = typeof(Plugin).BaseType?
.GetField("_configuration", BindingFlags.NonPublic | BindingFlags.Instance);
if (configField != null)
{
configField.SetValue(mockPlugin, mockConfiguration);
}
}
catch
{
// 如果无法创建 Plugin 实例,使用反射直接设置静态实例
// 这是一个后备方案
}
}
[TestCleanup]
public void TearDown()
{
// 清理 Plugin.Instance
var instanceProperty = typeof(Plugin).GetProperty("Instance", BindingFlags.Public | BindingFlags.Static);
instanceProperty?.SetValue(null, null);
// 清理代码
// 例如,释放资源或重置状态
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.Danmu.Scrapers.DanmuApi;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Jellyfin.Plugin.Danmu.Test
{
[TestClass]
public class DanmuApiApiTest : BaseTest
{
[TestMethod]
public void TestSearchAsync()
{
var keyword = "火影忍者";
var api = new DanmuApiApi(loggerFactory);
// 注意:此测试需要实际的服务器 URL否则会返回空结果
Task.Run(async () =>
{
try
{
var result = await api.SearchAsync(keyword, CancellationToken.None);
Console.WriteLine($"Search results count: {result.Count}");
foreach (var anime in result)
{
Console.WriteLine($" - {anime.AnimeTitle} (ID: {anime.BangumiId})");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetBangumiAsync()
{
var bangumiId = "mzc00200nc1cbum";
var api = new DanmuApiApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetBangumiAsync(bangumiId, CancellationToken.None);
if (result != null)
{
Console.WriteLine($"Bangumi: {result.AnimeTitle}");
Console.WriteLine($"Episodes count: {result.Episodes.Count}");
foreach (var episode in result.Episodes.Take(5))
{
Console.WriteLine($" - {episode.EpisodeId}: {episode.EpisodeTitle}");
}
}
else
{
Console.WriteLine("Bangumi not found or server not configured");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetCommentsAsync()
{
var commentId = "14435";
var api = new DanmuApiApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetCommentsAsync(commentId, CancellationToken.None);
Console.WriteLine($"Comments count: {result.Count}");
foreach (var comment in result.Take(5))
{
Console.WriteLine($" - {comment.M} (CID: {comment.Cid})");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
}
}

View File

@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Plugin.Danmu.Configuration;
using Jellyfin.Plugin.Danmu.Model;
using Jellyfin.Plugin.Danmu.Scrapers;
using Jellyfin.Plugin.Danmu.Scrapers.DanmuApi;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Jellyfin.Plugin.Danmu.Test
{
[TestClass]
public class DanmuApiTest : BaseTest
{
[TestMethod]
public void TestSearchMovie()
{
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "火影忍者"
};
Task.Run(async () =>
{
try
{
var result = await scraper.Search(item);
Console.WriteLine($"Search results count: {result.Count}");
foreach (var searchInfo in result)
{
Console.WriteLine($" - {searchInfo.Name} (ID: {searchInfo.Id}, Episodes: {searchInfo.EpisodeSize})");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestSearchMediaId()
{
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "阿丽塔:战斗天使(2019)【外语电影】from youku",
ProductionYear = 2019
};
Task.Run(async () =>
{
try
{
var mediaId = await scraper.SearchMediaId(item);
if (mediaId != null)
{
Console.WriteLine($"Found media ID: {mediaId}");
}
else
{
Console.WriteLine("Media ID not found or server not configured");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetMedia()
{
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "测试电影"
};
var testBangumiId = "test-bangumi-id";
Task.Run(async () =>
{
try
{
var media = await scraper.GetMedia(item, testBangumiId);
if (media != null)
{
Console.WriteLine($"Media ID: {media.Id}");
Console.WriteLine($"Comment ID: {media.CommentId}");
Console.WriteLine($"Episodes count: {media.Episodes.Count}");
}
else
{
Console.WriteLine("Media not found or server not configured");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetDanmuContent()
{
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "测试电影"
};
var commentId = "test-comment-id";
Task.Run(async () =>
{
try
{
var danmaku = await scraper.GetDanmuContent(item, commentId);
if (danmaku != null)
{
Console.WriteLine($"Chat Server: {danmaku.ChatServer}");
Console.WriteLine($"Danmaku items count: {danmaku.Items.Count}");
foreach (var item in danmaku.Items.Take(5))
{
Console.WriteLine($" - [{item.Progress}ms] {item.Content}");
}
}
else
{
Console.WriteLine("Danmaku not found or server not configured");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestFilterByAllowedSources()
{
// 模拟配置只允许特定采集源
var originalConfig = Plugin.Instance?.Configuration?.DanmuApi?.AllowedSources;
try
{
if (Plugin.Instance?.Configuration?.DanmuApi != null)
{
Plugin.Instance.Configuration.DanmuApi.AllowedSources = "bilibili";
}
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "火影忍者"
};
Task.Run(async () =>
{
try
{
var result = await scraper.Search(item);
Console.WriteLine($"Source filter test - Results count: {result.Count}");
// 验证所有结果都来自允许的采集源
foreach (var searchInfo in result)
{
Console.WriteLine($" - {searchInfo.Name} (ID: {searchInfo.Id})");
}
if (result.Count > 0)
{
Console.WriteLine("✓ Source filter is working - only allowed sources returned");
}
else
{
Console.WriteLine("⚠ No results found with source filter");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
finally
{
// 恢复原始配置
if (Plugin.Instance?.Configuration?.DanmuApi != null)
{
Plugin.Instance.Configuration.DanmuApi.AllowedSources = originalConfig ?? string.Empty;
}
}
}
[TestMethod]
public void TestFilterByAllowedPlatforms()
{
// 模拟配置只允许特定平台
var originalConfig = Plugin.Instance?.Configuration?.DanmuApi?.AllowedPlatforms;
try
{
if (Plugin.Instance?.Configuration?.DanmuApi != null)
{
Plugin.Instance.Configuration.DanmuApi.AllowedPlatforms = "qq";
}
var scraper = new DanmuApi(loggerFactory);
var item = new Movie
{
Name = "又见逍遥"
};
Task.Run(async () =>
{
try
{
var result = await scraper.GetMedia(item, "5510");
Console.WriteLine($"Platform filter test - Results count: {result.Episodes.Count}");
// 验证所有结果都来自允许的平台
foreach (var episode in result.Episodes)
{
Console.WriteLine($" - {episode.Title} (ID: {episode.Id})");
}
if (result.Episodes.Count > 0)
{
Console.WriteLine("✓ Platform filter is working - only allowed platforms returned");
}
else
{
Console.WriteLine("⚠ No results found with platform filter");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}).GetAwaiter().GetResult();
}
finally
{
// 恢复原始配置
if (Plugin.Instance?.Configuration?.DanmuApi != null)
{
Plugin.Instance.Configuration.DanmuApi.AllowedPlatforms = originalConfig ?? string.Empty;
}
}
}
}
}