mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-04-24 02:22:09 +08:00
feat: add dandanplay danmu. #3
This commit is contained in:
@@ -3,8 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Test
|
||||
|
||||
106
Jellyfin.Plugin.Danmu.Test/DandanApiTest.cs
Normal file
106
Jellyfin.Plugin.Danmu.Test/DandanApiTest.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
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.Dandan;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Test
|
||||
{
|
||||
|
||||
[TestClass]
|
||||
public class DandanApiTest
|
||||
{
|
||||
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
||||
builder.AddSimpleConsole(options =>
|
||||
{
|
||||
options.IncludeScopes = true;
|
||||
options.SingleLine = true;
|
||||
options.TimestampFormat = "hh:mm:ss ";
|
||||
}));
|
||||
|
||||
[TestMethod]
|
||||
public void TestSearch()
|
||||
{
|
||||
var keyword = "混沌武士";
|
||||
var _api = new DandanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _api.SearchAsync(keyword, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestSearchFrequently()
|
||||
{
|
||||
var _api = new DandanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var keyword = "剑风传奇";
|
||||
var result = await _api.SearchAsync(keyword, CancellationToken.None);
|
||||
keyword = "哆啦A梦";
|
||||
result = await _api.SearchAsync(keyword, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetAnimeAsync()
|
||||
{
|
||||
long animeID = 11829;
|
||||
var _api = new DandanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _api.GetAnimeAsync(animeID, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetCommentsAsync()
|
||||
{
|
||||
long epId = 118290001;
|
||||
var _api = new DandanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _api.GetCommentsAsync(epId, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,12 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Dandan;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -33,15 +36,14 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
[TestMethod]
|
||||
public void TestAddMovie()
|
||||
{
|
||||
|
||||
var _bilibiliApi = new BilibiliApi(loggerFactory);
|
||||
var scraperFactory = new ScraperFactory(loggerFactory, _bilibiliApi);
|
||||
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, scraperFactory);
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
@@ -68,9 +70,8 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
[TestMethod]
|
||||
public void TestUpdateMovie()
|
||||
{
|
||||
|
||||
var _bilibiliApi = new BilibiliApi(loggerFactory);
|
||||
var scraperFactory = new ScraperFactory(loggerFactory, _bilibiliApi);
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
fileSystemStub.Setup(x => x.Exists(It.IsAny<string>())).Returns(true);
|
||||
@@ -80,7 +81,7 @@ 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, scraperFactory);
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
@@ -110,13 +111,13 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
[TestMethod]
|
||||
public void TestAddSeason()
|
||||
{
|
||||
var _bilibiliApi = new BilibiliApi(loggerFactory);
|
||||
var scraperFactory = new ScraperFactory(loggerFactory, _bilibiliApi);
|
||||
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, scraperFactory);
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Season
|
||||
{
|
||||
@@ -143,13 +144,13 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
[TestMethod]
|
||||
public void TestUpdateSeason()
|
||||
{
|
||||
var _bilibiliApi = new BilibiliApi(loggerFactory);
|
||||
var scraperFactory = new ScraperFactory(loggerFactory, _bilibiliApi);
|
||||
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, scraperFactory);
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Season
|
||||
{
|
||||
@@ -173,6 +174,154 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestAddMovieByDandan()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(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 TestAddSeasonByDandan()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(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 Season
|
||||
{
|
||||
Name = "混沌武士"
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedSeasonEvents(list, EventType.Add);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestAddMovieByMultiScrapers()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(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 TestDownloadDanmu()
|
||||
{
|
||||
var dandanScraper = new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(loggerFactory);
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(dandanScraper);
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
fileSystemStub.Setup(x => x.Exists(It.IsAny<string>())).Returns(true);
|
||||
fileSystemStub.Setup(x => x.GetLastWriteTime(It.IsAny<string>())).Returns(DateTime.Now.AddDays(-1));
|
||||
fileSystemStub.Setup(x => x.WriteAllBytesAsync(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<CancellationToken>()));
|
||||
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 libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "阿基拉",
|
||||
ProviderIds = new Dictionary<string, string>() { { "DandanID", "280001" } },
|
||||
Path = "/tmp/test.mp4",
|
||||
};
|
||||
Movie.MediaSourceManager = mediaSourceManagerStub.Object;
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.DownloadDanmu(dandanScraper, item, "280001");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Common.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Jellyfin.Plugin.Danmu.Api.Entity;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using System.Net;
|
||||
using Jellyfin.Plugin.Danmu.Api.Http;
|
||||
using System.Web;
|
||||
using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api
|
||||
{
|
||||
public class BilibiliApi : IDisposable
|
||||
{
|
||||
const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
|
||||
private readonly ILogger<BilibiliApi> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private HttpClient httpClient;
|
||||
private CookieContainer _cookieContainer;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private static readonly object _lock = new object();
|
||||
private DateTime lastRequestTime = DateTime.Now.AddDays(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BilibiliApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public BilibiliApi(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<BilibiliApi>();
|
||||
|
||||
var handler = new HttpClientHandlerEx();
|
||||
_cookieContainer = handler.CookieContainer;
|
||||
httpClient = new HttpClient(handler, true);
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get bilibili danmu data.
|
||||
/// </summary>
|
||||
/// <param name="bvid">The Bilibili bvid.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>Task{TraktResponseDataContract}.</returns>
|
||||
public async Task<byte[]> GetDanmuContentAsync(string bvid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bvid))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bvid));
|
||||
}
|
||||
|
||||
// http://api.bilibili.com/x/player/pagelist?bvid={bvid}
|
||||
// https://api.bilibili.com/x/v1/dm/list.so?oid={cid}
|
||||
bvid = bvid.Trim();
|
||||
var pageUrl = $"http://api.bilibili.com/x/player/pagelist?bvid={bvid}";
|
||||
var response = await httpClient.GetAsync(pageUrl, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoPart[]>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Data != null)
|
||||
{
|
||||
var part = result.Data.FirstOrDefault();
|
||||
if (part != null)
|
||||
{
|
||||
return await GetDanmuContentByCidAsync(part.Cid, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Request fail. bvid={bvid}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get bilibili danmu data.
|
||||
/// </summary>
|
||||
/// <param name="bvid">The Bilibili bvid.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>Task{TraktResponseDataContract}.</returns>
|
||||
public async Task<byte[]> GetDanmuContentAsync(long epId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (epId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(epId));
|
||||
}
|
||||
|
||||
|
||||
var season = await GetEpisodeAsync(epId, cancellationToken).ConfigureAwait(false);
|
||||
if (season != null && season.Episodes.Length > 0)
|
||||
{
|
||||
var episode = season.Episodes.First(x => x.Id == epId);
|
||||
if (episode != null)
|
||||
{
|
||||
return await GetDanmuContentByCidAsync(episode.CId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Request fail. epId={epId}");
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetDanmuContentByCidAsync(long cid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cid <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cid));
|
||||
}
|
||||
|
||||
var url = $"https://api.bilibili.com/x/v1/dm/list.so?oid={cid}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Request fail. url={url} status_code={response.StatusCode}");
|
||||
}
|
||||
|
||||
// 数据太小可能是已经被b站下架,返回了出错信息
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (bytes == null || bytes.Length < 2000)
|
||||
{
|
||||
throw new Exception($"弹幕获取失败,可能视频已下架或弹幕太少. url: {url}");
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
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(30) };
|
||||
SearchResult searchResult;
|
||||
if (_memoryCache.TryGetValue<SearchResult>(cacheKey, out searchResult))
|
||||
{
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (seasonId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"season_{seasonId}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
VideoSeason? seasonData;
|
||||
if (_memoryCache.TryGetValue<VideoSeason?>(cacheKey, out seasonData))
|
||||
{
|
||||
return seasonData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"http://api.bilibili.com/pgc/view/web/season?season_id={seasonId}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Result != null)
|
||||
{
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, result.Result, expiredOption);
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<VideoSeason?> GetEpisodeAsync(long epId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (epId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"episode_{epId}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
VideoSeason? seasonData;
|
||||
if (_memoryCache.TryGetValue<VideoSeason?>(cacheKey, out seasonData))
|
||||
{
|
||||
return seasonData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"http://api.bilibili.com/pgc/view/web/season?ep_id={epId}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Result != null)
|
||||
{
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, result.Result, expiredOption);
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<Video?> GetVideoByBvidAsync(string bvid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bvid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"video_{bvid}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
Video? videoData;
|
||||
if (_memoryCache.TryGetValue<Video?>(cacheKey, out videoData))
|
||||
{
|
||||
return videoData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<Video>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Data != null)
|
||||
{
|
||||
_memoryCache.Set<Video?>(cacheKey, result.Data, expiredOption);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
_memoryCache.Set<Video?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task EnsureSessionCookie(CancellationToken cancellationToken)
|
||||
{
|
||||
var url = "https://www.bilibili.com";
|
||||
var cookies = _cookieContainer.GetCookies(new Uri(url, UriKind.Absolute));
|
||||
var existCookie = cookies.FirstOrDefault(x => x.Name == "buvid3");
|
||||
if (existCookie != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
protected void LimitRequestFrequently()
|
||||
{
|
||||
var diff = 0;
|
||||
lock (_lock)
|
||||
{
|
||||
var ts = DateTime.Now - lastRequestTime;
|
||||
diff = (int)(1000 - ts.TotalMilliseconds);
|
||||
lastRequestTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if (diff > 0)
|
||||
{
|
||||
this._logger.LogDebug("请求太频繁,等待{0}毫秒后继续执行...", diff);
|
||||
Thread.Sleep(diff);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_memoryCache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using System.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using System.Reflection;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The configuration options.
|
||||
/// </summary>
|
||||
public enum SomeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Option one.
|
||||
/// </summary>
|
||||
OneOption,
|
||||
|
||||
/// <summary>
|
||||
/// Second option.
|
||||
/// </summary>
|
||||
AnotherOption
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin configuration.
|
||||
@@ -24,47 +15,129 @@ public enum SomeOptions
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||
/// 版本信息
|
||||
/// </summary>
|
||||
public PluginConfiguration()
|
||||
{
|
||||
ToAss = false;
|
||||
AssFont = string.Empty;
|
||||
AssFontSize = string.Empty;
|
||||
AssLineCount = string.Empty;
|
||||
AssSpeed = string.Empty;
|
||||
AssTextOpacity = string.Empty;
|
||||
|
||||
}
|
||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时生成ASS格式弹幕.
|
||||
/// 是否同时生成ASS格式弹幕.
|
||||
/// </summary>
|
||||
public bool ToAss { get; set; }
|
||||
public bool ToAss { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 字体.
|
||||
/// <summary>
|
||||
/// 字体.
|
||||
/// </summary>
|
||||
public string AssFont { get; set; }
|
||||
public string AssFont { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 字体大小.
|
||||
/// <summary>
|
||||
/// 字体大小.
|
||||
/// </summary>
|
||||
public string AssFontSize { get; set; }
|
||||
public string AssFontSize { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 限制行数.
|
||||
/// <summary>
|
||||
/// 限制行数.
|
||||
/// </summary>
|
||||
public string AssLineCount { get; set; }
|
||||
public string AssLineCount { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 移动速度.
|
||||
/// <summary>
|
||||
/// 移动速度.
|
||||
/// </summary>
|
||||
public string AssSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度.
|
||||
public string AssSpeed { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 透明度.
|
||||
/// </summary>
|
||||
public string AssTextOpacity { get; set; }
|
||||
|
||||
public string AssTextOpacity { get; set; } = string.Empty;
|
||||
|
||||
public DandanOption Dandan { get; set; } = new DandanOption();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 弹幕源.
|
||||
/// </summary>
|
||||
private List<ScraperConfigItem> _scrapers;
|
||||
|
||||
[XmlArrayItem(ElementName = "Scraper")]
|
||||
public ScraperConfigItem[] Scrapers
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
var defaultScrapers = new List<ScraperConfigItem>();
|
||||
if (Plugin.Instance?.Scrapers != null)
|
||||
{
|
||||
foreach (var scaper in Plugin.Instance.Scrapers)
|
||||
{
|
||||
defaultScrapers.Add(new ScraperConfigItem(scaper.Name, scaper.DefaultEnable));
|
||||
}
|
||||
};
|
||||
|
||||
if (_scrapers?.Any() != true)
|
||||
{// 没旧配置,返回默认列表
|
||||
return defaultScrapers.ToArray();
|
||||
}
|
||||
else
|
||||
{// 已保存有配置
|
||||
|
||||
// 删除已废弃的插件配置
|
||||
var allValidScaperNames = defaultScrapers.Select(o => o.Name).ToList();
|
||||
_scrapers.RemoveAll(o => !allValidScaperNames.Contains(o.Name));
|
||||
|
||||
|
||||
|
||||
// 找出新增的插件
|
||||
var oldScrapers = _scrapers.Select(o => o.Name).ToList();
|
||||
defaultScrapers.RemoveAll(o => oldScrapers.Contains(o.Name));
|
||||
|
||||
// 合并新增的scrapers
|
||||
_scrapers.AddRange(defaultScrapers);
|
||||
}
|
||||
return _scrapers.ToArray();
|
||||
}
|
||||
set
|
||||
{
|
||||
_scrapers = value.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 弹幕源配置
|
||||
/// </summary>
|
||||
public class ScraperConfigItem
|
||||
{
|
||||
|
||||
public bool Enable { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public ScraperConfigItem()
|
||||
{
|
||||
this.Name = "";
|
||||
this.Enable = false;
|
||||
}
|
||||
|
||||
public ScraperConfigItem(string name, bool enable)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Enable = enable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹弹play配置
|
||||
/// </summary>
|
||||
public class DandanOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 同时获取关联的第三方弹幕
|
||||
/// </summary>
|
||||
public bool WithRelatedDanmu { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 中文简繁转换。0-不转换,1-转换为简体,2-转换为繁体
|
||||
/// </summary>
|
||||
public int ChConvert { get; set; } = 0;
|
||||
}
|
||||
@@ -1,46 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
|
||||
data-require="emby-input,emby-button,emby-select,emby-checkbox">
|
||||
<div data-role="content">
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">Danmu 配置</h2><span id="current_version" name="current_version"
|
||||
is="emby-linkbutton" class="emby-button"></span>
|
||||
<a is="emby-linkbutton" class="raised button-alt headerHelpButton emby-button" target="_blank"
|
||||
href="https://github.com/cxfksword/jellyfin-plugin-danmu">源码</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form id="TemplateConfigForm">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="ToAss" name="ToAss" type="checkbox" is="emby-checkbox" />
|
||||
<span>同时生成ASS格式弹幕</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,会在视频目录下生成ass格式的弹幕,命名格式:[视频名].danmu.ass</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFont">ass弹幕字体</label>
|
||||
<input id="AssFont" name="AssFont" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认黑体.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFontSize">ass弹幕字体大小</label>
|
||||
<input id="AssFontSize" name="AssFontSize" type="number" is="emby-input" min="25" />
|
||||
<div class="fieldDescription">可为空,默认60,可以此为基准,增大或缩小.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssTextOpacity">ass弹幕字体透明度</label>
|
||||
<input id="AssTextOpacity" name="AssTextOpacity" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认1,表示不透明,数值在0.0~1.0之间</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssLineCount">ass弹幕显示行数</label>
|
||||
<input id="AssLineCount" name="AssLineCount" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认全屏显示,1/4屏可填5,半屏可填9</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssSpeed">ass弹幕移动速度</label>
|
||||
<input id="AssSpeed" name="AssSpeed" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认8秒</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend>
|
||||
<h3>弹幕源配置</h3>
|
||||
</legend>
|
||||
<div class="checkboxList paperList checkboxList-paperList" id="Scrapers" name="Scrapers">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend>
|
||||
<h3>弹弹play配置</h3>
|
||||
</legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="WithRelatedDanmu" name="WithRelatedDanmu" type="checkbox"
|
||||
is="emby-checkbox" />
|
||||
<span>同时获取关联的第三方弹幕</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,返回此弹幕库对应的所有第三方关联网址的弹幕.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="ChConvert">弹幕中文简繁转换</label>
|
||||
<input id="ChConvert" name="ChConvert" type="number" is="emby-input" />
|
||||
<div class="fieldDescription">中文简繁转换。0-不转换,1-转换为简体,2-转换为繁体。</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend>
|
||||
<h3>生成ASS配置</h3>
|
||||
</legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label">
|
||||
<input id="ToAss" name="ToAss" type="checkbox" is="emby-checkbox" />
|
||||
<span>同时生成ASS格式弹幕</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,会在视频目录下生成ass格式的弹幕,命名格式:[视频名].danmu.ass</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFont">ass弹幕字体</label>
|
||||
<input id="AssFont" name="AssFont" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认黑体.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssFontSize">ass弹幕字体大小</label>
|
||||
<input id="AssFontSize" name="AssFontSize" type="number" is="emby-input" min="25" />
|
||||
<div class="fieldDescription">可为空,默认60,可以此为基准,增大或缩小.</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssTextOpacity">ass弹幕字体透明度</label>
|
||||
<input id="AssTextOpacity" name="AssTextOpacity" type="text" is="emby-input" />
|
||||
<div class="fieldDescription">可为空,默认1,表示不透明,数值在0.0~1.0之间</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssLineCount">ass弹幕显示行数</label>
|
||||
<input id="AssLineCount" name="AssLineCount" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认全屏显示,1/4屏可填5,半屏可填9</div>
|
||||
</div>
|
||||
<div class="inputContainer ass">
|
||||
<label class="inputLabel inputLabelUnfocused" for="AssSpeed">ass弹幕移动速度</label>
|
||||
<input id="AssSpeed" name="AssSpeed" type="number" is="emby-input" min="0" />
|
||||
<div class="fieldDescription">可为空,默认8秒</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
@@ -55,38 +104,109 @@
|
||||
};
|
||||
|
||||
document.querySelector('#TemplateConfigPage')
|
||||
.addEventListener('pageshow', function() {
|
||||
.addEventListener('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
$('#current_version').text("v" + config.Version);
|
||||
|
||||
document.querySelector('#ToAss').checked = config.ToAss;
|
||||
document.querySelector('#AssFont').value = config.AssFont;
|
||||
document.querySelector('#AssFontSize').value = config.AssFontSize;
|
||||
document.querySelector('#AssTextOpacity').value = config.AssTextOpacity;
|
||||
document.querySelector('#AssLineCount').value = config.AssLineCount;
|
||||
document.querySelector('#AssSpeed').value = config.AssSpeed;
|
||||
|
||||
document.querySelector('#WithRelatedDanmu').checked = config.Dandan.WithRelatedDanmu;
|
||||
document.querySelector('#ChConvert').value = config.Dandan.ChConvert;
|
||||
|
||||
|
||||
var html = '';
|
||||
config.Scrapers.forEach(function (e) {
|
||||
html += '<div class="listItem listItem-border sortableOption sortItem" data-sort="' + e + '">';
|
||||
html += ' <label class="listItemCheckboxContainer emby-checkbox-label">';
|
||||
html += ' <input type="checkbox" is="emby-checkbox" class="chkEnableCodec emby-checkbox emby-checkbox-focusring" name="ScraperItem" ' + (e.Enable ? 'checked' : '') + ' value="' + e.Name + '" >';
|
||||
html += ' <span class="checkboxLabel" style="width:200px" >' + e.Name + '</span>';
|
||||
html += ' </label>';
|
||||
html += ' <div class="listItemBody two-line listItemBodyText"></div>';
|
||||
html += ' <button type="button" is="paper-icon-button-light" title="上" class="btnSortable paper-icon-button-light btnSortableMoveUp btnViewItemUp" data-pluginindex="2"><span class="material-icons keyboard_arrow_up"></span></button>';
|
||||
html += ' <button type="button" is="paper-icon-button-light" title="下" class="btnSortable paper-icon-button-light btnSortableMoveDown btnViewItemDown" data-pluginindex="0"><span class="material-icons keyboard_arrow_down"></span></button>';
|
||||
html += '</div>';
|
||||
html += '\r\n';
|
||||
});
|
||||
|
||||
$('#Scrapers').empty().append(html);
|
||||
setButtons();
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#TemplateConfigForm')
|
||||
.addEventListener('submit', function(e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
config.ToAss = document.querySelector('#ToAss').checked;
|
||||
config.AssFont = document.querySelector('#AssFont').value;
|
||||
config.AssFontSize = document.querySelector('#AssFontSize').value;
|
||||
config.AssTextOpacity = document.querySelector('#AssTextOpacity').value;
|
||||
config.AssLineCount = document.querySelector('#AssLineCount').value;
|
||||
config.AssSpeed = document.querySelector('#AssSpeed').value;
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
config.ToAss = document.querySelector('#ToAss').checked;
|
||||
config.AssFont = document.querySelector('#AssFont').value;
|
||||
config.AssFontSize = document.querySelector('#AssFontSize').value;
|
||||
config.AssTextOpacity = document.querySelector('#AssTextOpacity').value;
|
||||
config.AssLineCount = document.querySelector('#AssLineCount').value;
|
||||
config.AssSpeed = document.querySelector('#AssSpeed').value;
|
||||
|
||||
var scrapers = [];
|
||||
$('input[name=ScraperItem]').each(function (index) {
|
||||
var scraper = new Object();
|
||||
scraper.Name = $(this).prop('value');
|
||||
scraper.Enable = $(this).prop('checked');
|
||||
scrapers.push(scraper);
|
||||
});
|
||||
config.Scrapers = scrapers;
|
||||
|
||||
var dandan = new Object();
|
||||
dandan.WithRelatedDanmu = document.querySelector('#WithRelatedDanmu').checked;
|
||||
dandan.ChConvert = document.querySelector('#ChConvert').value;
|
||||
config.Dandan = dandan;
|
||||
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
||||
function setButtons() {
|
||||
$('.sortItem button').css('visibility', 'visible')
|
||||
$('.sortItem:first-child button.btnViewItemUp').css('visibility', 'hidden')
|
||||
$('.sortItem:last-child button.btnViewItemDown').css('visibility', 'hidden')
|
||||
$(".sortItem").addClass("listItem-border");
|
||||
// $(".sortItem:last-child").removeClass("listItem-border");
|
||||
var i = 0;
|
||||
$('.sortItem').each(function () {
|
||||
$(this).attr("data-sort", i);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
setButtons();
|
||||
$(document).on('click', '.btnViewItemDown', function (e) {
|
||||
var cCard = $(this).closest('.sortItem');
|
||||
var tCard = cCard.next('.sortItem');
|
||||
cCard.insertAfter(tCard);
|
||||
setButtons();
|
||||
});
|
||||
|
||||
$(document).on('click', '.btnViewItemUp', function (e) {
|
||||
var cCard = $(this).closest('.sortItem');
|
||||
var tCard = cCard.prev('.sortItem');
|
||||
cCard.insertBefore(tCard);
|
||||
setButtons();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Http
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Http
|
||||
{
|
||||
public class HttpClientHandlerEx : HttpClientHandler
|
||||
{
|
||||
@@ -6,7 +6,6 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
|
||||
@@ -11,8 +11,6 @@ using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Api.Entity;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -29,6 +27,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
using Jellyfin.Plugin.Danmu.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu;
|
||||
|
||||
@@ -42,7 +41,15 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
private readonly ILogger<LibraryManagerEventsHelper> _logger;
|
||||
private readonly Jellyfin.Plugin.Danmu.Core.IFileSystem _fileSystem;
|
||||
private Timer _queueTimer;
|
||||
private readonly ScraperFactory _scraperFactory;
|
||||
private readonly ScraperManager _scraperManager;
|
||||
|
||||
public PluginConfiguration Config
|
||||
{
|
||||
get
|
||||
{
|
||||
return Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -52,7 +59,7 @@ 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, ScraperFactory scraperFactory)
|
||||
public LibraryManagerEventsHelper(ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager)
|
||||
{
|
||||
_queuedEvents = new List<LibraryEvent>();
|
||||
_pendingAddEventCache = new MemoryCache(new MemoryCacheOptions());
|
||||
@@ -60,7 +67,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManagerEventsHelper>();
|
||||
_fileSystem = fileSystem;
|
||||
_scraperFactory = scraperFactory;
|
||||
_scraperManager = scraperManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -228,7 +235,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
var queueUpdateMeta = new List<BaseItem>();
|
||||
foreach (var item in movies)
|
||||
{
|
||||
foreach (var scraper in _scraperFactory.All())
|
||||
foreach (var scraper in _scraperManager.All())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -260,11 +267,11 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
}
|
||||
catch (FrequentlyRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "api接口触发风控,中止执行,请稍候再试.");
|
||||
_logger.LogError(ex, "[{0}]api接口触发风控,中止执行,请稍候再试.", scraper.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled processing queued movie events");
|
||||
_logger.LogError(ex, "[{0}]Exception handled processing queued movie events", scraper.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,7 +285,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
{
|
||||
foreach (var item in movies)
|
||||
{
|
||||
foreach (var scraper in _scraperFactory.All())
|
||||
foreach (var scraper in _scraperManager.All())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -291,6 +298,8 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
// 下载弹幕xml文件
|
||||
await this.DownloadDanmu(scraper, item, episode.CommentId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// TODO:兼容支持用户设置seasonId???
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -383,7 +392,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
}
|
||||
|
||||
var series = season.GetParent();
|
||||
foreach (var scraper in _scraperFactory.All())
|
||||
foreach (var scraper in _scraperManager.All())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -432,7 +441,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var scraper in _scraperFactory.All())
|
||||
foreach (var scraper in _scraperManager.All())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -533,7 +542,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
{
|
||||
foreach (var item in episodes)
|
||||
{
|
||||
foreach (var scraper in _scraperFactory.All())
|
||||
foreach (var scraper in _scraperManager.All())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -590,7 +599,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
_logger.LogInformation("更新epid到元数据完成。item数:{0}", queue.Count);
|
||||
}
|
||||
|
||||
private async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string commentId)
|
||||
public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string commentId)
|
||||
{
|
||||
// 下载弹幕xml文件
|
||||
try
|
||||
@@ -611,7 +620,7 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception handled download danmu file");
|
||||
_logger.LogError(ex, "[{0}]Exception handled download danmu file. name={1}", scraper.Name, item.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,30 +643,29 @@ public class LibraryManagerEventsHelper : IDisposable
|
||||
var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml");
|
||||
await this._fileSystem.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var config = Plugin.Instance.Configuration;
|
||||
if (config.ToAss && bytes.Length > 0)
|
||||
if (this.Config.ToAss && bytes.Length > 0)
|
||||
{
|
||||
var assConfig = new Danmaku2Ass.Config();
|
||||
assConfig.Title = item.Name;
|
||||
if (!string.IsNullOrEmpty(config.AssFont.Trim()))
|
||||
if (!string.IsNullOrEmpty(this.Config.AssFont.Trim()))
|
||||
{
|
||||
assConfig.FontName = config.AssFont;
|
||||
assConfig.FontName = this.Config.AssFont;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssFontSize.Trim()))
|
||||
if (!string.IsNullOrEmpty(this.Config.AssFontSize.Trim()))
|
||||
{
|
||||
assConfig.BaseFontSize = config.AssFontSize.Trim().ToInt();
|
||||
assConfig.BaseFontSize = this.Config.AssFontSize.Trim().ToInt();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssTextOpacity.Trim()))
|
||||
if (!string.IsNullOrEmpty(this.Config.AssTextOpacity.Trim()))
|
||||
{
|
||||
assConfig.TextOpacity = config.AssTextOpacity.Trim().ToFloat();
|
||||
assConfig.TextOpacity = this.Config.AssTextOpacity.Trim().ToFloat();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssLineCount.Trim()))
|
||||
if (!string.IsNullOrEmpty(this.Config.AssLineCount.Trim()))
|
||||
{
|
||||
assConfig.LineCount = config.AssLineCount.Trim().ToInt();
|
||||
assConfig.LineCount = this.Config.AssLineCount.Trim().ToInt();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(config.AssSpeed.Trim()))
|
||||
if (!string.IsNullOrEmpty(this.Config.AssSpeed.Trim()))
|
||||
{
|
||||
assConfig.TuneDuration = config.AssSpeed.Trim().ToInt() - 8;
|
||||
assConfig.TuneDuration = this.Config.AssSpeed.Trim().ToInt() - 8;
|
||||
}
|
||||
|
||||
var assPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".danmu.ass");
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Plugin.Danmu.Configuration;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
@@ -26,6 +27,7 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
Scrapers = applicationHost.GetExports<AbstractScraper>(false).Where(o => o != null).OrderBy(x => x.DefaultOrder).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -39,6 +41,11 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||
/// </summary>
|
||||
public static Plugin? Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 全部的弹幕源
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<AbstractScraper> Scrapers { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginPageInfo> GetPages()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
@@ -7,8 +7,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -26,7 +24,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
public class RefreshDanmuTask : IScheduledTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ScraperFactory _scraperFactory;
|
||||
private readonly ScraperManager _scraperManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly LibraryManagerEventsHelper _libraryManagerEventsHelper;
|
||||
|
||||
@@ -43,13 +41,12 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
/// Initializes a new instance of the <see cref="RefreshDanmuTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
/// <param name="api">Instance of the <see cref="BilibiliApi"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public RefreshDanmuTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, LibraryManagerEventsHelper libraryManagerEventsHelper, ScraperFactory scraperFactory)
|
||||
public RefreshDanmuTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, LibraryManagerEventsHelper libraryManagerEventsHelper, ScraperManager scraperManager)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<RefreshDanmuTask>();
|
||||
_libraryManager = libraryManager;
|
||||
_scraperFactory = scraperFactory;
|
||||
_scraperManager = scraperManager;
|
||||
_libraryManagerEventsHelper = libraryManagerEventsHelper;
|
||||
}
|
||||
|
||||
@@ -69,7 +66,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
|
||||
progress?.Report(0);
|
||||
|
||||
var scrapers = this._scraperFactory.All();
|
||||
var scrapers = this._scraperManager.All();
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
// MediaTypes = new[] { MediaType.Video },
|
||||
@@ -91,6 +88,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
// 没epid元数据的不处理
|
||||
if (!this.HasAnyScraperProviderId(scrapers, item))
|
||||
{
|
||||
successCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||
@@ -25,7 +23,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
public class ScanLibraryTask : IScheduledTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ScraperFactory _scraperFactory;
|
||||
private readonly ScraperManager _scraperManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly LibraryManagerEventsHelper _libraryManagerEventsHelper;
|
||||
|
||||
@@ -42,13 +40,12 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
/// Initializes a new instance of the <see cref="ScanLibraryTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
/// <param name="api">Instance of the <see cref="BilibiliApi"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public ScanLibraryTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, LibraryManagerEventsHelper libraryManagerEventsHelper, ScraperFactory scraperFactory)
|
||||
public ScanLibraryTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, LibraryManagerEventsHelper libraryManagerEventsHelper, ScraperManager scraperManager)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ScanLibraryTask>();
|
||||
_libraryManager = libraryManager;
|
||||
_scraperFactory = scraperFactory;
|
||||
_scraperManager = scraperManager;
|
||||
_libraryManagerEventsHelper = libraryManagerEventsHelper;
|
||||
}
|
||||
|
||||
@@ -63,7 +60,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
|
||||
progress?.Report(0);
|
||||
|
||||
var scrapers = this._scraperFactory.All();
|
||||
var scrapers = this._scraperManager.All();
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
// MediaTypes = new[] { MediaType.Video },
|
||||
@@ -86,6 +83,7 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
// 有epid的忽略处理(不需要再匹配)
|
||||
if (this.HasAnyScraperProviderId(scrapers, item))
|
||||
{
|
||||
successCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -129,7 +127,6 @@ namespace Jellyfin.Plugin.Danmu.ScheduledTasks
|
||||
var providerVal = item.GetProviderId(scraper.ProviderId);
|
||||
if (!string.IsNullOrEmpty(providerVal))
|
||||
{
|
||||
_logger.LogInformation(scraper.Name + " -> " + providerVal);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
50
Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs
Normal file
50
Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.Plugin.Danmu.Core.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers;
|
||||
|
||||
public abstract class AbstractApi : IDisposable
|
||||
{
|
||||
const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
|
||||
protected ILogger _logger;
|
||||
protected JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
protected HttpClient httpClient;
|
||||
protected CookieContainer _cookieContainer;
|
||||
protected IMemoryCache _memoryCache;
|
||||
|
||||
|
||||
public AbstractApi(ILogger log)
|
||||
{
|
||||
this._logger = log;
|
||||
|
||||
var handler = new HttpClientHandlerEx();
|
||||
_cookieContainer = handler.CookieContainer;
|
||||
httpClient = new HttpClient(handler, true);
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_memoryCache.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,10 @@ public abstract class AbstractScraper
|
||||
{
|
||||
protected ILogger log;
|
||||
|
||||
public virtual int DefaultOrder => 999;
|
||||
|
||||
public virtual bool DefaultEnable => false;
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,8 +4,6 @@ using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
@@ -22,12 +20,16 @@ public class Bilibili : AbstractScraper
|
||||
|
||||
private readonly BilibiliApi _api;
|
||||
|
||||
public Bilibili(ILoggerFactory logManager, BilibiliApi api)
|
||||
public Bilibili(ILoggerFactory logManager)
|
||||
: base(logManager.CreateLogger<Bilibili>())
|
||||
{
|
||||
_api = api;
|
||||
_api = new BilibiliApi(logManager);
|
||||
}
|
||||
|
||||
public override int DefaultOrder => 1;
|
||||
|
||||
public override bool DefaultEnable => true;
|
||||
|
||||
public override string Name => "bilibili";
|
||||
|
||||
public override string ProviderName => ScraperProviderName;
|
||||
|
||||
309
Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs
Normal file
309
Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Common.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Jellyfin.Plugin.Danmu.Core.Http;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
|
||||
|
||||
public class BilibiliApi : IDisposable
|
||||
{
|
||||
const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
|
||||
private readonly ILogger<BilibiliApi> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private HttpClient httpClient;
|
||||
private CookieContainer _cookieContainer;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private static readonly object _lock = new object();
|
||||
private DateTime lastRequestTime = DateTime.Now.AddDays(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BilibiliApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public BilibiliApi(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<BilibiliApi>();
|
||||
|
||||
var handler = new HttpClientHandlerEx();
|
||||
_cookieContainer = handler.CookieContainer;
|
||||
httpClient = new HttpClient(handler, true);
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get bilibili danmu data.
|
||||
/// </summary>
|
||||
/// <param name="bvid">The Bilibili bvid.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>Task{TraktResponseDataContract}.</returns>
|
||||
public async Task<byte[]> GetDanmuContentAsync(string bvid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bvid))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bvid));
|
||||
}
|
||||
|
||||
// http://api.bilibili.com/x/player/pagelist?bvid={bvid}
|
||||
// https://api.bilibili.com/x/v1/dm/list.so?oid={cid}
|
||||
bvid = bvid.Trim();
|
||||
var pageUrl = $"http://api.bilibili.com/x/player/pagelist?bvid={bvid}";
|
||||
var response = await httpClient.GetAsync(pageUrl, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoPart[]>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Data != null)
|
||||
{
|
||||
var part = result.Data.FirstOrDefault();
|
||||
if (part != null)
|
||||
{
|
||||
return await GetDanmuContentByCidAsync(part.Cid, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Request fail. bvid={bvid}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get bilibili danmu data.
|
||||
/// </summary>
|
||||
/// <param name="bvid">The Bilibili bvid.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>Task{TraktResponseDataContract}.</returns>
|
||||
public async Task<byte[]> GetDanmuContentAsync(long epId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (epId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(epId));
|
||||
}
|
||||
|
||||
|
||||
var season = await GetEpisodeAsync(epId, cancellationToken).ConfigureAwait(false);
|
||||
if (season != null && season.Episodes.Length > 0)
|
||||
{
|
||||
var episode = season.Episodes.First(x => x.Id == epId);
|
||||
if (episode != null)
|
||||
{
|
||||
return await GetDanmuContentByCidAsync(episode.CId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Request fail. epId={epId}");
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetDanmuContentByCidAsync(long cid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cid <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cid));
|
||||
}
|
||||
|
||||
var url = $"https://api.bilibili.com/x/v1/dm/list.so?oid={cid}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Request fail. url={url} status_code={response.StatusCode}");
|
||||
}
|
||||
|
||||
// 数据太小可能是已经被b站下架,返回了出错信息
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (bytes == null || bytes.Length < 2000)
|
||||
{
|
||||
throw new Exception($"弹幕获取失败,可能视频已下架或弹幕太少. url: {url}");
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
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(30) };
|
||||
SearchResult searchResult;
|
||||
if (_memoryCache.TryGetValue<SearchResult>(cacheKey, out searchResult))
|
||||
{
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (seasonId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"season_{seasonId}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
VideoSeason? seasonData;
|
||||
if (_memoryCache.TryGetValue<VideoSeason?>(cacheKey, out seasonData))
|
||||
{
|
||||
return seasonData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"http://api.bilibili.com/pgc/view/web/season?season_id={seasonId}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Result != null)
|
||||
{
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, result.Result, expiredOption);
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<VideoSeason?> GetEpisodeAsync(long epId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (epId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"episode_{epId}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
VideoSeason? seasonData;
|
||||
if (_memoryCache.TryGetValue<VideoSeason?>(cacheKey, out seasonData))
|
||||
{
|
||||
return seasonData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"http://api.bilibili.com/pgc/view/web/season?ep_id={epId}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Result != null)
|
||||
{
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, result.Result, expiredOption);
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<VideoSeason?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<Video?> GetVideoByBvidAsync(string bvid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bvid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"video_{bvid}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
Video? videoData;
|
||||
if (_memoryCache.TryGetValue<Video?>(cacheKey, out videoData))
|
||||
{
|
||||
return videoData;
|
||||
}
|
||||
|
||||
await EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var url = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResult<Video>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Code == 0 && result.Data != null)
|
||||
{
|
||||
_memoryCache.Set<Video?>(cacheKey, result.Data, expiredOption);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
_memoryCache.Set<Video?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task EnsureSessionCookie(CancellationToken cancellationToken)
|
||||
{
|
||||
var url = "https://www.bilibili.com";
|
||||
var cookies = _cookieContainer.GetCookies(new Uri(url, UriKind.Absolute));
|
||||
var existCookie = cookies.FirstOrDefault(x => x.Name == "buvid3");
|
||||
if (existCookie != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
protected void LimitRequestFrequently()
|
||||
{
|
||||
var diff = 0;
|
||||
lock (_lock)
|
||||
{
|
||||
var ts = DateTime.Now - lastRequestTime;
|
||||
diff = (int)(1000 - ts.TotalMilliseconds);
|
||||
lastRequestTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if (diff > 0)
|
||||
{
|
||||
this._logger.LogDebug("请求太频繁,等待{0}毫秒后继续执行...", diff);
|
||||
Thread.Sleep(diff);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_memoryCache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class ApiResult<T>
|
||||
{
|
||||
@@ -7,7 +7,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Internal;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class Media
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class SearchResult
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class Video
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class VideoEpisode
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class VideoPart
|
||||
{
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Api.Entity
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
{
|
||||
public class VideoSeason
|
||||
{
|
||||
160
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs
Normal file
160
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan;
|
||||
|
||||
public class Dandan : AbstractScraper
|
||||
{
|
||||
public const string ScraperProviderName = "弹弹play";
|
||||
public const string ScraperProviderId = "DandanID";
|
||||
|
||||
private readonly DandanApi _api;
|
||||
|
||||
public Dandan(ILoggerFactory logManager)
|
||||
: base(logManager.CreateLogger<Dandan>())
|
||||
{
|
||||
_api = new DandanApi(logManager);
|
||||
}
|
||||
|
||||
public override int DefaultOrder => 2;
|
||||
|
||||
public override bool DefaultEnable => true;
|
||||
|
||||
public override string Name => "弹弹play";
|
||||
|
||||
public override string ProviderName => ScraperProviderName;
|
||||
|
||||
public override string ProviderId => ScraperProviderId;
|
||||
|
||||
public override async Task<string?> GetMatchMediaId(BaseItem item)
|
||||
{
|
||||
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;
|
||||
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
|
||||
if (isMovieItemType && anime.Type != "movie")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMovieItemType && anime.Type == "movie")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检测标题是否相似(越大越相似)
|
||||
var score = searchName.Distance(title);
|
||||
if (score < 0.7)
|
||||
{
|
||||
log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检测年份是否一致
|
||||
var itemPubYear = item.ProductionYear ?? 0;
|
||||
if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear)
|
||||
{
|
||||
log.LogInformation("[{0}] 发行年份不一致,忽略处理. dandan:{1} jellyfin: {2}", title, pubYear, itemPubYear);
|
||||
continue;
|
||||
}
|
||||
|
||||
return $"{animeId}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public override async Task<ScraperMedia?> GetMedia(string id)
|
||||
{
|
||||
var animeId = id.ToLong();
|
||||
if (animeId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var anime = await _api.GetAnimeAsync(animeId, CancellationToken.None).ConfigureAwait(false);
|
||||
if (anime == null)
|
||||
{
|
||||
log.LogInformation("[{0}]获取不到视频信息:id={1}", this.Name, animeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var media = new ScraperMedia();
|
||||
media.Id = id;
|
||||
media.Name = anime.AnimeTitle;
|
||||
if (anime.Episodes != null && anime.Episodes.Count > 0)
|
||||
{
|
||||
foreach (var item in anime.Episodes)
|
||||
{
|
||||
media.Episodes.Add(new ScraperEpisode() { Id = $"{item.EpisodeId}", CommentId = $"{item.EpisodeId}" });
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
public override async Task<ScraperEpisode?> GetMediaEpisode(string id)
|
||||
{
|
||||
var epId = id.ToLong();
|
||||
if (epId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScraperEpisode() { Id = id, CommentId = id };
|
||||
}
|
||||
|
||||
public override async Task<ScraperDanmaku?> GetDanmuContent(string commentId)
|
||||
{
|
||||
var cid = commentId.ToLong();
|
||||
if (cid <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var comments = await _api.GetCommentsAsync(cid, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmaku = new ScraperDanmaku();
|
||||
danmaku.ChatId = cid;
|
||||
danmaku.ChatServer = "api.dandanplay.net";
|
||||
foreach (var item in comments)
|
||||
{
|
||||
var danmakuText = new ScraperDanmakuText();
|
||||
var arr = item.P.Split(",");
|
||||
danmakuText.Progress = (int)(Convert.ToDouble(arr[0]) * 1000);
|
||||
danmakuText.Mode = Convert.ToInt32(arr[1]);
|
||||
danmakuText.Color = Convert.ToUInt32(arr[2]);
|
||||
danmakuText.MidHash = arr[3];
|
||||
danmakuText.Id = item.Cid;
|
||||
danmakuText.Content = item.Text;
|
||||
|
||||
danmaku.Items.Add(danmakuText);
|
||||
|
||||
}
|
||||
|
||||
return danmaku;
|
||||
}
|
||||
|
||||
|
||||
private string NormalizeSearchName(string name)
|
||||
{
|
||||
// 去掉可能存在的季名称
|
||||
return Regex.Replace(name, @"\s*第.季", "");
|
||||
}
|
||||
}
|
||||
151
Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs
Normal file
151
Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Common.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity;
|
||||
using Jellyfin.Plugin.Danmu.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan;
|
||||
|
||||
public class DandanApi : AbstractApi
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private DateTime lastRequestTime = DateTime.Now.AddDays(-1);
|
||||
|
||||
public DandanOption Config
|
||||
{
|
||||
get
|
||||
{
|
||||
return Plugin.Instance?.Configuration.Dandan ?? new DandanOption();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DandanApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public DandanApi(ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory.CreateLogger<DandanApi>())
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<Anime>> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
return new List<Anime>();
|
||||
}
|
||||
|
||||
var cacheKey = $"search_{keyword}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
List<Anime> searchResult;
|
||||
if (_memoryCache.TryGetValue<List<Anime>>(cacheKey, out searchResult))
|
||||
{
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
this.LimitRequestFrequently();
|
||||
|
||||
keyword = HttpUtility.UrlEncode(keyword);
|
||||
var url = $"https://api.dandanplay.net/api/v2/search/anime?keyword={keyword}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SearchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Success)
|
||||
{
|
||||
_memoryCache.Set<List<Anime>>(cacheKey, result.Animes, expiredOption);
|
||||
return result.Animes;
|
||||
}
|
||||
|
||||
_memoryCache.Set<List<Anime>>(cacheKey, new List<Anime>(), expiredOption);
|
||||
return new List<Anime>();
|
||||
}
|
||||
|
||||
public async Task<Anime?> GetAnimeAsync(long animeId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (animeId <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"anime_{animeId}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
Anime? anime;
|
||||
if (_memoryCache.TryGetValue<Anime?>(cacheKey, out anime))
|
||||
{
|
||||
return anime;
|
||||
}
|
||||
|
||||
var url = $"https://api.dandanplay.net/api/v2/bangumi/{animeId}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var ddd = response.Content.ToString();
|
||||
var result = await response.Content.ReadFromJsonAsync<AnimeResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Success)
|
||||
{
|
||||
_memoryCache.Set<Anime?>(cacheKey, result.Bangumi, expiredOption);
|
||||
return result.Bangumi;
|
||||
}
|
||||
|
||||
_memoryCache.Set<Anime?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<List<Comment>> GetCommentsAsync(long epId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (epId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(epId));
|
||||
}
|
||||
|
||||
var withRelated = this.Config.WithRelatedDanmu ? "true" : "false";
|
||||
var chConvert = this.Config.ChConvert;
|
||||
var url = $"https://api.dandanplay.net/api/v2/comment/{epId}?withRelated={withRelated}&chConvert={chConvert}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<CommentResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
{
|
||||
return result.Comments;
|
||||
}
|
||||
|
||||
throw new Exception($"Request fail. epId={epId}");
|
||||
}
|
||||
|
||||
protected void LimitRequestFrequently(double intervalMilliseconds = 1000)
|
||||
{
|
||||
var diff = 0;
|
||||
lock (_lock)
|
||||
{
|
||||
var ts = DateTime.Now - lastRequestTime;
|
||||
diff = (int)(intervalMilliseconds - ts.TotalMilliseconds);
|
||||
lastRequestTime = DateTime.Now;
|
||||
}
|
||||
|
||||
if (diff > 0)
|
||||
{
|
||||
this._logger.LogDebug("请求太频繁,等待{0}毫秒后继续执行...", diff);
|
||||
Thread.Sleep(diff);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
57
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Anime.cs
Normal file
57
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Anime.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class Anime
|
||||
{
|
||||
|
||||
[JsonPropertyName("animeId")]
|
||||
public long AnimeId { get; set; }
|
||||
|
||||
[JsonPropertyName("animeTitle")]
|
||||
public string AnimeTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("typeDescription")]
|
||||
public string TypeDescription { get; set; }
|
||||
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("startDate")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
[JsonPropertyName("episodeCount")]
|
||||
public int? EpisodeCount { get; set; }
|
||||
|
||||
[JsonPropertyName("episodes")]
|
||||
public List<Episode>? Episodes { get; set; }
|
||||
|
||||
public int? Year
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
if (StartDate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return DateTime.Parse(StartDate).Year;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/AnimeResult.cs
Normal file
25
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/AnimeResult.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class AnimeResult
|
||||
{
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; }
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
|
||||
[JsonPropertyName("bangumi")]
|
||||
public Anime? Bangumi { get; set; }
|
||||
}
|
||||
}
|
||||
22
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Comment.cs
Normal file
22
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Comment.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class Comment
|
||||
{
|
||||
[JsonPropertyName("cid")]
|
||||
public long Cid { get; set; }
|
||||
|
||||
// p参数格式为出现时间,模式,颜色,用户ID,各个参数之间使用英文逗号分隔
|
||||
[JsonPropertyName("p")]
|
||||
public string P { get; set; }
|
||||
|
||||
[JsonPropertyName("m")]
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class CommentResult
|
||||
{
|
||||
[JsonPropertyName("comments")]
|
||||
public List<Comment> Comments { get; set; }
|
||||
}
|
||||
}
|
||||
16
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Episode.cs
Normal file
16
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/Episode.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class Episode
|
||||
{
|
||||
[JsonPropertyName("episodeId")]
|
||||
public long EpisodeId { get; set; }
|
||||
|
||||
[JsonPropertyName("episodeTitle")]
|
||||
public string EpisodeTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("episodeNumber")]
|
||||
public int EpisodeNumber { get; set; }
|
||||
}
|
||||
}
|
||||
26
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/SearchResult.cs
Normal file
26
Jellyfin.Plugin.Danmu/Scrapers/Dandan/Entity/SearchResult.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
|
||||
{
|
||||
public class SearchResult
|
||||
{
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; }
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
|
||||
|
||||
[JsonPropertyName("animes")]
|
||||
public List<Anime> Animes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class EpisodeExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Dandan.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Dandan.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "#";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Episode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class MovieExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Dandan.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Dandan.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "#";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Movie;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SeasonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Dandan.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Dandan.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "#";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Season;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using System.Xml.Serialization;
|
||||
using System.IO;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
|
||||
@@ -85,13 +87,13 @@ public class ScraperDanmakuText : IXmlSerializable
|
||||
public long Id { get; set; } //弹幕dmID
|
||||
public int Progress { get; set; } //出现时间(单位ms)
|
||||
public int Mode { get; set; } //弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
|
||||
public int Fontsize { get; set; } //文字大小
|
||||
public int Fontsize { get; set; } = 25; //文字大小
|
||||
public uint Color { get; set; } //弹幕颜色
|
||||
public string MidHash { get; set; } //发送者UID的HASH
|
||||
public string Content { get; set; } //弹幕内容
|
||||
public long Ctime { get; set; } //发送时间
|
||||
public int Weight { get; set; } //权重
|
||||
//public string Action { get; set; } //动作?
|
||||
public int Weight { get; set; } = 1; //权重
|
||||
//public string Action { get; set; } //动作?
|
||||
public int Pool { get; set; } //弹幕池
|
||||
|
||||
public XmlSchema? GetSchema()
|
||||
@@ -106,12 +108,39 @@ public class ScraperDanmakuText : IXmlSerializable
|
||||
|
||||
public void WriteXml(XmlWriter writer)
|
||||
{
|
||||
|
||||
// bilibili弹幕格式:
|
||||
// <d p="944.95400,5,25,16707842,1657598634,0,ece5c9d1,1094775706690331648,11">今天的风儿甚是喧嚣</d>
|
||||
// time, mode, size, color, create, pool, sender, id, weight(屏蔽等级)
|
||||
var time = (Convert.ToDouble(Progress) / 1000).ToString("F05");
|
||||
var attr = string.Format("{0},{1},{2},{3},{4},{5},{6},{7},{8}", time, Mode, Fontsize, Color, Ctime, Pool, MidHash, Id, Weight);
|
||||
writer.WriteAttributeString("p", attr);
|
||||
writer.WriteString(Content);
|
||||
if (IsValidXmlString(Content))
|
||||
{
|
||||
writer.WriteString(Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteString(RemoveInvalidXmlChars(Content));
|
||||
}
|
||||
}
|
||||
|
||||
private string RemoveInvalidXmlChars(string text)
|
||||
{
|
||||
var validXmlChars = text.Where(ch => XmlConvert.IsXmlChar(ch)).ToArray();
|
||||
return new string(validXmlChars);
|
||||
}
|
||||
|
||||
private bool IsValidXmlString(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlConvert.VerifyXmlChars(text);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers;
|
||||
|
||||
public class ScraperFactory
|
||||
{
|
||||
private List<AbstractScraper> scrapers { get; }
|
||||
|
||||
public ScraperFactory(ILoggerFactory logManager, BilibiliApi api)
|
||||
{
|
||||
scrapers = new List<AbstractScraper>() {
|
||||
new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(logManager, api)
|
||||
};
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<AbstractScraper> All()
|
||||
{
|
||||
return new ReadOnlyCollection<AbstractScraper>(scrapers);
|
||||
}
|
||||
}
|
||||
61
Jellyfin.Plugin.Danmu/Scrapers/ScraperManager.cs
Normal file
61
Jellyfin.Plugin.Danmu/Scrapers/ScraperManager.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers;
|
||||
|
||||
public class ScraperManager
|
||||
{
|
||||
protected ILogger log;
|
||||
private List<AbstractScraper> _scrapers = new List<AbstractScraper>();
|
||||
|
||||
public ScraperManager(ILoggerFactory logManager)
|
||||
{
|
||||
log = logManager.CreateLogger<ScraperManager>();
|
||||
if (Plugin.Instance?.Scrapers != null)
|
||||
{
|
||||
this._scrapers.AddRange(Plugin.Instance.Scrapers);
|
||||
}
|
||||
}
|
||||
|
||||
public void register(AbstractScraper scraper)
|
||||
{
|
||||
this._scrapers.Add(scraper);
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<AbstractScraper> All()
|
||||
{
|
||||
// 存在配置时,根据配置调整源顺序,并删除不启用的源
|
||||
if (Plugin.Instance?.Configuration.Scrapers != null)
|
||||
{
|
||||
var orderScrapers = new List<AbstractScraper>();
|
||||
|
||||
var scraperMap = this._scrapers.ToDictionary(x => x.Name, x => x);
|
||||
var configScrapers = Plugin.Instance.Configuration.Scrapers;
|
||||
foreach (var config in configScrapers)
|
||||
{
|
||||
if (scraperMap.ContainsKey(config.Name) && config.Enable)
|
||||
{
|
||||
orderScrapers.Add(scraperMap[config.Name]);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新增并默认启用的源
|
||||
var allOldScaperNames = configScrapers.Select(o => o.Name).ToList();
|
||||
foreach (var scraper in this._scrapers)
|
||||
{
|
||||
if (!allOldScaperNames.Contains(scraper.Name) && scraper.DefaultEnable)
|
||||
{
|
||||
orderScrapers.Add(scraper);
|
||||
}
|
||||
}
|
||||
|
||||
return orderScrapers.AsReadOnly();
|
||||
}
|
||||
|
||||
return this._scrapers.AsReadOnly();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Api;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -12,6 +11,9 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Dandan;
|
||||
using MediaBrowser.Common;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu
|
||||
{
|
||||
@@ -25,17 +27,13 @@ namespace Jellyfin.Plugin.Danmu
|
||||
{
|
||||
return new Jellyfin.Plugin.Danmu.Core.FileSystem();
|
||||
});
|
||||
serviceCollection.AddSingleton<BilibiliApi>((ctx) =>
|
||||
serviceCollection.AddSingleton<ScraperManager>((ctx) =>
|
||||
{
|
||||
return new BilibiliApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
serviceCollection.AddSingleton<ScraperFactory>((ctx) =>
|
||||
{
|
||||
return new ScraperFactory(ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<BilibiliApi>());
|
||||
return new ScraperManager(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
serviceCollection.AddSingleton<LibraryManagerEventsHelper>((ctx) =>
|
||||
{
|
||||
return new LibraryManagerEventsHelper(ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<Jellyfin.Plugin.Danmu.Core.IFileSystem>(), ctx.GetRequiredService<ScraperFactory>());
|
||||
return new LibraryManagerEventsHelper(ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<Jellyfin.Plugin.Danmu.Core.IFileSystem>(), ctx.GetRequiredService<ScraperManager>());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/main/LICENSE)
|
||||
|
||||
jellyfin的b站弹幕自动下载插件,会匹配b站番剧和电影视频,自动下载对应弹幕,并定时更新。
|
||||
jellyfin的弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play。
|
||||
|
||||
支持功能:
|
||||
|
||||
@@ -76,7 +76,7 @@ $ dotnet publish Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
|
||||
|
||||
2. Create a folder, like `Danmu` and copy `bin/Release/Jellyfin.Plugin.Danmu.dll` into it
|
||||
|
||||
3. Move folder `Danmu` to jellyfin `data/plugin` folder
|
||||
3. Move folder `Danmu` to jellyfin `data/plugins` folder
|
||||
|
||||
## Thanks
|
||||
|
||||
|
||||
Reference in New Issue
Block a user