Compare commits

..

9 Commits
main ... ci

Author SHA1 Message Date
cxfksword 4cf5b24dfe ci: update github action 2023-12-02 16:30:23 +08:00
cxfksword fe717ef781 ci: update github action 2023-12-02 16:17:07 +08:00
cxfksword 209c46d620 ci: update github action 2023-12-02 16:12:21 +08:00
cxfksword a1fb0e9a57 ci: update github action 2023-12-02 16:08:21 +08:00
cxfksword 1528aa44b9 ci: update github action 2023-12-02 16:05:30 +08:00
cxfksword 072779f172 ci: update github action 2023-12-02 15:40:45 +08:00
cxfksword 7d6a172386 ci: update github action 2023-12-02 15:39:21 +08:00
cxfksword b24ac31a37 ci: update github action 2023-12-02 15:37:09 +08:00
cxfksword 5da4e53957 ci: update github action 2023-12-02 15:33:55 +08:00
47 changed files with 459 additions and 1205 deletions

View File

@ -1,28 +0,0 @@
---
name: 刮削失败相关问题
about: 报告刮削失败相关问题.
title: "[刮削]"
labels: ''
assignees: ''
---
**描述错误**
对错误是什么的清晰简明描述。
**屏幕截图**
请添加问题截图以帮助解释您的问题。
**日志**
请提供jellyfin打印的该影片的刮削日志。
日志查看方法: 控制台->高级->日志->点击log_yyyymmdd.log格式文件
**运行环境(请填写以下信息):**
- 操作系统:[例如 linux]
- jellyfin 版本:[例如 10.8.9]
- 插件版本:[例如 1.7.1]

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
dotnet-version: 8.0.x dotnet-version: 6.0.x
python-version: 3.8 python-version: 3.8
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
artifact: metashark artifact: metashark
@ -31,14 +31,14 @@ jobs:
run: | run: |
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//) VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
VERSION="$VERSION.0" VERSION="$VERSION.0"
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT echo ::set-output name=VERSION::${VERSION}
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
- name: Build - name: Build
run: | run: |
dotnet restore ${{ env.project }} --no-cache dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 ${{ env.project }} dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 ${{ env.project }}
mkdir -p artifacts mkdir -p artifacts
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/ cp ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

View File

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

View File

@ -5,7 +5,7 @@ on:
tags: ["*"] tags: ["*"]
env: env:
dotnet-version: 8.0.x dotnet-version: 6.0.x
python-version: 3.8 python-version: 3.8
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
artifact: metashark artifact: metashark
@ -36,32 +36,37 @@ jobs:
run: | run: |
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//) VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
VERSION="$VERSION.0" VERSION="$VERSION.0"
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT echo ::set-output name=VERSION::${VERSION}
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
- name: Build - name: Build
run: | run: |
dotnet restore ${{ env.project }} --no-cache dotnet restore ${{ env.project }} --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }} dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
mkdir -p artifacts mkdir -p artifacts
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll
cp ./doc/logo.png ./artifacts/logo.png
- name: Generate manifest - name: Generate manifest
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/} run: cd artifacts && python3 ../scripts/generate_manifest.py ${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
env: - name: Deploy to jellyfin release repo
CN_DOMAIN: ${{ vars.CN_DOMAIN }} uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.PAT }}
external_repository: cxfksword/jellyfin-release
destination_dir: ${{ env.artifact }}
publish_branch: master
publish_dir: ./artifacts
- name: Publish release - name: Publish release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/${{ env.artifact }}_*.zip file: ./artifacts/${{ env.artifact }}_*.zip
tag: ${{ github.ref }} tag: ${{ github.ref }}
release_name: '${{ github.ref_name }}: Jellyfin v10.9'
file_glob: true
overwrite: true
- name: Publish manifest
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./manifest*.json
tag: "manifest"
overwrite: true
file_glob: true file_glob: true
# - name: Publish manifest
# uses: svenstaro/upload-release-action@v2
# with:
# repo_token: ${{ secrets.GITHUB_TOKEN }}
# file: ./artifacts/manifest*.json
# tag: "manifest"
# overwrite: true
# file_glob: true

53
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,53 @@
name: "Test"
on:
push:
env:
dotnet-version: 6.0.x
python-version: 3.8
project: Jellyfin.Plugin.MetaShark
artifact: metashark
jobs:
build:
runs-on: ubuntu-latest
name: Build & Release
steps:
- uses: actions/checkout@v3
- name: Get tags (For CHANGELOG)
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Setup dotnet
uses: actions/setup-dotnet@v3
id: dotnet
with:
dotnet-version: ${{ env.dotnet-version }}
- name: Change default dotnet version
run: |
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
- name: Initialize workflow variables
id: vars
run: |
VERSION=$(echo "v1.7.4" | sed s/^v//)
VERSION="$VERSION.0"
echo ::set-output name=VERSION::${VERSION}
echo ::set-output name=APP_NAME::$(echo "${{ env.artifact }}_${VERSION}")
- name: Build
run: |
dotnet restore ${{ env.project }}/${{ env.project }}.csproj --no-cache
dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
mkdir -p artifacts
zip -j ./artifacts/${{steps.vars.outputs.APP_NAME}}.zip ./${{ env.project }}/bin/Release/net6.0/${{ env.project }}.dll
cp ./doc/logo.png ./artifacts/logo.png
- name: Generate manifest
run: cd artifacts && python3 ../scripts/generate_manifest.py ${{steps.vars.outputs.APP_NAME}}.zip v1.7.4
- name: Deploy to jellyfin release repo
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.PAT }}
external_repository: cxfksword/jellyfin-release
destination_dir: test
publish_branch: master
publish_dir: ./artifacts
keep_files: true

View File

@ -1,28 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>AnitomySharp.NET6</PackageId> <PackageId>AnitomySharp.NET6</PackageId>
<PackageVersion>0.4.0</PackageVersion> <Authors>tabratton;senritsu</Authors>
<Version>0.4.0</Version>
<Authors>tabratton;senritsu;chu-shen</Authors>
<Description>AnitomySharp is a C# port of Anitomy by erengy, a library for parsing anime video filenames. All credit to erengy for the actual library and logic. <Description>AnitomySharp is a C# port of Anitomy by erengy, a library for parsing anime video filenames. All credit to erengy for the actual library and logic.
This fork of AnitomySharp is inspired by tabratton and senritsu, which adds more custom rules.
</Description> </Description>
<RepositoryUrl>https://github.com/chu-shen/AnitomySharp.git</RepositoryUrl> <RepositoryUrl>https://github.com/chu-shen/AnitomySharp.git</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>Anitomy Anime</PackageTags>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<DocumentationFile>AnitomySharp.xml</DocumentationFile> <AssemblyVersion>0.3.0</AssemblyVersion>
<FileVersion>0.3.0</FileVersion>
<Version>0.3.0</Version>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<DocumentationFile>AnitomySharp.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\LICENSE" Pack="true" Visible="false" PackagePath="" /> <None Include="..\LICENSE" Pack="true" Visible="false" PackagePath="" />
<None Include="..\README.md" Pack="true" PackagePath=""/> <PackageReference Include="Microsoft.DocAsCode.App" Version="2.60.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,13 +24,13 @@ namespace AnitomySharp
public static class KeywordManager public static class KeywordManager
{ {
/// <summary> /// <summary>
/// 包含所有关键词的内部关键词元素词典,比较器忽略大小写 /// 包含所有关键词(大写)的内部关键词元素词典
/// </summary> /// </summary>
private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>();
/// <summary> /// <summary>
/// 文件扩展名,无值,比较器忽略大小写 /// 文件扩展名,无值
/// </summary> /// </summary>
private static readonly Dictionary<string, Keyword> Extensions = new Dictionary<string, Keyword>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, Keyword> Extensions = new Dictionary<string, Keyword>();
/// <summary> /// <summary>
/// ~~一眼真~~ /// ~~一眼真~~
@ -64,19 +64,18 @@ namespace AnitomySharp
"GEKIJOUBAN", "MOVIE", "GEKIJOUBAN", "MOVIE",
"OAD", "OAV", "ONA", "OVA", "OAD", "OAV", "ONA", "OVA",
"TV", "TV",
"番外編", "總集編","DRAMA", "番外編", "總集編","映像特典","特典","特典アニメ",
"映像特典","特典","特典アニメ",
// 特典 Special 剩下的各种类型可以全部命名成 SP对于较特殊意义的特典也可以自定义命名 // 特典 Special 剩下的各种类型可以全部命名成 SP对于较特殊意义的特典也可以自定义命名
"SPECIAL", "SPECIALS", "SP", "SPs", "特報", "SPECIAL", "SPECIALS", "SP",
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。 // 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
"IV", "IV",
// 音乐视频 Music Video // 音乐视频 Music Video
"MV"}); "MV"});
// add "SP" to ElementAnimeType with optionsUnidentifiable // add "SP" to ElementAnimeType with optionsUnidentifiable
// Add(Element.ElementCategory.ElementAnimeType, // Add(Element.ElementCategory.ElementAnimeType,
// optionsUnidentifiableUnsearchable, // optionsUnidentifiableUnsearchable,
// new List<string> { "SP" }); // e.g. "Yumeiro Patissiere SP Professional", but it is widely used to represent special // new List<string> {"SP"}); // e.g. "Yumeiro Patissiere SP Professional"
Add(Element.ElementCategory.ElementAnimeType, Add(Element.ElementCategory.ElementAnimeType,
optionsUnidentifiableInvalid, optionsUnidentifiableInvalid,
@ -85,7 +84,7 @@ namespace AnitomySharp
// 无字 OP/ED Non-Credit Opening/Ending // 无字 OP/ED Non-Credit Opening/Ending
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING", "ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面 // 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
"PREVIEW", "YOKOKU", "予告", "PREVIEW",
// 菜单 Menu BD/DVD 播放选择菜单 // 菜单 Menu BD/DVD 播放选择菜单
"MENU", "MENU",
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右 // 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右
@ -93,7 +92,7 @@ namespace AnitomySharp
// 语音信息 // 语音信息
"MESSAGE", "MESSAGE",
// 宣传片/预告片 Promotion Video / Trailer 一般时长在 1~2min 命名参考原盘和 jsum // 宣传片/预告片 Promotion Video / Trailer 一般时长在 1~2min 命名参考原盘和 jsum
"PV", "Teaser","TRAILER", "PV", "Teaser","TRAILER", "DRAMA",
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。 // 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
"INTERVIEW", "INTERVIEW",
"EVENT", "TOKUTEN", "LOGO"}); "EVENT", "TOKUTEN", "LOGO"});
@ -151,7 +150,7 @@ namespace AnitomySharp
Add(Element.ElementCategory.ElementOther, Add(Element.ElementCategory.ElementOther,
optionsDefault, optionsDefault,
new List<string> { "REMASTER", "REMASTERED", "UNCUT", "TS", "VFR", "WIDESCREEN", "WS", "SPURSENGINE","DISC" }); new List<string> { "REMASTER", "REMASTERED", "UNCUT", "TS", "VFR", "WIDESCREEN", "WS", "SPURSENGINE" });
Add(Element.ElementCategory.ElementReleaseGroup, Add(Element.ElementCategory.ElementReleaseGroup,
optionsDefault, optionsDefault,
@ -282,16 +281,6 @@ namespace AnitomySharp
return false; return false;
} }
/// <summary>
/// 判断预处理元素列表中是否包含给定的字符串(<paramref name="keyword"/>)
/// </summary>
/// <param name="category">元素类别</param>
/// <param name="keyword">待判断的字符串</param>
/// <returns>`true`表示包含</returns>
public static bool ContainsInPeekEntries(Element.ElementCategory category, string keyword)
{
return PeekEntries.Any(entry => entry.Item1 == category && entry.Item2.Contains(keyword, StringComparer.OrdinalIgnoreCase));
}
/// <summary> /// <summary>
/// Finds a particular <c>keyword</c>. If found sets <c>category</c> and <c>options</c> to the found search result. /// Finds a particular <c>keyword</c>. If found sets <c>category</c> and <c>options</c> to the found search result.

View File

@ -179,7 +179,6 @@ namespace AnitomySharp
private void SearchForEpisodeNumber() private void SearchForEpisodeNumber()
{ {
var tokens = new List<int>(); var tokens = new List<int>();
var allTokens = new List<int>();
for (var i = 0; i < Tokens.Count; i++) for (var i = 0; i < Tokens.Count; i++)
{ {
var token = Tokens[i]; var token = Tokens[i];
@ -188,7 +187,6 @@ namespace AnitomySharp
ParserHelper.IndexOfFirstDigit(token.Content) != -1) ParserHelper.IndexOfFirstDigit(token.Content) != -1)
{ {
tokens.Add(i); tokens.Add(i);
allTokens.Add(i);
} }
} }
@ -230,12 +228,6 @@ namespace AnitomySharp
// "e.g. "[12]", "(2006)" // "e.g. "[12]", "(2006)"
if (ParseNumber.SearchForIsolatedNumbers(tokens)) return; if (ParseNumber.SearchForIsolatedNumbers(tokens)) return;
// e.g. "OVA 3", "OtherToken[Hint05]", "[Web Preview 06]": maybe incorrect, so put the last
if (ParseNumber.SearchForSymbolWithEpisode(allTokens)) return;
// e.g. [13(341)], [13 (341)]
if (ParseNumber.SearchForEquivalentNumbersWithBracket(allTokens)) return;
// Consider using the last number as a last resort // Consider using the last number as a last resort
ParseNumber.SearchForLastNumber(tokens); ParseNumber.SearchForLastNumber(tokens);
} }
@ -243,7 +235,7 @@ namespace AnitomySharp
/// <summary> /// <summary>
/// Search for anime title /// Search for anime title
/// ///
/// 搜索动画名 /// 搜索动画名
/// </summary> /// </summary>
private void SearchForAnimeTitle() private void SearchForAnimeTitle()
{ {
@ -291,13 +283,6 @@ namespace AnitomySharp
{ {
tokenBegin = tokenBeginWithNoReleaseGroup; tokenBegin = tokenBeginWithNoReleaseGroup;
} }
// 去除纯数字标题
// skip token with only number
if (Regex.Match(Tokens[tokenBegin].Content, ParserNumber.RegexMatchOnlyStart + @"^[0-9]+$" + ParserNumber.RegexMatchOnlyEnd).Success)
{
tokenBegin = tokenBeginWithNoReleaseGroup;
}
skippedPreviousGroup = true; skippedPreviousGroup = true;
} while (Token.InListRange(tokenBegin, Tokens)); } while (Token.InListRange(tokenBegin, Tokens));
} }
@ -413,7 +398,7 @@ namespace AnitomySharp
{ {
var token = Tokens[i]; var token = Tokens[i];
/** 跳过括号标记类型的标记 */ /** 跳过括号标记类型的标记 */
if (token.Category != Token.TokenCategory.Unknown) continue; if (token.Category == Token.TokenCategory.Bracket) continue;
var tokenContent = token.Content; var tokenContent = token.Content;
// e.g. "2016-17" // e.g. "2016-17"
@ -423,21 +408,13 @@ namespace AnitomySharp
{ {
tokenContent = tokenContent.Split(match.Groups[2].Value)[0]; tokenContent = tokenContent.Split(match.Groups[2].Value)[0];
} }
// add newtype e.g. "2021 OVA"
if (!StringHelper.IsNumericString(tokenContent)) if (token.Category != Token.TokenCategory.Unknown || !StringHelper.IsNumericString(tokenContent) ||
!(ParseHelper.IsTokenContainAnimeType(i) ^ ParseHelper.IsTokenIsolated(i)))
{ {
continue; continue;
} }
// e.g. "[2021 OVA]"
if(ParseHelper.IsNextTokenContainAnimeType(i)&&!ParseHelper.IsTokenIsolated(i)){}
// TODO may not be necessary
// if (!ParseHelper.IsTokenIsolated(i))
// {
// continue;
// }
var number = StringHelper.StringToInt(tokenContent); var number = StringHelper.StringToInt(tokenContent);
// Anime year // Anime year
@ -445,7 +422,7 @@ namespace AnitomySharp
{ {
if (Empty(Element.ElementCategory.ElementAnimeYear)) if (Empty(Element.ElementCategory.ElementAnimeYear))
{ {
Elements.Add(new Element(Element.ElementCategory.ElementAnimeYear, tokenContent)); Elements.Add(new Element(Element.ElementCategory.ElementAnimeYear, token.Content));
token.Category = Token.TokenCategory.Identifier; token.Category = Token.TokenCategory.Identifier;
continue; continue;
} }

View File

@ -119,29 +119,6 @@ namespace AnitomySharp
if (string.IsNullOrEmpty(str)) return ""; if (string.IsNullOrEmpty(str)) return "";
return Ordinals.TryGetValue(str, out var foundString) ? foundString : ""; 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> /// <summary>
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise. /// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
@ -258,7 +235,7 @@ namespace AnitomySharp
/// <summary> /// <summary>
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces). /// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces).
/// ///
/// 判断当前位置标记(token)是否孤立,是否被括号包裹 /// 判断当前位置标记(token)是否孤立,是否被括号包裹
/// </summary> /// </summary>
/// <param name="pos"></param> /// <param name="pos"></param>
/// <returns></returns> /// <returns></returns>
@ -269,20 +246,6 @@ namespace AnitomySharp
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter); var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket); return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
} }
/// <summary>
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces, delimiter).
///
/// 判断当前位置标记(token)是否孤立,前面是否为分隔符,后面是否为括号包裹
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public bool IsTokenIsolatedWithDelimiterAndBracket(int pos)
{
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNone);
if (!IsTokenCategory(prevToken, Token.TokenCategory.Delimiter)) return false;
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
}
/// <summary> /// <summary>
/// Returns whether or not a token at the current <c>pos+1</c> is ElementAnimeType. /// Returns whether or not a token at the current <c>pos+1</c> is ElementAnimeType.
@ -291,40 +254,13 @@ namespace AnitomySharp
/// </summary> /// </summary>
/// <param name="pos"></param> /// <param name="pos"></param>
/// <returns></returns> /// <returns></returns>
public bool IsNextTokenContainAnimeType(int pos) public bool IsTokenContainAnimeType(int pos)
{ {
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter); var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false; if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
var nextToken = Token.FindNextToken(_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;
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content); return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
} }
/// <summary>
/// 判断当前标记(token)的上一个标记的类型是否为ElementAnimeType。如果是则返回`true`
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public bool IsPrevTokenContainAnimeType(int pos)
{
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);
}
/// <summary>
/// 判断当前标记(token)的上一个标记的类型是否为ElementAnimeType在 PeekEntries 中)。如果是,则返回`true`
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public bool IsPrevTokenContainAnimeTypeInPeekEntries(int pos)
{
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);
}
/// <summary> /// <summary>
/// Finds and sets the anime season keyword. /// Finds and sets the anime season keyword.
@ -458,4 +394,4 @@ namespace AnitomySharp
} }
} }
} }
} }

View File

@ -371,7 +371,7 @@ namespace AnitomySharp
/// <returns>true if the token matched</returns> /// <returns>true if the token matched</returns>
private bool MatchSeasonAndEpisodePattern(string word, Token token) private bool MatchSeasonAndEpisodePattern(string word, Token token)
{ {
const string regexPattern = RegexMatchOnlyStart + @"S?(\d{1,2})(?:-S?(\d{1,2}))?(?:x|[ ._-x]?EP?)(\d{1,4})(?:-E?P?(\d{1,4}))?(?:[vV](\d{1,2}))?" + RegexMatchOnlyEnd; const string regexPattern = RegexMatchOnlyStart + @"S?(\d{1,2})(?:-S?(\d{1,2}))?(?:x|[ ._-x]?E)(\d{1,4})(?:-E?(\d{1,4}))?(?:[vV](\d{1,2}))?" + RegexMatchOnlyEnd;
var match = Regex.Match(word, regexPattern); var match = Regex.Match(word, regexPattern);
if (!match.Success) return false; if (!match.Success) return false;
@ -412,7 +412,7 @@ namespace AnitomySharp
_parser.Tokens.Insert(foundIdx, _parser.Tokens.Insert(foundIdx,
new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix)); new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix));
return true; return true;
} }
@ -513,8 +513,7 @@ namespace AnitomySharp
return true; return true;
} }
// 全角数字:\uFF10-\uFF19 regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])";
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([回集話话幕夜発縛])";
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase); match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success) if (match.Success)
{ {
@ -523,33 +522,11 @@ namespace AnitomySharp
{ {
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber); 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); SetEpisodeNumber(episodeNumber, token, false);
return true; return true;
} }
regexPattern = @"(EPISODE|ACT|scene|ep|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)"; regexPattern = @"(vol|EPISODE|ACT|scene|ep|volume|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); match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
if (match.Success) if (match.Success)
{ {
@ -721,50 +698,6 @@ namespace AnitomySharp
return false; return false;
} }
/// <summary>
/// 搜索同动画类型同时出现的集数
/// </summary>
/// <param name="tokens"></param>
/// <returns></returns>
public bool SearchForSymbolWithEpisode(List<int> tokens)
{
// Match from back to front
for (int i = tokens.Count - 1; i >= 0; i--)
{
var it = tokens[i];
// e.g. OVA 3, [Web Preview 06]: Web Preview in PeekEntries
if ((_parser.ParseHelper.IsPrevTokenContainAnimeType(it) || _parser.ParseHelper.IsPrevTokenContainAnimeTypeInPeekEntries(it)) && !_parser.ParseHelper.IsTokenIsolated(it))
{
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], false);
return true;
}
// e.g. OtherToken[Hint05]
// it>1: makesure this token is not first one
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it))
{
var tokenContent = _parser.Tokens[it].Content;
var numberBegin = ParserHelper.IndexOfFirstDigit(tokenContent);
var prefix = StringHelper.SubstringWithCheck(tokenContent, 0, numberBegin);
var number = StringHelper.SubstringWithCheck(tokenContent, numberBegin, tokenContent.Length - numberBegin);
// token should be: alphaNumeric
if (prefix != "" && StringHelper.IsAlphaString(prefix) && StringHelper.IsNumericString(number))
{
SetEpisodeNumber(number, _parser.Tokens[it], true);
return true;
}
}
// e.g. OtherToken[Disc 01]
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(it) && StringHelper.IsNumericString(_parser.Tokens[it].Content))
{
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true);
return true;
}
}
return false;
}
/// <summary> /// <summary>
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114) /// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
/// ///
@ -797,7 +730,10 @@ namespace AnitomySharp
continue; continue;
} }
var list = new List<Token> { _parser.Tokens[it], _parser.Tokens[nextToken] }; var list = new List<Token>
{
_parser.Tokens[it], _parser.Tokens[nextToken]
};
list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content)); list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content));
SetEpisodeNumber(list[0].Content, list[0], false); SetEpisodeNumber(list[0].Content, list[0], false);
@ -807,50 +743,6 @@ namespace AnitomySharp
return false; return false;
} }
/// <summary>
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
///
/// 匹配自带等效集数的数字,常见于分割放送,匹配括号包裹的数字
/// </summary>
/// <param name="tokens">the list of tokens</param>
/// <returns>true if an equivalent number was found</returns>
public bool SearchForEquivalentNumbersWithBracket(List<int> tokens)
{
foreach (var it in tokens)
{
// Find the first enclosed, non-delimiter token
var nextToken = Token.FindNextToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter);
if (!Token.InListRange(nextToken, _parser.Tokens) || !(_parser.Tokens[it].Content.Contains("(") || _parser.Tokens[nextToken].Content.Contains(")")))
{
continue;
}
// e.g. [13(341)]
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it))
{
string[] episodes = _parser.Tokens[it].Content.Split(new string[] { "(", ")" }, StringSplitOptions.RemoveEmptyEntries);
if (StringHelper.IsNumericString(episodes[0]) && StringHelper.IsNumericString(episodes[1]))
{
SetEpisodeNumber(episodes[0], _parser.Tokens[it], false);
SetAlternativeEpisodeNumber(episodes[1], _parser.Tokens[it]);
return true;
}
}
// e.g. [13 (341)]
if (it > 1 && _parser.Tokens[nextToken].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(nextToken))
{
string episode = _parser.Tokens[nextToken].Content.Replace("(", "").Replace(")", "");
if (StringHelper.IsNumericString(_parser.Tokens[it].Content) && StringHelper.IsNumericString(episode))
{
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true);
SetAlternativeEpisodeNumber(episode, _parser.Tokens[nextToken]);
return true;
}
}
}
return false;
}
/// <summary> /// <summary>
/// Searches for the last number token in a list of <c>tokens</c> /// Searches for the last number token in a list of <c>tokens</c>
@ -897,4 +789,4 @@ namespace AnitomySharp
return false; return false;
} }
} }
} }

View File

@ -120,17 +120,6 @@ namespace AnitomySharp
{ {
return str.All(char.IsDigit); return str.All(char.IsDigit);
} }
/// <summary>
/// Returns whether or not the <c>str</c> is a alpha string.
///
/// 判断字符串是否全字母
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool IsAlphaString(string str)
{
return str.All(char.IsLetter);
}
/// <summary> /// <summary>
/// Returns the int value of the <c>str</c>; 0 otherwise. /// Returns the int value of the <c>str</c>; 0 otherwise.

View File

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

View File

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

View File

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

View File

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

View File

@ -64,11 +64,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
var imdbApi = new ImdbApi(loggerFactory); var imdbApi = new ImdbApi(loggerFactory);
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi); 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); Assert.AreEqual(result, 2);
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第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() var info = new MediaBrowser.Controller.Entities.Movies.Movie()
{ {
PreferredMetadataLanguage = "zh", PreferredMetadataLanguage = "zh",
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } } ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb } }
}; };
var httpClientFactory = new DefaultHttpClientFactory(); var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>(); var libraryManagerStub = new Mock<ILibraryManager>();

View File

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

View File

@ -8,7 +8,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.Languages; using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Test namespace Jellyfin.Plugin.MetaShark.Test
@ -125,27 +124,5 @@ namespace Jellyfin.Plugin.MetaShark.Test
}).GetAwaiter().GetResult(); }).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,15 +52,22 @@ namespace Jellyfin.Plugin.MetaShark.Api
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled); Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled); 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 regRole = new Regex(@"\([饰|配]?\s*?(.+?)\)", RegexOptions.Compiled);
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled); Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"(.+?) 至 (.+)", RegexOptions.Compiled); Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
Regex regHtmlTag = new Regex(@"<.?>", 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 regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled); Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
// 匹配除了换行符之外所有空白 // 匹配除了换行符之外所有空白
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled); Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
Regex regPhotoId = new Regex(@"/photo/(\d+?)/", 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次 // 默认200毫秒请求1次
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200)); private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200));
@ -82,7 +89,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
var handler = new HttpClientHandlerEx(); var handler = new HttpClientHandlerEx();
this._cookieContainer = handler.CookieContainer; this._cookieContainer = handler.CookieContainer;
httpClient = new HttpClient(handler); httpClient = new HttpClient(handler);
httpClient.Timeout = TimeSpan.FromSeconds(20); httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT); httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com"); httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/"); httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
@ -143,18 +150,6 @@ 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) public async Task<List<DoubanSubject>> SearchAsync(string keyword, CancellationToken cancellationToken)
{ {
var list = new List<DoubanSubject>(); var list = new List<DoubanSubject>();
@ -511,64 +506,48 @@ namespace Jellyfin.Plugin.MetaShark.Api
var contentNode = doc.QuerySelector("#content"); var contentNode = doc.QuerySelector("#content");
if (contentNode != null) if (contentNode != null)
{ {
celebrity.Img = contentNode.GetAttr("img.avatar", "src") ?? string.Empty; var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
var nameStr = contentNode.GetText("h1.subject-name") ?? string.Empty; var nameStr = contentNode.GetText("h1") ?? string.Empty;
celebrity.Name = this.ParseCelebrityName(nameStr); var name = this.ParseCelebrityName(nameStr);
celebrity.EnglishName = nameStr.Replace(celebrity.Name, "").Trim(); var englishName = nameStr.Replace(name, "").Trim();
var intro = contentNode.GetText("#intro span.all") ?? string.Empty;
var family = string.Empty; if (string.IsNullOrEmpty(intro))
var propertyNodes = contentNode.QuerySelectorAll("ul.subject-property>li");
foreach (var li in propertyNodes)
{ {
var label = li.GetText("span.label") ?? string.Empty; intro = contentNode.GetText("#intro div.bd") ?? string.Empty;
var value = li.GetText("span.value") ?? string.Empty; }
switch (label) var info = contentNode.GetText("div.info") ?? string.Empty;
{ var gender = info.GetMatchGroup(this.regGender);
case "性别:": var constellation = info.GetMatchGroup(this.regConstellation);
celebrity.Gender = value; var birthdate = info.GetMatchGroup(this.regBirthdate);
break;
case "星座:": // 生卒日期
celebrity.Constellation = value; var enddate = string.Empty;
break; var match = this.regLifedate.Match(info);
case "出生日期:": if (match.Success && match.Groups.Count > 2)
celebrity.Birthdate = value; {
break; birthdate = match.Groups[1].Value.Trim();
case "去世日期:": enddate = match.Groups[2].Value.Trim();
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 intro = contentNode.GetHtml("section.subject-intro div.content") ?? string.Empty; var role = info.GetMatchGroup(this.regCelebrityRole);
intro = regHtmlTag.Replace(intro.Replace("</p>", "\n"), ""); 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;
celebrity.Intro = formatOverview(intro); celebrity.Intro = formatOverview(intro);
celebrity.Constellation = constellation;
celebrity.Role = role;
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption); _memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
return celebrity; return celebrity;
} }
@ -823,27 +802,6 @@ namespace Jellyfin.Plugin.MetaShark.Api
return true; 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() protected async Task LimitRequestFrequently()
{ {
if (IsEnableAvoidRiskControl()) if (IsEnableAvoidRiskControl())

View File

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

View File

@ -24,10 +24,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public bool EnableDoubanAvoidRiskControl { get; set; } = false; public bool EnableDoubanAvoidRiskControl { get; set; } = false;
/// <summary> /// <summary>
/// 豆瓣海报使用大图
/// </summary>
public bool EnableDoubanLargePoster { get; set; } = false;
/// <summary>
/// 豆瓣背景图使用原图 /// 豆瓣背景图使用原图
/// </summary> /// </summary>
public bool EnableDoubanBackdropRaw { get; set; } = false; public bool EnableDoubanBackdropRaw { get; set; } = false;
@ -50,12 +46,6 @@ public class PluginConfiguration : BasePluginConfiguration
/// 启用tmdb获取背景图 /// 启用tmdb获取背景图
/// </summary> /// </summary>
public bool EnableTmdbBackdrop { get; set; } = true; public bool EnableTmdbBackdrop { get; set; } = true;
/// <summary>
/// 启用tmdb获取商标
/// </summary>
public bool EnableTmdbLogo { get; set; } = true;
/// <summary> /// <summary>
/// 是否获取电影系列信息 /// 是否获取电影系列信息
/// </summary> /// </summary>

View File

@ -28,8 +28,8 @@
</legend> </legend>
<div class="inputContainer"> <div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span <label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
id="login_msg" id="login_invalid"
style="margin-left: 8px; display: none;"></span></label> style="color: red; margin-left: 8px; display: none;">(已失效)</span></label>
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies" <textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea> class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div> <div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
@ -54,13 +54,6 @@
可为空填写jellyfin访问域名有端口时要加上端口只有使用了Nginx代理、Docker部署或启用了HTTPS并且豆瓣图片无法显示时才需要填写 可为空填写jellyfin访问域名有端口时要加上端口只有使用了Nginx代理、Docker部署或启用了HTTPS并且豆瓣图片无法显示时才需要填写
</div> </div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label" for="EnableDoubanLargePoster">
<input id="EnableDoubanLargePoster" name="EnableDoubanLargePoster" type="checkbox"
is="emby-checkbox" />
<span>海报使用大图</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription"> <div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label" for="EnableDoubanBackdropRaw"> <label class="emby-checkbox-label" for="EnableDoubanBackdropRaw">
<input id="EnableDoubanBackdropRaw" name="EnableDoubanBackdropRaw" type="checkbox" <input id="EnableDoubanBackdropRaw" name="EnableDoubanBackdropRaw" type="checkbox"
@ -98,14 +91,6 @@
</label> </label>
<div class="fieldDescription">勾选后当影片在豆瓣找不到背景图时改使用TheMovieDb的补全</div> <div class="fieldDescription">勾选后当影片在豆瓣找不到背景图时改使用TheMovieDb的补全</div>
</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"> <div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label" for="EnableTmdbCollection"> <label class="emby-checkbox-label" for="EnableTmdbCollection">
<input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox" <input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox"
@ -180,13 +165,11 @@
document.querySelector('#DoubanCookies').value = config.DoubanCookies; document.querySelector('#DoubanCookies').value = config.DoubanCookies;
document.querySelector('#DoubanImageProxyBaseUrl').value = config.DoubanImageProxyBaseUrl; document.querySelector('#DoubanImageProxyBaseUrl').value = config.DoubanImageProxyBaseUrl;
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl; document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
document.querySelector('#EnableDoubanLargePoster').checked = config.EnableDoubanLargePoster;
document.querySelector('#EnableDoubanBackdropRaw').checked = config.EnableDoubanBackdropRaw; document.querySelector('#EnableDoubanBackdropRaw').checked = config.EnableDoubanBackdropRaw;
document.querySelector('#EnableTmdb').checked = config.EnableTmdb; document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch; document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop; document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop;
document.querySelector('#EnableTmdbLogo').checked = config.EnableTmdbLogo;
document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection; document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection;
document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating; document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating;
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey; document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
@ -210,13 +193,11 @@
config.DoubanCookies = document.querySelector('#DoubanCookies').value; config.DoubanCookies = document.querySelector('#DoubanCookies').value;
config.DoubanImageProxyBaseUrl = document.querySelector('#DoubanImageProxyBaseUrl').value; config.DoubanImageProxyBaseUrl = document.querySelector('#DoubanImageProxyBaseUrl').value;
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked; config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
config.EnableDoubanLargePoster = document.querySelector('#EnableDoubanLargePoster').checked;
config.EnableDoubanBackdropRaw = document.querySelector('#EnableDoubanBackdropRaw').checked; config.EnableDoubanBackdropRaw = document.querySelector('#EnableDoubanBackdropRaw').checked;
config.EnableTmdb = document.querySelector('#EnableTmdb').checked; config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked; config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked; config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked;
config.EnableTmdbLogo = document.querySelector('#EnableTmdbLogo').checked;
config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked; config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked;
config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked; config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked;
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value; config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
@ -240,7 +221,6 @@
changeProxyDisplay(); changeProxyDisplay();
}); });
function changeProxyDisplay() { function changeProxyDisplay() {
let proxyType = document.querySelector('#TmdbProxyType').value; let proxyType = document.querySelector('#TmdbProxyType').value;
if (proxyType) { if (proxyType) {
@ -253,16 +233,16 @@
function checkDoubanLogin() { function checkDoubanLogin() {
let cookie = document.querySelector('#DoubanCookies').value let cookie = document.querySelector('#DoubanCookies').value
if (!cookie || !$.trim(cookie)) { if (!cookie || !$.trim(cookie)) {
$('#login_msg').hide(); $('#login_invalid').hide();
return; return;
} }
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) { $.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
if (resp && resp.code != 1) { if (resp && resp.code != 1) {
$('#login_msg').css("color", "red").text('(已失效)').show(); $('#login_invalid').show();
} else { } else {
$('#login_msg').css("color", "").text('(已生效)').show(); $('#login_invalid').hide();
} }
}) })
} }

View File

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

View File

@ -20,17 +20,6 @@ namespace Jellyfin.Plugin.MetaShark.Core
return null; 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 = "") public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
{ {
var node = el.QuerySelector(css); var node = el.QuerySelector(css);

View File

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

View File

@ -78,10 +78,5 @@ namespace Jellyfin.Plugin.MetaShark.Core
return string.Empty; 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" ?> <?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release'"> <Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release' or '$(Configuration)'=='Debug'">
<PropertyGroup> <PropertyGroup>
<DoILRepack>false</DoILRepack> <DoILRepack>false</DoILRepack>
</PropertyGroup> </PropertyGroup>

View File

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

View File

@ -1,14 +0,0 @@
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

@ -93,15 +93,6 @@ namespace Jellyfin.Plugin.MetaShark.Model
} }
} }
[JsonIgnore]
public string ImgLarge
{
get
{
return this.Img.Replace("s_ratio_poster", "l");
}
}
[JsonIgnore] [JsonIgnore]
public string[] Genres public string[] Genres
{ {
@ -164,16 +155,6 @@ namespace Jellyfin.Plugin.MetaShark.Model
return null; return null;
} }
} }
[JsonIgnore]
public string ImgMiddle
{
get
{
return this.Img.Replace("/raw/", "/m/").Replace("/s_ratio_poster/", "/m/");
}
}
} }
public class DoubanPhoto public class DoubanPhoto

View File

@ -6,34 +6,9 @@ using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Model namespace Jellyfin.Plugin.MetaShark.Model
{ {
public static class MetaSource
public enum MetaSource
{ {
Douban, public const string Douban = "douban";
Tmdb, public const string Tmdb = "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,7 +19,6 @@ using TMDbLib.Objects.General;
using Jellyfin.Plugin.MetaShark.Configuration; using Jellyfin.Plugin.MetaShark.Configuration;
using Jellyfin.Plugin.MetaShark.Core; using Jellyfin.Plugin.MetaShark.Core;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using MediaBrowser.Controller.Entities.TV;
namespace Jellyfin.Plugin.MetaShark.Providers namespace Jellyfin.Plugin.MetaShark.Providers
{ {
@ -51,7 +50,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled); 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 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 protected PluginConfiguration config
{ {
@ -109,17 +107,11 @@ namespace Jellyfin.Plugin.MetaShark.Providers
protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken) protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
{ {
var fileName = GetOriginalFileName(info); 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 parseResult = NameParser.Parse(fileName);
var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name; var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name;
info.Year = parseResult.Year; // 默认parser对anime年份会解析出错以anitomy为准 info.Year = parseResult.Year; // 默认parser对anime年份会解析出错以anitomy为准
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}"); this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
List<DoubanSubject> result; List<DoubanSubject> result;
DoubanSubject? item; DoubanSubject? item;
@ -133,13 +125,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault(); item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
if (item != null) if (item != null)
{ {
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)"); this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
return item.Sid; return item.Sid;
} }
item = result.Where(x => x.Year == info.Year).FirstOrDefault(); item = result.Where(x => x.Year == info.Year).FirstOrDefault();
if (item != null) if (item != null)
{ {
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)"); this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
return item.Sid; return item.Sid;
} }
} }
@ -177,7 +169,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
item = result.Where(x => x.Category == cat).FirstOrDefault(); item = result.Where(x => x.Category == cat).FirstOrDefault();
if (item != null) if (item != null)
{ {
this.Log($"Found douban [id] by first match: {item.Name}({item.Sid})"); this.Log($"GuessByDouban found -> {item.Name}({item.Sid})");
return item.Sid; return item.Sid;
} }
@ -327,7 +319,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
break; break;
} }
this.Log($"Not found tmdb id by [name]: {name} [year]: {year}");
return null; return null;
} }
@ -359,24 +350,11 @@ namespace Jellyfin.Plugin.MetaShark.Providers
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}"); this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
return $"{tmdbId}"; 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; break;
default: default:
break; break;
} }
this.Log($"Not found tmdb id by imdb id: {imdb}");
return null; return null;
} }
@ -402,11 +380,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public int? GuessSeasonNumberByDirectoryName(string path) public int? GuessSeasonNumberByDirectoryName(string path)
{ {
// TODO: 有时 series name 中会带有季信息 // TODO: 有时series name中会带有季信息
// 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber这时 path 为空,直接返回 // 当没有season级目录时path为空直接返回
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
this.Log($"Season path is empty!");
return null; return null;
} }
@ -417,7 +394,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null; return null;
} }
// 中文季名
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled); var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
var match = regSeason.Match(fileName); var match = regSeason.Match(fileName);
if (match.Success && match.Groups.Count > 1) if (match.Success && match.Groups.Count > 1)
@ -434,26 +410,12 @@ 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>() { var seasonNameMap = new Dictionary<string, int>() {
{@"[ ._](I|1st)[ ._]", 1}, {@"[ ._](I|1st|S01|S1)[ ._]", 1},
{@"[ ._](II|2nd)[ ._]", 2}, {@"[ ._](II|2nd|S02|S2)[ ._]", 2},
{@"[ ._](III|3rd)[ ._]", 3}, {@"[ ._](III|3rd|S03|S3)[ ._]", 3},
{@"[ ._](IIII|4th)[ ._]", 3}, {@"[ ._](IIII|4th|S04|S4)[ ._]", 3},
}; };
foreach (var entry in seasonNameMap) foreach (var entry in seasonNameMap)
@ -509,32 +471,53 @@ namespace Jellyfin.Plugin.MetaShark.Providers
protected string GetLocalProxyImageUrl(string url) protected string GetLocalProxyImageUrl(string url)
{ {
var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty; var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl();
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl)) if (!string.IsNullOrEmpty(config.DoubanImageProxyBaseUrl))
{ {
baseUrl = config.DoubanImageProxyBaseUrl.TrimEnd('/'); baseUrl = config.DoubanImageProxyBaseUrl.TrimEnd('/');
} }
if (!string.IsNullOrEmpty(baseUrl))
{
var encodedUrl = HttpUtility.UrlEncode(url);
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
}
var encodedUrl = HttpUtility.UrlEncode(url); return this.GetProxyImageUrl(url);
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
} }
private string GetBaseUrl() private string GetBaseUrl()
{ {
// 配置优先 // 配置优先
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl)) if (!string.IsNullOrEmpty(this.config.DoubanImageProxyBaseUrl))
{ {
return this.config.DoubanImageProxyBaseUrl.TrimEnd('/'); return this.config.DoubanImageProxyBaseUrl.TrimEnd('/');
} }
// TODOhttp请求时获取请求的host (nginx代理/docker中部署时没配置透传host时本方式会有问题) // http请求时获取请求的host (nginx代理/docker中部署时没配置透传host时本方式会有问题)
// 除自动扫描之外都会执行这里修改图片功能图片是直接下载不走插件图片代理处理函数host拿不到就下载不了
if (Plugin.Instance != null && this._httpContextAccessor.HttpContext != null) if (Plugin.Instance != null && this._httpContextAccessor.HttpContext != null)
{ {
// 特殊处理下搜索请求直接使用相对链接可以减少使用nginx代理但不透传host的问题
var userAgent = this._httpContextAccessor.HttpContext.Request.Headers.UserAgent.ToString();
var fromWeb = userAgent.Contains("Chrome") || userAgent.Contains("Safari");
var fromItemSearch = this._httpContextAccessor.HttpContext.Request.Path.ToString().Contains("/RemoteSearch");
if (fromWeb && fromItemSearch)
{
// 处理通过nginx反向代理后url加了subpath访问的情况
var subpath = string.Empty;
var baseUrl = Plugin.Instance.GetApiBaseUrl(this._httpContextAccessor.HttpContext.Request);
var uri = new UriBuilder(baseUrl);
if (!string.IsNullOrEmpty(uri.Path) && uri.Path != "/")
{
subpath = "/" + uri.Path.Trim('/');
}
return subpath;
}
return Plugin.Instance.GetApiBaseUrl(this._httpContextAccessor.HttpContext.Request); return Plugin.Instance.GetApiBaseUrl(this._httpContextAccessor.HttpContext.Request);
} }
// 自动扫描刷新时,直接使用本地地址(127.0.0.1) // 自动扫描刷新时,直接使用本地地址
return Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty; return Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
} }
@ -590,17 +573,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return string.Empty; return string.Empty;
} }
protected string GetDoubanPoster(DoubanSubject subject)
{
if (string.IsNullOrEmpty(subject.Img)) {
return string.Empty;
}
var url = config.EnableDoubanLargePoster ? subject.ImgLarge : subject.ImgMiddle;
return this.GetProxyImageUrl(url);
}
protected string GetOriginalFileName(ItemLookupInfo info) protected string GetOriginalFileName(ItemLookupInfo info)
{ {
@ -623,47 +596,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
} }
protected string? GetOriginalSeasonPath(EpisodeInfo info) protected string RemoveSeasonSubfix(string name)
{
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, ""); return regSeasonNameSuffix.Replace(name, "");
} }

View File

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

View File

@ -1,28 +0,0 @@
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,36 +36,33 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{ {
ImageType.Primary, ImageType.Primary,
ImageType.Backdrop, ImageType.Backdrop
ImageType.Logo,
}; };
/// <inheritdoc /> /// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{ {
var sid = item.GetProviderId(DoubanProviderId); var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId); var metaSource = item.GetProviderId(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}"); this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false); var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
if (primary == null || string.IsNullOrEmpty(primary.Img)) if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
{ {
return Enumerable.Empty<RemoteImageInfo>(); return Enumerable.Empty<RemoteImageInfo>();
} }
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false); var backdropImgs = await GetBackdrop(item, cancellationToken);
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
var res = new List<RemoteImageInfo> { var res = new List<RemoteImageInfo> {
new RemoteImageInfo new RemoteImageInfo
{ {
ProviderName = this.Name, ProviderName = this.Name,
Url = this.GetDoubanPoster(primary), Url = this.GetProxyImageUrl(primary.ImgMiddle),
Type = ImageType.Primary, Type = ImageType.Primary,
}, },
}; };
res.AddRange(backdropImgs); res.AddRange(backdropImgs);
res.AddRange(logoImgs);
return res; return res;
} }
@ -73,8 +70,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId)) if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
{ {
var language = item.GetPreferredMetadataLanguage(); var language = item.GetPreferredMetadataLanguage();
// 设定language会导致图片被过滤这里设为null保持取全部语言图片 var movie = await _tmdbApi
var movie = await this._tmdbApi
.GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken) .GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -85,41 +81,37 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var remoteImages = new List<RemoteImageInfo>(); var remoteImages = new List<RemoteImageInfo>();
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo { for (var i = 0; i < movie.Images.Posters.Count; i++)
ProviderName = this.Name, {
Url = this._tmdbApi.GetPosterUrl(x.FilePath), 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,
Type = ImageType.Primary, 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,
}));
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo { for (var i = 0; i < movie.Images.Backdrops.Count; i++)
ProviderName = this.Name, {
Url = this._tmdbApi.GetBackdropUrl(x.FilePath), 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,
Type = ImageType.Backdrop, Type = ImageType.Backdrop,
CommunityRating = x.VoteAverage, RatingType = RatingType.Score
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); return remoteImages.OrderByLanguageDescending(language);
} }
@ -141,7 +133,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
// 从豆瓣获取背景图 // 从豆瓣获取背景图
if (!string.IsNullOrEmpty(sid)) if (!string.IsNullOrEmpty(sid))
{ {
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken).ConfigureAwait(false); var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
if (photo != null && photo.Count > 0) if (photo != null && photo.Count > 0)
{ {
this.Log("GetBackdrop from douban sid: {0}", sid); this.Log("GetBackdrop from douban sid: {0}", sid);
@ -195,36 +187,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return list; 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,7 +7,6 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Text; using AngleSharp.Text;
using Jellyfin.Data.Enums;
using Jellyfin.Plugin.MetaShark.Api; using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core; using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model; using Jellyfin.Plugin.MetaShark.Model;
@ -43,14 +42,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
// 从douban搜索 // 从douban搜索
var res = await this._doubanApi.SearchMovieAsync(info.Name, cancellationToken).ConfigureAwait(false); // BUG注意ProviderIds传多个meta值会导致识别搜索时只返回一个结果
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x => result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
{ {
return new RemoteSearchResult return new RemoteSearchResult
{ {
// 注意jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性 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), ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year, ProductionYear = x.Year,
Name = x.Name, Name = x.Name,
@ -66,9 +64,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
return new RemoteSearchResult return new RemoteSearchResult
{ {
// 注意jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性 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.Title ?? x.OriginalTitle), Name = string.Format("[TMDB]{0}", x.Title ?? x.OriginalTitle),
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath), ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview, Overview = x.Overview,
@ -83,14 +79,18 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc /> /// <inheritdoc />
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{ {
var fileName = this.GetOriginalFileName(info); this.Log($"GetMovieMetadata of [name]: {info.Name} IsAutomated: {info.IsAutomated}");
this.Log($"GetMovieMetadata of [name]: {info.Name} [fileName]: {fileName} EnableTmdb: {config.EnableTmdb}");
var result = new MetadataResult<Movie>(); var result = new MetadataResult<Movie>();
// 使用刷新元数据时providerIds会保留旧有值只有识别/新增才会没值 // 使用刷新元数据时providerIds会保留旧有值只有识别/新增才会没值
var sid = info.GetProviderId(DoubanProviderId); var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetMetaSource(Plugin.ProviderId); var metaSource = info.GetProviderId(Plugin.ProviderId);
// 用于修正识别时指定tmdb没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
{
metaSource = MetaSource.Tmdb;
}
// 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致 // 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId); var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid); var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
@ -119,8 +119,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var movie = new Movie var movie = new Movie
{ {
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一 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 = subject.Name, Name = subject.Name,
OriginalTitle = subject.OriginalName, OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating, CommunityRating = subject.Rating,
@ -183,7 +182,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{ {
Name = c.Name, Name = c.Name,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor, Type = c.RoleType,
Role = c.Role, Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img), ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
@ -233,8 +232,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一 movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
movie.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
// 获取电影系列信息 // 获取电影系列信息
if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null) if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null)
@ -302,7 +300,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
Name = actor.Name.Trim(), Name = actor.Name.Trim(),
Role = actor.Character, Role = actor.Character,
Type = PersonKind.Actor, Type = PersonType.Actor,
SortOrder = actor.Order, SortOrder = actor.Order,
}; };
@ -345,7 +343,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job,
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor), Type = type
}; };
if (!string.IsNullOrWhiteSpace(person.ProfilePath)) if (!string.IsNullOrWhiteSpace(person.ProfilePath))

View File

@ -37,7 +37,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
var list = new List<RemoteImageInfo>(); var list = new List<RemoteImageInfo>();
var cid = item.GetProviderId(DoubanProviderId); var cid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId); var metaSource = item.GetProviderId(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}"); this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (!string.IsNullOrEmpty(cid)) if (!string.IsNullOrEmpty(cid))
{ {
@ -72,10 +72,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
}); });
} }
if (list.Count == 0) this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
{
this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
}
return list; return list;
} }

View File

@ -120,9 +120,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0) if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
{ {
var foundTmdbId = findResult.PersonResults.First().Id.ToString(); this.Log($"GetPersonMetadata of found tmdb [id]: {findResult.PersonResults[0].Id}");
this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}"); item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.PersonResults[0].Id}");
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}"); this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
var season = (Season)item; var season = (Season)item;
var series = season.Series; var series = season.Series;
var metaSource = series.GetMetaSource(Plugin.ProviderId); var metaSource = series.GetProviderId(Plugin.ProviderId);
// get image from douban // get image from douban
var sid = item.GetProviderId(DoubanProviderId); var sid = item.GetProviderId(DoubanProviderId);
@ -60,7 +60,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
new RemoteImageInfo new RemoteImageInfo
{ {
ProviderName = primary.Name, ProviderName = primary.Name,
Url = this.GetDoubanPoster(primary), Url = this.GetProxyImageUrl(primary.ImgMiddle),
Type = ImageType.Primary, Type = ImageType.Primary,
}, },
}; };

View File

@ -14,8 +14,6 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Jellyfin.Data.Enums;
using System.IO;
namespace Jellyfin.Plugin.MetaShark.Providers namespace Jellyfin.Plugin.MetaShark.Providers
{ {
@ -42,27 +40,23 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
var result = new MetadataResult<Season>(); var result = new MetadataResult<Season>();
// 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId); info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource); info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid); info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0 var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
var seasonSid = info.GetProviderId(DoubanProviderId); var seasonSid = info.GetProviderId(DoubanProviderId);
var fileName = Path.GetFileName(info.Path); this.Log($"GetSeasonMetaData of [name]: {info.Name} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} IsAutomated: {info.IsAutomated}");
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)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
// seasonNumber 为 null 有三种情况: // 季文件夹名称不规范没法拿到seasonNumber尝试从文件夹名猜出
// 1. 没有季文件夹时即虚拟季info.Path 为空 // 注意:本办法没法处理没有季文件夹的/虚拟季因为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) if (seasonNumber is null)
{ {
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path); seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
} }
// 搜索豆瓣季 id // 搜索豆瓣季id
if (string.IsNullOrEmpty(seasonSid)) if (string.IsNullOrEmpty(seasonSid))
{ {
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false); seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
@ -93,19 +87,18 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{ {
Name = c.Name, Name = c.Name,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor, Type = c.RoleType,
Role = c.Role, Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img), ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
})); }));
this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}");
return result; return result;
} }
} }
else else
{ {
this.Log($"Season [{info.Name}] not found douban season id!"); this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!");
} }
@ -143,19 +136,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return null; return null;
} }
// 没有季文件夹或季文件夹名不规范时即虚拟季info.Path 会为空seasonNumber 为 null // 没有季文件夹即虚拟季info.Path会为空直接用series的sid
if (string.IsNullOrEmpty(info.Path) && !seasonNumber.HasValue) if (string.IsNullOrEmpty(info.Path))
{ {
return null; return sid;
}
// 从季文件夹名属性格式获取,如 [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季非标准名称默认文件名 // 从sereis获取正确名称info.Name当是标准格式如S01等时会变成第x季非标准名称默认文件名
@ -164,7 +148,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
return null; return null;
} }
var seriesName = this.RemoveSeasonSuffix(series.Name); var seriesName = RemoveSeasonSubfix(series.Name);
// 没有季id但存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据 // 没有季id但存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据
var seasonYear = 0; var seasonYear = 0;

View File

@ -36,36 +36,33 @@ namespace Jellyfin.Plugin.MetaShark.Providers
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{ {
ImageType.Primary, ImageType.Primary,
ImageType.Backdrop, ImageType.Backdrop
ImageType.Logo,
}; };
/// <inheritdoc /> /// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{ {
var sid = item.GetProviderId(DoubanProviderId); var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetMetaSource(Plugin.ProviderId); var metaSource = item.GetProviderId(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}"); this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false); var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
if (primary == null || string.IsNullOrEmpty(primary.Img)) if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
{ {
return Enumerable.Empty<RemoteImageInfo>(); return Enumerable.Empty<RemoteImageInfo>();
} }
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false); var dropback = await GetBackdrop(item, cancellationToken);
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
var res = new List<RemoteImageInfo> { var res = new List<RemoteImageInfo> {
new RemoteImageInfo new RemoteImageInfo
{ {
ProviderName = this.Name, ProviderName = this.Name,
Url = this.GetDoubanPoster(primary), Url = this.GetProxyImageUrl(primary.ImgMiddle),
Type = ImageType.Primary, Type = ImageType.Primary,
}, },
}; };
res.AddRange(backdropImgs); res.AddRange(dropback);
res.AddRange(logoImgs);
return res; return res;
} }
@ -73,8 +70,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId)) if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
{ {
var language = item.GetPreferredMetadataLanguage(); var language = item.GetPreferredMetadataLanguage();
// 设定language会导致图片被过滤这里设为null保持取全部语言图片 var movie = await _tmdbApi
var movie = await this._tmdbApi
.GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken) .GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -85,41 +81,37 @@ namespace Jellyfin.Plugin.MetaShark.Providers
var remoteImages = new List<RemoteImageInfo>(); var remoteImages = new List<RemoteImageInfo>();
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo { for (var i = 0; i < movie.Images.Posters.Count; i++)
ProviderName = this.Name, {
Url = this._tmdbApi.GetPosterUrl(x.FilePath), 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,
Type = ImageType.Primary, 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,
}));
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo { for (var i = 0; i < movie.Images.Backdrops.Count; i++)
ProviderName = this.Name, {
Url = this._tmdbApi.GetBackdropUrl(x.FilePath), 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,
Type = ImageType.Backdrop, Type = ImageType.Backdrop,
CommunityRating = x.VoteAverage, RatingType = RatingType.Score
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); return remoteImages.OrderByLanguageDescending(language);
} }
@ -185,8 +177,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
this.Log("GetBackdrop from tmdb id: {0}", tmdbId); this.Log("GetBackdrop from tmdb id: {0}", tmdbId);
list.Add(new RemoteImageInfo list.Add(new RemoteImageInfo
{ {
ProviderName = this.Name, ProviderName = Name,
Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath), Url = _tmdbApi.GetBackdropUrl(movie.BackdropPath),
Type = ImageType.Backdrop, Type = ImageType.Backdrop,
}); });
} }
@ -195,36 +187,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return list; 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,5 +1,4 @@
using Jellyfin.Data.Enums; using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Model; using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -42,13 +41,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
// 从douban搜索 // 从douban搜索
var res = await this._doubanApi.SearchTVAsync(info.Name, cancellationToken).ConfigureAwait(false); var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x => result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
{ {
return new RemoteSearchResult return new RemoteSearchResult
{ {
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一 ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } },
ImageUrl = this.GetProxyImageUrl(x.Img), ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year, ProductionYear = x.Year,
Name = x.Name, Name = x.Name,
@ -63,8 +61,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
return new RemoteSearchResult return new RemoteSearchResult
{ {
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一 ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
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), Name = string.Format("[TMDB]{0}", x.Name ?? x.OriginalName),
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath), ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview, Overview = x.Overview,
@ -79,13 +76,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
/// <inheritdoc /> /// <inheritdoc />
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{ {
var fileName = this.GetOriginalFileName(info); this.Log($"GetSeriesMetadata of [name]: {info.Name} IsAutomated: {info.IsAutomated}");
this.Log($"GetSeriesMetadata of [name]: {info.Name} [fileName]: {fileName} IsAutomated: {info.IsAutomated}");
var result = new MetadataResult<Series>(); var result = new MetadataResult<Series>();
var sid = info.GetProviderId(DoubanProviderId); var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetMetaSource(Plugin.ProviderId); var metaSource = info.GetProviderId(Plugin.ProviderId);
// 用于修正识别时指定tmdb没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
{
metaSource = MetaSource.Tmdb;
}
// 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致 // 注意会存在元数据有tmdbId但metaSource没值的情况之前由TMDB插件刮削导致
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId); var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid); var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
@ -105,12 +106,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false); subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
var seriesName = RemoveSeasonSuffix(subject.Name); var seriesName = RemoveSeasonSubfix(subject.Name);
var item = new Series var item = new Series
{ {
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
Name = seriesName, Name = seriesName,
OriginalTitle = RemoveSeasonSuffix(subject.OriginalName), OriginalTitle = RemoveSeasonSubfix(subject.OriginalName),
CommunityRating = subject.Rating, CommunityRating = subject.Rating,
Overview = subject.Intro, Overview = subject.Intro,
ProductionYear = subject.Year, ProductionYear = subject.Year,
@ -153,7 +154,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
{ {
Name = c.Name, Name = c.Name,
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor, Type = c.RoleType,
Role = c.Role, Role = c.Role,
ImageUrl = this.GetLocalProxyImageUrl(c.Img), ImageUrl = this.GetLocalProxyImageUrl(c.Img),
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } }, ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
@ -339,7 +340,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId); series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
} }
} }
series.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{seriesResult.Id}"); series.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode); series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode);
@ -357,7 +358,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
Name = actor.Name.Trim(), Name = actor.Name.Trim(),
Role = actor.Character, Role = actor.Character,
Type = PersonKind.Actor, Type = PersonType.Actor,
SortOrder = actor.Order, SortOrder = actor.Order,
}; };
@ -401,7 +402,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
{ {
Name = person.Name.Trim(), Name = person.Name.Trim(),
Role = person.Job, Role = person.Job,
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor), Type = type
}; };
if (!string.IsNullOrWhiteSpace(person.ProfilePath)) if (!string.IsNullOrWhiteSpace(person.ProfilePath))

View File

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

View File

@ -1,7 +1,7 @@
# jellyfin-plugin-metashark # jellyfin-plugin-metashark
[![release](https://img.shields.io/github/v/release/cxfksword/jellyfin-plugin-metashark)](https://github.com/cxfksword/jellyfin-plugin-metashark/releases) [![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|10.9.x-lightgrey?logo=jellyfin)](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)
[![license](https://img.shields.io/github/license/cxfksword/jellyfin-plugin-metashark)](https://github.com/cxfksword/jellyfin-plugin-metashark/main/LICENSE) [![license](https://img.shields.io/github/license/cxfksword/jellyfin-plugin-metashark)](https://github.com/cxfksword/jellyfin-plugin-metashark/main/LICENSE)
jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMovieDb补全缺失的剧集数据。 jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMovieDb补全缺失的剧集数据。
@ -14,13 +14,9 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
## 安装插件 ## 安装插件
添加插件存储库: 只支持最新的`jellyfin 10.8.x`版本
国内加速https://gitee.com/cwhzy/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json 添加插件存储库https://jellyfin-plugin-release.pages.dev/metashark/manifest.json
国外访问https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json
> 如果都无法访问,可以直接从 [Release](https://github.com/cxfksword/jellyfin-plugin-metashark/releases) 页面下载,并解压到 jellyfin 插件目录中使用
## 如何使用 ## 如何使用
@ -34,19 +30,17 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
> 🚨假如需要刮削大量电影请到插件配置中打开防封禁功能避免频繁请求豆瓣导致被封IP封IP需要等6小时左右才能恢复访问 > 🚨假如需要刮削大量电影请到插件配置中打开防封禁功能避免频繁请求豆瓣导致被封IP封IP需要等6小时左右才能恢复访问
> :fire:遇到图片显示不出来时请到插件配置中配置jellyfin访问域名
## How to build ## How to build
1. Clone or download this repository 1. Clone or download this repository
2. Ensure you have .NET Core SDK 8.0 setup and installed 2. Ensure you have .NET Core SDK 6.0 setup and installed
3. Build plugin with following command. 3. Build plugin with following command.
```sh ```sh
dotnet restore dotnet restore
dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
``` ```
@ -54,7 +48,7 @@ dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin
1. Build the plugin 1. Build the plugin
2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll` into it 2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Debug/net6.0/Jellyfin.Plugin.MetaShark.dll` into it
3. Move folder `metashark` to jellyfin `data/plugins` folder 3. Move folder `metashark` to jellyfin `data/plugins` folder
@ -68,10 +62,4 @@ dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin
## Thanks ## Thanks
[AnitomySharp](https://github.com/chu-shen/AnitomySharp) [AnitomySharp](https://github.com/chu-shen/AnitomySharp)
## 免责声明
本项目代码仅用于学习交流编程技术,下载后请勿用于商业用途。
如果本项目存在侵犯您的合法权益的情况,请及时与开发者联系,开发者将会及时删除有关内容。

View File

@ -18,7 +18,7 @@ def generate_manifest():
"overview": "jellyfin电影元数据插件", "overview": "jellyfin电影元数据插件",
"owner": "cxfksword", "owner": "cxfksword",
"category": "Metadata", "category": "Metadata",
"imageUrl": "https://github.com/cxfksword/jellyfin-plugin-metashark/raw/main/doc/logo.png", "imageUrl": "https://jellyfin-plugin-release.pages.dev/metashark/logo.png",
"versions": [] "versions": []
}] }]
@ -26,8 +26,8 @@ def generate_version(filepath, version, changelog):
return { return {
'version': f"{version}.0", 'version': f"{version}.0",
'changelog': changelog, 'changelog': changelog,
'targetAbi': '10.9.0.0', 'targetAbi': '10.8.0.0',
'sourceUrl': f'https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/v{version}/metashark_{version}.0.zip', 'sourceUrl': f'https://jellyfin-plugin-release.pages.dev/metashark/metashark_{version}.0.zip',
'checksum': md5sum(filepath), 'checksum': md5sum(filepath),
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S') 'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
} }
@ -47,7 +47,7 @@ def main():
# 解析旧 manifest # 解析旧 manifest
try: try:
with urlopen('https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json') as f: with urlopen('https://raw.githubusercontent.com/cxfksword/jellyfin-release/master/metashark/manifest.json') as f:
manifest = json.load(f) manifest = json.load(f)
except HTTPError as err: except HTTPError as err:
if err.code == 404: if err.code == 404:
@ -63,14 +63,11 @@ def main():
json.dump(manifest, f, indent=2) json.dump(manifest, f, indent=2)
# # 国内加速 # # 国内加速
cn_domain = 'https://mirror.ghproxy.com/' # with open('manifest_cn.json', 'w') as f:
if 'CN_DOMAIN' in os.environ and os.environ["CN_DOMAIN"]: # manifest_cn = json.dumps(manifest, indent=2)
cn_domain = os.environ["CN_DOMAIN"] # manifest_cn = re.sub('https://github.com/cxfksword/jellyfin-plugin-metashark/raw/main/doc/logo.png', "https://jellyfin-plugin-release.pages.dev/metashark/logo.png", manifest_cn)
cn_domain = cn_domain.rstrip('/') # manifest_cn = re.sub('https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/v[0-9.]+', "https://jellyfin-plugin-release.pages.dev/metashark", manifest_cn)
with open('manifest_cn.json', 'w') as f: # f.write(manifest_cn)
manifest_cn = json.dumps(manifest, indent=2)
manifest_cn = re.sub('https://github.com', f'{cn_domain}/https://github.com', manifest_cn)
f.write(manifest_cn)
if __name__ == '__main__': if __name__ == '__main__':