Compare commits
69 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 | |
|
ae7b22707e | |
|
2655f025cc | |
|
7a214962a5 | |
|
0ed27ce2f3 | |
|
7776b87e18 | |
|
b5436c10cc | |
|
376b2967cc | |
|
ea4788f894 | |
|
d0f8f1e579 | |
|
61d2a5a97f | |
|
d27f0faf65 | |
|
a180b07716 | |
|
f263ded8dc | |
|
63d8694c21 | |
|
7589b61131 | |
|
8fc6c8aa12 | |
|
7b92a811b2 | |
|
3513da7f12 | |
|
a6cdbce02d | |
|
04c3ef949e | |
|
ca06af4b53 | |
|
45e4a40c28 | |
|
c29f80725b | |
|
7e4246003d | |
|
ed1a72dad6 | |
|
5f5a5d091d | |
|
54574cf0ce | |
|
e7eabfe7d7 | |
|
276498e179 | |
|
c371144985 | |
|
6e43e35c88 | |
|
c43e2791ea | |
|
51222ffb2f | |
|
a62d6e9ac7 | |
|
4ee8e53705 |
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: 刮削失败相关问题
|
||||
about: 报告刮削失败相关问题.
|
||||
title: "[刮削]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述错误**
|
||||
|
||||
对错误是什么的清晰简明描述。
|
||||
|
||||
**屏幕截图**
|
||||
|
||||
请添加问题截图以帮助解释您的问题。
|
||||
|
||||
**日志**
|
||||
|
||||
请提供jellyfin打印的该影片的刮削日志。
|
||||
|
||||
日志查看方法: 控制台->高级->日志->点击log_yyyymmdd.log格式文件
|
||||
|
||||
**运行环境(请填写以下信息):**
|
||||
|
||||
- 操作系统:[例如 linux]
|
||||
- jellyfin 版本:[例如 10.8.9]
|
||||
- 插件版本:[例如 1.7.1]
|
|
@ -0,0 +1,47 @@
|
|||
name: "🚀 Beta"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
dotnet-version: 8.0.x
|
||||
python-version: 3.8
|
||||
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||
artifact: metashark
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build & Release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get tags (For CHANGELOG)
|
||||
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v3
|
||||
id: dotnet
|
||||
with:
|
||||
dotnet-version: ${{ env.dotnet-version }}
|
||||
- name: Change default dotnet version
|
||||
run: |
|
||||
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
run: |
|
||||
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
||||
VERSION="$VERSION.0"
|
||||
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=net8.0 ${{ env.project }}
|
||||
mkdir -p artifacts
|
||||
cp ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{steps.vars.outputs.APP_NAME}}
|
||||
path: artifacts
|
||||
retention-days: 7
|
|
@ -12,8 +12,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
|
|
@ -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
|
||||
|
@ -21,43 +21,42 @@ jobs:
|
|||
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v3
|
||||
id: dotnet
|
||||
with:
|
||||
dotnet-version: ${{ env.dotnet-version }}
|
||||
- name: Change default dotnet version
|
||||
run: |
|
||||
echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.python-version }}
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
run: |
|
||||
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
||||
VERSION="$VERSION.0"
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
|
||||
- name: Install dependencies
|
||||
run: dotnet restore ${{ env.project }} --no-cache
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
|
||||
- name: Build
|
||||
run: dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 --output=artifacts -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
|
||||
- name: Clean publish dll
|
||||
run: cd artifacts && rm -rf MediaBrowser*.dll Microsoft*.dll Newtonsoft*.dll System*.dll Emby*.dll Jellyfin.Data*.dll Jellyfin.Extensions*.dll *.json *.pdb
|
||||
- name: Compress build files
|
||||
uses: thedoctor0/zip-release@main
|
||||
with:
|
||||
type: "zip"
|
||||
directory: "artifacts"
|
||||
filename: "artifacts.zip"
|
||||
exclusions: "*.json *.pdb"
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python-version }}
|
||||
- name: Install JPRM
|
||||
run: python -m pip install jprm
|
||||
- name: Run JPRM
|
||||
run: chmod +x ./build.sh && ./build.sh ${{ env.artifact }} ${{steps.vars.outputs.VERSION}} ${GITHUB_REF#refs/*/}
|
||||
run: |
|
||||
dotnet restore ${{ env.project }} --no-cache
|
||||
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/net8.0/Jellyfin.Plugin.MetaShark.dll
|
||||
- name: Generate manifest
|
||||
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
|
||||
env:
|
||||
CN_DOMAIN: ${{ vars.CN_DOMAIN }}
|
||||
- name: Publish release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./${{ env.artifact }}/${{ env.artifact }}_*.zip
|
||||
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:
|
||||
|
|
|
@ -5,6 +5,5 @@ obj/
|
|||
artifacts
|
||||
**/.DS_Store
|
||||
metashark/
|
||||
manifest_cn.json
|
||||
manifest.json
|
||||
*.json
|
||||
.vscode
|
||||
|
|
|
@ -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.
|
|
@ -1,15 +1,6 @@
|
|||
using System.Reflection;
|
||||
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.Core;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
using Jellyfin.Plugin.MetaShark.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Test
|
||||
{
|
||||
|
@ -41,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,15 +1,5 @@
|
|||
using System.Reflection;
|
||||
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.Core;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
using Jellyfin.Plugin.MetaShark.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Test
|
||||
{
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -8,7 +8,7 @@ using Jellyfin.Plugin.MetaShark.Core;
|
|||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
using Jellyfin.Plugin.MetaShark.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Test
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark",
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark.Test", "Jellyfin.Plugin.MetaShark.Test\Jellyfin.Plugin.MetaShark.Test.csproj", "{80814353-4291-4230-8C4A-4C45CAD4D5D3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnitomySharp", "AnitomySharp\AnitomySharp.csproj", "{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -20,6 +22,10 @@ Global
|
|||
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
AllowAutoRedirect = false
|
||||
};
|
||||
httpClient = new HttpClient(handler);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(5);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -39,8 +39,8 @@ 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);
|
||||
_tmDbClient.RequestTimeout = TimeSpan.FromSeconds(10);
|
||||
_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()
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using MediaBrowser.Model.Plugins;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Configuration;
|
||||
|
||||
|
@ -12,6 +13,9 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||
public const int MAX_CAST_MEMBERS = 15;
|
||||
public const int MAX_SEARCH_RESULT = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 插件版本
|
||||
/// </summary>
|
||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
||||
|
||||
public string DoubanCookies { get; set; } = string.Empty;
|
||||
|
@ -20,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;
|
||||
|
@ -28,30 +36,64 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||
/// </summary>
|
||||
public string DoubanImageProxyBaseUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 启用获取tmdb元数据
|
||||
/// </summary>
|
||||
public bool EnableTmdb { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用显示tmdb搜索结果
|
||||
/// </summary>
|
||||
public bool EnableTmdbSearch { get; set; } = false;
|
||||
|
||||
public bool EnableTmdbBackdrop { get; set; } = false;
|
||||
/// <summary>
|
||||
/// 启用tmdb获取背景图
|
||||
/// </summary>
|
||||
public bool EnableTmdbBackdrop { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 启用tmdb获取商标
|
||||
/// </summary>
|
||||
public bool EnableTmdbLogo { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否获取电影系列信息
|
||||
/// </summary>
|
||||
public bool EnableTmdbCollection { get; set; } = false;
|
||||
public bool EnableTmdbCollection { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 是否获取tmdb分级信息
|
||||
/// </summary>
|
||||
public bool EnableTmdbOfficialRating { get; set; } = false;
|
||||
|
||||
public bool EnableTmdbOfficialRating { get; set; } = true;
|
||||
/// <summary>
|
||||
/// tmdb api key
|
||||
/// </summary>
|
||||
public string TmdbApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// tmdb api host
|
||||
/// </summary>
|
||||
public string TmdbHost { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 代理服务器类型,0-禁用,1-http,2-https,3-socket5
|
||||
/// </summary>
|
||||
public string TmdbProxyType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 代理服务器host
|
||||
/// </summary>
|
||||
public string TmdbProxyPort { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// 代理服务器端口
|
||||
/// </summary>
|
||||
public string TmdbProxyHost { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public IWebProxy GetTmdbWebProxy()
|
||||
{
|
||||
|
||||
public int MaxCastMembers { get; set; } = 15;
|
||||
|
||||
public int MaxSearchResult { get; set; } = 5;
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(TmdbProxyType))
|
||||
{
|
||||
return new WebProxy($"{TmdbProxyType}://{TmdbProxyHost}:{TmdbProxyPort}", true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,26 +28,17 @@
|
|||
</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>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DoubanImageProxyBaseUrl">豆瓣图片代理地址</label>
|
||||
<input id="DoubanImageProxyBaseUrl" name="DoubanImageProxyBaseUrl" type="text"
|
||||
is="emby-input" placeholder="http://jellyfin.xxx.com" />
|
||||
<div class="fieldDescription">可为空,填写jellyfin访问域名,只有使用了nginx代理或docker,且豆瓣图片没法显示时,才需要填写
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="EnableDoubanAvoidRiskControl">
|
||||
<input id="EnableDoubanAvoidRiskControl" name="EnableDoubanAvoidRiskControl"
|
||||
type="checkbox" is="emby-checkbox" />
|
||||
<span class="checkboxLabel" style="position:relative">启用防封禁
|
||||
<!-- <span
|
||||
style="width:24px;color:red;font-size:12px;position: absolute;top:-3px;margin-left:3px;">实险</span> -->
|
||||
<img style="position: absolute; top:-12px; width: 32px; height:32px"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABS9JREFUeF7tmWtoHFUUx/9ndne2NmLph5q0yc4m1SSzMS2opRIpGEGLtWotqBS0NIIgCCotVD+aItSi+EB80S8V0YgKWouPNhUMqbWKsWqpya5pQjITRb8IKsruzO4cuXcf7m42yTRkgzs799Puzp17z/md/zn3sYQ6b1Tn/sMH4Cugzgn4KVDnAvCLoJ8CfgrUOQE/BepcAP4q4KeAnwJ1TsBPgToXwMWtAudbG5oCGfsVIlwHYF2NwPsFwIhuWDsq2es6BUbWYWVDUP2CgKtrxPFZZuqGNctf1wDGNLWfgCdq1fmc3e/phnVPsQ+uASQ09TwDV9UyAAb/FjPspkUBiGsq17LzedvL08C1AnwAFRTAwIGYYfULuvFI6HMQ9f7fVbKkCqg2ADF+gHlaVULHk449sBSAaw7AUiuspgAQ0N9pWAeWMsWWA8AZBgaJ0Q4F3WBsrFAXvgVwAcA0QJcBvBbM14NoTaEv8xARDZUAyD8srTXfAJgUYxFoNYObAdw6Vy2qKgCGcihmpk7kJ59sa2i0HPs1MO4s2M60r9NMPV9u4MR6rLLt0EEQPSSeVdq1id+LNmSmw3iky7SOlo81poWuJZDYtN1e/qxqAEbbVkSDdqYpHVA2Bck53TFlf581WBozIg1hDOimda/4KM4VQcfeS8A/9qXWU92jsKTUNXVM+M+Oc6P4HptJD8lxWoLZFUZReuWOlHCfPm29JX4ajai7CLgDhHOZoDLQPZk0pqIr1yY5LYKxoRhC1QAkoupOZryfn0xh54YOMz2cc+qYiIZCdFvHdOrj8Sasyajq1wDasv3ptG6ktkhnWtSdipIdZ55V5g8AwyA6pTg84xAGCk4SvapPp6SKEpp6hIG+ZQEQ19Q3AcjoSuOZX4qZ9sMSQCT0spB2QLVWtV/An/GI+iQRtGLDHMbxmGm9PdESbrcV/mkBAFlsQJIJBjv4kcCD6YB6tHvq71/z41Y6v1RNAXFN/RBChrlGwDudhrVLyldTDxKwVTesTcVOV/o8fiXCGUtNLgSAGR+kQfs3mKmJgsMtwV5SaIdu2Htz8846wFUNwFg09BwxyYlzGnhRN+xHC1Jk3q6b9uXyezT0ADv/qWUWiFyVnycFTuiGdYt0snVFKxynTwG2MbAZzEO6acv6sawKSGjh9YDzJYMaxeQOaGuXkTqZBRA6yaCbHIV7uqbsr2RalG6t/2Kmu4hY3DVcg+ydQ3sJgNIUe0Y3rMcKcIvyXBTPQuGscIRflAISWmg3g94oj1SxgeLZePMlLelAZjeYzuaXw4QW3MJQTuXe/UQ3rO35vplApofBqxUok51G6rMKUSs5v4uIAughoEM3LFlAZZQZm5kwzMSDRIHfY1PJKQk5Gt4P5qeL7b5oAHFNfRzAoUr5Wi5RdpQHYz+nZAETreI+AHTYgXM4ZthiM1RoQsqK4+xhQB6u8q3c4EQkvI+Jn2Xm+2Om/XrJGJFQHxEdyb8zqoVvVsCDiwYQ10IvACTzuGJjlmt0oREJ+Z4B8AOAKAixOXaCooSfI8ZZ8S5D3jHG5pwHOMbAdyLSIGzL9yNghkEfMXOjQtjIwBXyWZFdTNRMQPtcQOe8D4hr6rsA7p7HqJp85CoF4lH1UzBklfVSc3UlVr6h8RIAAAtfinrl6qtS4Fxdi+c2D3sAtHok+kvzx4hHYMxyw/WtsA/AowR8BXg0sK7d8hXgGpVHO/oK8GhgXbvlK8A1Ko929BXg0cC6dstXgGtUHu3oK8CjgXXtlq8A16g82vFfwx5yXxq/mYIAAAAASUVORK5CYII="
|
||||
alt="beta" />
|
||||
|
@ -55,6 +46,21 @@
|
|||
</label>
|
||||
<div class="fieldDescription">勾选后,刮削会变慢,适合刮削大量影片时使用,建议搭配网站cookie一起使用</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DoubanImageProxyBaseUrl">豆瓣图片代理地址</label>
|
||||
<input id="DoubanImageProxyBaseUrl" name="DoubanImageProxyBaseUrl" type="text"
|
||||
is="emby-input" placeholder="https://jellyfin.xxx.com:8920" />
|
||||
<div class="fieldDescription">
|
||||
可为空,填写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"
|
||||
|
@ -92,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"
|
||||
|
@ -120,6 +134,29 @@
|
|||
<div class="fieldDescription">
|
||||
填写Api域名,可选api.tmdb.org/api.themoviedb.org,默认api.tmdb.org.(需重启才能生效)</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<label class="selectLabel" for="TmdbProxyType">Api代理服务器:</label>
|
||||
<select is="emby-select" id="TmdbProxyType" name="TmdbProxyType"
|
||||
class="TmdbProxyType emby-select-withcolor emby-select">
|
||||
<option value="">禁用</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks5">Socks5</option>
|
||||
</select>
|
||||
<div class="fieldDescription">
|
||||
选择Api代理服务器类型.(需重启才能生效)</div>
|
||||
</div>
|
||||
<div class="inputContainer tmdb-proxy-wapper">
|
||||
<label class="inputLabel inputLabel-float inputLabelUnfocused"
|
||||
for="TmdbProxyHost">Api代理服务器Host:</label>
|
||||
<input is="emby-input" id="TmdbProxyHost" name="TmdbProxyHost" class="emby-input">
|
||||
</div>
|
||||
<div class="inputContainer tmdb-proxy-wapper">
|
||||
<label class="inputLabel inputLabel-float inputLabelUnfocused"
|
||||
for="TmdbProxyPort">Api代理服务器端口:</label>
|
||||
<input is="emby-input" type="number" id="TmdbProxyPort" name="TmdbProxyPort"
|
||||
pattern="[0-9]*" min="1" max="65535" class="emby-input">
|
||||
</div>
|
||||
</fieldset>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
|
@ -135,6 +172,7 @@
|
|||
|
||||
document.querySelector('#TemplateConfigPage')
|
||||
.addEventListener('pageshow', function () {
|
||||
console.log('metashark pageshow');
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
$('#current_version').text("v" + config.Version);
|
||||
|
@ -142,16 +180,23 @@
|
|||
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;
|
||||
document.querySelector('#TmdbHost').value = config.TmdbHost;
|
||||
|
||||
document.querySelector('#TmdbProxyType').value = config.TmdbProxyType;
|
||||
document.querySelector('#TmdbProxyHost').value = config.TmdbProxyHost;
|
||||
document.querySelector('#TmdbProxyPort').value = config.TmdbProxyPort;
|
||||
|
||||
changeProxyDisplay();
|
||||
checkDoubanLogin();
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
|
@ -165,15 +210,20 @@
|
|||
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;
|
||||
config.TmdbHost = document.querySelector('#TmdbHost').value;
|
||||
config.TmdbProxyType = document.querySelector('#TmdbProxyType').value;
|
||||
config.TmdbProxyHost = document.querySelector('#TmdbProxyHost').value;
|
||||
config.TmdbProxyPort = document.querySelector('#TmdbProxyPort').value;
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
|
||||
|
@ -185,19 +235,34 @@
|
|||
return false;
|
||||
});
|
||||
|
||||
document.querySelector('#TmdbProxyType')
|
||||
.addEventListener('change', function (e) {
|
||||
changeProxyDisplay();
|
||||
});
|
||||
|
||||
|
||||
function changeProxyDisplay() {
|
||||
let proxyType = document.querySelector('#TmdbProxyType').value;
|
||||
if (proxyType) {
|
||||
$('.tmdb-proxy-wapper').show()
|
||||
} else {
|
||||
$('.tmdb-proxy-wapper').hide()
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
|||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
[Route("/plugin/metashark")]
|
||||
public class MetaSharkController : ControllerBase
|
||||
public class ApiController : ControllerBase
|
||||
{
|
||||
private readonly DoubanApi _doubanApi;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MetaSharkController"/> class.
|
||||
/// Initializes a new instance of the <see cref="ApiController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||
public MetaSharkController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
|
||||
public ApiController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
|
||||
{
|
||||
this._httpClientFactory = httpClientFactory;
|
||||
this._doubanApi = doubanApi;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)'=='Release'">
|
||||
<PropertyGroup>
|
||||
<DoILRepack>false</DoILRepack>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InputAssemblies Include="$(OutputPath)$(AssemblyName).dll" />
|
||||
<InputAssemblies Include="$(OutputPath)RateLimiter.dll" />
|
||||
<InputAssemblies Include="$(OutputPath)ComposableAsync.Core.dll" />
|
||||
<InputAssemblies Include="$(OutputPath)TMDbLib.dll" />
|
||||
<InputAssemblies Include="$(OutputPath)Newtonsoft.Json.dll" />
|
||||
<InputAssemblies Include="$(OutputPath)AngleSharp.dll" />
|
||||
<InputAssemblies Include="$(OutputPath)AnitomySharp.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<ILRepack
|
||||
Parallel="false"
|
||||
Internalize="true"
|
||||
DebugInfo="true"
|
||||
InputAssemblies="@(InputAssemblies)"
|
||||
LibraryPath="$(OutputPath)"
|
||||
TargetKind="Dll"
|
||||
OutputFile="$(OutputPath)$(AssemblyName).dll"
|
||||
/>
|
||||
</Target>
|
||||
</Project>
|
|
@ -1,11 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<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>
|
||||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
|
@ -13,11 +14,21 @@
|
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Vendor\TMDbLib\**" />
|
||||
<EmbeddedResource Remove="Vendor\TMDbLib\**" />
|
||||
<None Remove="Vendor\TMDbLib\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.0.1" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
|
||||
<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.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
|
@ -29,6 +40,6 @@
|
|||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Vendor\TMDbLib\TMDbLib.csproj" />
|
||||
<ProjectReference Include="..\AnitomySharp\AnitomySharp.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
using MediaBrowser.Model.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model
|
||||
{
|
||||
|
@ -96,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
|
||||
{
|
||||
|
@ -158,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,9 +65,9 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||
};
|
||||
}
|
||||
|
||||
public string GetLocalApiBaseUrl(string hostname = "127.0.0.1")
|
||||
public string GetLocalApiBaseUrl()
|
||||
{
|
||||
return this._appHost.GetSmartApiUrl(hostname);
|
||||
return this._appHost.GetLocalApiUrl("127.0.0.1", "http");
|
||||
}
|
||||
|
||||
public string GetApiBaseUrl(HttpRequest request)
|
||||
|
|
|
@ -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,54 +509,33 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
protected string GetLocalProxyImageUrl(string url)
|
||||
{
|
||||
var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl("127.0.0.1");
|
||||
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);
|
||||
}
|
||||
|
||||
// 自动扫描刷新时,直接使用本地地址
|
||||
return Plugin.Instance?.GetLocalApiBaseUrl("127.0.0.1") ?? string.Empty;
|
||||
// 自动扫描刷新时,直接使用本地地址(127.0.0.1)
|
||||
return Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
|
||||
}
|
||||
|
||||
protected void Log(string? message, params object?[] args)
|
||||
|
@ -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,
|
||||
|
@ -179,10 +180,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
result.Item = movie;
|
||||
result.QueriedById = true;
|
||||
result.HasMetadata = true;
|
||||
subject.LimitDirectorCelebrities.Take(this.config.MaxCastMembers).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
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)
|
||||
|
@ -294,13 +296,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
// 演员
|
||||
if (item.Credits?.Cast != null)
|
||||
{
|
||||
foreach (var actor in item.Credits.Cast.OrderBy(a => a.Order).Take(this.config.MaxCastMembers))
|
||||
foreach (var actor in item.Credits.Cast.OrderBy(a => a.Order).Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS))
|
||||
{
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
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);
|
||||
|
@ -84,21 +90,22 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
result.Item = movie;
|
||||
result.HasMetadata = true;
|
||||
subject.LimitDirectorCelebrities.Take(this.config.MaxCastMembers).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
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,
|
||||
|
@ -151,10 +150,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
result.Item = item;
|
||||
result.QueriedById = true;
|
||||
result.HasMetadata = true;
|
||||
subject.LimitDirectorCelebrities.Take(this.config.MaxCastMembers).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = c.Name,
|
||||
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);
|
||||
|
||||
|
||||
|
@ -348,13 +351,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
// 演员
|
||||
if (seriesResult.Credits?.Cast != null)
|
||||
{
|
||||
foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(this.config.MaxCastMembers))
|
||||
foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS))
|
||||
{
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
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>());
|
||||
});
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ComposableAsync.Core.Test")]
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher awaiter, making a dispatcher awaitable
|
||||
/// </summary>
|
||||
public struct DispatcherAwaiter : INotifyCompletion
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher never is synchronous
|
||||
/// </summary>
|
||||
public bool IsCompleted => false;
|
||||
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a NotifyCompletion fom a dispatcher
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
public DispatcherAwaiter(IDispatcher dispatcher)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch on complete
|
||||
/// </summary>
|
||||
/// <param name="continuation"></param>
|
||||
[SecuritySafeCritical]
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
_Dispatcher.Dispatch(continuation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No Result
|
||||
/// </summary>
|
||||
public void GetResult() { }
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="DelegatingHandler"/> implementation based on <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
internal class DispatcherDelegatingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Build an <see cref="DelegatingHandler"/> from a <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
public DispatcherDelegatingHandler(IDispatcher dispatcher)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
InnerHandler = new HttpClientHandler();
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return _Dispatcher.Enqueue(() => base.SendAsync(request, cancellationToken), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
internal class ComposedDispatcher : IDispatcher, IAsyncDisposable
|
||||
{
|
||||
private readonly IDispatcher _First;
|
||||
private readonly IDispatcher _Second;
|
||||
|
||||
public ComposedDispatcher(IDispatcher first, IDispatcher second)
|
||||
{
|
||||
_First = first;
|
||||
_Second = second;
|
||||
}
|
||||
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
_First.Dispatch(() => _Second.Dispatch(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Action action)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new ComposedDispatcher(_First, _Second);
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return Task.WhenAll(DisposeAsync(_First), DisposeAsync(_Second));
|
||||
}
|
||||
|
||||
private static Task DisposeAsync(IDispatcher disposable) => (disposable as IAsyncDisposable)?.DisposeAsync() ?? Task.CompletedTask;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
internal class DispatcherAdapter : IDispatcher
|
||||
{
|
||||
private readonly IBasicDispatcher _BasicDispatcher;
|
||||
|
||||
public DispatcherAdapter(IBasicDispatcher basicDispatcher)
|
||||
{
|
||||
_BasicDispatcher = basicDispatcher;
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new DispatcherAdapter(_BasicDispatcher.Clone());
|
||||
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
_BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<Task> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcher"/> that run actions synchronously
|
||||
/// </summary>
|
||||
public sealed class NullDispatcher: IDispatcher
|
||||
{
|
||||
private NullDispatcher() { }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a static null dispatcher
|
||||
/// </summary>
|
||||
public static IDispatcher Instance { get; } = new NullDispatcher();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Enqueue(Action action)
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return Task.FromResult(action());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Enqueue(Func<Task> action)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(action());
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDispatcher Clone() => Instance;
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcher"/> extension methods provider
|
||||
/// </summary>
|
||||
public static class DispatcherExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns awaitable to enter in the dispatcher context
|
||||
/// This extension method make a dispatcher awaitable
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static DispatcherAwaiter GetAwaiter(this IDispatcher dispatcher)
|
||||
{
|
||||
return new DispatcherAwaiter(dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatcher
|
||||
/// after the first one
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, IDispatcher other)
|
||||
{
|
||||
if (dispatcher == null)
|
||||
throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
|
||||
return new ComposedDispatcher(dispatcher, other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatchers sequentially
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="others"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, params IDispatcher[] others)
|
||||
{
|
||||
return dispatcher.Then((IEnumerable<IDispatcher>)others);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatchers sequentially
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="others"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, IEnumerable<IDispatcher> others)
|
||||
{
|
||||
if (dispatcher == null)
|
||||
throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
||||
if (others == null)
|
||||
throw new ArgumentNullException(nameof(others));
|
||||
|
||||
return others.Aggregate(dispatcher, (cum, val) => cum.Then(val));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="DelegatingHandler"/> from an <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static DelegatingHandler AsDelegatingHandler(this IDispatcher dispatcher)
|
||||
{
|
||||
return new DispatcherDelegatingHandler(dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IDispatcher"/> from an <see cref="IBasicDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="basicDispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher ToFullDispatcher(this IBasicDispatcher @basicDispatcher)
|
||||
{
|
||||
return new DispatcherAdapter(@basicDispatcher);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher manager
|
||||
/// </summary>
|
||||
public interface IDispatcherManager : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// true if the Dispatcher should be released
|
||||
/// </summary>
|
||||
bool DisposeDispatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a consumable Dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDispatcher GetDispatcher();
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcherManager"/> implementation based on single <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
public sealed class MonoDispatcherManager : IDispatcherManager
|
||||
{
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public bool DisposeDispatcher { get; }
|
||||
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public IDispatcher GetDispatcher() => _Dispatcher;
|
||||
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Create
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="shouldDispose"></param>
|
||||
public MonoDispatcherManager(IDispatcher dispatcher, bool shouldDispose = false)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
DisposeDispatcher = shouldDispose;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return DisposeDispatcher && (_Dispatcher is IAsyncDisposable disposable) ?
|
||||
disposable.DisposeAsync() : Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcherProvider"/> extension
|
||||
/// </summary>
|
||||
public static class DispatcherProviderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the underlying <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcherProvider"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher GetAssociatedDispatcher(this IDispatcherProvider dispatcherProvider)
|
||||
{
|
||||
return dispatcherProvider?.Dispatcher ?? NullDispatcher.Instance;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAsyncDisposable"/> implementation aggregating other <see cref="IAsyncDisposable"/>
|
||||
/// </summary>
|
||||
public sealed class ComposableAsyncDisposable : IAsyncDisposable
|
||||
{
|
||||
private readonly ConcurrentQueue<IAsyncDisposable> _Disposables;
|
||||
|
||||
/// <summary>
|
||||
/// Build an empty ComposableAsyncDisposable
|
||||
/// </summary>
|
||||
public ComposableAsyncDisposable()
|
||||
{
|
||||
_Disposables = new ConcurrentQueue<IAsyncDisposable>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an <see cref="IAsyncDisposable"/> to the ComposableAsyncDisposable
|
||||
/// and returns it
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="disposable"></param>
|
||||
/// <returns></returns>
|
||||
public T Add<T>(T disposable) where T: IAsyncDisposable
|
||||
{
|
||||
if (disposable == null)
|
||||
return default(T);
|
||||
|
||||
_Disposables.Enqueue(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose all the resources asynchronously
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
var tasks = _Disposables.ToArray().Select(disposable => disposable.DisposeAsync()).ToArray();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous version of IDisposable
|
||||
/// For reference see discussion: https://github.com/dotnet/roslyn/issues/114
|
||||
/// </summary>
|
||||
public interface IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs asynchronously application-defined tasks associated with freeing,
|
||||
/// releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
Task DisposeAsync();
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Simplified version of <see cref="IDispatcher"/> that can be converted
|
||||
/// to a <see cref="IDispatcher"/> using the ToFullDispatcher extension method
|
||||
/// </summary>
|
||||
public interface IBasicDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Clone dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IBasicDispatcher Clone();
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// /// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher executes an action or a function
|
||||
/// on its own context
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute action on dispatcher context in a
|
||||
/// none-blocking way
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
void Dispatch(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding to
|
||||
/// the completion of the action
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding to
|
||||
/// the result of the function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding to
|
||||
/// the completion of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// /// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Clone dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDispatcher Clone();
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the fiber associated with an actor
|
||||
/// </summary>
|
||||
public interface IDispatcherProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the corresponding <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
IDispatcher Dispatcher { get; }
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension to interface <see cref="IAwaitableConstraint"/>
|
||||
/// </summary>
|
||||
public static class AwaitableConstraintExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Compose two awaitable constraint in a new one
|
||||
/// </summary>
|
||||
/// <param name="awaitableConstraint1"></param>
|
||||
/// <param name="awaitableConstraint2"></param>
|
||||
/// <returns></returns>
|
||||
public static IAwaitableConstraint Compose(this IAwaitableConstraint awaitableConstraint1, IAwaitableConstraint awaitableConstraint2)
|
||||
{
|
||||
if (awaitableConstraint1 == awaitableConstraint2)
|
||||
return awaitableConstraint1;
|
||||
|
||||
return new ComposedAwaitableConstraint(awaitableConstraint1, awaitableConstraint2);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class ComposedAwaitableConstraint : IAwaitableConstraint
|
||||
{
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint1;
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint2;
|
||||
private readonly SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
internal ComposedAwaitableConstraint(IAwaitableConstraint awaitableConstraint1, IAwaitableConstraint awaitableConstraint2)
|
||||
{
|
||||
_AwaitableConstraint1 = awaitableConstraint1;
|
||||
_AwaitableConstraint2 = awaitableConstraint2;
|
||||
}
|
||||
|
||||
public IAwaitableConstraint Clone()
|
||||
{
|
||||
return new ComposedAwaitableConstraint(_AwaitableConstraint1.Clone(), _AwaitableConstraint2.Clone());
|
||||
}
|
||||
|
||||
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync(cancellationToken);
|
||||
IDisposable[] disposables;
|
||||
try
|
||||
{
|
||||
disposables = await Task.WhenAll(_AwaitableConstraint1.WaitForReadiness(cancellationToken), _AwaitableConstraint2.WaitForReadiness(cancellationToken));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_Semaphore.Release();
|
||||
throw;
|
||||
}
|
||||
return new DisposeAction(() =>
|
||||
{
|
||||
foreach (var disposable in disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
_Semaphore.Release();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide an awaitable constraint based on number of times per duration
|
||||
/// </summary>
|
||||
public class CountByIntervalAwaitableConstraint : IAwaitableConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// List of the last time stamps
|
||||
/// </summary>
|
||||
public IReadOnlyList<DateTime> TimeStamps => _TimeStamps.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Stack of the last time stamps
|
||||
/// </summary>
|
||||
protected LimitedSizeStack<DateTime> _TimeStamps { get; }
|
||||
|
||||
private int _Count { get; }
|
||||
private TimeSpan _TimeSpan { get; }
|
||||
private SemaphoreSlim _Semaphore { get; } = new SemaphoreSlim(1, 1);
|
||||
private ITime _Time { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new AwaitableConstraint based on number of times per duration
|
||||
/// </summary>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="timeSpan"></param>
|
||||
public CountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan) : this(count, timeSpan, TimeSystem.StandardTime)
|
||||
{
|
||||
}
|
||||
|
||||
internal CountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan, ITime time)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("count should be strictly positive", nameof(count));
|
||||
|
||||
if (timeSpan.TotalMilliseconds <= 0)
|
||||
throw new ArgumentException("timeSpan should be strictly positive", nameof(timeSpan));
|
||||
|
||||
_Count = count;
|
||||
_TimeSpan = timeSpan;
|
||||
_TimeStamps = new LimitedSizeStack<DateTime>(_Count);
|
||||
_Time = time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a task that will complete once the constraint is fulfilled
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">
|
||||
/// Cancel the wait
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A disposable that should be disposed upon task completion
|
||||
/// </returns>
|
||||
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync(cancellationToken);
|
||||
var count = 0;
|
||||
var now = _Time.GetNow();
|
||||
var target = now - _TimeSpan;
|
||||
LinkedListNode<DateTime> element = _TimeStamps.First, last = null;
|
||||
while ((element != null) && (element.Value > target))
|
||||
{
|
||||
last = element;
|
||||
element = element.Next;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count < _Count)
|
||||
return new DisposeAction(OnEnded);
|
||||
|
||||
Debug.Assert(element == null);
|
||||
Debug.Assert(last != null);
|
||||
var timeToWait = last.Value.Add(_TimeSpan) - now;
|
||||
try
|
||||
{
|
||||
await _Time.GetDelay(timeToWait, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_Semaphore.Release();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new DisposeAction(OnEnded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone CountByIntervalAwaitableConstraint
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IAwaitableConstraint Clone()
|
||||
{
|
||||
return new CountByIntervalAwaitableConstraint(_Count, _TimeSpan, _Time);
|
||||
}
|
||||
|
||||
private void OnEnded()
|
||||
{
|
||||
var now = _Time.GetNow();
|
||||
_TimeStamps.Push(now);
|
||||
OnEnded(now);
|
||||
_Semaphore.Release();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when action has been executed
|
||||
/// </summary>
|
||||
/// <param name="now"></param>
|
||||
protected virtual void OnEnded(DateTime now)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class DisposeAction : IDisposable
|
||||
{
|
||||
private Action _Act;
|
||||
|
||||
public DisposeAction(Action act)
|
||||
{
|
||||
_Act = act;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Act?.Invoke();
|
||||
_Act = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a time constraints that can be awaited
|
||||
/// </summary>
|
||||
public interface IAwaitableConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// returns a task that will complete once the constraint is fulfilled
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">
|
||||
/// Cancel the wait
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A disposable that should be disposed upon task completion
|
||||
/// </returns>
|
||||
Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new IAwaitableConstraint with same constraints but unused
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAwaitableConstraint Clone();
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Time abstraction
|
||||
/// </summary>
|
||||
internal interface ITime
|
||||
{
|
||||
/// <summary>
|
||||
/// Return Now DateTime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
DateTime GetNow();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a task delay
|
||||
/// </summary>
|
||||
/// <param name="timespan"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task GetDelay(TimeSpan timespan, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// LinkedList with a limited size
|
||||
/// If the size exceeds the limit older entry are removed
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class LimitedSizeStack<T>: LinkedList<T>
|
||||
{
|
||||
private readonly int _MaxSize;
|
||||
|
||||
/// <summary>
|
||||
/// Construct the LimitedSizeStack with the given limit
|
||||
/// </summary>
|
||||
/// <param name="maxSize"></param>
|
||||
public LimitedSizeStack(int maxSize)
|
||||
{
|
||||
_MaxSize = maxSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push new entry. If he size exceeds the limit, the oldest entry is removed
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void Push(T item)
|
||||
{
|
||||
AddFirst(item);
|
||||
|
||||
if (Count > _MaxSize)
|
||||
RemoveLast();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="CountByIntervalAwaitableConstraint"/> that is able to save own state.
|
||||
/// </summary>
|
||||
public sealed class PersistentCountByIntervalAwaitableConstraint : CountByIntervalAwaitableConstraint
|
||||
{
|
||||
private readonly Action<DateTime> _SaveStateAction;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="PersistentCountByIntervalAwaitableConstraint"/>.
|
||||
/// </summary>
|
||||
/// <param name="count">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <param name="initialTimeStamps">Initial timestamps.</param>
|
||||
public PersistentCountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction, IEnumerable<DateTime> initialTimeStamps) : base(count, timeSpan)
|
||||
{
|
||||
_SaveStateAction = saveStateAction;
|
||||
|
||||
if (initialTimeStamps == null)
|
||||
return;
|
||||
|
||||
foreach (var timeStamp in initialTimeStamps)
|
||||
{
|
||||
_TimeStamps.Push(timeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save state
|
||||
/// </summary>
|
||||
protected override void OnEnded(DateTime now)
|
||||
{
|
||||
_SaveStateAction(now);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ComposableAsync;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// TimeLimiter implementation
|
||||
/// </summary>
|
||||
public class TimeLimiter : IDispatcher
|
||||
{
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint;
|
||||
|
||||
internal TimeLimiter(IAwaitableConstraint awaitableConstraint)
|
||||
{
|
||||
_AwaitableConstraint = awaitableConstraint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Func<Task> perform)
|
||||
{
|
||||
return Enqueue(perform, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> perform)
|
||||
{
|
||||
return Enqueue(perform, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Enqueue(Func<Task> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (await _AwaitableConstraint.WaitForReadiness(cancellationToken))
|
||||
{
|
||||
await perform();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (await _AwaitableConstraint.WaitForReadiness(cancellationToken))
|
||||
{
|
||||
return await perform();
|
||||
}
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new TimeLimiter(_AwaitableConstraint.Clone());
|
||||
|
||||
private static Func<Task> Transform(Action act)
|
||||
{
|
||||
return () => { act(); return Task.FromResult(0); };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="compute"></param>
|
||||
/// <returns></returns>
|
||||
private static Func<Task<T>> Transform<T>(Func<T> compute)
|
||||
{
|
||||
return () => Task.FromResult(compute());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Action perform)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<T> perform)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<T> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Action perform, CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a TimeLimiter based on a maximum number of times
|
||||
/// during a given period
|
||||
/// </summary>
|
||||
/// <param name="maxCount"></param>
|
||||
/// <param name="timeSpan"></param>
|
||||
/// <returns></returns>
|
||||
public static TimeLimiter GetFromMaxCountByInterval(int maxCount, TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeLimiter(new CountByIntervalAwaitableConstraint(maxCount, timeSpan));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create <see cref="TimeLimiter"/> that will save state using action passed through <paramref name="saveStateAction"/> parameter.
|
||||
/// </summary>
|
||||
/// <param name="maxCount">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <returns><see cref="TimeLimiter"/> instance with <see cref="PersistentCountByIntervalAwaitableConstraint"/>.</returns>
|
||||
public static TimeLimiter GetPersistentTimeLimiter(int maxCount, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction)
|
||||
{
|
||||
return GetPersistentTimeLimiter(maxCount, timeSpan, saveStateAction, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create <see cref="TimeLimiter"/> with initial timestamps that will save state using action passed through <paramref name="saveStateAction"/> parameter.
|
||||
/// </summary>
|
||||
/// <param name="maxCount">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <param name="initialTimeStamps">Initial timestamps.</param>
|
||||
/// <returns><see cref="TimeLimiter"/> instance with <see cref="PersistentCountByIntervalAwaitableConstraint"/>.</returns>
|
||||
public static TimeLimiter GetPersistentTimeLimiter(int maxCount, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction, IEnumerable<DateTime> initialTimeStamps)
|
||||
{
|
||||
return new TimeLimiter(new PersistentCountByIntervalAwaitableConstraint(maxCount, timeSpan, saveStateAction, initialTimeStamps));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose various IAwaitableConstraint in a TimeLimiter
|
||||
/// </summary>
|
||||
/// <param name="constraints"></param>
|
||||
/// <returns></returns>
|
||||
public static TimeLimiter Compose(params IAwaitableConstraint[] constraints)
|
||||
{
|
||||
var composed = constraints.Aggregate(default(IAwaitableConstraint),
|
||||
(accumulated, current) => (accumulated == null) ? current : accumulated.Compose(current));
|
||||
return new TimeLimiter(composed);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class TimeSystem : ITime
|
||||
{
|
||||
public static ITime StandardTime { get; }
|
||||
|
||||
static TimeSystem()
|
||||
{
|
||||
StandardTime = new TimeSystem();
|
||||
}
|
||||
|
||||
private TimeSystem()
|
||||
{
|
||||
}
|
||||
|
||||
DateTime ITime.GetNow()
|
||||
{
|
||||
return DateTime.Now;
|
||||
}
|
||||
|
||||
Task ITime.GetDelay(TimeSpan timespan, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Delay(timespan, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
[*.cs]
|
||||
|
||||
# CAC001: ConfigureAwaitChecker
|
||||
dotnet_diagnostic.CAC001.severity = error
|
|
@ -1,291 +0,0 @@
|
|||
using System;
|
||||
using TMDbLib.Objects.Account;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Objects.General;
|
||||
using ParameterType = TMDbLib.Rest.ParameterType;
|
||||
using RestClient = TMDbLib.Rest.RestClient;
|
||||
using RestRequest = TMDbLib.Rest.RestRequest;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities.Serializer;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient : IDisposable
|
||||
{
|
||||
private const string ApiVersion = "3";
|
||||
private const string ProductionUrl = "api.themoviedb.org";
|
||||
|
||||
private readonly ITMDbSerializer _serializer;
|
||||
private RestClient _client;
|
||||
private TMDbConfig _config;
|
||||
|
||||
public TMDbClient(string apiKey, bool useSsl = true, string baseUrl = ProductionUrl, ITMDbSerializer serializer = null, IWebProxy proxy = null)
|
||||
{
|
||||
DefaultLanguage = null;
|
||||
DefaultImageLanguage = null;
|
||||
DefaultCountry = null;
|
||||
|
||||
_serializer = serializer ?? TMDbJsonSerializer.Instance;
|
||||
|
||||
//Setup proxy to use during requests
|
||||
//Proxy is optional. If passed, will be used in every request.
|
||||
WebProxy = proxy;
|
||||
|
||||
Initialize(baseUrl, useSsl, apiKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The account details of the user account associated with the current user session
|
||||
/// </summary>
|
||||
/// <remarks>This value is automaticly populated when setting a user session</remarks>
|
||||
public AccountDetails ActiveAccount { get; private set; }
|
||||
|
||||
public string ApiKey { get; private set; }
|
||||
|
||||
public TMDbConfig Config
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasConfig)
|
||||
throw new InvalidOperationException("Call GetConfig() or SetConfig() first");
|
||||
return _config;
|
||||
}
|
||||
private set { _config = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ISO 3166-1 code. Ex. US
|
||||
/// </summary>
|
||||
public string DefaultCountry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 639-1 code. Ex en
|
||||
/// </summary>
|
||||
public string DefaultLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ISO 639-1 code. Ex en
|
||||
/// </summary>
|
||||
public string DefaultImageLanguage { get; set; }
|
||||
|
||||
public bool HasConfig { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Throw exceptions when TMDbs API returns certain errors, such as Not Found.
|
||||
/// </summary>
|
||||
public bool ThrowApiExceptions
|
||||
{
|
||||
get => _client.ThrowApiExceptions;
|
||||
set => _client.ThrowApiExceptions = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of times a call to TMDb will be retried
|
||||
/// </summary>
|
||||
/// <remarks>Default is 0</remarks>
|
||||
public int MaxRetryCount
|
||||
{
|
||||
get => _client.MaxRetryCount;
|
||||
set => _client.MaxRetryCount = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The request timeout call to TMDb
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout
|
||||
{
|
||||
get => _client.HttpClient.Timeout;
|
||||
set => _client.HttpClient.Timeout = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The session id that will be used when TMDb requires authentication
|
||||
/// </summary>
|
||||
/// <remarks>Use 'SetSessionInformation' to assign this value</remarks>
|
||||
public string SessionId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the session id, this will determine the level of access that is granted on the API
|
||||
/// </summary>
|
||||
/// <remarks>Use 'SetSessionInformation' to assign this value</remarks>
|
||||
public SessionType SessionType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Web Proxy to use during requests to TMDb API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Web Proxy is optional. If set, every request will be sent through it.
|
||||
/// Use the constructor for setting it.
|
||||
///
|
||||
/// For convenience, this library also offers a <see cref="IWebProxy"/> implementation.
|
||||
/// Check <see cref="Utilities.TMDbAPIProxy"/> for more information.
|
||||
/// </remarks>
|
||||
public IWebProxy WebProxy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used internally to assign a session id to a request. If no valid session is found, an exception is thrown.
|
||||
/// </summary>
|
||||
/// <param name="req">Request</param>
|
||||
/// <param name="targetType">The target session type to set. If set to Unassigned, the method will take the currently set session.</param>
|
||||
/// <param name="parameterType">The location of the paramter in the resulting query</param>
|
||||
private void AddSessionId(RestRequest req, SessionType targetType = SessionType.Unassigned, ParameterType parameterType = ParameterType.QueryString)
|
||||
{
|
||||
if ((targetType == SessionType.Unassigned && SessionType == SessionType.GuestSession) ||
|
||||
(targetType == SessionType.GuestSession))
|
||||
{
|
||||
// Either
|
||||
// - We needed ANY session ID and had a Guest session id
|
||||
// - We needed a Guest session id and had it
|
||||
req.AddParameter("guest_session_id", SessionId, parameterType);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((targetType == SessionType.Unassigned && SessionType == SessionType.UserSession) ||
|
||||
(targetType == SessionType.UserSession))
|
||||
{
|
||||
// Either
|
||||
// - We needed ANY session ID and had a User session id
|
||||
// - We needed a User session id and had it
|
||||
req.AddParameter("session_id", SessionId, parameterType);
|
||||
return;
|
||||
}
|
||||
|
||||
// We did not have the required session type ready
|
||||
throw new UserSessionRequiredException();
|
||||
}
|
||||
|
||||
public async Task<TMDbConfig> GetConfigAsync()
|
||||
{
|
||||
TMDbConfig config = await _client.Create("configuration").GetOfT<TMDbConfig>(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (config == null)
|
||||
throw new Exception("Unable to retrieve configuration");
|
||||
|
||||
// Store config
|
||||
Config = config;
|
||||
HasConfig = true;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public Uri GetImageUrl(string size, string filePath, bool useSsl = false)
|
||||
{
|
||||
string baseUrl = useSsl ? Config.Images.SecureBaseUrl : Config.Images.BaseUrl;
|
||||
return new Uri(baseUrl + size + filePath);
|
||||
}
|
||||
|
||||
[Obsolete("Use " + nameof(GetImageBytesAsync))]
|
||||
public Task<byte[]> GetImageBytes(string size, string filePath, bool useSsl = false, CancellationToken token = default)
|
||||
{
|
||||
return GetImageBytesAsync(size, filePath, useSsl, token);
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetImageBytesAsync(string size, string filePath, bool useSsl = false, CancellationToken token = default)
|
||||
{
|
||||
Uri url = GetImageUrl(size, filePath, useSsl);
|
||||
|
||||
using HttpResponseMessage response = await _client.HttpClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, token).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP003:Dispose previous before re-assigning.", Justification = "Only called from ctor")]
|
||||
private void Initialize(string baseUrl, bool useSsl, string apiKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
throw new ArgumentException("baseUrl");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
throw new ArgumentException("apiKey");
|
||||
|
||||
ApiKey = apiKey;
|
||||
|
||||
// Cleanup the provided url so that we don't get any issues when we are configuring the client
|
||||
if (baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
baseUrl = baseUrl.Substring("http://".Length);
|
||||
else if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
baseUrl = baseUrl.Substring("https://".Length);
|
||||
|
||||
string httpScheme = useSsl ? "https" : "http";
|
||||
|
||||
_client = new RestClient(new Uri(string.Format("{0}://{1}/{2}/", httpScheme, baseUrl, ApiVersion)), _serializer, WebProxy);
|
||||
_client.AddDefaultQueryString("api_key", apiKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used internally to determine if the current client has the required session set, if not an appropriate exception will be thrown
|
||||
/// </summary>
|
||||
/// <param name="sessionType">The type of session that is required by the calling method</param>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown if the calling method requires a user session and one isn't set on the client object</exception>
|
||||
/// <exception cref="GuestSessionRequiredException">Thrown if the calling method requires a guest session and no session is set on the client object. (neither user or client type session)</exception>
|
||||
private void RequireSessionId(SessionType sessionType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SessionId))
|
||||
{
|
||||
if (sessionType == SessionType.GuestSession)
|
||||
throw new UserSessionRequiredException();
|
||||
else
|
||||
throw new GuestSessionRequiredException();
|
||||
}
|
||||
|
||||
if (sessionType == SessionType.UserSession && SessionType == SessionType.GuestSession)
|
||||
throw new UserSessionRequiredException();
|
||||
}
|
||||
|
||||
public void SetConfig(TMDbConfig config)
|
||||
{
|
||||
// Store config
|
||||
Config = config;
|
||||
HasConfig = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this method to set the current client's authentication information.
|
||||
/// The session id assigned here will be used by the client when ever TMDb requires it.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session id to use when making calls that require authentication</param>
|
||||
/// <param name="sessionType">The type of session id</param>
|
||||
/// <remarks>
|
||||
/// - Use the 'AuthenticationGetUserSessionAsync' and 'AuthenticationCreateGuestSessionAsync' methods to optain the respective session ids.
|
||||
/// - User sessions have access to far for methods than guest sessions, these can currently only be used to rate media.
|
||||
/// </remarks>
|
||||
public async Task SetSessionInformationAsync(string sessionId, SessionType sessionType)
|
||||
{
|
||||
ActiveAccount = null;
|
||||
SessionId = sessionId;
|
||||
if (!string.IsNullOrWhiteSpace(sessionId) && sessionType == SessionType.Unassigned)
|
||||
{
|
||||
throw new ArgumentException("When setting the session id it must always be either a guest or user session");
|
||||
}
|
||||
|
||||
SessionType = string.IsNullOrWhiteSpace(sessionId) ? SessionType.Unassigned : sessionType;
|
||||
|
||||
// Populate the related account information
|
||||
if (sessionType == SessionType.UserSession)
|
||||
{
|
||||
try
|
||||
{
|
||||
ActiveAccount = await AccountGetDetailsAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Unable to complete the full process so reset all values and throw the exception
|
||||
ActiveAccount = null;
|
||||
SessionId = null;
|
||||
SessionType = SessionType.Unassigned;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
using TMDbLib.Utilities;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Account;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Lists;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<SearchContainer<T>> GetAccountListInternal<T>(int page, AccountSortBy sortBy, SortOrder sortOrder, string language, AccountListsMethods method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest request = _client.Create("account/{accountId}/" + method.GetDescription());
|
||||
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
if (page > 1)
|
||||
request.AddParameter("page", page.ToString());
|
||||
|
||||
if (sortBy != AccountSortBy.Undefined)
|
||||
request.AddParameter("sort_by", sortBy.GetDescription());
|
||||
|
||||
if (sortOrder != SortOrder.Undefined)
|
||||
request.AddParameter("sort_order", sortOrder.GetDescription());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
request.AddParameter("language", language);
|
||||
|
||||
SearchContainer<T> response = await request.GetOfT<SearchContainer<T>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the favorite status of a specific movie. Either make the movie a favorite or remove that status depending on the supplied boolean value.
|
||||
/// </summary>
|
||||
/// <param name="mediaType">The type of media to influence</param>
|
||||
/// <param name="mediaId">The id of the movie/tv show to influence</param>
|
||||
/// <param name="isFavorite">True if you want the specified movie to be marked as favorite, false if not</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True if the the movie's favorite status was successfully updated, false if not</returns>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> AccountChangeFavoriteStatusAsync(MediaType mediaType, int mediaId, bool isFavorite, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest request = _client.Create("account/{accountId}/favorite");
|
||||
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
|
||||
request.SetBody(new { media_type = mediaType.GetDescription(), media_id = mediaId, favorite = isFavorite });
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
PostReply response = await request.PostOfT<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// status code 1 = "Success" - Returned when adding a movie as favorite for the first time
|
||||
// status code 13 = "The item/record was deleted successfully" - When removing an item as favorite, no matter if it exists or not
|
||||
// status code 12 = "The item/record was updated successfully" - Used when an item is already marked as favorite and trying to do so doing again
|
||||
return response.StatusCode == 1 || response.StatusCode == 12 || response.StatusCode == 13;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the state of a specific movie on the users watchlist. Either add the movie to the list or remove it, depending on the specified boolean value.
|
||||
/// </summary>
|
||||
/// <param name="mediaType">The type of media to influence</param>
|
||||
/// <param name="mediaId">The id of the movie/tv show to influence</param>
|
||||
/// <param name="isOnWatchlist">True if you want the specified movie to be part of the watchlist, false if not</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>True if the the movie's status on the watchlist was successfully updated, false if not</returns>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> AccountChangeWatchlistStatusAsync(MediaType mediaType, int mediaId, bool isOnWatchlist, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest request = _client.Create("account/{accountId}/watchlist");
|
||||
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
|
||||
request.SetBody(new { media_type = mediaType.GetDescription(), media_id = mediaId, watchlist = isOnWatchlist });
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
PostReply response = await request.PostOfT<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// status code 1 = "Success"
|
||||
// status code 13 = "The item/record was deleted successfully" - When removing an item from the watchlist, no matter if it exists or not
|
||||
// status code 12 = "The item/record was updated successfully" - Used when an item is already on the watchlist and trying to add it again
|
||||
return response.StatusCode == 1 || response.StatusCode == 12 || response.StatusCode == 13;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will retrieve the details of the account associated with the current session id
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<AccountDetails> AccountGetDetailsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest request = _client.Create("account");
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
AccountDetails response = await request.GetOfT<AccountDetails>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the movies marked as favorite by the current user
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<SearchMovie>> AccountGetFavoriteMoviesAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<SearchMovie>(page, sortBy, sortOrder, language, AccountListsMethods.FavoriteMovies, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the tv shows marked as favorite by the current user
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<SearchTv>> AccountGetFavoriteTvAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<SearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.FavoriteTv, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all lists associated with the provided account id
|
||||
/// This can be lists that were created by the user or lists marked as favorite
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<AccountList>> AccountGetListsAsync(int page = 1, string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest request = _client.Create("account/{accountId}/lists");
|
||||
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
if (page > 1)
|
||||
{
|
||||
request.AddQueryString("page", page.ToString());
|
||||
}
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
request.AddQueryString("language", language);
|
||||
|
||||
SearchContainer<AccountList> response = await request.GetOfT<SearchContainer<AccountList>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the movies on the current users match list
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<SearchMovie>> AccountGetMovieWatchlistAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<SearchMovie>(page, sortBy, sortOrder, language, AccountListsMethods.MovieWatchlist, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the movies rated by the current user
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<SearchMovieWithRating>> AccountGetRatedMoviesAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<SearchMovieWithRating>(page, sortBy, sortOrder, language, AccountListsMethods.RatedMovies, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the tv show episodes rated by the current user
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<AccountSearchTvEpisode>> AccountGetRatedTvShowEpisodesAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<AccountSearchTvEpisode>(page, sortBy, sortOrder, language, AccountListsMethods.RatedTvEpisodes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the tv shows rated by the current user
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<AccountSearchTv>> AccountGetRatedTvShowsAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<AccountSearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.RatedTv, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the tv shows on the current users match list
|
||||
/// </summary>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<SearchContainer<SearchTv>> AccountGetTvWatchlistAsync(
|
||||
int page = 1,
|
||||
AccountSortBy sortBy = AccountSortBy.Undefined,
|
||||
SortOrder sortOrder = SortOrder.Undefined,
|
||||
string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetAccountListInternal<SearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.TvWatchlist, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private enum AccountListsMethods
|
||||
{
|
||||
[EnumValue("favorite/movies")]
|
||||
FavoriteMovies,
|
||||
[EnumValue("favorite/tv")]
|
||||
FavoriteTv,
|
||||
[EnumValue("rated/movies")]
|
||||
RatedMovies,
|
||||
[EnumValue("rated/tv")]
|
||||
RatedTv,
|
||||
[EnumValue("rated/tv/episodes")]
|
||||
RatedTvEpisodes,
|
||||
[EnumValue("watchlist/movies")]
|
||||
MovieWatchlist,
|
||||
[EnumValue("watchlist/tv")]
|
||||
TvWatchlist,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<GuestSession> AuthenticationCreateGuestSessionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest request = _client.Create("authentication/guest_session/new");
|
||||
//{
|
||||
// DateFormat = "yyyy-MM-dd HH:mm:ss UTC"
|
||||
//};
|
||||
|
||||
GuestSession response = await request.GetOfT<GuestSession>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<UserSession> AuthenticationGetUserSessionAsync(string initialRequestToken, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest request = _client.Create("authentication/session/new");
|
||||
request.AddParameter("request_token", initialRequestToken);
|
||||
|
||||
using RestResponse<UserSession> response = await request.Get<UserSession>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
throw new UnauthorizedAccessException();
|
||||
|
||||
return await response.GetDataObject().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conveniance method combining 'AuthenticationRequestAutenticationTokenAsync', 'AuthenticationValidateUserTokenAsync' and 'AuthenticationGetUserSessionAsync'
|
||||
/// </summary>
|
||||
/// <param name="username">A valid TMDb username</param>
|
||||
/// <param name="password">The passoword for the provided login</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<UserSession> AuthenticationGetUserSessionAsync(string username, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Token token = await AuthenticationRequestAutenticationTokenAsync(cancellationToken).ConfigureAwait(false);
|
||||
await AuthenticationValidateUserTokenAsync(token.RequestToken, username, password, cancellationToken).ConfigureAwait(false);
|
||||
return await AuthenticationGetUserSessionAsync(token.RequestToken, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Token> AuthenticationRequestAutenticationTokenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest request = _client.Create("authentication/token/new");
|
||||
|
||||
using RestResponse<Token> response = await request.Get<Token>(cancellationToken).ConfigureAwait(false);
|
||||
Token token = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
token.AuthenticationCallback = response.GetHeader("Authentication-Callback");
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task AuthenticationValidateUserTokenAsync(string initialRequestToken, string username, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest request = _client.Create("authentication/token/validate_with_login");
|
||||
request.AddParameter("request_token", initialRequestToken);
|
||||
request.AddParameter("username", username);
|
||||
request.AddParameter("password", password);
|
||||
|
||||
RestResponse response;
|
||||
try
|
||||
{
|
||||
response = await request.Get(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
throw ex.InnerException;
|
||||
}
|
||||
|
||||
using RestResponse _ = response;
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new UnauthorizedAccessException("Call to TMDb returned unauthorized. Most likely the provided user credentials are invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Certifications;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<CertificationsContainer> GetMovieCertificationsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("certification/movie/list");
|
||||
|
||||
CertificationsContainer resp = await req.GetOfT<CertificationsContainer>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<CertificationsContainer> GetTvCertificationsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("certification/tv/list");
|
||||
|
||||
CertificationsContainer resp = await req.GetOfT<CertificationsContainer>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Changes;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<T> GetChangesInternal<T>(string type, int page = 0, int? id = null, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string resource;
|
||||
if (id.HasValue)
|
||||
resource = "{type}/{id}/changes";
|
||||
else
|
||||
resource = "{type}/changes";
|
||||
|
||||
RestRequest req = _client.Create(resource);
|
||||
req.AddUrlSegment("type", type);
|
||||
|
||||
if (id.HasValue)
|
||||
req.AddUrlSegment("id", id.Value.ToString());
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (startDate.HasValue)
|
||||
req.AddParameter("start_date", startDate.Value.ToString("yyyy-MM-dd"));
|
||||
if (endDate != null)
|
||||
req.AddParameter("end_date", endDate.Value.ToString("yyyy-MM-dd"));
|
||||
|
||||
using RestResponse<T> resp = await req.Get<T>(cancellationToken).ConfigureAwait(false);
|
||||
T res = await resp.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
if (res is SearchContainer<ChangesListItem> asSearch)
|
||||
{
|
||||
// https://github.com/LordMike/TMDbLib/issues/296
|
||||
asSearch.Results.RemoveAll(s => s.Id == 0);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of movie ids that have been edited.
|
||||
/// By default we show the last 24 hours and only 100 items per page.
|
||||
/// The maximum number of days that can be returned in a single request is 14.
|
||||
/// You can then use the movie changes API to get the actual data that has been changed. (.GetMovieChangesAsync)
|
||||
/// </summary>
|
||||
/// <remarks>the change log system to support this was changed on October 5, 2012 and will only show movies that have been edited since.</remarks>
|
||||
public async Task<SearchContainer<ChangesListItem>> GetMoviesChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetChangesInternal<SearchContainer<ChangesListItem>>("movie", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of people ids that have been edited.
|
||||
/// By default we show the last 24 hours and only 100 items per page.
|
||||
/// The maximum number of days that can be returned in a single request is 14.
|
||||
/// You can then use the person changes API to get the actual data that has been changed.(.GetPersonChangesAsync)
|
||||
/// </summary>
|
||||
/// <remarks>the change log system to support this was changed on October 5, 2012 and will only show people that have been edited since.</remarks>
|
||||
public async Task<SearchContainer<ChangesListItem>> GetPeopleChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetChangesInternal<SearchContainer<ChangesListItem>>("person", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of TV show ids that have been edited.
|
||||
/// By default we show the last 24 hours and only 100 items per page.
|
||||
/// The maximum number of days that can be returned in a single request is 14.
|
||||
/// You can then use the TV changes API to get the actual data that has been changed. (.GetTvShowChangesAsync)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// the change log system to properly support TV was updated on May 13, 2014.
|
||||
/// You'll likely only find the edits made since then to be useful in the change log system.
|
||||
/// </remarks>
|
||||
public async Task<SearchContainer<ChangesListItem>> GetTvChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetChangesInternal<SearchContainer<ChangesListItem>>("tv", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IList<Change>> GetMovieChangesAsync(int movieId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("movie", page, movieId, startDate, endDate, cancellationToken).ConfigureAwait(false);
|
||||
return changesContainer.Changes;
|
||||
}
|
||||
|
||||
public async Task<IList<Change>> GetPersonChangesAsync(int personId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("person", page, personId, startDate, endDate, cancellationToken).ConfigureAwait(false);
|
||||
return changesContainer.Changes;
|
||||
}
|
||||
|
||||
public async Task<IList<Change>> GetTvSeasonChangesAsync(int seasonId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("tv/season", page, seasonId, startDate, endDate, cancellationToken).ConfigureAwait(false);
|
||||
return changesContainer.Changes;
|
||||
}
|
||||
|
||||
public async Task<IList<Change>> GetTvEpisodeChangesAsync(int episodeId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("tv/episode", page, episodeId, startDate, endDate, cancellationToken).ConfigureAwait(false);
|
||||
return changesContainer.Changes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Collections;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<T> GetCollectionMethodInternal<T>(int collectionId, CollectionMethods collectionMethod, string language = null, CancellationToken cancellationToken = default) where T : new()
|
||||
{
|
||||
RestRequest req = _client.Create("collection/{collectionId}/{method}");
|
||||
req.AddUrlSegment("collectionId", collectionId.ToString());
|
||||
req.AddUrlSegment("method", collectionMethod.GetDescription());
|
||||
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
|
||||
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<Collection> GetCollectionAsync(int collectionId, CollectionMethods extraMethods = CollectionMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetCollectionAsync(collectionId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Collection> GetCollectionAsync(int collectionId, string language, string includeImageLanguages, CollectionMethods extraMethods = CollectionMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("collection/{collectionId}");
|
||||
req.AddUrlSegment("collectionId", collectionId.ToString());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
includeImageLanguages ??= DefaultImageLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(includeImageLanguages))
|
||||
req.AddParameter("include_image_language", includeImageLanguages);
|
||||
|
||||
string appends = string.Join(",",
|
||||
Enum.GetValues(typeof(CollectionMethods))
|
||||
.OfType<CollectionMethods>()
|
||||
.Except(new[] { CollectionMethods.Undefined })
|
||||
.Where(s => extraMethods.HasFlag(s))
|
||||
.Select(s => s.GetDescription()));
|
||||
|
||||
if (appends != string.Empty)
|
||||
req.AddParameter("append_to_response", appends);
|
||||
|
||||
//req.DateFormat = "yyyy-MM-dd";
|
||||
|
||||
using RestResponse<Collection> response = await req.Get<Collection>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsValid)
|
||||
return null;
|
||||
|
||||
Collection item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
if (item != null)
|
||||
item.Overview = WebUtility.HtmlDecode(item.Overview);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public async Task<ImagesWithId> GetCollectionImagesAsync(int collectionId, string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetCollectionMethodInternal<ImagesWithId>(collectionId, CollectionMethods.Images, language, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Companies;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<T> GetCompanyMethodInternal<T>(int companyId, CompanyMethods companyMethod, int page = 0, string language = null, CancellationToken cancellationToken = default) where T : new()
|
||||
{
|
||||
RestRequest req = _client.Create("company/{companyId}/{method}");
|
||||
req.AddUrlSegment("companyId", companyId.ToString());
|
||||
req.AddUrlSegment("method", companyMethod.GetDescription());
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<Company> GetCompanyAsync(int companyId, CompanyMethods extraMethods = CompanyMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("company/{companyId}");
|
||||
req.AddUrlSegment("companyId", companyId.ToString());
|
||||
|
||||
string appends = string.Join(",",
|
||||
Enum.GetValues(typeof(CompanyMethods))
|
||||
.OfType<CompanyMethods>()
|
||||
.Except(new[] { CompanyMethods.Undefined })
|
||||
.Where(s => extraMethods.HasFlag(s))
|
||||
.Select(s => s.GetDescription()));
|
||||
|
||||
if (appends != string.Empty)
|
||||
req.AddParameter("append_to_response", appends);
|
||||
|
||||
//req.DateFormat = "yyyy-MM-dd";
|
||||
|
||||
Company resp = await req.GetOfT<Company>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetCompanyMoviesAsync(int companyId, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetCompanyMoviesAsync(companyId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetCompanyMoviesAsync(int companyId, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetCompanyMethodInternal<SearchContainerWithId<SearchMovie>>(companyId, CompanyMethods.Movies, page, language, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Configuration;
|
||||
using TMDbLib.Objects.Countries;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Languages;
|
||||
using TMDbLib.Objects.Timezones;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<APIConfiguration> GetAPIConfiguration(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("configuration");
|
||||
|
||||
using RestResponse<APIConfiguration> response = await req.Get<APIConfiguration>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public async Task<List<Country>> GetCountriesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("configuration/countries");
|
||||
|
||||
using RestResponse<List<Country>> response = await req.Get<List<Country>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await response.GetDataObject().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<Language>> GetLanguagesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("configuration/languages");
|
||||
|
||||
using RestResponse<List<Language>> response = await req.Get<List<Language>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetPrimaryTranslationsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("configuration/primary_translations");
|
||||
|
||||
using RestResponse<List<string>> response = await req.Get<List<string>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public async Task<Timezones> GetTimezonesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("timezones/list");
|
||||
|
||||
using RestResponse<List<Dictionary<string, List<string>>>> resp = await req.Get<List<Dictionary<string, List<string>>>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
List<Dictionary<string, List<string>>> item = await resp.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
if (item == null)
|
||||
return null;
|
||||
|
||||
Timezones result = new Timezones();
|
||||
result.List = new Dictionary<string, List<string>>();
|
||||
|
||||
foreach (Dictionary<string, List<string>> dictionary in item)
|
||||
{
|
||||
KeyValuePair<string, List<string>> item1 = dictionary.First();
|
||||
|
||||
result.List[item1.Key] = item1.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of departments and positions within
|
||||
/// </summary>
|
||||
/// <returns>Valid jobs and their departments</returns>
|
||||
public async Task<List<Job>> GetJobsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("configuration/jobs");
|
||||
|
||||
using RestResponse<List<Job>> response = await req.Get<List<Job>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Credit;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<Credit> GetCreditsAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetCreditsAsync(id, DefaultLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Credit> GetCreditsAsync(string id, string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("credit/{id}");
|
||||
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
req.AddUrlSegment("id", id);
|
||||
|
||||
Credit resp = await req.GetOfT<Credit>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Discover;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Can be used to discover movies matching certain criteria
|
||||
/// </summary>
|
||||
public DiscoverMovie DiscoverMoviesAsync()
|
||||
{
|
||||
return new DiscoverMovie(this);
|
||||
}
|
||||
|
||||
internal async Task<SearchContainer<T>> DiscoverPerformAsync<T>(string endpoint, string language, int page, SimpleNamedValueCollection parameters, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest request = _client.Create(endpoint);
|
||||
|
||||
if (page != 1 && page > 1)
|
||||
request.AddParameter("page", page.ToString());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
request.AddParameter("language", language);
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in parameters)
|
||||
request.AddParameter(pair.Key, pair.Value);
|
||||
|
||||
SearchContainer<T> response = await request.GetOfT<SearchContainer<T>>(cancellationToken).ConfigureAwait(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to discover new tv shows matching certain criteria
|
||||
/// </summary>
|
||||
public DiscoverTv DiscoverTvShowsAsync()
|
||||
{
|
||||
return new DiscoverTv(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Find;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
/// <summary>
|
||||
/// FindAsync movies, people and tv shows by an external id.
|
||||
/// The following types can be found based on the specified external id's
|
||||
/// - Movies: Imdb
|
||||
/// - People: Imdb, FreeBaseMid, FreeBaseId, TvRage
|
||||
/// - TV Series: Imdb, FreeBaseMid, FreeBaseId, TvRage, TvDb
|
||||
/// </summary>
|
||||
/// <param name="source">The source the specified id belongs to</param>
|
||||
/// <param name="id">The id of the object you wish to located</param>
|
||||
/// <returns>A list of all objects in TMDb that matched your id</returns>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public Task<FindContainer> FindAsync(FindExternalSource source, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FindAsync(source, id, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FindAsync movies, people and tv shows by an external id.
|
||||
/// The following types can be found based on the specified external id's
|
||||
/// - Movies: Imdb
|
||||
/// - People: Imdb, FreeBaseMid, FreeBaseId, TvRage
|
||||
/// - TV Series: Imdb, FreeBaseMid, FreeBaseId, TvRage, TvDb
|
||||
/// </summary>
|
||||
/// <param name="source">The source the specified id belongs to</param>
|
||||
/// <param name="id">The id of the object you wish to located</param>
|
||||
/// <returns>A list of all objects in TMDb that matched your id</returns>
|
||||
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<FindContainer> FindAsync(FindExternalSource source, string id, string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("find/{id}");
|
||||
|
||||
req.AddUrlSegment("id", WebUtility.UrlEncode(id));
|
||||
req.AddParameter("external_source", source.GetDescription());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
FindContainer resp = await req.GetOfT<FindContainer>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Genres;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
[Obsolete("GetGenreMovies is deprecated, use DiscoverMovies instead")]
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetGenreMoviesAsync(int genreId, int page = 0, bool? includeAllMovies = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetGenreMoviesAsync(genreId, DefaultLanguage, page, includeAllMovies, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Obsolete("GetGenreMovies is deprecated, use DiscoverMovies instead")]
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetGenreMoviesAsync(int genreId, string language, int page = 0, bool? includeAllMovies = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("genre/{genreId}/movies");
|
||||
req.AddUrlSegment("genreId", genreId.ToString());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (includeAllMovies.HasValue)
|
||||
req.AddParameter("include_all_movies", includeAllMovies.Value ? "true" : "false");
|
||||
|
||||
SearchContainerWithId<SearchMovie> resp = await req.GetOfT<SearchContainerWithId<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<List<Genre>> GetMovieGenresAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieGenresAsync(DefaultLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<Genre>> GetMovieGenresAsync(string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("genre/movie/list");
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
using RestResponse<GenreContainer> resp = await req.Get<GenreContainer>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await resp.GetDataObject().ConfigureAwait(false)).Genres;
|
||||
}
|
||||
|
||||
public async Task<List<Genre>> GetTvGenresAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetTvGenresAsync(DefaultLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<List<Genre>> GetTvGenresAsync(string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("genre/tv/list");
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
using RestResponse<GenreContainer> resp = await req.Get<GenreContainer>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await resp.GetDataObject().ConfigureAwait(false)).Genres;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Objects.TvShows;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<SearchContainer<SearchMovieWithRating>> GetGuestSessionRatedMoviesAsync(int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetGuestSessionRatedMoviesAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovieWithRating>> GetGuestSessionRatedMoviesAsync(string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.GuestSession);
|
||||
|
||||
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/movies");
|
||||
|
||||
if (page > 0)
|
||||
request.AddParameter("page", page.ToString());
|
||||
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
request.AddParameter("language", language);
|
||||
|
||||
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
|
||||
|
||||
SearchContainer<SearchMovieWithRating> resp = await request.GetOfT<SearchContainer<SearchMovieWithRating>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchTvShowWithRating>> GetGuestSessionRatedTvAsync(int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetGuestSessionRatedTvAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchTvShowWithRating>> GetGuestSessionRatedTvAsync(string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.GuestSession);
|
||||
|
||||
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/tv");
|
||||
|
||||
if (page > 0)
|
||||
request.AddParameter("page", page.ToString());
|
||||
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
request.AddParameter("language", language);
|
||||
|
||||
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
|
||||
|
||||
SearchContainer<SearchTvShowWithRating> resp = await request.GetOfT<SearchContainer<SearchTvShowWithRating>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<TvEpisodeWithRating>> GetGuestSessionRatedTvEpisodesAsync(int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetGuestSessionRatedTvEpisodesAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<TvEpisodeWithRating>> GetGuestSessionRatedTvEpisodesAsync(string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.GuestSession);
|
||||
|
||||
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/tv/episodes");
|
||||
|
||||
if (page > 0)
|
||||
request.AddParameter("page", page.ToString());
|
||||
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
request.AddParameter("language", language);
|
||||
|
||||
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
|
||||
|
||||
SearchContainer<TvEpisodeWithRating> resp = await request.GetOfT<SearchContainer<TvEpisodeWithRating>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
public async Task<Keyword> GetKeywordAsync(int keywordId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("keyword/{keywordId}");
|
||||
req.AddUrlSegment("keywordId", keywordId.ToString());
|
||||
|
||||
Keyword resp = await req.GetOfT<Keyword>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetKeywordMoviesAsync(int keywordId, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetKeywordMoviesAsync(keywordId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<SearchMovie>> GetKeywordMoviesAsync(int keywordId, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("keyword/{keywordId}/movies");
|
||||
req.AddUrlSegment("keywordId", keywordId.ToString());
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
|
||||
SearchContainerWithId<SearchMovie> resp = await req.GetOfT<SearchContainerWithId<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Lists;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<bool> GetManipulateMediaListAsyncInternal(string listId, int movieId, string method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
throw new ArgumentNullException(nameof(listId));
|
||||
|
||||
// Movie Id is expected by the API and can not be null
|
||||
if (movieId <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(movieId));
|
||||
|
||||
RestRequest req = _client.Create("list/{listId}/{method}");
|
||||
req.AddUrlSegment("listId", listId);
|
||||
req.AddUrlSegment("method", method);
|
||||
AddSessionId(req, SessionType.UserSession);
|
||||
|
||||
req.SetBody(new { media_id = movieId });
|
||||
|
||||
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Status code 12 = "The item/record was updated successfully"
|
||||
// Status code 13 = "The item/record was deleted successfully"
|
||||
PostReply item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// TODO: Previous code checked for item=null
|
||||
return item.StatusCode == 12 || item.StatusCode == 13;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list by it's id
|
||||
/// </summary>
|
||||
/// <param name="listId">The id of the list you want to retrieve</param>
|
||||
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<GenericList> GetListAsync(string listId, string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
throw new ArgumentNullException(nameof(listId));
|
||||
|
||||
RestRequest req = _client.Create("list/{listId}");
|
||||
req.AddUrlSegment("listId", listId);
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
GenericList resp = await req.GetOfT<GenericList>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will check if the provided movie id is present in the specified list
|
||||
/// </summary>
|
||||
/// <param name="listId">Id of the list to check in</param>
|
||||
/// <param name="movieId">Id of the movie to check for in the list</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<bool> GetListIsMoviePresentAsync(string listId, int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
throw new ArgumentNullException(nameof(listId));
|
||||
|
||||
if (movieId <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(movieId));
|
||||
|
||||
RestRequest req = _client.Create("list/{listId}/item_status");
|
||||
req.AddUrlSegment("listId", listId);
|
||||
req.AddParameter("movie_id", movieId.ToString());
|
||||
|
||||
using RestResponse<ListStatus> response = await req.Get<ListStatus>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false)).ItemPresent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a movie to a specified list
|
||||
/// </summary>
|
||||
/// <param name="listId">The id of the list to add the movie to</param>
|
||||
/// <param name="movieId">The id of the movie to add</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>True if the method was able to add the movie to the list, will retrun false in case of an issue or when the movie was already added to the list</returns>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> ListAddMovieAsync(string listId, int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetManipulateMediaListAsyncInternal(listId, movieId, "add_item", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a list, without confirmation.
|
||||
/// </summary>
|
||||
/// <param name="listId">The id of the list to clear</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>True if the method was able to remove the movie from the list, will retrun false in case of an issue or when the movie was not present in the list</returns>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> ListClearAsync(string listId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
throw new ArgumentNullException(nameof(listId));
|
||||
|
||||
RestRequest request = _client.Create("list/{listId}/clear");
|
||||
request.AddUrlSegment("listId", listId);
|
||||
request.AddParameter("confirm", "true");
|
||||
AddSessionId(request, SessionType.UserSession);
|
||||
|
||||
using RestResponse<PostReply> response = await request.Post<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Status code 12 = "The item/record was updated successfully"
|
||||
PostReply item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// TODO: Previous code checked for item=null
|
||||
return item.StatusCode == 12;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new list for the user associated with the current session
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the new list</param>
|
||||
/// <param name="description">Optional description for the list</param>
|
||||
/// <param name="language">Optional language that might indicate the language of the content in the list</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<string> ListCreateAsync(string name, string description = "", string language = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
// Description is expected by the API and can not be null
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
description = "";
|
||||
|
||||
RestRequest req = _client.Create("list");
|
||||
AddSessionId(req, SessionType.UserSession);
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
req.SetBody(new { name = name, description = description, language = language });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
req.SetBody(new { name = name, description = description });
|
||||
}
|
||||
|
||||
using RestResponse<ListCreateReply> response = await req.Post<ListCreateReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (await response.GetDataObject().ConfigureAwait(false)).ListId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified list that is owned by the user
|
||||
/// </summary>
|
||||
/// <param name="listId">A list id that is owned by the user associated with the current session id</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> ListDeleteAsync(string listId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(listId))
|
||||
throw new ArgumentNullException(nameof(listId));
|
||||
|
||||
RestRequest req = _client.Create("list/{listId}");
|
||||
req.AddUrlSegment("listId", listId);
|
||||
AddSessionId(req, SessionType.UserSession);
|
||||
|
||||
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Status code 13 = success
|
||||
PostReply item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// TODO: Previous code checked for item=null
|
||||
return item.StatusCode == 13;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a movie from the specified list
|
||||
/// </summary>
|
||||
/// <param name="listId">The id of the list to add the movie to</param>
|
||||
/// <param name="movieId">The id of the movie to add</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>True if the method was able to remove the movie from the list, will retrun false in case of an issue or when the movie was not present in the list</returns>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<bool> ListRemoveMovieAsync(string listId, int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetManipulateMediaListAsyncInternal(listId, movieId, "remove_item", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,392 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.Movies;
|
||||
using TMDbLib.Objects.Reviews;
|
||||
using TMDbLib.Objects.Search;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
using Credits = TMDbLib.Objects.Movies.Credits;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<T> GetMovieMethodInternal<T>(int movieId, MovieMethods movieMethod, string dateFormat = null,
|
||||
string country = null,
|
||||
string language = null, string includeImageLanguage = null, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default) where T : new()
|
||||
{
|
||||
RestRequest req = _client.Create("movie/{movieId}/{method}");
|
||||
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
|
||||
req.AddUrlSegment("method", movieMethod.GetDescription());
|
||||
|
||||
if (country != null)
|
||||
req.AddParameter("country", country);
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
|
||||
req.AddParameter("include_image_language", includeImageLanguage);
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (startDate.HasValue)
|
||||
req.AddParameter("start_date", startDate.Value.ToString("yyyy-MM-dd"));
|
||||
if (endDate != null)
|
||||
req.AddParameter("end_date", endDate.Value.ToString("yyyy-MM-dd"));
|
||||
|
||||
T response = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all information for a specific movie in relation to the current user account
|
||||
/// </summary>
|
||||
/// <param name="movieId">The id of the movie to get the account states for</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <remarks>Requires a valid user session</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
|
||||
public async Task<AccountState> GetMovieAccountStateAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest req = _client.Create("movie/{movieId}/{method}");
|
||||
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
|
||||
req.AddUrlSegment("method", MovieMethods.AccountStates.GetDescription());
|
||||
AddSessionId(req, SessionType.UserSession);
|
||||
|
||||
using RestResponse<AccountState> response = await req.Get<AccountState>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await response.GetDataObject().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AlternativeTitles> GetMovieAlternativeTitlesAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieAlternativeTitlesAsync(movieId, DefaultCountry, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AlternativeTitles> GetMovieAlternativeTitlesAsync(int movieId, string country, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<AlternativeTitles>(movieId, MovieMethods.AlternativeTitles, country: country, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Movie> GetMovieAsync(int movieId, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieAsync(movieId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Movie> GetMovieAsync(string imdbId, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieAsync(imdbId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Movie> GetMovieAsync(int movieId, string language, string includeImageLanguage = null, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieAsync(movieId.ToString(CultureInfo.InvariantCulture), language, includeImageLanguage, extraMethods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a movie by its IMDb Id
|
||||
/// </summary>
|
||||
/// <param name="imdbId">The IMDb id of the movie OR the TMDb id as string</param>
|
||||
/// <param name="language">Language to localize the results in.</param>
|
||||
/// <param name="includeImageLanguage">If specified the api will attempt to return localized image results eg. en,it,es.</param>
|
||||
/// <param name="extraMethods">A list of additional methods to execute for this req as enum flags</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>The reqed movie or null if it could not be found</returns>
|
||||
/// <remarks>Requires a valid user session when specifying the extra method 'AccountStates' flag</remarks>
|
||||
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned, see remarks.</exception>
|
||||
public async Task<Movie> GetMovieAsync(string imdbId, string language, string includeImageLanguage = null, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (extraMethods.HasFlag(MovieMethods.AccountStates))
|
||||
RequireSessionId(SessionType.UserSession);
|
||||
|
||||
RestRequest req = _client.Create("movie/{movieId}");
|
||||
req.AddUrlSegment("movieId", imdbId);
|
||||
if (extraMethods.HasFlag(MovieMethods.AccountStates))
|
||||
AddSessionId(req, SessionType.UserSession);
|
||||
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
|
||||
includeImageLanguage ??= DefaultImageLanguage;
|
||||
if (includeImageLanguage != null)
|
||||
req.AddParameter("include_image_language", includeImageLanguage);
|
||||
|
||||
string appends = string.Join(",",
|
||||
Enum.GetValues(typeof(MovieMethods))
|
||||
.OfType<MovieMethods>()
|
||||
.Except(new[] { MovieMethods.Undefined })
|
||||
.Where(s => extraMethods.HasFlag(s))
|
||||
.Select(s => s.GetDescription()));
|
||||
|
||||
if (appends != string.Empty)
|
||||
req.AddParameter("append_to_response", appends);
|
||||
|
||||
using RestResponse<Movie> response = await req.Get<Movie>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsValid)
|
||||
return null;
|
||||
|
||||
Movie item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// Patch up data, so that the end user won't notice that we share objects between req-types.
|
||||
if (item.Videos != null)
|
||||
item.Videos.Id = item.Id;
|
||||
|
||||
if (item.AlternativeTitles != null)
|
||||
item.AlternativeTitles.Id = item.Id;
|
||||
|
||||
if (item.Credits != null)
|
||||
item.Credits.Id = item.Id;
|
||||
|
||||
if (item.Releases != null)
|
||||
item.Releases.Id = item.Id;
|
||||
|
||||
if (item.Keywords != null)
|
||||
item.Keywords.Id = item.Id;
|
||||
|
||||
if (item.Translations != null)
|
||||
item.Translations.Id = item.Id;
|
||||
|
||||
if (item.AccountStates != null)
|
||||
item.AccountStates.Id = item.Id;
|
||||
|
||||
if (item.ExternalIds != null)
|
||||
item.ExternalIds.Id = item.Id;
|
||||
|
||||
// Overview is the only field that is HTML encoded from the source.
|
||||
item.Overview = WebUtility.HtmlDecode(item.Overview);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public async Task<Credits> GetMovieCreditsAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<Credits>(movieId, MovieMethods.Credits, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an object that contains all known exteral id's for the movie related to the specified TMDB id.
|
||||
/// </summary>
|
||||
/// <param name="id">The TMDb id of the target movie.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<ExternalIdsMovie> GetMovieExternalIdsAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<ExternalIdsMovie>(id, MovieMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ImagesWithId> GetMovieImagesAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieImagesAsync(movieId, DefaultLanguage, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ImagesWithId> GetMovieImagesAsync(int movieId, string language, string includeImageLanguage = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<ImagesWithId>(movieId, MovieMethods.Images, language: language, includeImageLanguage: includeImageLanguage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<KeywordsContainer> GetMovieKeywordsAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<KeywordsContainer>(movieId, MovieMethods.Keywords, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Movie> GetMovieLatestAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("movie/latest");
|
||||
using RestResponse<Movie> resp = await req.Get<Movie>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Movie item = await resp.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// Overview is the only field that is HTML encoded from the source.
|
||||
if (item != null)
|
||||
item.Overview = WebUtility.HtmlDecode(item.Overview);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<ListResult>> GetMovieListsAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieListsAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<ListResult>> GetMovieListsAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<SearchContainerWithId<ListResult>>(movieId, MovieMethods.Lists, page: page, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMovieRecommendationsAsync(int id, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieRecommendationsAsync(id, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMovieRecommendationsAsync(int id, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<SearchContainer<SearchMovie>>(id, MovieMethods.Recommendations, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithDates<SearchMovie>> GetMovieNowPlayingListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("movie/now_playing");
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
if (region != null)
|
||||
req.AddParameter("region", region);
|
||||
|
||||
SearchContainerWithDates<SearchMovie> resp = await req.GetOfT<SearchContainerWithDates<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMoviePopularListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("movie/popular");
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
if (region != null)
|
||||
req.AddParameter("region", region);
|
||||
|
||||
SearchContainer<SearchMovie> resp = await req.GetOfT<SearchContainer<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<ResultContainer<ReleaseDatesContainer>> GetMovieReleaseDatesAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<ResultContainer<ReleaseDatesContainer>>(movieId, MovieMethods.ReleaseDates, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Releases> GetMovieReleasesAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<Releases>(movieId, MovieMethods.Releases, dateFormat: "yyyy-MM-dd", cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<ReviewBase>> GetMovieReviewsAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieReviewsAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<ReviewBase>> GetMovieReviewsAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<SearchContainerWithId<ReviewBase>>(movieId, MovieMethods.Reviews, page: page, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMovieSimilarAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieSimilarAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMovieSimilarAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<SearchContainer<SearchMovie>>(movieId, MovieMethods.Similar, page: page, language: language, dateFormat: "yyyy-MM-dd", cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<SearchMovie>> GetMovieTopRatedListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("movie/top_rated");
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
if (region != null)
|
||||
req.AddParameter("region", region);
|
||||
|
||||
SearchContainer<SearchMovie> resp = await req.GetOfT<SearchContainer<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<TranslationsContainer> GetMovieTranslationsAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<TranslationsContainer>(movieId, MovieMethods.Translations, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithDates<SearchMovie>> GetMovieUpcomingListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("movie/upcoming");
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
if (region != null)
|
||||
req.AddParameter("region", region);
|
||||
|
||||
SearchContainerWithDates<SearchMovie> resp = await req.GetOfT<SearchContainerWithDates<SearchMovie>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<ResultContainer<Video>> GetMovieVideosAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<ResultContainer<Video>>(movieId, MovieMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SingleResultContainer<Dictionary<string, WatchProviders>>> GetMovieWatchProvidersAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetMovieMethodInternal<SingleResultContainer<Dictionary<string, WatchProviders>>>(movieId, MovieMethods.WatchProviders, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> MovieRemoveRatingAsync(int movieId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.GuestSession);
|
||||
|
||||
RestRequest req = _client.Create("movie/{movieId}/rating");
|
||||
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
|
||||
AddSessionId(req);
|
||||
|
||||
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// status code 13 = "The item/record was deleted successfully."
|
||||
PostReply item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// TODO: Previous code checked for item=null
|
||||
return item != null && item.StatusCode == 13;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the rating of a specified movie.
|
||||
/// </summary>
|
||||
/// <param name="movieId">The id of the movie to rate</param>
|
||||
/// <param name="rating">The rating you wish to assign to the specified movie. Value needs to be between 0.5 and 10 and must use increments of 0.5. Ex. using 7.1 will not work and return false.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>True if the the movie's rating was successfully updated, false if not</returns>
|
||||
/// <remarks>Requires a valid guest or user session</remarks>
|
||||
/// <exception cref="GuestSessionRequiredException">Thrown when the current client object doens't have a guest or user session assigned.</exception>
|
||||
public async Task<bool> MovieSetRatingAsync(int movieId, double rating, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequireSessionId(SessionType.GuestSession);
|
||||
|
||||
RestRequest req = _client.Create("movie/{movieId}/rating");
|
||||
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
|
||||
AddSessionId(req);
|
||||
|
||||
req.SetBody(new { value = rating });
|
||||
|
||||
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// status code 1 = "Success"
|
||||
// status code 12 = "The item/record was updated successfully" - Used when an item was previously rated by the user
|
||||
PostReply item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// TODO: Previous code checked for item=null
|
||||
return item.StatusCode == 1 || item.StatusCode == 12;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.TvShows;
|
||||
using TMDbLib.Rest;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a network by it's TMDb id. A network is a distributor of media content ex. HBO, AMC
|
||||
/// </summary>
|
||||
/// <param name="networkId">The id of the network object to retrieve</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<Network> GetNetworkAsync(int networkId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("network/{networkId}");
|
||||
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
Network response = await req.GetOfT<Network>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logos of a network given a TMDb id
|
||||
/// </summary>
|
||||
/// <param name="networkId">The TMDb id of the network</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<NetworkLogos> GetNetworkImagesAsync(int networkId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("network/{networkId}/images");
|
||||
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
NetworkLogos response = await req.GetOfT<NetworkLogos>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alternative names of a network given a TMDb id
|
||||
/// </summary>
|
||||
/// <param name="networkId">The TMDb id of the network</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task<AlternativeNames> GetNetworkAlternativeNamesAsync(int networkId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("network/{networkId}/alternative_names");
|
||||
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
AlternativeNames response = await req.GetOfT<AlternativeNames>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TMDbLib.Objects.General;
|
||||
using TMDbLib.Objects.People;
|
||||
using TMDbLib.Rest;
|
||||
using TMDbLib.Utilities;
|
||||
|
||||
namespace TMDbLib.Client
|
||||
{
|
||||
public partial class TMDbClient
|
||||
{
|
||||
private async Task<T> GetPersonMethodInternal<T>(int personId, PersonMethods personMethod, string dateFormat = null, string country = null, string language = null,
|
||||
int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default) where T : new()
|
||||
{
|
||||
RestRequest req = _client.Create("person/{personId}/{method}");
|
||||
req.AddUrlSegment("personId", personId.ToString());
|
||||
req.AddUrlSegment("method", personMethod.GetDescription());
|
||||
|
||||
// TODO: Dateformat?
|
||||
//if (dateFormat != null)
|
||||
// req.DateFormat = dateFormat;
|
||||
|
||||
if (country != null)
|
||||
req.AddParameter("country", country);
|
||||
|
||||
language ??= DefaultLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
req.AddParameter("language", language);
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
if (startDate.HasValue)
|
||||
req.AddParameter("startDate", startDate.Value.ToString("yyyy-MM-dd"));
|
||||
if (endDate != null)
|
||||
req.AddParameter("endDate", endDate.Value.ToString("yyyy-MM-dd"));
|
||||
|
||||
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<Person> GetLatestPersonAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("person/latest");
|
||||
|
||||
// TODO: Dateformat?
|
||||
//req.DateFormat = "yyyy-MM-dd";
|
||||
|
||||
Person resp = await req.GetOfT<Person>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<Person> GetPersonAsync(int personId, PersonMethods extraMethods = PersonMethods.Undefined,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonAsync(personId, DefaultLanguage, extraMethods, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Person> GetPersonAsync(int personId, string language, PersonMethods extraMethods = PersonMethods.Undefined, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req = _client.Create("person/{personId}");
|
||||
req.AddUrlSegment("personId", personId.ToString());
|
||||
|
||||
if (language != null)
|
||||
req.AddParameter("language", language);
|
||||
|
||||
string appends = string.Join(",",
|
||||
Enum.GetValues(typeof(PersonMethods))
|
||||
.OfType<PersonMethods>()
|
||||
.Except(new[] { PersonMethods.Undefined })
|
||||
.Where(s => extraMethods.HasFlag(s))
|
||||
.Select(s => s.GetDescription()));
|
||||
|
||||
if (appends != string.Empty)
|
||||
req.AddParameter("append_to_response", appends);
|
||||
|
||||
// TODO: Dateformat?
|
||||
//req.DateFormat = "yyyy-MM-dd";
|
||||
|
||||
using RestResponse<Person> response = await req.Get<Person>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsValid)
|
||||
return null;
|
||||
|
||||
Person item = await response.GetDataObject().ConfigureAwait(false);
|
||||
|
||||
// Patch up data, so that the end user won't notice that we share objects between request-types.
|
||||
if (item != null)
|
||||
{
|
||||
if (item.Images != null)
|
||||
item.Images.Id = item.Id;
|
||||
|
||||
if (item.TvCredits != null)
|
||||
item.TvCredits.Id = item.Id;
|
||||
|
||||
if (item.MovieCredits != null)
|
||||
item.MovieCredits.Id = item.Id;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public async Task<ExternalIdsPerson> GetPersonExternalIdsAsync(int personId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMethodInternal<ExternalIdsPerson>(personId, PersonMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ProfileImages> GetPersonImagesAsync(int personId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMethodInternal<ProfileImages>(personId, PersonMethods.Images, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainer<PersonResult>> GetPersonListAsync(PersonListType type, int page = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
RestRequest req;
|
||||
switch (type)
|
||||
{
|
||||
case PersonListType.Popular:
|
||||
req = _client.Create("person/popular");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
if (page >= 1)
|
||||
req.AddParameter("page", page.ToString());
|
||||
|
||||
// TODO: Dateformat?
|
||||
//req.DateFormat = "yyyy-MM-dd";
|
||||
|
||||
SearchContainer<PersonResult> resp = await req.GetOfT<SearchContainer<PersonResult>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public async Task<MovieCredits> GetPersonMovieCreditsAsync(int personId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMovieCreditsAsync(personId, DefaultLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<MovieCredits> GetPersonMovieCreditsAsync(int personId, string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMethodInternal<MovieCredits>(personId, PersonMethods.MovieCredits, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<TaggedImage>> GetPersonTaggedImagesAsync(int personId, int page, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonTaggedImagesAsync(personId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<SearchContainerWithId<TaggedImage>> GetPersonTaggedImagesAsync(int personId, string language, int page, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMethodInternal<SearchContainerWithId<TaggedImage>>(personId, PersonMethods.TaggedImages, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TvCredits> GetPersonTvCreditsAsync(int personId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonTvCreditsAsync(personId, DefaultLanguage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TvCredits> GetPersonTvCreditsAsync(int personId, string language, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPersonMethodInternal<TvCredits>(personId, PersonMethods.TvCredits, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue