mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-02-02 17:59:58 +08:00
Add danmu: iqiyi
This commit is contained in:
40
.github/workflows/publish.yaml
vendored
40
.github/workflows/publish.yaml
vendored
@@ -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"
|
||||
|
||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -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}",
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@@ -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",
|
||||
}
|
||||
55
.vscode/tasks.json
vendored
55
.vscode/tasks.json
vendored
@@ -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}/"
|
||||
]
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
101
Jellyfin.Plugin.Danmu.Test/DandanTest.cs
Normal file
101
Jellyfin.Plugin.Danmu.Test/DandanTest.cs
Normal file
@@ -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<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "异邦人:无皇刃谭"
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestUpdateMovie()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.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 = "异邦人:无皇刃谭",
|
||||
ProviderIds = new Dictionary<string, string>() { { Dandan.ScraperProviderId, "2185" } },
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Update });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
108
Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs
Normal file
108
Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
137
Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs
Normal file
137
Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs
Normal file
@@ -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<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "少年派的奇幻漂流"
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestUpdateMovie()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Iqiyi(loggerFactory));
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "少年派的奇幻漂流",
|
||||
ProviderIds = new Dictionary<string, string>() { { Iqiyi.ScraperProviderId, "19rrjv4kz0" } },
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Update });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestAddSeason()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Iqiyi(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 = "沉默的真相",
|
||||
ProductionYear = 2020,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jellyfin.Plugin.Danmu\Jellyfin.Plugin.Danmu.csproj" />
|
||||
|
||||
@@ -322,6 +322,8 @@ namespace Jellyfin.Plugin.Danmu.Test
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
136
Jellyfin.Plugin.Danmu.Test/YoukuTest.cs
Normal file
136
Jellyfin.Plugin.Danmu.Test/YoukuTest.cs
Normal file
@@ -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<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "西虹市首富"
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Add });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestUpdateMovie()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Youku.Youku(loggerFactory));
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "西虹市首富",
|
||||
ProviderIds = new Dictionary<string, string>() { { Youku.ScraperProviderId, "XMzg5Njc2Njk0OA==" } },
|
||||
};
|
||||
|
||||
var list = new List<LibraryEvent>();
|
||||
list.Add(new LibraryEvent { Item = item, EventType = EventType.Update });
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestAddSeason()
|
||||
{
|
||||
var scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Youku.Youku(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 = "布莱切利四人组",
|
||||
ProductionYear = 2012,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,12 +31,34 @@ public abstract class AbstractScraper
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public abstract Task<string?> GetMatchMediaId(BaseItem item);
|
||||
/// <summary>
|
||||
/// 搜索影片id
|
||||
/// </summary>
|
||||
/// <param name="item">元数据item</param>
|
||||
/// <returns>影片id</returns>
|
||||
public abstract Task<string?> SearchMediaId(BaseItem item);
|
||||
|
||||
public abstract Task<ScraperMedia?> GetMedia(string id);
|
||||
/// <summary>
|
||||
/// 获取影片信息
|
||||
/// </summary>
|
||||
/// <param name="item">元数据item</param>
|
||||
/// <param name="id">影片id</param>
|
||||
/// <returns>影片信息</returns>
|
||||
public abstract Task<ScraperMedia?> GetMedia(BaseItem item, string id);
|
||||
|
||||
public abstract Task<ScraperEpisode?> GetMediaEpisode(string id);
|
||||
/// <summary>
|
||||
/// 需要更新弹幕时调用
|
||||
/// </summary>
|
||||
/// <param name="item">元数据item</param>
|
||||
/// <param name="id">元数据保存的id</param>
|
||||
/// <returns>剧集信息</returns>
|
||||
public abstract Task<ScraperEpisode?> GetMediaEpisode(BaseItem item, string id);
|
||||
|
||||
|
||||
public abstract Task<ScraperDanmaku?> GetDanmuContent(string commentId);
|
||||
/// <summary>
|
||||
/// 获取弹幕
|
||||
/// </summary>
|
||||
/// <param name="item">元数据item</param>
|
||||
/// <param name="commentId">弹幕id</param>
|
||||
/// <returns>弹幕内容</returns>
|
||||
public abstract Task<ScraperDanmaku?> GetDanmuContent(BaseItem item, string commentId);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class Bilibili : AbstractScraper
|
||||
|
||||
public override string ProviderId => ScraperProviderId;
|
||||
|
||||
public override async Task<string?> GetMatchMediaId(BaseItem item)
|
||||
public override async Task<string?> 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<ScraperMedia?> GetMedia(string id)
|
||||
public override async Task<ScraperMedia?> 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<ScraperEpisode?> GetMediaEpisode(string id)
|
||||
public override async Task<ScraperEpisode?> 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<ScraperDanmaku?> GetDanmuContent(string commentId)
|
||||
public override async Task<ScraperDanmaku?> GetDanmuContent(BaseItem item, string commentId)
|
||||
{
|
||||
var cid = commentId.ToLong();
|
||||
if (cid > 0)
|
||||
|
||||
@@ -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));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BilibiliApi"/> 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<SearchResult>(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()
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
|
||||
|
||||
|
||||
[JsonPropertyName("episodes")]
|
||||
public VideoEpisode[] Episodes { get; set; }
|
||||
public List<VideoEpisode> Episodes { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,9 @@ public class Dandan : AbstractScraper
|
||||
|
||||
public override string ProviderId => ScraperProviderId;
|
||||
|
||||
public override async Task<string?> GetMatchMediaId(BaseItem item)
|
||||
public override async Task<string?> 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<ScraperMedia?> GetMedia(string id)
|
||||
public override async Task<ScraperMedia?> 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<ScraperEpisode?> GetMediaEpisode(string id)
|
||||
public override async Task<ScraperEpisode?> 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<ScraperDanmaku?> GetDanmuContent(string commentId)
|
||||
public override async Task<ScraperDanmaku?> 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);
|
||||
|
||||
|
||||
@@ -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<Anime> searchResult;
|
||||
if (_memoryCache.TryGetValue<List<Anime>>(cacheKey, out searchResult))
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.ExternalId
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "#";
|
||||
public string UrlFormatString => "https://api.dandanplay.net/api/v2/bangumi/{0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Movie;
|
||||
|
||||
@@ -4,16 +4,23 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
|
||||
public class ScraperMedia
|
||||
{
|
||||
/// <summary>
|
||||
/// item是电影/季时,使用本id作为元数据值
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// item是季时,本CommentId用不到
|
||||
/// </summary>
|
||||
public string CommentId { get; set; }
|
||||
public List<ScraperEpisode> Episodes { get; set; } = new List<ScraperEpisode>();
|
||||
|
||||
}
|
||||
|
||||
public class ScraperEpisode
|
||||
{
|
||||
/// <summary>
|
||||
/// 当item是剧集时,使用本id作为元数据值
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
public string CommentId { get; set; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
32
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiComment.cs
Normal file
32
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiComment.cs
Normal file
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<IqiyiCommentEntry> Data { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class IqiyiCommentEntry
|
||||
{
|
||||
[XmlElement("int")]
|
||||
public int Index { get; set; }
|
||||
|
||||
[XmlArray("list")]
|
||||
[XmlArrayItem("bulletInfo", typeof(IqiyiComment))]
|
||||
public List<IqiyiComment> List { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
68
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiEpisode.cs
Normal file
68
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiEpisode.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggest.cs
Normal file
44
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSuggest.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IqiyiSuggest> Data { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
54
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs
Normal file
54
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs
Normal file
@@ -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<IqiyiEpisode> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class EpisodeExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Iqiyi.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Iqiyi.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Episode;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class MovieExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Iqiyi.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Iqiyi.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Movie;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SeasonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Iqiyi.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Iqiyi.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://www.iqiyi.com/a_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Season;
|
||||
}
|
||||
|
||||
}
|
||||
188
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs
Normal file
188
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs
Normal file
@@ -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<Iqiyi>())
|
||||
{
|
||||
_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<string?> 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<ScraperMedia?> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ScraperEpisode?> 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<ScraperDanmaku?> 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*第.季", "");
|
||||
}
|
||||
}
|
||||
259
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs
Normal file
259
Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs
Normal file
@@ -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(@"<a.*?h5-show-card.*?>([\w\W]+?)</a>", RegexOptions.Compiled);
|
||||
private static readonly Regex trackInfoReg = new Regex(@"data-trackinfo=""(\{[\w\W]+?\})""", RegexOptions.Compiled);
|
||||
private static readonly Regex featureReg = new Regex(@"<div.*?show-feature.*?>([\w\W]+?)</div>", 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IqiyiApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public IqiyiApi(ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory.CreateLogger<IqiyiApi>())
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<IqiyiSuggest>> GetSuggestAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
return new List<IqiyiSuggest>();
|
||||
}
|
||||
|
||||
var cacheKey = $"search_{keyword}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) };
|
||||
if (_memoryCache.TryGetValue<List<IqiyiSuggest>>(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<IqiyiSuggest>();
|
||||
var searchResult = await response.Content.ReadFromJsonAsync<IqiyiSuggestResult>(_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<List<IqiyiSuggest>>(cacheKey, result, expiredOption);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IqiyiVideo?> 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<IqiyiVideo?>(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<IqiyiVideoResult>(_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<IqiyiEpisode>() {
|
||||
new IqiyiEpisode() {TvId = result.Data.TvId, Order = 1, Name = result.Data.Name, Duration = result.Data.Duration, PlayUrl = result.Data.PlayUrl}
|
||||
};
|
||||
}
|
||||
|
||||
_memoryCache.Set<IqiyiVideo?>(cacheKey, result.Data, expiredOption);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
_memoryCache.Set<IqiyiVideo?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<List<IqiyiEpisode>> GetEpisodesAsync(string albumId, int size, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(albumId))
|
||||
{
|
||||
return new List<IqiyiEpisode>();
|
||||
}
|
||||
|
||||
|
||||
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<IqiyiAlbumResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (albumResult != null && albumResult.Data != null && albumResult.Data.Epsodelist != null)
|
||||
{
|
||||
return albumResult.Data.Epsodelist;
|
||||
}
|
||||
|
||||
return new List<IqiyiEpisode>();
|
||||
}
|
||||
|
||||
public async Task<string> GetTvId(string id, bool isAlbum, CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = $"tvid_{id}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
if (_memoryCache.TryGetValue<string>(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<string>(cacheKey, tvid, expiredOption);
|
||||
|
||||
return tvid;
|
||||
}
|
||||
|
||||
public async Task<List<IqiyiComment>> GetDanmuContentAsync(string tvId, CancellationToken cancellationToken)
|
||||
{
|
||||
var danmuList = new List<IqiyiComment>();
|
||||
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<List<IqiyiComment>> GetDanmuContentByMatAsync(string tvId, int mat, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tvId))
|
||||
{
|
||||
return new List<IqiyiComment>();
|
||||
}
|
||||
|
||||
|
||||
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<IqiyiComment>();
|
||||
foreach (var entry in result.Data)
|
||||
{
|
||||
comments.AddRange(entry.List);
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new List<IqiyiComment>();
|
||||
}
|
||||
|
||||
protected async Task LimitRequestFrequently()
|
||||
{
|
||||
await this._timeConstraint;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class Youku : AbstractScraper
|
||||
|
||||
public override string ProviderId => ScraperProviderId;
|
||||
|
||||
public override async Task<string?> GetMatchMediaId(BaseItem item)
|
||||
public override async Task<string?> 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<ScraperMedia?> GetMedia(string id)
|
||||
public override async Task<ScraperMedia?> 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<ScraperEpisode?> GetMediaEpisode(string id)
|
||||
public override async Task<ScraperEpisode?> 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<ScraperDanmaku?> GetDanmuContent(string commentId)
|
||||
public override async Task<ScraperDanmaku?> 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<YoukuCommentProperty>(item.Propertis);
|
||||
var property = JsonSerializer.Deserialize<YoukuCommentProperty>(comment.Propertis);
|
||||
if (property != null)
|
||||
{
|
||||
danmakuText.Color = property.Color;
|
||||
|
||||
@@ -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<List<YoukuVideo>>(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<YoukuRpcResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && !string.IsNullOrEmpty(result.Data.Result))
|
||||
{
|
||||
|
||||
13
build.meta.json
Normal file
13
build.meta.json
Normal file
@@ -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"
|
||||
}
|
||||
15
build.yaml
15
build.yaml
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user