mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-10-28 19:04:02 +08:00
Support like subtitle plugin
This commit is contained in:
132
Jellyfin.Plugin.Danmu.Test/BilibiliTest.cs
Normal file
132
Jellyfin.Plugin.Danmu.Test/BilibiliTest.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Danmu.Model;
|
||||||
|
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||||
|
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Danmu.Test
|
||||||
|
{
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class BilibiliTest
|
||||||
|
{
|
||||||
|
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
||||||
|
builder.AddSimpleConsole(options =>
|
||||||
|
{
|
||||||
|
options.IncludeScopes = true;
|
||||||
|
options.SingleLine = true;
|
||||||
|
options.TimestampFormat = "hh:mm:ss ";
|
||||||
|
}));
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestSearch()
|
||||||
|
{
|
||||||
|
var scraperManager = new ScraperManager(loggerFactory);
|
||||||
|
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
|
||||||
|
|
||||||
|
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 item = new Movie
|
||||||
|
{
|
||||||
|
Name = "扬名立万"
|
||||||
|
};
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scraper = new Bilibili(loggerFactory);
|
||||||
|
var result = await scraper.Search(item);
|
||||||
|
Console.WriteLine(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddMovie()
|
||||||
|
{
|
||||||
|
var scraperManager = new ScraperManager(loggerFactory);
|
||||||
|
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
|
||||||
|
|
||||||
|
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 item = new Movie
|
||||||
|
{
|
||||||
|
Name = "异邦人:无皇刃谭"
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = new List<LibraryEvent>();
|
||||||
|
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestUpdateMovie()
|
||||||
|
{
|
||||||
|
var scraperManager = new ScraperManager(loggerFactory);
|
||||||
|
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
|
||||||
|
|
||||||
|
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 item = new Movie
|
||||||
|
{
|
||||||
|
Name = "异邦人:无皇刃谭",
|
||||||
|
ProviderIds = new Dictionary<string, string>() { { Bilibili.ScraperProviderId, "2185" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = new List<LibraryEvent>();
|
||||||
|
list.Add(new LibraryEvent { Item = item, EventType = EventType.Update });
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,5 +18,22 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
|||||||
jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
||||||
return JsonSerializer.Serialize(obj, jso);
|
return JsonSerializer.Serialize(obj, jso);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static T? FromJson<T>(this string str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str)) return default(T?);
|
||||||
|
|
||||||
|
// 不指定UnsafeRelaxedJsonEscaping,+号会被转码为unicode字符,和js/java的序列化不一致
|
||||||
|
var jso = new JsonSerializerOptions();
|
||||||
|
jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(str, jso);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return default(T?);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToBase64(this string str)
|
||||||
|
{
|
||||||
|
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(str);
|
||||||
|
return System.Convert.ToBase64String(plainTextBytes);
|
||||||
|
}
|
||||||
|
|
||||||
public static double Distance(this string s1, string s2)
|
public static double Distance(this string s1, string s2)
|
||||||
{
|
{
|
||||||
var jw = new JaroWinkler();
|
var jw = new JaroWinkler();
|
||||||
|
|||||||
184
Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs
Normal file
184
Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||||
|
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Controller.Subtitles;
|
||||||
|
using MediaBrowser.Model.Providers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Jellyfin.Plugin.Danmu.Model;
|
||||||
|
using System.Text.Json;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Danmu;
|
||||||
|
|
||||||
|
public class DanmuSubtitleProvider : ISubtitleProvider
|
||||||
|
{
|
||||||
|
public string Name => "Danmu";
|
||||||
|
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<LibraryManagerEventsHelper> _logger;
|
||||||
|
private readonly LibraryManagerEventsHelper _libraryManagerEventsHelper;
|
||||||
|
|
||||||
|
private readonly ScraperManager _scraperManager;
|
||||||
|
|
||||||
|
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
|
||||||
|
|
||||||
|
public DanmuSubtitleProvider(ILibraryManager libraryManager, ILoggerFactory loggerFactory, ScraperManager scraperManager, LibraryManagerEventsHelper libraryManagerEventsHelper)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_logger = loggerFactory.CreateLogger<LibraryManagerEventsHelper>();
|
||||||
|
_scraperManager = scraperManager;
|
||||||
|
_libraryManagerEventsHelper = libraryManagerEventsHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var base64EncodedBytes = System.Convert.FromBase64String(id);
|
||||||
|
id = System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
|
||||||
|
var info = id.FromJson<SubtitleId>();
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = _libraryManager.GetItemById(info.ItemId);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var scraper = _scraperManager.All().FirstOrDefault(x => x.ProviderId == info.ProviderId);
|
||||||
|
if (scraper != null)
|
||||||
|
{
|
||||||
|
UpdateDanmuMetadata(item, scraper.ProviderId, info.Id);
|
||||||
|
_libraryManagerEventsHelper.QueueItem(item, EventType.Force);
|
||||||
|
|
||||||
|
// if (item is Movie)
|
||||||
|
// {
|
||||||
|
// var media = await scraper.GetMedia(item, info.Id);
|
||||||
|
// if (media != null)
|
||||||
|
// {
|
||||||
|
// await ForceSaveProviderId(item, scraper.ProviderId, media.Id);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (item is Episode)
|
||||||
|
// {
|
||||||
|
// var season = ((Episode)item).Season;
|
||||||
|
// if (season != null)
|
||||||
|
// {
|
||||||
|
// var media = await scraper.GetMedia(season, info.Id);
|
||||||
|
// if (media != null)
|
||||||
|
// {
|
||||||
|
|
||||||
|
// // 更新季元数据
|
||||||
|
// await ForceSaveProviderId(season, scraper.ProviderId, media.Id);
|
||||||
|
|
||||||
|
// // 更新所有剧集元数据,GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息
|
||||||
|
// var episodes = season.GetEpisodes(null, new DtoOptions(true));
|
||||||
|
// foreach (var (episode, idx) in episodes.WithIndex())
|
||||||
|
// {
|
||||||
|
// // 没对应剧集号的,忽略处理
|
||||||
|
// var indexNumber = episode.IndexNumber ?? 0;
|
||||||
|
// if (indexNumber < 1 || indexNumber > media.Episodes.Count)
|
||||||
|
// {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var epId = media.Episodes[indexNumber - 1].Id;
|
||||||
|
// await ForceSaveProviderId(episode, scraper.ProviderId, epId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"弹幕下载已由{Plugin.Instance?.Name}插件接管,请忽略本异常.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var list = new List<RemoteSubtitleInfo>();
|
||||||
|
if (request.IsAutomated || string.IsNullOrEmpty(request.MediaPath))
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
Path = request.MediaPath,
|
||||||
|
}).FirstOrDefault();
|
||||||
|
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剧集使用series名称进行搜索
|
||||||
|
if (item is Episode)
|
||||||
|
{
|
||||||
|
item.Name = request.SeriesName;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var scraper in _scraperManager.All())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var result = await scraper.Search(item);
|
||||||
|
foreach (var searchInfo in result)
|
||||||
|
{
|
||||||
|
var title = searchInfo.Name;
|
||||||
|
if (!string.IsNullOrEmpty(searchInfo.Category))
|
||||||
|
{
|
||||||
|
title = $"[{searchInfo.Category}] {searchInfo.Name}";
|
||||||
|
}
|
||||||
|
if (searchInfo.Year != null)
|
||||||
|
{
|
||||||
|
title += $" ({searchInfo.Year})";
|
||||||
|
}
|
||||||
|
var idInfo = new SubtitleId() { ItemId = item.Id.ToString(), Id = searchInfo.Id.ToString(), ProviderId = scraper.ProviderId };
|
||||||
|
list.Add(new RemoteSubtitleInfo()
|
||||||
|
{
|
||||||
|
Id = idInfo.ToJson().ToBase64(), // Id不允许特殊字幕,做base64编码处理
|
||||||
|
Name = title,
|
||||||
|
ProviderName = $"{Name}",
|
||||||
|
Format = "xml",
|
||||||
|
Comment = $"来源:{scraper.Name}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{0}]Exception handled processing queued movie events", scraper.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDanmuMetadata(BaseItem item, string providerId, string providerVal)
|
||||||
|
{
|
||||||
|
// 先清空旧弹幕的所有元数据
|
||||||
|
foreach (var s in _scraperManager.All())
|
||||||
|
{
|
||||||
|
item.ProviderIds.Remove(s.ProviderId);
|
||||||
|
}
|
||||||
|
// 保存指定弹幕元数据
|
||||||
|
item.ProviderIds[providerId] = providerVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,8 +138,10 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
|
|
||||||
var queuedMovieAdds = new List<LibraryEvent>();
|
var queuedMovieAdds = new List<LibraryEvent>();
|
||||||
var queuedMovieUpdates = new List<LibraryEvent>();
|
var queuedMovieUpdates = new List<LibraryEvent>();
|
||||||
|
var queuedMovieForces = new List<LibraryEvent>();
|
||||||
var queuedEpisodeAdds = new List<LibraryEvent>();
|
var queuedEpisodeAdds = new List<LibraryEvent>();
|
||||||
var queuedEpisodeUpdates = new List<LibraryEvent>(); ;
|
var queuedEpisodeUpdates = new List<LibraryEvent>();
|
||||||
|
var queuedEpisodeForces = new List<LibraryEvent>();
|
||||||
var queuedShowAdds = new List<LibraryEvent>();
|
var queuedShowAdds = new List<LibraryEvent>();
|
||||||
var queuedShowUpdates = new List<LibraryEvent>();
|
var queuedShowUpdates = new List<LibraryEvent>();
|
||||||
var queuedSeasonAdds = new List<LibraryEvent>();
|
var queuedSeasonAdds = new List<LibraryEvent>();
|
||||||
@@ -148,6 +150,14 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
// add事件可能会在获取元数据完之前执行,导致可能会中断元数据获取,通过pending集合把add事件延缓到获取元数据后再执行(获取完元数据后,一般会多推送一个update事件)
|
// add事件可能会在获取元数据完之前执行,导致可能会中断元数据获取,通过pending集合把add事件延缓到获取元数据后再执行(获取完元数据后,一般会多推送一个update事件)
|
||||||
foreach (var ev in queue)
|
foreach (var ev in queue)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// item所在的媒体库不启用弹幕插件,忽略处理
|
||||||
|
if (IsIgnoreItem(ev.Item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
switch (ev.Item)
|
switch (ev.Item)
|
||||||
{
|
{
|
||||||
case Movie when ev.EventType is EventType.Add:
|
case Movie when ev.EventType is EventType.Add:
|
||||||
@@ -166,6 +176,10 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
queuedMovieUpdates.Add(ev);
|
queuedMovieUpdates.Add(ev);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Movie when ev.EventType is EventType.Force:
|
||||||
|
_logger.LogInformation("Movie force: {0}", ev.Item.Name);
|
||||||
|
queuedMovieForces.Add(ev);
|
||||||
|
break;
|
||||||
case Series when ev.EventType is EventType.Add:
|
case Series when ev.EventType is EventType.Add:
|
||||||
_logger.LogInformation("Series add: {0}", ev.Item.Name);
|
_logger.LogInformation("Series add: {0}", ev.Item.Name);
|
||||||
// _pendingAddEventCache.Set<LibraryEvent>(ev.Item.Id, ev, _expiredOption);
|
// _pendingAddEventCache.Set<LibraryEvent>(ev.Item.Id, ev, _expiredOption);
|
||||||
@@ -202,6 +216,10 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
_logger.LogInformation("Episode update: {0}", ev.Item.Name);
|
_logger.LogInformation("Episode update: {0}", ev.Item.Name);
|
||||||
queuedEpisodeUpdates.Add(ev);
|
queuedEpisodeUpdates.Add(ev);
|
||||||
break;
|
break;
|
||||||
|
case Episode when ev.EventType is EventType.Force:
|
||||||
|
_logger.LogInformation("Episode force: {0}", ev.Item.Name);
|
||||||
|
queuedEpisodeForces.Add(ev);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -217,6 +235,22 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
await ProcessQueuedShowEvents(queuedShowUpdates, EventType.Update).ConfigureAwait(false);
|
await ProcessQueuedShowEvents(queuedShowUpdates, EventType.Update).ConfigureAwait(false);
|
||||||
await ProcessQueuedSeasonEvents(queuedSeasonUpdates, EventType.Update).ConfigureAwait(false);
|
await ProcessQueuedSeasonEvents(queuedSeasonUpdates, EventType.Update).ConfigureAwait(false);
|
||||||
await ProcessQueuedEpisodeEvents(queuedEpisodeUpdates, EventType.Update).ConfigureAwait(false);
|
await ProcessQueuedEpisodeEvents(queuedEpisodeUpdates, EventType.Update).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await ProcessQueuedMovieEvents(queuedMovieForces, EventType.Force).ConfigureAwait(false);
|
||||||
|
await ProcessQueuedEpisodeEvents(queuedEpisodeForces, EventType.Force).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIgnoreItem(BaseItem item)
|
||||||
|
{
|
||||||
|
// item所在的媒体库不启用弹幕插件,忽略处理
|
||||||
|
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||||
|
if (libraryOptions == null || libraryOptions.DisabledSubtitleFetchers.Contains(Plugin.Instance?.Name))
|
||||||
|
{
|
||||||
|
// Console.WriteLine($"ignore {item.Path} {item.Name}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -327,7 +361,39 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 强制刷新指定来源弹幕
|
||||||
|
if (eventType == EventType.Force)
|
||||||
|
{
|
||||||
|
foreach (var item in movies)
|
||||||
|
{
|
||||||
|
// 找到强制的scraper
|
||||||
|
var scraper = _scraperManager.All().FirstOrDefault(x => item.ProviderIds.ContainsKey(x.ProviderId));
|
||||||
|
if (scraper == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaId = item.GetProviderId(scraper.ProviderId);
|
||||||
|
if (string.IsNullOrEmpty(mediaId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var media = await scraper.GetMedia(item, mediaId);
|
||||||
|
if (media != null)
|
||||||
|
{
|
||||||
|
var episode = await scraper.GetMediaEpisode(item, media.Id);
|
||||||
|
if (episode != null)
|
||||||
|
{
|
||||||
|
// 下载弹幕xml文件
|
||||||
|
await this.DownloadDanmu(scraper, item, episode.CommentId, true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ForceSaveProviderId(item, scraper.ProviderId, media.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -450,7 +516,8 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
foreach (var season in seasons)
|
foreach (var season in seasons)
|
||||||
{
|
{
|
||||||
var queueUpdateMeta = new List<BaseItem>();
|
var queueUpdateMeta = new List<BaseItem>();
|
||||||
var episodes = season.GetEpisodes(null, new DtoOptions(false));
|
// GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息
|
||||||
|
var episodes = season.GetEpisodes(null, new DtoOptions(true));
|
||||||
if (episodes == null)
|
if (episodes == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -587,6 +654,63 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 强制刷新指定来源弹幕
|
||||||
|
if (eventType == EventType.Force)
|
||||||
|
{
|
||||||
|
foreach (var item in episodes)
|
||||||
|
{
|
||||||
|
// 找到强制的scraper
|
||||||
|
var scraper = _scraperManager.All().FirstOrDefault(x => item.ProviderIds.ContainsKey(x.ProviderId));
|
||||||
|
if (scraper == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaId = item.GetProviderId(scraper.ProviderId);
|
||||||
|
if (string.IsNullOrEmpty(mediaId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var season = ((Episode)item).Season;
|
||||||
|
if (season == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var media = await scraper.GetMedia(season, mediaId);
|
||||||
|
if (media != null)
|
||||||
|
{
|
||||||
|
// 更新所有剧集元数据,GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息
|
||||||
|
var episodeList = season.GetEpisodes(null, new DtoOptions(true));
|
||||||
|
foreach (var (episode, idx) in episodeList.WithIndex())
|
||||||
|
{
|
||||||
|
// 没对应剧集号的,忽略处理
|
||||||
|
var indexNumber = episode.IndexNumber ?? 0;
|
||||||
|
if (indexNumber < 1 || indexNumber > media.Episodes.Count)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var epId = media.Episodes[indexNumber - 1].Id;
|
||||||
|
var commentId = media.Episodes[indexNumber - 1].CommentId;
|
||||||
|
|
||||||
|
// 下载弹幕xml文件
|
||||||
|
await this.DownloadDanmu(scraper, episode, commentId, true).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 更新剧集元数据
|
||||||
|
await ForceSaveProviderId(episode, scraper.ProviderId, epId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新季元数据
|
||||||
|
await ForceSaveProviderId(season, scraper.ProviderId, media.Id);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -607,6 +731,11 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
// 合并新添加的provider id
|
// 合并新添加的provider id
|
||||||
foreach (var pair in queueItem.ProviderIds)
|
foreach (var pair in queueItem.ProviderIds)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(pair.Value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
item.ProviderIds[pair.Key] = pair.Value;
|
item.ProviderIds[pair.Key] = pair.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,13 +746,13 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
_logger.LogInformation("更新epid到元数据完成。item数:{0}", queue.Count);
|
_logger.LogInformation("更新epid到元数据完成。item数:{0}", queue.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string commentId)
|
public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string commentId, bool ignoreCheck = false)
|
||||||
{
|
{
|
||||||
// 下载弹幕xml文件
|
// 下载弹幕xml文件
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 弹幕5分钟内更新过,忽略处理(有时Update事件会重复执行)
|
// 弹幕5分钟内更新过,忽略处理(有时Update事件会重复执行)
|
||||||
if (IsRepeatAction(item))
|
if (!ignoreCheck && IsRepeatAction(item))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[{0}]最近5分钟已更新过弹幕xml,忽略处理:{1}", scraper.Name, item.Name);
|
_logger.LogInformation("[{0}]最近5分钟已更新过弹幕xml,忽略处理:{1}", scraper.Name, item.Name);
|
||||||
return;
|
return;
|
||||||
@@ -697,6 +826,19 @@ public class LibraryManagerEventsHelper : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ForceSaveProviderId(BaseItem item, string providerId, string providerVal)
|
||||||
|
{
|
||||||
|
// 先清空旧弹幕的所有元数据
|
||||||
|
foreach (var s in _scraperManager.All())
|
||||||
|
{
|
||||||
|
item.ProviderIds.Remove(s.ProviderId);
|
||||||
|
}
|
||||||
|
// 保存指定弹幕元数据
|
||||||
|
item.ProviderIds[providerId] = providerVal;
|
||||||
|
|
||||||
|
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,5 +24,10 @@ public enum EventType
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The update event.
|
/// The update event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Update
|
Update,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The force update event.
|
||||||
|
/// </summary>
|
||||||
|
Force
|
||||||
}
|
}
|
||||||
|
|||||||
18
Jellyfin.Plugin.Danmu/Model/SubtitleId.cs
Normal file
18
Jellyfin.Plugin.Danmu/Model/SubtitleId.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Danmu.Model;
|
||||||
|
|
||||||
|
public class SubtitleId
|
||||||
|
{
|
||||||
|
public string ItemId { get; set; }
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string ProviderId { get; set; }
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ namespace Jellyfin.Plugin.Danmu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当剧集没有SXX/Season XX季文件夹时,LocationType就是Virtual,动画经常没有季文件夹
|
||||||
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual && itemChangeEventArgs.Item is not Season)
|
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual && itemChangeEventArgs.Item is not Season)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -103,25 +104,6 @@ namespace Jellyfin.Plugin.Danmu
|
|||||||
_libraryManagerEventsHelper.QueueItem(itemChangeEventArgs.Item, EventType.Update);
|
_libraryManagerEventsHelper.QueueItem(itemChangeEventArgs.Item, EventType.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Library item was removed.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">The sending entity.</param>
|
|
||||||
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
|
|
||||||
private void LibraryManagerItemRemoved(object sender, ItemChangeEventArgs itemChangeEventArgs)
|
|
||||||
{
|
|
||||||
if (itemChangeEventArgs.Item is not Movie and not Episode and not Series and not Season)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemChangeEventArgs.Item.LocationType == LocationType.Virtual)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryManagerEventsHelper.QueueItem(itemChangeEventArgs.Item, EventType.Remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
|||||||
|
|
||||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
{
|
{
|
||||||
yield return new TaskTriggerInfo
|
return new List<TaskTriggerInfo>();
|
||||||
{
|
// yield return new TaskTriggerInfo
|
||||||
Type = TaskTriggerInfo.TriggerWeekly,
|
// {
|
||||||
DayOfWeek = DayOfWeek.Monday,
|
// Type = TaskTriggerInfo.TriggerWeekly,
|
||||||
TimeOfDayTicks = TimeSpan.FromHours(4).Ticks
|
// DayOfWeek = DayOfWeek.Monday,
|
||||||
};
|
// TimeOfDayTicks = TimeSpan.FromHours(4).Ticks
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
@@ -92,6 +93,12 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// item所在的媒体库不启用弹幕插件,忽略处理
|
||||||
|
if (_libraryManagerEventsHelper.IsIgnoreItem(item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 推送下载最新的xml (season刷新会同时刷新episode,所以不需要再推送episode,而且season是bv号的,只能通过season来刷新)
|
// 推送下载最新的xml (season刷新会同时刷新episode,所以不需要再推送episode,而且season是bv号的,只能通过season来刷新)
|
||||||
switch (item)
|
switch (item)
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// item所在的媒体库不启用弹幕插件,忽略处理
|
||||||
|
if (_libraryManagerEventsHelper.IsIgnoreItem(item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// 推送刷新 (season刷新会同时刷新episode,所以不需要再推送episode,而且season是bv号的,只能通过season来刷新)
|
// 推送刷新 (season刷新会同时刷新episode,所以不需要再推送episode,而且season是bv号的,只能通过season来刷新)
|
||||||
switch (item)
|
switch (item)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -32,7 +33,14 @@ public abstract class AbstractScraper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 搜索影片id
|
/// 搜索影片
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">元数据item</param>
|
||||||
|
/// <returns>影片列表</returns>
|
||||||
|
public abstract Task<List<ScraperSearchInfo>> Search(BaseItem item);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索匹配的影片id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">元数据item</param>
|
/// <param name="item">元数据item</param>
|
||||||
/// <returns>影片id</returns>
|
/// <returns>影片id</returns>
|
||||||
|
|||||||
@@ -36,6 +36,46 @@ public class Bilibili : AbstractScraper
|
|||||||
|
|
||||||
public override string ProviderId => ScraperProviderId;
|
public override string ProviderId => ScraperProviderId;
|
||||||
|
|
||||||
|
public override async Task<List<ScraperSearchInfo>> Search(BaseItem item)
|
||||||
|
{
|
||||||
|
var list = new List<ScraperSearchInfo>();
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var searchResult = await _api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (searchResult != null && searchResult.Result != null)
|
||||||
|
{
|
||||||
|
foreach (var result in searchResult.Result)
|
||||||
|
{
|
||||||
|
if ((result.ResultType == "media_ft" || result.ResultType == "media_bangumi") && result.Data.Length > 0)
|
||||||
|
{
|
||||||
|
foreach (var media in result.Data)
|
||||||
|
{
|
||||||
|
var seasonId = media.SeasonId;
|
||||||
|
var title = media.Title;
|
||||||
|
var pubYear = Jellyfin.Plugin.Danmu.Core.Utils.UnixTimeStampToDateTime(media.PublishTime).Year;
|
||||||
|
|
||||||
|
list.Add(new ScraperSearchInfo()
|
||||||
|
{
|
||||||
|
Id = $"{seasonId}",
|
||||||
|
Name = title,
|
||||||
|
Category = media.SeasonTypeName,
|
||||||
|
Year = pubYear,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.LogError(ex, "Exception handled GetMatchSeasonId. {0}", searchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<string?> SearchMediaId(BaseItem item)
|
public override async Task<string?> SearchMediaId(BaseItem item)
|
||||||
{
|
{
|
||||||
var searchName = this.NormalizeSearchName(item.Name);
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
|
|||||||
@@ -52,6 +52,40 @@ public class BilibiliApi : IDisposable
|
|||||||
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<SearchResult> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keyword))
|
||||||
|
{
|
||||||
|
return new SearchResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheKey = $"search_{keyword}";
|
||||||
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) };
|
||||||
|
SearchResult searchResult;
|
||||||
|
if (_memoryCache.TryGetValue<SearchResult>(cacheKey, out searchResult))
|
||||||
|
{
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.LimitRequestFrequently();
|
||||||
|
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
keyword = HttpUtility.UrlEncode(keyword);
|
||||||
|
var url = $"http://api.bilibili.com/x/web-interface/search/all/v2?keyword={keyword}";
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ApiResult<SearchResult>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (result != null && result.Code == 0 && result.Data != null)
|
||||||
|
{
|
||||||
|
_memoryCache.Set<SearchResult>(cacheKey, result.Data, expiredOption);
|
||||||
|
return result.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_memoryCache.Set<SearchResult>(cacheKey, new SearchResult(), expiredOption);
|
||||||
|
return new SearchResult();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get bilibili danmu data.
|
/// Get bilibili danmu data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -136,38 +170,6 @@ public class BilibiliApi : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<SearchResult> SearchAsync(string keyword, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(keyword))
|
|
||||||
{
|
|
||||||
return new SearchResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheKey = $"search_{keyword}";
|
|
||||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) };
|
|
||||||
SearchResult searchResult;
|
|
||||||
if (_memoryCache.TryGetValue<SearchResult>(cacheKey, out searchResult))
|
|
||||||
{
|
|
||||||
return searchResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.LimitRequestFrequently();
|
|
||||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
keyword = HttpUtility.UrlEncode(keyword);
|
|
||||||
var url = $"http://api.bilibili.com/x/web-interface/search/all/v2?keyword={keyword}";
|
|
||||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<SearchResult>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (result != null && result.Code == 0 && result.Data != null)
|
|
||||||
{
|
|
||||||
_memoryCache.Set<SearchResult>(cacheKey, result.Data, expiredOption);
|
|
||||||
return result.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
_memoryCache.Set<SearchResult>(cacheKey, new SearchResult(), expiredOption);
|
|
||||||
return new SearchResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<VideoSeason?> GetSeasonAsync(long seasonId, CancellationToken cancellationToken)
|
public async Task<VideoSeason?> GetSeasonAsync(long seasonId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
|||||||
|
|
||||||
[JsonPropertyName("season_type")]
|
[JsonPropertyName("season_type")]
|
||||||
public int SeasonType { get; set; }
|
public int SeasonType { get; set; }
|
||||||
|
[JsonPropertyName("season_type_name")]
|
||||||
|
public string SeasonTypeName { get; set; }
|
||||||
[JsonPropertyName("season_id")]
|
[JsonPropertyName("season_id")]
|
||||||
public long SeasonId { get; set; }
|
public long SeasonId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,40 @@ public class Dandan : AbstractScraper
|
|||||||
|
|
||||||
public override string ProviderId => ScraperProviderId;
|
public override string ProviderId => ScraperProviderId;
|
||||||
|
|
||||||
|
public override async Task<List<ScraperSearchInfo>> Search(BaseItem item)
|
||||||
|
{
|
||||||
|
var list = new List<ScraperSearchInfo>();
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
|
var animes = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
foreach (var anime in animes)
|
||||||
|
{
|
||||||
|
var animeId = anime.AnimeId;
|
||||||
|
var title = anime.AnimeTitle;
|
||||||
|
var pubYear = anime.Year;
|
||||||
|
|
||||||
|
if (isMovieItemType && anime.Type != "movie")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMovieItemType && anime.Type == "movie")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new ScraperSearchInfo()
|
||||||
|
{
|
||||||
|
Id = $"{animeId}",
|
||||||
|
Name = title,
|
||||||
|
Category = anime.Type == "movie" ? "电影" : "电视剧",
|
||||||
|
Year = pubYear,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<string?> SearchMediaId(BaseItem item)
|
public override async Task<string?> SearchMediaId(BaseItem item)
|
||||||
{
|
{
|
||||||
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
|||||||
11
Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperSearchInfo.cs
Normal file
11
Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperSearchInfo.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||||
|
|
||||||
|
public class ScraperSearchInfo
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Category { get; set; } = string.Empty;
|
||||||
|
public int? Year { get; set; }
|
||||||
|
}
|
||||||
@@ -39,8 +39,44 @@ public class Iqiyi : AbstractScraper
|
|||||||
|
|
||||||
public override string ProviderId => ScraperProviderId;
|
public override string ProviderId => ScraperProviderId;
|
||||||
|
|
||||||
public override async Task<string?> SearchMediaId(BaseItem item)
|
public override async Task<List<ScraperSearchInfo>> Search(BaseItem item)
|
||||||
{
|
{
|
||||||
|
var list = new List<ScraperSearchInfo>();
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
|
var videos = await this._api.GetSuggestAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
foreach (var video in videos)
|
||||||
|
{
|
||||||
|
var videoId = video.VideoId;
|
||||||
|
var title = video.Name;
|
||||||
|
var pubYear = video.Year;
|
||||||
|
|
||||||
|
if (isMovieItemType && video.ChannelName != "电影")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMovieItemType && video.ChannelName == "电影")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new ScraperSearchInfo()
|
||||||
|
{
|
||||||
|
Id = $"{video.LinkId}",
|
||||||
|
Name = title,
|
||||||
|
Category = video.ChannelName,
|
||||||
|
Year = pubYear,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<string?> SearchMediaId(BaseItem item)
|
||||||
|
{
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
var searchName = this.NormalizeSearchName(item.Name);
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
var videos = await this._api.GetSuggestAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
var videos = await this._api.GetSuggestAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
foreach (var video in videos)
|
foreach (var video in videos)
|
||||||
@@ -48,7 +84,6 @@ public class Iqiyi : AbstractScraper
|
|||||||
var videoId = video.VideoId;
|
var videoId = video.VideoId;
|
||||||
var title = video.Name;
|
var title = video.Name;
|
||||||
var pubYear = video.Year;
|
var pubYear = video.Year;
|
||||||
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
|
||||||
|
|
||||||
if (isMovieItemType && video.ChannelName != "电影")
|
if (isMovieItemType && video.ChannelName != "电影")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,8 +39,43 @@ public class Youku : AbstractScraper
|
|||||||
|
|
||||||
public override string ProviderId => ScraperProviderId;
|
public override string ProviderId => ScraperProviderId;
|
||||||
|
|
||||||
public override async Task<string?> SearchMediaId(BaseItem item)
|
public override async Task<List<ScraperSearchInfo>> Search(BaseItem item)
|
||||||
{
|
{
|
||||||
|
var list = new List<ScraperSearchInfo>();
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
|
var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
foreach (var video in videos)
|
||||||
|
{
|
||||||
|
var videoId = video.ID;
|
||||||
|
var title = video.Title;
|
||||||
|
var pubYear = video.Year;
|
||||||
|
|
||||||
|
if (isMovieItemType && video.Type != "movie")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMovieItemType && video.Type == "movie")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new ScraperSearchInfo()
|
||||||
|
{
|
||||||
|
Id = $"{videoId}",
|
||||||
|
Name = title,
|
||||||
|
Category = video.Type == "movie" ? "电影" : "电视剧",
|
||||||
|
Year = pubYear,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<string?> SearchMediaId(BaseItem item)
|
||||||
|
{
|
||||||
|
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||||
var searchName = this.NormalizeSearchName(item.Name);
|
var searchName = this.NormalizeSearchName(item.Name);
|
||||||
var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||||
foreach (var video in videos)
|
foreach (var video in videos)
|
||||||
@@ -48,7 +83,6 @@ public class Youku : AbstractScraper
|
|||||||
var videoId = video.ID;
|
var videoId = video.ID;
|
||||||
var title = video.Title;
|
var title = video.Title;
|
||||||
var pubYear = video.Year;
|
var pubYear = video.Year;
|
||||||
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
|
||||||
|
|
||||||
if (isMovieItemType && video.Type != "movie")
|
if (isMovieItemType && video.Type != "movie")
|
||||||
{
|
{
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -10,7 +10,7 @@ jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play
|
|||||||
|
|
||||||
* 自动下载xml格式弹幕
|
* 自动下载xml格式弹幕
|
||||||
* 生成ass格式弹幕
|
* 生成ass格式弹幕
|
||||||
* 定时更新
|
* 可配置定时更新
|
||||||
* 支持api访问弹幕
|
* 支持api访问弹幕
|
||||||
|
|
||||||

|

|
||||||
@@ -27,10 +27,16 @@ jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play
|
|||||||
|
|
||||||
## 如何使用
|
## 如何使用
|
||||||
|
|
||||||
* 新加入的影片会自动获取弹幕(只匹配番剧和电影视频),旧影片可以通过计划任务**扫描媒体库匹配弹幕**手动执行获取
|
1. 安装后,进入`控制台 -> 插件`,查看下`Danmu`插件是否是**Active**状态
|
||||||
* 可以在元数据中手动指定匹配的视频ID,如播放链接`https://www.bilibili.com/bangumi/play/ep682965`,对应的视频ID就是`682965`
|
2. 进入`控制台 -> 媒体库`,点击任一媒体库进入配置页,在最下面的`字幕下载`选项中勾选**Danmu**,并保存
|
||||||
* 对于电视剧和动画,可以在元数据中指定季ID,如播放链接`https://www.bilibili.com/bangumi/play/ss1564`,对应的季ID就是`1564`,只要集数和b站的集数的一致,并正确填写了集号,每季视频的弹幕会自动获取
|
|
||||||
* 同时生成ass弹幕,需要在插件配置中打开,默认是关闭的
|
<img src="doc/tutorial.png" width="400px" />
|
||||||
|
|
||||||
|
3. 新加入的影片会自动获取弹幕(只匹配番剧和电影视频),旧影片可以通过计划任务**扫描媒体库匹配弹幕**手动执行获取
|
||||||
|
4. 可以在元数据中手动指定匹配的视频ID,如播放链接`https://www.bilibili.com/bangumi/play/ep682965`,对应的视频ID就是`682965`
|
||||||
|
5. 对于电视剧和动画,可以在元数据中指定季ID,如播放链接`https://www.bilibili.com/bangumi/play/ss1564`,对应的季ID就是`1564`,只要集数和b站的集数的一致,并正确填写了集号,每季视频的弹幕会自动获取
|
||||||
|
6. 同时生成ass弹幕,需要在插件配置中打开,默认是关闭的
|
||||||
|
7. 定时更新需要自己到计划任务中添加定时时间,默认手工执行更新
|
||||||
|
|
||||||
> 电影或季元数据也支持手动指定BV号,来匹配UP主上传的视频弹幕。多P视频和剧集是按顺序一一对应匹配的,所以保证jellyfin中剧集有正确的集号很重要
|
> 电影或季元数据也支持手动指定BV号,来匹配UP主上传的视频弹幕。多P视频和剧集是按顺序一一对应匹配的,所以保证jellyfin中剧集有正确的集号很重要
|
||||||
|
|
||||||
@@ -65,8 +71,12 @@ ass格式:
|
|||||||
3. Build plugin with following command.
|
3. Build plugin with following command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ dotnet restore
|
dotnet restore
|
||||||
$ dotnet publish Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
|
dotnet publish --output=artifacts Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
|
||||||
|
|
||||||
|
# remove unused dll
|
||||||
|
cd artifacts
|
||||||
|
rm -rf MediaBrowser*.dll Microsoft*.dll Newtonsoft*.dll System*.dll Emby*.dll Jellyfin.Data*.dll Jellyfin.Extensions*.dll *.json *.pdb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -74,9 +84,9 @@ $ dotnet publish Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
|
|||||||
|
|
||||||
1. Build the plugin
|
1. Build the plugin
|
||||||
|
|
||||||
2. Create a folder, like `Danmu` and copy `bin/Release/Jellyfin.Plugin.Danmu.dll` into it
|
2. Create a folder, like `danmu` and copy `artifacts/*.dll` into it
|
||||||
|
|
||||||
3. Move folder `Danmu` to jellyfin `data/plugins` folder
|
3. Move folder `danmu` to jellyfin `data/plugins` folder
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
|
|||||||
BIN
doc/tutorial.png
Normal file
BIN
doc/tutorial.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Reference in New Issue
Block a user