diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0200864..8e55fda 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -8,9 +8,7 @@ env: dotnet-version: 6.0.x python-version: 3.8 project: Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj - branch: main artifact: danmu - manifest: https://github.com/cxfksword/jellyfin-plugin-danmu/releases/download/manifest/manifest.json jobs: build: @@ -25,8 +23,26 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.dotnet-version }} - - name: Restore nuget packages - run: dotnet restore ${{ env.project }} # 需要指定项目,要不然会同时build多个项目导致出错 + - name: Initialize workflow variables + id: vars + run: | + VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//) + VERSION="$VERSION.0" + echo ::set-output name=VERSION::${VERSION} + echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}') + - name: Install dependencies + run: dotnet restore ${{ env.project }} --no-cache + - name: Build + run: dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 --output=artifacts -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }} + - name: Clean publish dll + run: cd artifacts && rm -rf MediaBrowser*.dll Microsoft*.dll Newtonsoft*.dll System*.dll Emby*.dll Jellyfin.Data*.dll Jellyfin.Extensions*.dll *.json *.pdb + - name: Compress build files + uses: thedoctor0/zip-release@main + with: + type: "zip" + directory: "artifacts" + filename: "artifacts.zip" + exclusions: "*.json *.pdb" - name: Setup python uses: actions/setup-python@v2 with: @@ -34,25 +50,19 @@ jobs: - name: Install JPRM run: python -m pip install jprm - name: Run JPRM - run: chmod +x ./build_plugin.sh && ./build_plugin.sh ${GITHUB_REF#refs/*/} - - name: Update release + run: chmod +x ./build_plugin.sh && ./build_plugin.sh ${{ env.artifact }} ${{steps.vars.outputs.VERSION}} ${GITHUB_REF#refs/*/} + - name: Publish release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ./artifacts/${{ env.artifact }}_*.zip + file: ./${{ env.artifact }}/${{ env.artifact }}_*.zip tag: ${{ github.ref }} file_glob: true - - name: Update manifest + - name: Publish manifest uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ./*.json + file: ./manifest*.json tag: "manifest" overwrite: true file_glob: true - # - name: Update manifest - # uses: stefanzweifel/git-auto-commit-action@v4 - # with: - # branch: ${{ env.branch }} - # commit_message: Update repo manifest - # file_pattern: "*.json" diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e132924..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Paths and plugin names are configured in settings.json - "version": "0.2.0", - "configurations": [ - { - "type": "coreclr", - "name": "Launch", - "request": "launch", - "preLaunchTask": "build-and-copy", - "program": "${config:jellyfinDir}/bin/Debug/net6.0/jellyfin.dll", - "args": [ - //"--nowebclient" - "--webdir", - "${config:jellyfinWebDir}/dist/" - ], - "cwd": "${config:jellyfinDir}", - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4b08fe7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // jellyfinDir : The directory of the cloned jellyfin server project - // This needs to be built once before it can be used - "jellyfinDir" : "${workspaceFolder}/../jellyfin/Jellyfin.Server", - // jellyfinWebDir : The directory of the cloned jellyfin-web project - // This needs to be built once before it can be used - "jellyfinWebDir" : "${workspaceFolder}/../jellyfin-web", - // jellyfinDataDir : the root data directory for a running jellyfin instance - // This is where jellyfin stores its configs, plugins, metadata etc - // This is platform specific by default, but on Windows defaults to - // ${env:LOCALAPPDATA}/jellyfin - "jellyfinDataDir" : "${env:LOCALAPPDATA}/jellyfin", - // The name of the plugin - "pluginName" : "Jellyfin.Plugin.Danmuku", -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index cd2f73f..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - // Paths and plugin name are configured in settings.json - "version": "2.0.0", - "tasks": [ - { - // A chain task - build the plugin, then copy it to your - // jellyfin server's plugin directory - "label": "build-and-copy", - "dependsOrder": "sequence", - "dependsOn": ["build", "make-plugin-dir", "copy-dll"] - }, - { - // Build the plugin - "label": "build", - "command": "dotnet", - "type": "shell", - "args": [ - "publish", - "${workspaceFolder}/${config:pluginName}.sln", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "group": "build", - "presentation": { - "reveal": "silent" - }, - "problemMatcher": "$msCompile" - }, - { - // Ensure the plugin directory exists before trying to use it - "label": "make-plugin-dir", - "type": "shell", - "command": "mkdir", - "args": [ - "-Force", - "-Path", - "${config:jellyfinDataDir}/plugins/${config:pluginName}/" - ] - }, - { - // Copy the plugin dll to the jellyfin plugin install path - // This command copies every .dll from the build directory to the plugin dir - // Usually, you probablly only need ${config:pluginName}.dll - // But some plugins may bundle extra requirements - "label": "copy-dll", - "type": "shell", - "command": "cp", - "args": [ - "./${config:pluginName}/bin/Debug/net6.0/publish/*", - "${config:jellyfinDataDir}/plugins/${config:pluginName}/" - ] - - }, - ] -} diff --git a/Jellyfin.Plugin.Danmu.Test/DandanTest.cs b/Jellyfin.Plugin.Danmu.Test/DandanTest.cs new file mode 100644 index 0000000..3fe8693 --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/DandanTest.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Model; +using Jellyfin.Plugin.Danmu.Scrapers; +using Jellyfin.Plugin.Danmu.Scrapers.Dandan; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class DandanTest + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + })); + + + + [TestMethod] + public void TestAddMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "异邦人:无皇刃谭" + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Add }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + [TestMethod] + public void TestUpdateMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Dandan.Dandan(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "异邦人:无皇刃谭", + ProviderIds = new Dictionary() { { Dandan.ScraperProviderId, "2185" } }, + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Update }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs new file mode 100644 index 0000000..227eeff --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs @@ -0,0 +1,108 @@ +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.Iqiyi; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class IqiyiApiTest + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + })); + + [TestMethod] + public void TestSearch() + { + var api = new IqiyiApi(loggerFactory); + + Task.Run(async () => + { + try + { + var keyword = "狂飙"; + var result = await api.GetSuggestAsync(keyword, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + + [TestMethod] + public void TestGetVideo() + { + var api = new IqiyiApi(loggerFactory); + + Task.Run(async () => + { + try + { + var vid = "3493131456125200"; // 电视剧 + // var vid = "429872"; // 电影 + var result = await api.GetVideoAsync(vid, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + + [TestMethod] + public void TestGetDanmuContentByMat() + { + var api = new IqiyiApi(loggerFactory); + + Task.Run(async () => + { + try + { + var vid = "132987200"; + var result = await api.GetDanmuContentByMatAsync(vid, 1, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + [TestMethod] + public void TestGetDanmu() + { + var api = new IqiyiApi(loggerFactory); + + Task.Run(async () => + { + try + { + var vid = "132987200"; + var result = await api.GetDanmuContentAsync(vid, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs b/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs new file mode 100644 index 0000000..60aebde --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Model; +using Jellyfin.Plugin.Danmu.Scrapers; +using Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class IqiyiTest + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + })); + + + + [TestMethod] + public void TestAddMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Iqiyi(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "少年派的奇幻漂流" + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Add }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + [TestMethod] + public void TestUpdateMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Iqiyi(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "少年派的奇幻漂流", + ProviderIds = new Dictionary() { { Iqiyi.ScraperProviderId, "19rrjv4kz0" } }, + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Update }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + + + [TestMethod] + public void TestAddSeason() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Iqiyi(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Season + { + Name = "沉默的真相", + ProductionYear = 2020, + }; + + var list = new List(); + 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(); + + } + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/Jellyfin.Plugin.Danmu.Test.csproj b/Jellyfin.Plugin.Danmu.Test/Jellyfin.Plugin.Danmu.Test.csproj index cfe2e6a..4ce5b0e 100644 --- a/Jellyfin.Plugin.Danmu.Test/Jellyfin.Plugin.Danmu.Test.csproj +++ b/Jellyfin.Plugin.Danmu.Test/Jellyfin.Plugin.Danmu.Test.csproj @@ -11,6 +11,7 @@ + diff --git a/Jellyfin.Plugin.Danmu.Test/LibraryManagerEventsHelperTest.cs b/Jellyfin.Plugin.Danmu.Test/LibraryManagerEventsHelperTest.cs index ca61a65..de305b1 100644 --- a/Jellyfin.Plugin.Danmu.Test/LibraryManagerEventsHelperTest.cs +++ b/Jellyfin.Plugin.Danmu.Test/LibraryManagerEventsHelperTest.cs @@ -322,6 +322,8 @@ namespace Jellyfin.Plugin.Danmu.Test }).GetAwaiter().GetResult(); } + + } } diff --git a/Jellyfin.Plugin.Danmu.Test/YoukuTest.cs b/Jellyfin.Plugin.Danmu.Test/YoukuTest.cs new file mode 100644 index 0000000..2f33c0c --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/YoukuTest.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Model; +using Jellyfin.Plugin.Danmu.Scrapers; +using Jellyfin.Plugin.Danmu.Scrapers.Youku; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class YoukuTest + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + })); + + + + [TestMethod] + public void TestAddMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Youku.Youku(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "西虹市首富" + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Add }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + [TestMethod] + public void TestUpdateMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Youku.Youku(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "西虹市首富", + ProviderIds = new Dictionary() { { Youku.ScraperProviderId, "XMzg5Njc2Njk0OA==" } }, + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Update }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + [TestMethod] + public void TestAddSeason() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Youku.Youku(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Season + { + Name = "布莱切利四人组", + ProductionYear = 2012, + }; + + var list = new List(); + 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(); + + } + + } +} diff --git a/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs b/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs index 30a1ba3..af4fc78 100644 --- a/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs +++ b/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs @@ -12,8 +12,7 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions { public static long ToLong(this string s) { - long val; - if (long.TryParse(s, out val)) + if (long.TryParse(s, out var val)) { return val; } @@ -23,8 +22,17 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions public static int ToInt(this string s) { - int val; - if (int.TryParse(s, out val)) + if (int.TryParse(s, out var val)) + { + return val; + } + + return 0; + } + + public static Int64 ToInt64(this string s) + { + if (Int64.TryParse(s, out var val)) { return val; } @@ -45,8 +53,7 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions public static double ToDouble(this string s) { - double val; - if (double.TryParse(s, out val)) + if (double.TryParse(s, out var val)) { return val; } diff --git a/Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj b/Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj index 47c9283..6e55755 100644 --- a/Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj +++ b/Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj @@ -16,6 +16,7 @@ + diff --git a/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs b/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs index bb7bff7..5b43640 100644 --- a/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs +++ b/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs @@ -254,18 +254,18 @@ public class LibraryManagerEventsHelper : IDisposable // 读取最新数据,要不然取不到年份信息 var currentItem = _libraryManager.GetItemById(item.Id) ?? item; - var mediaId = await scraper.GetMatchMediaId(currentItem); + var mediaId = await scraper.SearchMediaId(currentItem); if (string.IsNullOrEmpty(mediaId)) { - _logger.LogInformation("[{0}]匹配失败:{1}", scraper.Name, item.Name); + _logger.LogInformation("[{0}]匹配失败:{1} ({2})", scraper.Name, item.Name, item.ProductionYear); continue; } - var media = await scraper.GetMedia(mediaId); - if (media != null && media.Episodes.Count > 0) + var media = await scraper.GetMedia(item, mediaId); + if (media != null) { - var providerVal = media.Episodes[0].Id; - var commentId = media.Episodes[0].CommentId; + var providerVal = media.Id; + var commentId = media.CommentId; _logger.LogInformation("[{0}]匹配成功:name={1} ProviderId: {2}", scraper.Name, item.Name, providerVal); // 更新epid元数据 @@ -304,7 +304,7 @@ public class LibraryManagerEventsHelper : IDisposable var providerVal = item.GetProviderId(scraper.ProviderId); if (!string.IsNullOrEmpty(providerVal)) { - var episode = await scraper.GetMediaEpisode(providerVal); + var episode = await scraper.GetMediaEpisode(item, providerVal); if (episode != null) { // 下载弹幕xml文件 @@ -415,17 +415,20 @@ public class LibraryManagerEventsHelper : IDisposable { currentItem.Name = series.Name; } - var mediaId = await scraper.GetMatchMediaId(currentItem); - - if (!string.IsNullOrEmpty(mediaId)) + var mediaId = await scraper.SearchMediaId(currentItem); + if (string.IsNullOrEmpty(mediaId)) { - // 更新seasonId元数据 - season.SetProviderId(scraper.ProviderId, mediaId); - queueUpdateMeta.Add(season); - - _logger.LogInformation("[{0}]匹配成功:name={1} season_number={2} ProviderId: {3}", scraper.Name, season.Name, season.IndexNumber, mediaId); - break; + _logger.LogInformation("[{0}]匹配失败:{1} ({2})", scraper.Name, currentItem.Name, currentItem.ProductionYear); + continue; } + + + // 更新seasonId元数据 + season.SetProviderId(scraper.ProviderId, mediaId); + queueUpdateMeta.Add(season); + + _logger.LogInformation("[{0}]匹配成功:name={1} season_number={2} ProviderId: {3}", scraper.Name, season.Name, season.IndexNumber, mediaId); + break; } catch (FrequentlyRequestException ex) { @@ -463,7 +466,7 @@ public class LibraryManagerEventsHelper : IDisposable continue; } - var media = await scraper.GetMedia(providerVal); + var media = await scraper.GetMedia(season, providerVal); if (media == null) { _logger.LogInformation("[{0}]获取不到视频信息. ProviderId: {1}", scraper.Name, providerVal); @@ -489,7 +492,7 @@ public class LibraryManagerEventsHelper : IDisposable { var epId = media.Episodes[idx].Id; var commentId = media.Episodes[idx].CommentId; - _logger.LogInformation("[{0}]成功匹配. {1} -> epId: {2} cid: {3}", scraper.Name, episode.Name, epId, commentId); + _logger.LogInformation("[{0}]成功匹配. {1}.{2} -> epId: {3} cid: {4}", scraper.Name, indexNumber, episode.Name, epId, commentId); // 更新eposide元数据 var episodeProviderVal = episode.GetProviderId(scraper.ProviderId); @@ -564,7 +567,7 @@ public class LibraryManagerEventsHelper : IDisposable continue; } - var episode = await scraper.GetMediaEpisode(providerVal); + var episode = await scraper.GetMediaEpisode(item, providerVal); if (episode != null) { // 下载弹幕xml文件 @@ -619,14 +622,14 @@ public class LibraryManagerEventsHelper : IDisposable // 下载弹幕xml文件 try { - // 弹幕一分钟内更新过,忽略处理(有时Update事件会重复执行) + // 弹幕5分钟内更新过,忽略处理(有时Update事件会重复执行) if (IsRepeatAction(item)) { - _logger.LogInformation("[{0}]最近1分钟已更新过弹幕xml,忽略处理:{1}", scraper.Name, item.Name); + _logger.LogInformation("[{0}]最近5分钟已更新过弹幕xml,忽略处理:{1}", scraper.Name, item.Name); return; } - var danmaku = await scraper.GetDanmuContent(commentId); + var danmaku = await scraper.GetDanmuContent(item, commentId); if (danmaku != null) { await this.DownloadDanmuInternal(item, danmaku.ToXml()); @@ -641,6 +644,9 @@ public class LibraryManagerEventsHelper : IDisposable private bool IsRepeatAction(BaseItem item) { + // 单元测试时为null + if (item.FileNameWithoutExtension == null) return false; + var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml"); if (!this._fileSystem.Exists(danmuPath)) { @@ -649,11 +655,14 @@ public class LibraryManagerEventsHelper : IDisposable var lastWriteTime = this._fileSystem.GetLastWriteTime(danmuPath); var diff = DateTime.Now - lastWriteTime; - return diff.TotalSeconds < 60; + return diff.TotalSeconds < 300; } private async Task DownloadDanmuInternal(BaseItem item, byte[] bytes) { + // 单元测试时为null + if (item.FileNameWithoutExtension == null) return; + // 下载弹幕xml文件 var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml"); await this._fileSystem.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Plugin.Danmu/Scrapers/AbstractScraper.cs b/Jellyfin.Plugin.Danmu/Scrapers/AbstractScraper.cs index 64e2fac..fc1bea9 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/AbstractScraper.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/AbstractScraper.cs @@ -31,12 +31,34 @@ public abstract class AbstractScraper this.log = log; } - public abstract Task GetMatchMediaId(BaseItem item); + /// + /// 搜索影片id + /// + /// 元数据item + /// 影片id + public abstract Task SearchMediaId(BaseItem item); - public abstract Task GetMedia(string id); + /// + /// 获取影片信息 + /// + /// 元数据item + /// 影片id + /// 影片信息 + public abstract Task GetMedia(BaseItem item, string id); - public abstract Task GetMediaEpisode(string id); + /// + /// 需要更新弹幕时调用 + /// + /// 元数据item + /// 元数据保存的id + /// 剧集信息 + public abstract Task GetMediaEpisode(BaseItem item, string id); - - public abstract Task GetDanmuContent(string commentId); + /// + /// 获取弹幕 + /// + /// 元数据item + /// 弹幕id + /// 弹幕内容 + public abstract Task GetDanmuContent(BaseItem item, string commentId); } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs index 99ae781..a6d5d6e 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs @@ -36,7 +36,7 @@ public class Bilibili : AbstractScraper public override string ProviderId => ScraperProviderId; - public override async Task GetMatchMediaId(BaseItem item) + public override async Task SearchMediaId(BaseItem item) { var searchName = this.NormalizeSearchName(item.Name); var seasonId = await GetMatchBiliSeasonId(item, searchName).ConfigureAwait(false); @@ -49,9 +49,11 @@ public class Bilibili : AbstractScraper } - public override async Task GetMedia(string id) + public override async Task GetMedia(BaseItem item, string id) { var media = new ScraperMedia(); + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + if (id.StartsWith("BV", StringComparison.CurrentCulture)) { var video = await _api.GetVideoByBvidAsync(id, CancellationToken.None).ConfigureAwait(false); @@ -61,9 +63,6 @@ public class Bilibili : AbstractScraper return null; } - - media.Id = id; - media.Name = video.Title; if (video.UgcSeason != null && video.UgcSeason.Sections != null && video.UgcSeason.Sections.Count > 0) { // 合集 @@ -81,6 +80,16 @@ public class Bilibili : AbstractScraper } } + if (isMovieItemType) + { + media.Id = id; + media.CommentId = media.Episodes.Count > 0 ? $"{media.Episodes[0].CommentId}" : ""; + } + else + { + media.Id = id; + } + return media; } @@ -97,17 +106,25 @@ public class Bilibili : AbstractScraper return null; } - media.Id = id; - media.Name = season.Title; - foreach (var item in season.Episodes) + foreach (var ep in season.Episodes) { - media.Episodes.Add(new ScraperEpisode() { Id = $"{item.Id}", CommentId = $"{item.CId}" }); + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.Id}", CommentId = $"{ep.CId}" }); + } + + if (isMovieItemType) + { + media.Id = season.Episodes.Count > 0 ? $"{season.Episodes[0].Id}" : ""; + media.CommentId = season.Episodes.Count > 0 ? $"{season.Episodes[0].CId}" : ""; + } + else + { + media.Id = id; } return media; } - public override async Task GetMediaEpisode(string id) + public override async Task GetMediaEpisode(BaseItem item, string id) { var episode = new ScraperEpisode(); if (id.StartsWith("BV", StringComparison.CurrentCulture)) @@ -141,7 +158,7 @@ public class Bilibili : AbstractScraper return null; } - if (season.Episodes.Length > 0) + if (season.Episodes.Count > 0) { return new ScraperEpisode() { Id = $"{season.Episodes[0].Id}", CommentId = $"{season.Episodes[0].CId}" }; } @@ -149,7 +166,7 @@ public class Bilibili : AbstractScraper return null; } - public override async Task GetDanmuContent(string commentId) + public override async Task GetDanmuContent(BaseItem item, string commentId) { var cid = commentId.ToLong(); if (cid > 0) diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs index 5c58030..ad09e73 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs @@ -21,6 +21,8 @@ using System.Web; using Microsoft.Extensions.Caching.Memory; using Jellyfin.Plugin.Danmu.Core.Http; using Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity; +using RateLimiter; +using ComposableAsync; namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili; @@ -33,7 +35,7 @@ public class BilibiliApi : IDisposable private CookieContainer _cookieContainer; private readonly IMemoryCache _memoryCache; private static readonly object _lock = new object(); - private DateTime lastRequestTime = DateTime.Now.AddDays(-1); + private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); /// /// Initializes a new instance of the class. @@ -97,7 +99,7 @@ public class BilibiliApi : IDisposable var season = await GetEpisodeAsync(epId, cancellationToken).ConfigureAwait(false); - if (season != null && season.Episodes.Length > 0) + if (season != null && season.Episodes.Count > 0) { var episode = season.Episodes.First(x => x.Id == epId); if (episode != null) @@ -142,14 +144,14 @@ public class BilibiliApi : IDisposable } var cacheKey = $"search_{keyword}"; - var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; SearchResult searchResult; if (_memoryCache.TryGetValue(cacheKey, out searchResult)) { return searchResult; } - this.LimitRequestFrequently(); + await this.LimitRequestFrequently(); await EnsureSessionCookie(cancellationToken).ConfigureAwait(false); keyword = HttpUtility.UrlEncode(keyword); @@ -275,21 +277,9 @@ public class BilibiliApi : IDisposable response.EnsureSuccessStatusCode(); } - protected void LimitRequestFrequently() + protected async Task 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); - } + await this._timeConstraint; } public void Dispose() diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Entity/VideoSeason.cs b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Entity/VideoSeason.cs index 383e847..d18e220 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Entity/VideoSeason.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Entity/VideoSeason.cs @@ -17,7 +17,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity [JsonPropertyName("episodes")] - public VideoEpisode[] Episodes { get; set; } + public List Episodes { get; set; } } } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs index e575799..a3d90b3 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs @@ -37,8 +37,9 @@ public class Dandan : AbstractScraper public override string ProviderId => ScraperProviderId; - public override async Task GetMatchMediaId(BaseItem item) + public override async Task SearchMediaId(BaseItem item) { + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; var searchName = this.NormalizeSearchName(item.Name); var animes = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false); foreach (var anime in animes) @@ -46,7 +47,6 @@ public class Dandan : AbstractScraper 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") { @@ -81,7 +81,7 @@ public class Dandan : AbstractScraper } - public override async Task GetMedia(string id) + public override async Task GetMedia(BaseItem item, string id) { var animeId = id.ToLong(); if (animeId <= 0) @@ -89,6 +89,7 @@ public class Dandan : AbstractScraper return null; } + var anime = await _api.GetAnimeAsync(animeId, CancellationToken.None).ConfigureAwait(false); if (anime == null) { @@ -96,32 +97,54 @@ public class Dandan : AbstractScraper return null; } + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; var media = new ScraperMedia(); + media.Id = id; - media.Name = anime.AnimeTitle; + if (isMovieItemType && anime.Episodes != null && anime.Episodes.Count > 0) + { + media.CommentId = $"{anime.Episodes[0].EpisodeId}"; + } if (anime.Episodes != null && anime.Episodes.Count > 0) { - foreach (var item in anime.Episodes) + foreach (var ep in anime.Episodes) { - media.Episodes.Add(new ScraperEpisode() { Id = $"{item.EpisodeId}", CommentId = $"{item.EpisodeId}" }); + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.EpisodeId}", CommentId = $"{ep.EpisodeId}" }); } } + return media; } - public override async Task GetMediaEpisode(string id) + public override async Task GetMediaEpisode(BaseItem item, string id) { - var epId = id.ToLong(); - if (epId <= 0) + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + if (isMovieItemType) { - return null; - } + // id是animeId + var anime = await _api.GetAnimeAsync(id.ToLong(), CancellationToken.None).ConfigureAwait(false); + if (anime == null || anime.Episodes == null || anime.Episodes.Count <= 0) + { + return null; + } - return new ScraperEpisode() { Id = id, CommentId = id }; + return new ScraperEpisode() { Id = id, CommentId = $"{anime.Episodes[0].EpisodeId}" }; + } + else + { + // id是episodeId + var epId = id.ToLong(); + if (epId <= 0) + { + return null; + } + + return new ScraperEpisode() { Id = id, CommentId = id }; + } } - public override async Task GetDanmuContent(string commentId) + public override async Task GetDanmuContent(BaseItem item, string commentId) { var cid = commentId.ToLong(); if (cid <= 0) @@ -133,16 +156,16 @@ public class Dandan : AbstractScraper var danmaku = new ScraperDanmaku(); danmaku.ChatId = cid; danmaku.ChatServer = "api.dandanplay.net"; - foreach (var item in comments) + foreach (var comment in comments) { var danmakuText = new ScraperDanmakuText(); - var arr = item.P.Split(","); + var arr = comment.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; + danmakuText.Id = comment.Cid; + danmakuText.Content = comment.Text; danmaku.Items.Add(danmakuText); diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs index f8cca88..3b7fcc2 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/DandanApi.cs @@ -55,7 +55,7 @@ public class DandanApi : AbstractApi } var cacheKey = $"search_{keyword}"; - var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; List searchResult; if (_memoryCache.TryGetValue>(cacheKey, out searchResult)) { diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/ExternalId/MovieExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/ExternalId/MovieExternalId.cs index 1e451af..3cf753f 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/ExternalId/MovieExternalId.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/ExternalId/MovieExternalId.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId public ExternalIdMediaType? Type => null; /// - public string UrlFormatString => "#"; + public string UrlFormatString => "https://api.dandanplay.net/api/v2/bangumi/{0}"; /// public bool Supports(IHasProviderIds item) => item is Movie; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperMedia.cs b/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperMedia.cs index af8f159..24933f5 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperMedia.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperMedia.cs @@ -4,16 +4,23 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Entity; public class ScraperMedia { + /// + /// item是电影/季时,使用本id作为元数据值 + /// public string Id { get; set; } - public string Name { get; set; } - public int? Year { get; set; } - + /// + /// item是季时,本CommentId用不到 + /// + public string CommentId { get; set; } public List Episodes { get; set; } = new List(); } public class ScraperEpisode { + /// + /// 当item是剧集时,使用本id作为元数据值 + /// public string Id { get; set; } public string CommentId { get; set; } } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiAlbumResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiAlbumResult.cs new file mode 100644 index 0000000..b36bd4b --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiAlbumResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiAlbumResult + { + [JsonPropertyName("data")] + public IqiyiVideo Data { get; set; } + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiComment.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiComment.cs new file mode 100644 index 0000000..fdc3258 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiComment.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiComment + { + + + [XmlElement("contentId")] + public string ContentId { get; set; } + + [XmlElement("content")] + public string Content { get; set; } + + [XmlElement("font")] + public int Font { get; set; } + + [XmlElement("color")] + public string Color { get; set; } + [XmlElement("userInfo")] + public IqiyiCommentUser UserInfo { get; set; } + [XmlElement("showTime")] + public Int64 ShowTime { get; set; } + + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentDocument.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentDocument.cs new file mode 100644 index 0000000..8c79333 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentDocument.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + [XmlRoot("danmu")] + public class IqiyiCommentDocument + { + [XmlElement("sum")] + public Int64 sum { get; set; } + + [XmlElement("validSum")] + public Int64 validSum { get; set; } + + [XmlElement("duration")] + public Int64 duration { get; set; } + + + [XmlArray("data")] + [XmlArrayItem("entry", typeof(IqiyiCommentEntry))] + public List Data { get; set; } + } + + + public class IqiyiCommentEntry + { + [XmlElement("int")] + public int Index { get; set; } + + [XmlArray("list")] + [XmlArrayItem("bulletInfo", typeof(IqiyiComment))] + public List List { get; set; } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentUser.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentUser.cs new file mode 100644 index 0000000..9f574f6 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiCommentUser.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiCommentUser + { + [XmlElement("uid")] + public string Uid { get; set; } + [XmlElement("udid")] + public string Udid { get; set; } + + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiEpisode.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiEpisode.cs new file mode 100644 index 0000000..d639268 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiEpisode.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiEpisode + { + private static readonly Regex regLinkId = new Regex(@"v_(\w+?)\.html", RegexOptions.Compiled); + + + [JsonPropertyName("tvId")] + public Int64 TvId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("order")] + public int Order { get; set; } + + [JsonPropertyName("duration")] + public string Duration { get; set; } + + [JsonPropertyName("playUrl")] + public string PlayUrl { get; set; } + + + public int TotalMat + { + get + { + if (Duration.Length == 5 && TimeSpan.TryParseExact(Duration, @"mm\:ss", null, out var duration)) + { + return (int)Math.Floor(duration.TotalSeconds / 300) + 1; + } + + if (Duration.Length == 8 && TimeSpan.TryParseExact(Duration, @"hh\:mm\:ss", null, out var durationHour)) + { + return (int)Math.Floor(durationHour.TotalSeconds / 300) + 1; + } + + return 0; + } + + } + + public string LinkId + { + get + { + var match = regLinkId.Match(PlayUrl); + if (match.Success && match.Groups.Count > 1) + { + return match.Groups[1].Value.Trim(); + } + else + { + return null; + } + } + } + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggest.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggest.cs new file mode 100644 index 0000000..706a7ba --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggest.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiSuggest + { + private static readonly Regex regLinkId = new Regex(@"(v_|a_)(\w+?)\.html", RegexOptions.Compiled); + + [JsonPropertyName("aid")] + public Int64 AlbumId { get; set; } + [JsonPropertyName("vid")] + public Int64 VideoId { get; set; } + [JsonPropertyName("link")] + public string Link { get; set; } + [JsonPropertyName("name")] + public String Name { get; set; } + [JsonPropertyName("cname")] + public String ChannelName { get; set; } + [JsonPropertyName("year")] + public int Year { get; set; } + + public string LinkId + { + get + { + var match = regLinkId.Match(Link); + if (match.Success && match.Groups.Count > 2) + { + return match.Groups[2].Value.Trim(); + } + else + { + return null; + } + } + } + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggestResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggestResult.cs new file mode 100644 index 0000000..77441c4 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggestResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiSuggestResult + { + [JsonPropertyName("data")] + public List Data { get; set; } + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs new file mode 100644 index 0000000..105995c --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs @@ -0,0 +1,54 @@ +using System.Text.RegularExpressions; +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.Iqiyi.Entity +{ + public class IqiyiVideo + { + private static readonly Regex regLinkId = new Regex(@"(v_|a_)(\w+?)\.html", RegexOptions.Compiled); + + [JsonPropertyName("albumId")] + public Int64 AlbumId { get; set; } + [JsonPropertyName("tvId")] + public Int64 TvId { get; set; } + [JsonPropertyName("playUrl")] + public string PlayUrl { get; set; } + [JsonPropertyName("albumUrl")] + public string AlbumUrl { get; set; } + [JsonPropertyName("name")] + public String Name { get; set; } + [JsonPropertyName("channelId")] + public int ChannelId { get; set; } + [JsonPropertyName("videoCount")] + public int VideoCount { get; set; } + [JsonPropertyName("duration")] + public String Duration { get; set; } + [JsonPropertyName("publishTime")] + public Int64 publishTime { get; set; } + + [JsonPropertyName("epsodelist")] + public List Epsodelist { get; set; } + + + public string LinkId + { + get + { + var match = regLinkId.Match(AlbumUrl); + if (match.Success && match.Groups.Count > 2) + { + return match.Groups[2].Value.Trim(); + } + else + { + return null; + } + } + } + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideoResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideoResult.cs new file mode 100644 index 0000000..a22f208 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideoResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiVideoResult + { + [JsonPropertyName("data")] + public IqiyiVideo Data { get; set; } + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/EpisodeExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/EpisodeExternalId.cs new file mode 100644 index 0000000..391a4de --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/EpisodeExternalId.cs @@ -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.Iqiyi.ExternalId +{ + /// + public class EpisodeExternalId : IExternalId + { + /// + public string ProviderName => Iqiyi.ScraperProviderName; + + /// + public string Key => Iqiyi.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Episode; + + /// + public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Episode; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/MovieExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/MovieExternalId.cs new file mode 100644 index 0000000..b55ddd7 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/MovieExternalId.cs @@ -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.Iqiyi.ExternalId +{ + /// + public class MovieExternalId : IExternalId + { + /// + public string ProviderName => Iqiyi.ScraperProviderName; + + /// + public string Key => Iqiyi.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Movie; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs new file mode 100644 index 0000000..68a45dc --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs @@ -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.Iqiyi.ExternalId +{ + + /// + public class SeasonExternalId : IExternalId + { + /// + public string ProviderName => Iqiyi.ScraperProviderName; + + /// + public string Key => Iqiyi.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://www.iqiyi.com/a_{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Season; + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs new file mode 100644 index 0000000..1ca9f02 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs @@ -0,0 +1,188 @@ +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; +using System.Text.Json; +using Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; + +public class Iqiyi : AbstractScraper +{ + public const string ScraperProviderName = "爱奇艺"; + public const string ScraperProviderId = "IqiyiID"; + + private readonly IqiyiApi _api; + + public Iqiyi(ILoggerFactory logManager) + : base(logManager.CreateLogger()) + { + _api = new IqiyiApi(logManager); + } + + public override int DefaultOrder => 4; + + public override bool DefaultEnable => false; + + public override string Name => "爱奇艺"; + + public override string ProviderName => ScraperProviderName; + + public override string ProviderId => ScraperProviderId; + + public override async Task SearchMediaId(BaseItem item) + { + var searchName = this.NormalizeSearchName(item.Name); + var videos = await this._api.GetSuggestAsync(searchName, CancellationToken.None).ConfigureAwait(false); + foreach (var video in videos) + { + var videoId = video.VideoId; + var title = video.Name; + var pubYear = video.Year; + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + + if (isMovieItemType && video.ChannelName != "电影") + { + continue; + } + + if (!isMovieItemType && video.ChannelName == "电影") + { + 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}] 发行年份不一致,忽略处理. Iqiyi:{1} jellyfin: {2}", title, pubYear, itemPubYear); + continue; + } + + return video.LinkId; + } + + return null; + } + + + public override async Task GetMedia(BaseItem item, string id) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + // id是编码后的,需要还原为真实id + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + var tvId = await _api.GetTvId(id, !isMovieItemType, CancellationToken.None); + if (string.IsNullOrEmpty(tvId)) + { + return null; + } + + var video = await _api.GetVideoAsync(tvId, CancellationToken.None).ConfigureAwait(false); + if (video == null) + { + log.LogInformation("[{0}]获取不到视频信息:id={1}", this.Name, id); + return null; + } + + + var media = new ScraperMedia(); + media.Id = video.LinkId; // 使用url编码后的id,movie使用vid,电视剧使用aid + if (isMovieItemType && video.Epsodelist != null && video.Epsodelist.Count > 0) + { + media.CommentId = $"{video.Epsodelist[0].TvId}"; + } + if (video.Epsodelist != null && video.Epsodelist.Count > 0) + { + foreach (var ep in video.Epsodelist) + { + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.LinkId}", CommentId = $"{ep.TvId}" }); + } + } + + return media; + } + + /// + public override async Task GetMediaEpisode(BaseItem item, string id) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + // id是编码后的,需要还原为真实id + var tvId = await _api.GetTvId(id, false, CancellationToken.None); + if (string.IsNullOrEmpty(tvId)) + { + return null; + } + + return new ScraperEpisode() { Id = id, CommentId = tvId }; + } + + public override async Task GetDanmuContent(BaseItem item, string commentId) + { + if (string.IsNullOrEmpty(commentId)) + { + return null; + } + + var comments = await _api.GetDanmuContentAsync(commentId, CancellationToken.None).ConfigureAwait(false); + var danmaku = new ScraperDanmaku(); + danmaku.ChatId = commentId.ToLong(); + danmaku.ChatServer = "cmts.iqiyi.com"; + foreach (var comment in comments) + { + try + { + var danmakuText = new ScraperDanmakuText(); + danmakuText.Progress = (int)comment.ShowTime * 1000; + danmakuText.Mode = 1; + danmakuText.MidHash = $"[iqiyi]{comment.UserInfo.Uid}"; + danmakuText.Id = comment.ContentId.ToLong(); + danmakuText.Content = comment.Content; + if (uint.TryParse(comment.Color, System.Globalization.NumberStyles.HexNumber, null, out var color)) + { + danmakuText.Color = color; + } + + danmaku.Items.Add(danmakuText); + } + catch (Exception ex) + { + + } + + } + + return danmaku; + } + + + private string NormalizeSearchName(string name) + { + // 去掉可能存在的季名称 + return Regex.Replace(name, @"\s*第.季", ""); + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs new file mode 100644 index 0000000..b256b76 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs @@ -0,0 +1,259 @@ +using System.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using ComposableAsync; +using Jellyfin.Plugin.Danmu.Core.Extensions; +using Jellyfin.Plugin.Danmu.Scrapers.Entity; +using Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using RateLimiter; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System.Xml.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; + +public class IqiyiApi : AbstractApi +{ + const string HTTP_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/93.0.4577.63"; + private static readonly object _lock = new object(); + private static readonly Regex yearReg = new Regex(@"[12][890][0-9][0-9]", RegexOptions.Compiled); + private static readonly Regex moviesReg = new Regex(@"([\w\W]+?)", RegexOptions.Compiled); + private static readonly Regex trackInfoReg = new Regex(@"data-trackinfo=""(\{[\w\W]+?\})""", RegexOptions.Compiled); + private static readonly Regex featureReg = new Regex(@"([\w\W]+?)", RegexOptions.Compiled); + private static readonly Regex unusedReg = new Regex(@"\[.+?\]|\(.+?\)|【.+?】", RegexOptions.Compiled); + private static readonly Regex regTvId = new Regex(@"""tvid"":(\d+?),", RegexOptions.Compiled); + + + private DateTime lastRequestTime = DateTime.Now.AddDays(-1); + + private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); + + protected string _cna = string.Empty; + protected string _token = string.Empty; + protected string _tokenEnc = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public IqiyiApi(ILoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) + { + httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT); + } + + + public async Task> GetSuggestAsync(string keyword, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(keyword)) + { + return new List(); + } + + var cacheKey = $"search_{keyword}"; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; + if (_memoryCache.TryGetValue>(cacheKey, out var cacheValue)) + { + return cacheValue; + } + + keyword = HttpUtility.UrlEncode(keyword); + var url = $"https://suggest.video.iqiyi.com/?key={keyword}&platform=11&rltnum=10&ppuid="; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = new List(); + var searchResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResult != null && searchResult.Data != null) + { + result = searchResult.Data.Where(x => !string.IsNullOrEmpty(x.Link) && x.Link.Contains("iqiyi.com") && x.VideoId > 0).ToList(); + } + + _memoryCache.Set>(cacheKey, result, expiredOption); + return result; + } + + public async Task GetVideoAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + var cacheKey = $"video_{id}"; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + if (_memoryCache.TryGetValue(cacheKey, out var video)) + { + return video; + } + + // 获取影片信息(vid):https://pcw-api.iqiyi.com/video/video/baseinfo/429872 + // 获取电视剧信息(aid):https://pcw-api.iqiyi.com/album/album/baseinfo/5328486914190101 + // 获取影片剧集信息(aid):https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid=5328486914190101&page=1&size=100 + var url = $"https://pcw-api.iqiyi.com/video/video/baseinfo/{id}"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (result != null && result.Data != null) + { + // 电视剧需要再获取剧集信息 + if (result.Data.ChannelId != 1) + { + result.Data.Epsodelist = await this.GetEpisodesAsync($"{result.Data.AlbumId}", result.Data.VideoCount, cancellationToken).ConfigureAwait(false); + } + else + { + result.Data.Epsodelist = new List() { + new IqiyiEpisode() {TvId = result.Data.TvId, Order = 1, Name = result.Data.Name, Duration = result.Data.Duration, PlayUrl = result.Data.PlayUrl} + }; + } + + _memoryCache.Set(cacheKey, result.Data, expiredOption); + return result.Data; + } + + _memoryCache.Set(cacheKey, null, expiredOption); + return null; + } + + public async Task> GetEpisodesAsync(string albumId, int size, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(albumId)) + { + return new List(); + } + + + var url = $"https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid={albumId}&page=1&size={size}"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var albumResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (albumResult != null && albumResult.Data != null && albumResult.Data.Epsodelist != null) + { + return albumResult.Data.Epsodelist; + } + + return new List(); + } + + public async Task GetTvId(string id, bool isAlbum, CancellationToken cancellationToken) + { + var cacheKey = $"tvid_{id}"; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + if (_memoryCache.TryGetValue(cacheKey, out var tvid)) + { + return tvid; + } + + var url = isAlbum ? $"https://m.iqiyi.com/a_{id}.html" : $"https://m.iqiyi.com/v_{id}.html"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var body = await response.Content.ReadAsStringAsync(); + tvid = regTvId.FirstMatchGroup(body); + _memoryCache.Set(cacheKey, tvid, expiredOption); + + return tvid; + } + + public async Task> GetDanmuContentAsync(string tvId, CancellationToken cancellationToken) + { + var danmuList = new List(); + if (string.IsNullOrEmpty(tvId)) + { + return danmuList; + } + + int mat = 1; + do + { + try + { + var comments = await this.GetDanmuContentByMatAsync(tvId, mat, cancellationToken); + danmuList.AddRange(comments); + } + catch (Exception ex) + { + break; + } + + mat++; + } while (mat < 1000); + + return danmuList; + } + + // mat从0开始,视频分钟数 + public async Task> GetDanmuContentByMatAsync(string tvId, int mat, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(tvId)) + { + return new List(); + } + + + var s1 = tvId.Substring(tvId.Length - 4, 2); + var s2 = tvId.Substring(tvId.Length - 2); + // 一次拿300秒的弹幕 + var url = $"http://cmts.iqiyi.com/bullet/{s1}/{s2}/{tvId}_300_{mat}.z"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + using (var zipStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + { + byte[] decompressedData = new byte[4096]; + int decompressedLength = 0; + using (var memoryStream = new MemoryStream()) + { + using (InflaterInputStream inflater = new InflaterInputStream(zipStream)) + { + do + { + decompressedLength = inflater.Read(decompressedData, 0, decompressedData.Length); + memoryStream.Write(decompressedData, 0, decompressedLength); + } while (decompressedLength > 0); + } + + memoryStream.Position = 0; + using (var reader = new StreamReader(memoryStream)) + { + var serializer = new XmlSerializer(typeof(IqiyiCommentDocument)); + + var result = serializer.Deserialize(reader) as IqiyiCommentDocument; + if (result != null && result.Data != null) + { + var comments = new List(); + foreach (var entry in result.Data) + { + comments.AddRange(entry.List); + } + return comments; + } + } + } + + } + + return new List(); + } + + protected async Task LimitRequestFrequently() + { + await this._timeConstraint; + } + +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs b/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs index c4db22a..99f8d37 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs @@ -39,7 +39,7 @@ public class Youku : AbstractScraper public override string ProviderId => ScraperProviderId; - public override async Task GetMatchMediaId(BaseItem item) + public override async Task SearchMediaId(BaseItem item) { var searchName = this.NormalizeSearchName(item.Name); var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false); @@ -83,7 +83,7 @@ public class Youku : AbstractScraper } - public override async Task GetMedia(string id) + public override async Task GetMedia(BaseItem item, string id) { if (string.IsNullOrEmpty(id)) { @@ -97,21 +97,30 @@ public class Youku : AbstractScraper return null; } + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; var media = new ScraperMedia(); - media.Id = id; - media.Name = "no name"; if (video.Videos != null && video.Videos.Count > 0) { - foreach (var item in video.Videos) + foreach (var ep in video.Videos) { - media.Episodes.Add(new ScraperEpisode() { Id = $"{item.ID}", CommentId = $"{item.ID}" }); + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.ID}", CommentId = $"{ep.ID}" }); } } + if (isMovieItemType) + { + media.Id = media.Episodes.Count > 0 ? $"{media.Episodes[0].Id}" : ""; + media.CommentId = media.Episodes.Count > 0 ? $"{media.Episodes[0].CommentId}" : ""; + } + else + { + media.Id = id; + } + return media; } - public override async Task GetMediaEpisode(string id) + public override async Task GetMediaEpisode(BaseItem item, string id) { if (string.IsNullOrEmpty(id)) { @@ -121,7 +130,7 @@ public class Youku : AbstractScraper return new ScraperEpisode() { Id = id, CommentId = id }; } - public override async Task GetDanmuContent(string commentId) + public override async Task GetDanmuContent(BaseItem item, string commentId) { if (string.IsNullOrEmpty(commentId)) { @@ -132,18 +141,18 @@ public class Youku : AbstractScraper var danmaku = new ScraperDanmaku(); danmaku.ChatId = 1000; danmaku.ChatServer = "acs.youku.com"; - foreach (var item in comments) + foreach (var comment in comments) { try { var danmakuText = new ScraperDanmakuText(); - danmakuText.Progress = (int)item.Playat; + danmakuText.Progress = (int)comment.Playat; danmakuText.Mode = 1; - danmakuText.MidHash = item.Uid; - danmakuText.Id = item.ID; - danmakuText.Content = item.Content; + danmakuText.MidHash = $"[youku]{comment.Uid}"; + danmakuText.Id = comment.ID; + danmakuText.Content = comment.Content; - var property = JsonSerializer.Deserialize(item.Propertis); + var property = JsonSerializer.Deserialize(comment.Propertis); if (property != null) { danmakuText.Color = property.Color; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs index 76d277a..8692d60 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs @@ -56,7 +56,7 @@ public class YoukuApi : AbstractApi } var cacheKey = $"search_{keyword}"; - var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; if (_memoryCache.TryGetValue>(cacheKey, out var cacheValue)) { return cacheValue; @@ -79,8 +79,7 @@ public class YoukuApi : AbstractApi if (component.CommonData == null || component.CommonData.TitleDTO == null || component.CommonData.HasYouku != 1 - || component.CommonData.IsYouku != 1 - || component.CommonData.UgcSupply == 1) + || component.CommonData.IsYouku != 1) { continue; } @@ -259,7 +258,6 @@ public class YoukuApi : AbstractApi } response.EnsureSuccessStatusCode(); - var dd = await response.Content.ReadAsStringAsync(); var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); if (result != null && !string.IsNullOrEmpty(result.Data.Result)) { diff --git a/README.md b/README.md index 1420b5d..3b83af5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Danmu](https://img.shields.io/badge/jellyfin-10.8.x-lightgrey?logo=jellyfin)](https://github.com/cxfksword/jellyfin-plugin-danmu/releases) [![Danmu](https://img.shields.io/github/license/cxfksword/jellyfin-plugin-danmu)](https://github.com/cxfksword/jellyfin-plugin-danmu/main/LICENSE) -jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play,优酷。 +jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play,优酷,爱奇艺。 支持功能: diff --git a/build.meta.json b/build.meta.json new file mode 100644 index 0000000..eee1607 --- /dev/null +++ b/build.meta.json @@ -0,0 +1,13 @@ +{ + "category": "Metadata", + "changelog": "NA", + "description": "jellyfin的b站弹幕自动下载插件,会匹配b站番剧/电影视频,自动下载对应弹幕,并定时更新。", + "guid": "5B39DA44-5314-4940-8E26-54C821C17F86", + "imageUrl": "https://github.com/cxfksword/jellyfin-plugin-danmu/raw/main/doc/logo.png", + "name": "Danmu", + "overview": "jellyfin弹幕下载插件", + "owner": "cxfksword", + "targetAbi": "10.8.0.0", + "timestamp": "1970-01-01T00:00:00Z", + "version": "1.0.0.0" +} diff --git a/build.yaml b/build.yaml deleted file mode 100644 index 7757529..0000000 --- a/build.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Danmu" -guid: "5B39DA44-5314-4940-8E26-54C821C17F86" -imageUrl: https://github.com/cxfksword/jellyfin-plugin-danmu/raw/main/doc/logo.png -version: "1.0.0.0" -targetAbi: "10.8.0.0" -framework: "net6.0" -owner: "cxfksword" -overview: "jellyfin弹幕下载插件" -description: > - jellyfin的b站弹幕自动下载插件,会匹配b站番剧/电影视频,自动下载对应弹幕,并定时更新。 -category: "Metadata" -artifacts: - - "Jellyfin.Plugin.Danmu.dll" -changelog: > - NA diff --git a/build_plugin.sh b/build_plugin.sh index 56cf4a0..08da214 100755 --- a/build_plugin.sh +++ b/build_plugin.sh @@ -1,21 +1,19 @@ #!/bin/bash # $1 from github action -TAG=$1 +ARTIFACT=$1 +VERSION=$2 +TAG=$3 +CURRENT_DATE=$(date +'%Y-%m-%dT%H:%M:%S') WORK_DIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) -ARTIFACT_DIR="${WORK_DIR}/artifacts" -mkdir -p "${ARTIFACT_DIR}" +ARTIFACT_ZIP_FILE="${WORK_DIR}/artifacts/artifacts.zip" +ARTIFACT_META="${WORK_DIR}/build.meta.json" JELLYFIN_REPO_URL="https://github.com/cxfksword/jellyfin-plugin-danmu/releases/download" JELLYFIN_MANIFEST="${WORK_DIR}/manifest.json" JELLYFIN_MANIFEST_CN="${WORK_DIR}/manifest_cn.json" JELLYFIN_MANIFEST_OLD="https://github.com/cxfksword/jellyfin-plugin-danmu/releases/download/manifest/manifest.json" -BUILD_YAML_FILE="${WORK_DIR}/build.yaml" - - -VERSION=$(echo "$TAG" | sed s/^v//) # remove v prefix -VERSION="$VERSION.0" # .NET dll need major.minor[.build[.revision]] version format # download old manifest wget -q -O "$JELLYFIN_MANIFEST" "$JELLYFIN_MANIFEST_OLD" @@ -24,17 +22,19 @@ if [ $? -ne 0 ]; then jprm repo init $WORK_DIR fi -# update change log from tag message +# update meta json message +cp -f "${ARTIFACT_META}" "${ARTIFACT_ZIP_FILE}.meta.json" CHANGELOG=$(git tag -l --format='%(contents)' ${TAG}) -sed -i "s@NA@$CHANGELOG@" $BUILD_YAML_FILE # mac build need change to: -i '' +sed -i "s@NA@$CHANGELOG@" "${ARTIFACT_ZIP_FILE}.meta.json" +sed -i "s@1.0.0.0@$VERSION@" "${ARTIFACT_ZIP_FILE}.meta.json" +sed -i "s@1970-01-01T00:00:00Z@$CURRENT_DATE@" "${ARTIFACT_ZIP_FILE}.meta.json" -# build and generate new manifest -zipfile=$(jprm --verbosity=debug plugin build "." --output="${ARTIFACT_DIR}" --version="${VERSION}" --dotnet-framework="net6.0") && { - jprm --verbosity=debug repo add --url=${JELLYFIN_REPO_URL} "${JELLYFIN_MANIFEST}" "${zipfile}" -} + +# generate new manifest +jprm --verbosity=debug repo add --url=${JELLYFIN_REPO_URL} "${JELLYFIN_MANIFEST}" "${ARTIFACT_ZIP_FILE}" # fix menifest download url -sed -i "s@/danmu@/$TAG@" "$JELLYFIN_MANIFEST" +sed -i "s@/${ARTIFACT}/@/$TAG/@" "$JELLYFIN_MANIFEST" # 国内加速 cp -f "$JELLYFIN_MANIFEST" "$JELLYFIN_MANIFEST_CN"