├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ └── 刮削失败相关问题.md
└── workflows
│ ├── beta.yaml
│ ├── build.yaml
│ ├── issue_close_inactive.yml
│ └── publish.yaml
├── .gitignore
├── AnitomySharp
├── Anitomy.cs
├── AnitomySharp.csproj
├── Element.cs
├── Keyword.cs
├── Options.cs
├── Parser.cs
├── ParserHelper.cs
├── ParserNumber.cs
├── StringHelper.cs
├── Token.cs
├── TokenRange.cs
└── Tokenizer.cs
├── Jellyfin.Plugin.MetaShark.Test
├── BoxSetProviderTest.cs
├── DefaultHttpClientFactory.cs
├── DoubanApiTest.cs
├── EpisodeProviderTest.cs
├── ImdbApiTest.cs
├── Jellyfin.Plugin.MetaShark.Test.csproj
├── MovieImageProviderTest.cs
├── MovieProviderTest.cs
├── ParseNameTest.cs
├── PersonProviderTest.cs
├── SeasonProviderTest.cs
├── SeriesImageProviderTest.cs
├── SeriesProviderTest.cs
├── StringSimilarityTest.cs
├── TmdbApiTest.cs
└── Usings.cs
├── Jellyfin.Plugin.MetaShark.sln
├── Jellyfin.Plugin.MetaShark
├── Api
│ ├── DoubanApi.cs
│ ├── Http
│ │ ├── HttpClientHandlerEx.cs
│ │ └── LoggingHandler.cs
│ ├── ImdbApi.cs
│ ├── OmdbApi.cs
│ └── TmdbApi.cs
├── BoxSetManager.cs
├── Configuration
│ ├── PluginConfiguration.cs
│ └── configPage.html
├── Controllers
│ └── ApiController.cs
├── Core
│ ├── ElementExtension.cs
│ ├── JsonExtension.cs
│ ├── ListExtension.cs
│ ├── NameParser.cs
│ ├── RegexExtension.cs
│ ├── StringExtension.cs
│ ├── StringMetric
│ │ └── JaroWinkler.cs
│ └── Utils.cs
├── ILRepack.targets
├── Jellyfin.Plugin.MetaShark.csproj
├── Model
│ ├── ApiResult.cs
│ ├── DoubanLoginInfo.cs
│ ├── DoubanSubject.cs
│ ├── DoubanSuggest.cs
│ ├── DoubanSuggestResult.cs
│ ├── GuessInfo.cs
│ ├── MetaSource.cs
│ ├── OmdbItem.cs
│ └── ParseNameResult.cs
├── Plugin.cs
├── Providers
│ ├── BaseProvider.cs
│ ├── BoxSetImageProvider.cs
│ ├── BoxSetProvider.cs
│ ├── EpisodeImageProvider.cs
│ ├── EpisodeProvider.cs
│ ├── Extensions
│ │ ├── EnumerableExtensions.cs
│ │ └── ProviderIdsExtensions.cs
│ ├── ExternalId
│ │ ├── DoubanExternalId.cs
│ │ └── DoubanPersonExternalId.cs
│ ├── MovieImageProvider.cs
│ ├── MovieProvider.cs
│ ├── PersonImageProvider.cs
│ ├── PersonProvider.cs
│ ├── SeasonImageProvider.cs
│ ├── SeasonProvider.cs
│ ├── SeriesImageProvider.cs
│ └── SeriesProvider.cs
├── ScheduledTasks
│ └── AutoCreateCollectionTask.cs
└── ServiceRegistrator.cs
├── LICENSE
├── README.md
├── doc
└── logo.png
└── scripts
└── generate_manifest.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | Jellyfin.Plugin.MetaShark/Vendor/** linguist-vendored
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/刮削失败相关问题.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 刮削失败相关问题
3 | about: 报告刮削失败相关问题.
4 | title: "[刮削]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **描述错误**
11 |
12 | 对错误是什么的清晰简明描述。
13 |
14 | **屏幕截图**
15 |
16 | 请添加问题截图以帮助解释您的问题。
17 |
18 | **日志**
19 |
20 | 请提供jellyfin打印的该影片的刮削日志。
21 |
22 | 日志查看方法: 控制台->高级->日志->点击log_yyyymmdd.log格式文件
23 |
24 | **运行环境(请填写以下信息):**
25 |
26 | - 操作系统:[例如 linux]
27 | - jellyfin 版本:[例如 10.8.9]
28 | - 插件版本:[例如 1.7.1]
29 |
--------------------------------------------------------------------------------
/.github/workflows/beta.yaml:
--------------------------------------------------------------------------------
1 | name: "🚀 Beta"
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | env:
7 | dotnet-version: 8.0.x
8 | python-version: 3.8
9 | project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
10 | artifact: metashark
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | name: Build & Release
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Get tags (For CHANGELOG)
20 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
21 | - name: Setup dotnet
22 | uses: actions/setup-dotnet@v3
23 | id: dotnet
24 | with:
25 | dotnet-version: ${{ env.dotnet-version }}
26 | - name: Change default dotnet version
27 | run: |
28 | echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
29 | - name: Initialize workflow variables
30 | id: vars
31 | run: |
32 | VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
33 | VERSION="$VERSION.0"
34 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
35 | echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
36 | - name: Build
37 | run: |
38 | dotnet restore ${{ env.project }} --no-cache
39 | dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 ${{ env.project }}
40 | mkdir -p artifacts
41 | cp ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll ./artifacts/
42 | - name: Upload artifact
43 | uses: actions/upload-artifact@v3
44 | with:
45 | name: ${{steps.vars.outputs.APP_NAME}}
46 | path: artifacts
47 | retention-days: 7
48 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: "🏗️ Build Plugin"
2 |
3 | on:
4 | push:
5 | branches:
6 | - "**"
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-dotnet@v3
15 | id: dotnet
16 | with:
17 | dotnet-version: 8.0.x
18 | - name: Change default dotnet version
19 | run: |
20 | echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
21 | - name: Install dependencies
22 | run: dotnet restore
23 | - name: Build
24 | run: dotnet build --no-restore
25 | # - name: Test
26 | # run: dotnet test --no-restore --verbosity normal
27 |
--------------------------------------------------------------------------------
/.github/workflows/issue_close_inactive.yml:
--------------------------------------------------------------------------------
1 | name: "🚫 Close Inactive"
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * 1"
6 | workflow_dispatch:
7 |
8 | jobs:
9 | close-inactive:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: close-issues
13 | uses: actions/stale@v7
14 | with:
15 | stale-issue-message: "This issue was closed due to inactive more than 30 days. You can reopen it if you think it should continue."
16 | exempt-issue-labels: "FAQ,question,bug,enhancement"
17 | days-before-stale: 30
18 | days-before-close: 0
19 | days-before-pr-stale: -1
20 | days-before-pr-close: -1
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: "🚀 Publish Plugin"
2 |
3 | on:
4 | push:
5 | tags: ["*"]
6 |
7 | env:
8 | dotnet-version: 8.0.x
9 | python-version: 3.8
10 | project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
11 | artifact: metashark
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | name: Build & Release
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Get tags (For CHANGELOG)
21 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
22 | - name: Setup dotnet
23 | uses: actions/setup-dotnet@v3
24 | id: dotnet
25 | with:
26 | dotnet-version: ${{ env.dotnet-version }}
27 | - name: Change default dotnet version
28 | run: |
29 | echo '{"sdk":{"version": "${{ steps.dotnet.outputs.dotnet-version }}"}}' > ./global.json
30 | - name: Setup python
31 | uses: actions/setup-python@v4
32 | with:
33 | python-version: ${{ env.python-version }}
34 | - name: Initialize workflow variables
35 | id: vars
36 | run: |
37 | VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed s/^v//)
38 | VERSION="$VERSION.0"
39 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
40 | echo "APP_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT
41 | - name: Build
42 | run: |
43 | dotnet restore ${{ env.project }} --no-cache
44 | dotnet publish --nologo --no-restore --configuration=Release --framework=net8.0 -p:Version=${{steps.vars.outputs.VERSION}} ${{ env.project }}
45 | mkdir -p artifacts
46 | zip -j ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll
47 | - name: Generate manifest
48 | run: python3 ./scripts/generate_manifest.py ./artifacts/${{ env.artifact }}_${{steps.vars.outputs.VERSION}}.zip ${GITHUB_REF#refs/*/}
49 | env:
50 | CN_DOMAIN: ${{ vars.CN_DOMAIN }}
51 | - name: Publish release
52 | uses: svenstaro/upload-release-action@v2
53 | with:
54 | repo_token: ${{ secrets.GITHUB_TOKEN }}
55 | file: ./artifacts/${{ env.artifact }}_*.zip
56 | tag: ${{ github.ref }}
57 | release_name: '${{ github.ref_name }}'
58 | file_glob: true
59 | overwrite: true
60 | - name: Publish manifest
61 | uses: svenstaro/upload-release-action@v2
62 | with:
63 | repo_token: ${{ secrets.GITHUB_TOKEN }}
64 | file: ./manifest*.json
65 | tag: "manifest"
66 | overwrite: true
67 | file_glob: true
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 | .vs/
4 | .idea/
5 | artifacts
6 | **/.DS_Store
7 | metashark/
8 | *.json
9 | .vscode
10 |
--------------------------------------------------------------------------------
/AnitomySharp/Anitomy.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014-2017, Eren Okka
3 | * Copyright (c) 2016-2017, Paul Miller
4 | * Copyright (c) 2017-2018, Tyler Bratton
5 | *
6 | * This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
9 | */
10 |
11 | using System;
12 |
13 | using System.Collections.Generic;
14 | using System.Linq;
15 |
16 | namespace AnitomySharp
17 | {
18 |
19 | ///
20 | /// A library capable of parsing Anime filenames.
21 | ///
22 | /// 用于解析动漫文件名的库。
23 | ///
24 | /// This code is a C++ to C# port of Anitomy,
25 | /// using the already existing Java port AnitomyJ as a reference.
26 | ///
27 | public class AnitomySharp
28 | {
29 | ///
30 | ///
31 | ///
32 | private AnitomySharp() { }
33 |
34 | ///
35 | /// Parses an anime into its consituent elements.
36 | ///
37 | /// 将动画文件名拆分为其组成元素。
38 | ///
39 | /// the anime file name 动画文件名
40 | /// the list of parsed elements 分解后的元素列表
41 | public static IEnumerable Parse(string filename)
42 | {
43 | return Parse(filename, new Options());
44 | }
45 |
46 | ///
47 | /// Parses an anime into its constituent elements.
48 | ///
49 | /// 将动画文件名拆分为其组成元素。
50 | ///
51 | /// the anime file name 动画文件名
52 | /// the options to parse with, use to use default options
53 | /// the list of parsed elements 分解后的元素列表
54 | /// **逻辑:**
55 | /// 1. 提取文件扩展名;
56 | /// 2.
57 | /// 3. #TODO
58 | ///
59 | public static IEnumerable Parse(string filename, Options options)
60 | {
61 | var elements = new List(32);
62 | var tokens = new List();
63 |
64 | /** remove/parse extension */
65 | var fname = filename;
66 | if (options.ParseFileExtension)
67 | {
68 | var extension = "";
69 | if (RemoveExtensionFromFilename(ref fname, ref extension))
70 | {
71 | /** 将文件扩展名元素加入元素列表 */
72 | elements.Add(new Element(Element.ElementCategory.ElementFileExtension, extension));
73 | }
74 | }
75 |
76 | /** set filename */
77 | if (string.IsNullOrEmpty(filename))
78 | {
79 | return elements;
80 | }
81 | /** 将去除扩展名后的文件名加入元素列表 */
82 | elements.Add(new Element(Element.ElementCategory.ElementFileName, fname));
83 |
84 | /** tokenize
85 | 1. 根据括号、一眼真的关键词、分隔符进行分词(带先后顺序)
86 | 2. 只将一眼真的关键字加入元素列表
87 | */
88 | var isTokenized = new Tokenizer(fname, elements, options, tokens).Tokenize();
89 | if (!isTokenized)
90 | {
91 | return elements;
92 | }
93 | new Parser(elements, options, tokens).Parse();
94 |
95 | // elements.ForEach(x => Console.WriteLine("\"" + x.Category + "\"" + ": " + "\"" + x.Value + "\""));
96 |
97 | return elements;
98 | }
99 |
100 |
101 | ///
102 | /// Removes the extension from the
103 | ///
104 | /// 确认扩展名有效,即在指定的文件扩展名元素类别中,然后去除文件扩展名
105 | ///
106 | /// the ref that will be updated with the new filename
107 | /// the ref that will be updated with the file extension
108 | /// if the extension was successfully separated from the filename
109 | private static bool RemoveExtensionFromFilename(ref string filename, ref string extension)
110 | {
111 | int position;
112 | if (string.IsNullOrEmpty(filename) || (position = filename.LastIndexOf('.')) == -1)
113 | {
114 | return false;
115 | }
116 |
117 | /** remove file extension */
118 | extension = filename.Substring(position + 1);
119 | if (extension.Length > 4 || !extension.All(char.IsLetterOrDigit))
120 | {
121 | return false;
122 | }
123 |
124 | /** check if valid anime extension */
125 | var keyword = KeywordManager.Normalize(extension);
126 | if (!KeywordManager.Contains(Element.ElementCategory.ElementFileExtension, keyword))
127 | {
128 | return false;
129 | }
130 |
131 | filename = filename.Substring(0, position);
132 | return true;
133 | }
134 | }
135 | }
--------------------------------------------------------------------------------
/AnitomySharp/AnitomySharp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | true
6 | AnitomySharp.NET6
7 | 0.4.0
8 | 0.4.0
9 | tabratton;senritsu;chu-shen
10 | 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.
11 | This fork of AnitomySharp is inspired by tabratton and senritsu, which adds more custom rules.
12 |
13 | https://github.com/chu-shen/AnitomySharp.git
14 | git
15 | Anitomy Anime
16 | true
17 | LICENSE
18 | README.md
19 | AnitomySharp.xml
20 | false
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/AnitomySharp/Element.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014-2017, Eren Okka
3 | * Copyright (c) 2016-2017, Paul Miller
4 | * Copyright (c) 2017-2018, Tyler Bratton
5 | *
6 | * This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
9 | */
10 |
11 | namespace AnitomySharp
12 | {
13 | ///
14 | /// An represents an identified Anime .
15 | /// A single filename may contain multiple of the same
16 | /// token(e.g ).
17 | ///
18 | /// 一个元素即是一个已标识的标记(token)
19 | ///
20 | /// 单个文件名可能包含多个相同的标记,比如:`ElementEpisodeNumber`元素类别的标记
21 | ///
22 | public class Element
23 | {
24 | ///
25 | /// Element Categories
26 | ///
27 | /// 元素类别
28 | ///
29 | public enum ElementCategory
30 | {
31 | ///
32 | /// 元素类别:动画季度,不带前缀
33 | ///
34 | ElementAnimeSeason,
35 | ///
36 | /// 元素类别:季度前缀,用于标识季度的元素类别
37 | ///
38 | ElementAnimeSeasonPrefix,
39 | ///
40 | /// 元素类别:动画名
41 | ///
42 | ElementAnimeTitle,
43 | ///
44 | /// 元素类别:动画类型
45 | ///
46 | ElementAnimeType,
47 | ///
48 | /// 元素类别:动画年份,唯一
49 | ///
50 | ElementAnimeYear,
51 | ///
52 | /// 元素类别:音频术语
53 | ///
54 | ElementAudioTerm,
55 | ///
56 | /// 元素类别:设备,用于标识设备类型
57 | ///
58 | ElementDeviceCompatibility,
59 | ///
60 | /// 元素类别:剧集数
61 | ///
62 | ElementEpisodeNumber,
63 | ///
64 | /// 元素类别:等效剧集数,常见于多季度番剧
65 | ///
66 | ElementEpisodeNumberAlt,
67 | ///
68 | /// 元素类别:剧集前缀,比如:“E”
69 | ///
70 | ElementEpisodePrefix,
71 | ///
72 | /// 元素类别:剧集名
73 | ///
74 | ElementEpisodeTitle,
75 | ///
76 | /// 元素类别:文件校验码,唯一
77 | ///
78 | ElementFileChecksum,
79 | ///
80 | /// 元素类别:文件扩展名,唯一
81 | ///
82 | ElementFileExtension,
83 | ///
84 | /// 文件名,唯一
85 | ///
86 | ElementFileName,
87 | ///
88 | /// 元素类别:语言
89 | ///
90 | ElementLanguage,
91 | ///
92 | /// 元素类别:其他,暂时无法分类的元素
93 | ///
94 | ElementOther,
95 | ///
96 | /// 元素类别:发布组,唯一
97 | ///
98 | ElementReleaseGroup,
99 | ///
100 | /// 元素类别:发布信息
101 | ///
102 | ElementReleaseInformation,
103 | ///
104 | /// 元素类别:发布版本
105 | ///
106 | ElementReleaseVersion,
107 | ///
108 | /// 元素类别:来源
109 | ///
110 | ElementSource,
111 | ///
112 | /// 元素类别:字幕
113 | ///
114 | ElementSubtitles,
115 | ///
116 | /// 元素类别:视频分辨率
117 | ///
118 | ElementVideoResolution,
119 | ///
120 | /// 元素类别:视频术语
121 | ///
122 | ElementVideoTerm,
123 | ///
124 | /// 元素类别:卷数
125 | ///
126 | ElementVolumeNumber,
127 | ///
128 | /// 元素类别:卷前缀
129 | ///
130 | ElementVolumePrefix,
131 | ///
132 | /// 元素类别:未知元素类型
133 | ///
134 | ElementUnknown
135 | }
136 |
137 | ///
138 | ///
139 | ///
140 | public ElementCategory Category { get; set; }
141 | ///
142 | ///
143 | ///
144 | public string Value { get; }
145 |
146 | ///
147 | /// Constructs a new Element
148 | ///
149 | /// 构造一个元素
150 | ///
151 | /// the category of the element
152 | /// the element's value
153 | public Element(ElementCategory category, string value)
154 | {
155 | Category = category;
156 | Value = value;
157 | }
158 |
159 | ///
160 | ///
161 | ///
162 | ///
163 | public override int GetHashCode()
164 | {
165 | return -1926371015 + Value.GetHashCode();
166 | }
167 |
168 | ///
169 | ///
170 | ///
171 | ///
172 | ///
173 | public override bool Equals(object obj)
174 | {
175 | if (this == obj)
176 | {
177 | return true;
178 | }
179 |
180 | if (obj == null || GetType() != obj.GetType())
181 | {
182 | return false;
183 | }
184 |
185 | var other = (Element)obj;
186 | return Category.Equals(other.Category);
187 | }
188 | ///
189 | ///
190 | ///
191 | ///
192 | public override string ToString()
193 | {
194 | return $"Element{{category={Category}, value='{Value}'}}";
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------
/AnitomySharp/Options.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014-2017, Eren Okka
3 | * Copyright (c) 2016-2017, Paul Miller
4 | * Copyright (c) 2017-2018, Tyler Bratton
5 | *
6 | * This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
9 | */
10 |
11 | namespace AnitomySharp
12 | {
13 |
14 | ///
15 | /// AnitomySharp search configuration options
16 | ///
17 | /// 提取元素时的默认配置项
18 | ///
19 | public class Options
20 | {
21 | ///
22 | /// 提取元素时使用的分隔符
23 | ///
24 | public string AllowedDelimiters { get; }
25 | ///
26 | /// 是否尝试提取集数。`true`表示提取
27 | ///
28 | public bool ParseEpisodeNumber { get; }
29 | ///
30 | /// 是否尝试提取本集标题。`true`表示提取
31 | ///
32 | public bool ParseEpisodeTitle { get; }
33 | ///
34 | /// 是否提取文件扩展名。`true`表示提取
35 | ///
36 | public bool ParseFileExtension { get; }
37 | ///
38 | /// 是否提取发布组。`true`表示提取
39 | ///
40 | public bool ParseReleaseGroup { get; }
41 | ///
42 | /// 提取元素时的配置项
43 | ///
44 | /// 默认值:" _.+,|"
45 | /// 默认值:true
46 | /// 默认值:true
47 | /// 默认值:true
48 | /// 默认值:true
49 | public Options(string delimiters = " _.+,| ", bool episode = true, bool title = true, bool extension = true, bool group = true)
50 | {
51 | AllowedDelimiters = delimiters;
52 | ParseEpisodeNumber = episode;
53 | ParseEpisodeTitle = title;
54 | ParseFileExtension = extension;
55 | ParseReleaseGroup = group;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/AnitomySharp/StringHelper.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014-2017, Eren Okka
3 | * Copyright (c) 2016-2017, Paul Miller
4 | * Copyright (c) 2017-2018, Tyler Bratton
5 | *
6 | * This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
9 | */
10 |
11 | using System;
12 | using System.Linq;
13 |
14 | namespace AnitomySharp
15 | {
16 |
17 | ///
18 | /// A string helper class that is analogous to string.cpp of the original Anitomy, and StringHelper.java of AnitomyJ.
19 | ///
20 | public static class StringHelper
21 | {
22 |
23 | ///
24 | /// Returns whether or not the character is alphanumeric
25 | ///
26 | /// 如果给定字符为字母或数字,则返回`true`
27 | ///
28 | ///
29 | ///
30 | public static bool IsAlphanumericChar(char c)
31 | {
32 | return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
33 | }
34 |
35 | ///
36 | /// Returns whether or not the character is a hex character.
37 | ///
38 | /// 如果给定字符为十六进制字符,则返回`true`
39 | ///
40 | ///
41 | ///
42 | private static bool IsHexadecimalChar(char c)
43 | {
44 | return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
45 | }
46 |
47 | ///
48 | /// Returns whether or not the character is a latin character
49 | ///
50 | /// 判断给定字符是否为拉丁字符
51 | ///
52 | ///
53 | ///
54 | private static bool IsLatinChar(char c)
55 | {
56 | // We're just checking until the end of the Latin Extended-B block,
57 | // rather than all the blocks that belong to the Latin script.
58 | return c <= '\u024F';
59 | }
60 |
61 | ///
62 | /// Returns whether or not the character is a Chinese character
63 | ///
64 | /// 判断给定字符是否为中文字符
65 | ///
66 | ///
67 | ///
68 | private static bool IsChineseChar(char c)
69 | {
70 | // We're just checking until the end of the Latin Extended-B block,
71 | // rather than all the blocks that belong to the Latin script.
72 | return c <= '\u9FFF' && c >= '\u4E00';
73 | }
74 |
75 | ///
76 | /// Returns whether or not the str is a hex string.
77 | ///
78 | /// 如果给定字符串为十六进制字符串,则返回`true`
79 | ///
80 | ///
81 | ///
82 | public static bool IsHexadecimalString(string str)
83 | {
84 | return !string.IsNullOrEmpty(str) && str.All(IsHexadecimalChar);
85 | }
86 |
87 | ///
88 | /// Returns whether or not the str is mostly a latin string.
89 | ///
90 | /// 判断给定字符串是否过半字符为拉丁
91 | ///
92 | ///
93 | ///
94 | public static bool IsMostlyLatinString(string str)
95 | {
96 | var length = !string.IsNullOrEmpty(str) ? 1.0 : str.Length;
97 | return str.Where(IsLatinChar).Count() / length >= 0.5;
98 | }
99 |
100 | ///
101 | /// Returns whether or not the str is mostly a Chinese string.
102 | ///
103 | /// 判断给定字符串是否过半字符为中文
104 | ///
105 | ///
106 | ///
107 | public static bool IsMostlyChineseString(string str)
108 | {
109 | var length = !string.IsNullOrEmpty(str) ? 1.0 : str.Length;
110 | return str.Where(IsChineseChar).Count() / length >= 0.5;
111 | }
112 | ///
113 | /// Returns whether or not the str is a numeric string.
114 | ///
115 | /// 判断字符串是否全数字
116 | ///
117 | ///
118 | ///
119 | public static bool IsNumericString(string str)
120 | {
121 | return str.All(char.IsDigit);
122 | }
123 | ///
124 | /// Returns whether or not the str is a alpha string.
125 | ///
126 | /// 判断字符串是否全字母
127 | ///
128 | ///
129 | ///
130 | public static bool IsAlphaString(string str)
131 | {
132 | return str.All(char.IsLetter);
133 | }
134 |
135 | ///
136 | /// Returns the int value of the str; 0 otherwise.
137 | ///
138 | ///
139 | ///
140 | public static int StringToInt(string str)
141 | {
142 | try
143 | {
144 | return int.Parse(str);
145 | }
146 | catch (Exception ex)
147 | {
148 | Console.WriteLine(ex);
149 | return 0;
150 | }
151 | }
152 |
153 | ///
154 | /// 提取给定范围的子字符串
155 | ///
156 | ///
157 | ///
158 | ///
159 | ///
160 | public static string SubstringWithCheck(string str, int start, int count)
161 | {
162 | if (start + count > str.Length) count = str.Length - start;
163 | return str.Substring(start, count);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/AnitomySharp/TokenRange.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2014-2017, Eren Okka
3 | * Copyright (c) 2016-2017, Paul Miller
4 | * Copyright (c) 2017-2018, Tyler Bratton
5 | *
6 | * This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
9 | */
10 |
11 | namespace AnitomySharp
12 | {
13 | ///
14 | /// 标记(token)的位置
15 | ///
16 | public struct TokenRange
17 | {
18 | ///
19 | /// 偏移值
20 | ///
21 | public int Offset;
22 | ///
23 | /// Token长度
24 | ///
25 | public int Size;
26 |
27 | ///
28 | /// 构造
29 | ///
30 | ///
31 | ///
32 | public TokenRange(int offset, int size)
33 | {
34 | Offset = offset;
35 | Size = size;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/BoxSetProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.Extensions.Logging;
8 | using Moq;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Test
11 | {
12 | [TestClass]
13 | public class BoxSetProviderTest
14 | {
15 |
16 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
17 | builder.AddSimpleConsole(options =>
18 | {
19 | options.IncludeScopes = true;
20 | options.SingleLine = true;
21 | options.TimestampFormat = "hh:mm:ss ";
22 | }));
23 |
24 |
25 | [TestMethod]
26 | public void TestGetMetadata()
27 | {
28 | var info = new BoxSetInfo() { Name = "攻壳机动队(系列)", MetadataLanguage = "zh" };
29 | var httpClientFactory = new DefaultHttpClientFactory();
30 | var libraryManagerStub = new Mock();
31 | libraryManagerStub.Setup(x => x.ParseName(info.Name)).Returns(new ItemLookupInfo(){Name = info.Name});
32 | var httpContextAccessorStub = new Mock();
33 | var doubanApi = new DoubanApi(loggerFactory);
34 | var tmdbApi = new TmdbApi(loggerFactory);
35 | var omdbApi = new OmdbApi(loggerFactory);
36 | var imdbApi = new ImdbApi(loggerFactory);
37 |
38 | Task.Run(async () =>
39 | {
40 | var provider = new BoxSetProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
41 | var result = await provider.GetMetadata(info, CancellationToken.None);
42 | Assert.IsNotNull(result);
43 |
44 | var str = result.ToJson();
45 | Console.WriteLine(result.ToJson());
46 | }).GetAwaiter().GetResult();
47 | }
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/DefaultHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Test
8 | {
9 | internal class DefaultHttpClientFactory : IHttpClientFactory
10 | {
11 | public HttpClient CreateClient(string name)
12 | {
13 | return new HttpClient();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/DoubanApiTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace Jellyfin.Plugin.MetaShark.Test
6 | {
7 | [TestClass]
8 | public class DoubanApiTest
9 | {
10 | private TestContext testContextInstance;
11 |
12 | ///
13 | /// Gets or sets the test context which provides
14 | /// information about and functionality for the current test run.
15 | ///
16 | public TestContext TestContext
17 | {
18 | get { return testContextInstance; }
19 | set { testContextInstance = value; }
20 | }
21 |
22 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
23 | builder.AddSimpleConsole(options =>
24 | {
25 | options.IncludeScopes = true;
26 | options.SingleLine = true;
27 | options.TimestampFormat = "hh:mm:ss ";
28 | }));
29 |
30 |
31 |
32 | [TestMethod]
33 | public void TestSearch()
34 | {
35 | var keyword = "声生不息";
36 | var api = new DoubanApi(loggerFactory);
37 |
38 | Task.Run(async () =>
39 | {
40 | try
41 | {
42 | var result = await api.SearchAsync(keyword, CancellationToken.None);
43 | TestContext.WriteLine(result.ToJson());
44 | }
45 | catch (Exception ex)
46 | {
47 | TestContext.WriteLine(ex.Message);
48 | }
49 | }).GetAwaiter().GetResult();
50 | }
51 |
52 |
53 | [TestMethod]
54 | public void TestSearchBySuggest()
55 | {
56 | var keyword = "重返少年时";
57 | var api = new DoubanApi(loggerFactory);
58 |
59 | Task.Run(async () =>
60 | {
61 | try
62 | {
63 | var result = await api.SearchBySuggestAsync(keyword, CancellationToken.None);
64 | var str = result.ToJson();
65 | TestContext.WriteLine(result.ToJson());
66 | }
67 | catch (Exception ex)
68 | {
69 | TestContext.WriteLine(ex.Message);
70 | }
71 | }).GetAwaiter().GetResult();
72 | }
73 |
74 |
75 | [TestMethod]
76 | public void TestGetVideoBySidAsync()
77 | {
78 | var sid = "26654184";
79 |
80 | var api = new DoubanApi(loggerFactory);
81 |
82 | Task.Run(async () =>
83 | {
84 | try
85 | {
86 | var result = await api.GetMovieAsync(sid, CancellationToken.None);
87 | TestContext.WriteLine(result.ToJson());
88 | }
89 | catch (Exception ex)
90 | {
91 | Console.WriteLine(ex.Message);
92 | }
93 | }).GetAwaiter().GetResult();
94 | }
95 |
96 | [TestMethod]
97 | public void TestFixGetImage()
98 | {
99 | // 演员入驻了豆瓣, 下载的不是演员的头像#5
100 | var sid = "35460157";
101 |
102 | var api = new DoubanApi(loggerFactory);
103 |
104 | Task.Run(async () =>
105 | {
106 | try
107 | {
108 | var result = await api.GetMovieAsync(sid, CancellationToken.None);
109 | Assert.AreEqual("https://img2.doubanio.com/view/celebrity/raw/public/p1598199472.61.jpg", result.Celebrities.First(x => x.Name == "刘陆").Img);
110 | }
111 | catch (Exception ex)
112 | {
113 | Console.WriteLine(ex.Message);
114 | }
115 | }).GetAwaiter().GetResult();
116 | }
117 |
118 | [TestMethod]
119 | public void TestGetCelebritiesBySidAsync()
120 | {
121 | var sid = "26654184";
122 |
123 | var api = new DoubanApi(loggerFactory);
124 |
125 | Task.Run(async () =>
126 | {
127 | try
128 | {
129 | var result = await api.GetCelebritiesBySidAsync(sid, CancellationToken.None);
130 | TestContext.WriteLine(result.ToJson());
131 | }
132 | catch (Exception ex)
133 | {
134 | Console.WriteLine(ex.Message);
135 | }
136 | }).GetAwaiter().GetResult();
137 | }
138 |
139 | [TestMethod]
140 | public void TestGetCelebritiesByCidAsync()
141 | {
142 | var cid = "1340364";
143 |
144 | var api = new DoubanApi(loggerFactory);
145 |
146 | Task.Run(async () =>
147 | {
148 | try
149 | {
150 | var result = await api.GetCelebrityAsync(cid, CancellationToken.None);
151 | TestContext.WriteLine(result.ToJson());
152 | }
153 | catch (Exception ex)
154 | {
155 | Console.WriteLine(ex.Message);
156 | }
157 | }).GetAwaiter().GetResult();
158 | }
159 |
160 | [TestMethod]
161 | public void TestGetCelebrityPhotosAsync()
162 | {
163 | var cid = "1322205";
164 |
165 | var api = new DoubanApi(loggerFactory);
166 |
167 | Task.Run(async () =>
168 | {
169 | try
170 | {
171 | var result = await api.GetCelebrityPhotosAsync(cid, CancellationToken.None);
172 | TestContext.WriteLine(result.ToJson());
173 | }
174 | catch (Exception ex)
175 | {
176 | Console.WriteLine(ex.Message);
177 | }
178 | }).GetAwaiter().GetResult();
179 | }
180 |
181 |
182 |
183 | [TestMethod]
184 | public void TestParseCelebrityName()
185 | {
186 |
187 | var api = new DoubanApi(loggerFactory);
188 |
189 |
190 | var name = "佩吉·陆 Peggy Lu";
191 | var result = api.ParseCelebrityName(name);
192 | Assert.AreEqual(result, "佩吉·陆");
193 |
194 | name = "Antony Coleman Antony Coleman";
195 | result = api.ParseCelebrityName(name);
196 | Assert.AreEqual(result, "Antony Coleman");
197 |
198 | name = "Dick Cook";
199 | result = api.ParseCelebrityName(name);
200 | Assert.AreEqual(result, "Dick Cook");
201 |
202 | name = "李凡秀";
203 | result = api.ParseCelebrityName(name);
204 | Assert.AreEqual(result, "李凡秀");
205 |
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/EpisodeProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using Microsoft.Extensions.Logging;
7 | using Moq;
8 | using System;
9 | using System.Collections.Generic;
10 | using System.Linq;
11 | using System.Text;
12 | using System.Threading.Tasks;
13 | using MediaBrowser.Common.Configuration;
14 | using MediaBrowser.Model.Serialization;
15 | using Microsoft.AspNetCore.Http;
16 | using MediaBrowser.Model.Entities;
17 |
18 | namespace Jellyfin.Plugin.MetaShark.Test
19 | {
20 | [TestClass]
21 | public class EpisodeProviderTest
22 | {
23 |
24 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
25 | builder.AddSimpleConsole(options =>
26 | {
27 | options.IncludeScopes = true;
28 | options.SingleLine = true;
29 | options.TimestampFormat = "hh:mm:ss ";
30 | }));
31 |
32 |
33 |
34 | [TestMethod]
35 | public void TestGetMetadata()
36 | {
37 | var doubanApi = new DoubanApi(loggerFactory);
38 | var tmdbApi = new TmdbApi(loggerFactory);
39 | var omdbApi = new OmdbApi(loggerFactory);
40 | var imdbApi = new ImdbApi(loggerFactory);
41 |
42 | var httpClientFactory = new DefaultHttpClientFactory();
43 | var libraryManagerStub = new Mock();
44 | var httpContextAccessorStub = new Mock();
45 |
46 | Task.Run(async () =>
47 | {
48 | var info = new EpisodeInfo()
49 | {
50 | Name = "Spice and Wolf",
51 | Path = "/test/Spice and Wolf/S00/[VCB-Studio] Spice and Wolf II [01][Hi444pp_1080p][x264_flac].mkv",
52 | MetadataLanguage = "zh",
53 | ParentIndexNumber = 0,
54 | SeriesProviderIds = new Dictionary() { { MetadataProvider.Tmdb.ToString(), "26707" } },
55 | IsAutomated = false,
56 | };
57 | var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
58 | var result = await provider.GetMetadata(info, CancellationToken.None);
59 | Assert.IsNotNull(result);
60 |
61 | var str = result.ToJson();
62 | Console.WriteLine(result.ToJson());
63 | }).GetAwaiter().GetResult();
64 | }
65 |
66 | [TestMethod]
67 | public void TestFixParseInfo()
68 | {
69 | var doubanApi = new DoubanApi(loggerFactory);
70 | var tmdbApi = new TmdbApi(loggerFactory);
71 | var omdbApi = new OmdbApi(loggerFactory);
72 | var imdbApi = new ImdbApi(loggerFactory);
73 |
74 | var httpClientFactory = new DefaultHttpClientFactory();
75 | var libraryManagerStub = new Mock();
76 | var httpContextAccessorStub = new Mock();
77 |
78 |
79 | var provider = new EpisodeProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
80 | var parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/[POPGO][Stand_Alone_Complex][05][1080P][BluRay][x264_FLACx2_AC3x1][chs_jpn][D87C36B6].mkv" });
81 | Assert.AreEqual(parseResult.IndexNumber, 5);
82 |
83 | parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/Fullmetal Alchemist Brotherhood.E05.1920X1080" });
84 | Assert.AreEqual(parseResult.IndexNumber, 5);
85 |
86 | parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/[SAIO-Raws] Neon Genesis Evangelion 05 [BD 1440x1080 HEVC-10bit OPUSx2 ASSx2].mkv" });
87 | Assert.AreEqual(parseResult.IndexNumber, 5);
88 |
89 | parseResult = provider.FixParseInfo(new EpisodeInfo() { Path = "/test/[Moozzi2] Samurai Champloo [SP03] Battlecry (Opening) PV (BD 1920x1080 x.264 AC3).mkv" });
90 | Assert.AreEqual(parseResult.IndexNumber, 3);
91 | Assert.AreEqual(parseResult.ParentIndexNumber, 0);
92 | }
93 |
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/ImdbApiTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace Jellyfin.Plugin.MetaShark.Test
5 | {
6 | [TestClass]
7 | public class ImdbApiTest
8 | {
9 | private TestContext testContextInstance;
10 |
11 | ///
12 | /// Gets or sets the test context which provides
13 | /// information about and functionality for the current test run.
14 | ///
15 | public TestContext TestContext
16 | {
17 | get { return testContextInstance; }
18 | set { testContextInstance = value; }
19 | }
20 |
21 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
22 | builder.AddSimpleConsole(options =>
23 | {
24 | options.IncludeScopes = true;
25 | options.SingleLine = true;
26 | options.TimestampFormat = "hh:mm:ss ";
27 | }));
28 |
29 |
30 |
31 | [TestMethod]
32 | public void TestCheckPersonNewImdbID()
33 | {
34 | var api = new ImdbApi(loggerFactory);
35 |
36 | Task.Run(async () =>
37 | {
38 | try
39 | {
40 | var id = "nm1123737";
41 | var result = await api.CheckPersonNewIDAsync(id, CancellationToken.None);
42 | Assert.AreEqual("nm0170924", result);
43 |
44 | id = "nm0170924";
45 | result = await api.CheckPersonNewIDAsync(id, CancellationToken.None);
46 | Assert.AreEqual(null, result);
47 | }
48 | catch (Exception ex)
49 | {
50 | TestContext.WriteLine(ex.Message);
51 | }
52 | }).GetAwaiter().GetResult();
53 | }
54 |
55 |
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/Jellyfin.Plugin.MetaShark.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | enable
5 | enable
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/MovieImageProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using Jellyfin.Plugin.MetaShark.Providers;
5 | using MediaBrowser.Controller.Library;
6 | using MediaBrowser.Controller.Providers;
7 | using MediaBrowser.Model.Entities;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Logging;
10 | using Moq;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.Linq;
14 | using System.Text;
15 | using System.Threading.Tasks;
16 |
17 | namespace Jellyfin.Plugin.MetaShark.Test
18 | {
19 | [TestClass]
20 | public class MovieImageProviderTest
21 | {
22 |
23 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
24 | builder.AddSimpleConsole(options =>
25 | {
26 | options.IncludeScopes = true;
27 | options.SingleLine = true;
28 | options.TimestampFormat = "hh:mm:ss ";
29 | }));
30 |
31 |
32 | [TestMethod]
33 | public void TestGetImages()
34 | {
35 | var info = new MediaBrowser.Controller.Entities.Movies.Movie()
36 | {
37 | Name = "秒速5厘米",
38 | PreferredMetadataLanguage = "zh",
39 | ProviderIds = new Dictionary { { BaseProvider.DoubanProviderId, "2043546" }, { MetadataProvider.Tmdb.ToString(), "38142" } }
40 | };
41 | var httpClientFactory = new DefaultHttpClientFactory();
42 | var libraryManagerStub = new Mock();
43 | var httpContextAccessorStub = new Mock();
44 | var doubanApi = new DoubanApi(loggerFactory);
45 | var tmdbApi = new TmdbApi(loggerFactory);
46 | var omdbApi = new OmdbApi(loggerFactory);
47 | var imdbApi = new ImdbApi(loggerFactory);
48 |
49 | Task.Run(async () =>
50 | {
51 | var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
52 | var result = await provider.GetImages(info, CancellationToken.None);
53 | Assert.IsNotNull(result);
54 |
55 | var str = result.ToJson();
56 | Console.WriteLine(result.ToJson());
57 | }).GetAwaiter().GetResult();
58 | }
59 |
60 | [TestMethod]
61 | public void TestGetImagesFromTMDB()
62 | {
63 | var info = new MediaBrowser.Controller.Entities.Movies.Movie()
64 | {
65 | PreferredMetadataLanguage = "zh",
66 | ProviderIds = new Dictionary { { MetadataProvider.Tmdb.ToString(), "752" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
67 | };
68 | var httpClientFactory = new DefaultHttpClientFactory();
69 | var libraryManagerStub = new Mock();
70 | var httpContextAccessorStub = new Mock();
71 | var doubanApi = new DoubanApi(loggerFactory);
72 | var tmdbApi = new TmdbApi(loggerFactory);
73 | var omdbApi = new OmdbApi(loggerFactory);
74 | var imdbApi = new ImdbApi(loggerFactory);
75 |
76 | Task.Run(async () =>
77 | {
78 | var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
79 | var result = await provider.GetImages(info, CancellationToken.None);
80 | Assert.IsNotNull(result);
81 |
82 | var str = result.ToJson();
83 | Console.WriteLine(result.ToJson());
84 | }).GetAwaiter().GetResult();
85 | }
86 |
87 | [TestMethod]
88 | public void TestGetImageResponse()
89 | {
90 | var httpClientFactory = new DefaultHttpClientFactory();
91 | var libraryManagerStub = new Mock();
92 | var httpContextAccessorStub = new Mock();
93 | var doubanApi = new DoubanApi(loggerFactory);
94 | var tmdbApi = new TmdbApi(loggerFactory);
95 | var omdbApi = new OmdbApi(loggerFactory);
96 | var imdbApi = new ImdbApi(loggerFactory);
97 |
98 | Task.Run(async () =>
99 | {
100 | var provider = new MovieImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
101 | var result = await provider.GetImageResponse("https://img1.doubanio.com/view/photo/m/public/p2893270877.jpg", CancellationToken.None);
102 | Assert.IsNotNull(result);
103 |
104 | var str = result.ToJson();
105 | Console.WriteLine(result.ToJson());
106 | }).GetAwaiter().GetResult();
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/MovieProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using Jellyfin.Plugin.MetaShark.Providers;
5 | using MediaBrowser.Controller.Library;
6 | using MediaBrowser.Controller.Providers;
7 | using MediaBrowser.Model.Entities;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Logging;
10 | using Moq;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.Linq;
14 | using System.Text;
15 | using System.Threading.Tasks;
16 |
17 | namespace Jellyfin.Plugin.MetaShark.Test
18 | {
19 | [TestClass]
20 | public class MovieProviderTest
21 | {
22 |
23 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
24 | builder.AddSimpleConsole(options =>
25 | {
26 | options.IncludeScopes = true;
27 | options.SingleLine = true;
28 | options.TimestampFormat = "hh:mm:ss ";
29 | }));
30 |
31 |
32 | [TestMethod]
33 | public void TestSearch()
34 | {
35 | var httpClientFactory = new DefaultHttpClientFactory();
36 | var libraryManagerStub = new Mock();
37 | var httpContextAccessorStub = new Mock();
38 | var doubanApi = new DoubanApi(loggerFactory);
39 | var tmdbApi = new TmdbApi(loggerFactory);
40 | var omdbApi = new OmdbApi(loggerFactory);
41 | var imdbApi = new ImdbApi(loggerFactory);
42 |
43 | Task.Run(async () =>
44 | {
45 | var info = new MovieInfo() { Name = "我", MetadataLanguage = "zh" };
46 | var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
47 | var result = await provider.GetSearchResults(info, CancellationToken.None);
48 | Assert.IsNotNull(result);
49 |
50 | var str = result.ToJson();
51 | Console.WriteLine(result.ToJson());
52 | }).GetAwaiter().GetResult();
53 | }
54 |
55 | [TestMethod]
56 | public void TestGetMetadata()
57 | {
58 | var httpClientFactory = new DefaultHttpClientFactory();
59 | var libraryManagerStub = new Mock();
60 | var httpContextAccessorStub = new Mock();
61 | var doubanApi = new DoubanApi(loggerFactory);
62 | var tmdbApi = new TmdbApi(loggerFactory);
63 | var omdbApi = new OmdbApi(loggerFactory);
64 | var imdbApi = new ImdbApi(loggerFactory);
65 |
66 | Task.Run(async () =>
67 | {
68 | var info = new MovieInfo() { Name = "姥姥的外孙", MetadataLanguage = "zh" };
69 | var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
70 | var result = await provider.GetMetadata(info, CancellationToken.None);
71 | Assert.IsNotNull(result);
72 |
73 | var str = result.ToJson();
74 | Console.WriteLine(result.ToJson());
75 | }).GetAwaiter().GetResult();
76 | }
77 |
78 | [TestMethod]
79 | public void TestGetMetadataAnime()
80 | {
81 | var info = new MovieInfo() { Name = "[SAIO-Raws] もののけ姫 Mononoke Hime [BD 1920x1036 HEVC-10bit OPUSx2 AC3]" };
82 | var httpClientFactory = new DefaultHttpClientFactory();
83 | var libraryManagerStub = new Mock();
84 | var httpContextAccessorStub = new Mock();
85 | var doubanApi = new DoubanApi(loggerFactory);
86 | var tmdbApi = new TmdbApi(loggerFactory);
87 | var omdbApi = new OmdbApi(loggerFactory);
88 | var imdbApi = new ImdbApi(loggerFactory);
89 |
90 | Task.Run(async () =>
91 | {
92 | var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
93 | var result = await provider.GetMetadata(info, CancellationToken.None);
94 | Assert.IsNotNull(result);
95 |
96 | var str = result.ToJson();
97 | Console.WriteLine(result.ToJson());
98 | }).GetAwaiter().GetResult();
99 | }
100 |
101 | [TestMethod]
102 | public void TestGetMetadataByTMDB()
103 | {
104 | var info = new MovieInfo() { Name = "人生大事", MetadataLanguage = "zh", ProviderIds = new Dictionary { { Plugin.ProviderId, MetaSource.Tmdb.ToString() }, { MetadataProvider.Tmdb.ToString(), "945664" } } };
105 | var httpClientFactory = new DefaultHttpClientFactory();
106 | var libraryManagerStub = new Mock();
107 | var httpContextAccessorStub = new Mock();
108 | var doubanApi = new DoubanApi(loggerFactory);
109 | var tmdbApi = new TmdbApi(loggerFactory);
110 | var omdbApi = new OmdbApi(loggerFactory);
111 | var imdbApi = new ImdbApi(loggerFactory);
112 |
113 | Task.Run(async () =>
114 | {
115 | var provider = new MovieProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
116 | var result = await provider.GetMetadata(info, CancellationToken.None);
117 | Assert.IsNotNull(result);
118 |
119 | var str = result.ToJson();
120 | Console.WriteLine(result.ToJson());
121 | }).GetAwaiter().GetResult();
122 | }
123 |
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/PersonProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using MediaBrowser.Model.Entities;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 | using Moq;
10 | using System;
11 | using System.Collections.Generic;
12 | using System.Linq;
13 | using System.Text;
14 | using System.Threading.Tasks;
15 |
16 | namespace Jellyfin.Plugin.MetaShark.Test
17 | {
18 | [TestClass]
19 | public class PersonProviderTest
20 | {
21 |
22 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
23 | builder.AddSimpleConsole(options =>
24 | {
25 | options.IncludeScopes = true;
26 | options.SingleLine = true;
27 | options.TimestampFormat = "hh:mm:ss ";
28 | }));
29 |
30 |
31 | [TestMethod]
32 | public void TestGetMetadata()
33 | {
34 | var httpClientFactory = new DefaultHttpClientFactory();
35 | var libraryManagerStub = new Mock();
36 | var httpContextAccessorStub = new Mock();
37 | var doubanApi = new DoubanApi(loggerFactory);
38 | var tmdbApi = new TmdbApi(loggerFactory);
39 | var omdbApi = new OmdbApi(loggerFactory);
40 | var imdbApi = new ImdbApi(loggerFactory);
41 |
42 | Task.Run(async () =>
43 | {
44 | var info = new PersonLookupInfo() { ProviderIds = new Dictionary() { { BaseProvider.DoubanProviderId, "27257290" } } };
45 | var provider = new PersonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
46 | var result = await provider.GetMetadata(info, CancellationToken.None);
47 | Assert.IsNotNull(result);
48 |
49 | var str = result.ToJson();
50 | Console.WriteLine(result.ToJson());
51 | }).GetAwaiter().GetResult();
52 | }
53 |
54 | [TestMethod]
55 | public void TestGetMetadataByTmdb()
56 | {
57 | var httpClientFactory = new DefaultHttpClientFactory();
58 | var libraryManagerStub = new Mock();
59 | var httpContextAccessorStub = new Mock();
60 | var doubanApi = new DoubanApi(loggerFactory);
61 | var tmdbApi = new TmdbApi(loggerFactory);
62 | var omdbApi = new OmdbApi(loggerFactory);
63 | var imdbApi = new ImdbApi(loggerFactory);
64 |
65 | Task.Run(async () =>
66 | {
67 | var info = new PersonLookupInfo() { ProviderIds = new Dictionary() { { MetadataProvider.Tmdb.ToString(), "78871" } } };
68 | var provider = new PersonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
69 | var result = await provider.GetMetadata(info, CancellationToken.None);
70 | Assert.IsNotNull(result);
71 |
72 | var str = result.ToJson();
73 | Console.WriteLine(result.ToJson());
74 | }).GetAwaiter().GetResult();
75 | }
76 |
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/SeasonProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using MediaBrowser.Model.Entities;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 | using Moq;
10 | using System;
11 | using System.Collections.Generic;
12 | using System.Linq;
13 | using System.Text;
14 | using System.Threading.Tasks;
15 |
16 | namespace Jellyfin.Plugin.MetaShark.Test
17 | {
18 | [TestClass]
19 | public class SeasonProviderTest
20 | {
21 |
22 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
23 | builder.AddSimpleConsole(options =>
24 | {
25 | options.IncludeScopes = true;
26 | options.SingleLine = true;
27 | options.TimestampFormat = "hh:mm:ss ";
28 | }));
29 |
30 |
31 | [TestMethod]
32 | public void TestGetMetadata()
33 | {
34 | var info = new SeasonInfo() { Name = "第 18 季", IndexNumber = 18, SeriesProviderIds = new Dictionary() { { BaseProvider.DoubanProviderId, "2059529" }, { MetadataProvider.Tmdb.ToString(), "34860" } } };
35 | var httpClientFactory = new DefaultHttpClientFactory();
36 | var libraryManagerStub = new Mock();
37 | var httpContextAccessorStub = new Mock();
38 | var doubanApi = new DoubanApi(loggerFactory);
39 | var tmdbApi = new TmdbApi(loggerFactory);
40 | var omdbApi = new OmdbApi(loggerFactory);
41 | var imdbApi = new ImdbApi(loggerFactory);
42 |
43 | Task.Run(async () =>
44 | {
45 | var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
46 | var result = await provider.GetMetadata(info, CancellationToken.None);
47 | Assert.IsNotNull(result);
48 |
49 | var str = result.ToJson();
50 | Console.WriteLine(result.ToJson());
51 | }).GetAwaiter().GetResult();
52 | }
53 |
54 | [TestMethod]
55 | public void TestGuessSeasonNumberByFileName()
56 | {
57 | var info = new SeasonInfo() { };
58 | var httpClientFactory = new DefaultHttpClientFactory();
59 | var libraryManagerStub = new Mock();
60 | var httpContextAccessorStub = new Mock();
61 | var doubanApi = new DoubanApi(loggerFactory);
62 | var tmdbApi = new TmdbApi(loggerFactory);
63 | var omdbApi = new OmdbApi(loggerFactory);
64 | var imdbApi = new ImdbApi(loggerFactory);
65 |
66 | var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
67 |
68 | 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");
69 | Assert.AreEqual(result, 2);
70 |
71 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第2季");
72 | Assert.AreEqual(result, 2);
73 |
74 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活 第2季");
75 | Assert.AreEqual(result, 2);
76 |
77 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/向往的生活/第三季");
78 | Assert.AreEqual(result, 3);
79 |
80 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/攻壳机动队Ghost_in_The_Shell_S.A.C._2nd_GIG");
81 | Assert.AreEqual(result, 2);
82 |
83 | // result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/Spice and Wolf/Spice and Wolf 2");
84 | // Assert.AreEqual(result, 2);
85 |
86 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/Spice and Wolf/Spice and Wolf 2 test");
87 | Assert.AreEqual(result, null);
88 |
89 | result = provider.GuessSeasonNumberByDirectoryName("/data/downloads/jellyfin/tv/[BDrip] Made in Abyss S02 [7鲁ACG x Sakurato]");
90 | Assert.AreEqual(result, 2);
91 | }
92 |
93 | [TestMethod]
94 | public void TestGuestDoubanSeasonByYearAsync()
95 | {
96 | var httpClientFactory = new DefaultHttpClientFactory();
97 | var libraryManagerStub = new Mock();
98 | var httpContextAccessorStub = new Mock();
99 | var doubanApi = new DoubanApi(loggerFactory);
100 | var tmdbApi = new TmdbApi(loggerFactory);
101 | var omdbApi = new OmdbApi(loggerFactory);
102 | var imdbApi = new ImdbApi(loggerFactory);
103 |
104 | Task.Run(async () =>
105 | {
106 | var provider = new SeasonProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
107 | var result = await provider.GuestDoubanSeasonByYearAsync("机动战士高达0083 星尘的回忆", 1991, null, CancellationToken.None);
108 | Assert.AreEqual(result, "1766564");
109 | }).GetAwaiter().GetResult();
110 | }
111 |
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/SeriesImageProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using Jellyfin.Plugin.MetaShark.Providers;
5 | using MediaBrowser.Controller.Library;
6 | using MediaBrowser.Controller.Providers;
7 | using MediaBrowser.Model.Entities;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Logging;
10 | using Moq;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.Linq;
14 | using System.Text;
15 | using System.Threading.Tasks;
16 |
17 | namespace Jellyfin.Plugin.MetaShark.Test
18 | {
19 | [TestClass]
20 | public class SeriesImageProviderTest
21 | {
22 |
23 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
24 | builder.AddSimpleConsole(options =>
25 | {
26 | options.IncludeScopes = true;
27 | options.SingleLine = true;
28 | options.TimestampFormat = "hh:mm:ss ";
29 | }));
30 |
31 | [TestMethod]
32 | public void TestGetImages()
33 | {
34 | var info = new MediaBrowser.Controller.Entities.TV.Series()
35 | {
36 | Name = "花牌情缘",
37 | PreferredMetadataLanguage = "zh",
38 | ProviderIds = new Dictionary { { BaseProvider.DoubanProviderId, "6439459" }, { MetadataProvider.Tmdb.ToString(), "45247" } }
39 | };
40 | var httpClientFactory = new DefaultHttpClientFactory();
41 | var libraryManagerStub = new Mock();
42 | var httpContextAccessorStub = new Mock();
43 | var doubanApi = new DoubanApi(loggerFactory);
44 | var tmdbApi = new TmdbApi(loggerFactory);
45 | var omdbApi = new OmdbApi(loggerFactory);
46 | var imdbApi = new ImdbApi(loggerFactory);
47 |
48 | Task.Run(async () =>
49 | {
50 | var provider = new SeriesImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
51 | var result = await provider.GetImages(info, CancellationToken.None);
52 | Assert.IsNotNull(result);
53 |
54 | var str = result.ToJson();
55 | Console.WriteLine(result.ToJson());
56 | }).GetAwaiter().GetResult();
57 | }
58 |
59 |
60 | [TestMethod]
61 | public void TestGetImagesFromTMDB()
62 | {
63 | var info = new MediaBrowser.Controller.Entities.TV.Series()
64 | {
65 | PreferredMetadataLanguage = "zh",
66 | ProviderIds = new Dictionary { { MetadataProvider.Tmdb.ToString(), "67534" }, { Plugin.ProviderId, MetaSource.Tmdb.ToString() } }
67 | };
68 | var httpClientFactory = new DefaultHttpClientFactory();
69 | var libraryManagerStub = new Mock();
70 | var httpContextAccessorStub = new Mock();
71 | var doubanApi = new DoubanApi(loggerFactory);
72 | var tmdbApi = new TmdbApi(loggerFactory);
73 | var omdbApi = new OmdbApi(loggerFactory);
74 | var imdbApi = new ImdbApi(loggerFactory);
75 |
76 | Task.Run(async () =>
77 | {
78 | var provider = new SeriesImageProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
79 | var result = await provider.GetImages(info, CancellationToken.None);
80 | Assert.IsNotNull(result);
81 |
82 | var str = result.ToJson();
83 | Console.WriteLine(result.ToJson());
84 | }).GetAwaiter().GetResult();
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/SeriesProviderTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.Extensions.Logging;
8 | using Moq;
9 | using System;
10 | using System.Collections.Generic;
11 | using System.Linq;
12 | using System.Text;
13 | using System.Threading.Tasks;
14 |
15 | namespace Jellyfin.Plugin.MetaShark.Test
16 | {
17 | [TestClass]
18 | public class SeriesProviderTest
19 | {
20 |
21 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
22 | builder.AddSimpleConsole(options =>
23 | {
24 | options.IncludeScopes = true;
25 | options.SingleLine = true;
26 | options.TimestampFormat = "hh:mm:ss ";
27 | }));
28 |
29 |
30 | [TestMethod]
31 | public void TestGetMetadata()
32 | {
33 | var info = new SeriesInfo() { Name = "天下长河" };
34 | var httpClientFactory = new DefaultHttpClientFactory();
35 | var libraryManagerStub = new Mock();
36 | var httpContextAccessorStub = new Mock();
37 | var doubanApi = new DoubanApi(loggerFactory);
38 | var tmdbApi = new TmdbApi(loggerFactory);
39 | var omdbApi = new OmdbApi(loggerFactory);
40 | var imdbApi = new ImdbApi(loggerFactory);
41 |
42 | Task.Run(async () =>
43 | {
44 | var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
45 | var result = await provider.GetMetadata(info, CancellationToken.None);
46 | Assert.IsNotNull(result);
47 |
48 | var str = result.ToJson();
49 | Console.WriteLine(result.ToJson());
50 | }).GetAwaiter().GetResult();
51 | }
52 |
53 | [TestMethod]
54 | public void TestGetAnimeMetadata()
55 | {
56 | var info = new SeriesInfo() { Name = "命运-冠位嘉年华" };
57 | var httpClientFactory = new DefaultHttpClientFactory();
58 | var libraryManagerStub = new Mock();
59 | var httpContextAccessorStub = new Mock();
60 | var doubanApi = new DoubanApi(loggerFactory);
61 | var tmdbApi = new TmdbApi(loggerFactory);
62 | var omdbApi = new OmdbApi(loggerFactory);
63 | var imdbApi = new ImdbApi(loggerFactory);
64 |
65 | Task.Run(async () =>
66 | {
67 | var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi, imdbApi);
68 | var result = await provider.GetMetadata(info, CancellationToken.None);
69 | Assert.AreEqual(result.Item.Name, "命运/冠位指定嘉年华 公元2020奥林匹亚英灵限界大祭");
70 | Assert.AreEqual(result.Item.OriginalTitle, "Fate/Grand Carnival");
71 | Assert.IsNotNull(result);
72 |
73 | var str = result.ToJson();
74 | Console.WriteLine(result.ToJson());
75 | }).GetAwaiter().GetResult();
76 | }
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/StringSimilarityTest.cs:
--------------------------------------------------------------------------------
1 |
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Emby.Naming.TV;
4 |
5 | namespace Jellyfin.Plugin.MetaShark.Test
6 | {
7 | [TestClass]
8 | public class StringSimilarityTest
9 | {
10 | [TestMethod]
11 | public void TestString()
12 | {
13 |
14 | var str1 = "雄狮少年";
15 | var str2 = "我是特优声 剧团季";
16 |
17 | var score = str1.Distance(str2);
18 |
19 | str1 = "雄狮少年";
20 | str2 = "雄狮少年 第二季";
21 |
22 | score = str1.Distance(str2);
23 |
24 | var score2 = "君子和而不同".Distance("小人同而不和");
25 |
26 | Assert.IsTrue(score > 0);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/TmdbApiTest.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Providers;
4 | using Microsoft.Extensions.Logging;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using TMDbLib.Objects.Find;
12 | using TMDbLib.Objects.Languages;
13 |
14 | namespace Jellyfin.Plugin.MetaShark.Test
15 | {
16 | [TestClass]
17 | public class TmdbApiTest
18 | {
19 | private TestContext testContextInstance;
20 |
21 | ///
22 | /// Gets or sets the test context which provides
23 | /// information about and functionality for the current test run.
24 | ///
25 | public TestContext TestContext
26 | {
27 | get { return testContextInstance; }
28 | set { testContextInstance = value; }
29 | }
30 |
31 | ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
32 | builder.AddSimpleConsole(options =>
33 | {
34 | options.IncludeScopes = true;
35 | options.SingleLine = true;
36 | options.TimestampFormat = "hh:mm:ss ";
37 | }));
38 |
39 |
40 | [TestMethod]
41 | public void TestGetMovie()
42 | {
43 | var api = new TmdbApi(loggerFactory);
44 |
45 | Task.Run(async () =>
46 | {
47 | try
48 | {
49 | var result = await api.GetMovieAsync(752, "zh", "zh", CancellationToken.None)
50 | .ConfigureAwait(false);
51 | Assert.IsNotNull(result);
52 | TestContext.WriteLine(result.Images.ToJson());
53 | }
54 | catch (Exception ex)
55 | {
56 | TestContext.WriteLine(ex.Message);
57 | }
58 | }).GetAwaiter().GetResult();
59 | }
60 |
61 |
62 | [TestMethod]
63 | public void TestGetSeries()
64 | {
65 | var api = new TmdbApi(loggerFactory);
66 |
67 | Task.Run(async () =>
68 | {
69 | try
70 | {
71 | var result = await api.GetSeriesAsync(13372, "zh", "zh", CancellationToken.None)
72 | .ConfigureAwait(false);
73 | Assert.IsNotNull(result);
74 | TestContext.WriteLine(result.Images.ToJson());
75 | }
76 | catch (Exception ex)
77 | {
78 | TestContext.WriteLine(ex.Message);
79 | }
80 | }).GetAwaiter().GetResult();
81 | }
82 |
83 |
84 | [TestMethod]
85 | public void TestGetEpisode()
86 | {
87 | var api = new TmdbApi(loggerFactory);
88 |
89 | Task.Run(async () =>
90 | {
91 | try
92 | {
93 | var result = await api.GetEpisodeAsync(13372, 1, 1, "zh", "zh", CancellationToken.None)
94 | .ConfigureAwait(false);
95 | Assert.IsNotNull(result);
96 | TestContext.WriteLine(result.Images.Stills.ToJson());
97 | }
98 | catch (Exception ex)
99 | {
100 | TestContext.WriteLine(ex.Message);
101 | }
102 | }).GetAwaiter().GetResult();
103 | }
104 |
105 |
106 |
107 | [TestMethod]
108 | public void TestSearch()
109 | {
110 | var keyword = "狼与香辛料";
111 | var api = new TmdbApi(loggerFactory);
112 |
113 | Task.Run(async () =>
114 | {
115 | try
116 | {
117 | var result = await api.SearchSeriesAsync(keyword, "zh", CancellationToken.None).ConfigureAwait(false);
118 | Assert.IsNotNull(result);
119 | TestContext.WriteLine(result.ToJson());
120 | }
121 | catch (Exception ex)
122 | {
123 | TestContext.WriteLine(ex.Message);
124 | }
125 | }).GetAwaiter().GetResult();
126 | }
127 |
128 |
129 | [TestMethod]
130 | public void TestFindByExternalId()
131 | {
132 | var api = new TmdbApi(loggerFactory);
133 |
134 | Task.Run(async () =>
135 | {
136 | try
137 | {
138 | var result = await api.FindByExternalIdAsync("tt5924366", FindExternalSource.Imdb, "zh", CancellationToken.None)
139 | .ConfigureAwait(false);
140 | Assert.IsNotNull(result);
141 | TestContext.WriteLine(result.ToJson());
142 | }
143 | catch (Exception ex)
144 | {
145 | TestContext.WriteLine(ex.Message);
146 | }
147 | }).GetAwaiter().GetResult();
148 | }
149 |
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.Test/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Microsoft.VisualStudio.TestTools.UnitTesting;
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.3.32922.545
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark", "Jellyfin.Plugin.MetaShark\Jellyfin.Plugin.MetaShark.csproj", "{D921B930-CF91-406F-ACBC-08914DCD0D34}"
6 | EndProject
7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark.Test", "Jellyfin.Plugin.MetaShark.Test\Jellyfin.Plugin.MetaShark.Test.csproj", "{80814353-4291-4230-8C4A-4C45CAD4D5D3}"
8 | EndProject
9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnitomySharp", "AnitomySharp\AnitomySharp.csproj", "{B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}"
10 | EndProject
11 | Global
12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
13 | Debug|Any CPU = Debug|Any CPU
14 | Release|Any CPU = Release|Any CPU
15 | EndGlobalSection
16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
17 | {D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.Build.0 = Release|Any CPU
21 | {80814353-4291-4230-8C4A-4C45CAD4D5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {80814353-4291-4230-8C4A-4C45CAD4D5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {B6FC3E72-D30D-49D3-B545-0EF0CCFF220A}.Release|Any CPU.Build.0 = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(SolutionProperties) = preSolution
31 | HideSolutionNode = FALSE
32 | EndGlobalSection
33 | GlobalSection(ExtensibilityGlobals) = postSolution
34 | SolutionGuid = {7D81AE36-16A1-4386-8B86-21FACCB675DF}
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Api/Http/HttpClientHandlerEx.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Api.Http
11 | {
12 | public class HttpClientHandlerEx : HttpClientHandler
13 | {
14 | public HttpClientHandlerEx()
15 | {
16 | // 忽略SSL证书问题
17 | ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true;
18 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
19 | CookieContainer = new CookieContainer();
20 | UseCookies = true;
21 | }
22 |
23 | protected override Task SendAsync(
24 | HttpRequestMessage request, CancellationToken cancellationToken)
25 | {
26 | return base.SendAsync(request, cancellationToken);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Api/Http/LoggingHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Text;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Jellyfin.Plugin.MetaShark.Api.Http
12 | {
13 | public class LoggingHandler : DelegatingHandler
14 | {
15 | private readonly ILogger _logger;
16 | public LoggingHandler(HttpMessageHandler innerHandler, ILoggerFactory loggerFactory)
17 | : base(innerHandler)
18 | {
19 | _logger = loggerFactory.CreateLogger();
20 | }
21 |
22 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
23 | {
24 | _logger.LogInformation((request.RequestUri?.ToString() ?? string.Empty));
25 |
26 | return await base.SendAsync(request, cancellationToken);
27 | }
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Api/ImdbApi.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Core;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using Microsoft.Extensions.Logging;
4 | using System;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text.RegularExpressions;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace Jellyfin.Plugin.MetaShark.Api
12 | {
13 | public class ImdbApi : IDisposable
14 | {
15 | private readonly ILogger _logger;
16 | private readonly IMemoryCache _memoryCache;
17 | private readonly HttpClient httpClient;
18 |
19 | Regex regId = new Regex(@"/(tt\d+)", RegexOptions.Compiled);
20 | Regex regPersonId = new Regex(@"/(nm\d+)", RegexOptions.Compiled);
21 |
22 | public ImdbApi(ILoggerFactory loggerFactory)
23 | {
24 | _logger = loggerFactory.CreateLogger();
25 | _memoryCache = new MemoryCache(new MemoryCacheOptions());
26 |
27 | var handler = new HttpClientHandler()
28 | {
29 | AllowAutoRedirect = false
30 | };
31 | httpClient = new HttpClient(handler);
32 | httpClient.Timeout = TimeSpan.FromSeconds(10);
33 | }
34 |
35 | ///
36 | /// 通过imdb获取信息(会返回最新的imdb id)
37 | ///
38 | public async Task CheckNewIDAsync(string id, CancellationToken cancellationToken)
39 | {
40 | var cacheKey = $"CheckNewImdbID_{id}";
41 | var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
42 | if (this._memoryCache.TryGetValue(cacheKey, out var item))
43 | {
44 | return item;
45 | }
46 |
47 | try
48 | {
49 | var url = $"https://www.imdb.com/title/{id}/";
50 | var resp = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
51 | if (resp.Headers.TryGetValues("Location", out var values))
52 | {
53 | var location = values.First();
54 | var newId = location.GetMatchGroup(this.regId);
55 | if (!string.IsNullOrEmpty(newId))
56 | {
57 | item = newId;
58 | }
59 | }
60 | this._memoryCache.Set(cacheKey, item, expiredOption);
61 | return item;
62 | }
63 | catch (Exception ex)
64 | {
65 | this._logger.LogError(ex, "CheckNewImdbID error. id: {0}", id);
66 | this._memoryCache.Set(cacheKey, null, expiredOption);
67 | return null;
68 | }
69 |
70 | return null;
71 | }
72 |
73 | ///
74 | /// 通过imdb获取信息(会返回最新的imdb id)
75 | ///
76 | public async Task CheckPersonNewIDAsync(string id, CancellationToken cancellationToken)
77 | {
78 | var cacheKey = $"CheckPersonNewImdbID_{id}";
79 | var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
80 | if (this._memoryCache.TryGetValue(cacheKey, out var item))
81 | {
82 | return item;
83 | }
84 |
85 | try
86 | {
87 | var url = $"https://www.imdb.com/name/{id}/";
88 | var resp = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
89 | if (resp.Headers.TryGetValues("Location", out var values))
90 | {
91 | var location = values.First();
92 | var newId = location.GetMatchGroup(this.regPersonId);
93 | if (!string.IsNullOrEmpty(newId))
94 | {
95 | item = newId;
96 | }
97 | }
98 | this._memoryCache.Set(cacheKey, item, expiredOption);
99 | return item;
100 | }
101 | catch (Exception ex)
102 | {
103 | this._logger.LogError(ex, "CheckPersonNewImdbID error. id: {0}", id);
104 | this._memoryCache.Set(cacheKey, null, expiredOption);
105 | return null;
106 | }
107 | }
108 |
109 |
110 | public void Dispose()
111 | {
112 | Dispose(true);
113 | GC.SuppressFinalize(this);
114 | }
115 |
116 | protected virtual void Dispose(bool disposing)
117 | {
118 | if (disposing)
119 | {
120 | _memoryCache.Dispose();
121 | }
122 | }
123 |
124 | private bool IsEnable()
125 | {
126 | return Plugin.Instance?.Configuration.EnableTmdb ?? true;
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Api/OmdbApi.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Model;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using Microsoft.Extensions.Logging;
4 | using System;
5 | using System.Net.Http;
6 | using System.Net.Http.Json;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Api
11 | {
12 | public class OmdbApi : IDisposable
13 | {
14 | public const string DEFAULT_API_KEY = "2c9d9507";
15 | private readonly ILogger _logger;
16 | private readonly IMemoryCache _memoryCache;
17 | private readonly HttpClient httpClient;
18 |
19 | public OmdbApi(ILoggerFactory loggerFactory)
20 | {
21 | _logger = loggerFactory.CreateLogger();
22 | _memoryCache = new MemoryCache(new MemoryCacheOptions());
23 | httpClient = new HttpClient();
24 | httpClient.Timeout = TimeSpan.FromSeconds(5);
25 | }
26 |
27 | ///
28 | /// 通过imdb获取信息(会返回最新的imdb id)
29 | ///
30 | /// imdb id
31 | ///
32 | ///
33 | public async Task GetByImdbID(string id, CancellationToken cancellationToken)
34 | {
35 | if (!this.IsEnable())
36 | {
37 | return null;
38 | }
39 |
40 | var cacheKey = $"GetByImdbID_{id}";
41 | var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
42 | if (this._memoryCache.TryGetValue(cacheKey, out var item))
43 | {
44 | return item;
45 | }
46 |
47 | try
48 | {
49 | var url = $"https://www.omdbapi.com/?i={id}&apikey={DEFAULT_API_KEY}";
50 | item = await this.httpClient.GetFromJsonAsync(url, cancellationToken).ConfigureAwait(false);
51 | _memoryCache.Set(cacheKey, item, expiredOption);
52 | return item;
53 | }
54 | catch (Exception ex)
55 | {
56 | this._logger.LogError(ex, "GetByImdbID error. id: {0}", id);
57 | _memoryCache.Set(cacheKey, null, expiredOption);
58 | return null;
59 | }
60 | }
61 |
62 |
63 | public void Dispose()
64 | {
65 | Dispose(true);
66 | GC.SuppressFinalize(this);
67 | }
68 |
69 | protected virtual void Dispose(bool disposing)
70 | {
71 | if (disposing)
72 | {
73 | _memoryCache.Dispose();
74 | }
75 | }
76 |
77 | private bool IsEnable()
78 | {
79 | return Plugin.Instance?.Configuration.EnableTmdb ?? true;
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/BoxSetManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using AngleSharp.Text;
8 | using Jellyfin.Data.Enums;
9 | using Jellyfin.Plugin.MetaShark.Core;
10 | using MediaBrowser.Controller.Collections;
11 | using MediaBrowser.Controller.Entities;
12 | using MediaBrowser.Controller.Entities.Movies;
13 | using MediaBrowser.Controller.Library;
14 | using MediaBrowser.Model.Entities;
15 | using Microsoft.Extensions.Hosting;
16 | using Microsoft.Extensions.Logging;
17 |
18 | namespace Jellyfin.Plugin.MetaShark;
19 |
20 | public class BoxSetManager : IHostedService
21 | {
22 | private readonly ILibraryManager _libraryManager;
23 | private readonly ICollectionManager _collectionManager;
24 | private readonly Timer _timer;
25 | private readonly HashSet _queuedTmdbCollection;
26 | private readonly ILogger _logger; // TODO logging
27 |
28 | public BoxSetManager(ILibraryManager libraryManager, ICollectionManager collectionManager, ILoggerFactory loggerFactory)
29 | {
30 | _libraryManager = libraryManager;
31 | _collectionManager = collectionManager;
32 | _logger = loggerFactory.CreateLogger();
33 | _timer = new Timer(_ => OnTimerElapsed(), null, Timeout.Infinite, Timeout.Infinite);
34 | _queuedTmdbCollection = new HashSet();
35 | }
36 |
37 |
38 | public async Task ScanLibrary(IProgress progress)
39 | {
40 | var startIndex = 0;
41 | var pagesize = 1000;
42 |
43 | if (!(Plugin.Instance?.Configuration.EnableTmdbCollection ?? false))
44 | {
45 | _logger.LogInformation("插件配置中未打开自动创建合集功能");
46 | progress?.Report(100);
47 | return;
48 | }
49 |
50 | var boxSets = GetAllBoxSetsFromLibrary();
51 | var movieCollections = GetMoviesFromLibrary();
52 |
53 | _logger.LogInformation("共找到 {Count} 个合集信息", movieCollections.Count);
54 | int index = 0;
55 | foreach (var (collectionName, collectionMovies) in movieCollections)
56 | {
57 | progress?.Report(100.0 * index / movieCollections.Count);
58 |
59 | var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName);
60 | await AddMoviesToCollection(collectionMovies, collectionName, boxSet).ConfigureAwait(false);
61 | index++;
62 | }
63 |
64 | progress?.Report(100);
65 | }
66 |
67 | private async Task AddMoviesToCollection(IList movies, string collectionName, BoxSet boxSet)
68 | {
69 | if (movies.Count < 2)
70 | {
71 | // won't automatically create collection if only one movie in it
72 | return;
73 | }
74 |
75 | var movieIds = movies.Select(m => m.Id).ToList();
76 | if (boxSet is null)
77 | {
78 | _logger.LogInformation("创建合集 [{collectionName}],添加电影:{moviesNames}", collectionName, movies.Select(m => m.Name).Aggregate((a, b) => a + ", " + b));
79 | boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
80 | {
81 | Name = collectionName,
82 | });
83 |
84 | await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
85 |
86 | // HACK: 等获取 boxset 元数据后再更新一次合集,用于修正刷新元数据后丢失关联电影的 BUG
87 | _queuedTmdbCollection.Add(collectionName);
88 | _timer.Change(60000, Timeout.Infinite);
89 | }
90 | else
91 | {
92 | _logger.LogInformation("更新合集 [{collectionName}],添加电影:{moviesNames}", collectionName, movies.Select(m => m.Name).Aggregate((a, b) => a + ", " + b));
93 | await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
94 | }
95 | }
96 |
97 |
98 | private IReadOnlyCollection GetAllBoxSetsFromLibrary()
99 | {
100 | return _libraryManager.GetItemList(new InternalItemsQuery
101 | {
102 | IncludeItemTypes = new[] { BaseItemKind.BoxSet },
103 | CollapseBoxSetItems = false,
104 | Recursive = true
105 | }).Select(b => b as BoxSet).ToList();
106 | }
107 |
108 |
109 | public IDictionary> GetMoviesFromLibrary()
110 | {
111 | var collectionMoviesMap = new Dictionary>();
112 |
113 | foreach (var library in _libraryManager.RootFolder.Children)
114 | {
115 | // 判断当前是媒体库是否是电影,并开启了 metashark 插件
116 | var typeOptions = _libraryManager.GetLibraryOptions(library).TypeOptions;
117 | if (typeOptions.FirstOrDefault(x => x.Type == "Movie" && x.MetadataFetchers.Contains(Plugin.PluginName)) == null)
118 | {
119 | continue;
120 | }
121 |
122 | var startIndex = 0;
123 | var pagesize = 1000;
124 |
125 | while (true)
126 | {
127 | var movies = _libraryManager.GetItemList(new InternalItemsQuery
128 | {
129 | IncludeItemTypes = new[] { BaseItemKind.Movie },
130 | IsVirtualItem = false,
131 | OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
132 | Parent = library,
133 | StartIndex = startIndex,
134 | Limit = pagesize,
135 | Recursive = true,
136 | HasTmdbId = true
137 | }).Select(b => b as Movie).ToList();
138 |
139 | foreach (var movie in movies)
140 | {
141 | if (string.IsNullOrEmpty(movie.CollectionName))
142 | {
143 | continue;
144 | }
145 |
146 | if (collectionMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
147 | {
148 | movieList.Add(movie);
149 | }
150 | else
151 | {
152 | collectionMoviesMap[movie.CollectionName] = new List() { movie };
153 | }
154 | }
155 |
156 | if (movies.Count < pagesize)
157 | {
158 | break;
159 | }
160 |
161 | startIndex += pagesize;
162 | }
163 | }
164 |
165 | return collectionMoviesMap;
166 | }
167 |
168 | private void OnLibraryManagerItemUpdated(object sender, ItemChangeEventArgs e)
169 | {
170 | if (!(Plugin.Instance?.Configuration.EnableTmdbCollection ?? false))
171 | {
172 | return;
173 | }
174 |
175 | // Only support movies at this time
176 | if (e.Item is not Movie movie || e.Item.LocationType == LocationType.Virtual)
177 | {
178 | return;
179 | }
180 |
181 | if (string.IsNullOrEmpty(movie.CollectionName))
182 | {
183 | return;
184 | }
185 |
186 | // 判断 item 所在的媒体库是否是电影,并开启了 metashark 插件
187 | var typeOptions = _libraryManager.GetLibraryOptions(movie).TypeOptions;
188 | if (typeOptions.FirstOrDefault(x => x.Type == "Movie" && x.MetadataFetchers.Contains(Plugin.PluginName)) == null)
189 | {
190 | return;
191 | }
192 |
193 | _queuedTmdbCollection.Add(movie.CollectionName);
194 |
195 | // Restart the timer. After idling for 60 seconds it should trigger the callback. This is to avoid clobbering during a large library update.
196 | _timer.Change(60000, Timeout.Infinite);
197 | }
198 |
199 | private void OnTimerElapsed()
200 | {
201 | // Stop the timer until next update
202 | _timer.Change(Timeout.Infinite, Timeout.Infinite);
203 |
204 | var tmdbCollectionNames = _queuedTmdbCollection.ToArray();
205 | // Clear the queue now, TODO what if it crashes? Should it be cleared after it's done?
206 | _queuedTmdbCollection.Clear();
207 |
208 | var boxSets = GetAllBoxSetsFromLibrary();
209 | var movies = GetMoviesFromLibrary();
210 | foreach (var collectionName in tmdbCollectionNames)
211 | {
212 | if (movies.TryGetValue(collectionName, out var collectionMovies))
213 | {
214 | var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName);
215 | AddMoviesToCollection(collectionMovies, collectionName, boxSet).GetAwaiter().GetResult();
216 | }
217 | }
218 | }
219 |
220 | public Task StartAsync(CancellationToken cancellationToken)
221 | {
222 | _libraryManager.ItemUpdated += OnLibraryManagerItemUpdated;
223 | return Task.CompletedTask;
224 | }
225 |
226 | public Task StopAsync(CancellationToken cancellationToken)
227 | {
228 | _libraryManager.ItemUpdated -= OnLibraryManagerItemUpdated;
229 | return Task.CompletedTask;
230 | }
231 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Configuration/PluginConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Reflection;
3 | using MediaBrowser.Model.Plugins;
4 |
5 | namespace Jellyfin.Plugin.MetaShark.Configuration;
6 |
7 |
8 | ///
9 | /// Plugin configuration.
10 | ///
11 | public class PluginConfiguration : BasePluginConfiguration
12 | {
13 | public const int MAX_CAST_MEMBERS = 15;
14 | public const int MAX_SEARCH_RESULT = 5;
15 |
16 | ///
17 | /// 插件版本
18 | ///
19 | public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
20 |
21 | public string DoubanCookies { get; set; } = string.Empty;
22 | ///
23 | /// 豆瓣开启防封禁
24 | ///
25 | public bool EnableDoubanAvoidRiskControl { get; set; } = false;
26 | ///
27 | /// 豆瓣海报使用大图
28 | ///
29 | public bool EnableDoubanLargePoster { get; set; } = true;
30 | ///
31 | /// 豆瓣背景图使用原图
32 | ///
33 | public bool EnableDoubanBackdropRaw { get; set; } = false;
34 | ///
35 | /// 豆瓣图片代理地址
36 | ///
37 | public string DoubanImageProxyBaseUrl { get; set; } = string.Empty;
38 |
39 | ///
40 | /// 启用获取tmdb元数据
41 | ///
42 | public bool EnableTmdb { get; set; } = true;
43 |
44 | ///
45 | /// 启用显示tmdb搜索结果
46 | ///
47 | public bool EnableTmdbSearch { get; set; } = false;
48 |
49 | ///
50 | /// 启用tmdb获取背景图
51 | ///
52 | public bool EnableTmdbBackdrop { get; set; } = true;
53 |
54 | ///
55 | /// 启用tmdb获取商标
56 | ///
57 | public bool EnableTmdbLogo { get; set; } = true;
58 |
59 | ///
60 | /// 是否根据电影系列自动创建合集
61 | ///
62 | public bool EnableTmdbCollection { get; set; } = true;
63 | ///
64 | /// 是否获取tmdb分级信息
65 | ///
66 | public bool EnableTmdbOfficialRating { get; set; } = true;
67 | ///
68 | /// tmdb api key
69 | ///
70 | public string TmdbApiKey { get; set; } = string.Empty;
71 | ///
72 | /// tmdb api host
73 | ///
74 | public string TmdbHost { get; set; } = string.Empty;
75 | ///
76 | /// 代理服务器类型,0-禁用,1-http,2-https,3-socket5
77 | ///
78 | public string TmdbProxyType { get; set; } = string.Empty;
79 | ///
80 | /// 代理服务器host
81 | ///
82 | public string TmdbProxyPort { get; set; } = string.Empty;
83 | ///
84 | /// 代理服务器端口
85 | ///
86 | public string TmdbProxyHost { get; set; } = string.Empty;
87 |
88 |
89 | public IWebProxy GetTmdbWebProxy()
90 | {
91 |
92 | if (!string.IsNullOrEmpty(TmdbProxyType))
93 | {
94 | return new WebProxy($"{TmdbProxyType}://{TmdbProxyHost}:{TmdbProxyPort}", true);
95 | }
96 |
97 | return null;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Controllers/ApiController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Linq;
3 | using System.Net.Http;
4 | using System.IO;
5 | using System.Threading.Tasks;
6 | using MediaBrowser.Common.Extensions;
7 | using Microsoft.AspNetCore.Authorization;
8 | using Microsoft.AspNetCore.Mvc;
9 | using MediaBrowser.Common.Net;
10 | using Jellyfin.Plugin.MetaShark.Api;
11 | using Jellyfin.Plugin.MetaShark.Model;
12 |
13 | namespace Jellyfin.Plugin.MetaShark.Controllers
14 | {
15 | [ApiController]
16 | [AllowAnonymous]
17 | [Route("/plugin/metashark")]
18 | public class ApiController : ControllerBase
19 | {
20 | private readonly DoubanApi _doubanApi;
21 | private readonly IHttpClientFactory _httpClientFactory;
22 |
23 | ///
24 | /// Initializes a new instance of the class.
25 | ///
26 | /// The .
27 | public ApiController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
28 | {
29 | this._httpClientFactory = httpClientFactory;
30 | this._doubanApi = doubanApi;
31 | }
32 |
33 |
34 | ///
35 | /// 代理访问图片.
36 | ///
37 | [Route("proxy/image")]
38 | [HttpGet]
39 | public async Task ProxyImage(string url)
40 | {
41 |
42 | if (string.IsNullOrEmpty(url))
43 | {
44 | throw new ResourceNotFoundException();
45 | }
46 |
47 | HttpResponseMessage response;
48 | var httpClient = GetHttpClient();
49 | using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
50 | {
51 | requestMessage.Headers.Add("User-Agent", DoubanApi.HTTP_USER_AGENT);
52 | requestMessage.Headers.Add("Referer", DoubanApi.HTTP_REFERER);
53 |
54 | response = await httpClient.SendAsync(requestMessage);
55 | }
56 | var stream = await response.Content.ReadAsStreamAsync();
57 |
58 | Response.StatusCode = (int)response.StatusCode;
59 | if (response.Content.Headers.ContentType != null)
60 | {
61 | Response.ContentType = response.Content.Headers.ContentType.ToString();
62 | }
63 | Response.ContentLength = response.Content.Headers.ContentLength;
64 |
65 | foreach (var header in response.Headers)
66 | {
67 | Response.Headers.Add(header.Key, header.Value.First());
68 | }
69 |
70 | return stream;
71 | }
72 |
73 | ///
74 | /// 检查豆瓣cookie是否失效.
75 | ///
76 | [Route("douban/checklogin")]
77 | [HttpGet]
78 | public async Task CheckDoubanLogin()
79 | {
80 | var loginInfo = await this._doubanApi.GetLoginInfoAsync(CancellationToken.None).ConfigureAwait(false);
81 | return new ApiResult(loginInfo.IsLogined ? 1 : 0, loginInfo.Name);
82 | }
83 |
84 |
85 | private HttpClient GetHttpClient()
86 | {
87 | var client = _httpClientFactory.CreateClient(NamedClient.Default);
88 | return client;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/ElementExtension.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace Jellyfin.Plugin.MetaShark.Core
9 | {
10 | public static class ElementExtension
11 | {
12 | public static string? GetText(this IElement el, string css)
13 | {
14 | var node = el.QuerySelector(css);
15 | if (node != null)
16 | {
17 | return node.Text().Trim();
18 | }
19 |
20 | return null;
21 | }
22 |
23 | public static string? GetHtml(this IElement el, string css)
24 | {
25 | var node = el.QuerySelector(css);
26 | if (node != null)
27 | {
28 | return node.Html().Trim();
29 | }
30 |
31 | return null;
32 | }
33 |
34 | public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
35 | {
36 | var node = el.QuerySelector(css);
37 | if (node != null)
38 | {
39 | return node.Text().Trim();
40 | }
41 |
42 | return defaultVal;
43 | }
44 |
45 | public static string? GetAttr(this IElement el, string css, string attr)
46 | {
47 | var node = el.QuerySelector(css);
48 | if (node != null)
49 | {
50 | var attrVal = node.GetAttribute(attr);
51 | return attrVal != null ? attrVal.Trim() : null;
52 | }
53 |
54 | return null;
55 | }
56 |
57 | public static string? GetAttrOrDefault(this IElement el, string css, string attr, string defaultVal = "")
58 | {
59 | var node = el.QuerySelector(css);
60 | if (node != null)
61 | {
62 | var attrVal = node.GetAttribute(attr);
63 | return attrVal != null ? attrVal.Trim() : defaultVal;
64 | }
65 |
66 | return defaultVal;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/JsonExtension.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.Json;
7 | using System.Threading.Tasks;
8 |
9 | namespace Jellyfin.Plugin.MetaShark.Core
10 | {
11 | public static class JsonExtension
12 | {
13 | public static string ToJson(this object obj)
14 | {
15 | if (obj == null) return string.Empty;
16 |
17 | return JsonSerializer.Serialize(obj);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/ListExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Core
8 | {
9 | public static class ListExtension
10 | {
11 | public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable self)
12 | => self.Select((item, index) => (item, index));
13 |
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/RegexExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.Eventing.Reader;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 | using System.Threading.Tasks;
8 | using StringMetric;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Core
11 | {
12 | public static class RegexExtension
13 | {
14 | public static string FirstMatchGroup(this Regex reg, string text, string defalutVal = "")
15 | {
16 | var match = reg.Match(text);
17 | if (match.Success && match.Groups.Count > 1)
18 | {
19 | return match.Groups[1].Value.Trim();
20 | }
21 |
22 | return defalutVal;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/StringExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics.Eventing.Reader;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 | using System.Threading.Tasks;
8 | using StringMetric;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Core
11 | {
12 | public static class StringExtension
13 | {
14 | public static long ToLong(this string s)
15 | {
16 | long val;
17 | if (long.TryParse(s, out val))
18 | {
19 | return val;
20 | }
21 |
22 | return 0;
23 | }
24 |
25 | public static int ToInt(this string s)
26 | {
27 | int val;
28 | if (int.TryParse(s, out val))
29 | {
30 | return val;
31 | }
32 |
33 | return 0;
34 | }
35 |
36 | public static float ToFloat(this string s)
37 | {
38 | float val;
39 | if (float.TryParse(s, out val))
40 | {
41 | return val;
42 | }
43 |
44 | return 0.0f;
45 | }
46 |
47 | public static bool IsChinese(this string s)
48 | {
49 | Regex chineseReg = new Regex(@"[\u4e00-\u9fa5:]{1,}", RegexOptions.Compiled);
50 | return chineseReg.IsMatch(s.Replace(" ", string.Empty).Trim());
51 | }
52 |
53 | public static bool HasChinese(this string s)
54 | {
55 | Regex chineseReg = new Regex(@"[\u4e00-\u9fa5]", RegexOptions.Compiled);
56 | return chineseReg.Match(s).Success;
57 | }
58 |
59 | public static bool IsSameLanguage(this string s1, string s2)
60 | {
61 | return s1.IsChinese() == s2.IsChinese();
62 | }
63 |
64 | public static double Distance(this string s1, string s2)
65 | {
66 | var jw = new JaroWinkler();
67 |
68 | return jw.Similarity(s1, s2);
69 | }
70 |
71 | public static string GetMatchGroup(this string text, Regex reg)
72 | {
73 | var match = reg.Match(text);
74 | if (match.Success && match.Groups.Count > 1)
75 | {
76 | return match.Groups[1].Value.Trim();
77 | }
78 |
79 | return string.Empty;
80 | }
81 |
82 | public static bool IsNumericString(this string str)
83 | {
84 | return str.All(char.IsDigit);
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/StringMetric/JaroWinkler.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright 2016 feature[23]
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | using System;
26 | using System.Linq;
27 | // ReSharper disable SuggestVarOrType_Elsewhere
28 | // ReSharper disable LoopCanBeConvertedToQuery
29 |
30 | namespace StringMetric
31 | {
32 | /// The Jaro–Winkler distance metric is designed and best suited for short
33 | /// strings such as person names, and to detect typos; it is (roughly) a
34 | /// variation of Damerau-Levenshtein, where the substitution of 2 close
35 | /// characters is considered less important then the substitution of 2 characters
36 | /// that a far from each other.
37 | /// Jaro-Winkler was developed in the area of record linkage (duplicate
38 | /// detection) (Winkler, 1990). It returns a value in the interval [0.0, 1.0].
39 | /// The distance is computed as 1 - Jaro-Winkler similarity.
40 | public class JaroWinkler
41 | {
42 | private const double DEFAULT_THRESHOLD = 0.7;
43 | private const int THREE = 3;
44 | private const double JW_COEF = 0.1;
45 |
46 | ///
47 | /// The current value of the threshold used for adding the Winkler bonus. The default value is 0.7.
48 | ///
49 | private double Threshold { get; }
50 |
51 | ///
52 | /// Creates a new instance with default threshold (0.7)
53 | ///
54 | public JaroWinkler()
55 | {
56 | Threshold = DEFAULT_THRESHOLD;
57 | }
58 |
59 | ///
60 | /// Creates a new instance with given threshold to determine when Winkler bonus should
61 | /// be used. Set threshold to a negative value to get the Jaro distance.
62 | ///
63 | ///
64 | public JaroWinkler(double threshold)
65 | {
66 | Threshold = threshold;
67 | }
68 |
69 | ///
70 | /// Compute Jaro-Winkler similarity.
71 | ///
72 | /// The first string to compare.
73 | /// The second string to compare.
74 | /// The Jaro-Winkler similarity in the range [0, 1]
75 | /// If s1 or s2 is null.
76 | public double Similarity(string s1, string s2)
77 | {
78 | if (s1 == null)
79 | {
80 | throw new ArgumentNullException(nameof(s1));
81 | }
82 |
83 | if (s2 == null)
84 | {
85 | throw new ArgumentNullException(nameof(s2));
86 | }
87 |
88 | if (s1.Equals(s2))
89 | {
90 | return 1f;
91 | }
92 |
93 | int[] mtp = Matches(s1, s2);
94 | float m = mtp[0];
95 | if (m == 0)
96 | {
97 | return 0f;
98 | }
99 | double j = ((m / s1.Length + m / s2.Length + (m - mtp[1]) / m))
100 | / THREE;
101 | double jw = j;
102 |
103 | if (j > Threshold)
104 | {
105 | jw = j + Math.Min(JW_COEF, 1.0 / mtp[THREE]) * mtp[2] * (1 - j);
106 | }
107 | return jw;
108 | }
109 |
110 | ///
111 | /// Return 1 - similarity.
112 | ///
113 | /// The first string to compare.
114 | /// The second string to compare.
115 | /// 1 - similarity
116 | /// If s1 or s2 is null.
117 | public double Distance(string s1, string s2)
118 | => 1.0 - Similarity(s1, s2);
119 |
120 | private static int[] Matches(string s1, string s2)
121 | {
122 | string max, min;
123 | if (s1.Length > s2.Length)
124 | {
125 | max = s1;
126 | min = s2;
127 | }
128 | else
129 | {
130 | max = s2;
131 | min = s1;
132 | }
133 | int range = Math.Max(max.Length / 2 - 1, 0);
134 |
135 | //int[] matchIndexes = new int[min.Length];
136 | //Arrays.fill(matchIndexes, -1);
137 | int[] match_indexes = Enumerable.Repeat(-1, min.Length).ToArray();
138 |
139 | bool[] match_flags = new bool[max.Length];
140 | int matches = 0;
141 | for (int mi = 0; mi < min.Length; mi++)
142 | {
143 | char c1 = min[mi];
144 | for (int xi = Math.Max(mi - range, 0),
145 | xn = Math.Min(mi + range + 1, max.Length); xi < xn; xi++)
146 | {
147 | if (!match_flags[xi] && c1 == max[xi])
148 | {
149 | match_indexes[mi] = xi;
150 | match_flags[xi] = true;
151 | matches++;
152 | break;
153 | }
154 | }
155 | }
156 | char[] ms1 = new char[matches];
157 | char[] ms2 = new char[matches];
158 | for (int i = 0, si = 0; i < min.Length; i++)
159 | {
160 | if (match_indexes[i] != -1)
161 | {
162 | ms1[si] = min[i];
163 | si++;
164 | }
165 | }
166 | for (int i = 0, si = 0; i < max.Length; i++)
167 | {
168 | if (match_flags[i])
169 | {
170 | ms2[si] = max[i];
171 | si++;
172 | }
173 | }
174 | int transpositions = 0;
175 | for (int mi = 0; mi < ms1.Length; mi++)
176 | {
177 | if (ms1[mi] != ms2[mi])
178 | {
179 | transpositions++;
180 | }
181 | }
182 | int prefix = 0;
183 | for (int mi = 0; mi < min.Length; mi++)
184 | {
185 | if (s1[mi] == s2[mi])
186 | {
187 | prefix++;
188 | }
189 | else
190 | {
191 | break;
192 | }
193 | }
194 | return new[] { matches, transpositions / 2, prefix, max.Length };
195 | }
196 | }
197 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Core/Utils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Core
8 | {
9 | public static class Utils
10 | {
11 | public static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
12 | {
13 | // Unix timestamp is seconds past epoch
14 | DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
15 | dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
16 | return dateTime;
17 | }
18 |
19 | ///
20 | /// 转换数字
21 | ///
22 | public static int? ChineseNumberToInt(string str)
23 | {
24 | if (string.IsNullOrEmpty(str)) return null;
25 |
26 | var chineseNumberMap = new Dictionary() {
27 | {'一', '1'},
28 | {'二', '2'},
29 | {'三', '3'},
30 | {'四', '4'},
31 | {'五', '5'},
32 | {'六', '6'},
33 | {'七', '7'},
34 | {'八', '8'},
35 | {'九', '9'},
36 | {'零', '0'},
37 | };
38 |
39 | var numberArr = str.ToCharArray().Select(x => chineseNumberMap.ContainsKey(x) ? chineseNumberMap[x] : x).ToArray();
40 | var newNumberStr = new string(numberArr);
41 | if (int.TryParse(new string(numberArr), out var number))
42 | {
43 | return number;
44 | }
45 |
46 | return null;
47 | }
48 |
49 | ///
50 | /// 转换中文数字
51 | ///
52 | public static string? ToChineseNumber(int? number)
53 | {
54 | if (number is null) return null;
55 |
56 | var chineseNumberMap = new Dictionary() {
57 | {'1','一'},
58 | {'2','二'},
59 | {'3','三'},
60 | {'4','四'},
61 | {'5','五'},
62 | {'6','六'},
63 | {'7','七'},
64 | {'8','八'},
65 | {'9','九'},
66 | {'0','零'},
67 | };
68 |
69 | var numberArr = $"{number}".ToCharArray().Select(x => chineseNumberMap.ContainsKey(x) ? chineseNumberMap[x] : x).ToArray();
70 | return new string(numberArr);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/ILRepack.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Jellyfin.Plugin.MetaShark
5 | False
6 | true
7 | enable
8 | AllEnabledByDefault
9 | true
10 |
11 |
12 | False
13 |
14 |
15 | False
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 | all
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/ApiResult.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace Jellyfin.Plugin.MetaShark.Model;
4 |
5 | public class ApiResult
6 | {
7 | [JsonPropertyName("code")]
8 | public int Code { get; set; }
9 | [JsonPropertyName("msg")]
10 | public string Msg { get; set; } = string.Empty;
11 |
12 | public ApiResult(int code, string msg = "")
13 | {
14 | this.Code = code;
15 | this.Msg = msg;
16 | }
17 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/DoubanLoginInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.Linq;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Model
8 | {
9 | public class DoubanLoginInfo
10 | {
11 | public string Name { get; set; }
12 | public bool IsLogined { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/DoubanSubject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.Linq;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Model
8 | {
9 | public class DoubanSubject
10 | {
11 | // "name": "哈利·波特与魔法石",
12 | public string Name { get; set; }
13 | // "originalName": "Harry Potter and the Sorcerer's Stone",
14 | public string OriginalName { get; set; }
15 | // "rating": "9.1",
16 | public float Rating { get; set; }
17 | // "img": "https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2614949805.webp",
18 | public string Img { get; set; }
19 | // "sid": "1295038",
20 | public string Sid { get; set; }
21 | // "year": "2001",
22 | public int Year { get; set; }
23 | // "director": "克里斯·哥伦布",
24 | public string Director { get; set; }
25 | // "writer": "史蒂夫·克洛夫斯 / J·K·罗琳",
26 | public string Writer { get; set; }
27 | // "actor": "丹尼尔·雷德克里夫 / 艾玛·沃森 / 鲁伯特·格林特 / 艾伦·瑞克曼 / 玛吉·史密斯 / 更多...",
28 | public string Actor { get; set; }
29 | // "genre": "奇幻 / 冒险",
30 | public string Genre { get; set; }
31 | // 电影/电视剧
32 | public string Category { get; set; }
33 | // "site": "www.harrypotter.co.uk",
34 | public string Site { get; set; }
35 | // "country": "美国 / 英国",
36 | public string Country { get; set; }
37 | // "language": "英语",
38 | public string Language { get; set; }
39 | // "screen": "2002-01-26(中国大陆) / 2020-08-14(中国大陆重映) / 2001-11-04(英国首映) / 2001-11-16(美国)",
40 | public string Screen { get; set; }
41 | public DateTime? ScreenTime
42 | {
43 | get
44 | {
45 | if (Screen == null) return null;
46 |
47 | var items = Screen.Split("/");
48 | if (items.Length >= 0)
49 | {
50 | var item = items[0].Split("(")[0];
51 | DateTime result;
52 | DateTime.TryParseExact(item, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out result);
53 | return result;
54 | }
55 | return null;
56 | }
57 | }
58 | // "duration": "152分钟 / 159分钟(加长版)",
59 | public string Duration { get; set; }
60 | // "subname": "哈利波特1:神秘的魔法石(港/台) / 哈1 / Harry Potter and the Philosopher's Stone",
61 | public string Subname { get; set; }
62 | // "imdb": "tt0241527"
63 | public string Imdb { get; set; }
64 | public string Intro { get; set; }
65 |
66 | public List Celebrities { get; set; }
67 |
68 | [JsonIgnore]
69 | public List LimitDirectorCelebrities
70 | {
71 | get
72 | {
73 | // 限制导演最多返回5个
74 | var limitCelebrities = new List();
75 | if (Celebrities == null || Celebrities.Count == 0)
76 | {
77 | return limitCelebrities;
78 | }
79 |
80 | limitCelebrities.AddRange(Celebrities.Where(x => x.RoleType == MediaBrowser.Model.Entities.PersonType.Director && !string.IsNullOrEmpty(x.Name)).Take(5));
81 | limitCelebrities.AddRange(Celebrities.Where(x => x.RoleType != MediaBrowser.Model.Entities.PersonType.Director && !string.IsNullOrEmpty(x.Name)));
82 |
83 | return limitCelebrities;
84 | }
85 | }
86 |
87 | [JsonIgnore]
88 | public string ImgMiddle
89 | {
90 | get
91 | {
92 | return this.Img.Replace("s_ratio_poster", "m");
93 | }
94 | }
95 |
96 | [JsonIgnore]
97 | public string ImgLarge
98 | {
99 | get
100 | {
101 | return this.Img.Replace("s_ratio_poster", "l");
102 | }
103 | }
104 |
105 | [JsonIgnore]
106 | public string[] Genres
107 | {
108 | get
109 | {
110 | return this.Genre.Split("/").Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToArray();
111 | }
112 | }
113 |
114 |
115 | [JsonIgnore]
116 | public string PrimaryLanguageCode
117 | {
118 | get
119 | {
120 | var languageCodeMap = new Dictionary() {
121 | { "日语", "ja" },
122 | { "法语", "fr" },
123 | { "德语", "de" },
124 | { "俄语", "ru" },
125 | { "韩语", "ko" },
126 | { "泰语", "th" },
127 | { "泰米尔语", "ta" },
128 | };
129 | var primaryLanguage = this.Language.Split("/").Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).FirstOrDefault();
130 | if (!string.IsNullOrEmpty(primaryLanguage))
131 | {
132 | if (languageCodeMap.TryGetValue(primaryLanguage, out var lang))
133 | {
134 | return lang;
135 | }
136 | }
137 |
138 | return string.Empty;
139 | }
140 | }
141 | }
142 |
143 | public class DoubanCelebrity
144 | {
145 | public string Id { get; set; }
146 | public string Name { get; set; }
147 | public string Img { get; set; }
148 | public string Role { get; set; }
149 |
150 | public string Intro { get; set; }
151 | public string Gender { get; set; }
152 | public string Constellation { get; set; }
153 | public string Birthdate { get; set; }
154 | public string Enddate { get; set; }
155 | public string Birthplace { get; set; }
156 | public string NickName { get; set; }
157 | public string EnglishName { get; set; }
158 | public string Imdb { get; set; }
159 | public string Site { get; set; }
160 |
161 | private string _roleType;
162 | public string RoleType
163 | {
164 | get
165 | {
166 | if (string.IsNullOrEmpty(this._roleType))
167 | {
168 | return this.Role.Contains("导演", StringComparison.Ordinal) ? MediaBrowser.Model.Entities.PersonType.Director : MediaBrowser.Model.Entities.PersonType.Actor;
169 | }
170 |
171 | return this._roleType.Contains("导演", StringComparison.Ordinal) ? MediaBrowser.Model.Entities.PersonType.Director : MediaBrowser.Model.Entities.PersonType.Actor;
172 | }
173 | set
174 | {
175 | _roleType = value;
176 | }
177 | }
178 |
179 | public string? DisplayOriginalName
180 | {
181 | get
182 | {
183 | // 外国人才显示英文名
184 | if (Name.Contains("·") && Birthplace != null && !Birthplace.Contains("中国"))
185 | {
186 | return EnglishName;
187 | }
188 |
189 | return null;
190 | }
191 | }
192 |
193 | [JsonIgnore]
194 | public string ImgMiddle
195 | {
196 | get
197 | {
198 | return this.Img.Replace("/raw/", "/m/").Replace("/s_ratio_poster/", "/m/");
199 | }
200 | }
201 |
202 | }
203 |
204 | public class DoubanPhoto
205 | {
206 | public string Id { get; set; }
207 | public string Small { get; set; }
208 | public string Medium { get; set; }
209 | public string Large { get; set; }
210 | ///
211 | /// 原始图片url,必须带referer访问
212 | ///
213 | public string Raw { get; set; }
214 | public string Size { get; set; }
215 | public int? Width { get; set; }
216 | public int? Height { get; set; }
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/DoubanSuggest.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 | using System.Text.RegularExpressions;
3 | using Jellyfin.Plugin.MetaShark.Core;
4 |
5 | namespace Jellyfin.Plugin.MetaShark.Model;
6 |
7 | public class DoubanSuggest
8 | {
9 | [JsonPropertyName("title")]
10 | public string Title { get; set; } = string.Empty;
11 | [JsonPropertyName("url")]
12 | public string Url { get; set; } = string.Empty;
13 | [JsonPropertyName("year")]
14 | public string Year { get; set; } = string.Empty;
15 | [JsonPropertyName("type")]
16 | public string Type { get; set; } = string.Empty;
17 |
18 |
19 | public string Sid
20 | {
21 | get
22 | {
23 | var regSid = new Regex(@"subject\/(\d+?)\/", RegexOptions.Compiled);
24 | return this.Url.GetMatchGroup(regSid);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/DoubanSuggestResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace Jellyfin.Plugin.MetaShark.Model;
5 |
6 | public class DoubanSuggestResult
7 | {
8 | [JsonPropertyName("cards")]
9 | public List? Cards { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/GuessInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Model
8 | {
9 | public class GuessInfo
10 | {
11 | public int? episodeNumber { get; set; }
12 |
13 | public int? seasonNumber { get; set; }
14 |
15 | public string? Name { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/MetaSource.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Jellyfin.Plugin.MetaShark.Model
8 | {
9 |
10 | public enum MetaSource
11 | {
12 | Douban,
13 | Tmdb,
14 | None
15 | }
16 |
17 | public static class MetaSourceExtensions
18 | {
19 | public static MetaSource ToMetaSource(this string? str)
20 | {
21 | if (str == null)
22 | {
23 | return MetaSource.None;
24 | }
25 |
26 | if (str.ToLower().StartsWith("douban"))
27 | {
28 | return MetaSource.Douban;
29 | }
30 |
31 | if (str.ToLower().StartsWith("tmdb"))
32 | {
33 | return MetaSource.Tmdb;
34 | }
35 |
36 | return MetaSource.None;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/OmdbItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.Json.Serialization;
6 | using System.Threading.Tasks;
7 |
8 | namespace Jellyfin.Plugin.MetaShark.Model
9 | {
10 | public class OmdbItem
11 | {
12 | [JsonPropertyName("imdbID")]
13 | public string ImdbID { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Model/ParseNameResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Specialized;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.Json.Serialization;
7 | using System.Threading.Tasks;
8 | using MediaBrowser.Controller.Providers;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.Model
11 | {
12 | public class ParseNameResult : ItemLookupInfo
13 | {
14 | public string? ChineseName { get; set; } = null;
15 |
16 | ///
17 | /// 可能会解析不对,最好只在动画SP中才使用
18 | ///
19 | public string? EpisodeName { get; set; } = null;
20 |
21 | private string _animeType = string.Empty;
22 | public string AnimeType
23 | {
24 | get
25 | {
26 | return _animeType.ToUpper();
27 | }
28 | set
29 | {
30 | _animeType = value;
31 | }
32 | }
33 |
34 | public bool IsSpecial
35 | {
36 | get
37 | {
38 | return !string.IsNullOrEmpty(AnimeType) && AnimeType.ToUpper() == "SP";
39 | }
40 | }
41 |
42 | public bool IsExtra
43 | {
44 | get
45 | {
46 | return !string.IsNullOrEmpty(AnimeType) && AnimeType.ToUpper() != "SP";
47 | }
48 | }
49 |
50 | public string? PaddingZeroIndexNumber
51 | {
52 | get
53 | {
54 | if (!IndexNumber.HasValue)
55 | {
56 | return null;
57 | }
58 |
59 | return $"{IndexNumber:00}";
60 | }
61 | }
62 |
63 | public string ExtraName
64 | {
65 | get
66 | {
67 | if (IndexNumber.HasValue)
68 | {
69 | return $"{AnimeType} {PaddingZeroIndexNumber}";
70 | }
71 | else
72 | {
73 | return $"{AnimeType}";
74 | }
75 | }
76 | }
77 |
78 | public string SpecialName
79 | {
80 | get
81 | {
82 | if (!string.IsNullOrEmpty(EpisodeName) && IndexNumber.HasValue)
83 | {
84 | return $"{EpisodeName} {IndexNumber}";
85 | }
86 | else if (!string.IsNullOrEmpty(EpisodeName))
87 | {
88 | return EpisodeName;
89 | }
90 |
91 | return Name;
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Plugin.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using Jellyfin.Plugin.MetaShark.Configuration;
5 | using MediaBrowser.Common.Configuration;
6 | using MediaBrowser.Common.Plugins;
7 | using MediaBrowser.Controller;
8 | using MediaBrowser.Model.Plugins;
9 | using MediaBrowser.Model.Serialization;
10 | using Microsoft.AspNetCore.Http;
11 |
12 | namespace Jellyfin.Plugin.MetaShark;
13 |
14 | ///
15 | /// The main plugin.
16 | ///
17 | public class Plugin : BasePlugin, IHasWebPages
18 | {
19 | ///
20 | /// Gets the provider name.
21 | ///
22 | public const string PluginName = "MetaShark";
23 |
24 | ///
25 | /// Gets the provider id.
26 | ///
27 | public const string ProviderId = "MetaSharkID";
28 |
29 |
30 | private readonly IServerApplicationHost _appHost;
31 |
32 | ///
33 | /// Initializes a new instance of the class.
34 | ///
35 | /// Instance of the interface.
36 | /// Instance of the interface.
37 | public Plugin(IServerApplicationHost appHost, IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
38 | : base(applicationPaths, xmlSerializer)
39 | {
40 | this._appHost = appHost;
41 | Plugin.Instance = this;
42 | }
43 |
44 | ///
45 | public override string Name => PluginName;
46 |
47 | ///
48 | public override Guid Id => Guid.Parse("9A19103F-16F7-4668-BE54-9A1E7A4F7556");
49 |
50 | ///
51 | /// Gets the current plugin instance.
52 | ///
53 | public static Plugin? Instance { get; private set; }
54 |
55 | ///
56 | public IEnumerable GetPages()
57 | {
58 | return new[]
59 | {
60 | new PluginPageInfo
61 | {
62 | Name = this.Name,
63 | EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace),
64 | },
65 | };
66 | }
67 |
68 | public string GetLocalApiBaseUrl()
69 | {
70 | return this._appHost.GetLocalApiUrl("127.0.0.1", "http");
71 | }
72 |
73 | public string GetApiBaseUrl(HttpRequest request)
74 | {
75 | int? requestPort = request.Host.Port;
76 | if (requestPort == null
77 | || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
78 | || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
79 | {
80 | requestPort = -1;
81 | }
82 |
83 | return this._appHost.GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/BoxSetImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using MediaBrowser.Controller.Entities;
3 | using MediaBrowser.Controller.Entities.Movies;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using MediaBrowser.Model.Dto;
7 | using MediaBrowser.Model.Entities;
8 | using MediaBrowser.Model.Extensions;
9 | using MediaBrowser.Model.Providers;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.Extensions.Logging;
12 | using System;
13 | using System.Collections.Generic;
14 | using System.Globalization;
15 | using System.Linq;
16 | using System.Net.Http;
17 | using System.Threading;
18 | using System.Threading.Tasks;
19 |
20 | namespace Jellyfin.Plugin.MetaShark.Providers
21 | {
22 | ///
23 | /// BoxSet image provider powered by TMDb.
24 | ///
25 | public class BoxSetImageProvider : BaseProvider, IRemoteImageProvider
26 | {
27 | public BoxSetImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
28 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
29 | {
30 | }
31 |
32 | ///
33 | public string Name => Plugin.PluginName;
34 |
35 | ///
36 | public bool Supports(BaseItem item)
37 | {
38 | return item is BoxSet;
39 | }
40 |
41 | ///
42 | public IEnumerable GetSupportedImages(BaseItem item) =>
43 | [
44 | ImageType.Primary,
45 | ImageType.Backdrop,
46 | ImageType.Thumb
47 | ];
48 |
49 | ///
50 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
51 | {
52 | var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
53 | this.Log($"GetBoxSetImages of [name]: {item.Name} [tmdbId]: {tmdbId}");
54 |
55 | if (tmdbId <= 0)
56 | {
57 | return Enumerable.Empty();
58 | }
59 |
60 | var language = item.GetPreferredMetadataLanguage();
61 |
62 | // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
63 | var collection = await this._tmdbApi.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false);
64 |
65 | if (collection?.Images is null)
66 | {
67 | return Enumerable.Empty();
68 | }
69 |
70 | var posters = collection.Images.Posters;
71 | var backdrops = collection.Images.Backdrops;
72 | var remoteImages = new List(posters.Count + backdrops.Count);
73 | remoteImages.AddRange(posters.Select(x => new RemoteImageInfo {
74 | ProviderName = this.Name,
75 | Url = this._tmdbApi.GetPosterUrl(x.FilePath),
76 | Type = ImageType.Primary,
77 | CommunityRating = x.VoteAverage,
78 | VoteCount = x.VoteCount,
79 | Width = x.Width,
80 | Height = x.Height,
81 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
82 | RatingType = RatingType.Score,
83 | }));
84 |
85 | remoteImages.AddRange(backdrops.Select(x => new RemoteImageInfo {
86 | ProviderName = this.Name,
87 | Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
88 | Type = ImageType.Backdrop,
89 | CommunityRating = x.VoteAverage,
90 | VoteCount = x.VoteCount,
91 | Width = x.Width,
92 | Height = x.Height,
93 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
94 | RatingType = RatingType.Score,
95 | }));
96 |
97 | return remoteImages.OrderByLanguageDescending(language);
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/BoxSetProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Jellyfin.Data.Enums;
9 | using Jellyfin.Plugin.MetaShark.Api;
10 | using MediaBrowser.Controller.Entities;
11 | using MediaBrowser.Controller.Entities.Movies;
12 | using MediaBrowser.Controller.Library;
13 | using MediaBrowser.Controller.Providers;
14 | using MediaBrowser.Model.Entities;
15 | using MediaBrowser.Model.Providers;
16 | using Microsoft.AspNetCore.Http;
17 | using Microsoft.Extensions.Logging;
18 |
19 | namespace Jellyfin.Plugin.MetaShark.Providers
20 | {
21 | ///
22 | /// BoxSet provider powered by TMDb.
23 | ///
24 | public class BoxSetProvider : BaseProvider, IRemoteMetadataProvider
25 | {
26 | public BoxSetProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
27 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
28 | {
29 | }
30 |
31 | public string Name => Plugin.PluginName;
32 |
33 | ///
34 | public async Task> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
35 | {
36 | var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
37 | var language = searchInfo.MetadataLanguage;
38 |
39 | if (tmdbId > 0)
40 | {
41 | var collection = await _tmdbApi.GetCollectionAsync(tmdbId, language, language, cancellationToken).ConfigureAwait(false);
42 |
43 | if (collection is null)
44 | {
45 | return Enumerable.Empty();
46 | }
47 |
48 | var result = new RemoteSearchResult
49 | {
50 | Name = collection.Name,
51 | SearchProviderName = Name
52 | };
53 |
54 | if (collection.Images is not null)
55 | {
56 | result.ImageUrl = _tmdbApi.GetPosterUrl(collection.PosterPath);
57 | }
58 |
59 | result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
60 |
61 | return new[] { result };
62 | }
63 |
64 | var collectionSearchResults = await _tmdbApi.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
65 |
66 | var collections = new RemoteSearchResult[collectionSearchResults.Count];
67 | for (var i = 0; i < collectionSearchResults.Count; i++)
68 | {
69 | var result = collectionSearchResults[i];
70 | var collection = new RemoteSearchResult
71 | {
72 | Name = result.Name,
73 | SearchProviderName = Name,
74 | ImageUrl = _tmdbApi.GetPosterUrl(result.PosterPath)
75 | };
76 | collection.SetProviderId(MetadataProvider.Tmdb, result.Id.ToString(CultureInfo.InvariantCulture));
77 |
78 | collections[i] = collection;
79 | }
80 |
81 | return collections;
82 | }
83 |
84 | ///
85 | public async Task> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken)
86 | {
87 | var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
88 | var language = info.MetadataLanguage;
89 | this.Log($"GetBoxSetMetadata of [name]: {info.Name} [tmdbId]: {tmdbId} EnableTmdb: {config.EnableTmdb}");
90 |
91 | // We don't already have an Id, need to fetch it
92 | if (tmdbId <= 0)
93 | {
94 | // ParseName is required here.
95 | // Caller provides the filename with extension stripped and NOT the parsed filename
96 | var parsedName = _libraryManager.ParseName(info.Name);
97 | var searchResults = await _tmdbApi.SearchCollectionAsync(parsedName.Name, language, cancellationToken).ConfigureAwait(false);
98 |
99 | if (searchResults is not null && searchResults.Count > 0)
100 | {
101 | tmdbId = searchResults.FirstOrDefault(x => x.Name == info.Name)?.Id ?? 0;
102 | if (tmdbId <= 0)
103 | {
104 | tmdbId = searchResults[0].Id;
105 | }
106 | }
107 | }
108 |
109 | var result = new MetadataResult();
110 |
111 | if (tmdbId > 0)
112 | {
113 | var collection = await _tmdbApi.GetCollectionAsync(tmdbId, language, language, cancellationToken).ConfigureAwait(false);
114 |
115 | if (collection is not null)
116 | {
117 | var item = new BoxSet
118 | {
119 | Name = collection.Name,
120 | Overview = collection.Overview,
121 | };
122 |
123 | var oldBotSet = _libraryManager.GetItemList(new InternalItemsQuery
124 | {
125 | IncludeItemTypes = new[] { BaseItemKind.BoxSet },
126 | CollapseBoxSetItems = false,
127 | Recursive = true
128 | }).Select(b => b as BoxSet).FirstOrDefault(x => x.Name == collection.Name);
129 | if (oldBotSet != null)
130 | {
131 | item.LinkedChildren = oldBotSet.LinkedChildren;
132 | }
133 | item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
134 |
135 | result.HasMetadata = true;
136 | result.Item = item;
137 | }
138 | }
139 |
140 | return result;
141 | }
142 |
143 | }
144 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/EpisodeImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using MediaBrowser.Controller.Entities;
3 | using MediaBrowser.Controller.Entities.TV;
4 | using MediaBrowser.Controller.Library;
5 | using MediaBrowser.Controller.Providers;
6 | using MediaBrowser.Model.Entities;
7 | using MediaBrowser.Model.Providers;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Logging;
10 | using System;
11 | using System.Collections.Generic;
12 | using System.Globalization;
13 | using System.Linq;
14 | using System.Net.Http;
15 | using System.Threading;
16 | using System.Threading.Tasks;
17 |
18 | namespace Jellyfin.Plugin.MetaShark.Providers
19 | {
20 | public class EpisodeImageProvider : BaseProvider, IRemoteImageProvider
21 | {
22 | public EpisodeImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
23 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
24 | {
25 | }
26 |
27 | ///
28 | public string Name => Plugin.PluginName;
29 |
30 | ///
31 | public bool Supports(BaseItem item) => item is Episode;
32 |
33 | ///
34 | public IEnumerable GetSupportedImages(BaseItem item)
35 | {
36 | yield return ImageType.Primary;
37 | }
38 |
39 | ///
40 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
41 | {
42 | this.Log($"GetEpisodeImages of [name]: {item.Name} number: {item.IndexNumber} ParentIndexNumber: {item.ParentIndexNumber}");
43 |
44 | var episode = (MediaBrowser.Controller.Entities.TV.Episode)item;
45 | var series = episode.Series;
46 |
47 | var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
48 |
49 | if (seriesTmdbId <= 0)
50 | {
51 | this.Log($"[GetEpisodeImages] The seriesTmdbId is empty!");
52 | return Enumerable.Empty();
53 | }
54 |
55 | var seasonNumber = episode.ParentIndexNumber;
56 | var episodeNumber = episode.IndexNumber;
57 |
58 | if (seasonNumber is null or 0 || episodeNumber is null or 0)
59 | {
60 | this.Log($"[GetEpisodeImages] The seasonNumber or episodeNumber is empty! seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
61 | return Enumerable.Empty();
62 | }
63 | var language = item.GetPreferredMetadataLanguage();
64 |
65 | // 利用season缓存取剧集信息会更快
66 | var seasonResult = await this._tmdbApi
67 | .GetSeasonAsync(seriesTmdbId, seasonNumber.Value, null, null, cancellationToken)
68 | .ConfigureAwait(false);
69 | if (seasonResult == null || seasonResult.Episodes.Count < episodeNumber.Value)
70 | {
71 | this.Log($"[GetEpisodeImages] Can't get season data for seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
72 | return Enumerable.Empty();
73 | }
74 |
75 | var result = new List();
76 | var episodeResult = seasonResult.Episodes[episodeNumber.Value - 1];
77 | if (!string.IsNullOrEmpty(episodeResult.StillPath))
78 | {
79 | result.Add(new RemoteImageInfo
80 | {
81 | Url = this._tmdbApi.GetStillUrl(episodeResult.StillPath),
82 | CommunityRating = episodeResult.VoteAverage,
83 | VoteCount = episodeResult.VoteCount,
84 | ProviderName = Name,
85 | Type = ImageType.Primary,
86 | });
87 | }
88 | return result;
89 | }
90 |
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using MediaBrowser.Model.Providers;
5 |
6 | namespace Jellyfin.Plugin.MetaShark.Providers
7 | {
8 | public static class EnumerableExtensions
9 | {
10 | private const int MaxPriority = 99;
11 |
12 | public static IEnumerable OrderByLanguageDescending(this IEnumerable remoteImageInfos, params string[] requestedLanguages)
13 | {
14 | if (requestedLanguages.Length <= 0)
15 | {
16 | requestedLanguages = new[] { "en" };
17 | }
18 |
19 | var requestedLanguagePriorityMap = new Dictionary();
20 | for (int i = 0; i < requestedLanguages.Length; i++)
21 | {
22 | if (string.IsNullOrEmpty(requestedLanguages[i]))
23 | {
24 | continue;
25 | }
26 | requestedLanguagePriorityMap.Add(NormalizeLanguage(requestedLanguages[i]), MaxPriority - i);
27 | }
28 |
29 | return remoteImageInfos.OrderByDescending(delegate (RemoteImageInfo i)
30 | {
31 | if (string.IsNullOrEmpty(i.Language))
32 | {
33 | return 3;
34 | }
35 |
36 | if (requestedLanguagePriorityMap.TryGetValue(NormalizeLanguage(i.Language), out int priority))
37 | {
38 | return priority;
39 | }
40 |
41 | return string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase) ? 2 : 0;
42 | }).ThenByDescending((RemoteImageInfo i) => i.CommunityRating.GetValueOrDefault()).ThenByDescending((RemoteImageInfo i) => i.VoteCount.GetValueOrDefault());
43 | }
44 |
45 | private static string NormalizeLanguage(string language)
46 | {
47 | if (string.IsNullOrEmpty(language))
48 | {
49 | return language;
50 | }
51 |
52 | return language.Split('-')[0].ToLower();
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/Extensions/ProviderIdsExtensions.cs:
--------------------------------------------------------------------------------
1 |
2 | using System.Collections.Generic;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using MediaBrowser.Model.Entities;
5 |
6 | namespace Jellyfin.Plugin.MetaShark.Providers
7 | {
8 | public static class ProviderIdsExtensions
9 | {
10 | public static MetaSource GetMetaSource(this IHasProviderIds instance, string name)
11 | {
12 | var value = instance.GetProviderId(name);
13 | return value.ToMetaSource();
14 | }
15 |
16 | public static void TryGetMetaSource(this Dictionary dict, string name, out MetaSource metaSource)
17 | {
18 | if (dict.TryGetValue(name, out var value))
19 | {
20 | metaSource = value.ToMetaSource();
21 | }
22 | else
23 | {
24 | metaSource = MetaSource.None;
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/ExternalId/DoubanExternalId.cs:
--------------------------------------------------------------------------------
1 | using MediaBrowser.Controller.Entities.Movies;
2 | using MediaBrowser.Controller.Entities.TV;
3 | using MediaBrowser.Controller.Providers;
4 | using MediaBrowser.Model.Entities;
5 | using MediaBrowser.Model.Providers;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Text;
10 | using System.Threading.Tasks;
11 |
12 | namespace Jellyfin.Plugin.MetaShark.Providers.ExternalId
13 | {
14 | public class DoubanExternalId : IExternalId
15 | {
16 | public string ProviderName => BaseProvider.DoubanProviderName;
17 |
18 | public string Key => BaseProvider.DoubanProviderId;
19 |
20 | public ExternalIdMediaType? Type => null;
21 |
22 | public string UrlFormatString => "https://movie.douban.com/subject/{0}/";
23 |
24 | public bool Supports(IHasProviderIds item)
25 | {
26 | return item is Movie || item is Series || item is Season;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/ExternalId/DoubanPersonExternalId.cs:
--------------------------------------------------------------------------------
1 | using MediaBrowser.Controller.Entities;
2 | using MediaBrowser.Controller.Providers;
3 | using MediaBrowser.Model.Entities;
4 | using MediaBrowser.Model.Providers;
5 |
6 | namespace Jellyfin.Plugin.MetaShark.Providers.ExternalId
7 | {
8 | ///
9 | public class DoubanPersonExternalId : IExternalId
10 | {
11 | ///
12 | public string ProviderName => BaseProvider.DoubanProviderName;
13 |
14 | ///
15 | public string Key => BaseProvider.DoubanProviderId;
16 |
17 | ///
18 | public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
19 |
20 | ///
21 | public string UrlFormatString => "https://www.douban.com/personage/{0}/";
22 |
23 | ///
24 | public bool Supports(IHasProviderIds item) => item is Person;
25 | }
26 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/MovieImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using MediaBrowser.Controller.Entities;
5 | using MediaBrowser.Controller.Entities.Movies;
6 | using MediaBrowser.Controller.Library;
7 | using MediaBrowser.Controller.Providers;
8 | using MediaBrowser.Model.Dto;
9 | using MediaBrowser.Model.Entities;
10 | using MediaBrowser.Model.Providers;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.Extensions.Logging;
13 | using System.Collections.Generic;
14 | using System.Linq;
15 | using System.Net.Http;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 |
19 | namespace Jellyfin.Plugin.MetaShark.Providers
20 | {
21 | public class MovieImageProvider : BaseProvider, IRemoteImageProvider
22 | {
23 | public MovieImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
24 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
25 | {
26 | }
27 |
28 | ///
29 | public string Name => Plugin.PluginName;
30 |
31 | ///
32 | public bool Supports(BaseItem item) => item is Movie;
33 |
34 | ///
35 | public IEnumerable GetSupportedImages(BaseItem item) => new List
36 | {
37 | ImageType.Primary,
38 | ImageType.Backdrop,
39 | ImageType.Logo,
40 | };
41 |
42 | ///
43 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
44 | {
45 | var sid = item.GetProviderId(DoubanProviderId);
46 | var metaSource = item.GetMetaSource(Plugin.ProviderId);
47 | this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
48 | if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
49 | {
50 | var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
51 | if (primary == null || string.IsNullOrEmpty(primary.Img))
52 | {
53 | return Enumerable.Empty();
54 | }
55 | var backdropImgs = await this.GetBackdrop(item, primary.PrimaryLanguageCode, cancellationToken).ConfigureAwait(false);
56 | var logoImgs = await this.GetLogos(item, primary.PrimaryLanguageCode, cancellationToken).ConfigureAwait(false);
57 |
58 | var res = new List {
59 | new RemoteImageInfo
60 | {
61 | ProviderName = this.Name,
62 | Url = this.GetDoubanPoster(primary),
63 | Type = ImageType.Primary,
64 | Language = "zh",
65 | },
66 | };
67 | res.AddRange(backdropImgs);
68 | res.AddRange(logoImgs);
69 | return res;
70 | }
71 |
72 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
73 | if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
74 | {
75 | var language = item.GetPreferredMetadataLanguage();
76 | // 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
77 | var movie = await this._tmdbApi
78 | .GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
79 | .ConfigureAwait(false);
80 |
81 | if (movie?.Images == null)
82 | {
83 | return Enumerable.Empty();
84 | }
85 |
86 | var remoteImages = new List();
87 |
88 | remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
89 | ProviderName = this.Name,
90 | Url = this._tmdbApi.GetPosterUrl(x.FilePath),
91 | Type = ImageType.Primary,
92 | CommunityRating = x.VoteAverage,
93 | VoteCount = x.VoteCount,
94 | Width = x.Width,
95 | Height = x.Height,
96 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
97 | RatingType = RatingType.Score,
98 | }));
99 |
100 | remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
101 | ProviderName = this.Name,
102 | Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
103 | Type = ImageType.Backdrop,
104 | CommunityRating = x.VoteAverage,
105 | VoteCount = x.VoteCount,
106 | Width = x.Width,
107 | Height = x.Height,
108 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
109 | RatingType = RatingType.Score,
110 | }));
111 |
112 | remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
113 | ProviderName = this.Name,
114 | Url = this._tmdbApi.GetLogoUrl(x.FilePath),
115 | Type = ImageType.Logo,
116 | CommunityRating = x.VoteAverage,
117 | VoteCount = x.VoteCount,
118 | Width = x.Width,
119 | Height = x.Height,
120 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
121 | RatingType = RatingType.Score,
122 | }));
123 |
124 | return remoteImages.OrderByLanguageDescending(language);
125 | }
126 |
127 | this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
128 | return new List();
129 | }
130 |
131 | ///
132 | /// Query for a background photo
133 | ///
134 | /// Instance of the interface.
135 | private async Task> GetBackdrop(BaseItem item, string alternativeImageLanguage, CancellationToken cancellationToken)
136 | {
137 | var sid = item.GetProviderId(DoubanProviderId);
138 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
139 | var list = new List();
140 |
141 | // 从豆瓣获取背景图
142 | if (!string.IsNullOrEmpty(sid))
143 | {
144 | var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken).ConfigureAwait(false);
145 | if (photo != null && photo.Count > 0)
146 | {
147 | this.Log("GetBackdrop from douban sid: {0}", sid);
148 | list = photo.Where(x => x.Width >= 1280 && x.Width <= 4096 && x.Width > x.Height * 1.3).Select(x =>
149 | {
150 | if (config.EnableDoubanBackdropRaw)
151 | {
152 | return new RemoteImageInfo
153 | {
154 | ProviderName = this.Name,
155 | Url = this.GetProxyImageUrl(x.Raw),
156 | Height = x.Height,
157 | Width = x.Width,
158 | Type = ImageType.Backdrop,
159 | Language = "zh",
160 | };
161 | }
162 | else
163 | {
164 | return new RemoteImageInfo
165 | {
166 | ProviderName = this.Name,
167 | Url = this.GetProxyImageUrl(x.Large),
168 | Type = ImageType.Backdrop,
169 | Language = "zh",
170 | };
171 | }
172 | }).ToList();
173 |
174 | }
175 | }
176 |
177 | // 添加 TheMovieDb 背景图为备选
178 | if (config.EnableTmdbBackdrop && !string.IsNullOrEmpty(tmdbId))
179 | {
180 | var language = item.GetPreferredMetadataLanguage();
181 | var movie = await this._tmdbApi
182 | .GetMovieAsync(tmdbId.ToInt(), language, language, cancellationToken)
183 | .ConfigureAwait(false);
184 |
185 | if (movie != null && !string.IsNullOrEmpty(movie.BackdropPath))
186 | {
187 | this.Log("GetBackdrop from tmdb id: {0} lang: {1}", tmdbId, language);
188 | list.Add(new RemoteImageInfo
189 | {
190 | ProviderName = this.Name,
191 | Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath),
192 | Type = ImageType.Backdrop,
193 | Language = language,
194 | });
195 | }
196 | }
197 |
198 | return list;
199 | }
200 |
201 | private async Task> GetLogos(BaseItem item, string alternativeImageLanguage, CancellationToken cancellationToken)
202 | {
203 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
204 | var list = new List();
205 | var language = item.GetPreferredMetadataLanguage();
206 | if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
207 | {
208 | this.Log("GetLogos from tmdb id: {0}", tmdbId);
209 | var movie = await this._tmdbApi
210 | .GetMovieAsync(tmdbId.ToInt(), null, null, cancellationToken)
211 | .ConfigureAwait(false);
212 |
213 | if (movie != null && movie.Images != null)
214 | {
215 | list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
216 | ProviderName = this.Name,
217 | Url = this._tmdbApi.GetLogoUrl(x.FilePath),
218 | Type = ImageType.Logo,
219 | CommunityRating = x.VoteAverage,
220 | VoteCount = x.VoteCount,
221 | Width = x.Width,
222 | Height = x.Height,
223 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
224 | RatingType = RatingType.Score,
225 | }));
226 | }
227 | }
228 |
229 | // TODO:jellyfin 内部判断取哪个图片时,还会默认使用 OrderByLanguageDescending 排序一次,这里排序没用
230 | // 默认图片优先级是:默认语言 > 无语言 > en > 其他语言
231 | return this.AdjustImageLanguagePriority(list, language, alternativeImageLanguage);
232 | }
233 |
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/PersonImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using MediaBrowser.Controller.Entities;
3 | using MediaBrowser.Controller.Library;
4 | using MediaBrowser.Controller.Providers;
5 | using MediaBrowser.Model.Entities;
6 | using MediaBrowser.Model.Providers;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 | using System.Collections.Generic;
10 | using System.Net.Http;
11 | using System.Threading;
12 | using System.Threading.Tasks;
13 |
14 | namespace Jellyfin.Plugin.MetaShark.Providers
15 | {
16 | public class PersonImageProvider : BaseProvider, IRemoteImageProvider
17 | {
18 | public PersonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
19 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
20 | {
21 | }
22 |
23 | ///
24 | public string Name => Plugin.PluginName;
25 |
26 | ///
27 | public bool Supports(BaseItem item) => item is Person;
28 |
29 | ///
30 | public IEnumerable GetSupportedImages(BaseItem item)
31 | {
32 | yield return ImageType.Primary;
33 | }
34 |
35 | ///
36 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
37 | {
38 | var list = new List();
39 | var cid = item.GetProviderId(DoubanProviderId);
40 | var metaSource = item.GetMetaSource(Plugin.ProviderId);
41 | this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
42 | if (!string.IsNullOrEmpty(cid))
43 | {
44 | var celebrity = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
45 | if (celebrity != null)
46 | {
47 | list.Add(new RemoteImageInfo
48 | {
49 | ProviderName = this.Name,
50 | Url = this.GetProxyImageUrl(celebrity.Img),
51 | Type = ImageType.Primary,
52 | Language = "zh",
53 | });
54 | }
55 |
56 | var photos = await this._doubanApi.GetCelebrityPhotosAsync(cid, cancellationToken).ConfigureAwait(false);
57 | photos.ForEach(x =>
58 | {
59 | // 过滤不是竖图
60 | if (x.Width < 400 || x.Height < x.Width * 1.3)
61 | {
62 | return;
63 | }
64 |
65 | list.Add(new RemoteImageInfo
66 | {
67 | ProviderName = this.Name,
68 | Url = this.GetProxyImageUrl(x.Raw),
69 | Width = x.Width,
70 | Height = x.Height,
71 | Type = ImageType.Primary,
72 | Language = "zh",
73 | });
74 | });
75 | }
76 |
77 | if (list.Count == 0)
78 | {
79 | this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
80 | }
81 | return list;
82 | }
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/PersonProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.Logging;
10 | using Jellyfin.Plugin.MetaShark.Api;
11 | using Jellyfin.Plugin.MetaShark.Core;
12 | using MediaBrowser.Controller.Entities;
13 | using MediaBrowser.Controller.Library;
14 | using MediaBrowser.Controller.Providers;
15 | using MediaBrowser.Model.Entities;
16 | using MediaBrowser.Model.Providers;
17 | using TMDbLib.Objects.Find;
18 |
19 | namespace Jellyfin.Plugin.MetaShark.Providers
20 | {
21 | ///
22 | /// OddbPersonProvider.
23 | ///
24 | public class PersonProvider : BaseProvider, IRemoteMetadataProvider
25 | {
26 | public PersonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
27 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
28 | {
29 | }
30 |
31 | ///
32 | public string Name => Plugin.PluginName;
33 |
34 | ///
35 | public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
36 | {
37 | this.Log($"GetPersonSearchResults of [name]: {searchInfo.Name}");
38 |
39 | var result = new List();
40 | var cid = searchInfo.GetProviderId(DoubanProviderId);
41 | if (!string.IsNullOrEmpty(cid))
42 | {
43 | var celebrity = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
44 | if (celebrity != null)
45 | {
46 | result.Add(new RemoteSearchResult
47 | {
48 | SearchProviderName = DoubanProviderName,
49 | ProviderIds = new Dictionary { { DoubanProviderId, celebrity.Id } },
50 | ImageUrl = this.GetProxyImageUrl(celebrity.Img),
51 | Name = celebrity.Name,
52 | }
53 | );
54 |
55 | return result;
56 | }
57 | }
58 |
59 |
60 |
61 | var res = await this._doubanApi.SearchCelebrityAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
62 | result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x =>
63 | {
64 | return new RemoteSearchResult
65 | {
66 | SearchProviderName = DoubanProviderName,
67 | ProviderIds = new Dictionary { { DoubanProviderId, x.Id } },
68 | ImageUrl = this.GetProxyImageUrl(x.Img),
69 | Name = x.Name,
70 | };
71 | }));
72 |
73 | return result;
74 | }
75 |
76 | ///
77 | public async Task> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken)
78 | {
79 | var result = new MetadataResult();
80 |
81 | var cid = info.GetProviderId(DoubanProviderId);
82 | this.Log($"GetPersonMetadata of [name]: {info.Name} [cid]: {cid}");
83 | if (!string.IsNullOrEmpty(cid))
84 | {
85 |
86 | var c = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
87 | if (c != null)
88 | {
89 | var item = new Person
90 | {
91 | // Name = c.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
92 | OriginalTitle = c.DisplayOriginalName, // 外国人显示英文名
93 | HomePageUrl = c.Site,
94 | Overview = c.Intro,
95 | };
96 | if (DateTime.TryParseExact(c.Birthdate, "yyyy年MM月dd日", null, DateTimeStyles.None, out var premiereDate))
97 | {
98 | item.PremiereDate = premiereDate;
99 | item.ProductionYear = premiereDate.Year;
100 | }
101 | if (DateTime.TryParseExact(c.Enddate, "yyyy年MM月dd日", null, DateTimeStyles.None, out var endDate))
102 | {
103 | item.EndDate = endDate;
104 | }
105 | if (!string.IsNullOrWhiteSpace(c.Birthplace))
106 | {
107 | item.ProductionLocations = new[] { c.Birthplace };
108 | }
109 |
110 | item.SetProviderId(DoubanProviderId, c.Id);
111 | if (!string.IsNullOrEmpty(c.Imdb))
112 | {
113 | var newImdbId = await this._imdbApi.CheckPersonNewIDAsync(c.Imdb, cancellationToken).ConfigureAwait(false);
114 | if (!string.IsNullOrEmpty(newImdbId))
115 | {
116 | c.Imdb = newImdbId;
117 | }
118 | item.SetProviderId(MetadataProvider.Imdb, c.Imdb);
119 | // 通过imdb获取TMDB id
120 | var findResult = await this._tmdbApi.FindByExternalIdAsync(c.Imdb, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
121 | if (findResult?.PersonResults != null && findResult.PersonResults.Count > 0)
122 | {
123 | var foundTmdbId = findResult.PersonResults.First().Id.ToString();
124 | this.Log($"GetPersonMetadata of found tmdb [id]: {foundTmdbId}");
125 | item.SetProviderId(MetadataProvider.Tmdb, $"{foundTmdbId}");
126 | }
127 | }
128 |
129 | result.QueriedById = true;
130 | result.HasMetadata = true;
131 | result.Item = item;
132 |
133 | return result;
134 | }
135 | }
136 |
137 | // jellyfin强制最后一定使用默认的TheMovieDb插件获取一次,这里不太必要(除了使用自己的域名)
138 | var personTmdbId = info.GetProviderId(MetadataProvider.Tmdb);
139 | this.Log($"GetPersonMetadata of [personTmdbId]: {personTmdbId}");
140 | if (!string.IsNullOrEmpty(personTmdbId))
141 | {
142 | return await this.GetMetadataByTmdb(personTmdbId.ToInt(), info, cancellationToken).ConfigureAwait(false);
143 | }
144 |
145 | return result;
146 | }
147 |
148 | public async Task> GetMetadataByTmdb(int personTmdbId, PersonLookupInfo info, CancellationToken cancellationToken)
149 | {
150 | var result = new MetadataResult();
151 | var person = await this._tmdbApi.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
152 | if (person != null)
153 | {
154 | var item = new Person
155 | {
156 | // Name = info.Name.Trim(), // 名称需保持和info.Name一致,不然会导致关联不到影片,自动被删除
157 | HomePageUrl = person.Homepage,
158 | Overview = person.Biography,
159 | PremiereDate = person.Birthday?.ToUniversalTime(),
160 | EndDate = person.Deathday?.ToUniversalTime()
161 | };
162 |
163 | if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
164 | {
165 | item.ProductionLocations = new[] { person.PlaceOfBirth };
166 | }
167 |
168 | item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
169 | if (!string.IsNullOrEmpty(person.ImdbId))
170 | {
171 | item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
172 | }
173 |
174 | result.HasMetadata = true;
175 | result.Item = item;
176 |
177 | return result;
178 | }
179 |
180 | return result;
181 | }
182 |
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/SeasonImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Model;
3 | using MediaBrowser.Controller.Entities;
4 | using MediaBrowser.Controller.Entities.TV;
5 | using MediaBrowser.Controller.Library;
6 | using MediaBrowser.Controller.Providers;
7 | using MediaBrowser.Model.Entities;
8 | using MediaBrowser.Model.Providers;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.Extensions.Logging;
11 | using System;
12 | using System.Collections.Generic;
13 | using System.Globalization;
14 | using System.Linq;
15 | using System.Net.Http;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 |
19 | namespace Jellyfin.Plugin.MetaShark.Providers
20 | {
21 | public class SeasonImageProvider : BaseProvider, IRemoteImageProvider
22 | {
23 | public SeasonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
24 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
25 | {
26 | }
27 |
28 | ///
29 | public string Name => Plugin.PluginName;
30 |
31 | ///
32 | public bool Supports(BaseItem item) => item is Season;
33 |
34 | ///
35 | public IEnumerable GetSupportedImages(BaseItem item)
36 | {
37 | yield return ImageType.Primary;
38 | }
39 |
40 | ///
41 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
42 | {
43 | this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
44 | var season = (Season)item;
45 | var series = season.Series;
46 | var metaSource = series.GetMetaSource(Plugin.ProviderId);
47 |
48 | // get image from douban
49 | var sid = item.GetProviderId(DoubanProviderId);
50 | if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
51 | {
52 | var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
53 | if (primary == null)
54 | {
55 | return Enumerable.Empty();
56 | }
57 |
58 | var res = new List {
59 | new RemoteImageInfo
60 | {
61 | ProviderName = primary.Name,
62 | Url = this.GetDoubanPoster(primary),
63 | Type = ImageType.Primary,
64 | Language = "zh",
65 | },
66 | };
67 | return res;
68 | }
69 |
70 |
71 | // get image form TMDB
72 | var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
73 | if (seriesTmdbId <= 0 || season?.IndexNumber == null)
74 | {
75 | return Enumerable.Empty();
76 | }
77 |
78 | var language = item.GetPreferredMetadataLanguage();
79 | var seasonResult = await this._tmdbApi
80 | .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken)
81 | .ConfigureAwait(false);
82 | var posters = seasonResult?.Images?.Posters;
83 | if (posters == null)
84 | {
85 | return Enumerable.Empty();
86 | }
87 |
88 | var remoteImages = new RemoteImageInfo[posters.Count];
89 | for (var i = 0; i < posters.Count; i++)
90 | {
91 | var image = posters[i];
92 | remoteImages[i] = new RemoteImageInfo
93 | {
94 | Url = this._tmdbApi.GetPosterUrl(image.FilePath),
95 | CommunityRating = image.VoteAverage,
96 | VoteCount = image.VoteCount,
97 | Width = image.Width,
98 | Height = image.Height,
99 | Language = AdjustImageLanguage(image.Iso_639_1, language),
100 | ProviderName = Name,
101 | Type = ImageType.Primary,
102 | };
103 | }
104 |
105 | return remoteImages.OrderByLanguageDescending(language);
106 | }
107 |
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/Providers/SeriesImageProvider.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using Jellyfin.Plugin.MetaShark.Core;
3 | using Jellyfin.Plugin.MetaShark.Model;
4 | using MediaBrowser.Controller.Entities;
5 | using MediaBrowser.Controller.Entities.TV;
6 | using MediaBrowser.Controller.Library;
7 | using MediaBrowser.Controller.Providers;
8 | using MediaBrowser.Model.Dto;
9 | using MediaBrowser.Model.Entities;
10 | using MediaBrowser.Model.Providers;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.Extensions.Logging;
13 | using System.Collections.Generic;
14 | using System.Linq;
15 | using System.Net.Http;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 |
19 | namespace Jellyfin.Plugin.MetaShark.Providers
20 | {
21 | public class SeriesImageProvider : BaseProvider, IRemoteImageProvider
22 | {
23 | public SeriesImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi)
24 | : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi)
25 | {
26 | }
27 |
28 | ///
29 | public string Name => Plugin.PluginName;
30 |
31 | ///
32 | public bool Supports(BaseItem item) => item is Series;
33 |
34 | ///
35 | public IEnumerable GetSupportedImages(BaseItem item) => new List
36 | {
37 | ImageType.Primary,
38 | ImageType.Backdrop,
39 | ImageType.Logo,
40 | };
41 |
42 | ///
43 | public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
44 | {
45 | var sid = item.GetProviderId(DoubanProviderId);
46 | var metaSource = item.GetMetaSource(Plugin.ProviderId);
47 | this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
48 | if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
49 | {
50 | var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
51 | if (primary == null || string.IsNullOrEmpty(primary.Img))
52 | {
53 | return Enumerable.Empty();
54 | }
55 | var res = new List {
56 | new RemoteImageInfo
57 | {
58 | ProviderName = this.Name,
59 | Url = this.GetDoubanPoster(primary),
60 | Type = ImageType.Primary,
61 | Language = "zh",
62 | },
63 | };
64 |
65 | var backdropImgs = await this.GetBackdrop(item, primary.PrimaryLanguageCode, cancellationToken).ConfigureAwait(false);
66 | var logoImgs = await this.GetLogos(item, primary.PrimaryLanguageCode, cancellationToken).ConfigureAwait(false);
67 | res.AddRange(backdropImgs);
68 | res.AddRange(logoImgs);
69 | return res;
70 | }
71 |
72 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
73 | if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId))
74 | {
75 | var language = item.GetPreferredMetadataLanguage();
76 | // 设定language会导致图片被过滤,这里设为null,保持取全部语言图片
77 | var movie = await this._tmdbApi
78 | .GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
79 | .ConfigureAwait(false);
80 |
81 | if (movie?.Images == null)
82 | {
83 | return Enumerable.Empty();
84 | }
85 |
86 | var remoteImages = new List();
87 |
88 | remoteImages.AddRange(movie.Images.Posters.Select(x => new RemoteImageInfo {
89 | ProviderName = this.Name,
90 | Url = this._tmdbApi.GetPosterUrl(x.FilePath),
91 | Type = ImageType.Primary,
92 | CommunityRating = x.VoteAverage,
93 | VoteCount = x.VoteCount,
94 | Width = x.Width,
95 | Height = x.Height,
96 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
97 | RatingType = RatingType.Score,
98 | }));
99 |
100 | remoteImages.AddRange(movie.Images.Backdrops.Select(x => new RemoteImageInfo {
101 | ProviderName = this.Name,
102 | Url = this._tmdbApi.GetBackdropUrl(x.FilePath),
103 | Type = ImageType.Backdrop,
104 | CommunityRating = x.VoteAverage,
105 | VoteCount = x.VoteCount,
106 | Width = x.Width,
107 | Height = x.Height,
108 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
109 | RatingType = RatingType.Score,
110 | }));
111 |
112 | remoteImages.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
113 | ProviderName = this.Name,
114 | Url = this._tmdbApi.GetLogoUrl(x.FilePath),
115 | Type = ImageType.Logo,
116 | CommunityRating = x.VoteAverage,
117 | VoteCount = x.VoteCount,
118 | Width = x.Width,
119 | Height = x.Height,
120 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
121 | RatingType = RatingType.Score,
122 | }));
123 |
124 | return remoteImages.OrderByLanguageDescending(language);
125 | }
126 |
127 | this.Log($"Got images failed because the images of \"{item.Name}\" is empty!");
128 | return new List();
129 | }
130 |
131 | ///
132 | /// Query for a background photo
133 | ///
134 | /// Instance of the interface.
135 | private async Task> GetBackdrop(BaseItem item, string alternativeImageLanguage, CancellationToken cancellationToken)
136 | {
137 | var sid = item.GetProviderId(DoubanProviderId);
138 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
139 | var list = new List();
140 |
141 | // 从豆瓣获取背景图
142 | if (!string.IsNullOrEmpty(sid))
143 | {
144 | var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
145 | if (photo != null && photo.Count > 0)
146 | {
147 | this.Log("GetBackdrop from douban sid: {0}", sid);
148 | list = photo.Where(x => x.Width >= 1280 && x.Width <= 4096 && x.Width > x.Height * 1.3).Select(x =>
149 | {
150 | if (config.EnableDoubanBackdropRaw)
151 | {
152 | return new RemoteImageInfo
153 | {
154 | ProviderName = Name,
155 | Url = this.GetProxyImageUrl(x.Raw),
156 | Height = x.Height,
157 | Width = x.Width,
158 | Type = ImageType.Backdrop,
159 | Language = "zh",
160 | };
161 | }
162 | else
163 | {
164 | return new RemoteImageInfo
165 | {
166 | ProviderName = Name,
167 | Url = this.GetProxyImageUrl(x.Large),
168 | Type = ImageType.Backdrop,
169 | Language = "zh",
170 | };
171 | }
172 | }).ToList();
173 | }
174 | }
175 |
176 | // 添加 TheMovieDb 背景图为备选
177 | if (config.EnableTmdbBackdrop && !string.IsNullOrEmpty(tmdbId))
178 | {
179 | var language = item.GetPreferredMetadataLanguage();
180 | var movie = await _tmdbApi
181 | .GetSeriesAsync(tmdbId.ToInt(), language, language, cancellationToken)
182 | .ConfigureAwait(false);
183 |
184 | if (movie != null && !string.IsNullOrEmpty(movie.BackdropPath))
185 | {
186 | this.Log("GetBackdrop from tmdb id: {0} lang: {1}", tmdbId, language);
187 | list.Add(new RemoteImageInfo
188 | {
189 | ProviderName = this.Name,
190 | Url = this._tmdbApi.GetBackdropUrl(movie.BackdropPath),
191 | Type = ImageType.Backdrop,
192 | Language = language,
193 | });
194 | }
195 | }
196 |
197 | return list;
198 | }
199 |
200 | private async Task> GetLogos(BaseItem item, string alternativeImageLanguage, CancellationToken cancellationToken)
201 | {
202 | var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
203 | var language = item.GetPreferredMetadataLanguage();
204 | var list = new List();
205 | if (this.config.EnableTmdbLogo && !string.IsNullOrEmpty(tmdbId))
206 | {
207 | this.Log("GetLogos from tmdb id: {0}", tmdbId);
208 | var movie = await this._tmdbApi
209 | .GetSeriesAsync(tmdbId.ToInt(), null, null, cancellationToken)
210 | .ConfigureAwait(false);
211 |
212 | if (movie != null && movie.Images != null)
213 | {
214 | list.AddRange(movie.Images.Logos.Select(x => new RemoteImageInfo {
215 | ProviderName = this.Name,
216 | Url = this._tmdbApi.GetLogoUrl(x.FilePath),
217 | Type = ImageType.Logo,
218 | CommunityRating = x.VoteAverage,
219 | VoteCount = x.VoteCount,
220 | Width = x.Width,
221 | Height = x.Height,
222 | Language = this.AdjustImageLanguage(x.Iso_639_1, language),
223 | RatingType = RatingType.Score,
224 | }));
225 | }
226 | }
227 |
228 | // TODO:jellyfin 内部判断取哪个图片时,还会默认使用 OrderByLanguageDescending 排序一次,这里排序没用
229 | // 默认图片优先级是:默认语言 > 无语言 > en > 其他语言
230 | return this.AdjustImageLanguagePriority(list, language, alternativeImageLanguage);
231 | }
232 |
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/ScheduledTasks/AutoCreateCollectionTask.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using MediaBrowser.Controller.Library;
6 | using MediaBrowser.Model.Tasks;
7 | using Microsoft.Extensions.Logging;
8 | using MediaBrowser.Controller.Collections;
9 |
10 | namespace Jellyfin.Plugin.MetaShark.ScheduledTasks
11 | {
12 | public class AutoCreateCollectionTask : IScheduledTask
13 | {
14 | private readonly BoxSetManager _boxSetManager;
15 | private readonly ILogger _logger;
16 |
17 | public string Key => $"{Plugin.PluginName}AutoCreateCollection";
18 |
19 | public string Name => "扫描自动创建合集";
20 |
21 | public string Description => $"扫描媒体库创建合集,需要先在配置中开启获取电影系列信息";
22 |
23 | public string Category => Plugin.PluginName;
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | /// Instance of the interface.
29 | /// Instance of the interface.
30 | public AutoCreateCollectionTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, ICollectionManager collectionManager)
31 | {
32 | _logger = loggerFactory.CreateLogger();
33 | _boxSetManager = new BoxSetManager(libraryManager, collectionManager, loggerFactory);
34 | }
35 |
36 | public IEnumerable GetDefaultTriggers()
37 | {
38 | yield return new TaskTriggerInfo
39 | {
40 | Type = TaskTriggerInfo.TriggerDaily,
41 | TimeOfDayTicks = TimeSpan.FromHours(0).Ticks
42 | };
43 | }
44 |
45 | public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
46 | {
47 | _logger.LogInformation("开始扫描媒体库自动创建合集...");
48 | await _boxSetManager.ScanLibrary(progress).ConfigureAwait(false);
49 | _logger.LogInformation("扫描媒体库自动创建合集执行完成");
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/Jellyfin.Plugin.MetaShark/ServiceRegistrator.cs:
--------------------------------------------------------------------------------
1 | using Jellyfin.Plugin.MetaShark.Api;
2 | using MediaBrowser.Controller;
3 | using MediaBrowser.Controller.Plugins;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace Jellyfin.Plugin.MetaShark
8 | {
9 | ///
10 | public class ServiceRegistrator : IPluginServiceRegistrator
11 | {
12 | ///
13 | public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
14 | {
15 | serviceCollection.AddHostedService();
16 | serviceCollection.AddSingleton((ctx) =>
17 | {
18 | return new DoubanApi(ctx.GetRequiredService());
19 | });
20 | serviceCollection.AddSingleton((ctx) =>
21 | {
22 | return new TmdbApi(ctx.GetRequiredService());
23 | });
24 | serviceCollection.AddSingleton((ctx) =>
25 | {
26 | return new OmdbApi(ctx.GetRequiredService());
27 | });
28 | serviceCollection.AddSingleton((ctx) =>
29 | {
30 | return new ImdbApi(ctx.GetRequiredService());
31 | });
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # jellyfin-plugin-metashark
2 |
3 | [](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
4 | [](https://github.com/cxfksword/jellyfin-plugin-metashark/releases)
5 | [](https://github.com/cxfksword/jellyfin-plugin-metashark/main/LICENSE)
6 |
7 | jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TheMovieDb补全缺失的剧集数据。
8 |
9 | 功能:
10 | * 支持从豆瓣和TMDB获取元数据
11 | * 兼容anime动画命名格式
12 |
13 | 
14 |
15 | ## 安装插件
16 |
17 | 添加插件存储库:
18 |
19 | 国内加速:https://ghfast.top/https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest_cn.json
20 |
21 | 国外访问:https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json
22 |
23 | > 如果都无法访问,可以直接从 [Release](https://github.com/cxfksword/jellyfin-plugin-metashark/releases) 页面下载,并解压到 jellyfin 插件目录中使用
24 |
25 | ## 如何使用
26 |
27 | 1. 安装后,先进入`控制台 -> 插件`,查看下MetaShark插件是否是**Active**状态
28 | 2. 进入`控制台 -> 媒体库`,点击任一媒体库进入配置页,在元数据下载器选项中勾选**MetaShark**,并把**MetaShark**移动到第一位
29 |
30 |
31 |
32 | 3. 识别时默认不返回TheMovieDb结果,有需要可以到插件配置中打开
33 | 4. 假如网络原因访问TheMovieDb比较慢,可以到插件配置中关闭从TheMovieDb获取数据(关闭后不会再获取剧集信息)
34 |
35 | > 🚨假如需要刮削大量电影,请到插件配置中打开防封禁功能,避免频繁请求豆瓣导致被封IP(封IP需要等6小时左右才能恢复访问)
36 |
37 | > :fire:遇到图片显示不出来时,请到插件配置中配置jellyfin访问域名
38 |
39 | ## How to build
40 |
41 | 1. Clone or download this repository
42 |
43 | 2. Ensure you have .NET Core SDK 8.0 setup and installed
44 |
45 | 3. Build plugin with following command.
46 |
47 | ```sh
48 | dotnet restore
49 | dotnet publish --configuration=Release Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
50 | ```
51 |
52 |
53 | ## How to test
54 |
55 | 1. Build the plugin
56 |
57 | 2. Create a folder, like `metashark` and copy `./Jellyfin.Plugin.MetaShark/bin/Release/net8.0/Jellyfin.Plugin.MetaShark.dll` into it
58 |
59 | 3. Move folder `metashark` to jellyfin `data/plugins` folder
60 |
61 |
62 | ## FAQ
63 |
64 | 1. Plugin run in error: `System.BadImageFormatException: Bad IL format.`
65 |
66 | Remove all hidden file and `meta.json` in `metashark` plugin folder
67 |
68 |
69 | ## Thanks
70 |
71 | [AnitomySharp](https://github.com/chu-shen/AnitomySharp)
72 |
73 | ## 免责声明
74 |
75 | 本项目代码仅用于学习交流编程技术,下载后请勿用于商业用途。
76 |
77 | 如果本项目存在侵犯您的合法权益的情况,请及时与开发者联系,开发者将会及时删除有关内容。
--------------------------------------------------------------------------------
/doc/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cxfksword/jellyfin-plugin-metashark/023b1b647f28cb678cdfce945a0ce4383fea751c/doc/logo.png
--------------------------------------------------------------------------------
/scripts/generate_manifest.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import hashlib
3 | import json
4 | import sys
5 | import re
6 | import os
7 | import subprocess
8 | from datetime import datetime
9 | from urllib.request import urlopen
10 | from urllib.error import HTTPError
11 |
12 |
13 | def generate_manifest():
14 | return [{
15 | "guid": "9a19103f-16f7-4668-be54-9a1e7a4f7556",
16 | "name": "MetaShark",
17 | "description": "jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TMDB补充缺失的剧集数据。",
18 | "overview": "jellyfin电影元数据插件",
19 | "owner": "cxfksword",
20 | "category": "Metadata",
21 | "imageUrl": "https://github.com/cxfksword/jellyfin-plugin-metashark/raw/main/doc/logo.png",
22 | "versions": []
23 | }]
24 |
25 | def generate_version(filepath, version, changelog):
26 | return {
27 | 'version': f"{version}.0",
28 | 'changelog': changelog,
29 | 'targetAbi': '10.9.0.0',
30 | 'sourceUrl': f'https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/v{version}/metashark_{version}.0.zip',
31 | 'checksum': md5sum(filepath),
32 | 'timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
33 | }
34 |
35 | def md5sum(filename):
36 | with open(filename, 'rb') as f:
37 | return hashlib.md5(f.read()).hexdigest()
38 |
39 |
40 | def main():
41 | filename = sys.argv[1]
42 | tag = sys.argv[2]
43 | version = tag.lstrip('v')
44 | filepath = os.path.join(os.getcwd(), filename)
45 | result = subprocess.run(['git', 'tag','-l','--format=%(contents)', tag, '-l'], stdout=subprocess.PIPE)
46 | changelog = result.stdout.decode('utf-8').strip()
47 |
48 | # 解析旧 manifest
49 | try:
50 | with urlopen('https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json') as f:
51 | manifest = json.load(f)
52 | except HTTPError as err:
53 | if err.code == 404:
54 | manifest = generate_manifest()
55 | else:
56 | raise
57 |
58 | # 追加新版本/覆盖旧版本
59 | manifest[0]['versions'] = list(filter(lambda x: x['version'] != f"{version}.0", manifest[0]['versions']))
60 | manifest[0]['versions'].insert(0, generate_version(filepath, version, changelog))
61 |
62 | with open('manifest.json', 'w') as f:
63 | json.dump(manifest, f, indent=2)
64 |
65 | # # 国内加速
66 | cn_domain = 'https://ghfast.top/'
67 | if 'CN_DOMAIN' in os.environ and os.environ["CN_DOMAIN"]:
68 | cn_domain = os.environ["CN_DOMAIN"]
69 | cn_domain = cn_domain.rstrip('/')
70 | with open('manifest_cn.json', 'w') as f:
71 | manifest_cn = json.dumps(manifest, indent=2)
72 | manifest_cn = re.sub('https://github.com', f'{cn_domain}/https://github.com', manifest_cn)
73 | f.write(manifest_cn)
74 |
75 |
76 | if __name__ == '__main__':
77 | main()
--------------------------------------------------------------------------------