Compare commits

...

21 Commits
v1.7.8 ... main

Author SHA1 Message Date
cwhzy 2f44ddf6ec
update README.md.
Signed-off-by: cwhzy <cwhzy@foxmail.com>
2024-07-01 04:51:16 +00:00
cxfksword f337406ac9 tweak: optimize identify 2024-06-07 21:45:01 +08:00
cxfksword cb00027b77 fix: episode number not correctly identified. #82 2024-05-25 16:00:19 +08:00
cxfksword 9e9074bb5a build: update github action 2024-05-22 20:38:52 +08:00
cxfksword 747e69f3bf tweak: read attribute from season folder name 2024-05-18 16:12:52 +08:00
cxfksword 104d200e7b fix: actor lack of overview. close #80 2024-05-18 16:04:05 +08:00
cxfksword 8ded89422f
Merge pull request #78 from cxfksword/10.9/preview
tweak: update generate manifest version
2024-05-12 18:04:12 +08:00
cxfksword 25a844a2c4 tweak: update generate manifest version 2024-05-12 18:03:34 +08:00
cxfksword f1f35a6d32
Merge pull request #77 from cxfksword/10.9/preview
Compatible with Jellyfin 10.9
2024-05-12 18:00:02 +08:00
cxfksword 96be3222f9 feat: support path name attribute. close #75 2024-05-12 17:57:07 +08:00
cxfksword 1b547f7aaf Compatible with Jellyfin 10.9. #76 2024-05-12 17:15:46 +08:00
cxfksword 408929fc03 tweak: update log 2024-04-21 14:40:23 +08:00
cxfksword e26794fdcd docs: update README.md 2024-04-13 15:30:16 +08:00
cxfksword 2eca5c9a22 fix: fix find tmdb id by season imdb 2024-04-13 15:26:07 +08:00
cxfksword 23d10b94ed
Update issue template 2024-03-11 21:39:58 +08:00
cxfksword 6f3863eaa7 fix: fix movie similar list only one. #66 2024-02-28 11:02:18 +08:00
cxfksword a9acabc056 fix: support SxxEPxx format. close #60 2024-02-23 14:48:55 +08:00
cxfksword 169ce81a94 tweak: show douban cookie valid msg 2024-02-21 10:23:52 +08:00
cxfksword 3c054cfa2d tweak: split tmdb logo configuration. 2024-02-07 12:16:02 +08:00
cxfksword 10c3414920 fix: can't parse name with number. close #64 2024-02-07 11:54:39 +08:00
cxfksword 513a668c40 feat: add movie logo. close #63 2024-02-03 14:04:52 +08:00
43 changed files with 882 additions and 321 deletions

View File

@ -13,14 +13,16 @@ assignees: ''
**屏幕截图**
如果适用,请添加屏幕截图以帮助解释您的问题。
请添加问题截图以帮助解释您的问题。
**日志**
请提供jellyfin打印的该影片的刮削日志。
日志查看方法: 控制台->高级->日志->点击log_yyyymmdd.log格式文件
**运行环境(请填写以下信息):**
- 操作系统:[例如 linux]
- jellyfin 版本:[例如 22]
- jellyfin 版本:[例如 10.8.9]
- 插件版本:[例如 1.7.1]

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch:
env:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
python-version: 3.8
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
artifact: metashark
@ -36,9 +36,9 @@ jobs:
- name: Build
run: |
dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 ${{ env.project }}
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 ${{ env.project }}
mkdir -p artifacts
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v3
with:

View File

@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-dotnet@v3
id: dotnet
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Change default dotnet version
run: |
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json

View File

@ -5,7 +5,7 @@ on:
tags: ["*"]
env:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
python-version: 3.8
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
artifact: metashark
@ -41,9 +41,9 @@ jobs:
- name: Build
run: |
dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
mkdir -p artifacts
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll
- name: Generate manifest
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
env:
@ -54,6 +54,7 @@ jobs:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/${{ env.artifact }}_*.zip
tag: ${{ github.ref }}
release_name: '${{ github.ref_name }}: Jellyfin v10.9'
file_glob: true
overwrite: true
- name: Publish manifest

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>AnitomySharp.NET6</PackageId>
<PackageVersion>0.4.0</PackageVersion>

View File

@ -67,7 +67,7 @@ namespace AnitomySharp
"番外編", "總集編","DRAMA",
"映像特典","特典","特典アニメ",
// 特典 Special 剩下的各种类型可以全部命名成 SP对于较特殊意义的特典也可以自定义命名
"SPECIAL", "SPECIALS", "SP", "SPs",
"SPECIAL", "SPECIALS", "SP", "SPs", "特報",
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
"IV",
// 音乐视频 Music Video
@ -85,7 +85,7 @@ namespace AnitomySharp
// 无字 OP/ED Non-Credit Opening/Ending
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
"PREVIEW", "YOKOKU",
"PREVIEW", "YOKOKU", "予告",
// 菜单 Menu BD/DVD 播放选择菜单
"MENU",
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右

View File

@ -119,6 +119,29 @@ namespace AnitomySharp
if (string.IsNullOrEmpty(str)) return "";
return Ordinals.TryGetValue(str, out var foundString) ? foundString : "";
}
/// <summary>
/// 转换原始值中的全角数字
///
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string GetNumberFromFullWidth(string str)
{
string output = str;
for (int i = 0; i < str.Length; i++)
{
if (char.IsDigit(str[i]))
{
int fullwidthDigit = (int)str[i];
if (fullwidthDigit >= 65296 && fullwidthDigit <= 65305)
{
int halfwidthDigit = fullwidthDigit - 65248;
output = output.Replace(str[i], (char)halfwidthDigit);
}
}
}
return output;
}
/// <summary>
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
@ -273,6 +296,7 @@ namespace AnitomySharp
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
}
/// <summary>
@ -284,6 +308,7 @@ namespace AnitomySharp
{
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if(!Token.InListRange(prevToken, _parser.Tokens)||!Token.InListRange(nextToken, _parser.Tokens)) return false;
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
}
@ -296,6 +321,7 @@ namespace AnitomySharp
{
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
return KeywordManager.ContainsInPeekEntries(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
}
@ -432,4 +458,4 @@ namespace AnitomySharp
}
}
}
}
}

View File

@ -513,7 +513,8 @@ namespace AnitomySharp
return true;
}
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])";
// 全角数字:\uFF10-\uFF19
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([回集話话幕夜発縛])";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
@ -522,11 +523,33 @@ namespace AnitomySharp
{
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
}
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([期章巻卷])";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
var episodeNumber = match.Groups[2].Value;
if (!StringHelper.IsNumericString(episodeNumber))
{
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
}
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"(vol|EPISODE|ACT|scene|ep|volume|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
regexPattern = @"(EPISODE|ACT|scene|ep|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
var episodeNumber = match.Groups[3].Value;
SetEpisodeNumber(episodeNumber, token, false);
return true;
}
regexPattern = @"(vol|volume)([ \.\-_])([0-9]+)";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success)
{
@ -874,4 +897,4 @@ namespace AnitomySharp
return false;
}
}
}
}

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@ -63,7 +63,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
{
PreferredMetadataLanguage = "zh",
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb } }
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
};
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();

View File

@ -78,7 +78,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
[TestMethod]
public void TestGetMetadataByTMDB()
{
var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary<string, string> { { Plugin.ProviderId, MetaSource.Tmdb }, { MetadataProvider.Tmdb.ToString(), "945664" } } };
var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary<string, string> { { Plugin.ProviderId, MetaSource.Tmdb.ToString() }, { MetadataProvider.Tmdb.ToString(), "945664" } } };
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();

View File

@ -86,7 +86,6 @@ namespace Jellyfin.Plugin.MetaShark.Test
Assert.AreEqual(parseResult.Name, "秒速5厘米");
Assert.AreEqual(parseResult.Year, null);
// 标题加年份
fileName = "V字仇杀队 (2006)";
parseResult = NameParser.Parse(fileName);
@ -94,6 +93,12 @@ namespace Jellyfin.Plugin.MetaShark.Test
Assert.AreEqual(parseResult.Name, "V字仇杀队");
Assert.AreEqual(parseResult.Year, 2006);
fileName = "逃学威龙2 (1992)";
parseResult = NameParser.Parse(fileName);
Assert.AreEqual(parseResult.ChineseName, null);
Assert.AreEqual(parseResult.Name, "逃学威龙2");
Assert.AreEqual(parseResult.Year, 1992);
// anime
fileName = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3].mp4";
@ -162,50 +167,64 @@ namespace Jellyfin.Plugin.MetaShark.Test
[TestMethod]
public void TestEposideParse()
{
// 普通数字
var fileName = "03.mp4";
var parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "03");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 3);
fileName = "03 4K.mp4";
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "03");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 3);
// 混合中英文
var fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
var parseResult = NameParser.Parse(fileName);
fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.ChineseName, "新世界");
Assert.AreEqual(parseResult.Name, "New World");
Assert.AreEqual(parseResult.Year, 2013);
// 只英文S01E01
// 只英文 S01E01
fileName = "She-Hulk.Attorney.At.Law.S01E01.1080p.WEBRip.x265-RARBG";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "She-Hulk Attorney At Law");
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
Assert.AreEqual(parseResult.IndexNumber, 1);
// 只英文S01EP01
fileName = "Detective.Dee.S01EP18.2004.1080p.WEB-DL.x264.AAC-HQCS";
parseResult = NameParser.Parse(fileName);
Assert.AreEqual(parseResult.Name, "Detective Dee");
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
Assert.AreEqual(parseResult.IndexNumber, 18);
// 测试 SXXEPXX 格式
fileName = "神探狄仁杰2 Detective.Dee.Ⅱ.S02EP02.2006.2160p.WEB-DL.x264.AAC-HQC";
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.ChineseName, "神探狄仁杰2");
Assert.AreEqual(parseResult.Name, "Detective Dee Ⅱ");
Assert.AreEqual(parseResult.ParentIndexNumber, 2);
Assert.AreEqual(parseResult.IndexNumber, 2);
// 日文
fileName = "プロポーズ大作戦Ep05_x264.mp4";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "プロポーズ大作戦Ep05");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 5);
fileName = "[01] [ANK-Raws] あっちこっち 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC)";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "あっちこっち 01");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
// 只中文
fileName = "齊天大聖 第02集";
parseResult = NameParser.Parse(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖 第02集");
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
fileName = "齊天大聖 第 02 期";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "齊天大聖");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
@ -213,38 +232,40 @@ namespace Jellyfin.Plugin.MetaShark.Test
// anime
fileName = "[YYDM-11FANS][THERMAE_ROMAE][02][BDRIP][720P][X264-10bit_AAC][7FF2269F]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "THERMAE ROMAE");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 2);
// anime带季数
fileName = "[WMSUB][Detective Conan - Zeros Tea Time ][S01][E06][BIG5][1080P].mp4";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "Detective Conan - Zeros Tea Time");
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
Assert.AreEqual(parseResult.IndexNumber, 6);
fileName = "[KTXP][Machikado_Mazoku_S2][01][BIG5][1080p]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "Machikado Mazoku");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
fileName = "[異域字幕組][她和她的貓 - Everything Flows -][She and Her Cat - Everything Flows -][01][720p][繁體]";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.AreEqual(parseResult.Name, "她和她的貓 - Everything Flows");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, 1);
// anime特典
fileName = "[KissSub][Steins;Gate][SP][GB_BIG5_JP][BDrip][1080P][HEVC] 边界曲面的缺失之环";
parseResult = NameParser.Parse(fileName);
parseResult = NameParser.ParseEpisode(fileName);
Assert.IsTrue(parseResult.IsSpecial);
Assert.AreEqual(parseResult.Name, "边界曲面的缺失之环");
Assert.AreEqual(parseResult.ParentIndexNumber, null);
Assert.AreEqual(parseResult.IndexNumber, null);
}

View File

@ -64,7 +64,11 @@ namespace Jellyfin.Plugin.MetaShark.Test
var imdbApi = new ImdbApi(loggerFactory);
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/冰与火之歌S01-S08.Game.of.Thrones.1080p.Blu-ray.x265.10bit.AC3/冰与火之歌S2.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3");
Assert.AreEqual(result, 2);
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
Assert.AreEqual(result, 2);
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");

View File

@ -35,7 +35,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
{
PreferredMetadataLanguage = "zh",
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb } }
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
};
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();

View File

@ -30,7 +30,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
[TestMethod]
public void TestGetMetadata()
{
var info = new SeriesInfo() { Name = "一年一度喜剧大赛" };
var info = new SeriesInfo() { Name = "天下长河" };
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Test
@ -124,5 +125,27 @@ namespace Jellyfin.Plugin.MetaShark.Test
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestFindByExternalId()
{
var api = new TmdbApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.FindByExternalIdAsync("tt5924366", FindExternalSource.Imdb, "zh", CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
TestContext.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
}
}

View File

@ -52,22 +52,15 @@ namespace Jellyfin.Plugin.MetaShark.Api
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
Regex regNameMath = new Regex(@"(.+第\w季|[\w\uff1a\uff01\uff0c\u00b7]+)\s*(.*)", RegexOptions.Compiled);
Regex regRole = new Regex(@"\([饰|配]?\s*?(.+?)\)", RegexOptions.Compiled);
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
Regex regConstellation = new Regex(@"星座: \n(.+?)\n", RegexOptions.Compiled);
Regex regBirthdate = new Regex(@"出生日期: \n(.+?)\n", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"生卒日期: \n(.+?) 至 (.+)", RegexOptions.Compiled);
Regex regBirthplace = new Regex(@"出生地: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityRole = new Regex(@"职业: \n(.+?)\n", RegexOptions.Compiled);
Regex regNickname = new Regex(@"更多外文名: \n(.+?)\n", RegexOptions.Compiled);
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"(.+?) 至 (.+)", RegexOptions.Compiled);
Regex regHtmlTag = new Regex(@"<.?>", RegexOptions.Compiled);
Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
// 匹配除了换行符之外所有空白
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
Regex regPhotoId = new Regex(@"/photo/(\d+?)/", RegexOptions.Compiled);
Regex regLoginName = new Regex(@"<div[^>]*?db-usr-profile[^>]*?>[\w\W]*?<h1>([^>]*?)<", RegexOptions.Compiled);
// 默认200毫秒请求1次
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200));
@ -89,7 +82,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
var handler = new HttpClientHandlerEx();
this._cookieContainer = handler.CookieContainer;
httpClient = new HttpClient(handler);
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.Timeout = TimeSpan.FromSeconds(20);
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
@ -150,6 +143,18 @@ namespace Jellyfin.Plugin.MetaShark.Api
}
}
public async Task<List<DoubanSubject>> SearchMovieAsync(string keyword, CancellationToken cancellationToken)
{
var result = await this.SearchAsync(keyword, cancellationToken).ConfigureAwait(false);
return result.Where(x => x.Category == "电影").ToList();
}
public async Task<List<DoubanSubject>> SearchTVAsync(string keyword, CancellationToken cancellationToken)
{
var result = await this.SearchAsync(keyword, cancellationToken).ConfigureAwait(false);
return result.Where(x => x.Category == "电视剧").ToList();
}
public async Task<List<DoubanSubject>> SearchAsync(string keyword, CancellationToken cancellationToken)
{
var list = new List<DoubanSubject>();
@ -506,48 +511,64 @@ namespace Jellyfin.Plugin.MetaShark.Api
var contentNode = doc.QuerySelector("#content");
if (contentNode != null)
{
var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
var nameStr = contentNode.GetText("h1") ?? string.Empty;
var name = this.ParseCelebrityName(nameStr);
var englishName = nameStr.Replace(name, "").Trim();
celebrity.Img = contentNode.GetAttr("img.avatar", "src") ?? string.Empty;
var nameStr = contentNode.GetText("h1.subject-name") ?? string.Empty;
celebrity.Name = this.ParseCelebrityName(nameStr);
celebrity.EnglishName = nameStr.Replace(celebrity.Name, "").Trim();
var intro = contentNode.GetText("#intro span.all") ?? string.Empty;
if (string.IsNullOrEmpty(intro))
var family = string.Empty;
var propertyNodes = contentNode.QuerySelectorAll("ul.subject-property>li");
foreach (var li in propertyNodes)
{
intro = contentNode.GetText("#intro div.bd") ?? string.Empty;
}
var info = contentNode.GetText("div.info") ?? string.Empty;
var gender = info.GetMatchGroup(this.regGender);
var constellation = info.GetMatchGroup(this.regConstellation);
var birthdate = info.GetMatchGroup(this.regBirthdate);
// 生卒日期
var enddate = string.Empty;
var match = this.regLifedate.Match(info);
if (match.Success && match.Groups.Count > 2)
{
birthdate = match.Groups[1].Value.Trim();
enddate = match.Groups[2].Value.Trim();
var label = li.GetText("span.label") ?? string.Empty;
var value = li.GetText("span.value") ?? string.Empty;
switch (label)
{
case "性别:":
celebrity.Gender = value;
break;
case "星座:":
celebrity.Constellation = value;
break;
case "出生日期:":
celebrity.Birthdate = value;
break;
case "去世日期:":
celebrity.Enddate = value;
break;
case "生卒日期:":
var match = this.regLifedate.Match(value);
if (match.Success && match.Groups.Count > 2)
{
celebrity.Birthdate = match.Groups[1].Value.Trim();
celebrity.Enddate = match.Groups[2].Value.Trim();
}
break;
case "出生地:":
celebrity.Birthplace = value;
break;
case "职业:":
celebrity.Role = value;
break;
case "更多外文名:":
celebrity.NickName = value;
break;
case "家庭成员:":
family = value;
break;
case "IMDb编号:":
celebrity.Imdb = value;
break;
default:
break;
}
}
var birthplace = info.GetMatchGroup(this.regBirthplace);
var role = info.GetMatchGroup(this.regCelebrityRole);
var nickname = info.GetMatchGroup(this.regNickname);
var family = info.GetMatchGroup(this.regFamily);
var imdb = info.GetMatchGroup(this.regCelebrityImdb);
celebrity.Img = img;
celebrity.Gender = gender;
celebrity.Birthdate = birthdate;
celebrity.Enddate = enddate;
celebrity.NickName = nickname;
celebrity.EnglishName = englishName;
celebrity.Imdb = imdb;
celebrity.Birthplace = birthplace;
celebrity.Name = name;
// 保留段落关系,把段落替换为换行符
var intro = contentNode.GetHtml("section.subject-intro div.content") ?? string.Empty;
intro = regHtmlTag.Replace(intro.Replace("</p>", "\n"), "");
celebrity.Intro = formatOverview(intro);
celebrity.Constellation = constellation;
celebrity.Role = role;
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
return celebrity;
}
@ -802,6 +823,27 @@ namespace Jellyfin.Plugin.MetaShark.Api
return true;
}
public async Task<DoubanLoginInfo> GetLoginInfoAsync(CancellationToken cancellationToken)
{
var loginInfo = new DoubanLoginInfo();
try
{
var url = "https://www.douban.com/mine/";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
var requestUrl = response.RequestMessage?.RequestUri?.ToString();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var loginName = this.Match(body, regLoginName).Trim();
loginInfo.Name = loginName;
loginInfo.IsLogined = !(requestUrl == null || requestUrl.Contains("accounts.douban.com") || requestUrl.Contains("login") || requestUrl.Contains("sec.douban.com"));
}
catch (Exception ex)
{
this._logger.LogError(ex, "GetLoginInfoAsync error.");
}
return loginInfo;
}
protected async Task LimitRequestFrequently()
{
if (IsEnableAvoidRiskControl())

View File

@ -39,7 +39,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
var config = Plugin.Instance?.Configuration;
var apiKey = string.IsNullOrEmpty(config?.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
var host = string.IsNullOrEmpty(config?.TmdbHost) ? DEFAULT_API_HOST : config.TmdbHost;
_tmDbClient = new TMDbClient(apiKey, true, host, null, config.GetTmdbWebProxy());
_tmDbClient = new TMDbClient(apiKey, true, host, null, config?.GetTmdbWebProxy());
_tmDbClient.Timeout = TimeSpan.FromSeconds(10);
// Not really interested in NotFoundException
_tmDbClient.ThrowApiExceptions = false;
@ -597,6 +597,16 @@ namespace Jellyfin.Plugin.MetaShark.Api
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
}
public string? GetLogoUrl(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return null;
}
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.LogoSizes[^1], filePath).ToString();
}
/// <inheritdoc />
public void Dispose()
{

View File

@ -50,6 +50,12 @@ public class PluginConfiguration : BasePluginConfiguration
/// 启用tmdb获取背景图
/// </summary>
public bool EnableTmdbBackdrop { get; set; } = true;
/// <summary>
/// 启用tmdb获取商标
/// </summary>
public bool EnableTmdbLogo { get; set; } = true;
/// <summary>
/// 是否获取电影系列信息
/// </summary>

View File

@ -28,8 +28,8 @@
</legend>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
id="login_invalid"
style="color: red; margin-left: 8px; display: none;">(已失效)</span></label>
id="login_msg"
style="margin-left: 8px; display: none;"></span></label>
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
@ -98,6 +98,14 @@
</label>
<div class="fieldDescription">勾选后当影片在豆瓣找不到背景图时改使用TheMovieDb的补全</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label" for="EnableTmdbLogo">
<input id="EnableTmdbLogo" name="EnableTmdbLogo" type="checkbox"
is="emby-checkbox" />
<span>从TheMovieDb获取商标</span>
</label>
<div class="fieldDescription">勾选后使用TheMovieDb的商标图片补全</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label" for="EnableTmdbCollection">
<input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox"
@ -178,6 +186,7 @@
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop;
document.querySelector('#EnableTmdbLogo').checked = config.EnableTmdbLogo;
document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection;
document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating;
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
@ -207,6 +216,7 @@
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked;
config.EnableTmdbLogo = document.querySelector('#EnableTmdbLogo').checked;
config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked;
config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked;
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
@ -243,16 +253,16 @@
function checkDoubanLogin() {
let cookie = document.querySelector('#DoubanCookies').value
if (!cookie || !$.trim(cookie)) {
$('#login_invalid').hide();
$('#login_msg').hide();
return;
}
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
if (resp && resp.code != 1) {
$('#login_invalid').show();
$('#login_msg').css("color", "red").text('(已失效)').show();
} else {
$('#login_invalid').hide();
$('#login_msg').css("color", "").text('(已生效)').show();
}
})
}

View File

@ -77,10 +77,11 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
[HttpGet]
public async Task<ApiResult> CheckDoubanLogin()
{
var isLogin = await _doubanApi.CheckLoginAsync(CancellationToken.None);
return new ApiResult(isLogin ? 1 : 0, isLogin ? "logined" : "not login");
var loginInfo = await this._doubanApi.GetLoginInfoAsync(CancellationToken.None).ConfigureAwait(false);
return new ApiResult(loginInfo.IsLogined ? 1 : 0, loginInfo.Name);
}
private HttpClient GetHttpClient()
{
var client = _httpClientFactory.CreateClient(NamedClient.Default);

View File

@ -20,6 +20,17 @@ namespace Jellyfin.Plugin.MetaShark.Core
return null;
}
public static string? GetHtml(this IElement el, string css)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.Html().Trim();
}
return null;
}
public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
{
var node = el.QuerySelector(css);

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Naming.TV;
using Jellyfin.Plugin.MetaShark.Model;
namespace Jellyfin.Plugin.MetaShark.Core
@ -107,23 +108,32 @@ namespace Jellyfin.Plugin.MetaShark.Core
}
}
// 假如Anitomy解析不到year尝试使用jellyfin默认parser看能不能解析成功
// 假如 Anitomy 解析不到 year尝试使用 jellyfin 默认 parser看能不能解析成功
if (parseResult.Year == null && !isAnime)
{
var nativeParseResult = ParseMovie(fileName);
var nativeParseResult = ParseMovieByDefault(fileName);
if (nativeParseResult.Year != null)
{
parseResult = nativeParseResult;
}
}
// 假如 Anitomy 解析不到集数,判断 name 是否是数字集号
if (parseResult.IndexNumber is null && isEpisode)
{
if (!string.IsNullOrEmpty(parseResult.Name) && parseResult.Name.IsNumericString())
{
parseResult.IndexNumber = parseResult.Name.ToInt();
}
}
// 修复纯中文集数/特殊标识集数
if (parseResult.IndexNumber is null)
{
parseResult.IndexNumber = ParseChineseOrSpecialIndexNumber(fileName);
}
// 解析不到title时或解析出多个title时使用默认名
// 解析不到 title 时,或解析出多个 title 时,使用默认名
if (string.IsNullOrEmpty(parseResult.Name))
{
parseResult.Name = fileName;
@ -132,6 +142,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
return parseResult;
}
public static ParseNameResult ParseEpisode(string fileName)
{
return Parse(fileName, true);
}
private static string CleanName(string name)
{
// 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉
@ -143,8 +158,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
return name.Replace(".", " ").Trim();
}
// emby原始电影解析
public static ParseNameResult ParseMovie(string fileName)
/// <summary>
/// emby原始电影解析
/// </summary>
public static ParseNameResult ParseMovieByDefault(string fileName)
{
// 默认解析器会错误把分辨率当年份,先删除
fileName = resolutionReg.Replace(fileName, "");
@ -165,6 +182,18 @@ namespace Jellyfin.Plugin.MetaShark.Core
return parseResult;
}
/// <summary>
/// emby原始剧集解析
/// </summary>
public static EpisodePathParserResult ParseEpisodeByDefault(string fileName)
{
// EpisodePathParser需要路径信息 这里添加一个分隔符模拟路径
var path = Path.DirectorySeparatorChar + fileName;
var nameOptions = new Emby.Naming.Common.NamingOptions();
return new EpisodePathParser(nameOptions)
.Parse(path, false);
}
private static int ParseYear(string val)
{
@ -217,15 +246,21 @@ namespace Jellyfin.Plugin.MetaShark.Core
return null;
}
public static bool IsSpecialDirectory(string path)
public static bool IsSpecialDirectory(string path, bool isDirectory = false)
{
var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
return folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典");
if (isDirectory) {
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
}
return folder == "SP" || folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典");
}
public static bool IsExtraDirectory(string path)
public static bool IsExtraDirectory(string path, bool isDirectory = false)
{
var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
if (isDirectory) {
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
}
return folder == "EXTRA"
|| folder == "MENU"
|| folder == "MENUS"

View File

@ -78,5 +78,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
return string.Empty;
}
public static bool IsNumericString(this string str)
{
return str.All(char.IsDigit);
}
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release' or '$(Configuration)'=='Debug'">
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release'">
<PropertyGroup>
<DoILRepack>false</DoILRepack>
</PropertyGroup>

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -21,11 +21,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.1" />
<PackageReference Include="ILRepack.Lib.MSBuild.Minor" Version="2.1.19-alpha.2" />
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.32">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Jellyfin.Controller" Version="10.9.0" />
<PackageReference Include="Jellyfin.Model" Version="10.9.0" />
<PackageReference Include="RateLimiter" Version="2.2.0" />
<PackageReference Include="TMDbLib" Version="2.0.0" />
<PackageReference Include="TMDbLib" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.MetaShark.Model
{
public class DoubanLoginInfo
{
public string Name { get; set; }
public bool IsLogined { get; set; }
}
}

View File

@ -6,9 +6,34 @@ using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Model
{
public static class MetaSource
public enum MetaSource
{
public const string Douban = "douban";
public const string Tmdb = "tmdb";
Douban,
Tmdb,
None
}
public static class MetaSourceExtensions
{
public static MetaSource ToMetaSource(this string? str)
{
if (str == null)
{
return MetaSource.None;
}
if (str.ToLower().StartsWith("douban"))
{
return MetaSource.Douban;
}
if (str.ToLower().StartsWith("tmdb"))
{
return MetaSource.Tmdb;
}
return MetaSource.None;
}
}
}

View File

@ -19,6 +19,7 @@ using TMDbLib.Objects.General;
using Jellyfin.Plugin.MetaShark.Configuration;
using Jellyfin.Plugin.MetaShark.Core;
using Microsoft.AspNetCore.Http;
using MediaBrowser.Controller.Entities.TV;
namespace Jellyfin.Plugin.MetaShark.Providers
{
@ -50,6 +51,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled);
protected Regex regSeasonNameSuffix = new Regex(@"\s第[0-9一二三四五六七八九十]+?季$|\sSeason\s\d+?$|(?<![0-9a-zA-Z])\d$", RegexOptions.Compiled);
protected Regex regDoubanIdAttribute = new Regex(@"\[(?:douban|doubanid)-(\d+?)\]", RegexOptions.Compiled);
protected PluginConfiguration config
{
@ -107,11 +109,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
{
var fileName = GetOriginalFileName(info);
// 从文件名属性格式获取,如[douban-12345]或[doubanid-12345]
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
if (!string.IsNullOrWhiteSpace(doubanId))
{
this.Log($"Found douban [id] by attr: {doubanId}");
return doubanId;
}
var parseResult = NameParser.Parse(fileName);
var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name;
info.Year = parseResult.Year; // 默认parser对anime年份会解析出错以anitomy为准
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
List<DoubanSubject> result;
DoubanSubject? item;
@ -125,13 +133,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
if (item != null)
{
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
return item.Sid;
}
item = result.Where(x => x.Year == info.Year).FirstOrDefault();
if (item != null)
{
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
return item.Sid;
}
}
@ -169,7 +177,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
item = result.Where(x => x.Category == cat).FirstOrDefault();
if (item != null)
{
this.Log($"GuessByDouban found -> {item.Name}({item.Sid})");
this.Log($"Found douban [id] by first match: {item.Name}({item.Sid})");
return item.Sid;
}
@ -319,6 +327,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
break;
}
this.Log($"Not found tmdb id by [name]: {name} [year]: {year}");
return null;
}
@ -350,11 +359,24 @@ namespace Jellyfin.Plugin.MetaShark.Providers
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
return $"{tmdbId}";
}
if (findResult?.TvEpisode != null && findResult.TvEpisode.Count > 0)
{
var tmdbId = findResult.TvEpisode[0].ShowId;
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
return $"{tmdbId}";
}
if (findResult?.TvSeason != null && findResult.TvSeason.Count > 0)
{
var tmdbId = findResult.TvSeason[0].ShowId;
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
return $"{tmdbId}";
}
break;
default:
break;
}
this.Log($"Not found tmdb id by imdb id: {imdb}");
return null;
}
@ -380,10 +402,11 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public int? GuessSeasonNumberByDirectoryName(string path)
{
// TODO: 有时series name中会带有季信息
// 当没有season级目录时path为空直接返回
// TODO: 有时 series name 中会带有季信息
// 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber这时 path 为空,直接返回
if (string.IsNullOrEmpty(path))
{
this.Log($"Season path is empty!");
return null;
}
@ -394,6 +417,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null;
}
// 中文季名
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
var match = regSeason.Match(fileName);
if (match.Success && match.Groups.Count > 1)
@ -410,12 +434,26 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}
}
// SXX 季名
regSeason = new Regex(@"(?<![a-z])S(\d\d?)(?![0-9a-z])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
match = regSeason.Match(fileName);
if (match.Success && match.Groups.Count > 1)
{
var seasonNumber = match.Groups[1].Value.ToInt();
if (seasonNumber > 0)
{
this.Log($"Found season number of filename: {fileName} seasonNumber: {seasonNumber}");
return seasonNumber;
}
}
// 动漫季特殊命名
var seasonNameMap = new Dictionary<string, int>() {
{@"[ ._](I|1st|S01|S1)[ ._]", 1},
{@"[ ._](II|2nd|S02|S2)[ ._]", 2},
{@"[ ._](III|3rd|S03|S3)[ ._]", 3},
{@"[ ._](IIII|4th|S04|S4)[ ._]", 3},
{@"[ ._](I|1st)[ ._]", 1},
{@"[ ._](II|2nd)[ ._]", 2},
{@"[ ._](III|3rd)[ ._]", 3},
{@"[ ._](IIII|4th)[ ._]", 3},
};
foreach (var entry in seasonNameMap)
@ -585,7 +623,47 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}
}
protected string RemoveSeasonSubfix(string name)
protected string? GetOriginalSeasonPath(EpisodeInfo info)
{
if (info.Path == null) {
return null;
}
var seasonPath = Path.GetDirectoryName(info.Path);
var item = this._libraryManager.FindByPath(seasonPath, true);
// 没有季文件夹
if (item is Series) {
return null;
}
return seasonPath;
}
protected bool IsVirtualSeason(EpisodeInfo info)
{
if (info.Path == null)
{
return false;
}
var seasonPath = Path.GetDirectoryName(info.Path);
var parent = this._libraryManager.FindByPath(seasonPath, true);
// 没有季文件夹
if (parent is Series) {
return true;
}
var seriesPath = Path.GetDirectoryName(seasonPath);
var series = this._libraryManager.FindByPath(seriesPath, true);
// 季文件夹不规范,没法识别
if (series is Series && parent is not Season) {
return true;
}
return false;
}
protected string RemoveSeasonSuffix(string name)
{
return regSeasonNameSuffix.Replace(name, "");
}

View File

@ -44,9 +44,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
// 刷新元数据四种模式差别:
// 自动扫描匹配info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的假如命名不规范就会导致解析出错误值
// 识别info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds有指定选择项的ProvinceId
// 覆盖所有元数据info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的provinceIds保留所有旧值
// 搜索缺少的元数据info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取provinceIds保留所有旧值
// 覆盖所有元数据info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取provinceIds保留所有旧值
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} IsAutomated: {info.IsAutomated}");
var fileName = Path.GetFileName(info.Path);
this.Log($"GetEpisodeMetadata of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} EnableTmdb: {config.EnableTmdb}");
var result = new MetadataResult<Episode>();
// 动画特典和extras处理
@ -129,78 +130,72 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <summary>
/// 重新解析文件名
/// 注意:这里修改替换ParentIndexNumber值后会重新触发SeasonProvier的GetMetadata方法并带上最新的季数IndexNumber
/// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier GetMetadata 方法,并带上最新的季数 IndexNumber
/// </summary>
public EpisodeInfo FixParseInfo(EpisodeInfo info)
{
// 使用AnitomySharp进行重新解析解决anime识别错误
// 使用 AnitomySharp 进行重新解析,解决 anime 识别错误
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
var parseResult = NameParser.Parse(fileName);
var parseResult = NameParser.ParseEpisode(fileName);
info.Year = parseResult.Year;
info.Name = parseResult.ChineseName ?? parseResult.Name;
// 没有season级目录(即虚拟季)ParentIndexNumber默认是1季文件夹命名不规范时ParentIndexNumber默认是null
if (info.ParentIndexNumber is null)
// 文件名带有季数数据时,从文件名解析出季数进行修正
// 修正文件名有特殊命名 SXXEPXX 时,默认解析到错误季数的问题,如神探狄仁杰 Detective.Dee.S01EP01.2006.2160p.WEB-DL.x264.AAC-HQC
// TODO: 会导致覆盖用户手动修改元数据的季数
if (parseResult.ParentIndexNumber.HasValue && parseResult.ParentIndexNumber > 0 && info.ParentIndexNumber != parseResult.ParentIndexNumber)
{
this.Log("FixSeasonNumber by anitomy. old: {0} new: {1}", info.ParentIndexNumber, parseResult.ParentIndexNumber);
info.ParentIndexNumber = parseResult.ParentIndexNumber;
}
// 修正anime命名格式导致的seasonNumber错误从season元数据读取)
if (info.ParentIndexNumber is null)
// // 修正anime命名格式导致的seasonNumber错误从season元数据读取)
// if (info.ParentIndexNumber is null)
// {
// var episodeItem = this._libraryManager.FindByPath(info.Path, false);
// var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
// if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
// {
// info.ParentIndexNumber = season.IndexNumber;
// this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
// }
// }
// 从季文件夹名称猜出 season number
// 没有 season 级目录或部分特殊不规范命名会变成虚拟季ParentIndexNumber 默认设为 1
// https://github.com/jellyfin/jellyfin/blob/926470829d91d93b4c0b22c5b8b89a791abbb434/Emby.Server.Implementations/Library/LibraryManager.cs#L2626
var isVirtualSeason = this.IsVirtualSeason(info);
var seasonFolderPath = this.GetOriginalSeasonPath(info);
if (info.ParentIndexNumber is null or 1 && isVirtualSeason && seasonFolderPath != null)
{
var episodeItem = _libraryManager.FindByPath(info.Path, false);
var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
var guestSeasonNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
if (guestSeasonNumber.HasValue && guestSeasonNumber != info.ParentIndexNumber)
{
this.Log("FixSeasonNumber: old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
info.ParentIndexNumber = season.IndexNumber;
this.Log("FixSeasonNumber by season path. old: {0} new: {1}", info.ParentIndexNumber, guestSeasonNumber);
info.ParentIndexNumber = guestSeasonNumber;
}
// // 当没有season级目录时默认为1即当成只有一季不需要处理虚拟季jellyfin默认传的ParentIndexNumber=1
// if (info.ParentIndexNumber is null && season != null && season.LocationType == LocationType.Virtual)
// {
// this.Log("FixSeasonNumber: season is virtual, set to default 1");
// info.ParentIndexNumber = 1;
// }
}
// 从季文件夹名称猜出season number
var seasonFolderPath = Path.GetDirectoryName(info.Path);
if (info.ParentIndexNumber is null && seasonFolderPath != null)
{
info.ParentIndexNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
}
// 识别特典
if (info.ParentIndexNumber is null && NameParser.IsAnime(fileName) && (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path)))
{
this.Log("FixSeasonNumber to special. old: {0} new: 0", info.ParentIndexNumber);
info.ParentIndexNumber = 0;
}
// // 设为默认季数为1问题当同时存在S01和剧场版季文件夹时剧场版的影片会因为默认第一季而在S01也显示出来
// if (info.ParentIndexNumber is null)
// {
// this.Log("FixSeasonNumber: season number is null, set to default 1");
// info.ParentIndexNumber = 1;
// }
// 特典优先使用文件名特典除了前面特别设置还有SXX/Season XX等默认的
// 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的)
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
{
info.Name = parseResult.SpecialName == info.Name ? fileName : parseResult.SpecialName;
}
// 大于1000可能错误解析了分辨率
if (parseResult.IndexNumber.HasValue && parseResult.IndexNumber < 1000)
// 修正 episode number
if (parseResult.IndexNumber.HasValue && info.IndexNumber != parseResult.IndexNumber)
{
this.Log("FixEpisodeNumber by anitomy. old: {0} new: {1}", info.IndexNumber, parseResult.IndexNumber);
info.IndexNumber = parseResult.IndexNumber;
}
this.Log("FixParseInfo: fileName: {0} seasonNumber: {1} episodeNumber: {2} name: {3}", fileName, info.ParentIndexNumber, info.IndexNumber, info.Name);
return info;
}
@ -209,7 +204,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
// 特典或extra视频可能和正片剧集放在同一目录
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
var parseResult = NameParser.Parse(fileName);
var parseResult = NameParser.ParseEpisode(fileName);
if (parseResult.IsExtra)
{
this.Log($"Found anime extra of [name]: {fileName}");
@ -236,7 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return result;
}
//// 特典也有剧集信息,不在这里处理
//// 特典也有 tmdb 剧集信息,不在这里处理
// if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))
// {
// this.Log($"Found anime sp of [name]: {fileName}");

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public static class ProviderIdsExtensions
{
public static MetaSource GetMetaSource(this IHasProviderIds instance, string name)
{
var value = instance.GetProviderId(name);
return value.ToMetaSource();
}
public static void TryGetMetaSource(this Dictionary<string, string> dict, string name, out MetaSource metaSource)
{
if (dict.TryGetValue(name, out var value))
{
metaSource = value.ToMetaSource();
}
else
{
metaSource = MetaSource.None;
}
}
}
}

View File

@ -36,23 +36,25 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{
ImageType.Primary,
ImageType.Backdrop
ImageType.Backdrop,
ImageType.Logo,
};
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetProviderId(Plugin.ProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (primary == null || string.IsNullOrEmpty(primary.Img))
{
return Enumerable.Empty<RemoteImageInfo>();
}
var backdropImgs = await GetBackdrop(item, cancellationToken);
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false);
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
var res = new List<RemoteImageInfo> {
new RemoteImageInfo
@ -63,6 +65,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
},
};
res.AddRange(backdropImgs);
res.AddRange(logoImgs);
return res;
}
@ -70,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
{
var language = item.GetPreferredMetadataLanguage();
var movie = await _tmdbApi
// 设定language会导致图片被过滤这里设为null保持取全部语言图片
var movie = await this._tmdbApi
.GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
.ConfigureAwait(false);
@ -81,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var remoteImages = new List<RemoteImageInfo>();
for (var i = 0; i < movie.Images.Posters.Count; i++)
{
var poster = movie.Images.Posters[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
CommunityRating = poster.VoteAverage,
VoteCount = poster.VoteCount,
Width = poster.Width,
Height = poster.Height,
Language = AdjustImageLanguage(poster.Iso_639_1, language),
ProviderName = Name,
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
Type = ImageType.Primary,
});
}
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
{
var backdrop = movie.Images.Backdrops[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
CommunityRating = backdrop.VoteAverage,
VoteCount = backdrop.VoteCount,
Width = backdrop.Width,
Height = backdrop.Height,
ProviderName = Name,
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
Type = ImageType.Backdrop,
RatingType = RatingType.Score
});
}
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
Type = ImageType.Logo,
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
return remoteImages.OrderByLanguageDescending(language);
}
@ -133,7 +141,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
// 从豆瓣获取背景图
if (!string.IsNullOrEmpty(sid))
{
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken).ConfigureAwait(false);
if (photo != null && photo.Count > 0)
{
this.Log("GetBackdrop from douban sid: {0}", sid);
@ -187,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return list;
}
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
var list = new List<RemoteImageInfo>();
var language = item.GetPreferredMetadataLanguage();
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
{
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
var movie = await this._tmdbApi
.GetMovieAsync(tmdbId.ToInt(), language, language, cancellationToken)
.ConfigureAwait(false);
if (movie != null && movie.Images != null)
{
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
Type = ImageType.Logo,
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
}
}
return list.OrderByLanguageDescending(language);
}
}
}

View File

@ -7,6 +7,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Text;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
@ -42,13 +43,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}
// 从douban搜索
// BUG注意ProviderIds传多个meta值会导致识别搜索时只返回一个结果
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
var res = await this._doubanApi.SearchMovieAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
{
return new RemoteSearchResult
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
// 注意jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } },
ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year,
Name = x.Name,
@ -64,7 +66,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
return new RemoteSearchResult
{
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
// 注意jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) }, { Plugin.ProviderId, $"{MetaSource.Tmdb}_{x.Id}" } },
Name = string.Format("[TMDB]{0}", x.Title ?? x.OriginalTitle),
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview,
@ -80,18 +84,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
var fileName = this.GetOriginalFileName(info);
this.Log($"GetMovieMetadata of [name]: {info.Name} [fileName]: {fileName} IsAutomated: {info.IsAutomated}");
this.Log($"GetMovieMetadata of [name]: {info.Name} [fileName]: {fileName} EnableTmdb: {config.EnableTmdb}");
var result = new MetadataResult<Movie>();
// 使用刷新元数据时providerIds会保留旧有值只有识别/新增才会没值
var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetProviderId(Plugin.ProviderId);
// 用于修正识别时指定tmdb没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
{
metaSource = MetaSource.Tmdb;
}
var metaSource = info.GetMetaSource(Plugin.ProviderId);
// 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
@ -120,7 +119,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var movie = new Movie
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
@ -183,7 +183,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
@ -233,7 +233,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
movie.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
// 获取电影系列信息
if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null)
@ -301,7 +302,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
Name = actor.Name.Trim(),
Role = actor.Character,
Type = PersonType.Actor,
Type = PersonKind.Actor,
SortOrder = actor.Order,
};
@ -344,7 +345,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))

View File

@ -37,7 +37,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
var list = new List<RemoteImageInfo>();
var cid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetProviderId(Plugin.ProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (!string.IsNullOrEmpty(cid))
{

View File

@ -120,8 +120,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
{
this.Log($"GetPersonMetadata of found tmdb [id]: {findResult.PersonResults[0].Id}");
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.PersonResults[0].Id}");
var foundTmdbId = findResult.PersonResults.First().Id.ToString();
this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}");
item.SetProviderId(MetadataProvider.Tmdb, $"{foundTmdbId}");
}
}

View File

@ -44,7 +44,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
var season = (Season)item;
var series = season.Series;
var metaSource = series.GetProviderId(Plugin.ProviderId);
var metaSource = series.GetMetaSource(Plugin.ProviderId);
// get image from douban
var sid = item.GetProviderId(DoubanProviderId);

View File

@ -14,6 +14,8 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Jellyfin.Data.Enums;
using System.IO;
namespace Jellyfin.Plugin.MetaShark.Providers
{
@ -40,25 +42,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
var result = new MetadataResult<Season>();
// 使用刷新元数据时,之前识别的seasonNumber会保留不会被覆盖
// 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
var seasonSid = info.GetProviderId(DoubanProviderId);
var fileName = this.GetOriginalFileName(info);
this.Log($"GetSeasonMetaData of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} IsAutomated: {info.IsAutomated}");
var fileName = Path.GetFileName(info.Path);
this.Log($"GetSeasonMetaData of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} EnableTmdb: {config.EnableTmdb}");
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{
// 季文件夹名称不规范没法拿到seasonNumber尝试从文件夹名猜出
// 注意:本办法没法处理没有季文件夹的/虚拟季因为path会为空
// seasonNumber 为 null 有三种情况:
// 1. 没有季文件夹时即虚拟季info.Path 为空
// 2. 一般不规范文件夹命名,没法被 EpisodeResolver 解析的info.Path 不为空,如:摇曳露营△
// 3. 特殊不规范文件夹命名,能被 EpisodeResolver 错误解析这时被当成了视频文件相当于没有季文件夹info.Path 为空,如:冰与火之歌 S02.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3
// 相关代码https://github.com/jellyfin/jellyfin/blob/dc2eca9f2ca259b46c7b53f59251794903c730a4/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs#L70
if (seasonNumber is null)
{
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
}
// 搜索豆瓣季id
// 搜索豆瓣季 id
if (string.IsNullOrEmpty(seasonSid))
{
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
@ -89,19 +93,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
this.Log($"GetSeasonMetaData of douban [sid]: {seasonSid}");
this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}");
return result;
}
}
else
{
this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!");
this.Log($"Season [{info.Name}] not found douban season id!");
}
@ -139,10 +143,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null;
}
// 没有季文件夹即虚拟季info.Path会为空直接用series的sid
if (string.IsNullOrEmpty(info.Path))
// 没有季文件夹或季文件夹名不规范时即虚拟季info.Path 会为空seasonNumber 为 null
if (string.IsNullOrEmpty(info.Path) && !seasonNumber.HasValue)
{
return sid;
return null;
}
// 从季文件夹名属性格式获取,如 [douban-12345] 或 [doubanid-12345]
var fileName = this.GetOriginalFileName(info);
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
if (!string.IsNullOrWhiteSpace(doubanId))
{
this.Log($"Found season douban [id] by attr: {doubanId}");
return doubanId;
}
// 从sereis获取正确名称info.Name当是标准格式如S01等时会变成第x季非标准名称默认文件名
@ -151,7 +164,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
return null;
}
var seriesName = RemoveSeasonSubfix(series.Name);
var seriesName = this.RemoveSeasonSuffix(series.Name);
// 没有季id但存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据
var seasonYear = 0;

View File

@ -36,23 +36,25 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{
ImageType.Primary,
ImageType.Backdrop
ImageType.Backdrop,
ImageType.Logo,
};
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetProviderId(Plugin.ProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (primary == null || string.IsNullOrEmpty(primary.Img))
{
return Enumerable.Empty<RemoteImageInfo>();
}
var dropback = await GetBackdrop(item, cancellationToken);
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false);
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
var res = new List<RemoteImageInfo> {
new RemoteImageInfo
@ -62,7 +64,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
Type = ImageType.Primary,
},
};
res.AddRange(dropback);
res.AddRange(backdropImgs);
res.AddRange(logoImgs);
return res;
}
@ -70,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
{
var language = item.GetPreferredMetadataLanguage();
var movie = await _tmdbApi
// 设定language会导致图片被过滤这里设为null保持取全部语言图片
var movie = await this._tmdbApi
.GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
.ConfigureAwait(false);
@ -81,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var remoteImages = new List<RemoteImageInfo>();
for (var i = 0; i < movie.Images.Posters.Count; i++)
{
var poster = movie.Images.Posters[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
CommunityRating = poster.VoteAverage,
VoteCount = poster.VoteCount,
Width = poster.Width,
Height = poster.Height,
Language = AdjustImageLanguage(poster.Iso_639_1, language),
ProviderName = Name,
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
Type = ImageType.Primary,
});
}
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
{
var backdrop = movie.Images.Backdrops[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
CommunityRating = backdrop.VoteAverage,
VoteCount = backdrop.VoteCount,
Width = backdrop.Width,
Height = backdrop.Height,
ProviderName = Name,
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
Type = ImageType.Backdrop,
RatingType = RatingType.Score
});
}
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
Type = ImageType.Logo,
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
return remoteImages.OrderByLanguageDescending(language);
}
@ -177,8 +185,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
this.Log("GetBackdrop from tmdb id: {0}", tmdbId);
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Url = _tmdbApi.GetBackdropUrl(movie.BackdropPath),
ProviderName = this.Name,
Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath),
Type = ImageType.Backdrop,
});
}
@ -187,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return list;
}
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
var language = item.GetPreferredMetadataLanguage();
var list = new List<RemoteImageInfo>();
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
{
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
var movie = await this._tmdbApi
.GetSeriesAsync(tmdbId.ToInt(), language, language, cancellationToken)
.ConfigureAwait(false);
if (movie != null && movie.Images != null)
{
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
ProviderName = this.Name,
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
Type = ImageType.Logo,
CommunityRating = x.VoteAverage,
VoteCount = x.VoteCount,
Width = x.Width,
Height = x.Height,
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
RatingType = RatingType.Score,
}));
}
}
return list.OrderByLanguageDescending(language);
}
}
}

View File

@ -1,4 +1,5 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@ -41,12 +42,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}
// 从douban搜索
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
var res = await this._doubanApi.SearchTVAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
{
return new RemoteSearchResult
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } },
ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year,
Name = x.Name,
@ -61,7 +63,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
return new RemoteSearchResult
{
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) }, { Plugin.ProviderId, $"{MetaSource.Tmdb}_{x.Id}" } },
Name = string.Format("[TMDB]{0}", x.Name ?? x.OriginalName),
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview,
@ -82,12 +85,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetProviderId(Plugin.ProviderId);
// 用于修正识别时指定tmdb没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
{
metaSource = MetaSource.Tmdb;
}
var metaSource = info.GetMetaSource(Plugin.ProviderId);
// 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
@ -107,12 +105,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
var seriesName = RemoveSeasonSubfix(subject.Name);
var seriesName = RemoveSeasonSuffix(subject.Name);
var item = new Series
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } },
Name = seriesName,
OriginalTitle = RemoveSeasonSubfix(subject.OriginalName),
OriginalTitle = RemoveSeasonSuffix(subject.OriginalName),
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
@ -155,7 +153,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
@ -341,7 +339,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
}
series.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
series.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{seriesResult.Id}");
series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode);
@ -359,7 +357,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
Name = actor.Name.Trim(),
Role = actor.Character,
Type = PersonType.Actor,
Type = PersonKind.Actor,
SortOrder = actor.Order,
};
@ -403,7 +401,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))

View File

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Providers;
using Jellyfin.Plugin.MetaShark.Model;
namespace Jellyfin.Plugin.MetaShark.ScheduledTasks
{
public class FixMovieSimilarListTask : IScheduledTask
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
public string Key => $"{Plugin.PluginName}FixMovieSimilarList";
public string Name => "修复电影推荐列表";
public string Description => $"修复电影推荐列表只有一部影片的问题。";
public string Category => Plugin.PluginName;
/// <summary>
/// Initializes a new instance of the <see cref="FixMovieSimilarListTask"/> class.
/// </summary>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public FixMovieSimilarListTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager)
{
_logger = loggerFactory.CreateLogger<FixMovieSimilarListTask>();
_libraryManager = libraryManager;
}
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new List<TaskTriggerInfo>();
// yield return new TaskTriggerInfo
// {
// Type = TaskTriggerInfo.TriggerWeekly,
// DayOfWeek = DayOfWeek.Monday,
// TimeOfDayTicks = TimeSpan.FromHours(4).Ticks
// };
}
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
await Task.Yield();
progress?.Report(0);
// 只有电影有问题
var items = _libraryManager.GetItemList(new InternalItemsQuery
{
// MediaTypes = new[] { MediaType.Video },
HasAnyProviderId = new Dictionary<string, string>() { { Plugin.ProviderId, string.Empty } },
IncludeItemTypes = new[] { BaseItemKind.Movie }
}).ToList();
_logger.LogInformation("Fix movie similar list for {0} videos.", items.Count);
var successCount = 0;
var failCount = 0;
foreach (var (item, idx) in items.WithIndex())
{
cancellationToken.ThrowIfCancellationRequested();
progress?.Report((double)idx / items.Count * 100);
try
{
// 判断电影带有旧的元数据,进行替换处理
var sid = item.GetProviderId(BaseProvider.DoubanProviderId);
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
var providerVal = item.GetProviderId(Plugin.ProviderId);
if (providerVal == "douban" && !string.IsNullOrEmpty(sid))
{
var detail = this._libraryManager.GetItemById(item.Id);
detail.SetProviderId(Plugin.ProviderId, $"{MetaSource.Douban}_{sid}");
await detail.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
if (providerVal == "tmdb" && !string.IsNullOrEmpty(tmdbId))
{
var detail = this._libraryManager.GetItemById(item.Id);
detail.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
await detail.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
successCount++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Fix movie similar list for movie {0}: {1}", item.Name, ex.Message);
failCount++;
}
}
progress?.Report(100);
_logger.LogInformation("Exectue task completed. success: {0} fail: {1}", successCount, failCount);
}
}
}

View File

@ -1,18 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Providers;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MediaBrowser.Controller.Persistence;
using System.Net.Http;
namespace Jellyfin.Plugin.MetaShark
{
@ -20,21 +10,22 @@ namespace Jellyfin.Plugin.MetaShark
public class ServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection)
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
{
serviceCollection.AddSingleton<DoubanApi>((ctx) =>
serviceCollection.AddSingleton((ctx) =>
{
return new DoubanApi(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton<TmdbApi>((ctx) =>
serviceCollection.AddSingleton((ctx) =>
{
return new TmdbApi(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton<OmdbApi>((ctx) =>
serviceCollection.AddSingleton((ctx) =>
{
return new OmdbApi(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton<ImdbApi>((ctx) =>
serviceCollection.AddSingleton((ctx) =>
{
return new ImdbApi(ctx.GetRequiredService<ILoggerFactory>());
});

View File

@ -1,7 +1,7 @@
# jellyfin-plugin-metashark
[![release](https://img.shields.io/github/v/release/cxfksword/jellyfin-plugin-metashark)](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
[![platform](https://img.shields.io/badge/jellyfin-10.8.x-lightgrey?logo=jellyfin)](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
[![platform](https://img.shields.io/badge/jellyfin-10.8.x|10.9.x-lightgrey?logo=jellyfin)](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
[![license](https://img.shields.io/github/license/cxfksword/jellyfin-plugin-metashark)](https://github.com/cxfksword/jellyfin-plugin-metashark/main/LICENSE)
jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMovieDb补全缺失的剧集数据。
@ -14,11 +14,9 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
## 安装插件
只支持最新的`jellyfin 10.8.x`版本
添加插件存储库:
国内加速https://mirror.ghproxy.com/https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json
国内加速https://gitee.com/cwhzy/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json
国外访问https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json
@ -42,13 +40,13 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
1. Clone or download this repository
2. Ensure you have .NET Core SDK 6.0 setup and installed
2. Ensure you have .NET Core SDK 8.0 setup and installed
3. Build plugin with following command.
```sh
dotnet restore
dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
```
@ -56,7 +54,7 @@ dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
1. Build the plugin
2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Debug/net6.0/Jellyfin.Plugin.MetaShark.dll` into it
2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll` into it
3. Move folder `metashark` to jellyfin `data/plugins` folder
@ -70,4 +68,10 @@ dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
## Thanks
[AnitomySharp](https://github.com/chu-shen/AnitomySharp)
[AnitomySharp](https://github.com/chu-shen/AnitomySharp)
## 免责声明
本项目代码仅用于学习交流编程技术,下载后请勿用于商业用途。
如果本项目存在侵犯您的合法权益的情况,请及时与开发者联系,开发者将会及时删除有关内容。

View File

@ -26,7 +26,7 @@ def generate_version(filepath, version, changelog):
return {
'version': f"{version}.0",
'changelog': changelog,
'targetAbi': '10.8.0.0',
'targetAbi': '10.9.0.0',
'sourceUrl': f'https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/v{version}/metashark_{version}.0.zip',
'checksum': md5sum(filepath),
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S')