Compare commits
81 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 | |
|
41293f00e4 | |
|
aa37dd4e99 | |
|
0d3aa8ce80 | |
|
556f46e205 | |
|
2b07875250 | |
|
cff4008c57 | |
|
71335df943 | |
|
92215507b8 | |
|
fa3166ce63 | |
|
097b0514c4 | |
|
bf97db9fb4 | |
|
e32e897559 |
|
@ -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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-dotnet@v3
|
- uses: actions/setup-dotnet@v3
|
||||||
|
id: dotnet
|
||||||
with:
|
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
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
|
@ -10,12 +10,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: close-issues
|
- name: close-issues
|
||||||
uses: actions-cool/issues-helper@v3
|
uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
actions: "close-issues"
|
stale-issue-message: "This issue was closed due to inactive more than 30 days. You can reopen it if you think it should continue."
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
exempt-issue-labels: "FAQ,question,bug,enhancement"
|
||||||
inactive-day: 30
|
days-before-stale: 30
|
||||||
exclude-labels: "enhancement,bug"
|
days-before-close: 0
|
||||||
close-reason: "not_planned"
|
days-before-pr-stale: -1
|
||||||
body: |
|
days-before-pr-close: -1
|
||||||
This issue was closed due to inactive more than 30 days. You can reopen it if you think it should continue.
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ on:
|
||||||
tags: ["*"]
|
tags: ["*"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
dotnet-version: 6.0.x
|
dotnet-version: 8.0.x
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
|
||||||
artifact: metashark
|
artifact: metashark
|
||||||
|
@ -21,43 +21,42 @@ jobs:
|
||||||
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v3
|
||||||
|
id: dotnet
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.dotnet-version }}
|
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
|
- name: Initialize workflow variables
|
||||||
id: vars
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
|
||||||
VERSION="$VERSION.0"
|
VERSION="$VERSION.0"
|
||||||
echo ::set-output name=VERSION::${VERSION}
|
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
echo ::set-output name=APP_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')
|
echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore ${{ env.project }} --no-cache
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet publish --nologo --no-restore --configuration=Release --framework=net6.0 --output=artifacts -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
|
run: |
|
||||||
- name: Clean publish dll
|
dotnet restore ${{ env.project }} --no-cache
|
||||||
run: cd artifacts && rm -rf MediaBrowser*.dll Microsoft*.dll Newtonsoft*.dll System*.dll Emby*.dll Jellyfin.Data*.dll Jellyfin.Extensions*.dll *.json *.pdb
|
dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
|
||||||
- name: Compress build files
|
mkdir -p artifacts
|
||||||
uses: thedoctor0/zip-release@main
|
zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll
|
||||||
with:
|
- name: Generate manifest
|
||||||
type: "zip"
|
run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
|
||||||
directory: "artifacts"
|
env:
|
||||||
filename: "artifacts.zip"
|
CN_DOMAIN: ${{ vars.CN_DOMAIN }}
|
||||||
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/*/}
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: ./${{ env.artifact }}/${{ env.artifact }}_*.zip
|
file: ./artifacts/${{ env.artifact }}_*.zip
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
|
release_name: '${{ github.ref_name }}: Jellyfin v10.9'
|
||||||
file_glob: true
|
file_glob: true
|
||||||
|
overwrite: true
|
||||||
- name: Publish manifest
|
- name: Publish manifest
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -5,6 +5,5 @@ obj/
|
||||||
artifacts
|
artifacts
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
metashark/
|
metashark/
|
||||||
manifest_cn.json
|
*.json
|
||||||
manifest.json
|
|
||||||
.vscode
|
.vscode
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageId>AnitomySharp.NET6</PackageId>
|
<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.
|
<Description>AnitomySharp is a C# port of Anitomy by erengy, a library for parsing anime video filenames. All credit to erengy for the actual library and logic.
|
||||||
|
This fork of AnitomySharp is inspired by tabratton and senritsu, which adds more custom rules.
|
||||||
</Description>
|
</Description>
|
||||||
<RepositoryUrl>https://github.com/chu-shen/AnitomySharp.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/chu-shen/AnitomySharp.git</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageTags>Anitomy Anime</PackageTags>
|
||||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
<AssemblyVersion>0.3.0</AssemblyVersion>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
<FileVersion>0.3.0</FileVersion>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<Version>0.3.0</Version>
|
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
|
||||||
<DocumentationFile>AnitomySharp.xml</DocumentationFile>
|
<DocumentationFile>AnitomySharp.xml</DocumentationFile>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\LICENSE" Pack="true" Visible="false" PackagePath="" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
|
@ -24,13 +24,13 @@ namespace AnitomySharp
|
||||||
public static class KeywordManager
|
public static class KeywordManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 包含所有关键词(大写)的内部关键词元素词典
|
/// 包含所有关键词的内部关键词元素词典,比较器忽略大小写
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>();
|
private static readonly Dictionary<string, Keyword> Keys = new Dictionary<string, Keyword>(StringComparer.OrdinalIgnoreCase);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件扩展名,无值
|
/// 文件扩展名,无值,比较器忽略大小写
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// ~~一眼真~~
|
/// ~~一眼真~~
|
||||||
|
@ -64,18 +64,19 @@ namespace AnitomySharp
|
||||||
"GEKIJOUBAN", "MOVIE",
|
"GEKIJOUBAN", "MOVIE",
|
||||||
"OAD", "OAV", "ONA", "OVA",
|
"OAD", "OAV", "ONA", "OVA",
|
||||||
"TV",
|
"TV",
|
||||||
"番外編", "總集編","映像特典","特典","特典アニメ",
|
"番外編", "總集編","DRAMA",
|
||||||
|
"映像特典","特典","特典アニメ",
|
||||||
// 特典 Special 剩下的各种类型可以全部命名成 SP,对于较特殊意义的特典也可以自定义命名
|
// 特典 Special 剩下的各种类型可以全部命名成 SP,对于较特殊意义的特典也可以自定义命名
|
||||||
"SPECIAL", "SPECIALS", "SP",
|
"SPECIAL", "SPECIALS", "SP", "SPs", "特報",
|
||||||
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
||||||
"IV",
|
"IV",
|
||||||
// 音乐视频 Music Video
|
// 音乐视频 Music Video
|
||||||
"MV"});
|
"MV"});
|
||||||
|
|
||||||
// add "SP" to ElementAnimeType with optionsUnidentifiable
|
// add "SP" to ElementAnimeType with optionsUnidentifiable
|
||||||
// Add(Element.ElementCategory.ElementAnimeType,
|
// Add(Element.ElementCategory.ElementAnimeType,
|
||||||
// optionsUnidentifiableUnsearchable,
|
// optionsUnidentifiableUnsearchable,
|
||||||
// new List<string> {"SP"}); // e.g. "Yumeiro Patissiere SP Professional"
|
// new List<string> { "SP" }); // e.g. "Yumeiro Patissiere SP Professional", but it is widely used to represent special
|
||||||
|
|
||||||
Add(Element.ElementCategory.ElementAnimeType,
|
Add(Element.ElementCategory.ElementAnimeType,
|
||||||
optionsUnidentifiableInvalid,
|
optionsUnidentifiableInvalid,
|
||||||
|
@ -84,7 +85,7 @@ namespace AnitomySharp
|
||||||
// 无字 OP/ED Non-Credit Opening/Ending
|
// 无字 OP/ED Non-Credit Opening/Ending
|
||||||
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
|
"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING",
|
||||||
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
|
// 预告 Preview 预告下一话内容 注意编号表示其预告的是第几话的内容而不是跟在哪一话后面
|
||||||
"PREVIEW",
|
"PREVIEW", "YOKOKU", "予告",
|
||||||
// 菜单 Menu BD/DVD 播放选择菜单
|
// 菜单 Menu BD/DVD 播放选择菜单
|
||||||
"MENU",
|
"MENU",
|
||||||
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右
|
// 广告 Commercial Message 电视放送广告,时长一般在 7s/15s/30s/45s/... 左右
|
||||||
|
@ -92,7 +93,7 @@ namespace AnitomySharp
|
||||||
// 语音信息
|
// 语音信息
|
||||||
"MESSAGE",
|
"MESSAGE",
|
||||||
// 宣传片/预告片 Promotion Video / Trailer 一般时长在 1~2min 命名参考原盘和 jsum
|
// 宣传片/预告片 Promotion Video / Trailer 一般时长在 1~2min 命名参考原盘和 jsum
|
||||||
"PV", "Teaser","TRAILER", "DRAMA",
|
"PV", "Teaser","TRAILER",
|
||||||
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
// 真人特典 Interview/Talk/Stage... 目前我们对于节目、采访、舞台活动、制作等三次元画面的长视频,一概怼成 IV。
|
||||||
"INTERVIEW",
|
"INTERVIEW",
|
||||||
"EVENT", "TOKUTEN", "LOGO"});
|
"EVENT", "TOKUTEN", "LOGO"});
|
||||||
|
@ -150,7 +151,7 @@ namespace AnitomySharp
|
||||||
|
|
||||||
Add(Element.ElementCategory.ElementOther,
|
Add(Element.ElementCategory.ElementOther,
|
||||||
optionsDefault,
|
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,
|
Add(Element.ElementCategory.ElementReleaseGroup,
|
||||||
optionsDefault,
|
optionsDefault,
|
||||||
|
@ -281,6 +282,16 @@ namespace AnitomySharp
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 判断预处理元素列表中是否包含给定的字符串(<paramref name="keyword"/>)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category">元素类别</param>
|
||||||
|
/// <param name="keyword">待判断的字符串</param>
|
||||||
|
/// <returns>`true`表示包含</returns>
|
||||||
|
public static bool ContainsInPeekEntries(Element.ElementCategory category, string keyword)
|
||||||
|
{
|
||||||
|
return PeekEntries.Any(entry => entry.Item1 == category && entry.Item2.Contains(keyword, StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds a particular <c>keyword</c>. If found sets <c>category</c> and <c>options</c> to the found search result.
|
/// Finds a particular <c>keyword</c>. If found sets <c>category</c> and <c>options</c> to the found search result.
|
|
@ -179,6 +179,7 @@ namespace AnitomySharp
|
||||||
private void SearchForEpisodeNumber()
|
private void SearchForEpisodeNumber()
|
||||||
{
|
{
|
||||||
var tokens = new List<int>();
|
var tokens = new List<int>();
|
||||||
|
var allTokens = new List<int>();
|
||||||
for (var i = 0; i < Tokens.Count; i++)
|
for (var i = 0; i < Tokens.Count; i++)
|
||||||
{
|
{
|
||||||
var token = Tokens[i];
|
var token = Tokens[i];
|
||||||
|
@ -187,6 +188,7 @@ namespace AnitomySharp
|
||||||
ParserHelper.IndexOfFirstDigit(token.Content) != -1)
|
ParserHelper.IndexOfFirstDigit(token.Content) != -1)
|
||||||
{
|
{
|
||||||
tokens.Add(i);
|
tokens.Add(i);
|
||||||
|
allTokens.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +230,12 @@ namespace AnitomySharp
|
||||||
// "e.g. "[12]", "(2006)"
|
// "e.g. "[12]", "(2006)"
|
||||||
if (ParseNumber.SearchForIsolatedNumbers(tokens)) return;
|
if (ParseNumber.SearchForIsolatedNumbers(tokens)) return;
|
||||||
|
|
||||||
|
// e.g. "OVA 3", "OtherToken[Hint05]", "[Web Preview 06]": maybe incorrect, so put the last
|
||||||
|
if (ParseNumber.SearchForSymbolWithEpisode(allTokens)) return;
|
||||||
|
|
||||||
|
// e.g. [13(341)], [13 (341)]
|
||||||
|
if (ParseNumber.SearchForEquivalentNumbersWithBracket(allTokens)) return;
|
||||||
|
|
||||||
// Consider using the last number as a last resort
|
// Consider using the last number as a last resort
|
||||||
ParseNumber.SearchForLastNumber(tokens);
|
ParseNumber.SearchForLastNumber(tokens);
|
||||||
}
|
}
|
||||||
|
@ -235,7 +243,7 @@ namespace AnitomySharp
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Search for anime title
|
/// Search for anime title
|
||||||
///
|
///
|
||||||
/// 搜索动画名
|
/// 搜索动画名
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void SearchForAnimeTitle()
|
private void SearchForAnimeTitle()
|
||||||
{
|
{
|
||||||
|
@ -283,6 +291,13 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
tokenBegin = tokenBeginWithNoReleaseGroup;
|
tokenBegin = tokenBeginWithNoReleaseGroup;
|
||||||
}
|
}
|
||||||
|
// 去除纯数字标题
|
||||||
|
// skip token with only number
|
||||||
|
if (Regex.Match(Tokens[tokenBegin].Content, ParserNumber.RegexMatchOnlyStart + @"^[0-9]+$" + ParserNumber.RegexMatchOnlyEnd).Success)
|
||||||
|
{
|
||||||
|
tokenBegin = tokenBeginWithNoReleaseGroup;
|
||||||
|
}
|
||||||
|
|
||||||
skippedPreviousGroup = true;
|
skippedPreviousGroup = true;
|
||||||
} while (Token.InListRange(tokenBegin, Tokens));
|
} while (Token.InListRange(tokenBegin, Tokens));
|
||||||
}
|
}
|
||||||
|
@ -398,7 +413,7 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
var token = Tokens[i];
|
var token = Tokens[i];
|
||||||
/** 跳过括号标记类型的标记 */
|
/** 跳过括号标记类型的标记 */
|
||||||
if (token.Category == Token.TokenCategory.Bracket) continue;
|
if (token.Category != Token.TokenCategory.Unknown) continue;
|
||||||
var tokenContent = token.Content;
|
var tokenContent = token.Content;
|
||||||
|
|
||||||
// e.g. "2016-17"
|
// e.g. "2016-17"
|
||||||
|
@ -408,13 +423,21 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
tokenContent = tokenContent.Split(match.Groups[2].Value)[0];
|
tokenContent = tokenContent.Split(match.Groups[2].Value)[0];
|
||||||
}
|
}
|
||||||
// add newtype e.g. "2021 OVA"
|
|
||||||
if (token.Category != Token.TokenCategory.Unknown || !StringHelper.IsNumericString(tokenContent) ||
|
if (!StringHelper.IsNumericString(tokenContent))
|
||||||
!(ParseHelper.IsTokenContainAnimeType(i) ^ ParseHelper.IsTokenIsolated(i)))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// e.g. "[2021 OVA]"
|
||||||
|
if(ParseHelper.IsNextTokenContainAnimeType(i)&&!ParseHelper.IsTokenIsolated(i)){}
|
||||||
|
|
||||||
|
// TODO may not be necessary
|
||||||
|
// if (!ParseHelper.IsTokenIsolated(i))
|
||||||
|
// {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
var number = StringHelper.StringToInt(tokenContent);
|
var number = StringHelper.StringToInt(tokenContent);
|
||||||
|
|
||||||
// Anime year
|
// Anime year
|
||||||
|
@ -422,7 +445,7 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
if (Empty(Element.ElementCategory.ElementAnimeYear))
|
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;
|
token.Category = Token.TokenCategory.Identifier;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
|
@ -119,6 +119,29 @@ namespace AnitomySharp
|
||||||
if (string.IsNullOrEmpty(str)) return "";
|
if (string.IsNullOrEmpty(str)) return "";
|
||||||
return Ordinals.TryGetValue(str, out var foundString) ? foundString : "";
|
return Ordinals.TryGetValue(str, out var foundString) ? foundString : "";
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 转换原始值中的全角数字
|
||||||
|
/// 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>
|
/// <summary>
|
||||||
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
|
/// Returns the index of the first digit in the <c>str</c>; -1 otherwise.
|
||||||
|
@ -235,7 +258,7 @@ namespace AnitomySharp
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces).
|
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces).
|
||||||
///
|
///
|
||||||
/// 判断当前位置标记(token)是否孤立,即是否被括号包裹
|
/// 判断当前位置标记(token)是否孤立,是否被括号包裹
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pos"></param>
|
/// <param name="pos"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
@ -246,6 +269,20 @@ namespace AnitomySharp
|
||||||
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
|
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether or not a token at the current <c>pos</c> is isolated(surrounded by braces, delimiter).
|
||||||
|
///
|
||||||
|
/// 判断当前位置标记(token)是否孤立,前面是否为分隔符,后面是否为括号包裹
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pos"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsTokenIsolatedWithDelimiterAndBracket(int pos)
|
||||||
|
{
|
||||||
|
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNone);
|
||||||
|
if (!IsTokenCategory(prevToken, Token.TokenCategory.Delimiter)) return false;
|
||||||
|
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether or not a token at the current <c>pos+1</c> is ElementAnimeType.
|
/// Returns whether or not a token at the current <c>pos+1</c> is ElementAnimeType.
|
||||||
|
@ -254,13 +291,40 @@ namespace AnitomySharp
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pos"></param>
|
/// <param name="pos"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public bool IsTokenContainAnimeType(int pos)
|
public bool IsNextTokenContainAnimeType(int pos)
|
||||||
{
|
{
|
||||||
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
|
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
|
||||||
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
|
||||||
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
|
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[nextToken].Content);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 判断当前标记(token)的上一个标记的类型是否为ElementAnimeType。如果是,则返回`true`
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pos"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsPrevTokenContainAnimeType(int pos)
|
||||||
|
{
|
||||||
|
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
if(!Token.InListRange(prevToken, _parser.Tokens)||!Token.InListRange(nextToken, _parser.Tokens)) return false;
|
||||||
|
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
|
||||||
|
return KeywordManager.Contains(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 判断当前标记(token)的上一个标记的类型是否为ElementAnimeType(在 PeekEntries 中)。如果是,则返回`true`
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pos"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsPrevTokenContainAnimeTypeInPeekEntries(int pos)
|
||||||
|
{
|
||||||
|
var prevToken = Token.FindPrevToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
var nextToken = Token.FindNextToken(_parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
if (!Token.InListRange(prevToken, _parser.Tokens) || !Token.InListRange(nextToken, _parser.Tokens)) return false;
|
||||||
|
if (!IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) return false;
|
||||||
|
return KeywordManager.ContainsInPeekEntries(Element.ElementCategory.ElementAnimeType, _parser.Tokens[prevToken].Content);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finds and sets the anime season keyword.
|
/// Finds and sets the anime season keyword.
|
||||||
|
@ -394,4 +458,4 @@ namespace AnitomySharp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -371,7 +371,7 @@ namespace AnitomySharp
|
||||||
/// <returns>true if the token matched</returns>
|
/// <returns>true if the token matched</returns>
|
||||||
private bool MatchSeasonAndEpisodePattern(string word, Token token)
|
private bool MatchSeasonAndEpisodePattern(string word, Token token)
|
||||||
{
|
{
|
||||||
const string regexPattern = RegexMatchOnlyStart + @"S?(\d{1,2})(?:-S?(\d{1,2}))?(?:x|[ ._-x]?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);
|
var match = Regex.Match(word, regexPattern);
|
||||||
if (!match.Success) return false;
|
if (!match.Success) return false;
|
||||||
|
|
||||||
|
@ -412,7 +412,7 @@ namespace AnitomySharp
|
||||||
_parser.Tokens.Insert(foundIdx,
|
_parser.Tokens.Insert(foundIdx,
|
||||||
new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix));
|
new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, token.Enclosed, prefix));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,7 +513,8 @@ namespace AnitomySharp
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参]+)([期章話话巻卷幕夜期発縛])";
|
// 全角数字:\uFF10-\uFF19
|
||||||
|
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([回集話话幕夜発縛])";
|
||||||
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
|
@ -522,11 +523,33 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
|
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
|
||||||
}
|
}
|
||||||
|
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
|
||||||
|
SetEpisodeNumber(episodeNumber, token, false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
regexPattern = @"([第全]?)([0-9一二三四五六七八九十壱弐参\uFF10-\uFF19]+)([期章巻卷])";
|
||||||
|
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var episodeNumber = match.Groups[2].Value;
|
||||||
|
if (!StringHelper.IsNumericString(episodeNumber))
|
||||||
|
{
|
||||||
|
episodeNumber = ParserHelper.GetNumberFromOrdinal(episodeNumber);
|
||||||
|
}
|
||||||
|
episodeNumber = ParserHelper.GetNumberFromFullWidth(episodeNumber);
|
||||||
SetEpisodeNumber(episodeNumber, token, false);
|
SetEpisodeNumber(episodeNumber, token, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
regexPattern = @"(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);
|
match = Regex.Match(word, RegexMatchOnlyStart + regexPattern + RegexMatchOnlyEnd, RegexOptions.IgnoreCase);
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
{
|
{
|
||||||
|
@ -698,6 +721,50 @@ namespace AnitomySharp
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 搜索同动画类型同时出现的集数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tokens"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool SearchForSymbolWithEpisode(List<int> tokens)
|
||||||
|
{
|
||||||
|
// Match from back to front
|
||||||
|
for (int i = tokens.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var it = tokens[i];
|
||||||
|
|
||||||
|
// e.g. OVA 3, [Web Preview 06]: Web Preview in PeekEntries
|
||||||
|
if ((_parser.ParseHelper.IsPrevTokenContainAnimeType(it) || _parser.ParseHelper.IsPrevTokenContainAnimeTypeInPeekEntries(it)) && !_parser.ParseHelper.IsTokenIsolated(it))
|
||||||
|
{
|
||||||
|
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// e.g. OtherToken[Hint05]
|
||||||
|
// it>1: makesure this token is not first one
|
||||||
|
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it))
|
||||||
|
{
|
||||||
|
var tokenContent = _parser.Tokens[it].Content;
|
||||||
|
var numberBegin = ParserHelper.IndexOfFirstDigit(tokenContent);
|
||||||
|
var prefix = StringHelper.SubstringWithCheck(tokenContent, 0, numberBegin);
|
||||||
|
var number = StringHelper.SubstringWithCheck(tokenContent, numberBegin, tokenContent.Length - numberBegin);
|
||||||
|
// token should be: alphaNumeric
|
||||||
|
if (prefix != "" && StringHelper.IsAlphaString(prefix) && StringHelper.IsNumericString(number))
|
||||||
|
{
|
||||||
|
SetEpisodeNumber(number, _parser.Tokens[it], true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// e.g. OtherToken[Disc 01]
|
||||||
|
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(it) && StringHelper.IsNumericString(_parser.Tokens[it].Content))
|
||||||
|
{
|
||||||
|
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
|
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
|
||||||
///
|
///
|
||||||
|
@ -730,10 +797,7 @@ namespace AnitomySharp
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<Token>
|
var list = new List<Token> { _parser.Tokens[it], _parser.Tokens[nextToken] };
|
||||||
{
|
|
||||||
_parser.Tokens[it], _parser.Tokens[nextToken]
|
|
||||||
};
|
|
||||||
|
|
||||||
list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content));
|
list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content));
|
||||||
SetEpisodeNumber(list[0].Content, list[0], false);
|
SetEpisodeNumber(list[0].Content, list[0], false);
|
||||||
|
@ -743,6 +807,50 @@ namespace AnitomySharp
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for equivalent number in a list of <c>tokens</c>. e.g. 08(114)
|
||||||
|
///
|
||||||
|
/// 匹配自带等效集数的数字,常见于分割放送,匹配括号包裹的数字
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tokens">the list of tokens</param>
|
||||||
|
/// <returns>true if an equivalent number was found</returns>
|
||||||
|
public bool SearchForEquivalentNumbersWithBracket(List<int> tokens)
|
||||||
|
{
|
||||||
|
foreach (var it in tokens)
|
||||||
|
{
|
||||||
|
// Find the first enclosed, non-delimiter token
|
||||||
|
var nextToken = Token.FindNextToken(_parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter);
|
||||||
|
if (!Token.InListRange(nextToken, _parser.Tokens) || !(_parser.Tokens[it].Content.Contains("(") || _parser.Tokens[nextToken].Content.Contains(")")))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. [13(341)]
|
||||||
|
if (it > 1 && _parser.Tokens[it].Enclosed && _parser.ParseHelper.IsTokenIsolated(it))
|
||||||
|
{
|
||||||
|
string[] episodes = _parser.Tokens[it].Content.Split(new string[] { "(", ")" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (StringHelper.IsNumericString(episodes[0]) && StringHelper.IsNumericString(episodes[1]))
|
||||||
|
{
|
||||||
|
SetEpisodeNumber(episodes[0], _parser.Tokens[it], false);
|
||||||
|
SetAlternativeEpisodeNumber(episodes[1], _parser.Tokens[it]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. [13 (341)]
|
||||||
|
if (it > 1 && _parser.Tokens[nextToken].Enclosed && _parser.ParseHelper.IsTokenIsolatedWithDelimiterAndBracket(nextToken))
|
||||||
|
{
|
||||||
|
string episode = _parser.Tokens[nextToken].Content.Replace("(", "").Replace(")", "");
|
||||||
|
if (StringHelper.IsNumericString(_parser.Tokens[it].Content) && StringHelper.IsNumericString(episode))
|
||||||
|
{
|
||||||
|
SetEpisodeNumber(_parser.Tokens[it].Content, _parser.Tokens[it], true);
|
||||||
|
SetAlternativeEpisodeNumber(episode, _parser.Tokens[nextToken]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for the last number token in a list of <c>tokens</c>
|
/// Searches for the last number token in a list of <c>tokens</c>
|
||||||
|
@ -789,4 +897,4 @@ namespace AnitomySharp
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -120,6 +120,17 @@ namespace AnitomySharp
|
||||||
{
|
{
|
||||||
return str.All(char.IsDigit);
|
return str.All(char.IsDigit);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether or not the <c>str</c> is a alpha string.
|
||||||
|
///
|
||||||
|
/// 判断字符串是否全字母
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool IsAlphaString(string str)
|
||||||
|
{
|
||||||
|
return str.All(char.IsLetter);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the int value of the <c>str</c>; 0 otherwise.
|
/// Returns the int value of the <c>str</c>; 0 otherwise.
|
|
@ -1,14 +1,6 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Test
|
namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
{
|
{
|
||||||
|
@ -40,7 +32,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestSearch()
|
public void TestSearch()
|
||||||
{
|
{
|
||||||
var keyword = "V字仇杀队";
|
var keyword = "声生不息";
|
||||||
var api = new DoubanApi(loggerFactory);
|
var api = new DoubanApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
|
@ -147,7 +139,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetCelebritiesByCidAsync()
|
public void TestGetCelebritiesByCidAsync()
|
||||||
{
|
{
|
||||||
var sid = "1340364";
|
var cid = "1340364";
|
||||||
|
|
||||||
var api = new DoubanApi(loggerFactory);
|
var api = new DoubanApi(loggerFactory);
|
||||||
|
|
||||||
|
@ -155,7 +147,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await api.GetCelebrityAsync(sid, CancellationToken.None);
|
var result = await api.GetCelebrityAsync(cid, CancellationToken.None);
|
||||||
TestContext.WriteLine(result.ToJson());
|
TestContext.WriteLine(result.ToJson());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -164,5 +156,53 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}
|
}
|
||||||
}).GetAwaiter().GetResult();
|
}).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetCelebrityPhotosAsync()
|
||||||
|
{
|
||||||
|
var cid = "1322205";
|
||||||
|
|
||||||
|
var api = new DoubanApi(loggerFactory);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await api.GetCelebrityPhotosAsync(cid, CancellationToken.None);
|
||||||
|
TestContext.WriteLine(result.ToJson());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestParseCelebrityName()
|
||||||
|
{
|
||||||
|
|
||||||
|
var api = new DoubanApi(loggerFactory);
|
||||||
|
|
||||||
|
|
||||||
|
var name = "佩吉·陆 Peggy Lu";
|
||||||
|
var result = api.ParseCelebrityName(name);
|
||||||
|
Assert.AreEqual<string>(result, "佩吉·陆");
|
||||||
|
|
||||||
|
name = "Antony Coleman Antony Coleman";
|
||||||
|
result = api.ParseCelebrityName(name);
|
||||||
|
Assert.AreEqual<string>(result, "Antony Coleman");
|
||||||
|
|
||||||
|
name = "Dick Cook";
|
||||||
|
result = api.ParseCelebrityName(name);
|
||||||
|
Assert.AreEqual<string>(result, "Dick Cook");
|
||||||
|
|
||||||
|
name = "李凡秀";
|
||||||
|
result = api.ParseCelebrityName(name);
|
||||||
|
Assert.AreEqual<string>(result, "李凡秀");
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,15 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadata()
|
public void TestGetMetadata()
|
||||||
{
|
{
|
||||||
|
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
@ -52,7 +54,7 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
SeriesProviderIds = new Dictionary<string, string>() { { MetadataProvider.Tmdb.ToString(), "26707" } },
|
SeriesProviderIds = new Dictionary<string, string>() { { MetadataProvider.Tmdb.ToString(), "26707" } },
|
||||||
IsAutomated = false,
|
IsAutomated = false,
|
||||||
};
|
};
|
||||||
var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -64,15 +66,17 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestFixParseInfo()
|
public void TestFixParseInfo()
|
||||||
{
|
{
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
|
|
||||||
var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/[POPGO][Stand_Alone_Complex][05][1080P][BluRay][x264_FLACx2_AC3x1][chs_jpn][D87C36B6].mkv" });
|
var parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/[POPGO][Stand_Alone_Complex][05][1080P][BluRay][x264_FLACx2_AC3x1][chs_jpn][D87C36B6].mkv" });
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 5);
|
Assert.AreEqual(parseResult.IndexNumber, 5);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ImdbApiTest
|
||||||
|
{
|
||||||
|
private TestContext testContextInstance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the test context which provides
|
||||||
|
/// information about and functionality for the current test run.
|
||||||
|
/// </summary>
|
||||||
|
public TestContext TestContext
|
||||||
|
{
|
||||||
|
get { return testContextInstance; }
|
||||||
|
set { testContextInstance = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
||||||
|
builder.AddSimpleConsole(options =>
|
||||||
|
{
|
||||||
|
options.IncludeScopes = true;
|
||||||
|
options.SingleLine = true;
|
||||||
|
options.TimestampFormat = "hh:mm:ss ";
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestCheckPersonNewImdbID()
|
||||||
|
{
|
||||||
|
var api = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = "nm1123737";
|
||||||
|
var result = await api.CheckPersonNewIDAsync(id, CancellationToken.None);
|
||||||
|
Assert.AreEqual("nm0170924", result);
|
||||||
|
|
||||||
|
id = "nm0170924";
|
||||||
|
result = await api.CheckPersonNewIDAsync(id, CancellationToken.None);
|
||||||
|
Assert.AreEqual(null, result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TestContext.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
|
@ -29,7 +29,6 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMovieImage()
|
public void TestGetMovieImage()
|
||||||
{
|
{
|
||||||
|
@ -39,16 +38,17 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
PreferredMetadataLanguage = "zh",
|
PreferredMetadataLanguage = "zh",
|
||||||
ProviderIds = new Dictionary<string, string> { { BaseProvider.DoubanProviderId, "2043546" }, { MetadataProvider.Tmdb.ToString(), "38142" } }
|
ProviderIds = new Dictionary<string, string> { { BaseProvider.DoubanProviderId, "2043546" }, { MetadataProvider.Tmdb.ToString(), "38142" } }
|
||||||
};
|
};
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetImages(info, CancellationToken.None);
|
var result = await provider.GetImages(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -63,18 +63,19 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
||||||
{
|
{
|
||||||
PreferredMetadataLanguage = "zh",
|
PreferredMetadataLanguage = "zh",
|
||||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb } }
|
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
|
||||||
};
|
};
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetImages(info, CancellationToken.None);
|
var result = await provider.GetImages(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -82,5 +83,27 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
Console.WriteLine(result.ToJson());
|
Console.WriteLine(result.ToJson());
|
||||||
}).GetAwaiter().GetResult();
|
}).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetImageResponse()
|
||||||
|
{
|
||||||
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
|
var result = await provider.GetImageResponse("https://img1.doubanio.com/view/photo/m/public/p2893270877.jpg", CancellationToken.None);
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
var str = result.ToJson();
|
||||||
|
Console.WriteLine(result.ToJson());
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
using Jellyfin.Plugin.MetaShark.Providers;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
|
@ -28,22 +29,21 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadata()
|
public void TestGetMetadata()
|
||||||
{
|
{
|
||||||
|
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var info = new MovieInfo() { Name = "我", MetadataLanguage = "zh" };
|
var info = new MovieInfo() { Name = "我", MetadataLanguage = "zh" };
|
||||||
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetSearchResults(info, CancellationToken.None);
|
var result = await provider.GetSearchResults(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -56,16 +56,17 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
public void TestGetMetadataAnime()
|
public void TestGetMetadataAnime()
|
||||||
{
|
{
|
||||||
var info = new MovieInfo() { Name = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3]" };
|
var info = new MovieInfo() { Name = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3]" };
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -77,17 +78,18 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadataByTMDB()
|
public void TestGetMetadataByTMDB()
|
||||||
{
|
{
|
||||||
var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary<string, string> { { 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 doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
using Jellyfin.Plugin.MetaShark.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Test
|
namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
{
|
{
|
||||||
|
@ -86,7 +86,6 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
Assert.AreEqual(parseResult.Name, "秒速5厘米");
|
Assert.AreEqual(parseResult.Name, "秒速5厘米");
|
||||||
Assert.AreEqual(parseResult.Year, null);
|
Assert.AreEqual(parseResult.Year, null);
|
||||||
|
|
||||||
|
|
||||||
// 标题加年份
|
// 标题加年份
|
||||||
fileName = "V字仇杀队 (2006)";
|
fileName = "V字仇杀队 (2006)";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.Parse(fileName);
|
||||||
|
@ -94,6 +93,12 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
Assert.AreEqual(parseResult.Name, "V字仇杀队");
|
Assert.AreEqual(parseResult.Name, "V字仇杀队");
|
||||||
Assert.AreEqual(parseResult.Year, 2006);
|
Assert.AreEqual(parseResult.Year, 2006);
|
||||||
|
|
||||||
|
fileName = "逃学威龙2 (1992)";
|
||||||
|
parseResult = NameParser.Parse(fileName);
|
||||||
|
Assert.AreEqual(parseResult.ChineseName, null);
|
||||||
|
Assert.AreEqual(parseResult.Name, "逃学威龙2");
|
||||||
|
Assert.AreEqual(parseResult.Year, 1992);
|
||||||
|
|
||||||
|
|
||||||
// anime
|
// anime
|
||||||
fileName = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3].mp4";
|
fileName = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3].mp4";
|
||||||
|
@ -162,43 +167,64 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestEposideParse()
|
public void TestEposideParse()
|
||||||
{
|
{
|
||||||
|
// 普通数字
|
||||||
|
var fileName = "03.mp4";
|
||||||
|
var parseResult = NameParser.ParseEpisode(fileName);
|
||||||
|
Assert.AreEqual(parseResult.Name, "03");
|
||||||
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
|
Assert.AreEqual(parseResult.IndexNumber, 3);
|
||||||
|
|
||||||
|
fileName = "03 4K.mp4";
|
||||||
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
|
Assert.AreEqual(parseResult.Name, "03");
|
||||||
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
|
Assert.AreEqual(parseResult.IndexNumber, 3);
|
||||||
|
|
||||||
// 混合中英文
|
// 混合中英文
|
||||||
var fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
|
fileName = "新世界.New.World.2013.BluRay.1080p.x265.10bit.MNHD-FRDS";
|
||||||
var parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.ChineseName, "新世界");
|
Assert.AreEqual(parseResult.ChineseName, "新世界");
|
||||||
Assert.AreEqual(parseResult.Name, "New World");
|
Assert.AreEqual(parseResult.Name, "New World");
|
||||||
Assert.AreEqual(parseResult.Year, 2013);
|
Assert.AreEqual(parseResult.Year, 2013);
|
||||||
|
|
||||||
// 只英文
|
// 只英文 S01E01
|
||||||
fileName = "She-Hulk.Attorney.At.Law.S01E01.1080p.WEBRip.x265-RARBG";
|
fileName = "She-Hulk.Attorney.At.Law.S01E01.1080p.WEBRip.x265-RARBG";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "She-Hulk Attorney At Law");
|
Assert.AreEqual(parseResult.Name, "She-Hulk Attorney At Law");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||||
|
|
||||||
|
// 测试 SXXEPXX 格式
|
||||||
|
fileName = "神探狄仁杰2 Detective.Dee.Ⅱ.S02EP02.2006.2160p.WEB-DL.x264.AAC-HQC";
|
||||||
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
|
Assert.AreEqual(parseResult.ChineseName, "神探狄仁杰2");
|
||||||
|
Assert.AreEqual(parseResult.Name, "Detective Dee Ⅱ");
|
||||||
|
Assert.AreEqual(parseResult.ParentIndexNumber, 2);
|
||||||
|
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||||
|
|
||||||
|
|
||||||
// 日文
|
// 日文
|
||||||
fileName = "プロポーズ大作戦Ep05_x264.mp4";
|
fileName = "プロポーズ大作戦Ep05_x264.mp4";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "プロポーズ大作戦Ep05");
|
Assert.AreEqual(parseResult.Name, "プロポーズ大作戦Ep05");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 5);
|
Assert.AreEqual(parseResult.IndexNumber, 5);
|
||||||
|
|
||||||
fileName = "[01] [ANK-Raws] あっちこっち 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC)";
|
fileName = "[01] [ANK-Raws] あっちこっち 01 (BDrip 1920x1080 HEVC-YUV420P10 FLAC)";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "あっちこっち 01");
|
Assert.AreEqual(parseResult.Name, "あっちこっち 01");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||||
|
|
||||||
// 只中文
|
// 只中文
|
||||||
fileName = "齊天大聖 第02集";
|
fileName = "齊天大聖 第02集";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "齊天大聖 第02集");
|
Assert.AreEqual(parseResult.Name, "齊天大聖");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||||
|
|
||||||
fileName = "齊天大聖 第 02 期";
|
fileName = "齊天大聖 第 02 期";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "齊天大聖");
|
Assert.AreEqual(parseResult.Name, "齊天大聖");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||||
|
@ -206,38 +232,40 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
|
|
||||||
// anime
|
// anime
|
||||||
fileName = "[YYDM-11FANS][THERMAE_ROMAE][02][BDRIP][720P][X264-10bit_AAC][7FF2269F]";
|
fileName = "[YYDM-11FANS][THERMAE_ROMAE][02][BDRIP][720P][X264-10bit_AAC][7FF2269F]";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "THERMAE ROMAE");
|
Assert.AreEqual(parseResult.Name, "THERMAE ROMAE");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 2);
|
Assert.AreEqual(parseResult.IndexNumber, 2);
|
||||||
|
|
||||||
// anime带季数
|
// anime带季数
|
||||||
fileName = "[WMSUB][Detective Conan - Zero‘s Tea Time ][S01][E06][BIG5][1080P].mp4";
|
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.Name, "Detective Conan - Zero‘s Tea Time");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
Assert.AreEqual(parseResult.ParentIndexNumber, 1);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 6);
|
Assert.AreEqual(parseResult.IndexNumber, 6);
|
||||||
|
|
||||||
fileName = "[KTXP][Machikado_Mazoku_S2][01][BIG5][1080p]";
|
fileName = "[KTXP][Machikado_Mazoku_S2][01][BIG5][1080p]";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "Machikado Mazoku");
|
Assert.AreEqual(parseResult.Name, "Machikado Mazoku");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||||
|
|
||||||
fileName = "[異域字幕組][她和她的貓 - Everything Flows -][She and Her Cat - Everything Flows -][01][720p][繁體]";
|
fileName = "[異域字幕組][她和她的貓 - Everything Flows -][She and Her Cat - Everything Flows -][01][720p][繁體]";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.AreEqual(parseResult.Name, "她和她的貓 - Everything Flows");
|
Assert.AreEqual(parseResult.Name, "她和她的貓 - Everything Flows");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, 1);
|
Assert.AreEqual(parseResult.IndexNumber, 1);
|
||||||
|
|
||||||
// anime特典
|
// anime特典
|
||||||
fileName = "[KissSub][Steins;Gate][SP][GB_BIG5_JP][BDrip][1080P][HEVC] 边界曲面的缺失之环";
|
fileName = "[KissSub][Steins;Gate][SP][GB_BIG5_JP][BDrip][1080P][HEVC] 边界曲面的缺失之环";
|
||||||
parseResult = NameParser.Parse(fileName);
|
parseResult = NameParser.ParseEpisode(fileName);
|
||||||
Assert.IsTrue(parseResult.IsSpecial);
|
Assert.IsTrue(parseResult.IsSpecial);
|
||||||
Assert.AreEqual(parseResult.Name, "边界曲面的缺失之环");
|
Assert.AreEqual(parseResult.Name, "边界曲面的缺失之环");
|
||||||
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
Assert.AreEqual(parseResult.ParentIndexNumber, null);
|
||||||
Assert.AreEqual(parseResult.IndexNumber, null);
|
Assert.AreEqual(parseResult.IndexNumber, null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
using Jellyfin.Plugin.MetaShark.Providers;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
@ -27,22 +28,44 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadata()
|
public void TestGetMetadata()
|
||||||
{
|
{
|
||||||
|
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var info = new PersonLookupInfo() { Name = "柊瑠美", ProviderIds = new Dictionary<string, string>() { { BaseProvider.DoubanProviderId, "1023337" } } };
|
var info = new PersonLookupInfo() { ProviderIds = new Dictionary<string, string>() { { BaseProvider.DoubanProviderId, "1016771" } } };
|
||||||
var provider = new PersonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new PersonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
var str = result.ToJson();
|
||||||
|
Console.WriteLine(result.ToJson());
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGetMetadataByTmdb()
|
||||||
|
{
|
||||||
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var info = new PersonLookupInfo() { ProviderIds = new Dictionary<string, string>() { { MetadataProvider.Tmdb.ToString(), "78871" } } };
|
||||||
|
var provider = new PersonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
|
|
@ -28,21 +28,21 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadata()
|
public void TestGetMetadata()
|
||||||
{
|
{
|
||||||
var info = new SeasonInfo() { Name = "第 18 季", IndexNumber = 18, SeriesProviderIds = new Dictionary<string, string>() { { BaseProvider.DoubanProviderId, "2059529" }, { MetadataProvider.Tmdb.ToString(), "34860" } } };
|
var info = new SeasonInfo() { Name = "第 18 季", IndexNumber = 18, SeriesProviderIds = new Dictionary<string, string>() { { BaseProvider.DoubanProviderId, "2059529" }, { MetadataProvider.Tmdb.ToString(), "34860" } } };
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -55,15 +55,20 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
public void TestGuessSeasonNumberByFileName()
|
public void TestGuessSeasonNumberByFileName()
|
||||||
{
|
{
|
||||||
var info = new SeasonInfo() { };
|
var info = new SeasonInfo() { };
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
|
|
||||||
|
var result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/冰与火之歌S01-S08.Game.of.Thrones.1080p.Blu-ray.x265.10bit.AC3/冰与火之歌S2.列王的纷争.2012.1080p.Blu-ray.x265.10bit.AC3");
|
||||||
|
Assert.AreEqual(result, 2);
|
||||||
|
|
||||||
|
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
|
||||||
Assert.AreEqual(result, 2);
|
Assert.AreEqual(result, 2);
|
||||||
|
|
||||||
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");
|
result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");
|
||||||
|
@ -88,15 +93,17 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGuestDoubanSeasonByYearAsync()
|
public void TestGuestDoubanSeasonByYearAsync()
|
||||||
{
|
{
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GuestDoubanSeasonByYearAsync("机动战士高达0083 星尘的回忆", 1991, null, CancellationToken.None);
|
var result = await provider.GuestDoubanSeasonByYearAsync("机动战士高达0083 星尘的回忆", 1991, null, CancellationToken.None);
|
||||||
Assert.AreEqual(result, "1766564");
|
Assert.AreEqual(result, "1766564");
|
||||||
}).GetAwaiter().GetResult();
|
}).GetAwaiter().GetResult();
|
||||||
|
|
|
@ -29,26 +29,25 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMovieImageFromTMDB()
|
public void TestGetMovieImageFromTMDB()
|
||||||
{
|
{
|
||||||
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
var info = new MediaBrowser.Controller.Entities.Movies.Movie()
|
||||||
{
|
{
|
||||||
PreferredMetadataLanguage = "zh",
|
PreferredMetadataLanguage = "zh",
|
||||||
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb } }
|
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
|
||||||
};
|
};
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new SeriesImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeriesImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetImages(info, CancellationToken.None);
|
var result = await provider.GetImages(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
|
|
@ -27,21 +27,21 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void TestGetMetadata()
|
public void TestGetMetadata()
|
||||||
{
|
{
|
||||||
var info = new SeriesInfo() { Name = "奔跑吧兄弟" };
|
var info = new SeriesInfo() { Name = "天下长河" };
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.IsNotNull(result);
|
Assert.IsNotNull(result);
|
||||||
|
|
||||||
|
@ -54,16 +54,17 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
public void TestGetAnimeMetadata()
|
public void TestGetAnimeMetadata()
|
||||||
{
|
{
|
||||||
var info = new SeriesInfo() { Name = "命运-冠位嘉年华" };
|
var info = new SeriesInfo() { Name = "命运-冠位嘉年华" };
|
||||||
var doubanApi = new DoubanApi(loggerFactory);
|
|
||||||
var tmdbApi = new TmdbApi(loggerFactory);
|
|
||||||
var omdbApi = new OmdbApi(loggerFactory);
|
|
||||||
var httpClientFactory = new DefaultHttpClientFactory();
|
var httpClientFactory = new DefaultHttpClientFactory();
|
||||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||||
|
var doubanApi = new DoubanApi(loggerFactory);
|
||||||
|
var tmdbApi = new TmdbApi(loggerFactory);
|
||||||
|
var omdbApi = new OmdbApi(loggerFactory);
|
||||||
|
var imdbApi = new ImdbApi(loggerFactory);
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
|
||||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||||
Assert.AreEqual(result.Item.Name, "命运/冠位指定嘉年华 公元2020奥林匹亚英灵限界大祭");
|
Assert.AreEqual(result.Item.Name, "命运/冠位指定嘉年华 公元2020奥林匹亚英灵限界大祭");
|
||||||
Assert.AreEqual(result.Item.OriginalTitle, "Fate/Grand Carnival");
|
Assert.AreEqual(result.Item.OriginalTitle, "Fate/Grand Carnival");
|
||||||
|
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using TMDbLib.Objects.Find;
|
||||||
using TMDbLib.Objects.Languages;
|
using TMDbLib.Objects.Languages;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Test
|
namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
|
@ -124,5 +125,27 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
||||||
}).GetAwaiter().GetResult();
|
}).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestFindByExternalId()
|
||||||
|
{
|
||||||
|
var api = new TmdbApi(loggerFactory);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await api.FindByExternalIdAsync("tt5924366", FindExternalSource.Imdb, "zh", CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
TestContext.WriteLine(result.ToJson());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TestContext.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark",
|
||||||
EndProject
|
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}"
|
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
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnitomySharp", "AnitomySharp\AnitomySharp.csproj", "{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -2,35 +2,21 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Jellyfin.Plugin.MetaShark.Api.Http;
|
using Jellyfin.Plugin.MetaShark.Api.Http;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
|
||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using System.Net.WebSockets;
|
|
||||||
using Jellyfin.Data.Entities.Libraries;
|
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using System.Data;
|
|
||||||
using TMDbLib.Objects.Movies;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using RateLimiter;
|
using RateLimiter;
|
||||||
using ComposableAsync;
|
using ComposableAsync;
|
||||||
|
|
||||||
|
@ -38,7 +24,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
{
|
{
|
||||||
public class DoubanApi : IDisposable
|
public class DoubanApi : IDisposable
|
||||||
{
|
{
|
||||||
const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
|
public const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
|
||||||
|
public const string HTTP_REFERER = "https://www.douban.com/";
|
||||||
private readonly ILogger<DoubanApi> _logger;
|
private readonly ILogger<DoubanApi> _logger;
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
private CookieContainer _cookieContainer;
|
private CookieContainer _cookieContainer;
|
||||||
|
@ -65,21 +52,15 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
|
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
|
||||||
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
Regex regImdb = new Regex(@"IMDb: (tt\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
|
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
|
||||||
Regex regNameMath = new Regex(@"(.+第\w季|[\w\uff1a\uff01\uff0c\u00b7]+)\s*(.*)", RegexOptions.Compiled);
|
Regex regRole = new Regex(@"\([饰|配]?\s*?(.+?)\)", RegexOptions.Compiled);
|
||||||
Regex regRole = new Regex(@"\([饰|配] (.+?)\)", RegexOptions.Compiled);
|
|
||||||
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
|
Regex regBackgroundImage = new Regex(@"url\(([^)]+?)\)$", RegexOptions.Compiled);
|
||||||
Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
|
Regex regLifedate = new Regex(@"(.+?) 至 (.+)", RegexOptions.Compiled);
|
||||||
Regex regConstellation = new Regex(@"星座: \n(.+?)\n", RegexOptions.Compiled);
|
Regex regHtmlTag = new Regex(@"<.?>", RegexOptions.Compiled);
|
||||||
Regex regBirthdate = new Regex(@"出生日期: \n(.+?)\n", RegexOptions.Compiled);
|
|
||||||
Regex regLifedate = new Regex(@"生卒日期: \n(.+?) 至", RegexOptions.Compiled);
|
|
||||||
Regex regBirthplace = new Regex(@"出生地: \n(.+?)\n", RegexOptions.Compiled);
|
|
||||||
Regex regCelebrityRole = new Regex(@"职业: \n(.+?)\n", RegexOptions.Compiled);
|
|
||||||
Regex regNickname = new Regex(@"更多外文名: \n(.+?)\n", RegexOptions.Compiled);
|
|
||||||
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
|
|
||||||
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", RegexOptions.Compiled);
|
|
||||||
Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
|
Regex regImgHost = new Regex(@"\/\/(img\d+?)\.", RegexOptions.Compiled);
|
||||||
// 匹配除了换行符之外所有空白
|
// 匹配除了换行符之外所有空白
|
||||||
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
|
Regex regOverviewSpace = new Regex(@"\n[^\S\n]+", RegexOptions.Compiled);
|
||||||
|
Regex regPhotoId = new Regex(@"/photo/(\d+?)/", RegexOptions.Compiled);
|
||||||
|
Regex regLoginName = new Regex(@"<div[^>]*?db-usr-profile[^>]*?>[\w\W]*?<h1>([^>]*?)<", RegexOptions.Compiled);
|
||||||
|
|
||||||
// 默认200毫秒请求1次
|
// 默认200毫秒请求1次
|
||||||
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200));
|
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(200));
|
||||||
|
@ -101,7 +82,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
var handler = new HttpClientHandlerEx();
|
var handler = new HttpClientHandlerEx();
|
||||||
this._cookieContainer = handler.CookieContainer;
|
this._cookieContainer = handler.CookieContainer;
|
||||||
httpClient = new HttpClient(handler);
|
httpClient = new HttpClient(handler);
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
httpClient.Timeout = TimeSpan.FromSeconds(20);
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
|
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
|
||||||
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
|
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
|
||||||
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
|
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
|
||||||
|
@ -162,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)
|
public async Task<List<DoubanSubject>> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var list = new List<DoubanSubject>();
|
var list = new List<DoubanSubject>();
|
||||||
|
@ -197,6 +190,12 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
foreach (var movieElement in movieElements)
|
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 rating = movieElement.GetText("div.rating-info>.rating_nums") ?? "0";
|
||||||
var img = movieElement.GetAttr("a.nbg>img", "src") ?? string.Empty;
|
var img = movieElement.GetAttr("a.nbg>img", "src") ?? string.Empty;
|
||||||
var oncick = movieElement.GetAttr("div.title a", "onclick") ?? string.Empty;
|
var oncick = movieElement.GetAttr("div.title a", "onclick") ?? string.Empty;
|
||||||
|
@ -436,46 +435,49 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var context = BrowsingContext.New();
|
var context = BrowsingContext.New();
|
||||||
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
|
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
|
||||||
var celebrityElements = doc.QuerySelectorAll("#content ul.celebrities-list li.celebrity");
|
|
||||||
|
|
||||||
foreach (var node in celebrityElements)
|
var celebritiesElements = doc.QuerySelectorAll("div#celebrities>.list-wrapper");
|
||||||
|
foreach (var celebritiesNode in celebritiesElements)
|
||||||
{
|
{
|
||||||
|
var celebritiesTitle = celebritiesNode.GetText("h2") ?? string.Empty;
|
||||||
var celebrityIdStr = node.GetAttr("div.info a.name", "href") ?? string.Empty;
|
if (!celebritiesTitle.Contains("导演") && !celebritiesTitle.Contains("演员"))
|
||||||
var celebrityId = celebrityIdStr.GetMatchGroup(this.regId);
|
|
||||||
var celebrityImgStr = node.GetAttr("div.avatar", "style") ?? string.Empty;
|
|
||||||
var celebrityImg = celebrityImgStr.GetMatchGroup(this.regBackgroundImage);
|
|
||||||
var celebrityNameStr = node.GetText("div.info a.name") ?? string.Empty;
|
|
||||||
var arr = celebrityNameStr.Split(" ");
|
|
||||||
var celebrityName = arr.Length > 1 ? arr[0].Trim() : celebrityNameStr;
|
|
||||||
// 有时存在演员信息缺少名字的
|
|
||||||
if (string.IsNullOrEmpty(celebrityName))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var celebrityRoleStr = node.GetText("div.info span.role") ?? string.Empty;
|
|
||||||
var celebrityRole = celebrityRoleStr.GetMatchGroup(this.regRole);
|
|
||||||
var arrRole = celebrityRoleStr.Split(" ");
|
|
||||||
var celebrityRoleType = arrRole.Length > 1 ? arrRole[0] : string.Empty;
|
|
||||||
if (string.IsNullOrEmpty(celebrityRole))
|
|
||||||
{
|
|
||||||
celebrityRole = celebrityRoleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自己/嘉宾一般用于综艺
|
|
||||||
if (celebrityRoleType != "导演" && celebrityRoleType != "配音" && celebrityRoleType != "演员" && celebrityRoleType != "自己" && celebrityRoleType != "嘉宾")
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var celebrity = new DoubanCelebrity();
|
var celebrityElements = celebritiesNode.QuerySelectorAll("ul.celebrities-list li.celebrity");
|
||||||
celebrity.Id = celebrityId;
|
foreach (var node in celebrityElements)
|
||||||
celebrity.Name = celebrityName;
|
{
|
||||||
celebrity.Role = celebrityRole;
|
|
||||||
celebrity.RoleType = celebrityRoleType;
|
|
||||||
celebrity.Img = celebrityImg;
|
|
||||||
|
|
||||||
list.Add(celebrity);
|
var celebrityIdStr = node.GetAttr("div.info a.name", "href") ?? string.Empty;
|
||||||
|
var celebrityId = celebrityIdStr.GetMatchGroup(this.regId);
|
||||||
|
var celebrityImgStr = node.GetAttr("div.avatar", "style") ?? string.Empty;
|
||||||
|
var celebrityImg = celebrityImgStr.GetMatchGroup(this.regBackgroundImage);
|
||||||
|
var celebrityNameStr = node.GetText("div.info a.name") ?? string.Empty;
|
||||||
|
var celebrityName = this.ParseCelebrityName(celebrityNameStr);
|
||||||
|
// 有时存在演员信息缺少名字的
|
||||||
|
if (string.IsNullOrEmpty(celebrityName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var celebrityRoleStr = node.GetText("div.info span.role") ?? string.Empty;
|
||||||
|
var celebrityRole = celebrityRoleStr.GetMatchGroup(this.regRole);
|
||||||
|
var arrRole = celebrityRoleStr.Split(" ");
|
||||||
|
var celebrityRoleType = arrRole.Length > 1 ? arrRole[0] : string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(celebrityRole))
|
||||||
|
{
|
||||||
|
celebrityRole = celebrityRoleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
var celebrity = new DoubanCelebrity();
|
||||||
|
celebrity.Id = celebrityId;
|
||||||
|
celebrity.Name = celebrityName;
|
||||||
|
celebrity.Role = celebrityRole;
|
||||||
|
celebrity.RoleType = celebrityRoleType;
|
||||||
|
celebrity.Img = celebrityImg;
|
||||||
|
|
||||||
|
list.Add(celebrity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_memoryCache.Set<List<DoubanCelebrity>>(cacheKey, list, expiredOption);
|
_memoryCache.Set<List<DoubanCelebrity>>(cacheKey, list, expiredOption);
|
||||||
|
@ -509,42 +511,64 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
var contentNode = doc.QuerySelector("#content");
|
var contentNode = doc.QuerySelector("#content");
|
||||||
if (contentNode != null)
|
if (contentNode != null)
|
||||||
{
|
{
|
||||||
var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
|
celebrity.Img = contentNode.GetAttr("img.avatar", "src") ?? string.Empty;
|
||||||
var nameStr = contentNode.GetText("h1") ?? string.Empty;
|
var nameStr = contentNode.GetText("h1.subject-name") ?? string.Empty;
|
||||||
var arr = nameStr.Split(" ");
|
celebrity.Name = this.ParseCelebrityName(nameStr);
|
||||||
var name = arr.Length > 1 ? arr[0] : 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 label = li.GetText("span.label") ?? string.Empty;
|
||||||
}
|
var value = li.GetText("span.value") ?? string.Empty;
|
||||||
var info = contentNode.GetText("div.info") ?? string.Empty;
|
switch (label)
|
||||||
var gender = info.GetMatchGroup(this.regGender);
|
{
|
||||||
var constellation = info.GetMatchGroup(this.regConstellation);
|
case "性别:":
|
||||||
var birthdate = info.GetMatchGroup(this.regBirthdate);
|
celebrity.Gender = value;
|
||||||
var lifedate = info.GetMatchGroup(this.regLifedate);
|
break;
|
||||||
if (string.IsNullOrEmpty(birthdate))
|
case "星座:":
|
||||||
{
|
celebrity.Constellation = value;
|
||||||
birthdate = lifedate;
|
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 intro = contentNode.GetHtml("section.subject-intro div.content") ?? string.Empty;
|
||||||
var nickname = info.GetMatchGroup(this.regNickname);
|
intro = regHtmlTag.Replace(intro.Replace("</p>", "\n"), "");
|
||||||
var family = info.GetMatchGroup(this.regFamily);
|
|
||||||
var imdb = info.GetMatchGroup(this.regCelebrityImdb);
|
|
||||||
|
|
||||||
celebrity.Img = img;
|
|
||||||
celebrity.Gender = gender;
|
|
||||||
celebrity.Birthdate = birthdate;
|
|
||||||
celebrity.Nickname = nickname;
|
|
||||||
celebrity.Imdb = imdb;
|
|
||||||
celebrity.Birthplace = birthplace;
|
|
||||||
celebrity.Name = name;
|
|
||||||
celebrity.Intro = formatOverview(intro);
|
celebrity.Intro = formatOverview(intro);
|
||||||
celebrity.Constellation = constellation;
|
|
||||||
celebrity.Role = role;
|
|
||||||
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
|
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
|
||||||
return celebrity;
|
return celebrity;
|
||||||
}
|
}
|
||||||
|
@ -555,6 +579,106 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<DoubanPhoto>> GetCelebrityPhotosAsync(string cid, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var list = new List<DoubanPhoto>();
|
||||||
|
if (string.IsNullOrEmpty(cid))
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheKey = $"celebrity_photo_{cid}";
|
||||||
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
|
if (_memoryCache.TryGetValue<List<DoubanPhoto>>(cacheKey, out var photos))
|
||||||
|
{
|
||||||
|
return photos;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LimitRequestFrequently();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://movie.douban.com/celebrity/{cid}/photos/";
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var context = BrowsingContext.New();
|
||||||
|
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
|
||||||
|
var elements = doc.QuerySelectorAll(".poster-col3>li");
|
||||||
|
|
||||||
|
foreach (var node in elements)
|
||||||
|
{
|
||||||
|
|
||||||
|
var href = node.QuerySelector("a")?.GetAttribute("href") ?? string.Empty;
|
||||||
|
var id = href.GetMatchGroup(this.regPhotoId);
|
||||||
|
var raw = node.QuerySelector("img")?.GetAttribute("src") ?? string.Empty;
|
||||||
|
var size = node.GetText("div.prop") ?? string.Empty;
|
||||||
|
|
||||||
|
var photo = new DoubanPhoto();
|
||||||
|
photo.Id = id;
|
||||||
|
photo.Size = size;
|
||||||
|
photo.Raw = raw;
|
||||||
|
if (!string.IsNullOrEmpty(size))
|
||||||
|
{
|
||||||
|
var arr = size.Split('x');
|
||||||
|
if (arr.Length == 2)
|
||||||
|
{
|
||||||
|
photo.Width = arr[0].ToInt();
|
||||||
|
photo.Height = arr[1].ToInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
_memoryCache.Set<List<DoubanPhoto>>(cacheKey, list, expiredOption);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this._logger.LogError(ex, "GetCelebrityPhotosAsync error. cid: {0}", cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ParseCelebrityName(string nameString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(nameString))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有中文名情况
|
||||||
|
var idx = nameString.IndexOf(" ", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
return nameString.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中英名混合情况
|
||||||
|
var firstName = nameString.Substring(0, idx);
|
||||||
|
if (firstName.HasChinese())
|
||||||
|
{
|
||||||
|
return firstName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 英文名重复两次的情况
|
||||||
|
var nextIndex = nameString[idx..].IndexOf(firstName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (nextIndex >= 0)
|
||||||
|
{
|
||||||
|
nextIndex = idx + nextIndex;
|
||||||
|
return nameString[..nextIndex].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有英文名情况
|
||||||
|
return nameString.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<DoubanCelebrity>> SearchCelebrityAsync(string keyword, CancellationToken cancellationToken)
|
public async Task<List<DoubanCelebrity>> SearchCelebrityAsync(string keyword, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var list = new List<DoubanCelebrity>();
|
var list = new List<DoubanCelebrity>();
|
||||||
|
@ -565,8 +689,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
|
|
||||||
var cacheKey = $"search_celebrity_{keyword}";
|
var cacheKey = $"search_celebrity_{keyword}";
|
||||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
List<DoubanCelebrity> searchResult;
|
if (_memoryCache.TryGetValue<List<DoubanCelebrity>>(cacheKey, out var searchResult))
|
||||||
if (_memoryCache.TryGetValue<List<DoubanCelebrity>>(cacheKey, out searchResult))
|
|
||||||
{
|
{
|
||||||
return searchResult;
|
return searchResult;
|
||||||
}
|
}
|
||||||
|
@ -617,8 +740,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
|
|
||||||
var cacheKey = $"photo_{sid}";
|
var cacheKey = $"photo_{sid}";
|
||||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
List<DoubanPhoto> photos;
|
if (_memoryCache.TryGetValue<List<DoubanPhoto>>(cacheKey, out var photos))
|
||||||
if (_memoryCache.TryGetValue<List<DoubanPhoto>>(cacheKey, out photos))
|
|
||||||
{
|
{
|
||||||
return photos;
|
return photos;
|
||||||
}
|
}
|
||||||
|
@ -701,6 +823,27 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DoubanLoginInfo> GetLoginInfoAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var loginInfo = new DoubanLoginInfo();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = "https://www.douban.com/mine/";
|
||||||
|
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
var requestUrl = response.RequestMessage?.RequestUri?.ToString();
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var loginName = this.Match(body, regLoginName).Trim();
|
||||||
|
loginInfo.Name = loginName;
|
||||||
|
loginInfo.IsLogined = !(requestUrl == null || requestUrl.Contains("accounts.douban.com") || requestUrl.Contains("login") || requestUrl.Contains("sec.douban.com"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this._logger.LogError(ex, "GetLoginInfoAsync error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginInfo;
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task LimitRequestFrequently()
|
protected async Task LimitRequestFrequently()
|
||||||
{
|
{
|
||||||
if (IsEnableAvoidRiskControl())
|
if (IsEnableAvoidRiskControl())
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
|
{
|
||||||
|
public class ImdbApi : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<DoubanApi> _logger;
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
|
||||||
|
Regex regId = new Regex(@"/(tt\d+)", RegexOptions.Compiled);
|
||||||
|
Regex regPersonId = new Regex(@"/(nm\d+)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public ImdbApi(ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory.CreateLogger<DoubanApi>();
|
||||||
|
_memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
|
||||||
|
var handler = new HttpClientHandler()
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false
|
||||||
|
};
|
||||||
|
httpClient = new HttpClient(handler);
|
||||||
|
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过imdb获取信息(会返回最新的imdb id)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> CheckNewIDAsync(string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cacheKey = $"CheckNewImdbID_{id}";
|
||||||
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
|
if (this._memoryCache.TryGetValue<string?>(cacheKey, out var item))
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://www.imdb.com/title/{id}/";
|
||||||
|
var resp = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (resp.Headers.TryGetValues("Location", out var values))
|
||||||
|
{
|
||||||
|
var location = values.First();
|
||||||
|
var newId = location.GetMatchGroup(this.regId);
|
||||||
|
if (!string.IsNullOrEmpty(newId))
|
||||||
|
{
|
||||||
|
item = newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._memoryCache.Set(cacheKey, item, expiredOption);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this._logger.LogError(ex, "CheckNewImdbID error. id: {0}", id);
|
||||||
|
this._memoryCache.Set<string?>(cacheKey, null, expiredOption);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过imdb获取信息(会返回最新的imdb id)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string?> CheckPersonNewIDAsync(string id, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cacheKey = $"CheckPersonNewImdbID_{id}";
|
||||||
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
|
if (this._memoryCache.TryGetValue<string?>(cacheKey, out var item))
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://www.imdb.com/name/{id}/";
|
||||||
|
var resp = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (resp.Headers.TryGetValues("Location", out var values))
|
||||||
|
{
|
||||||
|
var location = values.First();
|
||||||
|
var newId = location.GetMatchGroup(this.regPersonId);
|
||||||
|
if (!string.IsNullOrEmpty(newId))
|
||||||
|
{
|
||||||
|
item = newId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._memoryCache.Set(cacheKey, item, expiredOption);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this._logger.LogError(ex, "CheckPersonNewImdbID error. id: {0}", id);
|
||||||
|
this._memoryCache.Set<string?>(cacheKey, null, expiredOption);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_memoryCache.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEnable()
|
||||||
|
{
|
||||||
|
return Plugin.Instance?.Configuration.EnableTmdb ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,9 @@
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using Jellyfin.Plugin.MetaShark.Api.Http;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -45,8 +39,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
|
|
||||||
var cacheKey = $"GetByImdbID_{id}";
|
var cacheKey = $"GetByImdbID_{id}";
|
||||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||||
OmdbItem? item;
|
if (this._memoryCache.TryGetValue<OmdbItem?>(cacheKey, out var item))
|
||||||
if (this._memoryCache.TryGetValue(cacheKey, out item))
|
|
||||||
{
|
{
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
var config = Plugin.Instance?.Configuration;
|
var config = Plugin.Instance?.Configuration;
|
||||||
var apiKey = string.IsNullOrEmpty(config?.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
|
var apiKey = string.IsNullOrEmpty(config?.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
|
||||||
var host = string.IsNullOrEmpty(config?.TmdbHost) ? DEFAULT_API_HOST : config.TmdbHost;
|
var host = string.IsNullOrEmpty(config?.TmdbHost) ? DEFAULT_API_HOST : config.TmdbHost;
|
||||||
_tmDbClient = new TMDbClient(apiKey, true, host);
|
_tmDbClient = new TMDbClient(apiKey, true, host, null, config?.GetTmdbWebProxy());
|
||||||
_tmDbClient.RequestTimeout = TimeSpan.FromSeconds(10);
|
_tmDbClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
// Not really interested in NotFoundException
|
// Not really interested in NotFoundException
|
||||||
_tmDbClient.ThrowApiExceptions = false;
|
_tmDbClient.ThrowApiExceptions = false;
|
||||||
}
|
}
|
||||||
|
@ -597,6 +597,16 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
||||||
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
|
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? GetLogoUrl(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.LogoSizes[^1], filePath).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,24 +1,9 @@
|
||||||
using MediaBrowser.Model.Plugins;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Configuration;
|
namespace Jellyfin.Plugin.MetaShark.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The configuration options.
|
|
||||||
/// </summary>
|
|
||||||
public enum SomeOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Option one.
|
|
||||||
/// </summary>
|
|
||||||
OneOption,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Second option.
|
|
||||||
/// </summary>
|
|
||||||
AnotherOption
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plugin configuration.
|
/// Plugin configuration.
|
||||||
|
@ -28,42 +13,87 @@ public class PluginConfiguration : BasePluginConfiguration
|
||||||
public const int MAX_CAST_MEMBERS = 15;
|
public const int MAX_CAST_MEMBERS = 15;
|
||||||
public const int MAX_SEARCH_RESULT = 5;
|
public const int MAX_SEARCH_RESULT = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插件版本
|
||||||
|
/// </summary>
|
||||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
public string DoubanCookies { get; set; } = string.Empty;
|
public string DoubanCookies { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 开启防封禁
|
/// 豆瓣开启防封禁
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableDoubanAvoidRiskControl { get; set; } = false;
|
public bool EnableDoubanAvoidRiskControl { get; set; } = false;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 背景图使用原图
|
/// 豆瓣海报使用大图
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableDoubanLargePoster { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// 豆瓣背景图使用原图
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableDoubanBackdropRaw { get; set; } = false;
|
public bool EnableDoubanBackdropRaw { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// 豆瓣图片代理地址
|
||||||
|
/// </summary>
|
||||||
|
public string DoubanImageProxyBaseUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用获取tmdb元数据
|
||||||
|
/// </summary>
|
||||||
public bool EnableTmdb { get; set; } = true;
|
public bool EnableTmdb { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用显示tmdb搜索结果
|
||||||
|
/// </summary>
|
||||||
public bool EnableTmdbSearch { get; set; } = false;
|
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>
|
||||||
/// 是否获取电影系列信息
|
/// 是否获取电影系列信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableTmdbCollection { get; set; } = false;
|
public bool EnableTmdbCollection { get; set; } = true;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否获取tmdb分级信息
|
/// 是否获取tmdb分级信息
|
||||||
/// </summary>
|
/// </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;
|
public string TmdbApiKey { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// tmdb api host
|
||||||
|
/// </summary>
|
||||||
public string TmdbHost { get; set; } = string.Empty;
|
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;
|
if (!string.IsNullOrEmpty(TmdbProxyType))
|
||||||
|
{
|
||||||
public int MaxSearchResult { get; set; } = 5;
|
return new WebProxy($"{TmdbProxyType}://{TmdbProxyHost}:{TmdbProxyPort}", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,8 @@
|
||||||
</legend>
|
</legend>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
|
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
|
||||||
id="login_invalid"
|
id="login_msg"
|
||||||
style="color: red; margin-left: 8px; display: none;">(已失效)</span></label>
|
style="margin-left: 8px; display: none;"></span></label>
|
||||||
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
|
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
|
||||||
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
|
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
|
||||||
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
|
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
|
||||||
|
@ -39,8 +39,6 @@
|
||||||
<input id="EnableDoubanAvoidRiskControl" name="EnableDoubanAvoidRiskControl"
|
<input id="EnableDoubanAvoidRiskControl" name="EnableDoubanAvoidRiskControl"
|
||||||
type="checkbox" is="emby-checkbox" />
|
type="checkbox" is="emby-checkbox" />
|
||||||
<span class="checkboxLabel" style="position:relative">启用防封禁
|
<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"
|
<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="
|
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" />
|
alt="beta" />
|
||||||
|
@ -48,6 +46,21 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">勾选后,刮削会变慢,适合刮削大量影片时使用,建议搭配网站cookie一起使用</div>
|
<div class="fieldDescription">勾选后,刮削会变慢,适合刮削大量影片时使用,建议搭配网站cookie一起使用</div>
|
||||||
</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">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label" for="EnableDoubanBackdropRaw">
|
<label class="emby-checkbox-label" for="EnableDoubanBackdropRaw">
|
||||||
<input id="EnableDoubanBackdropRaw" name="EnableDoubanBackdropRaw" type="checkbox"
|
<input id="EnableDoubanBackdropRaw" name="EnableDoubanBackdropRaw" type="checkbox"
|
||||||
|
@ -85,6 +98,14 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="fieldDescription">勾选后,当影片在豆瓣找不到背景图时,改使用TheMovieDb的补全</div>
|
<div class="fieldDescription">勾选后,当影片在豆瓣找不到背景图时,改使用TheMovieDb的补全</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label" for="EnableTmdbLogo">
|
||||||
|
<input id="EnableTmdbLogo" name="EnableTmdbLogo" type="checkbox"
|
||||||
|
is="emby-checkbox" />
|
||||||
|
<span>从TheMovieDb获取商标</span>
|
||||||
|
</label>
|
||||||
|
<div class="fieldDescription">勾选后,使用TheMovieDb的商标图片补全</div>
|
||||||
|
</div>
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
<label class="emby-checkbox-label" for="EnableTmdbCollection">
|
<label class="emby-checkbox-label" for="EnableTmdbCollection">
|
||||||
<input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox"
|
<input id="EnableTmdbCollection" name="EnableTmdbCollection" type="checkbox"
|
||||||
|
@ -113,6 +134,29 @@
|
||||||
<div class="fieldDescription">
|
<div class="fieldDescription">
|
||||||
填写Api域名,可选api.tmdb.org/api.themoviedb.org,默认api.tmdb.org.(需重启才能生效)</div>
|
填写Api域名,可选api.tmdb.org/api.themoviedb.org,默认api.tmdb.org.(需重启才能生效)</div>
|
||||||
</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>
|
</fieldset>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
<span>Save</span>
|
<span>Save</span>
|
||||||
|
@ -128,21 +172,31 @@
|
||||||
|
|
||||||
document.querySelector('#TemplateConfigPage')
|
document.querySelector('#TemplateConfigPage')
|
||||||
.addEventListener('pageshow', function () {
|
.addEventListener('pageshow', function () {
|
||||||
|
console.log('metashark pageshow');
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||||
$('#current_version').text("v" + config.Version);
|
$('#current_version').text("v" + config.Version);
|
||||||
|
|
||||||
document.querySelector('#DoubanCookies').value = config.DoubanCookies;
|
document.querySelector('#DoubanCookies').value = config.DoubanCookies;
|
||||||
|
document.querySelector('#DoubanImageProxyBaseUrl').value = config.DoubanImageProxyBaseUrl;
|
||||||
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
|
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
|
||||||
|
document.querySelector('#EnableDoubanLargePoster').checked = config.EnableDoubanLargePoster;
|
||||||
document.querySelector('#EnableDoubanBackdropRaw').checked = config.EnableDoubanBackdropRaw;
|
document.querySelector('#EnableDoubanBackdropRaw').checked = config.EnableDoubanBackdropRaw;
|
||||||
|
|
||||||
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
|
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
|
||||||
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
|
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
|
||||||
document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop;
|
document.querySelector('#EnableTmdbBackdrop').checked = config.EnableTmdbBackdrop;
|
||||||
|
document.querySelector('#EnableTmdbLogo').checked = config.EnableTmdbLogo;
|
||||||
document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection;
|
document.querySelector('#EnableTmdbCollection').checked = config.EnableTmdbCollection;
|
||||||
document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating;
|
document.querySelector('#EnableTmdbOfficialRating').checked = config.EnableTmdbOfficialRating;
|
||||||
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
|
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
|
||||||
document.querySelector('#TmdbHost').value = config.TmdbHost;
|
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();
|
checkDoubanLogin();
|
||||||
|
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
|
@ -154,15 +208,22 @@
|
||||||
Dashboard.showLoadingMsg();
|
Dashboard.showLoadingMsg();
|
||||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||||
config.DoubanCookies = document.querySelector('#DoubanCookies').value;
|
config.DoubanCookies = document.querySelector('#DoubanCookies').value;
|
||||||
|
config.DoubanImageProxyBaseUrl = document.querySelector('#DoubanImageProxyBaseUrl').value;
|
||||||
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
|
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
|
||||||
|
config.EnableDoubanLargePoster = document.querySelector('#EnableDoubanLargePoster').checked;
|
||||||
config.EnableDoubanBackdropRaw = document.querySelector('#EnableDoubanBackdropRaw').checked;
|
config.EnableDoubanBackdropRaw = document.querySelector('#EnableDoubanBackdropRaw').checked;
|
||||||
|
|
||||||
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
|
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
|
||||||
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
|
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
|
||||||
config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked;
|
config.EnableTmdbBackdrop = document.querySelector('#EnableTmdbBackdrop').checked;
|
||||||
|
config.EnableTmdbLogo = document.querySelector('#EnableTmdbLogo').checked;
|
||||||
config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked;
|
config.EnableTmdbCollection = document.querySelector('#EnableTmdbCollection').checked;
|
||||||
config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked;
|
config.EnableTmdbOfficialRating = document.querySelector('#EnableTmdbOfficialRating').checked;
|
||||||
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
|
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
|
||||||
config.TmdbHost = document.querySelector('#TmdbHost').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) {
|
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
|
|
||||||
|
@ -174,19 +235,34 @@
|
||||||
return false;
|
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() {
|
function checkDoubanLogin() {
|
||||||
let cookie = document.querySelector('#DoubanCookies').value
|
let cookie = document.querySelector('#DoubanCookies').value
|
||||||
if (!cookie || !$.trim(cookie)) {
|
if (!cookie || !$.trim(cookie)) {
|
||||||
$('#login_invalid').hide();
|
$('#login_msg').hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
|
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
|
||||||
if (resp && resp.code != 1) {
|
if (resp && resp.code != 1) {
|
||||||
$('#login_invalid').show();
|
$('#login_msg').css("color", "red").text('(已失效)').show();
|
||||||
} else {
|
} else {
|
||||||
$('#login_invalid').hide();
|
$('#login_msg').css("color", "").text('(已生效)').show();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Controller.Providers;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
|
@ -28,16 +15,16 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[Route("/plugin/metashark")]
|
[Route("/plugin/metashark")]
|
||||||
public class MetaSharkController : ControllerBase
|
public class ApiController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DoubanApi _doubanApi;
|
private readonly DoubanApi _doubanApi;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MetaSharkController"/> class.
|
/// Initializes a new instance of the <see cref="ApiController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||||
public MetaSharkController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
|
public ApiController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
|
||||||
{
|
{
|
||||||
this._httpClientFactory = httpClientFactory;
|
this._httpClientFactory = httpClientFactory;
|
||||||
this._doubanApi = doubanApi;
|
this._doubanApi = doubanApi;
|
||||||
|
@ -61,7 +48,8 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
||||||
var httpClient = GetHttpClient();
|
var httpClient = GetHttpClient();
|
||||||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
||||||
{
|
{
|
||||||
requestMessage.Headers.Add("Referer", "https://www.douban.com/");
|
requestMessage.Headers.Add("User-Agent", DoubanApi.HTTP_USER_AGENT);
|
||||||
|
requestMessage.Headers.Add("Referer", DoubanApi.HTTP_REFERER);
|
||||||
|
|
||||||
response = await httpClient.SendAsync(requestMessage);
|
response = await httpClient.SendAsync(requestMessage);
|
||||||
}
|
}
|
||||||
|
@ -89,10 +77,11 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ApiResult> CheckDoubanLogin()
|
public async Task<ApiResult> CheckDoubanLogin()
|
||||||
{
|
{
|
||||||
var isLogin = await _doubanApi.CheckLoginAsync(CancellationToken.None);
|
var loginInfo = await this._doubanApi.GetLoginInfoAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
return new ApiResult(isLogin ? 1 : 0, isLogin ? "logined" : "not login");
|
return new ApiResult(loginInfo.IsLogined ? 1 : 0, loginInfo.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private HttpClient GetHttpClient()
|
private HttpClient GetHttpClient()
|
||||||
{
|
{
|
||||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
|
@ -20,6 +20,17 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string? GetHtml(this IElement el, string css)
|
||||||
|
{
|
||||||
|
var node = el.QuerySelector(css);
|
||||||
|
if (node != null)
|
||||||
|
{
|
||||||
|
return node.Html().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
|
public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
|
||||||
{
|
{
|
||||||
var node = el.QuerySelector(css);
|
var node = el.QuerySelector(css);
|
||||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Emby.Naming.TV;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Core
|
namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
|
@ -107,23 +108,32 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 假如Anitomy解析不到year,尝试使用jellyfin默认parser,看能不能解析成功
|
// 假如 Anitomy 解析不到 year,尝试使用 jellyfin 默认 parser,看能不能解析成功
|
||||||
if (parseResult.Year == null && !isAnime)
|
if (parseResult.Year == null && !isAnime)
|
||||||
{
|
{
|
||||||
var nativeParseResult = ParseMovie(fileName);
|
var nativeParseResult = ParseMovieByDefault(fileName);
|
||||||
if (nativeParseResult.Year != null)
|
if (nativeParseResult.Year != null)
|
||||||
{
|
{
|
||||||
parseResult = nativeParseResult;
|
parseResult = nativeParseResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 假如 Anitomy 解析不到集数,判断 name 是否是数字集号
|
||||||
|
if (parseResult.IndexNumber is null && isEpisode)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(parseResult.Name) && parseResult.Name.IsNumericString())
|
||||||
|
{
|
||||||
|
parseResult.IndexNumber = parseResult.Name.ToInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 修复纯中文集数/特殊标识集数
|
// 修复纯中文集数/特殊标识集数
|
||||||
if (parseResult.IndexNumber is null)
|
if (parseResult.IndexNumber is null)
|
||||||
{
|
{
|
||||||
parseResult.IndexNumber = ParseChineseOrSpecialIndexNumber(fileName);
|
parseResult.IndexNumber = ParseChineseOrSpecialIndexNumber(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析不到title时,或解析出多个title时,使用默认名
|
// 解析不到 title 时,或解析出多个 title 时,使用默认名
|
||||||
if (string.IsNullOrEmpty(parseResult.Name))
|
if (string.IsNullOrEmpty(parseResult.Name))
|
||||||
{
|
{
|
||||||
parseResult.Name = fileName;
|
parseResult.Name = fileName;
|
||||||
|
@ -132,6 +142,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
return parseResult;
|
return parseResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ParseNameResult ParseEpisode(string fileName)
|
||||||
|
{
|
||||||
|
return Parse(fileName, true);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CleanName(string name)
|
private static string CleanName(string name)
|
||||||
{
|
{
|
||||||
// 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉
|
// 电视剧名称后紧跟季信息时,会附加到名称中,需要去掉
|
||||||
|
@ -143,8 +158,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
return name.Replace(".", " ").Trim();
|
return name.Replace(".", " ").Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// emby原始电影解析
|
/// <summary>
|
||||||
public static ParseNameResult ParseMovie(string fileName)
|
/// emby原始电影解析
|
||||||
|
/// </summary>
|
||||||
|
public static ParseNameResult ParseMovieByDefault(string fileName)
|
||||||
{
|
{
|
||||||
// 默认解析器会错误把分辨率当年份,先删除
|
// 默认解析器会错误把分辨率当年份,先删除
|
||||||
fileName = resolutionReg.Replace(fileName, "");
|
fileName = resolutionReg.Replace(fileName, "");
|
||||||
|
@ -165,6 +182,18 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
return parseResult;
|
return parseResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// emby原始剧集解析
|
||||||
|
/// </summary>
|
||||||
|
public static EpisodePathParserResult ParseEpisodeByDefault(string fileName)
|
||||||
|
{
|
||||||
|
// EpisodePathParser需要路径信息, 这里添加一个分隔符模拟路径
|
||||||
|
var path = Path.DirectorySeparatorChar + fileName;
|
||||||
|
var nameOptions = new Emby.Naming.Common.NamingOptions();
|
||||||
|
return new EpisodePathParser(nameOptions)
|
||||||
|
.Parse(path, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static int ParseYear(string val)
|
private static int ParseYear(string val)
|
||||||
{
|
{
|
||||||
|
@ -217,15 +246,21 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
return null;
|
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;
|
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;
|
var folder = Path.GetFileName(Path.GetDirectoryName(path))?.ToUpper() ?? string.Empty;
|
||||||
|
if (isDirectory) {
|
||||||
|
folder = Path.GetFileName(path)?.ToUpper() ?? string.Empty;
|
||||||
|
}
|
||||||
return folder == "EXTRA"
|
return folder == "EXTRA"
|
||||||
|| folder == "MENU"
|
|| folder == "MENU"
|
||||||
|| folder == "MENUS"
|
|| folder == "MENUS"
|
||||||
|
|
|
@ -78,5 +78,10 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
||||||
|
|
||||||
return string.Empty;
|
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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
|
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
|
||||||
<GenerateDocumentationFile>False</GenerateDocumentationFile>
|
<GenerateDocumentationFile>False</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||||
|
@ -13,11 +14,21 @@
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Vendor\TMDbLib\**" />
|
||||||
|
<EmbeddedResource Remove="Vendor\TMDbLib\**" />
|
||||||
|
<None Remove="Vendor\TMDbLib\**" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="1.0.1" />
|
<PackageReference Include="AngleSharp" Version="1.0.1" />
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
|
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.32">
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
|
<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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||||
|
@ -29,6 +40,6 @@
|
||||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Vendor\TMDbLib\TMDbLib.csproj" />
|
<ProjectReference Include="..\AnitomySharp\AnitomySharp.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</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 System;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Model
|
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]
|
[JsonIgnore]
|
||||||
public string[] Genres
|
public string[] Genres
|
||||||
{
|
{
|
||||||
|
@ -120,8 +126,10 @@ namespace Jellyfin.Plugin.MetaShark.Model
|
||||||
public string Gender { get; set; }
|
public string Gender { get; set; }
|
||||||
public string Constellation { get; set; }
|
public string Constellation { get; set; }
|
||||||
public string Birthdate { get; set; }
|
public string Birthdate { get; set; }
|
||||||
|
public string Enddate { get; set; }
|
||||||
public string Birthplace { get; set; }
|
public string Birthplace { get; set; }
|
||||||
public string Nickname { get; set; }
|
public string NickName { get; set; }
|
||||||
|
public string EnglishName { get; set; }
|
||||||
public string Imdb { get; set; }
|
public string Imdb { get; set; }
|
||||||
public string Site { get; set; }
|
public string Site { get; set; }
|
||||||
|
|
||||||
|
@ -142,6 +150,30 @@ namespace Jellyfin.Plugin.MetaShark.Model
|
||||||
_roleType = value;
|
_roleType = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? DisplayOriginalName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// 外国人才显示英文名
|
||||||
|
if (Name.Contains("·") && Birthplace != null && !Birthplace.Contains("中国"))
|
||||||
|
{
|
||||||
|
return EnglishName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string ImgMiddle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return this.Img.Replace("/raw/", "/m/").Replace("/s_ratio_poster/", "/m/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DoubanPhoto
|
public class DoubanPhoto
|
||||||
|
|
|
@ -6,9 +6,34 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Model
|
namespace Jellyfin.Plugin.MetaShark.Model
|
||||||
{
|
{
|
||||||
public static class MetaSource
|
|
||||||
|
public enum MetaSource
|
||||||
{
|
{
|
||||||
public const string Douban = "douban";
|
Douban,
|
||||||
public const string Tmdb = "tmdb";
|
Tmdb,
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MetaSourceExtensions
|
||||||
|
{
|
||||||
|
public static MetaSource ToMetaSource(this string? str)
|
||||||
|
{
|
||||||
|
if (str == null)
|
||||||
|
{
|
||||||
|
return MetaSource.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.ToLower().StartsWith("douban"))
|
||||||
|
{
|
||||||
|
return MetaSource.Douban;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.ToLower().StartsWith("tmdb"))
|
||||||
|
{
|
||||||
|
return MetaSource.Tmdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetaSource.None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||||
using Jellyfin.Plugin.MetaShark.Configuration;
|
using Jellyfin.Plugin.MetaShark.Configuration;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
@ -26,14 +27,17 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
public const string ProviderId = "MetaSharkID";
|
public const string ProviderId = "MetaSharkID";
|
||||||
|
|
||||||
|
|
||||||
|
private readonly IServerApplicationHost _appHost;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
public Plugin(IServerApplicationHost appHost, IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
|
this._appHost = appHost;
|
||||||
Plugin.Instance = this;
|
Plugin.Instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,8 +60,26 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
new PluginPageInfo
|
new PluginPageInfo
|
||||||
{
|
{
|
||||||
Name = this.Name,
|
Name = this.Name,
|
||||||
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
|
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetLocalApiBaseUrl()
|
||||||
|
{
|
||||||
|
return this._appHost.GetLocalApiUrl("127.0.0.1", "http");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetApiBaseUrl(HttpRequest request)
|
||||||
|
{
|
||||||
|
int? requestPort = request.Host.Port;
|
||||||
|
if (requestPort == null
|
||||||
|
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
requestPort = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._appHost.GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using StringMetric;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
@ -13,7 +11,6 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -22,6 +19,7 @@ using TMDbLib.Objects.General;
|
||||||
using Jellyfin.Plugin.MetaShark.Configuration;
|
using Jellyfin.Plugin.MetaShark.Configuration;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
|
@ -47,11 +45,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
protected readonly DoubanApi _doubanApi;
|
protected readonly DoubanApi _doubanApi;
|
||||||
protected readonly TmdbApi _tmdbApi;
|
protected readonly TmdbApi _tmdbApi;
|
||||||
protected readonly OmdbApi _omdbApi;
|
protected readonly OmdbApi _omdbApi;
|
||||||
|
protected readonly ImdbApi _imdbApi;
|
||||||
protected readonly ILibraryManager _libraryManager;
|
protected readonly ILibraryManager _libraryManager;
|
||||||
protected readonly IHttpContextAccessor _httpContextAccessor;
|
protected readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled);
|
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled);
|
||||||
protected Regex regSeasonNameSuffix = new Regex(@"\s第[0-9一二三四五六七八九十]+?季$|\sSeason\s\d+?$|(?<![0-9a-zA-Z])\d$", RegexOptions.Compiled);
|
protected Regex regSeasonNameSuffix = new Regex(@"\s第[0-9一二三四五六七八九十]+?季$|\sSeason\s\d+?$|(?<![0-9a-zA-Z])\d$", RegexOptions.Compiled);
|
||||||
|
protected Regex regDoubanIdAttribute = new Regex(@"\[(?:douban|doubanid)-(\d+?)\]", RegexOptions.Compiled);
|
||||||
|
|
||||||
protected PluginConfiguration config
|
protected PluginConfiguration config
|
||||||
{
|
{
|
||||||
|
@ -61,57 +61,65 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string RequestDomain
|
protected BaseProvider(IHttpClientFactory httpClientFactory, ILogger logger, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_httpContextAccessor.HttpContext != null)
|
|
||||||
{
|
|
||||||
return _httpContextAccessor.HttpContext.Request.Scheme + System.Uri.SchemeDelimiter + _httpContextAccessor.HttpContext.Request.Host;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected string RequestPath
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_httpContextAccessor.HttpContext != null)
|
|
||||||
{
|
|
||||||
return _httpContextAccessor.HttpContext.Request.Path.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected BaseProvider(IHttpClientFactory httpClientFactory, ILogger logger, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
{
|
{
|
||||||
this._doubanApi = doubanApi;
|
this._doubanApi = doubanApi;
|
||||||
this._tmdbApi = tmdbApi;
|
this._tmdbApi = tmdbApi;
|
||||||
this._omdbApi = omdbApi;
|
this._omdbApi = omdbApi;
|
||||||
|
this._imdbApi = imdbApi;
|
||||||
this._libraryManager = libraryManager;
|
this._libraryManager = libraryManager;
|
||||||
this._logger = logger;
|
this._logger = logger;
|
||||||
this._httpClientFactory = httpClientFactory;
|
this._httpClientFactory = httpClientFactory;
|
||||||
this._httpContextAccessor = httpContextAccessor;
|
this._httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (url.Contains("doubanio.com"))
|
||||||
|
{
|
||||||
|
// 相对链接补全
|
||||||
|
if (!url.StartsWith("http") && Plugin.Instance != null)
|
||||||
|
{
|
||||||
|
url = Plugin.Instance.GetLocalApiBaseUrl().TrimEnd('/') + url;
|
||||||
|
}
|
||||||
|
// 包含了代理地址的话,从url解析出原始豆瓣图片地址
|
||||||
|
if (url.Contains("/proxy/image"))
|
||||||
|
{
|
||||||
|
var uri = new UriBuilder(url);
|
||||||
|
url = HttpUtility.ParseQueryString(uri.Query).Get("url");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Log("GetImageResponse url: {0}", url);
|
||||||
|
// 豆瓣图,带referer下载
|
||||||
|
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
||||||
|
{
|
||||||
|
requestMessage.Headers.Add("User-Agent", DoubanApi.HTTP_USER_AGENT);
|
||||||
|
requestMessage.Headers.Add("Referer", DoubanApi.HTTP_REFERER);
|
||||||
|
return await this._httpClientFactory.CreateClient().SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.Log("GetImageResponse url: {0}", url);
|
||||||
|
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
|
protected async Task<string?> GuessByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var fileName = GetOriginalFileName(info);
|
var fileName = GetOriginalFileName(info);
|
||||||
|
// 从文件名属性格式获取,如[douban-12345]或[doubanid-12345]
|
||||||
|
var doubanId = this.regDoubanIdAttribute.FirstMatchGroup(fileName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(doubanId))
|
||||||
|
{
|
||||||
|
this.Log($"Found douban [id] by attr: {doubanId}");
|
||||||
|
return doubanId;
|
||||||
|
}
|
||||||
var parseResult = NameParser.Parse(fileName);
|
var parseResult = NameParser.Parse(fileName);
|
||||||
var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name;
|
var searchName = !string.IsNullOrEmpty(parseResult.ChineseName) ? parseResult.ChineseName : parseResult.Name;
|
||||||
info.Year = parseResult.Year; // 默认parser对anime年份会解析出错,以anitomy为准
|
info.Year = parseResult.Year; // 默认parser对anime年份会解析出错,以anitomy为准
|
||||||
|
|
||||||
|
|
||||||
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
|
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
|
||||||
List<DoubanSubject> result;
|
List<DoubanSubject> result;
|
||||||
DoubanSubject? item;
|
DoubanSubject? item;
|
||||||
|
@ -125,13 +133,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
|
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
|
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
|
||||||
return item.Sid;
|
return item.Sid;
|
||||||
}
|
}
|
||||||
item = result.Where(x => x.Year == info.Year).FirstOrDefault();
|
item = result.Where(x => x.Year == info.Year).FirstOrDefault();
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid}) (suggest)");
|
this.Log($"Found douban [id]: {item.Name}({item.Sid}) (suggest)");
|
||||||
return item.Sid;
|
return item.Sid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +177,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
item = result.Where(x => x.Category == cat).FirstOrDefault();
|
item = result.Where(x => x.Category == cat).FirstOrDefault();
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
this.Log($"GuessByDouban found -> {item.Name}({item.Sid})");
|
this.Log($"Found douban [id] by first match: {item.Name}({item.Sid})");
|
||||||
return item.Sid;
|
return item.Sid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +278,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
case MovieInfo:
|
case MovieInfo:
|
||||||
var movieResults = await this._tmdbApi.SearchMovieAsync(name, year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
var movieResults = await this._tmdbApi.SearchMovieAsync(name, year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||||
var movieItem = movieResults.FirstOrDefault();
|
// 结果可能多个,优先取名称完全相同的
|
||||||
|
var movieItem = movieResults.Where(x => x.Title == name || x.OriginalTitle == name).FirstOrDefault();
|
||||||
|
if (movieItem != null)
|
||||||
|
{
|
||||||
|
this.Log($"Found tmdb [id]: {movieItem.Title}({movieItem.Id})");
|
||||||
|
return movieItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
movieItem = movieResults.FirstOrDefault();
|
||||||
if (movieItem != null)
|
if (movieItem != null)
|
||||||
{
|
{
|
||||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||||
|
@ -280,7 +295,29 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
break;
|
break;
|
||||||
case SeriesInfo:
|
case SeriesInfo:
|
||||||
var seriesResults = await this._tmdbApi.SearchSeriesAsync(name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
var seriesResults = await this._tmdbApi.SearchSeriesAsync(name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||||
var seriesItem = seriesResults.FirstOrDefault();
|
// 年份在豆瓣可能匹配到第三季,但tmdb年份都是第一季的,可能匹配不上(例如:脱口秀大会)
|
||||||
|
// 优先年份和名称同时匹配
|
||||||
|
var seriesItem = seriesResults.Where(x => (x.Name == name || x.OriginalName == name) && x.FirstAirDate?.Year == year).FirstOrDefault();
|
||||||
|
if (seriesItem != null)
|
||||||
|
{
|
||||||
|
this.Log($"Found tmdb [id]: -> {seriesItem.Name}({seriesItem.Id})");
|
||||||
|
return seriesItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
// 年份匹配
|
||||||
|
seriesItem = seriesResults.Where(x => x.FirstAirDate?.Year == year).FirstOrDefault();
|
||||||
|
if (seriesItem != null)
|
||||||
|
{
|
||||||
|
this.Log($"Found tmdb [id]: -> {seriesItem.Name}({seriesItem.Id})");
|
||||||
|
return seriesItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
// 取名称完全相同的,可能综艺会有纯享版等非标准版本(例如:一年一度喜剧大赛)
|
||||||
|
seriesItem = seriesResults.Where(x => x.Name == name || x.OriginalName == name).FirstOrDefault();
|
||||||
|
if (seriesItem != null)
|
||||||
|
{
|
||||||
|
this.Log($"Found tmdb [id]: -> {seriesItem.Name}({seriesItem.Id})");
|
||||||
|
return seriesItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
seriesItem = seriesResults.FirstOrDefault();
|
||||||
if (seriesItem != null)
|
if (seriesItem != null)
|
||||||
{
|
{
|
||||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||||
|
@ -290,50 +327,86 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Log($"Not found tmdb id by [name]: {name} [year]: {year}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected async Task<string?> GetTmdbIdByImdbAsync(string imdb, string language, CancellationToken cancellationToken)
|
protected async Task<string?> GetTmdbIdByImdbAsync(string imdb, string language, ItemLookupInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(imdb))
|
if (string.IsNullOrEmpty(imdb))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 豆瓣的imdb id可能是旧的,需要先从omdb接口获取最新的imdb id
|
// 通过imdb获取tmdbId
|
||||||
|
var findResult = await this._tmdbApi.FindByExternalIdAsync(imdb, TMDbLib.Objects.Find.FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
switch (info)
|
||||||
|
{
|
||||||
|
case MovieInfo:
|
||||||
|
if (findResult?.MovieResults != null && findResult.MovieResults.Count > 0)
|
||||||
|
{
|
||||||
|
var tmdbId = findResult.MovieResults[0].Id;
|
||||||
|
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
||||||
|
return $"{tmdbId}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SeriesInfo:
|
||||||
|
if (findResult?.TvResults != null && findResult.TvResults.Count > 0)
|
||||||
|
{
|
||||||
|
var tmdbId = findResult.TvResults[0].Id;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 豆瓣的imdb id可能是旧的,需要先从omdb接口获取最新的imdb id
|
||||||
|
/// </summary>
|
||||||
|
protected async Task<string> CheckNewImdbID(string imdb, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(imdb))
|
||||||
|
{
|
||||||
|
return imdb;
|
||||||
|
}
|
||||||
|
|
||||||
var omdbItem = await this._omdbApi.GetByImdbID(imdb, cancellationToken).ConfigureAwait(false);
|
var omdbItem = await this._omdbApi.GetByImdbID(imdb, cancellationToken).ConfigureAwait(false);
|
||||||
if (!string.IsNullOrEmpty(omdbItem?.ImdbID))
|
if (!string.IsNullOrEmpty(omdbItem?.ImdbID))
|
||||||
{
|
{
|
||||||
imdb = omdbItem.ImdbID;
|
imdb = omdbItem.ImdbID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过imdb获取tmdbId
|
return imdb;
|
||||||
var findResult = await this._tmdbApi.FindByExternalIdAsync(imdb, TMDbLib.Objects.Find.FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (findResult?.MovieResults != null && findResult.MovieResults.Count > 0)
|
|
||||||
{
|
|
||||||
var tmdbId = findResult.MovieResults[0].Id;
|
|
||||||
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
|
||||||
return $"{tmdbId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (findResult?.TvResults != null && findResult.TvResults.Count > 0)
|
|
||||||
{
|
|
||||||
var tmdbId = findResult.TvResults[0].Id;
|
|
||||||
this.Log($"Found tmdb [id]: {tmdbId} by imdb id: {imdb}");
|
|
||||||
return $"{tmdbId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public int? GuessSeasonNumberByDirectoryName(string path)
|
public int? GuessSeasonNumberByDirectoryName(string path)
|
||||||
{
|
{
|
||||||
// TODO: 有时series name中会带有季信息
|
// TODO: 有时 series name 中会带有季信息
|
||||||
// 当没有season级目录时,path为空,直接返回
|
// 当没有 season 级目录时,或 season 文件夹特殊不规范命名时,会解析不到 seasonNumber,这时 path 为空,直接返回
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
|
this.Log($"Season path is empty!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,6 +417,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文季名
|
||||||
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
|
var regSeason = new Regex(@"第([0-9零一二三四五六七八九]+?)(季|部)", RegexOptions.Compiled);
|
||||||
var match = regSeason.Match(fileName);
|
var match = regSeason.Match(fileName);
|
||||||
if (match.Success && match.Groups.Count > 1)
|
if (match.Success && match.Groups.Count > 1)
|
||||||
|
@ -360,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>() {
|
var seasonNameMap = new Dictionary<string, int>() {
|
||||||
{@"[ ._](I|1st|S01|S1)[ ._]", 1},
|
{@"[ ._](I|1st)[ ._]", 1},
|
||||||
{@"[ ._](II|2nd|S02|S2)[ ._]", 2},
|
{@"[ ._](II|2nd)[ ._]", 2},
|
||||||
{@"[ ._](III|3rd|S03|S3)[ ._]", 3},
|
{@"[ ._](III|3rd)[ ._]", 3},
|
||||||
{@"[ ._](IIII|4th|S04|S4)[ ._]", 3},
|
{@"[ ._](IIII|4th)[ ._]", 3},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var entry in seasonNameMap)
|
foreach (var entry in seasonNameMap)
|
||||||
|
@ -412,35 +500,42 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 浏览器来源请求,返回代理地址(no-referer对于background-image不生效),其他客户端请求,返回原始图片地址
|
|
||||||
/// </summary>
|
|
||||||
protected string GetProxyImageUrl(string url)
|
protected string GetProxyImageUrl(string url)
|
||||||
{
|
{
|
||||||
var fromWeb = false;
|
var baseUrl = this.GetBaseUrl();
|
||||||
if (_httpContextAccessor.HttpContext != null)
|
var encodedUrl = HttpUtility.UrlEncode(url);
|
||||||
{
|
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
|
||||||
var userAgent = _httpContextAccessor.HttpContext.Request.Headers.UserAgent.ToString();
|
|
||||||
fromWeb = userAgent.Contains("Chrome") || userAgent.Contains("Safari");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromWeb)
|
|
||||||
{
|
|
||||||
var encodedUrl = HttpUtility.UrlEncode(url);
|
|
||||||
return $"/plugin/metashark/proxy/image/?url={encodedUrl}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected string GetLocalProxyImageUrl(string url)
|
||||||
protected string GetAbsoluteProxyImageUrl(string url)
|
|
||||||
{
|
{
|
||||||
|
var baseUrl = Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl))
|
||||||
|
{
|
||||||
|
baseUrl = config.DoubanImageProxyBaseUrl.TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
var encodedUrl = HttpUtility.UrlEncode(url);
|
var encodedUrl = HttpUtility.UrlEncode(url);
|
||||||
return $"{this.RequestDomain}/plugin/metashark/proxy/image/?url={encodedUrl}";
|
return $"{baseUrl}/plugin/metashark/proxy/image/?url={encodedUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetBaseUrl()
|
||||||
|
{
|
||||||
|
// 配置优先
|
||||||
|
if (!string.IsNullOrWhiteSpace(config.DoubanImageProxyBaseUrl))
|
||||||
|
{
|
||||||
|
return this.config.DoubanImageProxyBaseUrl.TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:http请求时,获取请求的host (nginx代理/docker中部署时,没配置透传host时,本方式会有问题)
|
||||||
|
// 除自动扫描之外都会执行这里,修改图片功能图片是直接下载,不走插件图片代理处理函数,host拿不到就下载不了
|
||||||
|
if (Plugin.Instance != null && this._httpContextAccessor.HttpContext != null)
|
||||||
|
{
|
||||||
|
return Plugin.Instance.GetApiBaseUrl(this._httpContextAccessor.HttpContext.Request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动扫描刷新时,直接使用本地地址(127.0.0.1)
|
||||||
|
return Plugin.Instance?.GetLocalApiBaseUrl() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Log(string? message, params object?[] args)
|
protected void Log(string? message, params object?[] args)
|
||||||
|
@ -495,7 +590,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected string GetDoubanPoster(DoubanSubject subject)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(subject.Img)) {
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = config.EnableDoubanLargePoster ? subject.ImgLarge : subject.ImgMiddle;
|
||||||
|
return this.GetProxyImageUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
protected string GetOriginalFileName(ItemLookupInfo info)
|
protected string GetOriginalFileName(ItemLookupInfo info)
|
||||||
{
|
{
|
||||||
|
@ -518,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, "");
|
return regSeasonNameSuffix.Replace(name, "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -18,24 +12,15 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
using static System.Net.Mime.MediaTypeNames;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class EpisodeImageProvider : BaseProvider, IRemoteImageProvider
|
public class EpisodeImageProvider : BaseProvider, IRemoteImageProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
public EpisodeImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="EpisodeImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<EpisodeImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public EpisodeImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<EpisodeImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,14 +88,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("[GetEpisodeImages] GetImageResponse url: {0}", url);
|
|
||||||
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Common.Plugins;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
|
@ -15,11 +12,8 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
@ -28,8 +22,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
public EpisodeProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
public EpisodeProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<EpisodeProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
: base(httpClientFactory, loggerFactory.CreateLogger<EpisodeProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
{
|
{
|
||||||
this._memoryCache = new MemoryCache(new MemoryCacheOptions());
|
this._memoryCache = new MemoryCache(new MemoryCacheOptions());
|
||||||
}
|
}
|
||||||
|
@ -50,9 +44,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 刷新元数据四种模式差别:
|
// 刷新元数据四种模式差别:
|
||||||
// 自动扫描匹配:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,假如命名不规范,就会导致解析出错误值
|
// 自动扫描匹配:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,假如命名不规范,就会导致解析出错误值
|
||||||
// 识别:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds有指定选择项的ProvinceId
|
// 识别:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds有指定选择项的ProvinceId
|
||||||
|
// 覆盖所有元数据:info的Name、IndexNumber和ParentIndexNumber是从文件名解析出来的,provinceIds保留所有旧值
|
||||||
// 搜索缺少的元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值
|
// 搜索缺少的元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值
|
||||||
// 覆盖所有元数据:info的Name、IndexNumber和ParentIndexNumber是从当前的元数据获取,provinceIds保留所有旧值
|
var fileName = Path.GetFileName(info.Path);
|
||||||
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} IsAutomated: {info.IsAutomated}");
|
this.Log($"GetEpisodeMetadata of [name]: {info.Name} [fileName]: {fileName} number: {info.IndexNumber} ParentIndexNumber: {info.ParentIndexNumber} EnableTmdb: {config.EnableTmdb}");
|
||||||
var result = new MetadataResult<Episode>();
|
var result = new MetadataResult<Episode>();
|
||||||
|
|
||||||
// 动画特典和extras处理
|
// 动画特典和extras处理
|
||||||
|
@ -135,78 +130,72 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重新解析文件名
|
/// 重新解析文件名
|
||||||
/// 注意:这里修改替换ParentIndexNumber值后,会重新触发SeasonProvier的GetMetadata方法,并带上最新的季数IndexNumber
|
/// 注意:这里修改替换 ParentIndexNumber 值后,会重新触发 SeasonProvier 的 GetMetadata 方法,并带上最新的季数 IndexNumber
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EpisodeInfo FixParseInfo(EpisodeInfo info)
|
public EpisodeInfo FixParseInfo(EpisodeInfo info)
|
||||||
{
|
{
|
||||||
// 使用AnitomySharp进行重新解析,解决anime识别错误
|
// 使用 AnitomySharp 进行重新解析,解决 anime 识别错误
|
||||||
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
||||||
var parseResult = NameParser.Parse(fileName);
|
var parseResult = NameParser.ParseEpisode(fileName);
|
||||||
info.Year = parseResult.Year;
|
info.Year = parseResult.Year;
|
||||||
info.Name = parseResult.ChineseName ?? parseResult.Name;
|
info.Name = parseResult.ChineseName ?? parseResult.Name;
|
||||||
|
|
||||||
// 没有season级目录(即虚拟季)ParentIndexNumber默认是1,季文件夹命名不规范时,ParentIndexNumber默认是null
|
// 文件名带有季数数据时,从文件名解析出季数进行修正
|
||||||
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;
|
info.ParentIndexNumber = parseResult.ParentIndexNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修正anime命名格式导致的seasonNumber错误(从season元数据读取)
|
// // 修正anime命名格式导致的seasonNumber错误(从season元数据读取)
|
||||||
if (info.ParentIndexNumber is null)
|
// if (info.ParentIndexNumber is null)
|
||||||
|
// {
|
||||||
|
// var episodeItem = this._libraryManager.FindByPath(info.Path, false);
|
||||||
|
// var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
|
||||||
|
// if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
|
||||||
|
// {
|
||||||
|
// info.ParentIndexNumber = season.IndexNumber;
|
||||||
|
// this.Log("FixSeasonNumber by season. old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 从季文件夹名称猜出 season number
|
||||||
|
// 没有 season 级目录或部分特殊不规范命名,会变成虚拟季,ParentIndexNumber 默认设为 1
|
||||||
|
// https://github.com/jellyfin/jellyfin/blob/926470829d91d93b4c0b22c5b8b89a791abbb434/Emby.Server.Implementations/Library/LibraryManager.cs#L2626
|
||||||
|
var isVirtualSeason = this.IsVirtualSeason(info);
|
||||||
|
var seasonFolderPath = this.GetOriginalSeasonPath(info);
|
||||||
|
if (info.ParentIndexNumber is null or 1 && isVirtualSeason && seasonFolderPath != null)
|
||||||
{
|
{
|
||||||
var episodeItem = _libraryManager.FindByPath(info.Path, false);
|
var guestSeasonNumber = this.GuessSeasonNumberByDirectoryName(seasonFolderPath);
|
||||||
var season = episodeItem != null ? ((Episode)episodeItem).Season : null;
|
if (guestSeasonNumber.HasValue && guestSeasonNumber != info.ParentIndexNumber)
|
||||||
if (season != null && season.IndexNumber.HasValue && info.ParentIndexNumber != season.IndexNumber)
|
|
||||||
{
|
{
|
||||||
this.Log("FixSeasonNumber: old: {0} new: {1}", info.ParentIndexNumber, season.IndexNumber);
|
this.Log("FixSeasonNumber by season path. old: {0} new: {1}", info.ParentIndexNumber, guestSeasonNumber);
|
||||||
info.ParentIndexNumber = season.IndexNumber;
|
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)))
|
if (info.ParentIndexNumber is null && NameParser.IsAnime(fileName) && (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path)))
|
||||||
{
|
{
|
||||||
|
this.Log("FixSeasonNumber to special. old: {0} new: 0", info.ParentIndexNumber);
|
||||||
info.ParentIndexNumber = 0;
|
info.ParentIndexNumber = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 设为默认季数为1(问题:当同时存在S01和剧场版季文件夹时,剧场版的影片会因为默认第一季而在S01也显示出来)
|
// 特典优先使用文件名(特典除了前面特别设置,还有 SXX/Season XX 等默认的)
|
||||||
// if (info.ParentIndexNumber is null)
|
|
||||||
// {
|
|
||||||
// this.Log("FixSeasonNumber: season number is null, set to default 1");
|
|
||||||
// info.ParentIndexNumber = 1;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// 特典优先使用文件名(特典除了前面特别设置,还有SXX/Season XX等默认的)
|
|
||||||
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
|
if (info.ParentIndexNumber.HasValue && info.ParentIndexNumber == 0)
|
||||||
{
|
{
|
||||||
info.Name = parseResult.SpecialName == info.Name ? fileName : parseResult.SpecialName;
|
info.Name = parseResult.SpecialName == info.Name ? fileName : parseResult.SpecialName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修正 episode number
|
||||||
// 大于1000,可能错误解析了分辨率
|
if (parseResult.IndexNumber.HasValue && info.IndexNumber != parseResult.IndexNumber)
|
||||||
if (parseResult.IndexNumber.HasValue && parseResult.IndexNumber < 1000)
|
|
||||||
{
|
{
|
||||||
|
this.Log("FixEpisodeNumber by anitomy. old: {0} new: {1}", info.IndexNumber, parseResult.IndexNumber);
|
||||||
info.IndexNumber = parseResult.IndexNumber;
|
info.IndexNumber = parseResult.IndexNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Log("FixParseInfo: fileName: {0} seasonNumber: {1} episodeNumber: {2} name: {3}", fileName, info.ParentIndexNumber, info.IndexNumber, info.Name);
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +204,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
// 特典或extra视频可能和正片剧集放在同一目录
|
// 特典或extra视频可能和正片剧集放在同一目录
|
||||||
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
var fileName = Path.GetFileNameWithoutExtension(info.Path) ?? info.Name;
|
||||||
var parseResult = NameParser.Parse(fileName);
|
var parseResult = NameParser.ParseEpisode(fileName);
|
||||||
if (parseResult.IsExtra)
|
if (parseResult.IsExtra)
|
||||||
{
|
{
|
||||||
this.Log($"Found anime extra of [name]: {fileName}");
|
this.Log($"Found anime extra of [name]: {fileName}");
|
||||||
|
@ -242,7 +231,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
//// 特典也有剧集信息,不在这里处理
|
//// 特典也有 tmdb 剧集信息,不在这里处理
|
||||||
// if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))
|
// if (parseResult.IsSpecial || NameParser.IsSpecialDirectory(info.Path))
|
||||||
// {
|
// {
|
||||||
// this.Log($"Found anime sp of [name]: {fileName}");
|
// this.Log($"Found anime sp of [name]: {fileName}");
|
||||||
|
@ -297,13 +286,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return videoFilesCount;
|
return videoFilesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
return _httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
@ -12,27 +11,18 @@ using MediaBrowser.Model.Extensions;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class MovieImageProvider : BaseProvider, IRemoteImageProvider
|
public class MovieImageProvider : BaseProvider, IRemoteImageProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
public MovieImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="MovieImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<MovieImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public MovieImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<MovieImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,33 +36,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
||||||
{
|
{
|
||||||
ImageType.Primary,
|
ImageType.Primary,
|
||||||
ImageType.Backdrop
|
ImageType.Backdrop,
|
||||||
|
ImageType.Logo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sid = item.GetProviderId(DoubanProviderId);
|
var sid = item.GetProviderId(DoubanProviderId);
|
||||||
var metaSource = item.GetProviderId(Plugin.ProviderId);
|
var metaSource = item.GetMetaSource(Plugin.ProviderId);
|
||||||
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||||
{
|
{
|
||||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
if (primary == null || string.IsNullOrEmpty(primary.Img))
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteImageInfo>();
|
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> {
|
var res = new List<RemoteImageInfo> {
|
||||||
new RemoteImageInfo
|
new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = primary.Name,
|
ProviderName = this.Name,
|
||||||
Url = primary.ImgMiddle,
|
Url = this.GetDoubanPoster(primary),
|
||||||
Type = ImageType.Primary
|
Type = ImageType.Primary,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
res.AddRange(backdropImgs);
|
res.AddRange(backdropImgs);
|
||||||
|
res.AddRange(logoImgs);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
||||||
{
|
{
|
||||||
var language = item.GetPreferredMetadataLanguage();
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
var movie = await _tmdbApi
|
// 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
|
||||||
|
var movie = await this._tmdbApi
|
||||||
.GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
.GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -91,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
var remoteImages = new List<RemoteImageInfo>();
|
var remoteImages = new List<RemoteImageInfo>();
|
||||||
|
|
||||||
for (var i = 0; i < movie.Images.Posters.Count; i++)
|
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
|
||||||
{
|
ProviderName = this.Name,
|
||||||
var poster = movie.Images.Posters[i];
|
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
|
||||||
remoteImages.Add(new RemoteImageInfo
|
|
||||||
{
|
|
||||||
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
|
|
||||||
CommunityRating = poster.VoteAverage,
|
|
||||||
VoteCount = poster.VoteCount,
|
|
||||||
Width = poster.Width,
|
|
||||||
Height = poster.Height,
|
|
||||||
Language = AdjustImageLanguage(poster.Iso_639_1, language),
|
|
||||||
ProviderName = Name,
|
|
||||||
Type = ImageType.Primary,
|
Type = ImageType.Primary,
|
||||||
});
|
CommunityRating = x.VoteAverage,
|
||||||
}
|
VoteCount = x.VoteCount,
|
||||||
|
Width = x.Width,
|
||||||
|
Height = x.Height,
|
||||||
|
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||||
|
RatingType = RatingType.Score,
|
||||||
|
}));
|
||||||
|
|
||||||
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
|
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
|
||||||
{
|
ProviderName = this.Name,
|
||||||
var backdrop = movie.Images.Backdrops[i];
|
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
|
||||||
remoteImages.Add(new RemoteImageInfo
|
|
||||||
{
|
|
||||||
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
|
|
||||||
CommunityRating = backdrop.VoteAverage,
|
|
||||||
VoteCount = backdrop.VoteCount,
|
|
||||||
Width = backdrop.Width,
|
|
||||||
Height = backdrop.Height,
|
|
||||||
ProviderName = Name,
|
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
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);
|
return remoteImages.OrderByLanguageDescending(language);
|
||||||
}
|
}
|
||||||
|
@ -130,25 +128,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return new List<RemoteImageInfo>();
|
return new List<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
|
|
||||||
if (url.Contains("doubanio.com"))
|
|
||||||
{// 豆瓣图,带referer下载
|
|
||||||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
|
||||||
{
|
|
||||||
requestMessage.Headers.Add("Referer", "https://www.douban.com/");
|
|
||||||
return await this._httpClientFactory.CreateClient().SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query for a background photo
|
/// Query for a background photo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -162,7 +141,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 从豆瓣获取背景图
|
// 从豆瓣获取背景图
|
||||||
if (!string.IsNullOrEmpty(sid))
|
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)
|
if (photo != null && photo.Count > 0)
|
||||||
{
|
{
|
||||||
this.Log("GetBackdrop from douban sid: {0}", sid);
|
this.Log("GetBackdrop from douban sid: {0}", sid);
|
||||||
|
@ -170,11 +149,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
if (config.EnableDoubanBackdropRaw)
|
if (config.EnableDoubanBackdropRaw)
|
||||||
{
|
{
|
||||||
var fromBackdropSearch = RequestPath.Contains("/RemoteImages");
|
|
||||||
return new RemoteImageInfo
|
return new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
ProviderName = Name,
|
||||||
Url = fromBackdropSearch ? GetAbsoluteProxyImageUrl(x.Raw) : x.Raw,
|
Url = this.GetProxyImageUrl(x.Raw),
|
||||||
Height = x.Height,
|
Height = x.Height,
|
||||||
Width = x.Width,
|
Width = x.Width,
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
|
@ -185,7 +163,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return new RemoteImageInfo
|
return new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
ProviderName = Name,
|
||||||
Url = x.Large,
|
Url = this.GetProxyImageUrl(x.Large),
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -217,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
|
||||||
|
var list = new List<RemoteImageInfo>();
|
||||||
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
|
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
|
||||||
|
{
|
||||||
|
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
|
||||||
|
var movie = await this._tmdbApi
|
||||||
|
.GetMovieAsync(tmdbId.ToInt(), language, language, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (movie != null && movie.Images != null)
|
||||||
|
{
|
||||||
|
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||||
|
ProviderName = this.Name,
|
||||||
|
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||||
|
Type = ImageType.Logo,
|
||||||
|
CommunityRating = x.VoteAverage,
|
||||||
|
VoteCount = x.VoteCount,
|
||||||
|
Width = x.Width,
|
||||||
|
Height = x.Height,
|
||||||
|
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||||
|
RatingType = RatingType.Score,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.OrderByLanguageDescending(language);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,10 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
|
||||||
using AngleSharp.Text;
|
using AngleSharp.Text;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
|
@ -22,19 +19,13 @@ using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using StringMetric;
|
|
||||||
using TMDbLib.Client;
|
|
||||||
using TMDbLib.Objects.Find;
|
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
using TMDbLib.Objects.TvShows;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieInfo>
|
public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieInfo>
|
||||||
{
|
{
|
||||||
public MovieProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
public MovieProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<MovieProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
: base(httpClientFactory, loggerFactory.CreateLogger<MovieProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,13 +43,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从douban搜索
|
// 从douban搜索
|
||||||
// BUG注意:ProviderIds传多个meta值,会导致识别搜索时只返回一个结果
|
var res = await this._doubanApi.SearchMovieAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
||||||
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
|
|
||||||
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
|
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
|
||||||
{
|
{
|
||||||
return new RemoteSearchResult
|
return new RemoteSearchResult
|
||||||
{
|
{
|
||||||
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),
|
ImageUrl = this.GetProxyImageUrl(x.Img),
|
||||||
ProductionYear = x.Year,
|
ProductionYear = x.Year,
|
||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
|
@ -74,7 +66,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
return new RemoteSearchResult
|
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),
|
Name = string.Format("[TMDB]{0}", x.Title ?? x.OriginalTitle),
|
||||||
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
||||||
Overview = x.Overview,
|
Overview = x.Overview,
|
||||||
|
@ -89,18 +83,14 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
|
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
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>();
|
var result = new MetadataResult<Movie>();
|
||||||
|
|
||||||
// 使用刷新元数据时,providerIds会保留旧有值,只有识别/新增才会没值
|
// 使用刷新元数据时,providerIds会保留旧有值,只有识别/新增才会没值
|
||||||
var sid = info.GetProviderId(DoubanProviderId);
|
var sid = info.GetProviderId(DoubanProviderId);
|
||||||
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||||
var metaSource = info.GetProviderId(Plugin.ProviderId);
|
var metaSource = info.GetMetaSource(Plugin.ProviderId);
|
||||||
// 用于修正识别时指定tmdb,没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
|
|
||||||
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
|
|
||||||
{
|
|
||||||
metaSource = MetaSource.Tmdb;
|
|
||||||
}
|
|
||||||
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
||||||
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
||||||
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
||||||
|
@ -129,7 +119,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
var movie = new Movie
|
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,
|
Name = subject.Name,
|
||||||
OriginalTitle = subject.OriginalName,
|
OriginalTitle = subject.OriginalName,
|
||||||
CommunityRating = subject.Rating,
|
CommunityRating = subject.Rating,
|
||||||
|
@ -137,16 +128,16 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
ProductionYear = subject.Year,
|
ProductionYear = subject.Year,
|
||||||
HomePageUrl = "https://www.douban.com",
|
HomePageUrl = "https://www.douban.com",
|
||||||
Genres = subject.Genres,
|
Genres = subject.Genres,
|
||||||
// ProductionLocations = [x?.Country],
|
|
||||||
PremiereDate = subject.ScreenTime,
|
PremiereDate = subject.ScreenTime,
|
||||||
Tagline = string.Empty,
|
|
||||||
};
|
};
|
||||||
if (!string.IsNullOrEmpty(subject.Imdb))
|
if (!string.IsNullOrEmpty(subject.Imdb))
|
||||||
{
|
{
|
||||||
movie.SetProviderId(MetadataProvider.Imdb, subject.Imdb);
|
var newImdbId = await this.CheckNewImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false);
|
||||||
|
subject.Imdb = newImdbId;
|
||||||
|
movie.SetProviderId(MetadataProvider.Imdb, newImdbId);
|
||||||
|
|
||||||
// 通过imdb获取TMDB id
|
// 通过imdb获取TMDB id
|
||||||
var newTmdbId = await this.GetTmdbIdByImdbAsync(subject.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
var newTmdbId = await this.GetTmdbIdByImdbAsync(subject.Imdb, info.MetadataLanguage, info, cancellationToken).ConfigureAwait(false);
|
||||||
if (!string.IsNullOrEmpty(newTmdbId))
|
if (!string.IsNullOrEmpty(newTmdbId))
|
||||||
{
|
{
|
||||||
tmdbId = newTmdbId;
|
tmdbId = newTmdbId;
|
||||||
|
@ -189,12 +180,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
result.Item = movie;
|
result.Item = movie;
|
||||||
result.QueriedById = true;
|
result.QueriedById = true;
|
||||||
result.HasMetadata = 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,
|
Name = c.Name,
|
||||||
Type = c.RoleType,
|
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||||
Role = c.Role,
|
Role = c.Role,
|
||||||
ImageUrl = c.Img,
|
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -204,67 +195,75 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
||||||
{
|
{
|
||||||
this.Log($"GetMovieMetadata of tmdb [id]: \"{tmdbId}\"");
|
return await this.GetMetadataByTmdb(tmdbId, info, cancellationToken).ConfigureAwait(false);
|
||||||
var movieResult = await _tmdbApi
|
}
|
||||||
|
|
||||||
|
this.Log($"匹配失败!可检查下年份是否与豆瓣一致,是否需要登录访问. [name]: {info.Name} [year]: {info.Year}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<MetadataResult<Movie>> GetMetadataByTmdb(string tmdbId, MovieInfo info, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
this.Log($"GetMovieMetadata of tmdb [id]: \"{tmdbId}\"");
|
||||||
|
var result = new MetadataResult<Movie>();
|
||||||
|
var movieResult = await _tmdbApi
|
||||||
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
|
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (movieResult == null)
|
if (movieResult == null)
|
||||||
{
|
{
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var movie = new Movie
|
|
||||||
{
|
|
||||||
Name = movieResult.Title ?? movieResult.OriginalTitle,
|
|
||||||
OriginalTitle = movieResult.OriginalTitle,
|
|
||||||
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
|
|
||||||
Tagline = movieResult.Tagline,
|
|
||||||
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
|
|
||||||
};
|
|
||||||
result = new MetadataResult<Movie>
|
|
||||||
{
|
|
||||||
QueriedById = true,
|
|
||||||
HasMetadata = true,
|
|
||||||
ResultLanguage = info.MetadataLanguage,
|
|
||||||
Item = movie
|
|
||||||
};
|
|
||||||
|
|
||||||
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
|
||||||
movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
|
|
||||||
|
|
||||||
// 获取电影系列信息
|
|
||||||
if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null)
|
|
||||||
{
|
|
||||||
movie.CollectionName = movieResult.BelongsToCollection.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
movie.CommunityRating = (float)System.Math.Round(movieResult.VoteAverage, 2);
|
|
||||||
movie.OfficialRating = this.GetTmdbOfficialRatingByData(movieResult, info.MetadataCountryCode);
|
|
||||||
movie.PremiereDate = movieResult.ReleaseDate;
|
|
||||||
movie.ProductionYear = movieResult.ReleaseDate?.Year;
|
|
||||||
|
|
||||||
if (movieResult.ProductionCompanies != null)
|
|
||||||
{
|
|
||||||
movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
var genres = movieResult.Genres;
|
|
||||||
|
|
||||||
foreach (var genre in genres.Select(g => g.Name))
|
|
||||||
{
|
|
||||||
movie.AddGenre(genre);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var person in GetPersons(movieResult))
|
|
||||||
{
|
|
||||||
result.AddPerson(person);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var movie = new Movie
|
||||||
|
{
|
||||||
|
Name = movieResult.Title ?? movieResult.OriginalTitle,
|
||||||
|
OriginalTitle = movieResult.OriginalTitle,
|
||||||
|
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
|
||||||
|
Tagline = movieResult.Tagline,
|
||||||
|
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
|
||||||
|
};
|
||||||
|
result = new MetadataResult<Movie>
|
||||||
|
{
|
||||||
|
QueriedById = true,
|
||||||
|
HasMetadata = true,
|
||||||
|
ResultLanguage = info.MetadataLanguage,
|
||||||
|
Item = movie
|
||||||
|
};
|
||||||
|
|
||||||
|
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||||
|
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||||
|
// 这里 Plugin.ProviderId 的值做这么复杂,是为了保持唯一
|
||||||
|
movie.SetProviderId(Plugin.ProviderId, $"{MetaSource.Tmdb}_{tmdbId}");
|
||||||
|
|
||||||
|
// 获取电影系列信息
|
||||||
|
if (this.config.EnableTmdbCollection && movieResult.BelongsToCollection != null)
|
||||||
|
{
|
||||||
|
movie.CollectionName = movieResult.BelongsToCollection.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
movie.CommunityRating = (float)System.Math.Round(movieResult.VoteAverage, 2);
|
||||||
|
movie.OfficialRating = this.GetTmdbOfficialRatingByData(movieResult, info.MetadataCountryCode);
|
||||||
|
movie.PremiereDate = movieResult.ReleaseDate;
|
||||||
|
movie.ProductionYear = movieResult.ReleaseDate?.Year;
|
||||||
|
|
||||||
|
if (movieResult.ProductionCompanies != null)
|
||||||
|
{
|
||||||
|
movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
var genres = movieResult.Genres;
|
||||||
|
|
||||||
|
foreach (var genre in genres.Select(g => g.Name))
|
||||||
|
{
|
||||||
|
movie.AddGenre(genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var person in GetPersons(movieResult))
|
||||||
|
{
|
||||||
|
result.AddPerson(person);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,13 +296,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 演员
|
// 演员
|
||||||
if (item.Credits?.Cast != null)
|
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
|
var personInfo = new PersonInfo
|
||||||
{
|
{
|
||||||
Name = actor.Name.Trim(),
|
Name = actor.Name.Trim(),
|
||||||
Role = actor.Character,
|
Role = actor.Character,
|
||||||
Type = PersonType.Actor,
|
Type = PersonKind.Actor,
|
||||||
SortOrder = actor.Order,
|
SortOrder = actor.Order,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -346,7 +345,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
Name = person.Name.Trim(),
|
Name = person.Name.Trim(),
|
||||||
Role = person.Job,
|
Role = person.Job,
|
||||||
Type = type
|
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
||||||
|
@ -423,14 +422,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,22 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Extensions;
|
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class PersonImageProvider : BaseProvider, IRemoteImageProvider
|
public class PersonImageProvider : BaseProvider, IRemoteImageProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
public PersonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="MovieImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<PersonImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public PersonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<PersonImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,61 +35,48 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var list = new List<RemoteImageInfo>();
|
||||||
var cid = item.GetProviderId(DoubanProviderId);
|
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}");
|
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||||
if (!string.IsNullOrEmpty(cid))
|
if (!string.IsNullOrEmpty(cid))
|
||||||
{
|
{
|
||||||
var celebrity = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
|
var celebrity = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
|
||||||
if (celebrity != null)
|
if (celebrity != null)
|
||||||
{
|
{
|
||||||
return new List<RemoteImageInfo> {
|
list.Add(new RemoteImageInfo
|
||||||
new RemoteImageInfo
|
{
|
||||||
{
|
ProviderName = this.Name,
|
||||||
ProviderName = celebrity.Name,
|
Url = this.GetProxyImageUrl(celebrity.Img),
|
||||||
Url = celebrity.Img,
|
Type = ImageType.Primary,
|
||||||
Type = ImageType.Primary
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
|
var photos = await this._doubanApi.GetCelebrityPhotosAsync(cid, cancellationToken).ConfigureAwait(false);
|
||||||
return new List<RemoteImageInfo>();
|
photos.ForEach(x =>
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Query for a background photo
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sid">a subject/movie id</param>
|
|
||||||
/// <param name="cancellationToken">Instance of the <see cref="CancellationToken"/> interface.</param>
|
|
||||||
private async Task<IEnumerable<RemoteImageInfo>> GetBackdrop(string sid, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetBackdrop of sid: {0}", sid);
|
|
||||||
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
|
|
||||||
var list = new List<RemoteImageInfo>();
|
|
||||||
|
|
||||||
if (photo == null)
|
|
||||||
{
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
return photo.Where(x => x.Width > x.Height * 1.3).Select(x =>
|
|
||||||
{
|
|
||||||
return new RemoteImageInfo
|
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
// 过滤不是竖图
|
||||||
Url = x.Large,
|
if (x.Width < 400 || x.Height < x.Width * 1.3)
|
||||||
Type = ImageType.Backdrop,
|
{
|
||||||
};
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
list.Add(new RemoteImageInfo
|
||||||
|
{
|
||||||
|
ProviderName = this.Name,
|
||||||
|
Url = this.GetProxyImageUrl(x.Raw),
|
||||||
|
Width = x.Width,
|
||||||
|
Height = x.Height,
|
||||||
|
Type = ImageType.Primary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
|
||||||
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,20 @@
|
||||||
using System.Net.Mime;
|
using System;
|
||||||
using System.Xml.Schema;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using TMDbLib.Objects.Find;
|
using TMDbLib.Objects.Find;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
|
@ -27,14 +23,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PersonProvider : BaseProvider, IRemoteMetadataProvider<Person, PersonLookupInfo>
|
public class PersonProvider : BaseProvider, IRemoteMetadataProvider<Person, PersonLookupInfo>
|
||||||
{
|
{
|
||||||
/// <summary>
|
public PersonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="MovieImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<PersonProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public PersonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<PersonProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,10 +89,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
var item = new Person
|
var item = new Person
|
||||||
{
|
{
|
||||||
// Name = c.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
|
// Name = c.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
|
||||||
|
OriginalTitle = c.DisplayOriginalName, // 外国人显示英文名
|
||||||
HomePageUrl = c.Site,
|
HomePageUrl = c.Site,
|
||||||
Overview = c.Intro,
|
Overview = c.Intro,
|
||||||
PremiereDate = DateTime.ParseExact(c.Birthdate, "yyyy年MM月dd日", System.Globalization.CultureInfo.CurrentCulture)
|
|
||||||
};
|
};
|
||||||
|
if (DateTime.TryParseExact(c.Birthdate, "yyyy年MM月dd日", null, DateTimeStyles.None, out var premiereDate))
|
||||||
|
{
|
||||||
|
item.PremiereDate = premiereDate;
|
||||||
|
item.ProductionYear = premiereDate.Year;
|
||||||
|
}
|
||||||
|
if (DateTime.TryParseExact(c.Enddate, "yyyy年MM月dd日", null, DateTimeStyles.None, out var endDate))
|
||||||
|
{
|
||||||
|
item.EndDate = endDate;
|
||||||
|
}
|
||||||
if (!string.IsNullOrWhiteSpace(c.Birthplace))
|
if (!string.IsNullOrWhiteSpace(c.Birthplace))
|
||||||
{
|
{
|
||||||
item.ProductionLocations = new[] { c.Birthplace };
|
item.ProductionLocations = new[] { c.Birthplace };
|
||||||
|
@ -111,13 +110,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
item.SetProviderId(DoubanProviderId, cid);
|
item.SetProviderId(DoubanProviderId, cid);
|
||||||
if (!string.IsNullOrEmpty(c.Imdb))
|
if (!string.IsNullOrEmpty(c.Imdb))
|
||||||
{
|
{
|
||||||
|
var newImdbId = await this._imdbApi.CheckPersonNewIDAsync(c.Imdb, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrEmpty(newImdbId))
|
||||||
|
{
|
||||||
|
c.Imdb = newImdbId;
|
||||||
|
}
|
||||||
item.SetProviderId(MetadataProvider.Imdb, c.Imdb);
|
item.SetProviderId(MetadataProvider.Imdb, c.Imdb);
|
||||||
// 通过imdb获取TMDB id
|
// 通过imdb获取TMDB id
|
||||||
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||||
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
|
if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
|
||||||
{
|
{
|
||||||
this.Log($"GetPersonMetadata of found tmdb [id]: {findResult.PersonResults[0].Id}");
|
var foundTmdbId = findResult.PersonResults.First().Id.ToString();
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.PersonResults[0].Id}");
|
this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}");
|
||||||
|
item.SetProviderId(MetadataProvider.Tmdb, $"{foundTmdbId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,48 +134,51 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jellyfin强制最后一定使用默认的TheMovieDb插件获取一次,这里不太必要(除了使用自己的域名)
|
||||||
var personTmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
var personTmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||||
this.Log($"GetPersonMetadata of [personTmdbId]: {personTmdbId}");
|
this.Log($"GetPersonMetadata of [personTmdbId]: {personTmdbId}");
|
||||||
if (!string.IsNullOrEmpty(personTmdbId))
|
if (!string.IsNullOrEmpty(personTmdbId))
|
||||||
{
|
{
|
||||||
var person = await this._tmdbApi.GetPersonAsync(personTmdbId.ToInt(), cancellationToken).ConfigureAwait(false);
|
return await this.GetMetadataByTmdb(personTmdbId.ToInt(), info, cancellationToken).ConfigureAwait(false);
|
||||||
if (person != null)
|
|
||||||
{
|
|
||||||
var item = new Person
|
|
||||||
{
|
|
||||||
// Name = info.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
|
|
||||||
HomePageUrl = person.Homepage,
|
|
||||||
Overview = person.Biography,
|
|
||||||
PremiereDate = person.Birthday?.ToUniversalTime(),
|
|
||||||
EndDate = person.Deathday?.ToUniversalTime()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
|
|
||||||
{
|
|
||||||
item.ProductionLocations = new[] { person.PlaceOfBirth };
|
|
||||||
}
|
|
||||||
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
|
|
||||||
if (!string.IsNullOrEmpty(person.ImdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.HasMetadata = true;
|
|
||||||
result.Item = item;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
public async Task<MetadataResult<Person>> GetMetadataByTmdb(int personTmdbId, PersonLookupInfo info, CancellationToken cancellationToken)
|
||||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
this.Log("Person GetImageResponse url: {0}", url);
|
var result = new MetadataResult<Person>();
|
||||||
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
|
var person = await this._tmdbApi.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (person != null)
|
||||||
|
{
|
||||||
|
var item = new Person
|
||||||
|
{
|
||||||
|
// Name = info.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
|
||||||
|
HomePageUrl = person.Homepage,
|
||||||
|
Overview = person.Biography,
|
||||||
|
PremiereDate = person.Birthday?.ToUniversalTime(),
|
||||||
|
EndDate = person.Deathday?.ToUniversalTime()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
|
||||||
|
{
|
||||||
|
item.ProductionLocations = new[] { person.PlaceOfBirth };
|
||||||
|
}
|
||||||
|
|
||||||
|
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
if (!string.IsNullOrEmpty(person.ImdbId))
|
||||||
|
{
|
||||||
|
item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.HasMetadata = true;
|
||||||
|
result.Item = item;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Extensions;
|
using MediaBrowser.Model.Extensions;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
|
@ -17,23 +14,15 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class SeasonImageProvider : BaseProvider, IRemoteImageProvider
|
public class SeasonImageProvider : BaseProvider, IRemoteImageProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
public SeasonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="SeasonImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<SeasonImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public SeasonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<SeasonImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +44,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
|
this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
|
||||||
var season = (Season)item;
|
var season = (Season)item;
|
||||||
var series = season.Series;
|
var series = season.Series;
|
||||||
var metaSource = series.GetProviderId(Plugin.ProviderId);
|
var metaSource = series.GetMetaSource(Plugin.ProviderId);
|
||||||
|
|
||||||
// get image from douban
|
// get image from douban
|
||||||
var sid = item.GetProviderId(DoubanProviderId);
|
var sid = item.GetProviderId(DoubanProviderId);
|
||||||
|
@ -71,9 +60,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
new RemoteImageInfo
|
new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = primary.Name,
|
ProviderName = primary.Name,
|
||||||
Url = primary.ImgMiddle,
|
Url = this.GetDoubanPoster(primary),
|
||||||
Type = ImageType.Primary
|
Type = ImageType.Primary,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -116,12 +105,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return remoteImages.OrderByLanguageDescending(language);
|
return remoteImages.OrderByLanguageDescending(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("[GetSeasonImages] GetImageResponse url: {0}", url);
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,27 +8,22 @@ using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Find;
|
|
||||||
using TMDbLib.Objects.TvShows;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class SeasonProvider : BaseProvider, IRemoteMetadataProvider<Season, SeasonInfo>
|
public class SeasonProvider : BaseProvider, IRemoteMetadataProvider<Season, SeasonInfo>
|
||||||
{
|
{
|
||||||
|
|
||||||
public SeasonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
public SeasonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<SeasonProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
: base(httpClientFactory, loggerFactory.CreateLogger<SeasonProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,23 +42,27 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
var result = new MetadataResult<Season>();
|
var result = new MetadataResult<Season>();
|
||||||
|
|
||||||
|
// 使用刷新元数据时,之前识别的 seasonNumber 会保留,不会被覆盖
|
||||||
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
|
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
|
||||||
info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
|
info.SeriesProviderIds.TryGetMetaSource(Plugin.ProviderId, out var metaSource);
|
||||||
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
|
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
|
||||||
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
|
var seasonNumber = info.IndexNumber; // S00/Season 00特典目录会为0
|
||||||
var seasonSid = info.GetProviderId(DoubanProviderId);
|
var seasonSid = info.GetProviderId(DoubanProviderId);
|
||||||
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))
|
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||||
{
|
{
|
||||||
// 季文件夹名称不规范,没法拿到seasonNumber,尝试从文件夹名猜出
|
// seasonNumber 为 null 有三种情况:
|
||||||
// 注意:本办法没法处理没有季文件夹的/虚拟季,因为path会为空
|
// 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)
|
if (seasonNumber is null)
|
||||||
{
|
{
|
||||||
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
|
seasonNumber = this.GuessSeasonNumberByDirectoryName(info.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索豆瓣季id
|
// 搜索豆瓣季 id
|
||||||
if (string.IsNullOrEmpty(seasonSid))
|
if (string.IsNullOrEmpty(seasonSid))
|
||||||
{
|
{
|
||||||
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
|
seasonSid = await this.GuessDoubanSeasonId(sid, seriesTmdbId, seasonNumber, info, cancellationToken).ConfigureAwait(false);
|
||||||
|
@ -91,21 +90,22 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
result.Item = movie;
|
result.Item = movie;
|
||||||
result.HasMetadata = 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,
|
Name = c.Name,
|
||||||
Type = c.RoleType,
|
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||||
Role = c.Role,
|
Role = c.Role,
|
||||||
ImageUrl = c.Img,
|
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.Log($"Season [{info.Name}] found douban [sid]: {seasonSid}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.Log($"GetSeasonMetaData of [name]: {info.Name} not found douban season id!");
|
this.Log($"Season [{info.Name}] not found douban season id!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,10 +143,19 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有季文件夹(即虚拟季),info.Path会为空,直接用series的sid
|
// 没有季文件夹或季文件夹名不规范时(即虚拟季),info.Path 会为空,seasonNumber 为 null
|
||||||
if (string.IsNullOrEmpty(info.Path))
|
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季,非标准名称默认文件名
|
// 从sereis获取正确名称,info.Name当是标准格式如S01等时,会变成第x季,非标准名称默认文件名
|
||||||
|
@ -155,7 +164,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var seriesName = RemoveSeasonSubfix(series.Name);
|
var seriesName = this.RemoveSeasonSuffix(series.Name);
|
||||||
|
|
||||||
// 没有季id,但存在tmdbid,尝试从tmdb获取对应季的年份信息,用于从豆瓣搜索对应季数据
|
// 没有季id,但存在tmdbid,尝试从tmdb获取对应季的年份信息,用于从豆瓣搜索对应季数据
|
||||||
var seasonYear = 0;
|
var seasonYear = 0;
|
||||||
|
@ -227,17 +236,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Log(string? message, params object?[] args)
|
|
||||||
{
|
|
||||||
this._logger.LogInformation($"[MetaShark] {message}", args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Core;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
|
@ -12,27 +11,18 @@ using MediaBrowser.Model.Extensions;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Languages;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark.Providers
|
namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class SeriesImageProvider : BaseProvider, IRemoteImageProvider
|
public class SeriesImageProvider : BaseProvider, IRemoteImageProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
public SeriesImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
/// Initializes a new instance of the <see cref="SeriesImageProvider"/> class.
|
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
|
|
||||||
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
|
|
||||||
public SeriesImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesImageProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,33 +36,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
|
||||||
{
|
{
|
||||||
ImageType.Primary,
|
ImageType.Primary,
|
||||||
ImageType.Backdrop
|
ImageType.Backdrop,
|
||||||
|
ImageType.Logo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sid = item.GetProviderId(DoubanProviderId);
|
var sid = item.GetProviderId(DoubanProviderId);
|
||||||
var metaSource = item.GetProviderId(Plugin.ProviderId);
|
var metaSource = item.GetMetaSource(Plugin.ProviderId);
|
||||||
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
|
||||||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||||
{
|
{
|
||||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
if (primary == null || string.IsNullOrEmpty(primary.Img))
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<RemoteImageInfo>();
|
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> {
|
var res = new List<RemoteImageInfo> {
|
||||||
new RemoteImageInfo
|
new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = primary.Name,
|
ProviderName = this.Name,
|
||||||
Url = primary.ImgMiddle,
|
Url = this.GetDoubanPoster(primary),
|
||||||
Type = ImageType.Primary
|
Type = ImageType.Primary,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
res.AddRange(dropback);
|
res.AddRange(backdropImgs);
|
||||||
|
res.AddRange(logoImgs);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +73,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
|
||||||
{
|
{
|
||||||
var language = item.GetPreferredMetadataLanguage();
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
var movie = await _tmdbApi
|
// 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
|
||||||
|
var movie = await this._tmdbApi
|
||||||
.GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
.GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
@ -91,37 +85,41 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
var remoteImages = new List<RemoteImageInfo>();
|
var remoteImages = new List<RemoteImageInfo>();
|
||||||
|
|
||||||
for (var i = 0; i < movie.Images.Posters.Count; i++)
|
remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
|
||||||
{
|
ProviderName = this.Name,
|
||||||
var poster = movie.Images.Posters[i];
|
Url = this._tmdbApi.GetPosterUrl(x.FilePath),
|
||||||
remoteImages.Add(new RemoteImageInfo
|
|
||||||
{
|
|
||||||
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
|
|
||||||
CommunityRating = poster.VoteAverage,
|
|
||||||
VoteCount = poster.VoteCount,
|
|
||||||
Width = poster.Width,
|
|
||||||
Height = poster.Height,
|
|
||||||
Language = AdjustImageLanguage(poster.Iso_639_1, language),
|
|
||||||
ProviderName = Name,
|
|
||||||
Type = ImageType.Primary,
|
Type = ImageType.Primary,
|
||||||
});
|
CommunityRating = x.VoteAverage,
|
||||||
}
|
VoteCount = x.VoteCount,
|
||||||
|
Width = x.Width,
|
||||||
|
Height = x.Height,
|
||||||
|
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||||
|
RatingType = RatingType.Score,
|
||||||
|
}));
|
||||||
|
|
||||||
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
|
remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
|
||||||
{
|
ProviderName = this.Name,
|
||||||
var backdrop = movie.Images.Backdrops[i];
|
Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
|
||||||
remoteImages.Add(new RemoteImageInfo
|
|
||||||
{
|
|
||||||
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
|
|
||||||
CommunityRating = backdrop.VoteAverage,
|
|
||||||
VoteCount = backdrop.VoteCount,
|
|
||||||
Width = backdrop.Width,
|
|
||||||
Height = backdrop.Height,
|
|
||||||
ProviderName = Name,
|
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
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);
|
return remoteImages.OrderByLanguageDescending(language);
|
||||||
}
|
}
|
||||||
|
@ -130,25 +128,6 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return new List<RemoteImageInfo>();
|
return new List<RemoteImageInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
|
|
||||||
if (url.Contains("doubanio.com"))
|
|
||||||
{// 豆瓣图,带referer下载
|
|
||||||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
|
||||||
{
|
|
||||||
requestMessage.Headers.Add("Referer", "https://www.douban.com/");
|
|
||||||
return await this._httpClientFactory.CreateClient().SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query for a background photo
|
/// Query for a background photo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -170,11 +149,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
if (config.EnableDoubanBackdropRaw)
|
if (config.EnableDoubanBackdropRaw)
|
||||||
{
|
{
|
||||||
var fromBackdropSearch = RequestPath.Contains("/RemoteImages");
|
|
||||||
return new RemoteImageInfo
|
return new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
ProviderName = Name,
|
||||||
Url = fromBackdropSearch ? GetAbsoluteProxyImageUrl(x.Raw) : x.Raw,
|
Url = this.GetProxyImageUrl(x.Raw),
|
||||||
Height = x.Height,
|
Height = x.Height,
|
||||||
Width = x.Width,
|
Width = x.Width,
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
|
@ -185,7 +163,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return new RemoteImageInfo
|
return new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
ProviderName = Name,
|
||||||
Url = x.Large,
|
Url = this.GetProxyImageUrl(x.Large),
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -207,8 +185,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
this.Log("GetBackdrop from tmdb id: {0}", tmdbId);
|
this.Log("GetBackdrop from tmdb id: {0}", tmdbId);
|
||||||
list.Add(new RemoteImageInfo
|
list.Add(new RemoteImageInfo
|
||||||
{
|
{
|
||||||
ProviderName = Name,
|
ProviderName = this.Name,
|
||||||
Url = _tmdbApi.GetBackdropUrl(movie.BackdropPath),
|
Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath),
|
||||||
Type = ImageType.Backdrop,
|
Type = ImageType.Backdrop,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -217,5 +195,36 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<RemoteImageInfo>> GetLogos(BaseItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
|
||||||
|
var language = item.GetPreferredMetadataLanguage();
|
||||||
|
var list = new List<RemoteImageInfo>();
|
||||||
|
if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
|
||||||
|
{
|
||||||
|
this.Log("GetLogos from tmdb id: {0} lang: {1}", tmdbId, language);
|
||||||
|
var movie = await this._tmdbApi
|
||||||
|
.GetSeriesAsync(tmdbId.ToInt(), language, language, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (movie != null && movie.Images != null)
|
||||||
|
{
|
||||||
|
list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
|
||||||
|
ProviderName = this.Name,
|
||||||
|
Url = this._tmdbApi.GetLogoUrl(x.FilePath),
|
||||||
|
Type = ImageType.Logo,
|
||||||
|
CommunityRating = x.VoteAverage,
|
||||||
|
VoteCount = x.VoteCount,
|
||||||
|
Width = x.Width,
|
||||||
|
Height = x.Height,
|
||||||
|
Language = this.AdjustImageLanguage(x.Iso_639_1, language),
|
||||||
|
RatingType = RatingType.Score,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.OrderByLanguageDescending(language);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using Jellyfin.Plugin.MetaShark.Api;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Plugin.MetaShark.Core;
|
using Jellyfin.Plugin.MetaShark.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Model;
|
using Jellyfin.Plugin.MetaShark.Model;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
@ -14,10 +14,8 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TMDbLib.Objects.Find;
|
|
||||||
using TMDbLib.Objects.TvShows;
|
using TMDbLib.Objects.TvShows;
|
||||||
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
|
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
|
||||||
|
|
||||||
|
@ -25,8 +23,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
public class SeriesProvider : BaseProvider, IRemoteMetadataProvider<Series, SeriesInfo>
|
public class SeriesProvider : BaseProvider, IRemoteMetadataProvider<Series, SeriesInfo>
|
||||||
{
|
{
|
||||||
public SeriesProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
|
public SeriesProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
|
||||||
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi)
|
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,12 +42,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从douban搜索
|
// 从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 =>
|
result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
|
||||||
{
|
{
|
||||||
return new RemoteSearchResult
|
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),
|
ImageUrl = this.GetProxyImageUrl(x.Img),
|
||||||
ProductionYear = x.Year,
|
ProductionYear = x.Year,
|
||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
|
@ -64,7 +63,8 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
return new RemoteSearchResult
|
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),
|
Name = string.Format("[TMDB]{0}", x.Name ?? x.OriginalName),
|
||||||
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
|
||||||
Overview = x.Overview,
|
Overview = x.Overview,
|
||||||
|
@ -79,17 +79,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
|
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
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 result = new MetadataResult<Series>();
|
||||||
|
|
||||||
var sid = info.GetProviderId(DoubanProviderId);
|
var sid = info.GetProviderId(DoubanProviderId);
|
||||||
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||||
var metaSource = info.GetProviderId(Plugin.ProviderId);
|
var metaSource = info.GetMetaSource(Plugin.ProviderId);
|
||||||
// 用于修正识别时指定tmdb,没法读取tmdb数据的BUG。。。两个合在一起太难了。。。
|
|
||||||
if (string.IsNullOrEmpty(metaSource) && info.Name.StartsWith("[TMDB]"))
|
|
||||||
{
|
|
||||||
metaSource = MetaSource.Tmdb;
|
|
||||||
}
|
|
||||||
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
// 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致)
|
||||||
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId);
|
||||||
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid);
|
||||||
|
@ -109,18 +105,17 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
}
|
}
|
||||||
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
|
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var seriesName = RemoveSeasonSubfix(subject.Name);
|
var seriesName = RemoveSeasonSuffix(subject.Name);
|
||||||
var item = new Series
|
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,
|
Name = seriesName,
|
||||||
OriginalTitle = RemoveSeasonSubfix(subject.OriginalName),
|
OriginalTitle = RemoveSeasonSuffix(subject.OriginalName),
|
||||||
CommunityRating = subject.Rating,
|
CommunityRating = subject.Rating,
|
||||||
Overview = subject.Intro,
|
Overview = subject.Intro,
|
||||||
ProductionYear = subject.Year,
|
ProductionYear = subject.Year,
|
||||||
HomePageUrl = "https://www.douban.com",
|
HomePageUrl = "https://www.douban.com",
|
||||||
Genres = subject.Genres,
|
Genres = subject.Genres,
|
||||||
// ProductionLocations = [x?.Country],
|
|
||||||
PremiereDate = subject.ScreenTime,
|
PremiereDate = subject.ScreenTime,
|
||||||
Tagline = string.Empty,
|
Tagline = string.Empty,
|
||||||
};
|
};
|
||||||
|
@ -128,7 +123,9 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 设置imdb元数据
|
// 设置imdb元数据
|
||||||
if (!string.IsNullOrEmpty(subject.Imdb))
|
if (!string.IsNullOrEmpty(subject.Imdb))
|
||||||
{
|
{
|
||||||
item.SetProviderId(MetadataProvider.Imdb, subject.Imdb);
|
var newImdbId = await this.CheckNewImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false);
|
||||||
|
subject.Imdb = newImdbId;
|
||||||
|
item.SetProviderId(MetadataProvider.Imdb, newImdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索匹配tmdbId
|
// 搜索匹配tmdbId
|
||||||
|
@ -153,12 +150,12 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
result.Item = item;
|
result.Item = item;
|
||||||
result.QueriedById = true;
|
result.QueriedById = true;
|
||||||
result.HasMetadata = 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,
|
Name = c.Name,
|
||||||
Type = c.RoleType,
|
Type = c.RoleType == PersonType.Director ? PersonKind.Director : PersonKind.Actor,
|
||||||
Role = c.Role,
|
Role = c.Role,
|
||||||
ImageUrl = c.Img,
|
ImageUrl = this.GetLocalProxyImageUrl(c.Img),
|
||||||
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -170,6 +167,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
return await this.GetMetadataByTmdb(tmdbId, info, cancellationToken).ConfigureAwait(false);
|
return await this.GetMetadataByTmdb(tmdbId, info, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Log($"匹配失败!可检查下年份是否与豆瓣一致,是否需要登录访问. [name]: {info.Name} [year]: {info.Year}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,11 +210,15 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 通过imdb获取TMDB id
|
// 通过imdb获取TMDB id
|
||||||
if (!string.IsNullOrEmpty(imdb))
|
if (!string.IsNullOrEmpty(imdb))
|
||||||
{
|
{
|
||||||
var tmdbId = await this.GetTmdbIdByImdbAsync(imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
var tmdbId = await this.GetTmdbIdByImdbAsync(imdb, info.MetadataLanguage, info, cancellationToken).ConfigureAwait(false);
|
||||||
if (!string.IsNullOrEmpty(tmdbId))
|
if (!string.IsNullOrEmpty(tmdbId))
|
||||||
{
|
{
|
||||||
return tmdbId;
|
return tmdbId;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.Log($"Can not found tmdb [id] by imdb id: \"{imdb}\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试通过搜索匹配获取tmdbId
|
// 尝试通过搜索匹配获取tmdbId
|
||||||
|
@ -227,6 +229,10 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
return tmdbId;
|
return tmdbId;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.Log($"Can not found tmdb [id] by name: \"{name}\" and year: \"{year}\"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -333,7 +339,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
|
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);
|
series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode);
|
||||||
|
|
||||||
|
|
||||||
|
@ -345,13 +351,13 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
// 演员
|
// 演员
|
||||||
if (seriesResult.Credits?.Cast != null)
|
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
|
var personInfo = new PersonInfo
|
||||||
{
|
{
|
||||||
Name = actor.Name.Trim(),
|
Name = actor.Name.Trim(),
|
||||||
Role = actor.Character,
|
Role = actor.Character,
|
||||||
Type = PersonType.Actor,
|
Type = PersonKind.Actor,
|
||||||
SortOrder = actor.Order,
|
SortOrder = actor.Order,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -395,7 +401,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
{
|
{
|
||||||
Name = person.Name.Trim(),
|
Name = person.Name.Trim(),
|
||||||
Role = person.Job,
|
Role = person.Job,
|
||||||
Type = type
|
Type = type == PersonType.Director ? PersonKind.Director : (type == PersonType.Producer ? PersonKind.Producer : PersonKind.Actor),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
||||||
|
@ -414,13 +420,5 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
this.Log("GetImageResponse url: {0}", url);
|
|
||||||
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Api;
|
||||||
using Jellyfin.Plugin.MetaShark.Providers;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Common.Plugins;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.MetaShark
|
namespace Jellyfin.Plugin.MetaShark
|
||||||
{
|
{
|
||||||
|
@ -20,20 +10,25 @@ namespace Jellyfin.Plugin.MetaShark
|
||||||
public class ServiceRegistrator : IPluginServiceRegistrator
|
public class ServiceRegistrator : IPluginServiceRegistrator
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <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>());
|
return new DoubanApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||||
});
|
});
|
||||||
serviceCollection.AddSingleton<TmdbApi>((ctx) =>
|
serviceCollection.AddSingleton((ctx) =>
|
||||||
{
|
{
|
||||||
return new TmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
return new TmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||||
});
|
});
|
||||||
serviceCollection.AddSingleton<OmdbApi>((ctx) =>
|
serviceCollection.AddSingleton((ctx) =>
|
||||||
{
|
{
|
||||||
return new OmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
return new OmdbApi(ctx.GetRequiredService<ILoggerFactory>());
|
||||||
});
|
});
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue