Add danmu: iqiyi

This commit is contained in:
cxfksword
2023-02-11 16:03:44 +08:00
parent 3fab33f241
commit f53a81ba94
40 changed files with 1575 additions and 239 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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}/"
]
},
]
}

View 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();
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View File

@@ -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" />

View File

@@ -322,6 +322,8 @@ namespace Jellyfin.Plugin.Danmu.Test
}).GetAwaiter().GetResult();
}
}
}

View 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();
}
}
}

View File

@@ -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;
}

View File

@@ -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" />

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -17,7 +17,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
[JsonPropertyName("episodes")]
public VideoEpisode[] Episodes { get; set; }
public List<VideoEpisode> Episodes { get; set; }
}
}

View File

@@ -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);

View File

@@ -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))
{

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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; }
}
}

View 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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View 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;
}
}
}
}
}

View 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;
}
}
}
}
}

View File

@@ -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; }
}
}

View 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;
}
}
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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编码后的idmovie使用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*第.季", "");
}
}

View 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;
}
}

View File

@@ -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;

View File

@@ -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
View 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"
}

View File

@@ -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

View File

@@ -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"