Compare commits
34 Commits
Author | SHA1 | Date |
---|---|---|
|
2f44ddf6ec | |
|
f337406ac9 | |
|
cb00027b77 | |
|
9e9074bb5a | |
|
747e69f3bf | |
|
104d200e7b | |
|
8ded89422f | |
|
25a844a2c4 | |
|
f1f35a6d32 | |
|
96be3222f9 | |
|
1b547f7aaf | |
|
408929fc03 | |
|
e26794fdcd | |
|
2eca5c9a22 | |
|
23d10b94ed | |
|
6f3863eaa7 | |
|
a9acabc056 | |
|
169ce81a94 | |
|
3c054cfa2d | |
|
10c3414920 | |
|
513a668c40 | |
|
9c0448898a | |
|
b7541fbb03 | |
|
f222258080 | |
|
f5cf162e67 | |
|
54fd425849 | |
|
223c93ec49 | |
|
64b401c049 | |
|
e3a4ea6d73 | |
|
a1b8533376 | |
|
2522faebe2 | |
|
e3a559d970 | |
|
17a2d093a6 | |
|
2d25d398bd |
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: 刮削失败相关问题
|
||||
about: 报告刮削失败相关问题.
|
||||
title: "[刮削]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述错误**
|
||||
|
||||
对错误是什么的清晰简明描述。
|
||||
|
||||
**屏幕截图**
|
||||
|
||||
请添加问题截图以帮助解释您的问题。
|
||||
|
||||
**日志**
|
||||
|
||||
请提供jellyfin打印的该影片的刮削日志。
|
||||
|
||||
日志查看方法: 控制台->高级->日志->点击log_yyyymmdd.log格式文件
|
||||
|
||||
**运行环境(请填写以下信息):**
|
||||
|
||||
- 操作系统:[例如 linux]
|
||||
- jellyfin 版本:[例如 10.8.9]
|
||||
- 插件版本:[例如 1.7.1]
|
|
@ -4,7 +4,7 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 8.0.x
|
||||
python-version: 3.8
|
||||
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||
artifact: metashark
|
||||
|
@ -31,14 +31,14 @@ jobs:
|
|||
run: |
|
||||
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
||||
VERSION="$VERSION.0"
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet restore ${{ env.project }} --no-cache
|
||||
dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 ${{ env.project }}
|
||||
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 ${{ env.project }}
|
||||
mkdir -p artifacts
|
||||
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
|
||||
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: actions/setup-dotnet@v3
|
||||
id: dotnet
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 8.0.x
|
||||
- name: Change default dotnet version
|
||||
run: |
|
||||
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
|
||||
|
|
|
@ -5,7 +5,7 @@ on:
|
|||
tags: ["*"]
|
||||
|
||||
env:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 8.0.x
|
||||
python-version: 3.8
|
||||
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||
artifact: metashark
|
||||
|
@ -36,37 +36,32 @@ jobs:
|
|||
run: |
|
||||
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
||||
VERSION="$VERSION.0"
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet restore ${{ env.project }} --no-cache
|
||||
dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
|
||||
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
|
||||
mkdir -p artifacts
|
||||
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net6.0/Jellyfin.Plugin.MetaShark.dll
|
||||
cp ./doc/logo.png ./artifacts/logo.png
|
||||
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll
|
||||
- name: Generate manifest
|
||||
run: cd artifacts && python3 ../generate_manifest.py ${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
|
||||
- name: Deploy to jellyfin release repo
|
||||
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
|
||||
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
|
||||
env:
|
||||
CN_DOMAIN: ${{ vars.CN_DOMAIN }}
|
||||
- name: Publish release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./artifacts/${{ env.artifact }}_*.zip
|
||||
tag: ${{ github.ref }}
|
||||
release_name: '${{ github.ref_name }}: Jellyfin v10.9'
|
||||
file_glob: true
|
||||
overwrite: true
|
||||
- name: Publish manifest
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./manifest*.json
|
||||
tag: "manifest"
|
||||
overwrite: 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
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageId>AnitomySharp.NET6</PackageId>
|
||||
<Authors>tabratton;senritsu</Authors>
|
||||
<PackageVersion>0.4.0</PackageVersion>
|
||||
<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.
|
||||
This fork of AnitomySharp is inspired by tabratton and senritsu, which adds more custom rules.
|
||||
</Description>
|
||||
<RepositoryUrl>https://github.com/chu-shen/AnitomySharp.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageTags>Anitomy Anime</PackageTags>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<AssemblyVersion>0.3.0</AssemblyVersion>
|
||||
<FileVersion>0.3.0</FileVersion>
|
||||
<Version>0.3.0</Version>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<DocumentationFile>AnitomySharp.xml</DocumentationFile>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE" Pack="true" Visible="false" PackagePath="" />
|
||||
<PackageReference Include="Microsoft.DocAsCode.App" Version="2.60.0" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -24,13 +24,13 @@ namespace AnitomySharp
|
|||
public static class KeywordManager
|
||||
{
|
||||
/// <summary>
|
||||
/// 包含所有关键词(大写)的内部关键词元素词典
|
||||
/// 包含所有关键词的内部关键词元素词典,比较器忽略大小写
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>();
|
||||
private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>
|
||||
/// 文件扩展名,无值
|
||||
/// 文件扩展名,无值,比较器忽略大小写
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, Keyword> Extensions = new Dictionary<string, Keyword>();
|
||||
private static readonly Dictionary<string, Keyword> Extensions = new Dictionary<string, Keyword>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// ~~一眼真~~
|
||||
|
@ -64,18 +64,19 @@ namespace AnitomySharp
|
|||
"GEKIJOUBAN", "MOVIE",
|
||||
"OAD", "OAV", "ONA", "OVA",
|
||||
"TV",
|
||||
"番外編", "總集編","映像特典","特典","特典アニメ",
|
||||
"番外編", "總集編","DRAMA",
|
||||
"映像特典","特典","特典アニメ",
|
||||
// 特典 Special 剩下的各种类型可以全部命名成 SP,对于较特殊意义的特典也可以自定义命名
|
||||
"SPECIAL", "SPECIALS", "SP",
|
||||
"SPECIAL", "SPECIALS", "SP", "SPs", "特報",
|
||||
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
||||
"IV",
|
||||
// 音乐视频 Music Video
|
||||
"MV"});
|
||||
|
||||
// add "SP" to ElementAnimeType with optionsUnidentifiable
|
||||
// Add(Element.ElementCategory.ElementAnimeType,
|
||||
// optionsUnidentifiableUnsearchable,
|
||||
// new List<string> {"SP"}); // e.g. "Yumeiro Patissiere SP Professional"
|
||||
// add "SP" to ElementAnimeType with optionsUnidentifiable
|
||||
// Add(Element.ElementCategory.ElementAnimeType,
|
||||
// optionsUnidentifiableUnsearchable,
|
||||
// new List<string> { "SP" }); // e.g. "Yumeiro Patissiere SP Professional", but it is widely used to represent special
|
||||
|
||||
Add(Element.ElementCategory.ElementAnimeType,
|
||||
optionsUnidentifiableInvalid,
|
||||
|
@ -84,7 +85,7 @@ namespace AnitomySharp
|
|||
// 无字 OP/ED Non-Credit Opening/Ending
|
||||
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
|
||||
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
|
||||
"PREVIEW",
|
||||
"PREVIEW", "YOKOKU", "予告",
|
||||
// 菜单 Menu BD/DVD 播放选择菜单
|
||||
"MENU",
|
||||
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右
|
||||
|
@ -92,7 +93,7 @@ namespace AnitomySharp
|
|||
// 语音信息
|
||||
"MESSAGE",
|
||||
// 宣传片/预告片 Promotion Video / Trailer 一般时长在 1~2min 命名参考原盘和 jsum
|
||||
"PV", "Teaser","TRAILER", "DRAMA",
|
||||
"PV", "Teaser","TRAILER",
|
||||
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
||||
"INTERVIEW",
|
||||
"EVENT", "TOKUTEN", "LOGO"});
|
||||
|
@ -150,7 +151,7 @@ namespace AnitomySharp
|
|||
|
||||
Add(Element.ElementCategory.ElementOther,
|
||||
optionsDefault,
|
||||
new List<string> { "REMASTER", "REMASTERED", "UNCUT", "TS", "VFR", "WIDESCREEN", "WS", "SPURSENGINE" });
|
||||
new List<string> { "REMASTER", "REMASTERED", "UNCUT", "TS", "VFR", "WIDESCREEN", "WS", "SPURSENGINE","DISC" });
|
||||
|
||||
Add(Element.ElementCategory.ElementReleaseGroup,
|
||||
optionsDefault,
|
||||
|
@ -281,6 +282,16 @@ namespace AnitomySharp
|
|||
|
||||
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>
|
||||
/// Finds a particular <c>keyword</c>. If found sets <c>category</c> and <c>options</c> to the found search result.
|
||||
|
|
|
@ -179,6 +179,7 @@ namespace AnitomySharp
|
|||
private void SearchForEpisodeNumber()
|
||||
{
|
||||
var tokens = new List<int>();
|
||||
var allTokens = new List<int>();
|
||||
for (var i = 0; i < Tokens.Count; i++)
|
||||
{
|
||||
var token = Tokens[i];
|
||||
|
@ -187,6 +188,7 @@ namespace AnitomySharp
|
|||
ParserHelper.IndexOfFirstDigit(token.Content) != -1)
|
||||
{
|
||||
tokens.Add(i);
|
||||
allTokens.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,6 +230,12 @@ namespace AnitomySharp
|
|||
// "e.g. "[12]", "(2006)"
|
||||
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
|
||||
ParseNumber.SearchForLastNumber(tokens);
|
||||
}
|
||||
|
@ -235,7 +243,7 @@ namespace AnitomySharp
|
|||
/// <summary>
|
||||
/// Search for anime title
|
||||
///
|
||||
/// 搜索动画名
|
||||
/// 搜索动画名
|
||||
/// </summary>
|
||||
private void SearchForAnimeTitle()
|
||||
{
|
||||
|
@ -283,6 +291,13 @@ namespace AnitomySharp
|
|||
{
|
||||
tokenBegin = tokenBeginWithNoReleaseGroup;
|
||||
}
|
||||
// 去除纯数字标题
|
||||
// skip token with only number
|
||||
if (Regex.Match(Tokens[tokenBegin].Content, ParserNumber.RegexMatchOnlyStart + @"^[0-9]+$" + ParserNumber.RegexMatchOnlyEnd).Success)
|
||||
{
|
||||
tokenBegin = tokenBeginWithNoReleaseGroup;
|
||||
}
|
||||
|
||||
skippedPreviousGroup = true;
|
||||
} while (Token.InListRange(tokenBegin, Tokens));
|
||||
}
|
||||
|
@ -398,7 +413,7 @@ namespace AnitomySharp
|
|||
{
|
||||
var token = Tokens[i];
|
||||
/** 跳过括号标记类型的标记 */
|
||||
if (token.Category == Token.TokenCategory.Bracket) continue;
|
||||
if (token.Category != Token.TokenCategory.Unknown) continue;
|
||||
var tokenContent = token.Content;
|
||||
|
||||
// e.g. "2016-17"
|
||||
|
@ -408,13 +423,21 @@ namespace AnitomySharp
|
|||
{
|
||||
tokenContent = tokenContent.Split(match.Groups[2].Value)[0];
|
||||
}
|
||||
// add newtype e.g. "2021 OVA"
|
||||
if (token.Category != Token.TokenCategory.Unknown || !StringHelper.IsNumericString(tokenContent) ||
|
||||
!(ParseHelper.IsTokenContainAnimeType(i) ^ ParseHelper.IsTokenIsolated(i)))
|
||||
|
||||
if (!StringHelper.IsNumericString(tokenContent))
|
||||
{
|
||||
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);
|
||||
|
||||
// Anime year
|
||||
|
@ -422,7 +445,7 @@ namespace AnitomySharp
|
|||
{
|
||||
if (Empty(Element.ElementCategory.ElementAnimeYear))
|
||||
{
|
||||
Elements.Add(new Element(Element.ElementCategory.ElementAnimeYear, token.Content));
|
||||
Elements.Add(new Element(Element.ElementCategory.ElementAnimeYear, tokenContent));
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -119,6 +119,29 @@ namespace AnitomySharp
|
|||
if (string.IsNullOrEmpty(str)) return "";
|
||||
return Ordinals.TryGetValue(str, out var foundString) ? foundString : "";
|
||||
}
|
||||
/// <summary>
|
||||
/// 转换原始值中的全角数字
|
||||
/// 1234567890
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetNumberFromFullWidth(string str)
|
||||
{
|
||||
string output = str;
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
if (char.IsDigit(str[i]))
|
||||
{
|
||||
int fullwidthDigit = (int)str[i];
|
||||
if (fullwidthDigit >= 65296 && fullwidthDigit <= 65305)
|
||||
{
|
||||
int halfwidthDigit = fullwidthDigit - 65248;
|
||||
output = output.Replace(str[i], (char)halfwidthDigit);
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
|
||||
|
@ -235,7 +258,7 @@ namespace AnitomySharp
|
|||
/// <summary>
|
||||
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces).
|
||||
///
|
||||
/// 判断当前位置标记(token)是否孤立,即是否被括号包裹
|
||||
/// 判断当前位置标记(token)是否孤立,是否被括号包裹
|
||||
/// </summary>
|
||||
/// <param name="pos"></param>
|
||||
/// <returns></returns>
|
||||
|
@ -246,6 +269,20 @@ namespace AnitomySharp
|
|||
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||
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>
|
||||
/// Returns whether or not a token at the current <c>pos+1</c> is ElementAnimeType.
|
||||
|
@ -254,13 +291,40 @@ namespace AnitomySharp
|
|||
/// </summary>
|
||||
/// <param name="pos"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsTokenContainAnimeType(int pos)
|
||||
public bool IsNextTokenContainAnimeType(int pos)
|
||||
{
|
||||
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
|
||||
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
|
||||
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 判断当前标记(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>
|
||||
/// Finds and sets the anime season keyword.
|
||||
|
@ -394,4 +458,4 @@ namespace AnitomySharp
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -371,7 +371,7 @@ namespace AnitomySharp
|
|||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchSeasonAndEpisodePattern(string word, Token token)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
var match = Regex.Match(word, regexPattern);
|
||||
if (!match.Success) return false;
|
||||
|
||||
|
@ -412,7 +412,7 @@ namespace AnitomySharp
|
|||
_parser.Tokens.Insert(foundIdx,
|
||||
new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix));
|
||||
|
||||
return true;
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
@ -513,7 +513,8 @@ namespace AnitomySharp
|
|||
return true;
|
||||
}
|
||||
|
||||
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])";
|
||||
// 全角数字:\uFF10-\uFF19
|
||||
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([回集話话幕夜発縛])";
|
||||
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
|
@ -522,11 +523,33 @@ namespace AnitomySharp
|
|||
{
|
||||
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
|
||||
}
|
||||
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
|
||||
SetEpisodeNumber(episodeNumber, token, false);
|
||||
return true;
|
||||
}
|
||||
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([期章巻卷])";
|
||||
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
var episodeNumber = match.Groups[2].Value;
|
||||
if (!StringHelper.IsNumericString(episodeNumber))
|
||||
{
|
||||
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
|
||||
}
|
||||
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
|
||||
SetEpisodeNumber(episodeNumber, token, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
regexPattern = @"(vol|EPISODE|ACT|scene|ep|volume|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
|
||||
regexPattern = @"(EPISODE|ACT|scene|ep|screen|voice|case|menu|rail|round|game|page|collection|cage|office|doll|Princess)([ \.\-_])([0-9]+)";
|
||||
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
var episodeNumber = match.Groups[3].Value;
|
||||
SetEpisodeNumber(episodeNumber, token, false);
|
||||
return true;
|
||||
}
|
||||
regexPattern = @"(vol|volume)([ \.\-_])([0-9]+)";
|
||||
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
|
@ -698,6 +721,50 @@ namespace AnitomySharp
|
|||
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>
|
||||
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
|
||||
///
|
||||
|
@ -730,10 +797,7 @@ namespace AnitomySharp
|
|||
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));
|
||||
SetEpisodeNumber(list[0].Content, list[0], false);
|
||||
|
@ -743,6 +807,50 @@ namespace AnitomySharp
|
|||
|
||||
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>
|
||||
/// Searches for the last number token in a list of <c>tokens</c>
|
||||
|
@ -789,4 +897,4 @@ namespace AnitomySharp
|
|||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,6 +120,17 @@ namespace AnitomySharp
|
|||
{
|
||||
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>
|
||||
/// Returns the int value of the <c>str</c>; 0 otherwise.
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
[TestMethod]
|
||||
public void TestSearch()
|
||||
{
|
||||
var keyword = "V字仇杀队";
|
||||
var keyword = "声生不息";
|
||||
var api = new DoubanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
||||
{
|
||||
PreferredMetadataLanguage = "zh",
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb } }
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
|
||||
};
|
||||
var httpClientFactory = new DefaultHttpClientFactory();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
|
|
|
@ -78,7 +78,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
[TestMethod]
|
||||
public void TestGetMetadataByTMDB()
|
||||
{
|
||||
var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary<string, string> { { Plugin.ProviderId, MetaSource.Tmdb }, { MetadataProvider.Tmdb.ToString(), "945664" } } };
|
||||
var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary<string, string> { { Plugin.ProviderId, MetaSource.Tmdb.ToString() }, { MetadataProvider.Tmdb.ToString(), "945664" } } };
|
||||
var httpClientFactory = new DefaultHttpClientFactory();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||
|
|
|
@ -86,7 +86,6 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
Assert.AreEqual(parseResult.Name, "秒速5厘米");
|
||||
Assert.AreEqual(parseResult.Year, null);
|
||||
|
||||
|
||||
// 标题加年份
|
||||
fileName = "V字仇杀队 (2006)";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
|
@ -94,6 +93,12 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
Assert.AreEqual(parseResult.Name, "V字仇杀队");
|
||||
Assert.AreEqual(parseResult.Year, 2006);
|
||||
|
||||
fileName = "逃学威龙2 (1992)";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
Assert.AreEqual(parseResult.ChineseName, null);
|
||||
Assert.AreEqual(parseResult.Name, "逃学威龙2");
|
||||
Assert.AreEqual(parseResult.Year, 1992);
|
||||
|
||||
|
||||
// anime
|
||||
fileName = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3].mp4";
|
||||
|
@ -162,43 +167,64 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
[TestMethod]
|
||||
public void TestEposideParse()
|
||||
{
|
||||
// 普通数字
|
||||
var fileName = "03.mp4";
|
||||
var parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "03");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 3);
|
||||
|
||||
fileName = "03 4K.mp4";
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "03");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 3);
|
||||
|
||||
// 混合中英文
|
||||
var fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
|
||||
var parseResult = NameParser.Parse(fileName);
|
||||
fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.ChineseName, "新世界");
|
||||
Assert.AreEqual(parseResult.Name, "New World");
|
||||
Assert.AreEqual(parseResult.Year, 2013);
|
||||
|
||||
// 只英文
|
||||
// 只英文 S01E01
|
||||
fileName = "She-Hulk.Attorney.At.Law.S01E01.1080p.WEBRip.x265-RARBG";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "She-Hulk Attorney At Law");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||
|
||||
// 测试 SXXEPXX 格式
|
||||
fileName = "神探狄仁杰2 Detective.Dee.Ⅱ.S02EP02.2006.2160p.WEB-DL.x264.AAC-HQC";
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.ChineseName, "神探狄仁杰2");
|
||||
Assert.AreEqual(parseResult.Name, "Detective Dee Ⅱ");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, 2);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||
|
||||
|
||||
// 日文
|
||||
fileName = "プロポーズ大作戦Ep05_x264.mp4";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "プロポーズ大作戦Ep05");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 5);
|
||||
|
||||
fileName = "[01] [ANK-Raws] あっちこっち 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC)";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "あっちこっち 01");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||
|
||||
// 只中文
|
||||
fileName = "齊天大聖 第02集";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "齊天大聖 第02集");
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "齊天大聖");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||
|
||||
fileName = "齊天大聖 第 02 期";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "齊天大聖");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||
|
@ -206,38 +232,40 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
|
||||
// anime
|
||||
fileName = "[YYDM-11FANS][THERMAE_ROMAE][02][BDRIP][720P][X264-10bit_AAC][7FF2269F]";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "THERMAE ROMAE");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||
|
||||
// anime带季数
|
||||
fileName = "[WMSUB][Detective Conan - Zero‘s Tea Time ][S01][E06][BIG5][1080P].mp4";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "Detective Conan - Zero‘s Tea Time");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 6);
|
||||
|
||||
fileName = "[KTXP][Machikado_Mazoku_S2][01][BIG5][1080p]";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "Machikado Mazoku");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||
|
||||
fileName = "[異域字幕組][她和她的貓 - Everything Flows -][She and Her Cat - Everything Flows -][01][720p][繁體]";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.AreEqual(parseResult.Name, "她和她的貓 - Everything Flows");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||
|
||||
// anime特典
|
||||
fileName = "[KissSub][Steins;Gate][SP][GB_BIG5_JP][BDrip][1080P][HEVC] 边界曲面的缺失之环";
|
||||
parseResult = NameParser.Parse(fileName);
|
||||
parseResult = NameParser.ParseEpisode(fileName);
|
||||
Assert.IsTrue(parseResult.IsSpecial);
|
||||
Assert.AreEqual(parseResult.Name, "边界曲面的缺失之环");
|
||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||
Assert.AreEqual(parseResult.IndexNumber, null);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -64,7 +64,11 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
var imdbApi = new ImdbApi(loggerFactory);
|
||||
|
||||
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
|
||||
|
||||
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/冰与火之歌S01-S08.Game.of.Thrones.1080p.Blu-ray.x265.10bit.AC3/冰与火之歌S2.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3");
|
||||
Assert.AreEqual(result, 2);
|
||||
|
||||
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
|
||||
Assert.AreEqual(result, 2);
|
||||
|
||||
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
||||
{
|
||||
PreferredMetadataLanguage = "zh",
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb } }
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
|
||||
};
|
||||
var httpClientFactory = new DefaultHttpClientFactory();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
[TestMethod]
|
||||
public void TestGetMetadata()
|
||||
{
|
||||
var info = new SeriesInfo() { Name = "一年一度喜剧大赛" };
|
||||
var info = new SeriesInfo() { Name = "天下长河" };
|
||||
var httpClientFactory = new DefaultHttpClientFactory();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Find;
|
||||
using TMDbLib.Objects.Languages;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Test
|
||||
|
@ -124,5 +125,27 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestFindByExternalId()
|
||||
{
|
||||
var api = new TmdbApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await api.FindByExternalIdAsync("tt5924366", FindExternalSource.Imdb, "zh", CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
Assert.IsNotNull(result);
|
||||
TestContext.WriteLine(result.ToJson());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TestContext.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,22 +52,15 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
|
||||
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
|
||||
Regex regNameMath = new Regex(@"(.+第\w季|[\w\uff1a\uff01\uff0c\u00b7]+)\s*(.*)", RegexOptions.Compiled);
|
||||
Regex regRole = new Regex(@"\([饰|配]?\s*?(.+?)\)", RegexOptions.Compiled);
|
||||
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
|
||||
Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regConstellation = new Regex(@"星座: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regBirthdate = new Regex(@"出生日期: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regLifedate = new Regex(@"生卒日期: \n(.+?) 至 (.+)", RegexOptions.Compiled);
|
||||
Regex regBirthplace = new Regex(@"出生地: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regCelebrityRole = new Regex(@"职业: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regNickname = new Regex(@"更多外文名: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", RegexOptions.Compiled);
|
||||
Regex regLifedate = new Regex(@"(.+?) 至 (.+)", RegexOptions.Compiled);
|
||||
Regex regHtmlTag = new Regex(@"<.?>", RegexOptions.Compiled);
|
||||
Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
|
||||
// 匹配除了换行符之外所有空白
|
||||
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
|
||||
Regex regPhotoId = new Regex(@"/photo/(\d+?)/", RegexOptions.Compiled);
|
||||
Regex regLoginName = new Regex(@"<div[^>]*?db-usr-profile[^>]*?>[\w\W]*?<h1>([^>]*?)<", RegexOptions.Compiled);
|
||||
|
||||
// 默认200毫秒请求1次
|
||||
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200));
|
||||
|
@ -89,7 +82,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
var handler = new HttpClientHandlerEx();
|
||||
this._cookieContainer = handler.CookieContainer;
|
||||
httpClient = new HttpClient(handler);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(20);
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
|
||||
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
|
||||
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
|
||||
|
@ -150,6 +143,18 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<List<DoubanSubject>> SearchMovieAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await this.SearchAsync(keyword, cancellationToken).ConfigureAwait(false);
|
||||
return result.Where(x => x.Category == "电影").ToList();
|
||||
}
|
||||
|
||||
public async Task<List<DoubanSubject>> SearchTVAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await this.SearchAsync(keyword, cancellationToken).ConfigureAwait(false);
|
||||
return result.Where(x => x.Category == "电视剧").ToList();
|
||||
}
|
||||
|
||||
public async Task<List<DoubanSubject>> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<DoubanSubject>();
|
||||
|
@ -185,6 +190,12 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
foreach (var movieElement in movieElements)
|
||||
{
|
||||
|
||||
var ratingStr = movieElement.GetText("div.rating-info") ?? string.Empty;
|
||||
if (ratingStr.Contains("尚未播出"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rating = movieElement.GetText("div.rating-info>.rating_nums") ?? "0";
|
||||
var img = movieElement.GetAttr("a.nbg>img", "src") ?? string.Empty;
|
||||
var oncick = movieElement.GetAttr("div.title a", "onclick") ?? string.Empty;
|
||||
|
@ -500,48 +511,64 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
var contentNode = doc.QuerySelector("#content");
|
||||
if (contentNode != null)
|
||||
{
|
||||
var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
|
||||
var nameStr = contentNode.GetText("h1") ?? string.Empty;
|
||||
var name = this.ParseCelebrityName(nameStr);
|
||||
var englishName = nameStr.Replace(name, "").Trim();
|
||||
celebrity.Img = contentNode.GetAttr("img.avatar", "src") ?? string.Empty;
|
||||
var nameStr = contentNode.GetText("h1.subject-name") ?? string.Empty;
|
||||
celebrity.Name = this.ParseCelebrityName(nameStr);
|
||||
celebrity.EnglishName = nameStr.Replace(celebrity.Name, "").Trim();
|
||||
|
||||
var intro = contentNode.GetText("#intro span.all") ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(intro))
|
||||
|
||||
var family = string.Empty;
|
||||
var propertyNodes = contentNode.QuerySelectorAll("ul.subject-property>li");
|
||||
foreach (var li in propertyNodes)
|
||||
{
|
||||
intro = contentNode.GetText("#intro div.bd") ?? string.Empty;
|
||||
}
|
||||
var info = contentNode.GetText("div.info") ?? string.Empty;
|
||||
var gender = info.GetMatchGroup(this.regGender);
|
||||
var constellation = info.GetMatchGroup(this.regConstellation);
|
||||
var birthdate = info.GetMatchGroup(this.regBirthdate);
|
||||
|
||||
// 生卒日期
|
||||
var enddate = string.Empty;
|
||||
var match = this.regLifedate.Match(info);
|
||||
if (match.Success && match.Groups.Count > 2)
|
||||
{
|
||||
birthdate = match.Groups[1].Value.Trim();
|
||||
enddate = match.Groups[2].Value.Trim();
|
||||
var label = li.GetText("span.label") ?? string.Empty;
|
||||
var value = li.GetText("span.value") ?? string.Empty;
|
||||
switch (label)
|
||||
{
|
||||
case "性别:":
|
||||
celebrity.Gender = value;
|
||||
break;
|
||||
case "星座:":
|
||||
celebrity.Constellation = value;
|
||||
break;
|
||||
case "出生日期:":
|
||||
celebrity.Birthdate = value;
|
||||
break;
|
||||
case "去世日期:":
|
||||
celebrity.Enddate = value;
|
||||
break;
|
||||
case "生卒日期:":
|
||||
var match = this.regLifedate.Match(value);
|
||||
if (match.Success && match.Groups.Count > 2)
|
||||
{
|
||||
celebrity.Birthdate = match.Groups[1].Value.Trim();
|
||||
celebrity.Enddate = match.Groups[2].Value.Trim();
|
||||
}
|
||||
break;
|
||||
case "出生地:":
|
||||
celebrity.Birthplace = value;
|
||||
break;
|
||||
case "职业:":
|
||||
celebrity.Role = value;
|
||||
break;
|
||||
case "更多外文名:":
|
||||
celebrity.NickName = value;
|
||||
break;
|
||||
case "家庭成员:":
|
||||
family = value;
|
||||
break;
|
||||
case "IMDb编号:":
|
||||
celebrity.Imdb = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var birthplace = info.GetMatchGroup(this.regBirthplace);
|
||||
var role = info.GetMatchGroup(this.regCelebrityRole);
|
||||
var nickname = info.GetMatchGroup(this.regNickname);
|
||||
var family = info.GetMatchGroup(this.regFamily);
|
||||
var imdb = info.GetMatchGroup(this.regCelebrityImdb);
|
||||
|
||||
celebrity.Img = img;
|
||||
celebrity.Gender = gender;
|
||||
celebrity.Birthdate = birthdate;
|
||||
celebrity.Enddate = enddate;
|
||||
celebrity.NickName = nickname;
|
||||
celebrity.EnglishName = englishName;
|
||||
celebrity.Imdb = imdb;
|
||||
celebrity.Birthplace = birthplace;
|
||||
celebrity.Name = name;
|
||||
// 保留段落关系,把段落替换为换行符
|
||||
var intro = contentNode.GetHtml("section.subject-intro div.content") ?? string.Empty;
|
||||
intro = regHtmlTag.Replace(intro.Replace("</p>", "\n"), "");
|
||||
celebrity.Intro = formatOverview(intro);
|
||||
celebrity.Constellation = constellation;
|
||||
celebrity.Role = role;
|
||||
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
|
||||
return celebrity;
|
||||
}
|
||||
|
@ -796,6 +823,27 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
return true;
|
||||
}
|
||||
|
||||
public async Task<DoubanLoginInfo> GetLoginInfoAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var loginInfo = new DoubanLoginInfo();
|
||||
try
|
||||
{
|
||||
var url = "https://www.douban.com/mine/";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
var requestUrl = response.RequestMessage?.RequestUri?.ToString();
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var loginName = this.Match(body, regLoginName).Trim();
|
||||
loginInfo.Name = loginName;
|
||||
loginInfo.IsLogined = !(requestUrl == null || requestUrl.Contains("accounts.douban.com") || requestUrl.Contains("login") || requestUrl.Contains("sec.douban.com"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this._logger.LogError(ex, "GetLoginInfoAsync error.");
|
||||
}
|
||||
|
||||
return loginInfo;
|
||||
}
|
||||
|
||||
protected async Task LimitRequestFrequently()
|
||||
{
|
||||
if (IsEnableAvoidRiskControl())
|
||||
|
|
|
@ -39,7 +39,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
var config = Plugin.Instance?.Configuration;
|
||||
var apiKey = string.IsNullOrEmpty(config?.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
|
||||
var host = string.IsNullOrEmpty(config?.TmdbHost) ? DEFAULT_API_HOST : config.TmdbHost;
|
||||
_tmDbClient = new TMDbClient(apiKey, true, host, null, config.GetTmdbWebProxy());
|
||||
_tmDbClient = new TMDbClient(apiKey, true, host, null, config?.GetTmdbWebProxy());
|
||||
_tmDbClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
// Not really interested in NotFoundException
|
||||
_tmDbClient.ThrowApiExceptions = false;
|
||||
|
@ -597,6 +597,16 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
|
||||
}
|
||||
|
||||
public string? GetLogoUrl(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.LogoSizes[^1], filePath).ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
@ -24,6 +24,10 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||
/// </summary>
|
||||
public bool EnableDoubanAvoidRiskControl { get; set; } = false;
|
||||
/// <summary>
|
||||
/// 豆瓣海报使用大图
|
||||
/// </summary>
|
||||
public bool EnableDoubanLargePoster { get; set; } = false;
|
||||
/// <summary>
|
||||
/// 豆瓣背景图使用原图
|
||||
/// </summary>
|
||||
public bool EnableDoubanBackdropRaw { get; set; } = false;
|
||||
|
@ -46,6 +50,12 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||
/// 启用tmdb获取背景图
|
||||
/// </summary>
|
||||
public bool EnableTmdbBackdrop { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用tmdb获取商标
|
||||
/// </summary>
|
||||
public bool EnableTmdbLogo { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否获取电影系列信息
|
||||
/// </summary>
|
||||
|
|
|
@ -28,8 +28,8 @@
|
|||
</legend>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
|
||||
id="login_invalid"
|
||||
style="color: red; margin-left: 8px; display: none;">(已失效)</span></label>
|
||||
id="login_msg"
|
||||
style="margin-left: 8px; display: none;"></span></label>
|
||||
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
|
||||
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
|
||||
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
|
||||
|
@ -54,6 +54,13 @@
|
|||
可为空,填写jellyfin访问域名(有端口时要加上端口),只有使用了Nginx代理、Docker部署或启用了HTTPS,并且豆瓣图片无法显示时,才需要填写
|
||||
</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">
|
||||
<label class="emby-checkbox-label" for="EnableDoubanBackdropRaw">
|
||||
<input id="EnableDoubanBackdropRaw" name="EnableDoubanBackdropRaw" type="checkbox"
|
||||
|
@ -91,6 +98,14 @@
|
|||
</label>
|
||||
<div class="fieldDescription">勾选后,当影片在豆瓣找不到背景图时,改使用TheMovieDb的补全</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="EnableTmdbLogo">
|
||||
<input id="EnableTmdbLogo" name="EnableTmdbLogo" type="checkbox"
|
||||
is="emby-checkbox" />
|
||||
<span>从TheMovieDb获取商标</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,使用TheMovieDb的商标图片补全</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="EnableTmdbCollection">
|
||||
<input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox"
|
||||
|
@ -165,11 +180,13 @@
|
|||
document.querySelector('#DoubanCookies').value = config.DoubanCookies;
|
||||
document.querySelector('#DoubanImageProxyBaseUrl').value = config.DoubanImageProxyBaseUrl;
|
||||
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
|
||||
document.querySelector('#EnableDoubanLargePoster').checked = config.EnableDoubanLargePoster;
|
||||
document.querySelector('#EnableDoubanBackdropRaw').checked = config.EnableDoubanBackdropRaw;
|
||||
|
||||
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
|
||||
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
|
||||
document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop;
|
||||
document.querySelector('#EnableTmdbLogo').checked = config.EnableTmdbLogo;
|
||||
document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection;
|
||||
document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating;
|
||||
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
|
||||
|
@ -193,11 +210,13 @@
|
|||
config.DoubanCookies = document.querySelector('#DoubanCookies').value;
|
||||
config.DoubanImageProxyBaseUrl = document.querySelector('#DoubanImageProxyBaseUrl').value;
|
||||
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
|
||||
config.EnableDoubanLargePoster = document.querySelector('#EnableDoubanLargePoster').checked;
|
||||
config.EnableDoubanBackdropRaw = document.querySelector('#EnableDoubanBackdropRaw').checked;
|
||||
|
||||
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
|
||||
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
|
||||
config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked;
|
||||
config.EnableTmdbLogo = document.querySelector('#EnableTmdbLogo').checked;
|
||||
config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked;
|
||||
config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked;
|
||||
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
|
||||
|
@ -221,6 +240,7 @@
|
|||
changeProxyDisplay();
|
||||
});
|
||||
|
||||
|
||||
function changeProxyDisplay() {
|
||||
let proxyType = document.querySelector('#TmdbProxyType').value;
|
||||
if (proxyType) {
|
||||
|
@ -233,16 +253,16 @@
|
|||
function checkDoubanLogin() {
|
||||
let cookie = document.querySelector('#DoubanCookies').value
|
||||
if (!cookie || !$.trim(cookie)) {
|
||||
$('#login_invalid').hide();
|
||||
$('#login_msg').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
|
||||
if (resp && resp.code != 1) {
|
||||
$('#login_invalid').show();
|
||||
$('#login_msg').css("color", "red").text('(已失效)').show();
|
||||
} else {
|
||||
$('#login_invalid').hide();
|
||||
$('#login_msg').css("color", "").text('(已生效)').show();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -77,10 +77,11 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
|||
[HttpGet]
|
||||
public async Task<ApiResult> CheckDoubanLogin()
|
||||
{
|
||||
var isLogin = await _doubanApi.CheckLoginAsync(CancellationToken.None);
|
||||
return new ApiResult(isLogin ? 1 : 0, isLogin ? "logined" : "not login");
|
||||
var loginInfo = await this._doubanApi.GetLoginInfoAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
return new ApiResult(loginInfo.IsLogined ? 1 : 0, loginInfo.Name);
|
||||
}
|
||||
|
||||
|
||||
private HttpClient GetHttpClient()
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
|
|
@ -20,6 +20,17 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return null;
|
||||
}
|
||||
|
||||
public static string? GetHtml(this IElement el, string css)
|
||||
{
|
||||
var node = el.QuerySelector(css);
|
||||
if (node != null)
|
||||
{
|
||||
return node.Html().Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
|
||||
{
|
||||
var node = el.QuerySelector(css);
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.TV;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Core
|
||||
|
@ -107,23 +108,32 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
}
|
||||
}
|
||||
|
||||
// 假如Anitomy解析不到year,尝试使用jellyfin默认parser,看能不能解析成功
|
||||
// 假如 Anitomy 解析不到 year,尝试使用 jellyfin 默认 parser,看能不能解析成功
|
||||
if (parseResult.Year == null && !isAnime)
|
||||
{
|
||||
var nativeParseResult = ParseMovie(fileName);
|
||||
var nativeParseResult = ParseMovieByDefault(fileName);
|
||||
if (nativeParseResult.Year != null)
|
||||
{
|
||||
parseResult = nativeParseResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 假如 Anitomy 解析不到集数,判断 name 是否是数字集号
|
||||
if (parseResult.IndexNumber is null && isEpisode)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parseResult.Name) && parseResult.Name.IsNumericString())
|
||||
{
|
||||
parseResult.IndexNumber = parseResult.Name.ToInt();
|
||||
}
|
||||
}
|
||||
|
||||
// 修复纯中文集数/特殊标识集数
|
||||
if (parseResult.IndexNumber is null)
|
||||
{
|
||||
parseResult.IndexNumber = ParseChineseOrSpecialIndexNumber(fileName);
|
||||
}
|
||||
|
||||
// 解析不到title时,或解析出多个title时,使用默认名
|
||||
// 解析不到 title 时,或解析出多个 title 时,使用默认名
|
||||
if (string.IsNullOrEmpty(parseResult.Name))
|
||||
{
|
||||
parseResult.Name = fileName;
|
||||
|
@ -132,6 +142,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return parseResult;
|
||||
}
|
||||
|
||||
public static ParseNameResult ParseEpisode(string fileName)
|
||||
{
|
||||
return Parse(fileName, true);
|
||||
}
|
||||
|
||||
private static string CleanName(string name)
|
||||
{
|
||||
// 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉
|
||||
|
@ -143,8 +158,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return name.Replace(".", " ").Trim();
|
||||
}
|
||||
|
||||
// emby原始电影解析
|
||||
public static ParseNameResult ParseMovie(string fileName)
|
||||
/// <summary>
|
||||
/// emby原始电影解析
|
||||
/// </summary>
|
||||
public static ParseNameResult ParseMovieByDefault(string fileName)
|
||||
{
|
||||
// 默认解析器会错误把分辨率当年份,先删除
|
||||
fileName = resolutionReg.Replace(fileName, "");
|
||||
|
@ -165,6 +182,18 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return parseResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// emby原始剧集解析
|
||||
/// </summary>
|
||||
public static EpisodePathParserResult ParseEpisodeByDefault(string fileName)
|
||||
{
|
||||
// EpisodePathParser需要路径信息, 这里添加一个分隔符模拟路径
|
||||
var path = Path.DirectorySeparatorChar + fileName;
|
||||
var nameOptions = new Emby.Naming.Common.NamingOptions();
|
||||
return new EpisodePathParser(nameOptions)
|
||||
.Parse(path, false);
|
||||
}
|
||||
|
||||
|
||||
private static int ParseYear(string val)
|
||||
{
|
||||
|
@ -217,15 +246,21 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return null;
|
||||
}
|
||||
|
||||
public static bool IsSpecialDirectory(string path)
|
||||
public static bool IsSpecialDirectory(string path, bool isDirectory = false)
|
||||
{
|
||||
var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
|
||||
return folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典");
|
||||
if (isDirectory) {
|
||||
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
|
||||
}
|
||||
return folder == "SP" || folder == "SPS" || folder == "SPECIALS" || folder.Contains("特典");
|
||||
}
|
||||
|
||||
public static bool IsExtraDirectory(string path)
|
||||
public static bool IsExtraDirectory(string path, bool isDirectory = false)
|
||||
{
|
||||
var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
|
||||
if (isDirectory) {
|
||||
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
|
||||
}
|
||||
return folder == "EXTRA"
|
||||
|| folder == "MENU"
|
||||
|| folder == "MENUS"
|
||||
|
|
|
@ -78,5 +78,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static bool IsNumericString(this string str)
|
||||
{
|
||||
return str.All(char.IsDigit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release' or '$(Configuration)'=='Debug'">
|
||||
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release'">
|
||||
<PropertyGroup>
|
||||
<DoILRepack>false</DoILRepack>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
|
||||
<GenerateDocumentationFile>False</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
|
@ -21,11 +21,14 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.0.1" />
|
||||
<PackageReference Include="ILRepack.Lib.MSBuild.Minor" Version="2.1.19-alpha.2" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
|
||||
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.32">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.9.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.9.0" />
|
||||
<PackageReference Include="RateLimiter" Version="2.2.0" />
|
||||
<PackageReference Include="TMDbLib" Version="2.0.0" />
|
||||
<PackageReference Include="TMDbLib" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model
|
||||
{
|
||||
public class DoubanLoginInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool IsLogined { get; set; }
|
||||
}
|
||||
}
|
|
@ -93,6 +93,15 @@ namespace Jellyfin.Plugin.MetaShark.Model
|
|||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string ImgLarge
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Img.Replace("s_ratio_poster", "l");
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string[] Genres
|
||||
{
|
||||
|
@ -155,6 +164,16 @@ namespace Jellyfin.Plugin.MetaShark.Model
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string ImgMiddle
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Img.Replace("/raw/", "/m/").Replace("/s_ratio_poster/", "/m/");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class DoubanPhoto
|
||||
|
|
|
@ -6,9 +6,34 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model
|
||||
{
|
||||
public static class MetaSource
|
||||
|
||||
public enum MetaSource
|
||||
{
|
||||
public const string Douban = "douban";
|
||||
public const string Tmdb = "tmdb";
|
||||
Douban,
|
||||
Tmdb,
|
||||
None
|
||||
}
|
||||
|
||||
public static class MetaSourceExtensions
|
||||
{
|
||||
public static MetaSource ToMetaSource(this string? str)
|
||||
{
|
||||
if (str == null)
|
||||
{
|
||||
return MetaSource.None;
|
||||
}
|
||||
|
||||
if (str.ToLower().StartsWith("douban"))
|
||||
{
|
||||
return MetaSource.Douban;
|
||||
}
|
||||
|
||||
if (str.ToLower().StartsWith("tmdb"))
|
||||
{
|
||||
return MetaSource.Tmdb;
|
||||
}
|
||||
|
||||
return MetaSource.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ using TMDbLib.Objects.General;
|
|||
using Jellyfin.Plugin.MetaShark.Configuration;
|
||||
using Jellyfin.Plugin.MetaShark.Core;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||
{
|
||||
|
@ -50,6 +51,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled);
|
||||
protected Regex regSeasonNameSuffix = new Regex(@"\s第[0-9一二三四五六七八九十]+?季$|\sSeason\s\d+?$|(?<![0-9a-zA-Z])\d$", RegexOptions.Compiled);
|
||||
protected Regex regDoubanIdAttribute = new Regex(@"\[(?:douban|doubanid)-(\d+?)\]", RegexOptions.Compiled);
|
||||
|
||||
protected PluginConfiguration config
|
||||
{
|
||||
|
@ -107,11 +109,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var fileName = GetOriginalFileName(info);
|
||||
// 从文件名属性格式获取,如[douban-12345]或[doubanid-12345]
|
||||
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
|
||||
if (!string.IsNullOrWhiteSpace(doubanId))
|
||||
{
|
||||
this.Log($"Found douban [id] by attr: {doubanId}");
|
||||
return doubanId;
|
||||
}
|
||||
var parseResult = NameParser.Parse(fileName);
|
||||
var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name;
|
||||
info.Year = parseResult.Year; // 默认parser对anime年份会解析出错,以anitomy为准
|
||||
|
||||
|
||||
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
|
||||
List<DoubanSubject> result;
|
||||
DoubanSubject? item;
|
||||
|
@ -125,13 +133,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
|
||||
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
|
||||
return item.Sid;
|
||||
}
|
||||
item = result.Where(x => x.Year == info.Year).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
|
||||
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
|
||||
return item.Sid;
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +177,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
item = result.Where(x => x.Category == cat).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid})");
|
||||
this.Log($"Found douban [id] by first match: {item.Name}({item.Sid})");
|
||||
return item.Sid;
|
||||
}
|
||||
|
||||
|
@ -319,6 +327,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
break;
|
||||
}
|
||||
|
||||
this.Log($"Not found tmdb id by [name]: {name} [year]: {year}");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -350,11 +359,24 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
||||
return $"{tmdbId}";
|
||||
}
|
||||
if (findResult?.TvEpisode != null && findResult.TvEpisode.Count > 0)
|
||||
{
|
||||
var tmdbId = findResult.TvEpisode[0].ShowId;
|
||||
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
||||
return $"{tmdbId}";
|
||||
}
|
||||
if (findResult?.TvSeason != null && findResult.TvSeason.Count > 0)
|
||||
{
|
||||
var tmdbId = findResult.TvSeason[0].ShowId;
|
||||
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
||||
return $"{tmdbId}";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.Log($"Not found tmdb id by imdb id: {imdb}");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -380,10 +402,11 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
public int? GuessSeasonNumberByDirectoryName(string path)
|
||||
{
|
||||
// TODO: 有时series name中会带有季信息
|
||||
// 当没有season级目录时,path为空,直接返回
|
||||
// TODO: 有时 series name 中会带有季信息
|
||||
// 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber,这时 path 为空,直接返回
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
this.Log($"Season path is empty!");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -394,6 +417,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return null;
|
||||
}
|
||||
|
||||
// 中文季名
|
||||
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
|
||||
var match = regSeason.Match(fileName);
|
||||
if (match.Success && match.Groups.Count > 1)
|
||||
|
@ -410,12 +434,26 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
}
|
||||
|
||||
// SXX 季名
|
||||
regSeason = new Regex(@"(?<![a-z])S(\d\d?)(?![0-9a-z])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
match = regSeason.Match(fileName);
|
||||
if (match.Success && match.Groups.Count > 1)
|
||||
{
|
||||
var seasonNumber = match.Groups[1].Value.ToInt();
|
||||
if (seasonNumber > 0)
|
||||
{
|
||||
this.Log($"Found season number of filename: {fileName} seasonNumber: {seasonNumber}");
|
||||
return seasonNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 动漫季特殊命名
|
||||
var seasonNameMap = new Dictionary<string, int>() {
|
||||
{@"[ ._](I|1st|S01|S1)[ ._]", 1},
|
||||
{@"[ ._](II|2nd|S02|S2)[ ._]", 2},
|
||||
{@"[ ._](III|3rd|S03|S3)[ ._]", 3},
|
||||
{@"[ ._](IIII|4th|S04|S4)[ ._]", 3},
|
||||
{@"[ ._](I|1st)[ ._]", 1},
|
||||
{@"[ ._](II|2nd)[ ._]", 2},
|
||||
{@"[ ._](III|3rd)[ ._]", 3},
|
||||
{@"[ ._](IIII|4th)[ ._]", 3},
|
||||
};
|
||||
|
||||
foreach (var entry in seasonNameMap)
|
||||
|
@ -471,53 +509,32 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
protected string GetLocalProxyImageUrl(string url)
|
||||
{
|
||||
var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl();
|
||||
if (!string.IsNullOrEmpty(config.DoubanImageProxyBaseUrl))
|
||||
var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl))
|
||||
{
|
||||
baseUrl = config.DoubanImageProxyBaseUrl.TrimEnd('/');
|
||||
}
|
||||
if (!string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
var encodedUrl = HttpUtility.UrlEncode(url);
|
||||
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
|
||||
}
|
||||
|
||||
return this.GetProxyImageUrl(url);
|
||||
var encodedUrl = HttpUtility.UrlEncode(url);
|
||||
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
|
||||
}
|
||||
|
||||
private string GetBaseUrl()
|
||||
{
|
||||
// 配置优先
|
||||
if (!string.IsNullOrEmpty(this.config.DoubanImageProxyBaseUrl))
|
||||
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl))
|
||||
{
|
||||
return this.config.DoubanImageProxyBaseUrl.TrimEnd('/');
|
||||
}
|
||||
|
||||
// http请求时,获取请求的host (nginx代理/docker中部署时,没配置透传host时,本方式会有问题)
|
||||
// TODO:http请求时,获取请求的host (nginx代理/docker中部署时,没配置透传host时,本方式会有问题)
|
||||
// 除自动扫描之外都会执行这里,修改图片功能图片是直接下载,不走插件图片代理处理函数,host拿不到就下载不了
|
||||
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);
|
||||
}
|
||||
|
||||
// 自动扫描刷新时,直接使用本地地址
|
||||
// 自动扫描刷新时,直接使用本地地址(127.0.0.1)
|
||||
return Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
|
||||
}
|
||||
|
||||
|
@ -573,7 +590,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
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)
|
||||
{
|
||||
|
@ -596,7 +623,47 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
}
|
||||
|
||||
protected string RemoveSeasonSubfix(string name)
|
||||
protected string? GetOriginalSeasonPath(EpisodeInfo info)
|
||||
{
|
||||
if (info.Path == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var seasonPath = Path.GetDirectoryName(info.Path);
|
||||
var item = this._libraryManager.FindByPath(seasonPath, true);
|
||||
// 没有季文件夹
|
||||
if (item is Series) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return seasonPath;
|
||||
}
|
||||
|
||||
protected bool IsVirtualSeason(EpisodeInfo info)
|
||||
{
|
||||
if (info.Path == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var seasonPath = Path.GetDirectoryName(info.Path);
|
||||
var parent = this._libraryManager.FindByPath(seasonPath, true);
|
||||
// 没有季文件夹
|
||||
if (parent is Series) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var seriesPath = Path.GetDirectoryName(seasonPath);
|
||||
var series = this._libraryManager.FindByPath(seriesPath, true);
|
||||
// 季文件夹不规范,没法识别
|
||||
if (series is Series && parent is not Season) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string RemoveSeasonSuffix(string name)
|
||||
{
|
||||
return regSeasonNameSuffix.Replace(name, "");
|
||||
}
|
||||
|
|
|
@ -44,9 +44,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
// 刷新元数据四种模式差别:
|
||||
// 自动扫描匹配:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,假如命名不规范,就会导致解析出错误值
|
||||
// 识别:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds有指定选择项的ProvinceId
|
||||
// 覆盖所有元数据:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds保留所有旧值
|
||||
// 搜索缺少的元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值
|
||||
// 覆盖所有元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值
|
||||
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} IsAutomated: {info.IsAutomated}");
|
||||
var fileName = Path.GetFileName(info.Path);
|
||||
this.Log($"GetEpisodeMetadata of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} EnableTmdb: {config.EnableTmdb}");
|
||||
var result = new MetadataResult<Episode>();
|
||||
|
||||
// 动画特典和extras处理
|
||||
|
@ -129,78 +130,72 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
/// <summary>
|
||||
/// 重新解析文件名
|
||||
/// 注意:这里修改替换ParentIndexNumber值后,会重新触发SeasonProvier的GetMetadata方法,并带上最新的季数IndexNumber
|
||||
/// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier 的 GetMetadata 方法,并带上最新的季数 IndexNumber
|
||||
/// </summary>
|
||||
public EpisodeInfo FixParseInfo(EpisodeInfo info)
|
||||
{
|
||||
// 使用AnitomySharp进行重新解析,解决anime识别错误
|
||||
// 使用 AnitomySharp 进行重新解析,解决 anime 识别错误
|
||||
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
||||
var parseResult = NameParser.Parse(fileName);
|
||||
var parseResult = NameParser.ParseEpisode(fileName);
|
||||
info.Year = parseResult.Year;
|
||||
info.Name = parseResult.ChineseName ?? parseResult.Name;
|
||||
|
||||
// 没有season级目录(即虚拟季)ParentIndexNumber默认是1,季文件夹命名不规范时,ParentIndexNumber默认是null
|
||||
if (info.ParentIndexNumber is null)
|
||||
// 文件名带有季数数据时,从文件名解析出季数进行修正
|
||||
// 修正文件名有特殊命名 SXXEPXX 时,默认解析到错误季数的问题,如神探狄仁杰 Detective.Dee.S01EP01.2006.2160p.WEB-DL.x264.AAC-HQC
|
||||
// TODO: 会导致覆盖用户手动修改元数据的季数
|
||||
if (parseResult.ParentIndexNumber.HasValue && parseResult.ParentIndexNumber > 0 && info.ParentIndexNumber != parseResult.ParentIndexNumber)
|
||||
{
|
||||
this.Log("FixSeasonNumber by anitomy. old: {0} new: {1}", info.ParentIndexNumber, parseResult.ParentIndexNumber);
|
||||
info.ParentIndexNumber = parseResult.ParentIndexNumber;
|
||||
}
|
||||
|
||||
// 修正anime命名格式导致的seasonNumber错误(从season元数据读取)
|
||||
if (info.ParentIndexNumber is null)
|
||||
// // 修正anime命名格式导致的seasonNumber错误(从season元数据读取)
|
||||
// if (info.ParentIndexNumber is null)
|
||||
// {
|
||||
// var episodeItem = this._libraryManager.FindByPath(info.Path, false);
|
||||
// var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
|
||||
// if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
|
||||
// {
|
||||
// info.ParentIndexNumber = season.IndexNumber;
|
||||
// this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 从季文件夹名称猜出 season number
|
||||
// 没有 season 级目录或部分特殊不规范命名,会变成虚拟季,ParentIndexNumber 默认设为 1
|
||||
// https://github.com/jellyfin/jellyfin/blob/926470829d91d93b4c0b22c5b8b89a791abbb434/Emby.Server.Implementations/Library/LibraryManager.cs#L2626
|
||||
var isVirtualSeason = this.IsVirtualSeason(info);
|
||||
var seasonFolderPath = this.GetOriginalSeasonPath(info);
|
||||
if (info.ParentIndexNumber is null or 1 && isVirtualSeason && seasonFolderPath != null)
|
||||
{
|
||||
var episodeItem = _libraryManager.FindByPath(info.Path, false);
|
||||
var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
|
||||
if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
|
||||
var guestSeasonNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
|
||||
if (guestSeasonNumber.HasValue && guestSeasonNumber != info.ParentIndexNumber)
|
||||
{
|
||||
this.Log("FixSeasonNumber: old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
|
||||
info.ParentIndexNumber = season.IndexNumber;
|
||||
this.Log("FixSeasonNumber by season path. old: {0} new: {1}", info.ParentIndexNumber, guestSeasonNumber);
|
||||
info.ParentIndexNumber = guestSeasonNumber;
|
||||
}
|
||||
|
||||
|
||||
// // 当没有season级目录时,默认为1,即当成只有一季(不需要处理,虚拟季jellyfin默认传的ParentIndexNumber=1)
|
||||
// if (info.ParentIndexNumber is null && season != null && season.LocationType == LocationType.Virtual)
|
||||
// {
|
||||
// this.Log("FixSeasonNumber: season is virtual, set to default 1");
|
||||
// info.ParentIndexNumber = 1;
|
||||
// }
|
||||
}
|
||||
|
||||
// 从季文件夹名称猜出season number
|
||||
var seasonFolderPath = Path.GetDirectoryName(info.Path);
|
||||
if (info.ParentIndexNumber is null && seasonFolderPath != null)
|
||||
{
|
||||
info.ParentIndexNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
|
||||
}
|
||||
|
||||
|
||||
// 识别特典
|
||||
if (info.ParentIndexNumber is null && NameParser.IsAnime(fileName) && (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path)))
|
||||
{
|
||||
this.Log("FixSeasonNumber to special. old: {0} new: 0", info.ParentIndexNumber);
|
||||
info.ParentIndexNumber = 0;
|
||||
}
|
||||
|
||||
// // 设为默认季数为1(问题:当同时存在S01和剧场版季文件夹时,剧场版的影片会因为默认第一季而在S01也显示出来)
|
||||
// if (info.ParentIndexNumber is null)
|
||||
// {
|
||||
// this.Log("FixSeasonNumber: season number is null, set to default 1");
|
||||
// info.ParentIndexNumber = 1;
|
||||
// }
|
||||
|
||||
|
||||
// 特典优先使用文件名(特典除了前面特别设置,还有SXX/Season XX等默认的)
|
||||
// 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的)
|
||||
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
|
||||
{
|
||||
info.Name = parseResult.SpecialName == info.Name ? fileName : parseResult.SpecialName;
|
||||
}
|
||||
|
||||
|
||||
// 大于1000,可能错误解析了分辨率
|
||||
if (parseResult.IndexNumber.HasValue && parseResult.IndexNumber < 1000)
|
||||
// 修正 episode number
|
||||
if (parseResult.IndexNumber.HasValue && info.IndexNumber != parseResult.IndexNumber)
|
||||
{
|
||||
this.Log("FixEpisodeNumber by anitomy. old: {0} new: {1}", info.IndexNumber, parseResult.IndexNumber);
|
||||
info.IndexNumber = parseResult.IndexNumber;
|
||||
}
|
||||
|
||||
this.Log("FixParseInfo: fileName: {0} seasonNumber: {1} episodeNumber: {2} name: {3}", fileName, info.ParentIndexNumber, info.IndexNumber, info.Name);
|
||||
return info;
|
||||
}
|
||||
|
||||
|
@ -209,7 +204,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
// 特典或extra视频可能和正片剧集放在同一目录
|
||||
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
||||
var parseResult = NameParser.Parse(fileName);
|
||||
var parseResult = NameParser.ParseEpisode(fileName);
|
||||
if (parseResult.IsExtra)
|
||||
{
|
||||
this.Log($"Found anime extra of [name]: {fileName}");
|
||||
|
@ -236,7 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return result;
|
||||
}
|
||||
|
||||
//// 特典也有剧集信息,不在这里处理
|
||||
//// 特典也有 tmdb 剧集信息,不在这里处理
|
||||
// if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))
|
||||
// {
|
||||
// this.Log($"Found anime sp of [name]: {fileName}");
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||
{
|
||||
public static class ProviderIdsExtensions
|
||||
{
|
||||
public static MetaSource GetMetaSource(this IHasProviderIds instance, string name)
|
||||
{
|
||||
var value = instance.GetProviderId(name);
|
||||
return value.ToMetaSource();
|
||||
}
|
||||
|
||||
public static void TryGetMetaSource(this Dictionary<string, string> dict, string name, out MetaSource metaSource)
|
||||
{
|
||||
if (dict.TryGetValue(name, out var value))
|
||||
{
|
||||
metaSource = value.ToMetaSource();
|
||||
}
|
||||
else
|
||||
{
|
||||
metaSource = MetaSource.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,33 +36,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
||||
{
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop
|
||||
ImageType.Backdrop,
|
||||
ImageType.Logo,
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var sid = item.GetProviderId(DoubanProviderId);
|
||||
var metaSource = item.GetProviderId(Plugin.ProviderId);
|
||||
var metaSource = item.GetMetaSource(Plugin.ProviderId);
|
||||
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||
if (primary == null || string.IsNullOrEmpty(primary.Img))
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
var backdropImgs = await GetBackdrop(item, cancellationToken);
|
||||
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false);
|
||||
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var res = new List<RemoteImageInfo> {
|
||||
new RemoteImageInfo
|
||||
{
|
||||
ProviderName = this.Name,
|
||||
Url = this.GetProxyImageUrl(primary.ImgMiddle),
|
||||
Url = this.GetDoubanPoster(primary),
|
||||
Type = ImageType.Primary,
|
||||
},
|
||||
};
|
||||
res.AddRange(backdropImgs);
|
||||
res.AddRange(logoImgs);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -70,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var movie = await _tmdbApi
|
||||
// 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
|
||||
var movie = await this._tmdbApi
|
||||
.GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
@ -81,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
var remoteImages = new List<RemoteImageInfo>();
|
||||
|
||||
for (var i = 0; i < movie.Images.Posters.Count; i++)
|
||||
{
|
||||
var poster = movie.Images.Posters[i];
|
||||
remoteImages.Add(new RemoteImageInfo
|
||||
{
|
||||
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
|
||||
CommunityRating = poster.VoteAverage,
|
||||
VoteCount = poster.VoteCount,
|
||||
Width = poster.Width,
|
||||
Height = poster.Height,
|
||||
Language = AdjustImageLanguage(poster.Iso_639_1, language),
|
||||
ProviderName = Name,
|
||||
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
|
||||
Type = ImageType.Primary,
|
||||
});
|
||||
}
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
|
||||
{
|
||||
var backdrop = movie.Images.Backdrops[i];
|
||||
remoteImages.Add(new RemoteImageInfo
|
||||
{
|
||||
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
|
||||
CommunityRating = backdrop.VoteAverage,
|
||||
VoteCount = backdrop.VoteCount,
|
||||
Width = backdrop.Width,
|
||||
Height = backdrop.Height,
|
||||
ProviderName = Name,
|
||||
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
|
||||
Type = ImageType.Backdrop,
|
||||
RatingType = RatingType.Score
|
||||
});
|
||||
}
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||
Type = ImageType.Logo,
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
return remoteImages.OrderByLanguageDescending(language);
|
||||
}
|
||||
|
@ -133,7 +141,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
// 从豆瓣获取背景图
|
||||
if (!string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
|
||||
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||
if (photo != null && photo.Count > 0)
|
||||
{
|
||||
this.Log("GetBackdrop from douban sid: {0}", sid);
|
||||
|
@ -187,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return list;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
|
||||
var movie = await this._tmdbApi
|
||||
.GetMovieAsync(tmdbId.ToInt(), language, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movie != null && movie.Images != null)
|
||||
{
|
||||
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||
Type = ImageType.Logo,
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return list.OrderByLanguageDescending(language);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Text;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.MetaShark.Api;
|
||||
using Jellyfin.Plugin.MetaShark.Core;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
|
@ -42,13 +43,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
|
||||
// 从douban搜索
|
||||
// BUG注意:ProviderIds传多个meta值,会导致识别搜索时只返回一个结果
|
||||
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
||||
var res = await this._doubanApi.SearchMovieAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
||||
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
|
||||
{
|
||||
return new RemoteSearchResult
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
|
||||
// 注意:jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } },
|
||||
ImageUrl = this.GetProxyImageUrl(x.Img),
|
||||
ProductionYear = x.Year,
|
||||
Name = x.Name,
|
||||
|
@ -64,7 +66,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
return new RemoteSearchResult
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
|
||||
// 注意:jellyfin 会判断电影所有 provider id 是否有相同的,有相同的值就会认为是同一影片,会被合并不返回,必须保持 provider id 的唯一性
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) }, { Plugin.ProviderId, $"{MetaSource.Tmdb}_{x.Id}" } },
|
||||
Name = string.Format("[TMDB]{0}", x.Title ?? x.OriginalTitle),
|
||||
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
||||
Overview = x.Overview,
|
||||
|
@ -79,18 +83,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
/// <inheritdoc />
|
||||
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Log($"GetMovieMetadata of [name]: {info.Name} IsAutomated: {info.IsAutomated}");
|
||||
var fileName = this.GetOriginalFileName(info);
|
||||
this.Log($"GetMovieMetadata of [name]: {info.Name} [fileName]: {fileName} EnableTmdb: {config.EnableTmdb}");
|
||||
var result = new MetadataResult<Movie>();
|
||||
|
||||
// 使用刷新元数据时,providerIds会保留旧有值,只有识别/新增才会没值
|
||||
var sid = info.GetProviderId(DoubanProviderId);
|
||||
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||
var metaSource = info.GetProviderId(Plugin.ProviderId);
|
||||
// 用于修正识别时指定tmdb,没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
|
||||
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
|
||||
{
|
||||
metaSource = MetaSource.Tmdb;
|
||||
}
|
||||
var metaSource = info.GetMetaSource(Plugin.ProviderId);
|
||||
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
||||
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
||||
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
||||
|
@ -119,7 +119,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
var movie = new Movie
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } },
|
||||
Name = subject.Name,
|
||||
OriginalTitle = subject.OriginalName,
|
||||
CommunityRating = subject.Rating,
|
||||
|
@ -182,7 +183,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
Type = c.RoleType,
|
||||
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||
Role = c.Role,
|
||||
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||
|
@ -232,7 +233,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||
movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
|
||||
movie.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
|
||||
|
||||
// 获取电影系列信息
|
||||
if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null)
|
||||
|
@ -300,7 +302,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character,
|
||||
Type = PersonType.Actor,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order,
|
||||
};
|
||||
|
||||
|
@ -343,7 +345,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Type = type
|
||||
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
var list = new List<RemoteImageInfo>();
|
||||
var cid = item.GetProviderId(DoubanProviderId);
|
||||
var metaSource = item.GetProviderId(Plugin.ProviderId);
|
||||
var metaSource = item.GetMetaSource(Plugin.ProviderId);
|
||||
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||
if (!string.IsNullOrEmpty(cid))
|
||||
{
|
||||
|
@ -72,7 +72,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
});
|
||||
}
|
||||
|
||||
this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
|
||||
if (list.Count == 0)
|
||||
{
|
||||
this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
|
@ -120,8 +120,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
|
||||
{
|
||||
this.Log($"GetPersonMetadata of found tmdb [id]: {findResult.PersonResults[0].Id}");
|
||||
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.PersonResults[0].Id}");
|
||||
var foundTmdbId = findResult.PersonResults.First().Id.ToString();
|
||||
this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}");
|
||||
item.SetProviderId(MetadataProvider.Tmdb, $"{foundTmdbId}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
|
||||
var season = (Season)item;
|
||||
var series = season.Series;
|
||||
var metaSource = series.GetProviderId(Plugin.ProviderId);
|
||||
var metaSource = series.GetMetaSource(Plugin.ProviderId);
|
||||
|
||||
// get image from douban
|
||||
var sid = item.GetProviderId(DoubanProviderId);
|
||||
|
@ -60,7 +60,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
new RemoteImageInfo
|
||||
{
|
||||
ProviderName = primary.Name,
|
||||
Url = this.GetProxyImageUrl(primary.ImgMiddle),
|
||||
Url = this.GetDoubanPoster(primary),
|
||||
Type = ImageType.Primary,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,6 +14,8 @@ using System.Net.Http;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Jellyfin.Data.Enums;
|
||||
using System.IO;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||
{
|
||||
|
@ -40,23 +42,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
var result = new MetadataResult<Season>();
|
||||
|
||||
// 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖
|
||||
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
|
||||
info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
|
||||
info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource);
|
||||
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
|
||||
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
|
||||
var seasonSid = info.GetProviderId(DoubanProviderId);
|
||||
this.Log($"GetSeasonMetaData of [name]: {info.Name} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} IsAutomated: {info.IsAutomated}");
|
||||
|
||||
var fileName = Path.GetFileName(info.Path);
|
||||
this.Log($"GetSeasonMetaData of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} seriesTmdbId: {seriesTmdbId} sid: {sid} metaSource: {metaSource} EnableTmdb: {config.EnableTmdb}");
|
||||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
// 季文件夹名称不规范,没法拿到seasonNumber,尝试从文件夹名猜出
|
||||
// 注意:本办法没法处理没有季文件夹的/虚拟季,因为path会为空
|
||||
// seasonNumber 为 null 有三种情况:
|
||||
// 1. 没有季文件夹时,即虚拟季,info.Path 为空
|
||||
// 2. 一般不规范文件夹命名,没法被 EpisodeResolver 解析的,info.Path 不为空,如:摇曳露营△
|
||||
// 3. 特殊不规范文件夹命名,能被 EpisodeResolver 错误解析,这时被当成了视频文件,相当于没有季文件夹,info.Path 为空,如:冰与火之歌 S02.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3
|
||||
// 相关代码:https://github.com/jellyfin/jellyfin/blob/dc2eca9f2ca259b46c7b53f59251794903c730a4/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs#L70
|
||||
if (seasonNumber is null)
|
||||
{
|
||||
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
|
||||
}
|
||||
|
||||
// 搜索豆瓣季id
|
||||
// 搜索豆瓣季 id
|
||||
if (string.IsNullOrEmpty(seasonSid))
|
||||
{
|
||||
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -87,18 +93,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
Type = c.RoleType,
|
||||
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||
Role = c.Role,
|
||||
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||
}));
|
||||
|
||||
this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!");
|
||||
this.Log($"Season [{info.Name}] not found douban season id!");
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,10 +143,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return null;
|
||||
}
|
||||
|
||||
// 没有季文件夹(即虚拟季),info.Path会为空,直接用series的sid
|
||||
if (string.IsNullOrEmpty(info.Path))
|
||||
// 没有季文件夹或季文件夹名不规范时(即虚拟季),info.Path 会为空,seasonNumber 为 null
|
||||
if (string.IsNullOrEmpty(info.Path) && !seasonNumber.HasValue)
|
||||
{
|
||||
return sid;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从季文件夹名属性格式获取,如 [douban-12345] 或 [doubanid-12345]
|
||||
var fileName = this.GetOriginalFileName(info);
|
||||
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
|
||||
if (!string.IsNullOrWhiteSpace(doubanId))
|
||||
{
|
||||
this.Log($"Found season douban [id] by attr: {doubanId}");
|
||||
return doubanId;
|
||||
}
|
||||
|
||||
// 从sereis获取正确名称,info.Name当是标准格式如S01等时,会变成第x季,非标准名称默认文件名
|
||||
|
@ -148,7 +164,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
return null;
|
||||
}
|
||||
var seriesName = RemoveSeasonSubfix(series.Name);
|
||||
var seriesName = this.RemoveSeasonSuffix(series.Name);
|
||||
|
||||
// 没有季id,但存在tmdbid,尝试从tmdb获取对应季的年份信息,用于从豆瓣搜索对应季数据
|
||||
var seasonYear = 0;
|
||||
|
|
|
@ -36,33 +36,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
||||
{
|
||||
ImageType.Primary,
|
||||
ImageType.Backdrop
|
||||
ImageType.Backdrop,
|
||||
ImageType.Logo,
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var sid = item.GetProviderId(DoubanProviderId);
|
||||
var metaSource = item.GetProviderId(Plugin.ProviderId);
|
||||
var metaSource = item.GetMetaSource(Plugin.ProviderId);
|
||||
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||
if (primary == null || string.IsNullOrEmpty(primary.Img))
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
var dropback = await GetBackdrop(item, cancellationToken);
|
||||
var backdropImgs = await this.GetBackdrop(item, cancellationToken).ConfigureAwait(false);
|
||||
var logoImgs = await this.GetLogos(item, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var res = new List<RemoteImageInfo> {
|
||||
new RemoteImageInfo
|
||||
{
|
||||
ProviderName = this.Name,
|
||||
Url = this.GetProxyImageUrl(primary.ImgMiddle),
|
||||
Url = this.GetDoubanPoster(primary),
|
||||
Type = ImageType.Primary,
|
||||
},
|
||||
};
|
||||
res.AddRange(dropback);
|
||||
res.AddRange(backdropImgs);
|
||||
res.AddRange(logoImgs);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -70,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var movie = await _tmdbApi
|
||||
// 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
|
||||
var movie = await this._tmdbApi
|
||||
.GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
@ -81,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
var remoteImages = new List<RemoteImageInfo>();
|
||||
|
||||
for (var i = 0; i < movie.Images.Posters.Count; i++)
|
||||
{
|
||||
var poster = movie.Images.Posters[i];
|
||||
remoteImages.Add(new RemoteImageInfo
|
||||
{
|
||||
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
|
||||
CommunityRating = poster.VoteAverage,
|
||||
VoteCount = poster.VoteCount,
|
||||
Width = poster.Width,
|
||||
Height = poster.Height,
|
||||
Language = AdjustImageLanguage(poster.Iso_639_1, language),
|
||||
ProviderName = Name,
|
||||
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
|
||||
Type = ImageType.Primary,
|
||||
});
|
||||
}
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
|
||||
{
|
||||
var backdrop = movie.Images.Backdrops[i];
|
||||
remoteImages.Add(new RemoteImageInfo
|
||||
{
|
||||
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
|
||||
CommunityRating = backdrop.VoteAverage,
|
||||
VoteCount = backdrop.VoteCount,
|
||||
Width = backdrop.Width,
|
||||
Height = backdrop.Height,
|
||||
ProviderName = Name,
|
||||
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
|
||||
Type = ImageType.Backdrop,
|
||||
RatingType = RatingType.Score
|
||||
});
|
||||
}
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||
Type = ImageType.Logo,
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
|
||||
return remoteImages.OrderByLanguageDescending(language);
|
||||
}
|
||||
|
@ -177,8 +185,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
this.Log("GetBackdrop from tmdb id: {0}", tmdbId);
|
||||
list.Add(new RemoteImageInfo
|
||||
{
|
||||
ProviderName = Name,
|
||||
Url = _tmdbApi.GetBackdropUrl(movie.BackdropPath),
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath),
|
||||
Type = ImageType.Backdrop,
|
||||
});
|
||||
}
|
||||
|
@ -187,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return list;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
|
||||
var language = item.GetPreferredMetadataLanguage();
|
||||
var list = new List<RemoteImageInfo>();
|
||||
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
|
||||
var movie = await this._tmdbApi
|
||||
.GetSeriesAsync(tmdbId.ToInt(), language, language, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (movie != null && movie.Images != null)
|
||||
{
|
||||
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||
ProviderName = this.Name,
|
||||
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||
Type = ImageType.Logo,
|
||||
CommunityRating = x.VoteAverage,
|
||||
VoteCount = x.VoteCount,
|
||||
Width = x.Width,
|
||||
Height = x.Height,
|
||||
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||
RatingType = RatingType.Score,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return list.OrderByLanguageDescending(language);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using Jellyfin.Plugin.MetaShark.Api;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.MetaShark.Api;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
|
@ -41,12 +42,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
|
||||
// 从douban搜索
|
||||
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
||||
var res = await this._doubanApi.SearchTVAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
||||
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
|
||||
{
|
||||
return new RemoteSearchResult
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } },
|
||||
ImageUrl = this.GetProxyImageUrl(x.Img),
|
||||
ProductionYear = x.Year,
|
||||
Name = x.Name,
|
||||
|
@ -61,7 +63,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
return new RemoteSearchResult
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
|
||||
// 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一
|
||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) }, { Plugin.ProviderId, $"{MetaSource.Tmdb}_{x.Id}" } },
|
||||
Name = string.Format("[TMDB]{0}", x.Name ?? x.OriginalName),
|
||||
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
||||
Overview = x.Overview,
|
||||
|
@ -76,17 +79,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
/// <inheritdoc />
|
||||
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Log($"GetSeriesMetadata of [name]: {info.Name} IsAutomated: {info.IsAutomated}");
|
||||
var fileName = this.GetOriginalFileName(info);
|
||||
this.Log($"GetSeriesMetadata of [name]: {info.Name} [fileName]: {fileName} IsAutomated: {info.IsAutomated}");
|
||||
var result = new MetadataResult<Series>();
|
||||
|
||||
var sid = info.GetProviderId(DoubanProviderId);
|
||||
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||
var metaSource = info.GetProviderId(Plugin.ProviderId);
|
||||
// 用于修正识别时指定tmdb,没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
|
||||
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
|
||||
{
|
||||
metaSource = MetaSource.Tmdb;
|
||||
}
|
||||
var metaSource = info.GetMetaSource(Plugin.ProviderId);
|
||||
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
||||
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
||||
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
||||
|
@ -106,12 +105,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var seriesName = RemoveSeasonSubfix(subject.Name);
|
||||
var seriesName = RemoveSeasonSuffix(subject.Name);
|
||||
var item = new Series
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } },
|
||||
Name = seriesName,
|
||||
OriginalTitle = RemoveSeasonSubfix(subject.OriginalName),
|
||||
OriginalTitle = RemoveSeasonSuffix(subject.OriginalName),
|
||||
CommunityRating = subject.Rating,
|
||||
Overview = subject.Intro,
|
||||
ProductionYear = subject.Year,
|
||||
|
@ -154,7 +153,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
Type = c.RoleType,
|
||||
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||
Role = c.Role,
|
||||
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||
|
@ -230,6 +229,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
return tmdbId;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Log($"Can not found tmdb [id] by name: \"{name}\" and year: \"{year}\"");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -336,7 +339,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
|
||||
}
|
||||
}
|
||||
series.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
|
||||
series.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{seriesResult.Id}");
|
||||
series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode);
|
||||
|
||||
|
||||
|
@ -354,7 +357,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character,
|
||||
Type = PersonType.Actor,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order,
|
||||
};
|
||||
|
||||
|
@ -398,7 +401,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job,
|
||||
Type = type
|
||||
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.ObjectModel;
|
||||
using Jellyfin.Plugin.MetaShark.Core;
|
||||
using Jellyfin.Plugin.MetaShark.Providers;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.ScheduledTasks
|
||||
{
|
||||
public class FixMovieSimilarListTask : IScheduledTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Key => $"{Plugin.PluginName}FixMovieSimilarList";
|
||||
|
||||
public string Name => "修复电影推荐列表";
|
||||
|
||||
public string Description => $"修复电影推荐列表只有一部影片的问题。";
|
||||
|
||||
public string Category => Plugin.PluginName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixMovieSimilarListTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public FixMovieSimilarListTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<FixMovieSimilarListTask>();
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return new List<TaskTriggerInfo>();
|
||||
// yield return new TaskTriggerInfo
|
||||
// {
|
||||
// Type = TaskTriggerInfo.TriggerWeekly,
|
||||
// DayOfWeek = DayOfWeek.Monday,
|
||||
// TimeOfDayTicks = TimeSpan.FromHours(4).Ticks
|
||||
// };
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
progress?.Report(0);
|
||||
// 只有电影有问题
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
// MediaTypes = new[] { MediaType.Video },
|
||||
HasAnyProviderId = new Dictionary<string, string>() { { Plugin.ProviderId, string.Empty } },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Movie }
|
||||
}).ToList();
|
||||
|
||||
_logger.LogInformation("Fix movie similar list for {0} videos.", items.Count);
|
||||
|
||||
var successCount = 0;
|
||||
var failCount = 0;
|
||||
foreach (var (item, idx) in items.WithIndex())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
progress?.Report((double)idx / items.Count * 100);
|
||||
|
||||
try
|
||||
{
|
||||
// 判断电影带有旧的元数据,进行替换处理
|
||||
var sid = item.GetProviderId(BaseProvider.DoubanProviderId);
|
||||
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
|
||||
var providerVal = item.GetProviderId(Plugin.ProviderId);
|
||||
if (providerVal == "douban" && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var detail = this._libraryManager.GetItemById(item.Id);
|
||||
detail.SetProviderId(Plugin.ProviderId, $"{MetaSource.Douban}_{sid}");
|
||||
await detail.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (providerVal == "tmdb" && !string.IsNullOrEmpty(tmdbId))
|
||||
{
|
||||
var detail = this._libraryManager.GetItemById(item.Id);
|
||||
detail.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
|
||||
await detail.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fix movie similar list for movie {0}: {1}", item.Name, ex.Message);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
_logger.LogInformation("Exectue task completed. success: {0} fail: {1}", successCount, failCount);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,18 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.MetaShark.Api;
|
||||
using Jellyfin.Plugin.MetaShark.Providers;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark
|
||||
{
|
||||
|
@ -20,21 +10,22 @@ namespace Jellyfin.Plugin.MetaShark
|
|||
public class ServiceRegistrator : IPluginServiceRegistrator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void RegisterServices(IServiceCollection serviceCollection)
|
||||
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
||||
{
|
||||
serviceCollection.AddSingleton<DoubanApi>((ctx) =>
|
||||
|
||||
serviceCollection.AddSingleton((ctx) =>
|
||||
{
|
||||
return new DoubanApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
serviceCollection.AddSingleton<TmdbApi>((ctx) =>
|
||||
serviceCollection.AddSingleton((ctx) =>
|
||||
{
|
||||
return new TmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
serviceCollection.AddSingleton<OmdbApi>((ctx) =>
|
||||
serviceCollection.AddSingleton((ctx) =>
|
||||
{
|
||||
return new OmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
serviceCollection.AddSingleton<ImdbApi>((ctx) =>
|
||||
serviceCollection.AddSingleton((ctx) =>
|
||||
{
|
||||
return new ImdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||
});
|
||||
|
|
26
README.md
26
README.md
|
@ -1,7 +1,7 @@
|
|||
# jellyfin-plugin-metashark
|
||||
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-metashark/main/LICENSE)
|
||||
|
||||
jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TheMovieDb补全缺失的剧集数据。
|
||||
|
@ -14,9 +14,13 @@ jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TheMov
|
|||
|
||||
## 安装插件
|
||||
|
||||
只支持最新的`jellyfin 10.8.x`版本
|
||||
添加插件存储库:
|
||||
|
||||
添加插件存储库:https://jellyfin-plugin-release.pages.dev/metashark/manifest.json
|
||||
国内加速:https://gitee.com/cwhzy/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json
|
||||
|
||||
国外访问:https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json
|
||||
|
||||
> 如果都无法访问,可以直接从 [Release](https://github.com/cxfksword/jellyfin-plugin-metashark/releases) 页面下载,并解压到 jellyfin 插件目录中使用
|
||||
|
||||
## 如何使用
|
||||
|
||||
|
@ -30,17 +34,19 @@ jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TheMov
|
|||
|
||||
> 🚨假如需要刮削大量电影,请到插件配置中打开防封禁功能,避免频繁请求豆瓣导致被封IP(封IP需要等6小时左右才能恢复访问)
|
||||
|
||||
> :fire:遇到图片显示不出来时,请到插件配置中配置jellyfin访问域名
|
||||
|
||||
## How to build
|
||||
|
||||
1. Clone or download this repository
|
||||
|
||||
2. Ensure you have .NET Core SDK 6.0 setup and installed
|
||||
2. Ensure you have .NET Core SDK 8.0 setup and installed
|
||||
|
||||
3. Build plugin with following command.
|
||||
|
||||
```sh
|
||||
dotnet restore
|
||||
dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||
dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||
```
|
||||
|
||||
|
||||
|
@ -48,7 +54,7 @@ dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
|||
|
||||
1. Build the plugin
|
||||
|
||||
2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Debug/net6.0/Jellyfin.Plugin.MetaShark.dll` into it
|
||||
2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll` into it
|
||||
|
||||
3. Move folder `metashark` to jellyfin `data/plugins` folder
|
||||
|
||||
|
@ -62,4 +68,10 @@ dotnet publish Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
|||
|
||||
## Thanks
|
||||
|
||||
[AnitomySharp](https://github.com/chu-shen/AnitomySharp)
|
||||
[AnitomySharp](https://github.com/chu-shen/AnitomySharp)
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目代码仅用于学习交流编程技术,下载后请勿用于商业用途。
|
||||
|
||||
如果本项目存在侵犯您的合法权益的情况,请及时与开发者联系,开发者将会及时删除有关内容。
|
|
@ -18,7 +18,7 @@ def generate_manifest():
|
|||
"overview": "jellyfin电影元数据插件",
|
||||
"owner": "cxfksword",
|
||||
"category": "Metadata",
|
||||
"imageUrl": "https://jellyfin-plugin-release.pages.dev/metashark/logo.png",
|
||||
"imageUrl": "https://github.com/cxfksword/jellyfin-plugin-metashark/raw/main/doc/logo.png",
|
||||
"versions": []
|
||||
}]
|
||||
|
||||
|
@ -26,8 +26,8 @@ def generate_version(filepath, version, changelog):
|
|||
return {
|
||||
'version': f"{version}.0",
|
||||
'changelog': changelog,
|
||||
'targetAbi': '10.8.0.0',
|
||||
'sourceUrl': f'https://jellyfin-plugin-release.pages.dev/metashark/metashark_{version}.0.zip',
|
||||
'targetAbi': '10.9.0.0',
|
||||
'sourceUrl': f'https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/v{version}/metashark_{version}.0.zip',
|
||||
'checksum': md5sum(filepath),
|
||||
'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ def main():
|
|||
|
||||
# 解析旧 manifest
|
||||
try:
|
||||
with urlopen('https://raw.githubusercontent.com/cxfksword/jellyfin-release/master/metashark/manifest.json') as f:
|
||||
with urlopen('https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json') as f:
|
||||
manifest = json.load(f)
|
||||
except HTTPError as err:
|
||||
if err.code == 404:
|
||||
|
@ -56,18 +56,21 @@ def main():
|
|||
raise
|
||||
|
||||
# 追加新版本/覆盖旧版本
|
||||
manifest[0]['versions'] = list(filter(lambda x: x['version'] == version, manifest[0]['versions']))
|
||||
manifest[0]['versions'] = list(filter(lambda x: x['version'] != f"{version}.0", manifest[0]['versions']))
|
||||
manifest[0]['versions'].insert(0, generate_version(filepath, version, changelog))
|
||||
|
||||
with open('manifest.json', 'w') as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
# # 国内加速
|
||||
# with open('manifest_cn.json', 'w') as f:
|
||||
# manifest_cn = json.dumps(manifest, indent=2)
|
||||
# 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)
|
||||
# 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)
|
||||
# f.write(manifest_cn)
|
||||
cn_domain = 'https://mirror.ghproxy.com/'
|
||||
if 'CN_DOMAIN' in os.environ and os.environ["CN_DOMAIN"]:
|
||||
cn_domain = os.environ["CN_DOMAIN"]
|
||||
cn_domain = cn_domain.rstrip('/')
|
||||
with open('manifest_cn.json', 'w') as f:
|
||||
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__':
|
Loading…
Reference in New Issue