first commit

This commit is contained in:
cxfksword 2022-10-25 14:32:28 +08:00
commit a964167a04
281 changed files with 14646 additions and 0 deletions

22
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,22 @@
name: "🏗️ Build Plugin"
on:
push:
branches:
- "**"
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
# - name: Test
# run: dotnet test --no-restore --verbosity normal

58
.github/workflows/publish.yaml vendored Normal file
View File

@ -0,0 +1,58 @@
name: "🚀 Publish Plugin"
on:
push:
tags: ["*"]
env:
dotnet-version: 6.0.x
python-version: 3.8
project: Jellyfin.Plugin.MetaShark/Jellyfin.Plugin.MetaShark.csproj
branch: main
artifact: metashark
manifest: https://github.com/cxfksword/jellyfin-plugin-metashark/releases/download/manifest/manifest.json
jobs:
build:
runs-on: ubuntu-latest
name: Build & Release
steps:
- uses: actions/checkout@v3
- name: Get tags (For CHANGELOG)
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.dotnet-version }}
- name: Restore nuget packages
run: dotnet restore ${{ env.project }} # 需要指定项目要不然会同时build多个项目导致出错
- name: Setup python
uses: actions/setup-python@v2
with:
python-version: ${{ env.python-version }}
- name: Install JPRM
run: python -m pip install jprm
- name: Run JPRM
run: chmod +x ./build_plugin.sh && ./build_plugin.sh ${{ env.artifact }} ${GITHUB_REF#refs/*/}
- name: Update release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/${{ env.artifact }}_*.zip
tag: ${{ github.ref }}
file_glob: true
- name: Update manifest
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./*.json
tag: "manifest"
overwrite: true
file_glob: true
# - name: Update manifest
# uses: stefanzweifel/git-auto-commit-action@v4
# with:
# branch: ${{ env.branch }}
# commit_message: Update repo manifest
# file_pattern: "*.json"

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin/
obj/
.vs/
.idea/
artifacts
**/.DS_Store

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Test
{
internal class DefaultHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using Jellyfin.Plugin.MetaShark.Providers;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Jellyfin.Plugin.MetaShark.Test
{
[TestClass]
public class DoubanApiTest
{
private TestContext testContextInstance;
/// <summary>
/// Gets or sets the test context which provides
/// information about and functionality for the current test run.
/// </summary>
public TestContext TestContext
{
get { return testContextInstance; }
set { testContextInstance = value; }
}
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
}));
[TestMethod]
public void TestSearch()
{
var keyword = "重返少年时";
var api = new DoubanApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.SearchAsync(keyword, CancellationToken.None);
var str = result.ToJson();
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
TestContext.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetVideoByBvidAsync()
{
var sid = "26654184";
var api = new DoubanApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetMovieAsync(sid, CancellationToken.None);
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetCelebritiesBySidAsync()
{
var sid = "26654184";
var api = new DoubanApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetCelebritiesBySidAsync(sid, CancellationToken.None);
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jellyfin.Plugin.MetaShark\Jellyfin.Plugin.MetaShark.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Providers;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Test
{
[TestClass]
public class SeriesProviderTest
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
}));
[TestMethod]
public void TestGetMetadata()
{
var info = new SeriesInfo() { Name = "外科医生奉达熙", ProviderIds = new Dictionary<string, string>() { { BaseProvider.DoubanProviderId, "2241528" } } };
var doubanApi = new DoubanApi(loggerFactory);
var tmdbApi = new TmdbApi(loggerFactory);
var omdbApi = new OmdbApi(loggerFactory);
var httpClientFactory = new DefaultHttpClientFactory();
var libraryManagerStub = new Mock<ILibraryManager>();
Task.Run(async () =>
{
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, doubanApi, tmdbApi, omdbApi);
var result = await provider.GetMetadata(info, CancellationToken.None);
Assert.IsNotNull(result);
var str = result.ToJson();
Console.WriteLine(result.ToJson());
}).GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1,28 @@
using Jellyfin.Plugin.MetaShark.Core;
namespace Jellyfin.Plugin.MetaShark.Test
{
[TestClass]
public class StringSimilarityTest
{
[TestMethod]
public void TestString()
{
var str1 = "雄狮少年";
var str2 = "我是特优声 剧团季";
var score = str1.Distance(str2);
str1 = "雄狮少年";
str2 = "雄狮少年 第二季";
score = str1.Distance(str2);
var score2 = "君子和而不同".Distance("小人同而不和");
Assert.IsTrue(score > 0);
}
}
}

View File

@ -0,0 +1,119 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Test
{
[TestClass]
public class TmdbApiTest
{
private TestContext testContextInstance;
/// <summary>
/// Gets or sets the test context which provides
/// information about and functionality for the current test run.
/// </summary>
public TestContext TestContext
{
get { return testContextInstance; }
set { testContextInstance = value; }
}
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options =>
{
options.IncludeScopes = true;
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
}));
[TestMethod]
public void TestGetSeries()
{
var api = new TmdbApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetSeriesAsync(13372, "zh", BaseProvider.GetImageLanguagesParam("zh"), CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.Images.ToJson());
result = await api.GetSeriesAsync(13372, "zh", null, CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.Images.ToJson());
}
catch (Exception ex)
{
TestContext.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetEpisode()
{
var api = new TmdbApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.GetEpisodeAsync(13372, 1, 1, "zh", BaseProvider.GetImageLanguagesParam("zh"), CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.Images.Stills.ToJson());
result = await api.GetEpisodeAsync(13372, 1, 1, null, null, CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
TestContext.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestSearch()
{
var keyword = "重返少年时";
var api = new TmdbApi(loggerFactory);
Task.Run(async () =>
{
try
{
var result = await api.SearchSeriesAsync(keyword, "zh", CancellationToken.None)
.ConfigureAwait(false);
Assert.IsNotNull(result);
TestContext.WriteLine(result.ToJson());
}
catch (Exception ex)
{
TestContext.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@ -0,0 +1,30 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark", "Jellyfin.Plugin.MetaShark\Jellyfin.Plugin.MetaShark.csproj", "{D921B930-CF91-406F-ACBC-08914DCD0D34}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.MetaShark.Test", "Jellyfin.Plugin.MetaShark.Test\Jellyfin.Plugin.MetaShark.Test.csproj", "{80814353-4291-4230-8C4A-4C45CAD4D5D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D921B930-CF91-406F-ACBC-08914DCD0D34}.Release|Any CPU.Build.0 = Release|Any CPU
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80814353-4291-4230-8C4A-4C45CAD4D5D3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7D81AE36-16A1-4386-8B86-21FACCB675DF}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,562 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.MetaShark.Model;
using System.Threading;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Common.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using System.Net;
using Jellyfin.Plugin.MetaShark.Api.Http;
using System.Web;
using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
using Microsoft.Extensions.Caching.Memory;
using Jellyfin.Plugin.MetaShark.Providers;
using AngleSharp;
using System.Net.WebSockets;
using Jellyfin.Data.Entities.Libraries;
using AngleSharp.Dom;
using System.Text.RegularExpressions;
using Jellyfin.Plugin.MetaShark.Core;
using System.Data;
using TMDbLib.Objects.Movies;
using System.Xml.Linq;
namespace Jellyfin.Plugin.MetaShark.Api
{
public class DoubanApi : IDisposable
{
const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44";
private readonly ILogger<DoubanApi> _logger;
private HttpClient httpClient;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly IMemoryCache _memoryCache;
private static readonly object _lock = new object();
private DateTime lastRequestTime = DateTime.Now.AddDays(-1);
Regex regId = new Regex(@"/(\d+?)/", RegexOptions.Compiled);
Regex regSid = new Regex(@"sid: (\d+?),", RegexOptions.Compiled);
Regex regCat = new Regex(@"\[(.+?)\]", RegexOptions.Compiled);
Regex regYear = new Regex(@"(\d{4})", RegexOptions.Compiled);
Regex regDirector = new Regex(@"导演: (.+?)\n", RegexOptions.Compiled);
Regex regWriter = new Regex(@"编剧: (.+?)\n", RegexOptions.Compiled);
Regex regActor = new Regex(@"主演: (.+?)\n", RegexOptions.Compiled);
Regex regGenre = new Regex(@"类型: (.+?)\n", RegexOptions.Compiled);
Regex regCountry = new Regex(@"制片国家/地区: (.+?)\n", RegexOptions.Compiled);
Regex regLanguage = new Regex(@"语言: (.+?)\n", RegexOptions.Compiled);
Regex regDuration = new Regex(@"片长: (.+?)\n", RegexOptions.Compiled);
Regex regScreen = new Regex(@"上映日期: (.+?)\n", RegexOptions.Compiled);
Regex regSubname = new Regex(@"又名: (.+?)\n", RegexOptions.Compiled);
Regex regImdb = new Regex(@"IMDb: (.+?)$", RegexOptions.Compiled);
Regex regSite = new Regex(@"官方网站: (.+?)\n", RegexOptions.Compiled);
Regex regNameMath = new Regex(@"(.+第\w季|[\w\uff1a\uff01\uff0c\u00b7]+)\s*(.*)", RegexOptions.Compiled);
Regex regRole = new Regex(@"\([饰|配] (.+?)\)", RegexOptions.Compiled);
Regex regBackgroundImage = new Regex(@"url\((.+?)\)", RegexOptions.Compiled);
Regex regGender = new Regex(@"性别: \n(.+?)\n", RegexOptions.Compiled);
Regex regConstellation = new Regex(@"星座: \n(.+?)\n", RegexOptions.Compiled);
Regex regBirthdate = new Regex(@"出生日期: \n(.+?)\n", RegexOptions.Compiled);
Regex regLifedate = new Regex(@"生卒日期: \n(.+?) 至", RegexOptions.Compiled);
Regex regBirthplace = new Regex(@"出生地: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityRole = new Regex(@"职业: \n(.+?)\n", RegexOptions.Compiled);
Regex regNickname = new Regex(@"更多外文名: \n(.+?)\n", RegexOptions.Compiled);
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityImdb = new Regex(@"imdb编号: \n(.+?)\n", RegexOptions.Compiled);
/// <summary>
/// Initializes a new instance of the <see cref="DoubanApi"/> class.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public DoubanApi(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<DoubanApi>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
var handler = new HttpClientHandlerEx();
this.SetDoubanCookie(handler.CookieContainer);
httpClient = new HttpClient(handler, true);
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
}
private void SetDoubanCookie(CookieContainer cookieContainer)
{
var configCookie = Plugin.Instance?.Configuration.DoubanCookies.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(configCookie))
{
return;
}
var uri = new Uri("https://douban.com/");
var arr = configCookie.Split(';');
foreach (var str in arr)
{
var cookieArr = str.Split('=');
if (cookieArr.Length != 2)
{
continue;
}
var key = cookieArr[0].Trim();
var value = cookieArr[1].Trim();
Console.WriteLine($"key={key} value={value}");
try
{
cookieContainer.Add(new Cookie(key, value, "/", ".douban.com"));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
public async Task<List<DoubanSubject>> SearchAsync(string keyword, CancellationToken cancellationToken)
{
var list = new List<DoubanSubject>();
if (string.IsNullOrEmpty(keyword))
{
return list;
}
var cacheKey = $"search_{keyword}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
List<DoubanSubject> searchResult;
if (_memoryCache.TryGetValue<List<DoubanSubject>>(cacheKey, out searchResult))
{
return searchResult;
}
keyword = HttpUtility.UrlEncode(keyword);
var url = $"https://www.douban.com/search?cat=1002&q={keyword}";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return list;
}
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var context = BrowsingContext.New();
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
var movieElements = doc.QuerySelectorAll("div.result-list .result");
foreach (var movieElement in movieElements)
{
var rating = movieElement.GetText("div.rating-info>.rating_nums") ?? "0";
var img = movieElement.GetAttr("a.nbg>img", "src") ?? string.Empty;
var oncick = movieElement.GetAttr("div.title a", "onclick") ?? string.Empty;
var sid = oncick.GetMatchGroup(this.regSid);
var name = movieElement.GetText("div.title a") ?? string.Empty;
var titleStr = movieElement.GetText("div.title>h3>span") ?? string.Empty;
var cat = titleStr.GetMatchGroup(this.regCat);
var subjectStr = movieElement.GetText("div.rating-info>.subject-cast") ?? string.Empty;
var year = subjectStr.GetMatchGroup(this.regYear);
if (cat != "电影" && cat != "电视剧")
{
continue;
}
var movie = new DoubanSubject();
movie.Sid = sid;
movie.Name = name;
movie.Genre = cat;
movie.Img = img;
movie.Rating = rating.ToFloat();
movie.Year = year.ToInt();
list.Add(movie);
}
_memoryCache.Set<List<DoubanSubject>>(cacheKey, list, expiredOption);
return list;
}
public async Task<DoubanSubject?> GetMovieAsync(string sid, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(sid))
{
return null;
}
var cacheKey = $"movie_{sid}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
DoubanSubject? movie;
if (_memoryCache.TryGetValue<DoubanSubject?>(cacheKey, out movie))
{
return movie;
}
var url = $"https://movie.douban.com/subject/{sid}/";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return null;
}
movie = new DoubanSubject();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var context = BrowsingContext.New();
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
var contentNode = doc.QuerySelector("#content");
if (contentNode != null)
{
var nameStr = contentNode.GetText("h1>span:first-child");
var match = this.regNameMath.Match(nameStr);
var name = string.Empty;
var orginalName = string.Empty;
if (match.Success && match.Groups.Count == 3)
{
name = match.Groups[1].Value;
orginalName = match.Groups[2].Value;
}
var yearStr = contentNode.GetText("h1>span.year") ?? string.Empty;
var year = yearStr.GetMatchGroup(this.regYear);
var rating = contentNode.GetText("div.rating_self strong.rating_num") ?? "0";
var img = contentNode.GetAttr("a.nbgnbg>img", "src") ?? string.Empty;
var intro = contentNode.GetText("div.indent>span") ?? string.Empty;
intro = intro.Replace("©豆瓣", string.Empty);
var info = contentNode.GetText("#info") ?? string.Empty;
var director = info.GetMatchGroup(this.regDirector);
var writer = info.GetMatchGroup(this.regWriter);
var actor = info.GetMatchGroup(this.regActor);
var genre = info.GetMatchGroup(this.regGenre);
var country = info.GetMatchGroup(this.regCountry);
var language = info.GetMatchGroup(this.regLanguage);
var duration = info.GetMatchGroup(this.regDuration);
var screen = info.GetMatchGroup(this.regScreen);
var subname = info.GetMatchGroup(this.regSubname);
var imdb = info.GetMatchGroup(this.regImdb);
var site = info.GetMatchGroup(this.regSite);
movie.Sid = sid;
movie.Name = name;
movie.OriginalName = orginalName;
movie.Year = year.ToInt();
movie.Rating = rating.ToFloat();
movie.Img = img;
movie.Intro = intro;
movie.Subname = subname;
movie.Director = director;
movie.Genre = genre;
movie.Country = country;
movie.Language = language;
movie.Duration = duration;
movie.Screen = screen;
movie.Site = site;
movie.Actor = actor;
movie.Writer = writer;
movie.Imdb = imdb;
movie.Celebrities = new List<DoubanCelebrity>();
var celebrityNodes = contentNode.QuerySelectorAll("#celebrities li.celebrity");
foreach (var node in celebrityNodes)
{
var celebrityIdStr = node.GetAttr("div.info a.name", "href") ?? string.Empty;
var celebrityId = celebrityIdStr.GetMatchGroup(this.regId);
var celebrityImgStr = node.GetAttr("div.avatar", "style") ?? string.Empty;
var celebrityImg = celebrityImgStr.GetMatchGroup(this.regBackgroundImage);
var celebrityName = node.GetText("div.info a.name") ?? string.Empty;
var celebrityRole = node.GetText("div.info span.role") ?? string.Empty;
var celebrityRoleType = string.Empty;
var celebrity = new DoubanCelebrity();
celebrity.Id = celebrityId;
celebrity.Name = celebrityName;
celebrity.Role = celebrityRole;
celebrity.RoleType = celebrityRoleType;
celebrity.Img = celebrityImg;
movie.Celebrities.Add(celebrity);
}
_memoryCache.Set<DoubanSubject?>(cacheKey, movie, expiredOption);
return movie;
}
_memoryCache.Set<DoubanSubject?>(cacheKey, null, expiredOption);
return null;
}
public async Task<List<DoubanCelebrity>> GetCelebritiesBySidAsync(string sid, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(sid))
{
return new List<DoubanCelebrity>();
}
var cacheKey = $"celebrities_{sid}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
List<DoubanCelebrity> celebrities;
if (this._memoryCache.TryGetValue(cacheKey, out celebrities))
{
return celebrities;
}
var list = new List<DoubanCelebrity>();
var url = $"https://movie.douban.com/subject/{sid}/celebrities";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return new List<DoubanCelebrity>();
}
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var context = BrowsingContext.New();
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
var celebrityElements = doc.QuerySelectorAll("#content ul.celebrities-list li.celebrity");
foreach (var node in celebrityElements)
{
var celebrityIdStr = node.GetAttr("div.info a.name", "href") ?? string.Empty;
var celebrityId = celebrityIdStr.GetMatchGroup(this.regId);
var celebrityImgStr = node.GetAttr("div.avatar", "style") ?? string.Empty;
var celebrityImg = celebrityImgStr.GetMatchGroup(this.regBackgroundImage);
var celebrityNameStr = node.GetText("div.info a.name") ?? string.Empty;
var arr = celebrityNameStr.Split(" ");
var celebrityName = arr.Length > 1 ? arr[0] : string.Empty;
var celebrityRoleStr = node.GetText("div.info span.role") ?? string.Empty;
var celebrityRole = celebrityRoleStr.GetMatchGroup(this.regRole);
var arrRole = celebrityRoleStr.Split(" ");
var celebrityRoleType = arrRole.Length > 1 ? arrRole[0] : string.Empty;
if (string.IsNullOrEmpty(celebrityRole))
{
celebrityRole = celebrityRoleType;
}
if (celebrityRoleType != "导演" && celebrityRoleType != "配音" && celebrityRoleType != "演员")
{
continue;
}
var celebrity = new DoubanCelebrity();
celebrity.Id = celebrityId;
celebrity.Name = celebrityName;
celebrity.Role = celebrityRole;
celebrity.RoleType = celebrityRoleType;
celebrity.Img = celebrityImg;
list.Add(celebrity);
}
_memoryCache.Set<List<DoubanCelebrity>>(cacheKey, list.Take(15).ToList(), expiredOption);
return list;
}
public async Task<DoubanCelebrity?> GetCelebrityAsync(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
return null;
}
var cacheKey = $"celebrity_{id}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
DoubanCelebrity? celebrity;
if (_memoryCache.TryGetValue<DoubanCelebrity?>(cacheKey, out celebrity))
{
return celebrity;
}
var url = $"https://movie.douban.com/celebrity/{id}/";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
celebrity = new DoubanCelebrity();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var context = BrowsingContext.New();
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
var contentNode = doc.QuerySelector("#content");
if (contentNode != null)
{
var img = contentNode.GetAttr("#headline .nbg img", "src") ?? string.Empty;
var name = contentNode.GetText("h1") ?? string.Empty;
var intro = contentNode.GetText("#intro span.all") ?? string.Empty;
if (string.IsNullOrEmpty(intro))
{
intro = contentNode.GetText("#intro div.bd") ?? string.Empty;
}
var info = contentNode.GetText("div.info") ?? string.Empty;
var gender = info.GetMatchGroup(this.regGender);
var constellation = info.GetMatchGroup(this.regConstellation);
var birthdate = info.GetMatchGroup(this.regBirthdate);
var lifedate = info.GetMatchGroup(this.regLifedate);
if (string.IsNullOrEmpty(birthdate))
{
birthdate = lifedate;
}
var birthplace = info.GetMatchGroup(this.regBirthplace);
var role = info.GetMatchGroup(this.regCelebrityRole);
var nickname = info.GetMatchGroup(this.regNickname);
var family = info.GetMatchGroup(this.regFamily);
var imdb = info.GetMatchGroup(this.regCelebrityImdb);
celebrity.Img = img;
celebrity.Gender = gender;
celebrity.Birthdate = birthdate;
celebrity.Nickname = nickname;
celebrity.Imdb = imdb;
celebrity.Birthplace = birthplace;
celebrity.Name = name;
celebrity.Intro = intro;
celebrity.Constellation = constellation;
celebrity.Role = role;
_memoryCache.Set<DoubanCelebrity?>(cacheKey, celebrity, expiredOption);
return celebrity;
}
_memoryCache.Set<DoubanCelebrity?>(cacheKey, null, expiredOption);
return null;
}
public async Task<List<DoubanPhoto>> GetWallpaperBySidAsync(string sid, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(sid))
{
return new List<DoubanPhoto>();
}
var cacheKey = $"photo_{sid}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
List<DoubanPhoto> photos;
if (_memoryCache.TryGetValue<List<DoubanPhoto>>(cacheKey, out photos))
{
return photos;
}
var list = new List<DoubanPhoto>();
var url = $"https://movie.douban.com/subject/{sid}/photos?type=W&start=0&sortby=size&size=a&subtype=a";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return new List<DoubanPhoto>();
}
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var context = BrowsingContext.New();
var doc = await context.OpenAsync(req => req.Content(body), cancellationToken).ConfigureAwait(false);
var elements = doc.QuerySelectorAll(".poster-col3>li");
foreach (var node in elements)
{
var id = node.GetAttribute("data-id") ?? string.Empty;
var small = $"https://img2.doubanio.com/view/photo/s/public/p{id}.jpg";
var medium = $"https://img2.doubanio.com/view/photo/m/public/p{id}.jpg";
var large = $"https://img2.doubanio.com/view/photo/l/public/p{id}.jpg";
var size = node.GetText("div.prop") ?? string.Empty;
var width = string.Empty;
var height = string.Empty;
if (!string.IsNullOrEmpty(size))
{
var arr = size.Split('x');
if (arr.Length == 2)
{
width = arr[0];
height = arr[1];
}
}
var photo = new DoubanPhoto();
photo.Id = id;
photo.Size = size;
photo.Small = small;
photo.Medium = medium;
photo.Large = large;
photo.Width = width.ToInt();
photo.Height = height.ToInt();
list.Add(photo);
}
_memoryCache.Set<List<DoubanPhoto>>(cacheKey, list, expiredOption);
return list;
}
protected void LimitRequestFrequently()
{
var startTime = DateTime.Now;
lock (_lock)
{
var ts = DateTime.Now - lastRequestTime;
var diff = (int)(200 - ts.TotalMilliseconds);
if (diff > 0)
{
Thread.Sleep(diff);
}
lastRequestTime = DateTime.Now;
}
var endTime = DateTime.Now;
var tt = (endTime - startTime).TotalMilliseconds;
Console.WriteLine(tt);
}
private string? GetText(IElement el, string css)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.Text();
}
return null;
}
private string? GetAttr(IElement el, string css, string attr)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.GetAttribute(attr);
}
return null;
}
private string Match(string text, Regex reg)
{
var match = reg.Match(text);
if (match.Success && match.Groups.Count > 1)
{
return match.Groups[1].Value;
}
return string.Empty;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_memoryCache.Dispose();
}
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Api.Http
{
public class HttpClientHandlerEx : HttpClientHandler
{
public HttpClientHandlerEx()
{
// 忽略SSL证书问题
ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true;
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
CookieContainer = new CookieContainer();
UseCookies = true;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken);
}
}
}

View File

@ -0,0 +1,73 @@
using Jellyfin.Extensions.Json;
using Jellyfin.Plugin.MetaShark.Api.Http;
using Jellyfin.Plugin.MetaShark.Model;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Api
{
public class OmdbApi : IDisposable
{
public const string DEFAULT_API_KEY = "2c9d9507";
private readonly ILogger<DoubanApi> _logger;
private readonly IMemoryCache _memoryCache;
private readonly HttpClient httpClient;
public OmdbApi(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<DoubanApi>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
}
public async Task<OmdbItem?> GetByImdbID(string id, CancellationToken cancellationToken)
{
var cacheKey = $"GetByImdbID_{id}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
OmdbItem? item;
if (this._memoryCache.TryGetValue(cacheKey, out item))
{
return item;
}
try
{
var url = $"http://www.omdbapi.com/?i={id}&apikey={DEFAULT_API_KEY}";
item = await this.httpClient.GetFromJsonAsync<OmdbItem>(url, cancellationToken).ConfigureAwait(false);
_memoryCache.Set(cacheKey, item, expiredOption);
return item;
}
catch (Exception ex)
{
this._logger.LogError(ex, "GetByImdbID error. id: {0}", id);
_memoryCache.Set<OmdbItem?>(cacheKey, null, expiredOption);
return null;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_memoryCache.Dispose();
}
}
}
}

View File

@ -0,0 +1,558 @@
using Jellyfin.Plugin.MetaShark.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Client;
using TMDbLib.Objects.Collections;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Movies;
using TMDbLib.Objects.People;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.TvShows;
namespace Jellyfin.Plugin.MetaShark.Api
{
public class TmdbApi : IDisposable
{
public const string DEFAULT_API_KEY = "4219e299c89411838049ab0dab19ebd5";
private const int CacheDurationInHours = 1;
private readonly ILogger<TmdbApi> _logger;
private readonly IMemoryCache _memoryCache;
private readonly TMDbClient _tmDbClient;
/// <summary>
/// Initializes a new instance of the <see cref="TmdbApi"/> class.
/// </summary>
public TmdbApi(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TmdbApi>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
var config = Plugin.Instance?.Configuration ?? new PluginConfiguration();
var apiKey = string.IsNullOrEmpty(config.TmdbApiKey) ? DEFAULT_API_KEY : config.TmdbApiKey;
_tmDbClient = new TMDbClient(apiKey);
_tmDbClient.RequestTimeout = TimeSpan.FromSeconds(10);
// Not really interested in NotFoundException
_tmDbClient.ThrowApiExceptions = false;
}
/// <summary>
/// Gets a movie from the TMDb API based on its TMDb id.
/// </summary>
/// <param name="tmdbId">The movie's TMDb id.</param>
/// <param name="language">The movie's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie or null if not found.</returns>
public async Task<Movie?> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
{
var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out Movie movie))
{
return movie;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
movie = await _tmDbClient.GetMovieAsync(
tmdbId,
NormalizeLanguage(language),
GetImageLanguagesParam(imageLanguages),
MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
cancellationToken).ConfigureAwait(false);
if (movie != null)
{
_memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours));
}
return movie;
}
/// <summary>
/// Gets a collection from the TMDb API based on its TMDb id.
/// </summary>
/// <param name="tmdbId">The collection's TMDb id.</param>
/// <param name="language">The collection's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection or null if not found.</returns>
public async Task<Collection?> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
{
var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out Collection collection))
{
return collection;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
collection = await _tmDbClient.GetCollectionAsync(
tmdbId,
NormalizeLanguage(language),
imageLanguages,
CollectionMethods.Images,
cancellationToken).ConfigureAwait(false);
if (collection != null)
{
_memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours));
}
return collection;
}
/// <summary>
/// Gets a tv show from the TMDb API based on its TMDb id.
/// </summary>
/// <param name="tmdbId">The tv show's TMDb id.</param>
/// <param name="language">The tv show's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information or null if not found.</returns>
public async Task<TvShow?> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
{
var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out TvShow series))
{
return series;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
series = await _tmDbClient.GetTvShowAsync(
tmdbId,
language: NormalizeLanguage(language),
includeImageLanguage: GetImageLanguagesParam(imageLanguages),
extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (series != null)
{
_memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours));
}
return series;
}
/// <summary>
/// Gets a tv season from the TMDb API based on the tv show's TMDb id.
/// </summary>
/// <param name="tvShowId">The tv season's TMDb id.</param>
/// <param name="seasonNumber">The season number.</param>
/// <param name="language">The tv season's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv season information or null if not found.</returns>
public async Task<TvSeason?> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
{
var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out TvSeason season))
{
Console.WriteLine("#season from cache.");
return season;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
season = await _tmDbClient.GetTvSeasonAsync(
tvShowId,
seasonNumber,
language: NormalizeLanguage(language),
includeImageLanguage: GetImageLanguagesParam(imageLanguages),
extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (season != null)
{
_memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours));
}
return season;
}
/// <summary>
/// Gets a movie from the TMDb API based on the tv show's TMDb id.
/// </summary>
/// <param name="tvShowId">The tv show's TMDb id.</param>
/// <param name="seasonNumber">The season number.</param>
/// <param name="episodeNumber">The episode number.</param>
/// <param name="language">The episode's language.</param>
/// <param name="imageLanguages">A comma-separated list of image languages.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns>
public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken)
{
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode episode))
{
return episode;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
episode = await _tmDbClient.GetTvEpisodeAsync(
tvShowId,
seasonNumber,
episodeNumber,
language: NormalizeLanguage(language),
includeImageLanguage: GetImageLanguagesParam(imageLanguages),
extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (episode != null)
{
_memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours));
}
return episode;
}
/// <summary>
/// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
/// </summary>
/// <param name="personTmdbId">The person's TMDb id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information or null if not found.</returns>
public async Task<Person?> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
{
var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}";
if (_memoryCache.TryGetValue(key, out Person person))
{
return person;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
person = await _tmDbClient.GetPersonAsync(
personTmdbId,
PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
cancellationToken).ConfigureAwait(false);
if (person != null)
{
_memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
}
return person;
}
/// <summary>
/// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
/// </summary>
/// <param name="externalId">The item's external id.</param>
/// <param name="source">The source of the id eg. IMDb.</param>
/// <param name="language">The item's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb item or null if not found.</returns>
public async Task<FindContainer?> FindByExternalIdAsync(
string externalId,
FindExternalSource source,
string language,
CancellationToken cancellationToken)
{
var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out FindContainer result))
{
return result;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
result = await _tmDbClient.FindAsync(
source,
externalId,
NormalizeLanguage(language),
cancellationToken).ConfigureAwait(false);
if (result != null)
{
_memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
}
return result;
}
/// <summary>
/// Searches for a tv show using the TMDb API based on its name.
/// </summary>
/// <param name="name">The name of the tv show.</param>
/// <param name="language">The tv show's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns>
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
{
var key = $"searchseries-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
{
return series.Results;
}
try
{
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
.SearchTvShowAsync(name, NormalizeLanguage(language), cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults.Results;
}
catch (Exception ex)
{
return new List<SearchTv>();
}
}
/// <summary>
/// Searches for a person based on their name using the TMDb API.
/// </summary>
/// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information.</returns>
public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
{
var key = $"searchperson-{name}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
{
return person.Results;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
.SearchPersonAsync(name, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults.Results;
}
/// <summary>
/// Searches for a movie based on its name using the TMDb API.
/// </summary>
/// <param name="name">The name of the movie.</param>
/// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns>
public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
{
return SearchMovieAsync(name, 0, language, cancellationToken);
}
/// <summary>
/// Searches for a movie based on its name using the TMDb API.
/// </summary>
/// <param name="name">The name of the movie.</param>
/// <param name="year">The release year of the movie.</param>
/// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns>
public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
{
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
{
return movies.Results;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
.SearchMovieAsync(name, NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults.Results;
}
/// <summary>
/// Searches for a collection based on its name using the TMDb API.
/// </summary>
/// <param name="name">The name of the collection.</param>
/// <param name="language">The collection's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection information.</returns>
public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
{
var key = $"collectionsearch-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
{
return collections.Results;
}
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
.SearchCollectionAsync(name, NormalizeLanguage(language), cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
return searchResults.Results;
}
/// <summary>
/// Gets the absolute URL of the poster.
/// </summary>
/// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
public string? GetPosterUrl(string posterPath)
{
if (string.IsNullOrEmpty(posterPath))
{
return null;
}
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
}
/// <summary>
/// Gets the absolute URL of the backdrop image.
/// </summary>
/// <param name="posterPath">The relative URL of the backdrop image.</param>
/// <returns>The absolute URL.</returns>
public string? GetBackdropUrl(string posterPath)
{
if (string.IsNullOrEmpty(posterPath))
{
return null;
}
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
}
/// <summary>
/// Gets the absolute URL of the profile image.
/// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns>
public string? GetProfileUrl(string actorProfilePath)
{
if (string.IsNullOrEmpty(actorProfilePath))
{
return null;
}
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
}
/// <summary>
/// Gets the absolute URL of the still image.
/// </summary>
/// <param name="filePath">The relative URL of the still image.</param>
/// <returns>The absolute URL.</returns>
public string? GetStillUrl(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return null;
}
return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose.
/// </summary>
/// <param name="disposing">Dispose all members.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_memoryCache.Dispose();
_tmDbClient.Dispose();
}
}
private Task EnsureClientConfigAsync()
{
return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
}
/// <summary>
/// Normalizes a language string for use with TMDb's language parameter.
/// </summary>
/// <param name="language">The language code.</param>
/// <returns>The normalized language code.</returns>
public string NormalizeLanguage(string language)
{
if (string.IsNullOrEmpty(language))
{
return language;
}
// They require this to be uppercase
// Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
// See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
var parts = language.Split('-');
if (parts.Length == 2)
{
language = parts[0] + "-" + parts[1].ToUpperInvariant();
}
return language;
}
public string GetImageLanguagesParam(string preferredLanguage)
{
var languages = new List<string>();
if (!string.IsNullOrEmpty(preferredLanguage))
{
preferredLanguage = NormalizeLanguage(preferredLanguage);
languages.Add(preferredLanguage);
if (preferredLanguage.Length == 5) // like en-US
{
// Currently, TMDB supports 2-letter language codes only
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
}
}
languages.Add("null");
if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
{
languages.Add("en");
}
return string.Join(',', languages);
}
}
}

View File

@ -0,0 +1,43 @@
using MediaBrowser.Model.Plugins;
using System.Net;
using System.Reflection;
namespace Jellyfin.Plugin.MetaShark.Configuration;
/// <summary>
/// The configuration options.
/// </summary>
public enum SomeOptions
{
/// <summary>
/// Option one.
/// </summary>
OneOption,
/// <summary>
/// Second option.
/// </summary>
AnotherOption
}
/// <summary>
/// Plugin configuration.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
public string Pattern { get; set; } = @"(S\d{2}|E\d{2}|HDR|\d{3,4}p|WEBRip|WEB|YIFY|BrRip|BluRay|H265|H264|x264|AAC\.\d\.\d|AAC|HDTV|mkv|mp4)|(\[.*\])|(\-\w+|\{.*\}|【.*】|\(.*\)|\d+MB)|(\.|\-)";
public bool EnableTmdb { get; set; } = true;
public string TmdbApiKey { get; set; } = string.Empty;
public string DoubanCookies { get; set; } = string.Empty;
public int MaxCastMembers { get; set; } = 15;
public int MaxSearchResult { get; set; } = 3;
}

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Template</title>
</head>
<body>
<div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<div class="verticalSection verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">MetaShark 配置</h2><span id="current_version" name="current_version"
is="emby-linkbutton" class="emby-button"></span>
<a is="emby-linkbutton" class="raised button-alt headerHelpButton emby-button" target="_blank"
href="https://github.com/cxfksword/jellyfin-plugin-metashark">源码</a>
</div>
</div>
<form id="TemplateConfigForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie</label>
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
class="emby-input"></textarea>
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片.(需重启才能生效)</div>
</div>
<fieldset class="verticalSection verticalSection-extrabottompadding">
<legend>
<h3>TheMovieDb</h3>
</legend>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableTmdb" name="EnableTmdb" type="checkbox" is="emby-checkbox" />
<span>启用从TheMovieDb获取元数据</span>
</label>
<div class="fieldDescription">勾选后会尝试从TheMovieDb获取季度和剧集元数据</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="TmdbApiKey">Api Key</label>
<input id="TmdbApiKey" name="TmdbApiKey" type="text" is="emby-input" />
<div class="fieldDescription">填写自定义Api Key不填写会使用默认api key.(需重启才能生效)</div>
</div>
</fieldset>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</form>
</div>
</div>
<script type="text/javascript">
var TemplateConfig = {
pluginUniqueId: '9A19103F-16F7-4668-BE54-9A1E7A4F7556'
};
document.querySelector('#TemplateConfigPage')
.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
document.querySelector('#current_version').value = "v" + config.Version;
document.querySelector('#DoubanCookies').value = config.DoubanCookies;
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
Dashboard.hideLoadingMsg();
});
});
document.querySelector('#TemplateConfigForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
config.DoubanCookies = document.querySelector('#DoubanCookies').value;
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
e.preventDefault();
return false;
});
</script>
</div>
</body>
</html>

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.IO;
using MediaBrowser.Controller.Providers;
using Jellyfin.Plugin.MetaShark.Providers;
using System.Runtime.InteropServices;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Common.Net;
namespace Jellyfin.Plugin.MetaShark.Controllers
{
[ApiController]
[AllowAnonymous]
[Route("/plugin/metashark")]
public class MetaSharkController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
/// <summary>
/// Initializes a new instance of the <see cref="MetaSharkController"/> class.
/// </summary>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
public MetaSharkController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
/// <summary>
/// 获取弹幕文件内容.
/// </summary>
/// <returns>xml弹幕文件内容</returns>
[Route("proxy/image")]
[HttpGet]
public async Task<Stream> ProxyImage(string url)
{
if (string.IsNullOrEmpty(url))
{
throw new ResourceNotFoundException();
}
var httpClient = GetHttpClient();
return await httpClient.GetStreamAsync(url).ConfigureAwait(false);
}
private HttpClient GetHttpClient()
{
var client = _httpClientFactory.CreateClient(NamedClient.Default);
return client;
}
}
}

View File

@ -0,0 +1,34 @@
using AngleSharp.Dom;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Core
{
public static class ElementExtension
{
public static string? GetText(this IElement el, string css)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.Text().Trim();
}
return null;
}
public static string? GetAttr(this IElement el, string css, string attr)
{
var node = el.QuerySelector(css);
if (node != null)
{
return node.GetAttribute(attr).Trim();
}
return null;
}
}
}

View File

@ -0,0 +1,20 @@
using AngleSharp.Dom;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Core
{
public static class JsonExtension
{
public static string ToJson(this object obj)
{
if (obj == null) return string.Empty;
return JsonSerializer.Serialize(obj);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Core
{
public static class ListExtension
{
public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)
=> self.Select((item, index) => (item, index));
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using StringMetric;
namespace Jellyfin.Plugin.MetaShark.Core
{
public static class StringExtension
{
public static long ToLong(this string s)
{
long val;
if (long.TryParse(s, out val))
{
return val;
}
return 0;
}
public static int ToInt(this string s)
{
int val;
if (int.TryParse(s, out val))
{
return val;
}
return 0;
}
public static float ToFloat(this string s)
{
float val;
if (float.TryParse(s, out val))
{
return val;
}
return 0.0f;
}
public static double Distance(this string s1, string s2)
{
var jw = new JaroWinkler();
return jw.Similarity(s1, s2);
}
public static string GetMatchGroup(this string text, Regex reg)
{
var match = reg.Match(text);
if (match.Success && match.Groups.Count > 1)
{
return match.Groups[1].Value;
}
return string.Empty;
}
}
}

View File

@ -0,0 +1,197 @@
/*
* The MIT License
*
* Copyright 2016 feature[23]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
using System;
using System.Linq;
// ReSharper disable SuggestVarOrType_Elsewhere
// ReSharper disable LoopCanBeConvertedToQuery
namespace StringMetric
{
/// The JaroWinkler distance metric is designed and best suited for short
/// strings such as person names, and to detect typos; it is (roughly) a
/// variation of Damerau-Levenshtein, where the substitution of 2 close
/// characters is considered less important then the substitution of 2 characters
/// that a far from each other.
/// Jaro-Winkler was developed in the area of record linkage (duplicate
/// detection) (Winkler, 1990). It returns a value in the interval [0.0, 1.0].
/// The distance is computed as 1 - Jaro-Winkler similarity.
public class JaroWinkler
{
private const double DEFAULT_THRESHOLD = 0.7;
private const int THREE = 3;
private const double JW_COEF = 0.1;
/// <summary>
/// The current value of the threshold used for adding the Winkler bonus. The default value is 0.7.
/// </summary>
private double Threshold { get; }
/// <summary>
/// Creates a new instance with default threshold (0.7)
/// </summary>
public JaroWinkler()
{
Threshold = DEFAULT_THRESHOLD;
}
/// <summary>
/// Creates a new instance with given threshold to determine when Winkler bonus should
/// be used. Set threshold to a negative value to get the Jaro distance.
/// </summary>
/// <param name="threshold"></param>
public JaroWinkler(double threshold)
{
Threshold = threshold;
}
/// <summary>
/// Compute Jaro-Winkler similarity.
/// </summary>
/// <param name="s1">The first string to compare.</param>
/// <param name="s2">The second string to compare.</param>
/// <returns>The Jaro-Winkler similarity in the range [0, 1]</returns>
/// <exception cref="ArgumentNullException">If s1 or s2 is null.</exception>
public double Similarity(string s1, string s2)
{
if (s1 == null)
{
throw new ArgumentNullException(nameof(s1));
}
if (s2 == null)
{
throw new ArgumentNullException(nameof(s2));
}
if (s1.Equals(s2))
{
return 1f;
}
int[] mtp = Matches(s1, s2);
float m = mtp[0];
if (m == 0)
{
return 0f;
}
double j = ((m / s1.Length + m / s2.Length + (m - mtp[1]) / m))
/ THREE;
double jw = j;
if (j > Threshold)
{
jw = j + Math.Min(JW_COEF, 1.0 / mtp[THREE]) * mtp[2] * (1 - j);
}
return jw;
}
/// <summary>
/// Return 1 - similarity.
/// </summary>
/// <param name="s1">The first string to compare.</param>
/// <param name="s2">The second string to compare.</param>
/// <returns>1 - similarity</returns>
/// <exception cref="ArgumentNullException">If s1 or s2 is null.</exception>
public double Distance(string s1, string s2)
=> 1.0 - Similarity(s1, s2);
private static int[] Matches(string s1, string s2)
{
string max, min;
if (s1.Length > s2.Length)
{
max = s1;
min = s2;
}
else
{
max = s2;
min = s1;
}
int range = Math.Max(max.Length / 2 - 1, 0);
//int[] matchIndexes = new int[min.Length];
//Arrays.fill(matchIndexes, -1);
int[] match_indexes = Enumerable.Repeat(-1, min.Length).ToArray();
bool[] match_flags = new bool[max.Length];
int matches = 0;
for (int mi = 0; mi < min.Length; mi++)
{
char c1 = min[mi];
for (int xi = Math.Max(mi - range, 0),
xn = Math.Min(mi + range + 1, max.Length); xi < xn; xi++)
{
if (!match_flags[xi] && c1 == max[xi])
{
match_indexes[mi] = xi;
match_flags[xi] = true;
matches++;
break;
}
}
}
char[] ms1 = new char[matches];
char[] ms2 = new char[matches];
for (int i = 0, si = 0; i < min.Length; i++)
{
if (match_indexes[i] != -1)
{
ms1[si] = min[i];
si++;
}
}
for (int i = 0, si = 0; i < max.Length; i++)
{
if (match_flags[i])
{
ms2[si] = max[i];
si++;
}
}
int transpositions = 0;
for (int mi = 0; mi < ms1.Length; mi++)
{
if (ms1[mi] != ms2[mi])
{
transpositions++;
}
}
int prefix = 0;
for (int mi = 0; mi < min.Length; mi++)
{
if (s1[mi] == s2[mi])
{
prefix++;
}
else
{
break;
}
}
return new[] { matches, transpositions / 2, prefix, max.Length };
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Core
{
public static class Utils
{
public static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{
// Unix timestamp is seconds past epoch
DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dateTime = dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
return dateTime;
}
}
}

View File

@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
<GenerateDocumentationFile>False</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.17.1" />
<PackageReference Include="AnitomySharp" Version="0.2.0" />
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup>
<ItemGroup>
<None Include="Vendor\TMDbLib\TMDbLib.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,133 @@
using MediaBrowser.Model.Entities;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Model
{
public class DoubanSubject
{
// "name": "哈利·波特与魔法石",
public string Name { get; set; }
// "originalName": "Harry Potter and the Sorcerer's Stone",
public string OriginalName { get; set; }
// "rating": "9.1",
public float Rating { get; set; }
// "img": "https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2614949805.webp",
public string Img { get; set; }
// "sid": "1295038",
public string Sid { get; set; }
// "year": "2001",
public int Year { get; set; }
// "director": "克里斯·哥伦布",
public string Director { get; set; }
// "writer": "史蒂夫·克洛夫斯 / J·K·罗琳",
public string Writer { get; set; }
// "actor": "丹尼尔·雷德克里夫 / 艾玛·沃森 / 鲁伯特·格林特 / 艾伦·瑞克曼 / 玛吉·史密斯 / 更多...",
public string Actor { get; set; }
// "genre": "奇幻 / 冒险",
public string Genre { get; set; }
// "site": "www.harrypotter.co.uk",
public string Site { get; set; }
// "country": "美国 / 英国",
public string Country { get; set; }
// "language": "英语",
public string Language { get; set; }
// "screen": "2002-01-26(中国大陆) / 2020-08-14(中国大陆重映) / 2001-11-04(英国首映) / 2001-11-16(美国)",
public string Screen { get; set; }
public DateTime? ScreenTime
{
get
{
if (Screen == null) return null;
var items = Screen.Split("/");
if (items.Length >= 0)
{
var item = items[0].Split("(")[0];
DateTime result;
DateTime.TryParseExact(item, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out result);
return result;
}
return null;
}
}
// "duration": "152分钟 / 159分钟(加长版)",
public string Duration { get; set; }
// "subname": "哈利波特1神秘的魔法石(港/台) / 哈1 / Harry Potter and the Philosopher's Stone",
public string Subname { get; set; }
// "imdb": "tt0241527"
public string Imdb { get; set; }
public string Intro { get; set; }
public List<DoubanCelebrity> Celebrities { get; set; }
[JsonIgnore]
public string ImgMiddle
{
get
{
return this.Img.Replace("s_ratio_poster", "m");
}
}
[JsonIgnore]
public string[] Genres
{
get
{
return this.Genre.Split("/").Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToArray();
}
}
}
public class DoubanCelebrity
{
public string Id { get; set; }
public string Name { get; set; }
public string Img { get; set; }
public string Role { get; set; }
public string Intro { get; set; }
public string Gender { get; set; }
public string Constellation { get; set; }
public string Birthdate { get; set; }
public string Birthplace { get; set; }
public string Nickname { get; set; }
public string Imdb { get; set; }
public string Site { get; set; }
private string _roleType;
public string RoleType
{
get
{
if (string.IsNullOrEmpty(this._roleType))
{
return this.Role.Equals("导演", StringComparison.Ordinal) ? MediaBrowser.Model.Entities.PersonType.Director : MediaBrowser.Model.Entities.PersonType.Actor;
}
return this._roleType.Equals("导演", StringComparison.Ordinal) ? MediaBrowser.Model.Entities.PersonType.Director : MediaBrowser.Model.Entities.PersonType.Actor;
}
set
{
_roleType = value;
}
}
}
public class DoubanPhoto
{
public string Id { get; set; }
public string Small { get; set; }
public string Medium { get; set; }
public string Large { get; set; }
public string Size { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Model
{
public static class MetaSource
{
public const string Douban = "douban";
public const string Tmdb = "tmdb";
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Model
{
public class OmdbItem
{
[JsonPropertyName("imdbID")]
public string ImdbID { get; set; }
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Jellyfin.Plugin.MetaShark.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.MetaShark;
/// <summary>
/// The main plugin.
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
/// <summary>
/// Gets the provider name.
/// </summary>
public const string PluginName = "MetaShark";
/// <summary>
/// Gets the provider id.
/// </summary>
public const string ProviderId = "MetaSharkID";
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
/// </summary>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <inheritdoc />
public override string Name => PluginName;
/// <inheritdoc />
public override Guid Id => Guid.Parse("9A19103F-16F7-4668-BE54-9A1E7A4F7556");
/// <summary>
/// Gets the current plugin instance.
/// </summary>
public static Plugin? Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = this.Name,
EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace)
}
};
}
}

View File

@ -0,0 +1,310 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using StringMetric;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using TMDbLib.Objects.General;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public abstract class BaseProvider
{
/// <summary>
/// Gets the provider name.
/// </summary>
public const string DoubanProviderName = "Douban";
/// <summary>
/// Gets the provider id.
/// </summary>
public const string DoubanProviderId = "DoubanID";
/// <summary>
/// Name of the provider.
/// </summary>
public const string TmdbProviderName = "TheMovieDb";
protected readonly Configuration.PluginConfiguration _config;
protected readonly ILogger _logger;
protected readonly IHttpClientFactory _httpClientFactory;
protected readonly DoubanApi _doubanApi;
protected readonly TmdbApi _tmdbApi;
protected readonly OmdbApi _omdbApi;
protected readonly ILibraryManager _libraryManager;
protected Regex regMetaSourcePrefix = new Regex(@"^\[.+\]", RegexOptions.Compiled);
public string Pattern
{
get
{
return this._config.Pattern;
}
}
protected BaseProvider(IHttpClientFactory httpClientFactory, ILogger logger, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
{
this._doubanApi = doubanApi;
this._tmdbApi = tmdbApi;
this._omdbApi = omdbApi;
this._libraryManager = libraryManager;
this._logger = logger;
this._httpClientFactory = httpClientFactory;
this._config = Plugin.Instance == null ?
new Configuration.PluginConfiguration() :
Plugin.Instance.Configuration;
}
protected async Task<string?> GuestByDoubanAsync(ItemLookupInfo info, CancellationToken cancellationToken)
{
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = this._libraryManager.ParseName(info.Name);
this.Log($"GuestByDouban of [name]: {info.Name} year: {info.Year} search name: {parsedName.Name}");
var result = await this._doubanApi.SearchAsync(parsedName.Name, cancellationToken).ConfigureAwait(false);
var jw = new JaroWinkler();
foreach (var item in result)
{
if (jw.Similarity(parsedName.Name, item.Name) < 0.8)
{
continue;
}
if (parsedName.Year == null || parsedName.Year == 0)
{
this.Log($"GuestByDouban of [name] found Sid: \"{item.Sid}\"");
return item.Sid;
}
if (parsedName.Year == item.Year)
{
this.Log($"GuestByDouban of [name] found Sid: \"{item.Sid}\"");
return item.Sid;
}
}
return null;
}
protected async Task<string?> GuestSeasonByDoubanAsync(string name, int? year, CancellationToken cancellationToken)
{
if (year == null || year == 0)
{
return null;
}
this.Log($"GuestSeasonByDouban of [name]: {name} year: {year}");
var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false);
var jw = new JaroWinkler();
foreach (var item in result)
{
this.Log($"GuestSeasonByDouban name: {name} item.Name: {item.Name} score: {jw.Similarity(name, item.Name)} ");
if (jw.Similarity(name, item.Name) < 0.8)
{
continue;
}
if (year == item.Year)
{
this.Log($"GuestSeasonByDouban of [name] found Sid: \"{item.Sid}\"");
return item.Sid;
}
}
return null;
}
protected async Task<string?> GuestByTmdbAsync(ItemLookupInfo info, CancellationToken cancellationToken)
{
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = this._libraryManager.ParseName(info.Name);
this.Log($"GuestByTmdb of [name]: {info.Name} search name: {parsedName.Name}");
var jw = new JaroWinkler();
switch (info)
{
case MovieInfo:
var movieResults = await this._tmdbApi.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
foreach (var item in movieResults)
{
if (jw.Similarity(parsedName.Name, item.Title) > 0.8)
{
this.Log($"GuestByTmdb of [name] found tmdb id: \"{item.Id}\"");
return item.Id.ToString(CultureInfo.InvariantCulture);
}
}
break;
case SeriesInfo:
var seriesResults = await this._tmdbApi.SearchSeriesAsync(parsedName.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
foreach (var item in seriesResults)
{
if (jw.Similarity(parsedName.Name, item.Name) > 0.8)
{
this.Log($"GuestByTmdb of [name] found tmdb id: \"{item.Id}\"");
return item.Id.ToString(CultureInfo.InvariantCulture);
}
}
break;
}
return null;
}
protected string AppendMetaSourcePrefix(string name, string source)
{
if (string.IsNullOrEmpty(name))
{
return name;
}
return $"[{source}]{name}";
}
protected string RemoveMetaSourcePrefix(string name)
{
if (string.IsNullOrEmpty(name))
{
return name;
}
return regMetaSourcePrefix.Replace(name, string.Empty);
}
protected string GetProxyImageUrl(string url)
{
var encodedUrl = HttpUtility.UrlEncode(url);
return $"/plugin/metashark/proxy/image/?url={encodedUrl}";
}
protected void Log(string? message, params object?[] args)
{
this._logger.LogInformation($"[MetaShark] {message}", args);
}
/// <summary>
/// Adjusts the image's language code preferring the 5 letter language code eg. en-US.
/// </summary>
/// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param>
/// <returns>The language code.</returns>
protected string AdjustImageLanguage(string imageLanguage, string requestLanguage)
{
if (!string.IsNullOrEmpty(imageLanguage)
&& !string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2
&& imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
{
return requestLanguage;
}
return imageLanguage;
}
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
/// </summary>
/// <param name="crew">Crew member to map against the Jellyfin person types.</param>
/// <returns>The Jellyfin person type.</returns>
[SuppressMessage("Microsoft.Maintainability", "CA1309: Use ordinal StringComparison", Justification = "AFAIK we WANT InvariantCulture comparisons here and not Ordinal")]
public string MapCrewToPersonType(Crew crew)
{
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
&& crew.Job.Contains("director", StringComparison.InvariantCultureIgnoreCase))
{
return PersonType.Director;
}
if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase)
&& crew.Job.Contains("producer", StringComparison.InvariantCultureIgnoreCase))
{
return PersonType.Producer;
}
if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase))
{
return PersonType.Writer;
}
return string.Empty;
}
/// <summary>
/// Normalizes a language string for use with TMDb's include image language parameter.
/// </summary>
/// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param>
/// <returns>The comma separated language string.</returns>
public static string GetImageLanguagesParam(string preferredLanguage)
{
var languages = new List<string>();
if (!string.IsNullOrEmpty(preferredLanguage))
{
preferredLanguage = NormalizeLanguage(preferredLanguage);
languages.Add(preferredLanguage);
if (preferredLanguage.Length == 5) // like en-US
{
// Currently, TMDB supports 2-letter language codes only
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
}
}
languages.Add("null");
if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
{
languages.Add("en");
}
return string.Join(',', languages);
}
/// <summary>
/// Normalizes a language string for use with TMDb's language parameter.
/// </summary>
/// <param name="language">The language code.</param>
/// <returns>The normalized language code.</returns>
public static string NormalizeLanguage(string language)
{
if (string.IsNullOrEmpty(language))
{
return language;
}
// They require this to be uppercase
// Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
// See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
var parts = language.Split('-');
if (parts.Length == 2)
{
language = parts[0] + "-" + parts[1].ToUpperInvariant();
}
return language;
}
}
}

View File

@ -0,0 +1,118 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Languages;
using static System.Net.Mime.MediaTypeNames;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class EpisodeImageProvider : BaseProvider, IRemoteImageProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
public EpisodeImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
/// <inheritdoc />
public string Name => Plugin.PluginName;
/// <inheritdoc />
public bool Supports(BaseItem item) => item is Episode;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Primary;
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
Console.WriteLine(item.ProviderIds.ToJson());
this.Log($"GetEpisodeImages for item: {item.Name} number: {item.IndexNumber}");
var episode = (MediaBrowser.Controller.Entities.TV.Episode)item;
var series = episode.Series;
var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
if (seriesTmdbId <= 0)
{
this.Log($"Got images failed because the seriesTmdbId is empty!");
return Enumerable.Empty<RemoteImageInfo>();
}
var seasonNumber = episode.ParentIndexNumber;
var episodeNumber = episode.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
this.Log($"Got images failed because the seasonNumber or episodeNumber is empty! seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
// 利用season缓存取剧集信息会更快
var seasonResult = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId, seasonNumber.Value, language, language, cancellationToken)
.ConfigureAwait(false);
Console.WriteLine($"seasonResult.Episodes.Count={seasonResult?.Episodes.Count}");
if (seasonResult == null || seasonResult.Episodes.Count < episodeNumber.Value)
{
this.Log($"Not valid season data for seasonNumber: {seasonNumber} episodeNumber: {episodeNumber}");
return Enumerable.Empty<RemoteImageInfo>();
}
var result = new List<RemoteImageInfo>();
var episodeResult = seasonResult.Episodes[episodeNumber.Value - 1];
Console.WriteLine(episodeResult.ToJson());
if (!string.IsNullOrEmpty(episodeResult.StillPath))
{
result.Add(new RemoteImageInfo
{
Url = this._tmdbApi.GetStillUrl(episodeResult.StillPath),
CommunityRating = episodeResult.VoteAverage,
VoteCount = episodeResult.VoteCount,
ProviderName = Name,
Type = ImageType.Primary
});
}
return result;
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}
}
}

View File

@ -0,0 +1,149 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class EpisodeProvider : BaseProvider, IRemoteMetadataProvider<Episode, EpisodeInfo>
{
private static readonly Regex[] EpisodeFileNameRegex =
{
new(@"\[([\d\.]{2,})\]"),
new(@"- ?([\d\.]{2,})"),
new(@"EP?([\d\.]{2,})", RegexOptions.IgnoreCase),
new(@"\[([\d\.]{2,})"),
new(@"#([\d\.]{2,})"),
new(@"(\d{2,})")
};
public EpisodeProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
public string Name => Plugin.PluginName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSearchResults of [name]: {info.Name}");
return await Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
}
/// <inheritdoc />
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
this.Log($"GetEpisodeMetadata of [name]: {info.Name} number: {info.IndexNumber}");
Console.WriteLine(info.ToJson());
Console.WriteLine(info.SeriesProviderIds.ToJson());
var result = new MetadataResult<Episode>();
// 剧集信息只有tmdb有
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
var seasonNumber = info.ParentIndexNumber; // 没有season级目录时会为null
var episodeNumber = info.IndexNumber;
var indexNumberEnd = info.IndexNumberEnd;
Console.WriteLine($"seasonNumber:{seasonNumber} episodeNumber:{episodeNumber} indexNumberEnd:{indexNumberEnd}");
if (episodeNumber is null or 0)
{
// 从文件名获取剧集的indexNumber
var fileName = Path.GetFileName(info.Path) ?? string.Empty;
episodeNumber = this.GuessEpisodeNumber(episodeNumber, fileName);
this.Log("GuessEpisodeNumber: fileName: {0} episodeNumber: {1}", fileName, episodeNumber);
}
if (episodeNumber is null or 0 || seasonNumber is null or 0 || string.IsNullOrEmpty(seriesTmdbId))
{
return result;
}
// 利用season缓存取剧集信息会更快
var seasonResult = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId.ToInt(), seasonNumber.Value, info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
if (seasonResult == null || seasonResult.Episodes.Count < episodeNumber.Value)
{
return result;
}
var episodeResult = seasonResult.Episodes[episodeNumber.Value - 1];
result.HasMetadata = true;
result.QueriedById = true;
if (!string.IsNullOrEmpty(episodeResult.Overview))
{
// if overview is non-empty, we can assume that localized data was returned
result.ResultLanguage = info.MetadataLanguage;
}
var item = new Episode
{
IndexNumber = episodeNumber,
ParentIndexNumber = seasonNumber,
IndexNumberEnd = info.IndexNumberEnd
};
item.PremiereDate = episodeResult.AirDate;
item.ProductionYear = episodeResult.AirDate?.Year;
item.Name = episodeResult.Name;
item.Overview = episodeResult.Overview;
item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
result.Item = item;
return result;
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return _httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}
private int? GuessEpisodeNumber(int? current, string fileName, double max = double.PositiveInfinity)
{
var episodeIndex = current;
var result = AnitomySharp.AnitomySharp.Parse(fileName).FirstOrDefault(x => x.Category == AnitomySharp.Element.ElementCategory.ElementEpisodeNumber);
if (result != null)
{
episodeIndex = result.Value.ToInt();
}
foreach (var regex in EpisodeFileNameRegex)
{
if (!regex.IsMatch(fileName))
continue;
if (!int.TryParse(regex.Match(fileName).Groups[1].Value.Trim('.'), out var index))
continue;
episodeIndex = index;
break;
}
return episodeIndex;
}
}
}

View File

@ -0,0 +1,29 @@
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Providers.ExternalId
{
public class DoubanExternalId : IExternalId
{
public string ProviderName => BaseProvider.DoubanProviderName;
public string Key => BaseProvider.DoubanProviderId;
public ExternalIdMediaType? Type => null;
public string UrlFormatString => "https://movie.douban.com/subject/{0}/";
public bool Supports(IHasProviderIds item)
{
return item is Movie || item is Series || item is Season;
}
}
}

View File

@ -0,0 +1,164 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class MovieImageProvider : BaseProvider, IRemoteImageProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="MovieImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
public MovieImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
/// <inheritdoc />
public string Name => Plugin.PluginName;
/// <inheritdoc />
public bool Supports(BaseItem item) => item is Movie;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{
ImageType.Primary,
ImageType.Backdrop
};
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
Console.WriteLine(item.ToJson());
Console.WriteLine(item.ProviderIds.ToJson());
var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetProviderId(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (!string.IsNullOrEmpty(sid) && metaSource == MetaSource.Douban)
{
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
var dropback = await GetBackdrop(sid, cancellationToken);
var res = new List<RemoteImageInfo> {
new RemoteImageInfo
{
ProviderName = primary.Name,
Url = primary.ImgMiddle,
Type = ImageType.Primary
}
};
res.AddRange(dropback);
return res;
}
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb).ToInt();
if (tmdbId > 0)
{
var language = item.GetPreferredMetadataLanguage();
var movie = await _tmdbApi
.GetMovieAsync(tmdbId, language, language, cancellationToken)
.ConfigureAwait(false);
if (movie?.Images == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var remoteImages = new List<RemoteImageInfo>();
for (var i = 0; i < movie.Images.Posters.Count; i++)
{
var poster = movie.Images.Posters[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
CommunityRating = poster.VoteAverage,
VoteCount = poster.VoteCount,
Width = poster.Width,
Height = poster.Height,
ProviderName = Name,
Type = ImageType.Primary,
});
}
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
{
var backdrop = movie.Images.Backdrops[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
CommunityRating = backdrop.VoteAverage,
VoteCount = backdrop.VoteCount,
Width = backdrop.Width,
Height = backdrop.Height,
ProviderName = Name,
Type = ImageType.Backdrop,
RatingType = RatingType.Score
});
}
return remoteImages.OrderByLanguageDescending(language);
}
this.Log($"Got images failed because the sid of \"{item.Name}\" is empty!");
return new List<RemoteImageInfo>();
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Query for a background photo
/// </summary>
/// <param name="sid">a subject/movie id</param>
/// <param name="cancellationToken">Instance of the <see cref="CancellationToken"/> interface.</param>
private async Task<IEnumerable<RemoteImageInfo>> GetBackdrop(string sid, CancellationToken cancellationToken)
{
this.Log("GetBackdrop of sid: {0}", sid);
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
var list = new List<RemoteImageInfo>();
if (photo == null)
{
return list;
}
return photo.Where(x => x.Width > x.Height * 1.3).Select(x =>
{
return new RemoteImageInfo
{
ProviderName = Name,
Url = x.Large,
Type = ImageType.Backdrop,
};
});
}
}
}

View File

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using AngleSharp.Text;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using StringMetric;
using TMDbLib.Client;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.Languages;
using TMDbLib.Objects.TvShows;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class MovieProvider : BaseProvider, IRemoteMetadataProvider<Movie, MovieInfo>
{
public MovieProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
public string Name => Plugin.PluginName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSearchResults of [name]: {info.Name}");
var result = new List<RemoteSearchResult>();
if (string.IsNullOrEmpty(info.Name))
{
return result;
}
// 从douban搜索
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(this._config.MaxSearchResult).Select(x =>
{
return new RemoteSearchResult
{
SearchProviderName = DoubanProviderName,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year,
Name = x.Name,
};
}));
// 从tmdb搜索
var tmdbList = await _tmdbApi.SearchMovieAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.AddRange(tmdbList.Take(this._config.MaxSearchResult).Select(x =>
{
return new RemoteSearchResult
{
SearchProviderName = TmdbProviderName,
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
Name = x.Title ?? x.OriginalTitle,
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview,
ProductionYear = x.ReleaseDate?.Year,
};
}));
return result;
}
/// <inheritdoc />
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
this.Log($"GetMovieMetadata of [name]: {info.Name}");
var result = new MetadataResult<Movie>();
Console.WriteLine(info.ToJson());
var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetProviderId(Plugin.ProviderId);
if (string.IsNullOrEmpty(sid) && string.IsNullOrEmpty(tmdbId))
{
// 刷新元数据匹配搜索
sid = await this.GuestByDoubanAsync(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(sid))
{
tmdbId = await this.GuestByTmdbAsync(info, cancellationToken).ConfigureAwait(false);
}
}
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{
this.Log($"GetMovieMetadata of douban [sid]: \"{sid}\"");
var subject = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (subject == null)
{
return result;
}
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
var movie = new Movie
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
HomePageUrl = "https://www.douban.com",
Genres = subject.Genres,
// ProductionLocations = [x?.Country],
PremiereDate = subject.ScreenTime,
};
if (!string.IsNullOrEmpty(subject.Imdb))
{
movie.SetProviderId(MetadataProvider.Imdb, subject.Imdb);
// 通过imdb获取TMDB id
var movieResult = await this._tmdbApi.FindByExternalIdAsync(subject.Imdb, FindExternalSource.Imdb, null, cancellationToken).ConfigureAwait(false);
if (movieResult?.MovieResults != null && movieResult.MovieResults.Count > 0)
{
Console.WriteLine(movieResult.MovieResults.ToJson());
this.Log($"GetMovieMetadata of found tmdb [id]: \"{movieResult.MovieResults[0].Id}\"");
movie.SetProviderId(MetadataProvider.Tmdb, $"{movieResult.MovieResults[0].Id}");
}
}
result.Item = movie;
result.QueriedById = true;
result.HasMetadata = true;
subject.Celebrities.ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Role = c.Role,
ImageUrl = c.Img,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
return result;
}
if (!string.IsNullOrEmpty(tmdbId))
{
this.Log($"GetMovieMetadata of tmdb [id]: \"{tmdbId}\"");
var movieResult = await _tmdbApi
.GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
if (movieResult == null)
{
return result;
}
var movie = new Movie
{
Name = movieResult.Title ?? movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
Tagline = movieResult.Tagline,
ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
};
var metadataResult = new MetadataResult<Movie>
{
HasMetadata = true,
ResultLanguage = info.MetadataLanguage,
Item = movie
};
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
movie.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage);
movie.PremiereDate = movieResult.ReleaseDate;
movie.ProductionYear = movieResult.ReleaseDate?.Year;
if (movieResult.ProductionCompanies != null)
{
movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
}
var genres = movieResult.Genres;
foreach (var genre in genres.Select(g => g.Name))
{
movie.AddGenre(genre);
}
foreach (var person in GetPersons(movieResult))
{
result.AddPerson(person);
}
return metadataResult;
}
return result;
}
private IEnumerable<PersonInfo> GetPersons(TMDbLib.Objects.Movies.Movie item)
{
// 演员
if (item.Credits?.Cast != null)
{
foreach (var actor in item.Credits.Cast.OrderBy(a => a.Order).Take(this._config.MaxCastMembers))
{
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character,
Type = PersonType.Actor,
SortOrder = actor.Order,
};
if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
{
personInfo.ImageUrl = _tmdbApi.GetProfileUrl(actor.ProfilePath);
}
if (actor.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
// 导演
if (item.Credits?.Crew != null)
{
var keepTypes = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer
};
foreach (var person in item.Credits.Crew)
{
// Normalize this
var type = MapCrewToPersonType(person);
if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
!keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
{
personInfo.ImageUrl = this._tmdbApi.GetPosterUrl(person.ProfilePath);
}
if (person.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,139 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.MetaShark.Providers
{
/// <summary>
/// OddbPersonProvider.
/// </summary>
public class PersonProvider : BaseProvider, IRemoteMetadataProvider<Person, PersonLookupInfo>
{
/// <summary>
/// Initializes a new instance of the <see cref="MovieImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
public PersonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
/// <inheritdoc />
public string Name => Plugin.PluginName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
{
return await Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
}
/// <inheritdoc />
public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken)
{
MetadataResult<Person> result = new MetadataResult<Person>();
var cid = info.GetProviderId(DoubanProviderId);
this.Log($"GetMetadata of [cid]: {cid}");
if (!string.IsNullOrEmpty(cid))
{
var c = await this._doubanApi.GetCelebrityAsync(cid, cancellationToken).ConfigureAwait(false);
if (c != null)
{
Person p = new Person
{
Name = c.Name,
HomePageUrl = c.Site,
Overview = c.Intro,
PremiereDate = DateTime.ParseExact(c.Birthdate, "yyyy年MM月dd日", System.Globalization.CultureInfo.CurrentCulture)
};
p.SetProviderId(Plugin.ProviderId, c.Id);
if (!string.IsNullOrWhiteSpace(c.Birthplace))
{
p.ProductionLocations = new[] { c.Birthplace };
}
if (!string.IsNullOrEmpty(c.Imdb))
{
p.SetProviderId(MetadataProvider.Imdb, c.Imdb);
}
result.HasMetadata = true;
result.Item = p;
return result;
}
}
var personTmdbId = info.GetProviderId(MetadataProvider.Tmdb);
this.Log($"GetMetadata of [personTmdbId]: {personTmdbId}");
if (!string.IsNullOrEmpty(personTmdbId))
{
var person = await this._tmdbApi.GetPersonAsync(personTmdbId.ToInt(), cancellationToken).ConfigureAwait(false);
if (person != null)
{
result.HasMetadata = true;
var item = new Person
{
// Take name from incoming info, don't rename the person
// TODO: This should go in PersonMetadataService, not each person provider
Name = info.Name,
HomePageUrl = person.Homepage,
Overview = person.Biography,
PremiereDate = person.Birthday?.ToUniversalTime(),
EndDate = person.Deathday?.ToUniversalTime()
};
if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
{
item.ProductionLocations = new[] { person.PlaceOfBirth };
}
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(person.ImdbId))
{
item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
}
result.HasMetadata = true;
result.Item = item;
return result;
}
}
return result;
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("Person GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
private void Log(string? message, params object?[] args)
{
this._logger.LogInformation($"[MetaShark] {message}", args);
}
}
}

View File

@ -0,0 +1,128 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class SeasonImageProvider : BaseProvider, IRemoteImageProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="SeasonImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
public SeasonImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
/// <inheritdoc />
public string Name => Plugin.PluginName;
/// <inheritdoc />
public bool Supports(BaseItem item) => item is Season;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Primary;
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
this.Log($"GetSeasonImages for item: {item.Name} number: {item.IndexNumber}");
var season = (Season)item;
var series = season.Series;
var metaSource = series.GetProviderId(Plugin.ProviderId);
// get image from douban
var sid = item.GetProviderId(DoubanProviderId);
if (metaSource == MetaSource.Douban && !string.IsNullOrEmpty(sid))
{
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (primary == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var res = new List<RemoteImageInfo> {
new RemoteImageInfo
{
ProviderName = primary.Name,
Url = primary.ImgMiddle,
Type = ImageType.Primary
}
};
return res;
}
// get image form TMDB
var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
Console.WriteLine($"seriesTmdbId={seriesTmdbId} season?.IndexNumber={season?.IndexNumber}");
if (seriesTmdbId <= 0 || season?.IndexNumber == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
var seasonResult = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, language, cancellationToken)
.ConfigureAwait(false);
var posters = seasonResult?.Images?.Posters;
Console.WriteLine(posters?.ToJson());
if (posters == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var remoteImages = new RemoteImageInfo[posters.Count];
for (var i = 0; i < posters.Count; i++)
{
var image = posters[i];
remoteImages[i] = new RemoteImageInfo
{
Url = this._tmdbApi.GetPosterUrl(image.FilePath),
CommunityRating = image.VoteAverage,
VoteCount = image.VoteCount,
Width = image.Width,
Height = image.Height,
ProviderName = Name,
Type = ImageType.Primary,
};
}
return remoteImages.OrderByLanguageDescending(language);
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,277 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.TvShows;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class SeasonProvider : BaseProvider, IRemoteMetadataProvider<Season, SeasonInfo>
{
public SeasonProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
public string Name => Plugin.PluginName;
/// <summary>
/// Pattern for media name filtering
/// </summary>
private string _pattern;
public string Pattern
{
get
{
if (string.IsNullOrEmpty(_pattern))
{
return Plugin.Instance?.Configuration.Pattern;
}
return _pattern;
}
set
{
_pattern = value;
}
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSeasonSearchResults of [name]: {info.Name}");
return await Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
}
/// <inheritdoc />
public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSeasonMetaData of [name]: {info.Name} number: {info.IndexNumber}");
var result = new MetadataResult<Season>();
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out var seriesTmdbId);
info.SeriesProviderIds.TryGetValue(Plugin.ProviderId, out var metaSource);
info.SeriesProviderIds.TryGetValue(DoubanProviderId, out var sid);
Console.WriteLine(info.SeriesProviderIds.ToJson());
var seasonNumber = info.IndexNumber;
Console.WriteLine($"seriesTmdbId: {seriesTmdbId} seasonNumber: {seasonNumber}");
if (metaSource == MetaSource.Douban && !string.IsNullOrEmpty(sid))
{
// 从sereis获取正确名称季名称有时不对
var series = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (series == null)
{
return result;
}
var seiresName = series.Name;
// 存在tmdbid尝试从tmdb获取对应季的年份信息用于从豆瓣搜索对应季数据
int seasonYear = 0;
if (!string.IsNullOrEmpty(seriesTmdbId) && seasonNumber.HasValue)
{
var season = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId.ToInt(), seasonNumber.Value, info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
seasonYear = season?.AirDate?.Year ?? 0;
}
if (!string.IsNullOrEmpty(seiresName) && seasonYear > 0)
{
var seasonSid = await this.GuestSeasonByDoubanAsync(seiresName, seasonYear, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(seasonSid))
{
var subject = await this._doubanApi.GetMovieAsync(seasonSid, cancellationToken).ConfigureAwait(false);
if (subject != null)
{
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(seasonSid, cancellationToken).ConfigureAwait(false);
var movie = new Season
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
Genres = subject.Genres,
PremiereDate = subject.ScreenTime,
IndexNumber = info.IndexNumber,
};
result.Item = movie;
result.HasMetadata = true;
subject.Celebrities.ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Role = c.Role,
ImageUrl = c.Img,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
return result;
}
}
}
// 从豆瓣获取不到季信息直接使用series信息
result.Item = new Season
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, sid } },
Name = series.Name,
OriginalTitle = series.OriginalName,
CommunityRating = series.Rating,
Overview = series.Intro,
ProductionYear = series.Year,
Genres = series.Genres,
PremiereDate = series.ScreenTime,
};
result.QueriedById = true;
result.HasMetadata = true;
return result;
}
// series使用TMDB元数据来源
// tmdb季级没有对应id只通过indexNumber区分
if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue)
{
return result;
}
var seasonResult = await this._tmdbApi
.GetSeasonAsync(seriesTmdbId.ToInt(), seasonNumber.Value, info.MetadataLanguage, null, cancellationToken)
.ConfigureAwait(false);
if (seasonResult == null)
{
this.Log($"Not found season from TMDB. {info.Name} seriesTmdbId: {seriesTmdbId} seasonNumber: {seasonNumber}");
return result;
}
result.HasMetadata = true;
result.Item = new Season
{
IndexNumber = seasonNumber,
Overview = seasonResult.Overview,
PremiereDate = seasonResult.AirDate,
ProductionYear = seasonResult.AirDate?.Year,
};
if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
{
result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
}
foreach (var person in GetPersons(seasonResult))
{
result.AddPerson(person);
}
return result;
}
private IEnumerable<PersonInfo> GetPersons(TvSeason item)
{
// 演员
if (item.Credits?.Cast != null)
{
foreach (var actor in item.Credits.Cast.OrderBy(a => a.Order).Take(this._config.MaxCastMembers))
{
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character,
Type = PersonType.Actor,
SortOrder = actor.Order,
};
if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
{
personInfo.ImageUrl = this._tmdbApi.GetProfileUrl(actor.ProfilePath);
}
if (actor.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
// 导演
if (item.Credits?.Crew != null)
{
var keepTypes = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer
};
foreach (var person in item.Credits.Crew)
{
// Normalize this
var type = MapCrewToPersonType(person);
if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
&& !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
{
personInfo.ImageUrl = this._tmdbApi.GetPosterUrl(person.ProfilePath);
}
if (person.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return await this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
private void Log(string? message, params object?[] args)
{
this._logger.LogInformation($"[MetaShark] {message}", args);
}
}
}

View File

@ -0,0 +1,164 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Languages;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class SeriesImageProvider : BaseProvider, IRemoteImageProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{OddbImageProvider}"/> interface.</param>
/// <param name="doubanApi">Instance of <see cref="DoubanApi"/>.</param>
public SeriesImageProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
/// <inheritdoc />
public string Name => Plugin.PluginName;
/// <inheritdoc />
public bool Supports(BaseItem item) => item is Series;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) => new List<ImageType>
{
ImageType.Primary,
ImageType.Backdrop
};
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
Console.WriteLine(item.ToJson());
Console.WriteLine(item.ProviderIds.ToJson());
var sid = item.GetProviderId(DoubanProviderId);
var metaSource = item.GetProviderId(Plugin.ProviderId);
this.Log($"GetImages for item: {item.Name} [metaSource]: {metaSource}");
if (!string.IsNullOrEmpty(sid) && metaSource == MetaSource.Douban)
{
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
var dropback = await GetBackdrop(sid, cancellationToken);
var res = new List<RemoteImageInfo> {
new RemoteImageInfo
{
ProviderName = primary.Name,
Url = primary.ImgMiddle,
Type = ImageType.Primary
}
};
res.AddRange(dropback);
return res;
}
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb).ToInt();
if (tmdbId > 0)
{
var language = item.GetPreferredMetadataLanguage();
var movie = await _tmdbApi
.GetSeriesAsync(tmdbId, language, language, cancellationToken)
.ConfigureAwait(false);
if (movie?.Images == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var remoteImages = new List<RemoteImageInfo>();
for (var i = 0; i < movie.Images.Posters.Count; i++)
{
var poster = movie.Images.Posters[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(poster.FilePath),
CommunityRating = poster.VoteAverage,
VoteCount = poster.VoteCount,
Width = poster.Width,
Height = poster.Height,
ProviderName = Name,
Type = ImageType.Primary,
});
}
for (var i = 0; i < movie.Images.Backdrops.Count; i++)
{
var backdrop = movie.Images.Backdrops[i];
remoteImages.Add(new RemoteImageInfo
{
Url = _tmdbApi.GetPosterUrl(backdrop.FilePath),
CommunityRating = backdrop.VoteAverage,
VoteCount = backdrop.VoteCount,
Width = backdrop.Width,
Height = backdrop.Height,
ProviderName = Name,
Type = ImageType.Backdrop,
RatingType = RatingType.Score
});
}
return remoteImages.OrderByLanguageDescending(language);
}
this.Log($"Got images failed because the sid of \"{item.Name}\" is empty!");
return new List<RemoteImageInfo>();
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}
/// <summary>
/// Query for a background photo
/// </summary>
/// <param name="sid">a subject/movie id</param>
/// <param name="cancellationToken">Instance of the <see cref="CancellationToken"/> interface.</param>
private async Task<IEnumerable<RemoteImageInfo>> GetBackdrop(string sid, CancellationToken cancellationToken)
{
this.Log("GetBackdrop of sid: {0}", sid);
var photo = await this._doubanApi.GetWallpaperBySidAsync(sid, cancellationToken);
var list = new List<RemoteImageInfo>();
if (photo == null)
{
return list;
}
return photo.Where(x => x.Width > x.Height * 1.3).Select(x =>
{
return new RemoteImageInfo
{
ProviderName = Name,
Url = x.Large,
Type = ImageType.Backdrop,
};
});
}
}
}

View File

@ -0,0 +1,361 @@
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Core;
using Jellyfin.Plugin.MetaShark.Model;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Find;
using TMDbLib.Objects.TvShows;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
namespace Jellyfin.Plugin.MetaShark.Providers
{
public class SeriesProvider : BaseProvider, IRemoteMetadataProvider<Series, SeriesInfo>
{
public SeriesProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi)
: base(httpClientFactory, loggerFactory.CreateLogger<SeriesProvider>(), libraryManager, doubanApi, tmdbApi, omdbApi)
{
}
public string Name => Plugin.PluginName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSearchResults of [name]: {info.Name}");
var result = new List<RemoteSearchResult>();
if (string.IsNullOrEmpty(info.Name))
{
return result;
}
// 从douban搜索
var res = await this._doubanApi.SearchAsync(info.Name, cancellationToken).ConfigureAwait(false);
result.AddRange(res.Take(this._config.MaxSearchResult).Select(x =>
{
return new RemoteSearchResult
{
SearchProviderName = DoubanProviderName,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, x.Sid } },
ImageUrl = this.GetProxyImageUrl(x.Img),
ProductionYear = x.Year,
Name = x.Name,
};
}));
// 尝试从tmdb搜索
Console.WriteLine($"info.MetadataLanguage={info.MetadataLanguage}");
var tmdbList = await this._tmdbApi.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.AddRange(tmdbList.Take(this._config.MaxSearchResult).Select(x =>
{
return new RemoteSearchResult
{
SearchProviderName = TmdbProviderName,
ProviderIds = new Dictionary<string, string> { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) } },
Name = x.Name ?? x.OriginalName,
ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath),
Overview = x.Overview,
ProductionYear = x.FirstAirDate?.Year,
};
}));
return result;
}
/// <inheritdoc />
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
this.Log($"GetSeriesMetadata of [name]: {info.Name} [providerIds]: {info.ProviderIds.ToJson()}");
info.Name = this.RemoveMetaSourcePrefix(info.Name);
var result = new MetadataResult<Series>();
var sid = info.GetProviderId(DoubanProviderId);
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
var metaSource = info.GetProviderId(Plugin.ProviderId);
if (string.IsNullOrEmpty(sid) && string.IsNullOrEmpty(tmdbId))
{
// 刷新元数据自动匹配搜索
sid = await this.GuestByDoubanAsync(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(sid))
{
tmdbId = await this.GuestByTmdbAsync(info, cancellationToken).ConfigureAwait(false);
}
}
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{
this.Log($"GetSeriesMetadata of douban [sid]: \"{sid}\"");
var subject = await this._doubanApi.GetMovieAsync(sid, cancellationToken).ConfigureAwait(false);
if (subject == null)
{
return result;
}
subject.Celebrities = await this._doubanApi.GetCelebritiesBySidAsync(sid, cancellationToken).ConfigureAwait(false);
var item = new Series
{
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, MetaSource.Douban } },
Name = subject.Name,
OriginalTitle = subject.OriginalName,
CommunityRating = subject.Rating,
Overview = subject.Intro,
ProductionYear = subject.Year,
HomePageUrl = "https://www.douban.com",
Genres = subject.Genres,
// ProductionLocations = [x?.Country],
PremiereDate = subject.ScreenTime,
};
if (!string.IsNullOrEmpty(subject.Imdb))
{
item.SetProviderId(MetadataProvider.Imdb, subject.Imdb);
// 通过imdb获取TMDB id (豆瓣的imdb id可能是旧的需要先从omdb接口获取最新的imdb id
var omdbItem = await this._omdbApi.GetByImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false);
if (omdbItem != null)
{
var findResult = await this._tmdbApi.FindByExternalIdAsync(omdbItem.ImdbID, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (findResult?.TvResults != null && findResult.TvResults.Count > 0)
{
this.Log($"GetSeriesMetadata found tmdb [id]: {findResult.TvResults[0].Id} by imdb id: {subject.Imdb}");
item.SetProviderId(MetadataProvider.Tmdb, $"{findResult.TvResults[0].Id}");
}
}
}
result.Item = item;
result.QueriedById = true;
result.HasMetadata = true;
subject.Celebrities.ForEach(c => result.AddPerson(new PersonInfo
{
Name = c.Name,
Type = c.RoleType,
Role = c.Role,
ImageUrl = c.Img,
ProviderIds = new Dictionary<string, string> { { DoubanProviderId, c.Id } },
}));
return result;
}
if (!string.IsNullOrEmpty(tmdbId))
{
this.Log($"GetSeriesMetadata of tmdb [id]: \"{tmdbId}\"");
var tvShow = await _tmdbApi
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, info.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
if (tvShow == null)
{
return result;
}
result = new MetadataResult<Series>
{
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
};
foreach (var person in GetPersons(tvShow))
{
result.AddPerson(person);
}
result.QueriedById = true;
result.HasMetadata = true;
return result;
}
return result;
}
private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
{
var series = new Series
{
Name = seriesResult.Name,
OriginalTitle = seriesResult.OriginalName
};
series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture));
series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage);
series.Overview = seriesResult.Overview;
if (seriesResult.Networks != null)
{
series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
}
if (seriesResult.Genres != null)
{
series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();
}
if (seriesResult.Keywords?.Results != null)
{
for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
{
series.AddTag(seriesResult.Keywords.Results[i].Name);
}
}
series.HomePageUrl = seriesResult.Homepage;
series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase))
{
series.Status = SeriesStatus.Ended;
series.EndDate = seriesResult.LastAirDate;
}
else
{
series.Status = SeriesStatus.Continuing;
}
series.PremiereDate = seriesResult.FirstAirDate;
var ids = seriesResult.ExternalIds;
if (ids != null)
{
if (!string.IsNullOrWhiteSpace(ids.ImdbId))
{
series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId);
}
if (!string.IsNullOrEmpty(ids.TvrageId))
{
series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId);
}
if (!string.IsNullOrEmpty(ids.TvdbId))
{
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
}
series.SetProviderId(Plugin.ProviderId, MetaSource.Tmdb);
var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
var minimumRelease = contentRatings.FirstOrDefault();
if (ourRelease != null)
{
series.OfficialRating = ourRelease.Rating;
}
else if (usRelease != null)
{
series.OfficialRating = usRelease.Rating;
}
else if (minimumRelease != null)
{
series.OfficialRating = minimumRelease.Rating;
}
return series;
}
private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
{
// 演员
if (seriesResult.Credits?.Cast != null)
{
foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(this._config.MaxCastMembers))
{
var personInfo = new PersonInfo
{
Name = actor.Name.Trim(),
Role = actor.Character,
Type = PersonType.Actor,
SortOrder = actor.Order,
};
if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
{
personInfo.ImageUrl = this._tmdbApi.GetProfileUrl(actor.ProfilePath);
}
if (actor.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
// 导演
if (seriesResult.Credits?.Crew != null)
{
var keepTypes = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer
};
foreach (var person in seriesResult.Credits.Crew)
{
// Normalize this
var type = MapCrewToPersonType(person);
if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
&& !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
continue;
}
var personInfo = new PersonInfo
{
Name = person.Name.Trim(),
Role = person.Job,
Type = type
};
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
{
personInfo.ImageUrl = this._tmdbApi.GetPosterUrl(person.ProfilePath);
}
if (person.Id > 0)
{
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
}
yield return personInfo;
}
}
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
this.Log("GetImageResponse url: {0}", url);
return this._httpClientFactory.CreateClient().GetAsync(new Uri(url), cancellationToken);
}
private void Log(string? message, params object?[] args)
{
this._logger.LogInformation($"[MetaShark] {message}", args);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Providers;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MediaBrowser.Controller.Persistence;
using System.Net.Http;
namespace Jellyfin.Plugin.MetaShark
{
/// <inheritdoc />
public class ServiceRegistrator : IPluginServiceRegistrator
{
/// <inheritdoc />
public void RegisterServices(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<DoubanApi>((ctx) =>
{
return new DoubanApi(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton<TmdbApi>((ctx) =>
{
return new TmdbApi(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton<OmdbApi>((ctx) =>
{
return new OmdbApi(ctx.GetRequiredService<ILoggerFactory>());
});
}
}
}

View File

@ -0,0 +1,4 @@
[*.cs]
# CAC001: ConfigureAwaitChecker
dotnet_diagnostic.CAC001.severity = error

View File

@ -0,0 +1,291 @@
using System;
using TMDbLib.Objects.Account;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using ParameterType = TMDbLib.Rest.ParameterType;
using RestClient = TMDbLib.Rest.RestClient;
using RestRequest = TMDbLib.Rest.RestRequest;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Rest;
using TMDbLib.Utilities.Serializer;
namespace TMDbLib.Client
{
public partial class TMDbClient : IDisposable
{
private const string ApiVersion = "3";
private const string ProductionUrl = "api.themoviedb.org";
private readonly ITMDbSerializer _serializer;
private RestClient _client;
private TMDbConfig _config;
public TMDbClient(string apiKey, bool useSsl = true, string baseUrl = ProductionUrl, ITMDbSerializer serializer = null, IWebProxy proxy = null)
{
DefaultLanguage = null;
DefaultImageLanguage = null;
DefaultCountry = null;
_serializer = serializer ?? TMDbJsonSerializer.Instance;
//Setup proxy to use during requests
//Proxy is optional. If passed, will be used in every request.
WebProxy = proxy;
Initialize(baseUrl, useSsl, apiKey);
}
/// <summary>
/// The account details of the user account associated with the current user session
/// </summary>
/// <remarks>This value is automaticly populated when setting a user session</remarks>
public AccountDetails ActiveAccount { get; private set; }
public string ApiKey { get; private set; }
public TMDbConfig Config
{
get
{
if (!HasConfig)
throw new InvalidOperationException("Call GetConfig() or SetConfig() first");
return _config;
}
private set { _config = value; }
}
/// <summary>
/// ISO 3166-1 code. Ex. US
/// </summary>
public string DefaultCountry { get; set; }
/// <summary>
/// ISO 639-1 code. Ex en
/// </summary>
public string DefaultLanguage { get; set; }
/// <summary>
/// ISO 639-1 code. Ex en
/// </summary>
public string DefaultImageLanguage { get; set; }
public bool HasConfig { get; private set; }
/// <summary>
/// Throw exceptions when TMDbs API returns certain errors, such as Not Found.
/// </summary>
public bool ThrowApiExceptions
{
get => _client.ThrowApiExceptions;
set => _client.ThrowApiExceptions = value;
}
/// <summary>
/// The maximum number of times a call to TMDb will be retried
/// </summary>
/// <remarks>Default is 0</remarks>
public int MaxRetryCount
{
get => _client.MaxRetryCount;
set => _client.MaxRetryCount = value;
}
/// <summary>
/// The request timeout call to TMDb
/// </summary>
public TimeSpan RequestTimeout
{
get => _client.HttpClient.Timeout;
set => _client.HttpClient.Timeout = value;
}
/// <summary>
/// The session id that will be used when TMDb requires authentication
/// </summary>
/// <remarks>Use 'SetSessionInformation' to assign this value</remarks>
public string SessionId { get; private set; }
/// <summary>
/// The type of the session id, this will determine the level of access that is granted on the API
/// </summary>
/// <remarks>Use 'SetSessionInformation' to assign this value</remarks>
public SessionType SessionType { get; private set; }
/// <summary>
/// Gets or sets the Web Proxy to use during requests to TMDb API.
/// </summary>
/// <remarks>
/// The Web Proxy is optional. If set, every request will be sent through it.
/// Use the constructor for setting it.
///
/// For convenience, this library also offers a <see cref="IWebProxy"/> implementation.
/// Check <see cref="Utilities.TMDbAPIProxy"/> for more information.
/// </remarks>
public IWebProxy WebProxy { get; private set; }
/// <summary>
/// Used internally to assign a session id to a request. If no valid session is found, an exception is thrown.
/// </summary>
/// <param name="req">Request</param>
/// <param name="targetType">The target session type to set. If set to Unassigned, the method will take the currently set session.</param>
/// <param name="parameterType">The location of the paramter in the resulting query</param>
private void AddSessionId(RestRequest req, SessionType targetType = SessionType.Unassigned, ParameterType parameterType = ParameterType.QueryString)
{
if ((targetType == SessionType.Unassigned && SessionType == SessionType.GuestSession) ||
(targetType == SessionType.GuestSession))
{
// Either
// - We needed ANY session ID and had a Guest session id
// - We needed a Guest session id and had it
req.AddParameter("guest_session_id", SessionId, parameterType);
return;
}
if ((targetType == SessionType.Unassigned && SessionType == SessionType.UserSession) ||
(targetType == SessionType.UserSession))
{
// Either
// - We needed ANY session ID and had a User session id
// - We needed a User session id and had it
req.AddParameter("session_id", SessionId, parameterType);
return;
}
// We did not have the required session type ready
throw new UserSessionRequiredException();
}
public async Task<TMDbConfig> GetConfigAsync()
{
TMDbConfig config = await _client.Create("configuration").GetOfT<TMDbConfig>(CancellationToken.None).ConfigureAwait(false);
if (config == null)
throw new Exception("Unable to retrieve configuration");
// Store config
Config = config;
HasConfig = true;
return config;
}
public Uri GetImageUrl(string size, string filePath, bool useSsl = false)
{
string baseUrl = useSsl ? Config.Images.SecureBaseUrl : Config.Images.BaseUrl;
return new Uri(baseUrl + size + filePath);
}
[Obsolete("Use " + nameof(GetImageBytesAsync))]
public Task<byte[]> GetImageBytes(string size, string filePath, bool useSsl = false, CancellationToken token = default)
{
return GetImageBytesAsync(size, filePath, useSsl, token);
}
public async Task<byte[]> GetImageBytesAsync(string size, string filePath, bool useSsl = false, CancellationToken token = default)
{
Uri url = GetImageUrl(size, filePath, useSsl);
using HttpResponseMessage response = await _client.HttpClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP003:Dispose previous before re-assigning.", Justification = "Only called from ctor")]
private void Initialize(string baseUrl, bool useSsl, string apiKey)
{
if (string.IsNullOrWhiteSpace(baseUrl))
throw new ArgumentException("baseUrl");
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentException("apiKey");
ApiKey = apiKey;
// Cleanup the provided url so that we don't get any issues when we are configuring the client
if (baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
baseUrl = baseUrl.Substring("http://".Length);
else if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
baseUrl = baseUrl.Substring("https://".Length);
string httpScheme = useSsl ? "https" : "http";
_client = new RestClient(new Uri(string.Format("{0}://{1}/{2}/", httpScheme, baseUrl, ApiVersion)), _serializer, WebProxy);
_client.AddDefaultQueryString("api_key", apiKey);
}
/// <summary>
/// Used internally to determine if the current client has the required session set, if not an appropriate exception will be thrown
/// </summary>
/// <param name="sessionType">The type of session that is required by the calling method</param>
/// <exception cref="UserSessionRequiredException">Thrown if the calling method requires a user session and one isn't set on the client object</exception>
/// <exception cref="GuestSessionRequiredException">Thrown if the calling method requires a guest session and no session is set on the client object. (neither user or client type session)</exception>
private void RequireSessionId(SessionType sessionType)
{
if (string.IsNullOrWhiteSpace(SessionId))
{
if (sessionType == SessionType.GuestSession)
throw new UserSessionRequiredException();
else
throw new GuestSessionRequiredException();
}
if (sessionType == SessionType.UserSession && SessionType == SessionType.GuestSession)
throw new UserSessionRequiredException();
}
public void SetConfig(TMDbConfig config)
{
// Store config
Config = config;
HasConfig = true;
}
/// <summary>
/// Use this method to set the current client's authentication information.
/// The session id assigned here will be used by the client when ever TMDb requires it.
/// </summary>
/// <param name="sessionId">The session id to use when making calls that require authentication</param>
/// <param name="sessionType">The type of session id</param>
/// <remarks>
/// - Use the 'AuthenticationGetUserSessionAsync' and 'AuthenticationCreateGuestSessionAsync' methods to optain the respective session ids.
/// - User sessions have access to far for methods than guest sessions, these can currently only be used to rate media.
/// </remarks>
public async Task SetSessionInformationAsync(string sessionId, SessionType sessionType)
{
ActiveAccount = null;
SessionId = sessionId;
if (!string.IsNullOrWhiteSpace(sessionId) && sessionType == SessionType.Unassigned)
{
throw new ArgumentException("When setting the session id it must always be either a guest or user session");
}
SessionType = string.IsNullOrWhiteSpace(sessionId) ? SessionType.Unassigned : sessionType;
// Populate the related account information
if (sessionType == SessionType.UserSession)
{
try
{
ActiveAccount = await AccountGetDetailsAsync().ConfigureAwait(false);
}
catch (Exception)
{
// Unable to complete the full process so reset all values and throw the exception
ActiveAccount = null;
SessionId = null;
SessionType = SessionType.Unassigned;
throw;
}
}
}
public virtual void Dispose()
{
_client?.Dispose();
}
}
}

View File

@ -0,0 +1,257 @@
using TMDbLib.Utilities;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Account;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Lists;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<SearchContainer<T>> GetAccountListInternal<T>(int page, AccountSortBy sortBy, SortOrder sortOrder, string language, AccountListsMethods method, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest request = _client.Create("account/{accountId}/" + method.GetDescription());
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
AddSessionId(request, SessionType.UserSession);
if (page > 1)
request.AddParameter("page", page.ToString());
if (sortBy != AccountSortBy.Undefined)
request.AddParameter("sort_by", sortBy.GetDescription());
if (sortOrder != SortOrder.Undefined)
request.AddParameter("sort_order", sortOrder.GetDescription());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
request.AddParameter("language", language);
SearchContainer<T> response = await request.GetOfT<SearchContainer<T>>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Change the favorite status of a specific movie. Either make the movie a favorite or remove that status depending on the supplied boolean value.
/// </summary>
/// <param name="mediaType">The type of media to influence</param>
/// <param name="mediaId">The id of the movie/tv show to influence</param>
/// <param name="isFavorite">True if you want the specified movie to be marked as favorite, false if not</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if the the movie's favorite status was successfully updated, false if not</returns>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> AccountChangeFavoriteStatusAsync(MediaType mediaType, int mediaId, bool isFavorite, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest request = _client.Create("account/{accountId}/favorite");
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
request.SetBody(new { media_type = mediaType.GetDescription(), media_id = mediaId, favorite = isFavorite });
AddSessionId(request, SessionType.UserSession);
PostReply response = await request.PostOfT<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 1 = "Success" - Returned when adding a movie as favorite for the first time
// status code 13 = "The item/record was deleted successfully" - When removing an item as favorite, no matter if it exists or not
// status code 12 = "The item/record was updated successfully" - Used when an item is already marked as favorite and trying to do so doing again
return response.StatusCode == 1 || response.StatusCode == 12 || response.StatusCode == 13;
}
/// <summary>
/// Change the state of a specific movie on the users watchlist. Either add the movie to the list or remove it, depending on the specified boolean value.
/// </summary>
/// <param name="mediaType">The type of media to influence</param>
/// <param name="mediaId">The id of the movie/tv show to influence</param>
/// <param name="isOnWatchlist">True if you want the specified movie to be part of the watchlist, false if not</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the the movie's status on the watchlist was successfully updated, false if not</returns>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> AccountChangeWatchlistStatusAsync(MediaType mediaType, int mediaId, bool isOnWatchlist, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest request = _client.Create("account/{accountId}/watchlist");
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
request.SetBody(new { media_type = mediaType.GetDescription(), media_id = mediaId, watchlist = isOnWatchlist });
AddSessionId(request, SessionType.UserSession);
PostReply response = await request.PostOfT<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 1 = "Success"
// status code 13 = "The item/record was deleted successfully" - When removing an item from the watchlist, no matter if it exists or not
// status code 12 = "The item/record was updated successfully" - Used when an item is already on the watchlist and trying to add it again
return response.StatusCode == 1 || response.StatusCode == 12 || response.StatusCode == 13;
}
/// <summary>
/// Will retrieve the details of the account associated with the current session id
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<AccountDetails> AccountGetDetailsAsync(CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest request = _client.Create("account");
AddSessionId(request, SessionType.UserSession);
AccountDetails response = await request.GetOfT<AccountDetails>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Get a list of all the movies marked as favorite by the current user
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<SearchMovie>> AccountGetFavoriteMoviesAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<SearchMovie>(page, sortBy, sortOrder, language, AccountListsMethods.FavoriteMovies, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of all the tv shows marked as favorite by the current user
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<SearchTv>> AccountGetFavoriteTvAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<SearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.FavoriteTv, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieve all lists associated with the provided account id
/// This can be lists that were created by the user or lists marked as favorite
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<AccountList>> AccountGetListsAsync(int page = 1, string language = null, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest request = _client.Create("account/{accountId}/lists");
request.AddUrlSegment("accountId", ActiveAccount.Id.ToString(CultureInfo.InvariantCulture));
AddSessionId(request, SessionType.UserSession);
if (page > 1)
{
request.AddQueryString("page", page.ToString());
}
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
request.AddQueryString("language", language);
SearchContainer<AccountList> response = await request.GetOfT<SearchContainer<AccountList>>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Get a list of all the movies on the current users match list
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<SearchMovie>> AccountGetMovieWatchlistAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<SearchMovie>(page, sortBy, sortOrder, language, AccountListsMethods.MovieWatchlist, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of all the movies rated by the current user
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<SearchMovieWithRating>> AccountGetRatedMoviesAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<SearchMovieWithRating>(page, sortBy, sortOrder, language, AccountListsMethods.RatedMovies, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of all the tv show episodes rated by the current user
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<AccountSearchTvEpisode>> AccountGetRatedTvShowEpisodesAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<AccountSearchTvEpisode>(page, sortBy, sortOrder, language, AccountListsMethods.RatedTvEpisodes, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of all the tv shows rated by the current user
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<AccountSearchTv>> AccountGetRatedTvShowsAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<AccountSearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.RatedTv, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of all the tv shows on the current users match list
/// </summary>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<SearchContainer<SearchTv>> AccountGetTvWatchlistAsync(
int page = 1,
AccountSortBy sortBy = AccountSortBy.Undefined,
SortOrder sortOrder = SortOrder.Undefined,
string language = null, CancellationToken cancellationToken = default)
{
return await GetAccountListInternal<SearchTv>(page, sortBy, sortOrder, language, AccountListsMethods.TvWatchlist, cancellationToken).ConfigureAwait(false);
}
private enum AccountListsMethods
{
[EnumValue("favorite/movies")]
FavoriteMovies,
[EnumValue("favorite/tv")]
FavoriteTv,
[EnumValue("rated/movies")]
RatedMovies,
[EnumValue("rated/tv")]
RatedTv,
[EnumValue("rated/tv/episodes")]
RatedTvEpisodes,
[EnumValue("watchlist/movies")]
MovieWatchlist,
[EnumValue("watchlist/tv")]
TvWatchlist,
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using System.Net;
using System.Threading;
using TMDbLib.Objects.Authentication;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<GuestSession> AuthenticationCreateGuestSessionAsync(CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create("authentication/guest_session/new");
//{
// DateFormat = "yyyy-MM-dd HH:mm:ss UTC"
//};
GuestSession response = await request.GetOfT<GuestSession>(cancellationToken).ConfigureAwait(false);
return response;
}
public async Task<UserSession> AuthenticationGetUserSessionAsync(string initialRequestToken, CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create("authentication/session/new");
request.AddParameter("request_token", initialRequestToken);
using RestResponse<UserSession> response = await request.Get<UserSession>(cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.Unauthorized)
throw new UnauthorizedAccessException();
return await response.GetDataObject().ConfigureAwait(false);
}
/// <summary>
/// Conveniance method combining 'AuthenticationRequestAutenticationTokenAsync', 'AuthenticationValidateUserTokenAsync' and 'AuthenticationGetUserSessionAsync'
/// </summary>
/// <param name="username">A valid TMDb username</param>
/// <param name="password">The passoword for the provided login</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<UserSession> AuthenticationGetUserSessionAsync(string username, string password, CancellationToken cancellationToken = default)
{
Token token = await AuthenticationRequestAutenticationTokenAsync(cancellationToken).ConfigureAwait(false);
await AuthenticationValidateUserTokenAsync(token.RequestToken, username, password, cancellationToken).ConfigureAwait(false);
return await AuthenticationGetUserSessionAsync(token.RequestToken, cancellationToken).ConfigureAwait(false);
}
public async Task<Token> AuthenticationRequestAutenticationTokenAsync(CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create("authentication/token/new");
using RestResponse<Token> response = await request.Get<Token>(cancellationToken).ConfigureAwait(false);
Token token = await response.GetDataObject().ConfigureAwait(false);
token.AuthenticationCallback = response.GetHeader("Authentication-Callback");
return token;
}
public async Task AuthenticationValidateUserTokenAsync(string initialRequestToken, string username, string password, CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create("authentication/token/validate_with_login");
request.AddParameter("request_token", initialRequestToken);
request.AddParameter("username", username);
request.AddParameter("password", password);
RestResponse response;
try
{
response = await request.Get(cancellationToken).ConfigureAwait(false);
}
catch (AggregateException ex)
{
throw ex.InnerException;
}
using RestResponse _ = response;
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorizedAccessException("Call to TMDb returned unauthorized. Most likely the provided user credentials are invalid.");
}
}
}
}

View File

@ -0,0 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Certifications;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<CertificationsContainer> GetMovieCertificationsAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("certification/movie/list");
CertificationsContainer resp = await req.GetOfT<CertificationsContainer>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<CertificationsContainer> GetTvCertificationsAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("certification/tv/list");
CertificationsContainer resp = await req.GetOfT<CertificationsContainer>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Changes;
using TMDbLib.Objects.General;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetChangesInternal<T>(string type, int page = 0, int? id = null, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
string resource;
if (id.HasValue)
resource = "{type}/{id}/changes";
else
resource = "{type}/changes";
RestRequest req = _client.Create(resource);
req.AddUrlSegment("type", type);
if (id.HasValue)
req.AddUrlSegment("id", id.Value.ToString());
if (page >= 1)
req.AddParameter("page", page.ToString());
if (startDate.HasValue)
req.AddParameter("start_date", startDate.Value.ToString("yyyy-MM-dd"));
if (endDate != null)
req.AddParameter("end_date", endDate.Value.ToString("yyyy-MM-dd"));
using RestResponse<T> resp = await req.Get<T>(cancellationToken).ConfigureAwait(false);
T res = await resp.GetDataObject().ConfigureAwait(false);
if (res is SearchContainer<ChangesListItem> asSearch)
{
// https://github.com/LordMike/TMDbLib/issues/296
asSearch.Results.RemoveAll(s => s.Id == 0);
}
return res;
}
/// <summary>
/// Get a list of movie ids that have been edited.
/// By default we show the last 24 hours and only 100 items per page.
/// The maximum number of days that can be returned in a single request is 14.
/// You can then use the movie changes API to get the actual data that has been changed. (.GetMovieChangesAsync)
/// </summary>
/// <remarks>the change log system to support this was changed on October 5, 2012 and will only show movies that have been edited since.</remarks>
public async Task<SearchContainer<ChangesListItem>> GetMoviesChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
return await GetChangesInternal<SearchContainer<ChangesListItem>>("movie", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of people ids that have been edited.
/// By default we show the last 24 hours and only 100 items per page.
/// The maximum number of days that can be returned in a single request is 14.
/// You can then use the person changes API to get the actual data that has been changed.(.GetPersonChangesAsync)
/// </summary>
/// <remarks>the change log system to support this was changed on October 5, 2012 and will only show people that have been edited since.</remarks>
public async Task<SearchContainer<ChangesListItem>> GetPeopleChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
return await GetChangesInternal<SearchContainer<ChangesListItem>>("person", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get a list of TV show ids that have been edited.
/// By default we show the last 24 hours and only 100 items per page.
/// The maximum number of days that can be returned in a single request is 14.
/// You can then use the TV changes API to get the actual data that has been changed. (.GetTvShowChangesAsync)
/// </summary>
/// <remarks>
/// the change log system to properly support TV was updated on May 13, 2014.
/// You'll likely only find the edits made since then to be useful in the change log system.
/// </remarks>
public async Task<SearchContainer<ChangesListItem>> GetTvChangesAsync(int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
return await GetChangesInternal<SearchContainer<ChangesListItem>>("tv", page, startDate: startDate, endDate: endDate, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<IList<Change>> GetMovieChangesAsync(int movieId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("movie", page, movieId, startDate, endDate, cancellationToken).ConfigureAwait(false);
return changesContainer.Changes;
}
public async Task<IList<Change>> GetPersonChangesAsync(int personId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("person", page, personId, startDate, endDate, cancellationToken).ConfigureAwait(false);
return changesContainer.Changes;
}
public async Task<IList<Change>> GetTvSeasonChangesAsync(int seasonId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("tv/season", page, seasonId, startDate, endDate, cancellationToken).ConfigureAwait(false);
return changesContainer.Changes;
}
public async Task<IList<Change>> GetTvEpisodeChangesAsync(int episodeId, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default)
{
ChangesContainer changesContainer = await GetChangesInternal<ChangesContainer>("tv/episode", page, episodeId, startDate, endDate, cancellationToken).ConfigureAwait(false);
return changesContainer.Changes;
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Collections;
using TMDbLib.Objects.General;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetCollectionMethodInternal<T>(int collectionId, CollectionMethods collectionMethod, string language = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("collection/{collectionId}/{method}");
req.AddUrlSegment("collectionId", collectionId.ToString());
req.AddUrlSegment("method", collectionMethod.GetDescription());
if (language != null)
req.AddParameter("language", language);
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<Collection> GetCollectionAsync(int collectionId, CollectionMethods extraMethods = CollectionMethods.Undefined, CancellationToken cancellationToken = default)
{
return await GetCollectionAsync(collectionId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
}
public async Task<Collection> GetCollectionAsync(int collectionId, string language, string includeImageLanguages, CollectionMethods extraMethods = CollectionMethods.Undefined, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("collection/{collectionId}");
req.AddUrlSegment("collectionId", collectionId.ToString());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
includeImageLanguages ??= DefaultImageLanguage;
if (!string.IsNullOrWhiteSpace(includeImageLanguages))
req.AddParameter("include_image_language", includeImageLanguages);
string appends = string.Join(",",
Enum.GetValues(typeof(CollectionMethods))
.OfType<CollectionMethods>()
.Except(new[] { CollectionMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
//req.DateFormat = "yyyy-MM-dd";
using RestResponse<Collection> response = await req.Get<Collection>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
Collection item = await response.GetDataObject().ConfigureAwait(false);
if (item != null)
item.Overview = WebUtility.HtmlDecode(item.Overview);
return item;
}
public async Task<ImagesWithId> GetCollectionImagesAsync(int collectionId, string language = null, CancellationToken cancellationToken = default)
{
return await GetCollectionMethodInternal<ImagesWithId>(collectionId, CollectionMethods.Images, language, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Companies;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetCompanyMethodInternal<T>(int companyId, CompanyMethods companyMethod, int page = 0, string language = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("company/{companyId}/{method}");
req.AddUrlSegment("companyId", companyId.ToString());
req.AddUrlSegment("method", companyMethod.GetDescription());
if (page >= 1)
req.AddParameter("page", page.ToString());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<Company> GetCompanyAsync(int companyId, CompanyMethods extraMethods = CompanyMethods.Undefined, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("company/{companyId}");
req.AddUrlSegment("companyId", companyId.ToString());
string appends = string.Join(",",
Enum.GetValues(typeof(CompanyMethods))
.OfType<CompanyMethods>()
.Except(new[] { CompanyMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
//req.DateFormat = "yyyy-MM-dd";
Company resp = await req.GetOfT<Company>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainerWithId<SearchMovie>> GetCompanyMoviesAsync(int companyId, int page = 0, CancellationToken cancellationToken = default)
{
return await GetCompanyMoviesAsync(companyId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<SearchMovie>> GetCompanyMoviesAsync(int companyId, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetCompanyMethodInternal<SearchContainerWithId<SearchMovie>>(companyId, CompanyMethods.Movies, page, language, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Configuration;
using TMDbLib.Objects.Countries;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Languages;
using TMDbLib.Objects.Timezones;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<APIConfiguration> GetAPIConfiguration(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("configuration");
using RestResponse<APIConfiguration> response = await req.Get<APIConfiguration>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false));
}
public async Task<List<Country>> GetCountriesAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("configuration/countries");
using RestResponse<List<Country>> response = await req.Get<List<Country>>(cancellationToken).ConfigureAwait(false);
return await response.GetDataObject().ConfigureAwait(false);
}
public async Task<List<Language>> GetLanguagesAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("configuration/languages");
using RestResponse<List<Language>> response = await req.Get<List<Language>>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false));
}
public async Task<List<string>> GetPrimaryTranslationsAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("configuration/primary_translations");
using RestResponse<List<string>> response = await req.Get<List<string>>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false));
}
public async Task<Timezones> GetTimezonesAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("timezones/list");
using RestResponse<List<Dictionary<string, List<string>>>> resp = await req.Get<List<Dictionary<string, List<string>>>>(cancellationToken).ConfigureAwait(false);
List<Dictionary<string, List<string>>> item = await resp.GetDataObject().ConfigureAwait(false);
if (item == null)
return null;
Timezones result = new Timezones();
result.List = new Dictionary<string, List<string>>();
foreach (Dictionary<string, List<string>> dictionary in item)
{
KeyValuePair<string, List<string>> item1 = dictionary.First();
result.List[item1.Key] = item1.Value;
}
return result;
}
/// <summary>
/// Retrieves a list of departments and positions within
/// </summary>
/// <returns>Valid jobs and their departments</returns>
public async Task<List<Job>> GetJobsAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("configuration/jobs");
using RestResponse<List<Job>> response = await req.Get<List<Job>>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false));
}
}
}

View File

@ -0,0 +1,29 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Credit;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<Credit> GetCreditsAsync(string id, CancellationToken cancellationToken = default)
{
return await GetCreditsAsync(id, DefaultLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<Credit> GetCreditsAsync(string id, string language, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("credit/{id}");
if (!string.IsNullOrEmpty(language))
req.AddParameter("language", language);
req.AddUrlSegment("id", id);
Credit resp = await req.GetOfT<Credit>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Discover;
using TMDbLib.Objects.General;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
/// <summary>
/// Can be used to discover movies matching certain criteria
/// </summary>
public DiscoverMovie DiscoverMoviesAsync()
{
return new DiscoverMovie(this);
}
internal async Task<SearchContainer<T>> DiscoverPerformAsync<T>(string endpoint, string language, int page, SimpleNamedValueCollection parameters, CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create(endpoint);
if (page != 1 && page > 1)
request.AddParameter("page", page.ToString());
if (!string.IsNullOrWhiteSpace(language))
request.AddParameter("language", language);
foreach (KeyValuePair<string, string> pair in parameters)
request.AddParameter(pair.Key, pair.Value);
SearchContainer<T> response = await request.GetOfT<SearchContainer<T>>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Can be used to discover new tv shows matching certain criteria
/// </summary>
public DiscoverTv DiscoverTvShowsAsync()
{
return new DiscoverTv(this);
}
}
}

View File

@ -0,0 +1,56 @@
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Find;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
/// <summary>
/// FindAsync movies, people and tv shows by an external id.
/// The following types can be found based on the specified external id's
/// - Movies: Imdb
/// - People: Imdb, FreeBaseMid, FreeBaseId, TvRage
/// - TV Series: Imdb, FreeBaseMid, FreeBaseId, TvRage, TvDb
/// </summary>
/// <param name="source">The source the specified id belongs to</param>
/// <param name="id">The id of the object you wish to located</param>
/// <returns>A list of all objects in TMDb that matched your id</returns>
/// <param name="cancellationToken">A cancellation token</param>
public Task<FindContainer> FindAsync(FindExternalSource source, string id, CancellationToken cancellationToken = default)
{
return FindAsync(source, id, null, cancellationToken);
}
/// <summary>
/// FindAsync movies, people and tv shows by an external id.
/// The following types can be found based on the specified external id's
/// - Movies: Imdb
/// - People: Imdb, FreeBaseMid, FreeBaseId, TvRage
/// - TV Series: Imdb, FreeBaseMid, FreeBaseId, TvRage, TvDb
/// </summary>
/// <param name="source">The source the specified id belongs to</param>
/// <param name="id">The id of the object you wish to located</param>
/// <returns>A list of all objects in TMDb that matched your id</returns>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<FindContainer> FindAsync(FindExternalSource source, string id, string language, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("find/{id}");
req.AddUrlSegment("id", WebUtility.UrlEncode(id));
req.AddParameter("external_source", source.GetDescription());
language ??= DefaultLanguage;
if (!string.IsNullOrEmpty(language))
req.AddParameter("language", language);
FindContainer resp = await req.GetOfT<FindContainer>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Genres;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
[Obsolete("GetGenreMovies is deprecated, use DiscoverMovies instead")]
public async Task<SearchContainerWithId<SearchMovie>> GetGenreMoviesAsync(int genreId, int page = 0, bool? includeAllMovies = null, CancellationToken cancellationToken = default)
{
return await GetGenreMoviesAsync(genreId, DefaultLanguage, page, includeAllMovies, cancellationToken).ConfigureAwait(false);
}
[Obsolete("GetGenreMovies is deprecated, use DiscoverMovies instead")]
public async Task<SearchContainerWithId<SearchMovie>> GetGenreMoviesAsync(int genreId, string language, int page = 0, bool? includeAllMovies = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("genre/{genreId}/movies");
req.AddUrlSegment("genreId", genreId.ToString());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (page >= 1)
req.AddParameter("page", page.ToString());
if (includeAllMovies.HasValue)
req.AddParameter("include_all_movies", includeAllMovies.Value ? "true" : "false");
SearchContainerWithId<SearchMovie> resp = await req.GetOfT<SearchContainerWithId<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<List<Genre>> GetMovieGenresAsync(CancellationToken cancellationToken = default)
{
return await GetMovieGenresAsync(DefaultLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<List<Genre>> GetMovieGenresAsync(string language, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("genre/movie/list");
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
using RestResponse<GenreContainer> resp = await req.Get<GenreContainer>(cancellationToken).ConfigureAwait(false);
return (await resp.GetDataObject().ConfigureAwait(false)).Genres;
}
public async Task<List<Genre>> GetTvGenresAsync(CancellationToken cancellationToken = default)
{
return await GetTvGenresAsync(DefaultLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<List<Genre>> GetTvGenresAsync(string language, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("genre/tv/list");
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
using RestResponse<GenreContainer> resp = await req.Get<GenreContainer>(cancellationToken).ConfigureAwait(false);
return (await resp.GetDataObject().ConfigureAwait(false)).Genres;
}
}
}

View File

@ -0,0 +1,85 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<SearchContainer<SearchMovieWithRating>> GetGuestSessionRatedMoviesAsync(int page = 0, CancellationToken cancellationToken = default)
{
return await GetGuestSessionRatedMoviesAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovieWithRating>> GetGuestSessionRatedMoviesAsync(string language, int page = 0, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/movies");
if (page > 0)
request.AddParameter("page", page.ToString());
if (!string.IsNullOrEmpty(language))
request.AddParameter("language", language);
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
SearchContainer<SearchMovieWithRating> resp = await request.GetOfT<SearchContainer<SearchMovieWithRating>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<SearchTvShowWithRating>> GetGuestSessionRatedTvAsync(int page = 0, CancellationToken cancellationToken = default)
{
return await GetGuestSessionRatedTvAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTvShowWithRating>> GetGuestSessionRatedTvAsync(string language, int page = 0, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/tv");
if (page > 0)
request.AddParameter("page", page.ToString());
if (!string.IsNullOrEmpty(language))
request.AddParameter("language", language);
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
SearchContainer<SearchTvShowWithRating> resp = await request.GetOfT<SearchContainer<SearchTvShowWithRating>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<TvEpisodeWithRating>> GetGuestSessionRatedTvEpisodesAsync(int page = 0, CancellationToken cancellationToken = default)
{
return await GetGuestSessionRatedTvEpisodesAsync(DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<TvEpisodeWithRating>> GetGuestSessionRatedTvEpisodesAsync(string language, int page = 0, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest request = _client.Create("guest_session/{guest_session_id}/rated/tv/episodes");
if (page > 0)
request.AddParameter("page", page.ToString());
if (!string.IsNullOrEmpty(language))
request.AddParameter("language", language);
AddSessionId(request, SessionType.GuestSession, ParameterType.UrlSegment);
SearchContainer<TvEpisodeWithRating> resp = await request.GetOfT<SearchContainer<TvEpisodeWithRating>>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,43 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<Keyword> GetKeywordAsync(int keywordId, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("keyword/{keywordId}");
req.AddUrlSegment("keywordId", keywordId.ToString());
Keyword resp = await req.GetOfT<Keyword>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainerWithId<SearchMovie>> GetKeywordMoviesAsync(int keywordId, int page = 0, CancellationToken cancellationToken = default)
{
return await GetKeywordMoviesAsync(keywordId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<SearchMovie>> GetKeywordMoviesAsync(int keywordId, string language, int page = 0, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("keyword/{keywordId}/movies");
req.AddUrlSegment("keywordId", keywordId.ToString());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (page >= 1)
req.AddParameter("page", page.ToString());
SearchContainerWithId<SearchMovie> resp = await req.GetOfT<SearchContainerWithId<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,210 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Lists;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<bool> GetManipulateMediaListAsyncInternal(string listId, int movieId, string method, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
// Movie Id is expected by the API and can not be null
if (movieId <= 0)
throw new ArgumentOutOfRangeException(nameof(movieId));
RestRequest req = _client.Create("list/{listId}/{method}");
req.AddUrlSegment("listId", listId);
req.AddUrlSegment("method", method);
AddSessionId(req, SessionType.UserSession);
req.SetBody(new { media_id = movieId });
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
// Status code 12 = "The item/record was updated successfully"
// Status code 13 = "The item/record was deleted successfully"
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Previous code checked for item=null
return item.StatusCode == 12 || item.StatusCode == 13;
}
/// <summary>
/// Retrieve a list by it's id
/// </summary>
/// <param name="listId">The id of the list you want to retrieve</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<GenericList> GetListAsync(string listId, string language = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
RestRequest req = _client.Create("list/{listId}");
req.AddUrlSegment("listId", listId);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
GenericList resp = await req.GetOfT<GenericList>(cancellationToken).ConfigureAwait(false);
return resp;
}
/// <summary>
/// Will check if the provided movie id is present in the specified list
/// </summary>
/// <param name="listId">Id of the list to check in</param>
/// <param name="movieId">Id of the movie to check for in the list</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<bool> GetListIsMoviePresentAsync(string listId, int movieId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
if (movieId <= 0)
throw new ArgumentOutOfRangeException(nameof(movieId));
RestRequest req = _client.Create("list/{listId}/item_status");
req.AddUrlSegment("listId", listId);
req.AddParameter("movie_id", movieId.ToString());
using RestResponse<ListStatus> response = await req.Get<ListStatus>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false)).ItemPresent;
}
/// <summary>
/// Adds a movie to a specified list
/// </summary>
/// <param name="listId">The id of the list to add the movie to</param>
/// <param name="movieId">The id of the movie to add</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the method was able to add the movie to the list, will retrun false in case of an issue or when the movie was already added to the list</returns>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> ListAddMovieAsync(string listId, int movieId, CancellationToken cancellationToken = default)
{
return await GetManipulateMediaListAsyncInternal(listId, movieId, "add_item", cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Clears a list, without confirmation.
/// </summary>
/// <param name="listId">The id of the list to clear</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the method was able to remove the movie from the list, will retrun false in case of an issue or when the movie was not present in the list</returns>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> ListClearAsync(string listId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
RestRequest request = _client.Create("list/{listId}/clear");
request.AddUrlSegment("listId", listId);
request.AddParameter("confirm", "true");
AddSessionId(request, SessionType.UserSession);
using RestResponse<PostReply> response = await request.Post<PostReply>(cancellationToken).ConfigureAwait(false);
// Status code 12 = "The item/record was updated successfully"
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Previous code checked for item=null
return item.StatusCode == 12;
}
/// <summary>
/// Creates a new list for the user associated with the current session
/// </summary>
/// <param name="name">The name of the new list</param>
/// <param name="description">Optional description for the list</param>
/// <param name="language">Optional language that might indicate the language of the content in the list</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<string> ListCreateAsync(string name, string description = "", string language = null, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
// Description is expected by the API and can not be null
if (string.IsNullOrWhiteSpace(description))
description = "";
RestRequest req = _client.Create("list");
AddSessionId(req, SessionType.UserSession);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
{
req.SetBody(new { name = name, description = description, language = language });
}
else
{
req.SetBody(new { name = name, description = description });
}
using RestResponse<ListCreateReply> response = await req.Post<ListCreateReply>(cancellationToken).ConfigureAwait(false);
return (await response.GetDataObject().ConfigureAwait(false)).ListId;
}
/// <summary>
/// Deletes the specified list that is owned by the user
/// </summary>
/// <param name="listId">A list id that is owned by the user associated with the current session id</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> ListDeleteAsync(string listId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
if (string.IsNullOrWhiteSpace(listId))
throw new ArgumentNullException(nameof(listId));
RestRequest req = _client.Create("list/{listId}");
req.AddUrlSegment("listId", listId);
AddSessionId(req, SessionType.UserSession);
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
// Status code 13 = success
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Previous code checked for item=null
return item.StatusCode == 13;
}
/// <summary>
/// Removes a movie from the specified list
/// </summary>
/// <param name="listId">The id of the list to add the movie to</param>
/// <param name="movieId">The id of the movie to add</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the method was able to remove the movie from the list, will retrun false in case of an issue or when the movie was not present in the list</returns>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<bool> ListRemoveMovieAsync(string listId, int movieId, CancellationToken cancellationToken = default)
{
return await GetManipulateMediaListAsyncInternal(listId, movieId, "remove_item", cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,392 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Movies;
using TMDbLib.Objects.Reviews;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
using TMDbLib.Utilities;
using Credits = TMDbLib.Objects.Movies.Credits;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetMovieMethodInternal<T>(int movieId, MovieMethods movieMethod, string dateFormat = null,
string country = null,
string language = null, string includeImageLanguage = null, int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("movie/{movieId}/{method}");
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", movieMethod.GetDescription());
if (country != null)
req.AddParameter("country", country);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
req.AddParameter("include_image_language", includeImageLanguage);
if (page >= 1)
req.AddParameter("page", page.ToString());
if (startDate.HasValue)
req.AddParameter("start_date", startDate.Value.ToString("yyyy-MM-dd"));
if (endDate != null)
req.AddParameter("end_date", endDate.Value.ToString("yyyy-MM-dd"));
T response = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Retrieves all information for a specific movie in relation to the current user account
/// </summary>
/// <param name="movieId">The id of the movie to get the account states for</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<AccountState> GetMovieAccountStateAsync(int movieId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("movie/{movieId}/{method}");
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", MovieMethods.AccountStates.GetDescription());
AddSessionId(req, SessionType.UserSession);
using RestResponse<AccountState> response = await req.Get<AccountState>(cancellationToken).ConfigureAwait(false);
return await response.GetDataObject().ConfigureAwait(false);
}
public async Task<AlternativeTitles> GetMovieAlternativeTitlesAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieAlternativeTitlesAsync(movieId, DefaultCountry, cancellationToken).ConfigureAwait(false);
}
public async Task<AlternativeTitles> GetMovieAlternativeTitlesAsync(int movieId, string country, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<AlternativeTitles>(movieId, MovieMethods.AlternativeTitles, country: country, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<Movie> GetMovieAsync(int movieId, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
{
return await GetMovieAsync(movieId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
}
public async Task<Movie> GetMovieAsync(string imdbId, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
{
return await GetMovieAsync(imdbId, DefaultLanguage, null, extraMethods, cancellationToken).ConfigureAwait(false);
}
public async Task<Movie> GetMovieAsync(int movieId, string language, string includeImageLanguage = null, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
{
return await GetMovieAsync(movieId.ToString(CultureInfo.InvariantCulture), language, includeImageLanguage, extraMethods, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieves a movie by its IMDb Id
/// </summary>
/// <param name="imdbId">The IMDb id of the movie OR the TMDb id as string</param>
/// <param name="language">Language to localize the results in.</param>
/// <param name="includeImageLanguage">If specified the api will attempt to return localized image results eg. en,it,es.</param>
/// <param name="extraMethods">A list of additional methods to execute for this req as enum flags</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The reqed movie or null if it could not be found</returns>
/// <remarks>Requires a valid user session when specifying the extra method 'AccountStates' flag</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned, see remarks.</exception>
public async Task<Movie> GetMovieAsync(string imdbId, string language, string includeImageLanguage = null, MovieMethods extraMethods = MovieMethods.Undefined, CancellationToken cancellationToken = default)
{
if (extraMethods.HasFlag(MovieMethods.AccountStates))
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("movie/{movieId}");
req.AddUrlSegment("movieId", imdbId);
if (extraMethods.HasFlag(MovieMethods.AccountStates))
AddSessionId(req, SessionType.UserSession);
if (language != null)
req.AddParameter("language", language);
includeImageLanguage ??= DefaultImageLanguage;
if (includeImageLanguage != null)
req.AddParameter("include_image_language", includeImageLanguage);
string appends = string.Join(",",
Enum.GetValues(typeof(MovieMethods))
.OfType<MovieMethods>()
.Except(new[] { MovieMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
using RestResponse<Movie> response = await req.Get<Movie>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
Movie item = await response.GetDataObject().ConfigureAwait(false);
// Patch up data, so that the end user won't notice that we share objects between req-types.
if (item.Videos != null)
item.Videos.Id = item.Id;
if (item.AlternativeTitles != null)
item.AlternativeTitles.Id = item.Id;
if (item.Credits != null)
item.Credits.Id = item.Id;
if (item.Releases != null)
item.Releases.Id = item.Id;
if (item.Keywords != null)
item.Keywords.Id = item.Id;
if (item.Translations != null)
item.Translations.Id = item.Id;
if (item.AccountStates != null)
item.AccountStates.Id = item.Id;
if (item.ExternalIds != null)
item.ExternalIds.Id = item.Id;
// Overview is the only field that is HTML encoded from the source.
item.Overview = WebUtility.HtmlDecode(item.Overview);
return item;
}
public async Task<Credits> GetMovieCreditsAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<Credits>(movieId, MovieMethods.Credits, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns an object that contains all known exteral id's for the movie related to the specified TMDB id.
/// </summary>
/// <param name="id">The TMDb id of the target movie.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<ExternalIdsMovie> GetMovieExternalIdsAsync(int id, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<ExternalIdsMovie>(id, MovieMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ImagesWithId> GetMovieImagesAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieImagesAsync(movieId, DefaultLanguage, null, cancellationToken).ConfigureAwait(false);
}
public async Task<ImagesWithId> GetMovieImagesAsync(int movieId, string language, string includeImageLanguage = null, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<ImagesWithId>(movieId, MovieMethods.Images, language: language, includeImageLanguage: includeImageLanguage, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<KeywordsContainer> GetMovieKeywordsAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<KeywordsContainer>(movieId, MovieMethods.Keywords, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<Movie> GetMovieLatestAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("movie/latest");
using RestResponse<Movie> resp = await req.Get<Movie>(cancellationToken).ConfigureAwait(false);
Movie item = await resp.GetDataObject().ConfigureAwait(false);
// Overview is the only field that is HTML encoded from the source.
if (item != null)
item.Overview = WebUtility.HtmlDecode(item.Overview);
return item;
}
public async Task<SearchContainerWithId<ListResult>> GetMovieListsAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieListsAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<ListResult>> GetMovieListsAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<SearchContainerWithId<ListResult>>(movieId, MovieMethods.Lists, page: page, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> GetMovieRecommendationsAsync(int id, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieRecommendationsAsync(id, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> GetMovieRecommendationsAsync(int id, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<SearchContainer<SearchMovie>>(id, MovieMethods.Recommendations, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithDates<SearchMovie>> GetMovieNowPlayingListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("movie/now_playing");
if (page >= 1)
req.AddParameter("page", page.ToString());
if (language != null)
req.AddParameter("language", language);
if (region != null)
req.AddParameter("region", region);
SearchContainerWithDates<SearchMovie> resp = await req.GetOfT<SearchContainerWithDates<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<SearchMovie>> GetMoviePopularListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("movie/popular");
if (page >= 1)
req.AddParameter("page", page.ToString());
if (language != null)
req.AddParameter("language", language);
if (region != null)
req.AddParameter("region", region);
SearchContainer<SearchMovie> resp = await req.GetOfT<SearchContainer<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<ResultContainer<ReleaseDatesContainer>> GetMovieReleaseDatesAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<ResultContainer<ReleaseDatesContainer>>(movieId, MovieMethods.ReleaseDates, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<Releases> GetMovieReleasesAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<Releases>(movieId, MovieMethods.Releases, dateFormat: "yyyy-MM-dd", cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<ReviewBase>> GetMovieReviewsAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieReviewsAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<ReviewBase>> GetMovieReviewsAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<SearchContainerWithId<ReviewBase>>(movieId, MovieMethods.Reviews, page: page, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> GetMovieSimilarAsync(int movieId, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieSimilarAsync(movieId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> GetMovieSimilarAsync(int movieId, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<SearchContainer<SearchMovie>>(movieId, MovieMethods.Similar, page: page, language: language, dateFormat: "yyyy-MM-dd", cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> GetMovieTopRatedListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("movie/top_rated");
if (page >= 1)
req.AddParameter("page", page.ToString());
if (language != null)
req.AddParameter("language", language);
if (region != null)
req.AddParameter("region", region);
SearchContainer<SearchMovie> resp = await req.GetOfT<SearchContainer<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<TranslationsContainer> GetMovieTranslationsAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<TranslationsContainer>(movieId, MovieMethods.Translations, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithDates<SearchMovie>> GetMovieUpcomingListAsync(string language = null, int page = 0, string region = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("movie/upcoming");
if (page >= 1)
req.AddParameter("page", page.ToString());
if (language != null)
req.AddParameter("language", language);
if (region != null)
req.AddParameter("region", region);
SearchContainerWithDates<SearchMovie> resp = await req.GetOfT<SearchContainerWithDates<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<ResultContainer<Video>> GetMovieVideosAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<ResultContainer<Video>>(movieId, MovieMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SingleResultContainer<Dictionary<string, WatchProviders>>> GetMovieWatchProvidersAsync(int movieId, CancellationToken cancellationToken = default)
{
return await GetMovieMethodInternal<SingleResultContainer<Dictionary<string, WatchProviders>>>(movieId, MovieMethods.WatchProviders, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<bool> MovieRemoveRatingAsync(int movieId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("movie/{movieId}/rating");
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 13 = "The item/record was deleted successfully."
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Previous code checked for item=null
return item != null && item.StatusCode == 13;
}
/// <summary>
/// Change the rating of a specified movie.
/// </summary>
/// <param name="movieId">The id of the movie to rate</param>
/// <param name="rating">The rating you wish to assign to the specified movie. Value needs to be between 0.5 and 10 and must use increments of 0.5. Ex. using 7.1 will not work and return false.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the the movie's rating was successfully updated, false if not</returns>
/// <remarks>Requires a valid guest or user session</remarks>
/// <exception cref="GuestSessionRequiredException">Thrown when the current client object doens't have a guest or user session assigned.</exception>
public async Task<bool> MovieSetRatingAsync(int movieId, double rating, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("movie/{movieId}/rating");
req.AddUrlSegment("movieId", movieId.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
req.SetBody(new { value = rating });
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 1 = "Success"
// status code 12 = "The item/record was updated successfully" - Used when an item was previously rated by the user
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Previous code checked for item=null
return item.StatusCode == 1 || item.StatusCode == 12;
}
}
}

View File

@ -0,0 +1,57 @@
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
/// <summary>
/// Retrieves a network by it's TMDb id. A network is a distributor of media content ex. HBO, AMC
/// </summary>
/// <param name="networkId">The id of the network object to retrieve</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<Network> GetNetworkAsync(int networkId, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("network/{networkId}");
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
Network response = await req.GetOfT<Network>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Gets the logos of a network given a TMDb id
/// </summary>
/// <param name="networkId">The TMDb id of the network</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<NetworkLogos> GetNetworkImagesAsync(int networkId, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("network/{networkId}/images");
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
NetworkLogos response = await req.GetOfT<NetworkLogos>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Gets the alternative names of a network given a TMDb id
/// </summary>
/// <param name="networkId">The TMDb id of the network</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<AlternativeNames> GetNetworkAlternativeNamesAsync(int networkId, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("network/{networkId}/alternative_names");
req.AddUrlSegment("networkId", networkId.ToString(CultureInfo.InvariantCulture));
AlternativeNames response = await req.GetOfT<AlternativeNames>(cancellationToken).ConfigureAwait(false);
return response;
}
}
}

View File

@ -0,0 +1,170 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.People;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetPersonMethodInternal<T>(int personId, PersonMethods personMethod, string dateFormat = null, string country = null, string language = null,
int page = 0, DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("person/{personId}/{method}");
req.AddUrlSegment("personId", personId.ToString());
req.AddUrlSegment("method", personMethod.GetDescription());
// TODO: Dateformat?
//if (dateFormat != null)
// req.DateFormat = dateFormat;
if (country != null)
req.AddParameter("country", country);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (page >= 1)
req.AddParameter("page", page.ToString());
if (startDate.HasValue)
req.AddParameter("startDate", startDate.Value.ToString("yyyy-MM-dd"));
if (endDate != null)
req.AddParameter("endDate", endDate.Value.ToString("yyyy-MM-dd"));
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<Person> GetLatestPersonAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("person/latest");
// TODO: Dateformat?
//req.DateFormat = "yyyy-MM-dd";
Person resp = await req.GetOfT<Person>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<Person> GetPersonAsync(int personId, PersonMethods extraMethods = PersonMethods.Undefined,
CancellationToken cancellationToken = default)
{
return await GetPersonAsync(personId, DefaultLanguage, extraMethods, cancellationToken).ConfigureAwait(false);
}
public async Task<Person> GetPersonAsync(int personId, string language, PersonMethods extraMethods = PersonMethods.Undefined, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("person/{personId}");
req.AddUrlSegment("personId", personId.ToString());
if (language != null)
req.AddParameter("language", language);
string appends = string.Join(",",
Enum.GetValues(typeof(PersonMethods))
.OfType<PersonMethods>()
.Except(new[] { PersonMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
// TODO: Dateformat?
//req.DateFormat = "yyyy-MM-dd";
using RestResponse<Person> response = await req.Get<Person>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
Person item = await response.GetDataObject().ConfigureAwait(false);
// Patch up data, so that the end user won't notice that we share objects between request-types.
if (item != null)
{
if (item.Images != null)
item.Images.Id = item.Id;
if (item.TvCredits != null)
item.TvCredits.Id = item.Id;
if (item.MovieCredits != null)
item.MovieCredits.Id = item.Id;
}
return item;
}
public async Task<ExternalIdsPerson> GetPersonExternalIdsAsync(int personId, CancellationToken cancellationToken = default)
{
return await GetPersonMethodInternal<ExternalIdsPerson>(personId, PersonMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ProfileImages> GetPersonImagesAsync(int personId, CancellationToken cancellationToken = default)
{
return await GetPersonMethodInternal<ProfileImages>(personId, PersonMethods.Images, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<PersonResult>> GetPersonListAsync(PersonListType type, int page = 0, CancellationToken cancellationToken = default)
{
RestRequest req;
switch (type)
{
case PersonListType.Popular:
req = _client.Create("person/popular");
break;
default:
throw new ArgumentOutOfRangeException(nameof(type));
}
if (page >= 1)
req.AddParameter("page", page.ToString());
// TODO: Dateformat?
//req.DateFormat = "yyyy-MM-dd";
SearchContainer<PersonResult> resp = await req.GetOfT<SearchContainer<PersonResult>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<MovieCredits> GetPersonMovieCreditsAsync(int personId, CancellationToken cancellationToken = default)
{
return await GetPersonMovieCreditsAsync(personId, DefaultLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<MovieCredits> GetPersonMovieCreditsAsync(int personId, string language, CancellationToken cancellationToken = default)
{
return await GetPersonMethodInternal<MovieCredits>(personId, PersonMethods.MovieCredits, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<TaggedImage>> GetPersonTaggedImagesAsync(int personId, int page, CancellationToken cancellationToken = default)
{
return await GetPersonTaggedImagesAsync(personId, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<TaggedImage>> GetPersonTaggedImagesAsync(int personId, string language, int page, CancellationToken cancellationToken = default)
{
return await GetPersonMethodInternal<SearchContainerWithId<TaggedImage>>(personId, PersonMethods.TaggedImages, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<TvCredits> GetPersonTvCreditsAsync(int personId, CancellationToken cancellationToken = default)
{
return await GetPersonTvCreditsAsync(personId, DefaultLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<TvCredits> GetPersonTvCreditsAsync(int personId, string language, CancellationToken cancellationToken = default)
{
return await GetPersonMethodInternal<TvCredits>(personId, PersonMethods.TvCredits, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Reviews;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<Review> GetReviewAsync(string reviewId, CancellationToken cancellationToken = default)
{
RestRequest request = _client.Create("review/{reviewId}");
request.AddUrlSegment("reviewId", reviewId);
// TODO: Dateformat?
//request.DateFormat = "yyyy-MM-dd";
Review resp = await request.GetOfT<Review>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,112 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> SearchMethodInternal<T>(string method, string query, int page, string language = null, bool? includeAdult = null, int year = 0, string dateFormat = null, string region = null, int primaryReleaseYear = 0, int firstAirDateYear = 0, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("search/{method}");
req.AddUrlSegment("method", method);
req.AddParameter("query", query);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (page >= 1)
req.AddParameter("page", page.ToString());
if (year >= 1)
req.AddParameter("year", year.ToString());
if (includeAdult.HasValue)
req.AddParameter("include_adult", includeAdult.Value ? "true" : "false");
// TODO: Dateformat?
//if (dateFormat != null)
// req.DateFormat = dateFormat;
if (!string.IsNullOrWhiteSpace(region))
req.AddParameter("region", region);
if (primaryReleaseYear >= 1)
req.AddParameter("primary_release_year", primaryReleaseYear.ToString());
if (firstAirDateYear >= 1)
req.AddParameter("first_air_date_year", firstAirDateYear.ToString());
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<SearchCollection>> SearchCollectionAsync(string query, int page = 0, CancellationToken cancellationToken = default)
{
return await SearchCollectionAsync(query, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchCollection>> SearchCollectionAsync(string query, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchCollection>>("collection", query, page, language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchCompany>> SearchCompanyAsync(string query, int page = 0, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchCompany>>("company", query, page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchKeyword>> SearchKeywordAsync(string query, int page = 0, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchKeyword>>("keyword", query, page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
[Obsolete("20200701 No longer present in public API")]
public async Task<SearchContainer<SearchList>> SearchListAsync(string query, int page = 0, bool includeAdult = false, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchList>>("list", query, page, includeAdult: includeAdult, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> SearchMovieAsync(string query, int page = 0, bool includeAdult = false, int year = 0, string region = null, int primaryReleaseYear = 0, CancellationToken cancellationToken = default)
{
return await SearchMovieAsync(query, DefaultLanguage, page, includeAdult, year, region, primaryReleaseYear, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchMovie>> SearchMovieAsync(string query, string language, int page = 0, bool includeAdult = false, int year = 0, string region = null, int primaryReleaseYear = 0, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchMovie>>("movie", query, page, language, includeAdult, year, "yyyy-MM-dd", region, primaryReleaseYear, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchBase>> SearchMultiAsync(string query, int page = 0, bool includeAdult = false, int year = 0, string region = null, CancellationToken cancellationToken = default)
{
return await SearchMultiAsync(query, DefaultLanguage, page, includeAdult, year, region, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchBase>> SearchMultiAsync(string query, string language, int page = 0, bool includeAdult = false, int year = 0, string region = null, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchBase>>("multi", query, page, language, includeAdult, year, "yyyy-MM-dd", region, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchPerson>> SearchPersonAsync(string query, int page = 0, bool includeAdult = false, string region = null, CancellationToken cancellationToken = default)
{
return await SearchPersonAsync(query, DefaultLanguage, page, includeAdult, region, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchPerson>> SearchPersonAsync(string query, string language, int page = 0, bool includeAdult = false, string region = null, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchPerson>>("person", query, page, language, includeAdult, region: region, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> SearchTvShowAsync(string query, int page = 0, bool includeAdult = false, int firstAirDateYear = 0, CancellationToken cancellationToken = default)
{
return await SearchTvShowAsync(query, DefaultLanguage, page, includeAdult, firstAirDateYear, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> SearchTvShowAsync(string query, string language, int page = 0, bool includeAdult = false, int firstAirDateYear = 0, CancellationToken cancellationToken = default)
{
return await SearchMethodInternal<SearchContainer<SearchTv>>("tv", query, page, language, includeAdult, firstAirDateYear: firstAirDateYear, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,52 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.Trending;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
public async Task<SearchContainer<SearchMovie>> GetTrendingMoviesAsync(TimeWindow timeWindow, int page = 0, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("trending/movie/{time_window}");
req.AddUrlSegment("time_window", timeWindow.GetDescription());
if (page >= 1)
req.AddQueryString("page", page.ToString());
SearchContainer<SearchMovie> resp = await req.GetOfT<SearchContainer<SearchMovie>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<SearchTv>> GetTrendingTvAsync(TimeWindow timeWindow, int page = 0, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("trending/tv/{time_window}");
req.AddUrlSegment("time_window", timeWindow.GetDescription());
if (page >= 1)
req.AddQueryString("page", page.ToString());
SearchContainer<SearchTv> resp = await req.GetOfT<SearchContainer<SearchTv>>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<SearchContainer<SearchPerson>> GetTrendingPeopleAsync(TimeWindow timeWindow, int page = 0, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("trending/person/{time_window}");
req.AddUrlSegment("time_window", timeWindow.GetDescription());
if (page >= 1)
req.AddQueryString("page", page.ToString());
SearchContainer<SearchPerson> resp = await req.GetOfT<SearchContainer<SearchPerson>>(cancellationToken).ConfigureAwait(false);
return resp;
}
}
}

View File

@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
/// <summary>
/// Retrieve a collection of tv episode groups by id
/// </summary>
/// <param name="id">Episode group id</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The requested collection of tv episode groups</returns>
public async Task<TvGroupCollection> GetTvEpisodeGroupsAsync(string id, string language = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("tv/episode_group/{id}");
req.AddUrlSegment("id", id);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
using RestResponse<TvGroupCollection> response = await req.Get<TvGroupCollection>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
return await response.GetDataObject().ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,219 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
using TMDbLib.Utilities;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetTvEpisodeMethodInternal<T>(int tvShowId, int seasonNumber, int episodeNumber, TvEpisodeMethods tvShowMethod, string dateFormat = null, string language = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("tv/{id}/season/{season_number}/episode/{episode_number}/{method}");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("episode_number", episodeNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", tvShowMethod.GetDescription());
// TODO: Dateformat?
//if (dateFormat != null)
// req.DateFormat = dateFormat;
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
public async Task<TvEpisodeAccountState> GetTvEpisodeAccountStateAsync(int tvShowId, int seasonNumber, int episodeNumber, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}/episode/{episode_number}/account_states");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("episode_number", episodeNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", TvEpisodeMethods.AccountStates.GetDescription());
AddSessionId(req, SessionType.UserSession);
using RestResponse<TvEpisodeAccountState> response = await req.Get<TvEpisodeAccountState>(cancellationToken).ConfigureAwait(false);
return await response.GetDataObject().ConfigureAwait(false);
}
/// <summary>
/// Retrieve a specific episode using TMDb id of the associated tv show.
/// </summary>
/// <param name="tvShowId">TMDb id of the tv show the desired episode belongs to.</param>
/// <param name="seasonNumber">The season number of the season the episode belongs to. Note use 0 for specials.</param>
/// <param name="episodeNumber">The episode number of the episode you want to retrieve.</param>
/// <param name="extraMethods">Enum flags indicating any additional data that should be fetched in the same request.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="includeImageLanguage">If specified the api will attempt to return localized image results eg. en,it,es.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<TvEpisode> GetTvEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, TvEpisodeMethods extraMethods = TvEpisodeMethods.Undefined, string language = null, string includeImageLanguage = null, CancellationToken cancellationToken = default)
{
if (extraMethods.HasFlag(TvEpisodeMethods.AccountStates))
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}/episode/{episode_number}");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("episode_number", episodeNumber.ToString(CultureInfo.InvariantCulture));
if (extraMethods.HasFlag(TvEpisodeMethods.AccountStates))
AddSessionId(req, SessionType.UserSession);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
includeImageLanguage ??= DefaultImageLanguage;
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
req.AddParameter("include_image_language", includeImageLanguage);
string appends = string.Join(",",
Enum.GetValues(typeof(TvEpisodeMethods))
.OfType<TvEpisodeMethods>()
.Except(new[] { TvEpisodeMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
using RestResponse<TvEpisode> response = await req.Get<TvEpisode>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
TvEpisode item = await response.GetDataObject().ConfigureAwait(false);
// No data to patch up so return
if (item == null)
return null;
// Patch up data, so that the end user won't notice that we share objects between request-types.
if (item.Videos != null)
item.Videos.Id = item.Id ?? 0;
if (item.Credits != null)
item.Credits.Id = item.Id ?? 0;
if (item.Images != null)
item.Images.Id = item.Id ?? 0;
if (item.ExternalIds != null)
item.ExternalIds.Id = item.Id ?? 0;
return item;
}
public async Task<ResultContainer<TvEpisodeInfo>> GetTvEpisodesScreenedTheatricallyAsync(int tvShowId, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("tv/{tv_id}/screened_theatrically");
req.AddUrlSegment("tv_id", tvShowId.ToString(CultureInfo.InvariantCulture));
return await req.GetOfT<ResultContainer<TvEpisodeInfo>>(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns a credits object for the specified episode.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season the episode belongs to. Note use 0 for specials.</param>
/// <param name="episodeNumber">The episode number of the episode you want to retrieve information for.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<CreditsWithGuestStars> GetTvEpisodeCreditsAsync(int tvShowId, int seasonNumber, int episodeNumber, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvEpisodeMethodInternal<CreditsWithGuestStars>(tvShowId, seasonNumber, episodeNumber, TvEpisodeMethods.Credits, dateFormat: "yyyy-MM-dd", language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns an object that contains all known exteral id's for the specified episode.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season the episode belongs to. Note use 0 for specials.</param>
/// <param name="episodeNumber">The episode number of the episode you want to retrieve information for.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<ExternalIdsTvEpisode> GetTvEpisodeExternalIdsAsync(int tvShowId, int seasonNumber, int episodeNumber, CancellationToken cancellationToken = default)
{
return await GetTvEpisodeMethodInternal<ExternalIdsTvEpisode>(tvShowId, seasonNumber, episodeNumber, TvEpisodeMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieves all images all related to the season of specified episode.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season the episode belongs to. Note use 0 for specials.</param>
/// <param name="episodeNumber">The episode number of the episode you want to retrieve information for.</param>
/// <param name="language">
/// If specified the api will attempt to return a localized result. ex: en,it,es.
/// For images this means that the image might contain language specifc text
/// </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<StillImages> GetTvEpisodeImagesAsync(int tvShowId, int seasonNumber, int episodeNumber, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvEpisodeMethodInternal<StillImages>(tvShowId, seasonNumber, episodeNumber, TvEpisodeMethods.Images, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ResultContainer<Video>> GetTvEpisodeVideosAsync(int tvShowId, int seasonNumber, int episodeNumber, CancellationToken cancellationToken = default)
{
return await GetTvEpisodeMethodInternal<ResultContainer<Video>>(tvShowId, seasonNumber, episodeNumber, TvEpisodeMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<bool> TvEpisodeRemoveRatingAsync(int tvShowId, int seasonNumber, int episodeNumber, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}/episode/{episode_number}/rating");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("episode_number", episodeNumber.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 13 = "The item/record was deleted successfully."
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Original code had a check for item=null
return item.StatusCode == 13;
}
public async Task<bool> TvEpisodeSetRatingAsync(int tvShowId, int seasonNumber, int episodeNumber, double rating, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}/episode/{episode_number}/rating");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("episode_number", episodeNumber.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
req.SetBody(new { value = rating });
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 1 = "Success"
// status code 12 = "The item/record was updated successfully" - Used when an item was previously rated by the user
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Original code had a check for item=null
return item.StatusCode == 1 || item.StatusCode == 12;
}
}
}

View File

@ -0,0 +1,164 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.General;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
using TMDbLib.Utilities;
using Credits = TMDbLib.Objects.TvShows.Credits;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetTvSeasonMethodInternal<T>(int tvShowId, int seasonNumber, TvSeasonMethods tvShowMethod, string dateFormat = null, string language = null, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("tv/{id}/season/{season_number}/{method}");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", tvShowMethod.GetDescription());
// TODO: Dateformat?
//if (dateFormat != null)
// req.DateFormat = dateFormat;
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
T response = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return response;
}
public async Task<ResultContainer<TvEpisodeAccountStateWithNumber>> GetTvSeasonAccountStateAsync(int tvShowId, int seasonNumber, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}/account_states");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", TvEpisodeMethods.AccountStates.GetDescription());
AddSessionId(req, SessionType.UserSession);
using RestResponse<ResultContainer<TvEpisodeAccountStateWithNumber>> response = await req.Get<ResultContainer<TvEpisodeAccountStateWithNumber>>(cancellationToken).ConfigureAwait(false);
return await response.GetDataObject().ConfigureAwait(false);
}
/// <summary>
/// Retrieve a season for a specifc tv Show by id.
/// </summary>
/// <param name="tvShowId">TMDb id of the tv show the desired season belongs to.</param>
/// <param name="seasonNumber">The season number of the season you want to retrieve. Note use 0 for specials.</param>
/// <param name="extraMethods">Enum flags indicating any additional data that should be fetched in the same request.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="includeImageLanguage">If specified the api will attempt to return localized image results eg. en,it,es.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The requested season for the specified tv show</returns>
public async Task<TvSeason> GetTvSeasonAsync(int tvShowId, int seasonNumber, TvSeasonMethods extraMethods = TvSeasonMethods.Undefined, string language = null, string includeImageLanguage = null, CancellationToken cancellationToken = default)
{
if (extraMethods.HasFlag(TvSeasonMethods.AccountStates))
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{id}/season/{season_number}");
req.AddUrlSegment("id", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("season_number", seasonNumber.ToString(CultureInfo.InvariantCulture));
if (extraMethods.HasFlag(TvSeasonMethods.AccountStates))
AddSessionId(req, SessionType.UserSession);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
includeImageLanguage ??= DefaultImageLanguage;
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
req.AddParameter("include_image_language", includeImageLanguage);
string appends = string.Join(",",
Enum.GetValues(typeof(TvSeasonMethods))
.OfType<TvSeasonMethods>()
.Except(new[] { TvSeasonMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
using RestResponse<TvSeason> response = await req.Get<TvSeason>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
TvSeason item = await response.GetDataObject().ConfigureAwait(false);
// Nothing to patch up
if (item == null)
return null;
if (item.Images != null)
item.Images.Id = item.Id ?? 0;
if (item.Credits != null)
item.Credits.Id = item.Id ?? 0;
if (item.ExternalIds != null)
item.ExternalIds.Id = item.Id ?? 0;
if (item.AccountStates != null)
item.AccountStates.Id = item.Id ?? 0;
if (item.Videos != null)
item.Videos.Id = item.Id ?? 0;
return item;
}
/// <summary>
/// Returns a credits object for the season of the tv show associated with the provided TMDb id.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season you want to retrieve information for. Note use 0 for specials.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<Credits> GetTvSeasonCreditsAsync(int tvShowId, int seasonNumber, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvSeasonMethodInternal<Credits>(tvShowId, seasonNumber, TvSeasonMethods.Credits, dateFormat: "yyyy-MM-dd", language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns an object that contains all known exteral id's for the season of the tv show related to the specified TMDB id.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season you want to retrieve information for. Note use 0 for specials.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<ExternalIdsTvSeason> GetTvSeasonExternalIdsAsync(int tvShowId, int seasonNumber, CancellationToken cancellationToken = default)
{
return await GetTvSeasonMethodInternal<ExternalIdsTvSeason>(tvShowId, seasonNumber, TvSeasonMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieves all images all related to the season of specified tv show.
/// </summary>
/// <param name="tvShowId">The TMDb id of the target tv show.</param>
/// <param name="seasonNumber">The season number of the season you want to retrieve information for. Note use 0 for specials.</param>
/// <param name="language">
/// If specified the api will attempt to return a localized result. ex: en,it,es.
/// For images this means that the image might contain language specifc text
/// </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<PosterImages> GetTvSeasonImagesAsync(int tvShowId, int seasonNumber, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvSeasonMethodInternal<PosterImages>(tvShowId, seasonNumber, TvSeasonMethods.Images, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ResultContainer<Video>> GetTvSeasonVideosAsync(int tvShowId, int seasonNumber, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvSeasonMethodInternal<ResultContainer<Video>>(tvShowId, seasonNumber, TvSeasonMethods.Videos, language: language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.Authentication;
using TMDbLib.Objects.Changes;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Reviews;
using TMDbLib.Objects.Search;
using TMDbLib.Objects.TvShows;
using TMDbLib.Rest;
using TMDbLib.Utilities;
using Credits = TMDbLib.Objects.TvShows.Credits;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
private async Task<T> GetTvShowMethodInternal<T>(int id, TvShowMethods tvShowMethod, string dateFormat = null, string language = null, string includeImageLanguage = null, int page = 0, CancellationToken cancellationToken = default) where T : new()
{
RestRequest req = _client.Create("tv/{id}/{method}");
req.AddUrlSegment("id", id.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", tvShowMethod.GetDescription());
// TODO: Dateformat?
//if (dateFormat != null)
// req.DateFormat = dateFormat;
if (page > 0)
req.AddParameter("page", page.ToString());
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
includeImageLanguage ??= DefaultImageLanguage;
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
req.AddParameter("include_image_language", includeImageLanguage);
T resp = await req.GetOfT<T>(cancellationToken).ConfigureAwait(false);
return resp;
}
private async Task<SearchContainer<SearchTv>> GetTvShowListInternal(int page, string language, string tvShowListType, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("tv/" + tvShowListType);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (page >= 1)
req.AddParameter("page", page.ToString());
SearchContainer<SearchTv> response = await req.GetOfT<SearchContainer<SearchTv>>(cancellationToken).ConfigureAwait(false);
return response;
}
public async Task<TvShow> GetLatestTvShowAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("tv/latest");
TvShow resp = await req.GetOfT<TvShow>(cancellationToken).ConfigureAwait(false);
return resp;
}
/// <summary>
/// Retrieves all information for a specific tv show in relation to the current user account
/// </summary>
/// <param name="tvShowId">The id of the tv show to get the account states for</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Requires a valid user session</remarks>
/// <exception cref="UserSessionRequiredException">Thrown when the current client object doens't have a user session assigned.</exception>
public async Task<AccountState> GetTvShowAccountStateAsync(int tvShowId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{tvShowId}/{method}");
req.AddUrlSegment("tvShowId", tvShowId.ToString(CultureInfo.InvariantCulture));
req.AddUrlSegment("method", TvShowMethods.AccountStates.GetDescription());
AddSessionId(req, SessionType.UserSession);
using RestResponse<AccountState> response = await req.Get<AccountState>(cancellationToken).ConfigureAwait(false);
return await response.GetDataObject().ConfigureAwait(false);
}
public async Task<ResultContainer<AlternativeTitle>> GetTvShowAlternativeTitlesAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ResultContainer<AlternativeTitle>>(id, TvShowMethods.AlternativeTitles, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieve a tv Show by id.
/// </summary>
/// <param name="id">TMDb id of the tv show to retrieve.</param>
/// <param name="extraMethods">Enum flags indicating any additional data that should be fetched in the same request.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es.</param>
/// <param name="includeImageLanguage">If specified the api will attempt to return localized image results eg. en,it,es.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The requested Tv Show</returns>
public async Task<TvShow> GetTvShowAsync(int id, TvShowMethods extraMethods = TvShowMethods.Undefined, string language = null, string includeImageLanguage = null, CancellationToken cancellationToken = default)
{
if (extraMethods.HasFlag(TvShowMethods.AccountStates))
RequireSessionId(SessionType.UserSession);
RestRequest req = _client.Create("tv/{id}");
req.AddUrlSegment("id", id.ToString(CultureInfo.InvariantCulture));
if (extraMethods.HasFlag(TvShowMethods.AccountStates))
AddSessionId(req, SessionType.UserSession);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
if (!string.IsNullOrWhiteSpace(includeImageLanguage))
req.AddParameter("include_image_language", includeImageLanguage);
string appends = string.Join(",",
Enum.GetValues(typeof(TvShowMethods))
.OfType<TvShowMethods>()
.Except(new[] { TvShowMethods.Undefined })
.Where(s => extraMethods.HasFlag(s))
.Select(s => s.GetDescription()));
if (appends != string.Empty)
req.AddParameter("append_to_response", appends);
using RestResponse<TvShow> response = await req.Get<TvShow>(cancellationToken).ConfigureAwait(false);
if (!response.IsValid)
return null;
TvShow item = await response.GetDataObject().ConfigureAwait(false);
// No data to patch up so return
if (item == null)
return null;
// Patch up data, so that the end user won't notice that we share objects between request-types.
if (item.Translations != null)
item.Translations.Id = id;
if (item.AccountStates != null)
item.AccountStates.Id = id;
if (item.Recommendations != null)
item.Recommendations.Id = id;
if (item.ExternalIds != null)
item.ExternalIds.Id = id;
return item;
}
public async Task<ChangesContainer> GetTvShowChangesAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ChangesContainer>(id, TvShowMethods.Changes, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ResultContainer<ContentRating>> GetTvShowContentRatingsAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ResultContainer<ContentRating>>(id, TvShowMethods.ContentRatings, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns a credits object for the tv show associated with the provided TMDb id.
/// </summary>
/// <param name="id">The TMDb id of the target tv show.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<Credits> GetTvShowCreditsAsync(int id, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<Credits>(id, TvShowMethods.Credits, "yyyy-MM-dd", language, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns a credits object for the aggragation of tv show associated with the provided TMDb id.
/// </summary>
/// <param name="id">The TMDb id of the target tv show.</param>
/// <param name="language">If specified the api will attempt to return a localized result. ex: en,it,es </param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns></returns>
public async Task<CreditsAggregate> GetAggregateCredits(int id, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<CreditsAggregate>(id, TvShowMethods.CreditsAggregate, language: language, page: 0, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Returns an object that contains all known exteral id's for the tv show related to the specified TMDB id.
/// </summary>
/// <param name="id">The TMDb id of the target tv show.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<ExternalIdsTvShow> GetTvShowExternalIdsAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ExternalIdsTvShow>(id, TvShowMethods.ExternalIds, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Retrieves all images all related to the specified tv show.
/// </summary>
/// <param name="id">The TMDb id of the target tv show.</param>
/// <param name="language">
/// If specified the api will attempt to return a localized result. ex: en,it,es.
/// For images this means that the image might contain language specifc text
/// </param>
/// <param name="includeImageLanguage">If you want to include a fallback language (especially useful for backdrops) you can use the include_image_language parameter. This should be a comma separated value like so: include_image_language=en,null.</param>
/// <param name="cancellationToken">A cancellation token</param>
public async Task<ImagesWithId> GetTvShowImagesAsync(int id, string language = null, string includeImageLanguage = null, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ImagesWithId>(id, TvShowMethods.Images, language: language, includeImageLanguage: includeImageLanguage, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainerWithId<ReviewBase>> GetTvShowReviewsAsync(int id, string language = null, int page = 0, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<SearchContainerWithId<ReviewBase>>(id, TvShowMethods.Reviews, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ResultContainer<Keyword>> GetTvShowKeywordsAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ResultContainer<Keyword>>(id, TvShowMethods.Keywords, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Fetches a dynamic list of TV Shows
/// </summary>
/// <param name="list">Type of list to fetch</param>
/// <param name="page">Page</param>
/// <param name="timezone">Only relevant for list type AiringToday</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns></returns>
public async Task<SearchContainer<SearchTv>> GetTvShowListAsync(TvShowListType list, int page = 0, string timezone = null, CancellationToken cancellationToken = default)
{
return await GetTvShowListAsync(list, DefaultLanguage, page, timezone, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Fetches a dynamic list of TV Shows
/// </summary>
/// <param name="list">Type of list to fetch</param>
/// <param name="language">Language</param>
/// <param name="page">Page</param>
/// <param name="timezone">Only relevant for list type AiringToday</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns></returns>
public async Task<SearchContainer<SearchTv>> GetTvShowListAsync(TvShowListType list, string language, int page = 0, string timezone = null, CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("tv/{method}");
req.AddUrlSegment("method", list.GetDescription());
if (page > 0)
req.AddParameter("page", page.ToString());
if (!string.IsNullOrEmpty(timezone))
req.AddParameter("timezone", timezone);
language ??= DefaultLanguage;
if (!string.IsNullOrWhiteSpace(language))
req.AddParameter("language", language);
SearchContainer<SearchTv> resp = await req.GetOfT<SearchContainer<SearchTv>>(cancellationToken).ConfigureAwait(false);
return resp;
}
/// <summary>
/// Get the list of popular TV shows. This list refreshes every day.
/// </summary>
/// <returns>
/// Returns the basic information about a tv show.
/// For additional data use the main GetTvShowAsync method using the tv show id as parameter.
/// </returns>
public async Task<SearchContainer<SearchTv>> GetTvShowPopularAsync(int page = -1, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvShowListInternal(page, language, "popular", cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> GetTvShowSimilarAsync(int id, int page = 0, CancellationToken cancellationToken = default)
{
return await GetTvShowSimilarAsync(id, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> GetTvShowSimilarAsync(int id, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<SearchContainer<SearchTv>>(id, TvShowMethods.Similar, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> GetTvShowRecommendationsAsync(int id, int page = 0, CancellationToken cancellationToken = default)
{
return await GetTvShowRecommendationsAsync(id, DefaultLanguage, page, cancellationToken).ConfigureAwait(false);
}
public async Task<SearchContainer<SearchTv>> GetTvShowRecommendationsAsync(int id, string language, int page = 0, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<SearchContainer<SearchTv>>(id, TvShowMethods.Recommendations, language: language, page: page, cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Get the list of top rated TV shows. By default, this list will only include TV shows that have 2 or more votes. This list refreshes every day.
/// </summary>
/// <returns>
/// Returns the basic information about a tv show.
/// For additional data use the main GetTvShowAsync method using the tv show id as parameter
/// </returns>
public async Task<SearchContainer<SearchTv>> GetTvShowTopRatedAsync(int page = -1, string language = null, CancellationToken cancellationToken = default)
{
return await GetTvShowListInternal(page, language, "top_rated", cancellationToken).ConfigureAwait(false);
}
public async Task<TranslationsContainerTv> GetTvShowTranslationsAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<TranslationsContainerTv>(id, TvShowMethods.Translations, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<ResultContainer<Video>> GetTvShowVideosAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<ResultContainer<Video>>(id, TvShowMethods.Videos, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<SingleResultContainer<Dictionary<string, WatchProviders>>> GetTvShowWatchProvidersAsync(int id, CancellationToken cancellationToken = default)
{
return await GetTvShowMethodInternal<SingleResultContainer<Dictionary<string, WatchProviders>>>(id, TvShowMethods.WatchProviders, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<bool> TvShowRemoveRatingAsync(int tvShowId, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("tv/{tvShowId}/rating");
req.AddUrlSegment("tvShowId", tvShowId.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
using RestResponse<PostReply> response = await req.Delete<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 13 = "The item/record was deleted successfully."
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Original code had a check for item=null
return item.StatusCode == 13;
}
/// <summary>
/// Change the rating of a specified tv show.
/// </summary>
/// <param name="tvShowId">The id of the tv show to rate</param>
/// <param name="rating">The rating you wish to assign to the specified tv show. Value needs to be between 0.5 and 10 and must use increments of 0.5. Ex. using 7.1 will not work and return false.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>True if the the tv show's rating was successfully updated, false if not</returns>
/// <remarks>Requires a valid guest or user session</remarks>
/// <exception cref="GuestSessionRequiredException">Thrown when the current client object doens't have a guest or user session assigned.</exception>
public async Task<bool> TvShowSetRatingAsync(int tvShowId, double rating, CancellationToken cancellationToken = default)
{
RequireSessionId(SessionType.GuestSession);
RestRequest req = _client.Create("tv/{tvShowId}/rating");
req.AddUrlSegment("tvShowId", tvShowId.ToString(CultureInfo.InvariantCulture));
AddSessionId(req);
req.SetBody(new { value = rating });
using RestResponse<PostReply> response = await req.Post<PostReply>(cancellationToken).ConfigureAwait(false);
// status code 1 = "Success"
// status code 12 = "The item/record was updated successfully" - Used when an item was previously rated by the user
PostReply item = await response.GetDataObject().ConfigureAwait(false);
// TODO: Original code had a check for item=null
return item.StatusCode == 1 || item.StatusCode == 12;
}
}
}

View File

@ -0,0 +1,64 @@
using System.Threading;
using System.Threading.Tasks;
using TMDbLib.Objects.General;
using TMDbLib.Rest;
namespace TMDbLib.Client
{
public partial class TMDbClient
{
/// <summary>
/// Returns a list of all of the countries TMDb has watch provider (OTT/streaming) data for.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Uses <see cref="DefaultLanguage"/> to translate data</remarks>
public async Task<ResultContainer<WatchProviderRegion>> GetWatchProviderRegionsAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("watch/providers/regions");
if (DefaultLanguage != null)
req.AddParameter("language", DefaultLanguage);
ResultContainer<WatchProviderRegion> response = await req.GetOfT<ResultContainer<WatchProviderRegion>>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Returns a list of the watch provider (OTT/streaming) data TMDb has available for movies.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Uses <see cref="DefaultCountry"/> and <see cref="DefaultLanguage"/> to filter or translate data</remarks>
public async Task<ResultContainer<WatchProviderItem>> GetMovieWatchProvidersAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("watch/providers/movie");
if (DefaultLanguage != null)
req.AddParameter("language", DefaultLanguage);
if (DefaultCountry != null)
req.AddParameter("watch_region", DefaultCountry);
ResultContainer<WatchProviderItem> response = await req.GetOfT<ResultContainer<WatchProviderItem>>(cancellationToken).ConfigureAwait(false);
return response;
}
/// <summary>
/// Returns a list of the watch provider (OTT/streaming) data TMDb has available for shows.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
/// <remarks>Uses <see cref="DefaultCountry"/> and <see cref="DefaultLanguage"/> to filter or translate data</remarks>
public async Task<ResultContainer<WatchProviderItem>> GetTvWatchProvidersAsync(CancellationToken cancellationToken = default)
{
RestRequest req = _client.Create("watch/providers/tv");
if (DefaultLanguage != null)
req.AddParameter("language", DefaultLanguage);
if (DefaultCountry != null)
req.AddParameter("watch_region", DefaultCountry);
ResultContainer<WatchProviderItem> response = await req.GetOfT<ResultContainer<WatchProviderItem>>(cancellationToken).ConfigureAwait(false);
return response;
}
}
}

View File

@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Account
{
public class AccountDetails
{
[JsonProperty("avatar")]
public Avatar Avatar { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("include_adult")]
public bool IncludeAdult { get; set; }
/// <summary>
/// A country code, e.g. US
/// </summary>
[JsonProperty("iso_3166_1")]
public string Iso_3166_1 { get; set; }
/// <summary>
/// A language code, e.g. en
/// </summary>
[JsonProperty("iso_639_1")]
public string Iso_639_1 { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using TMDbLib.Utilities;
namespace TMDbLib.Objects.Account
{
public enum AccountSortBy
{
Undefined = 0,
[EnumValue("created_at")]
CreatedAt = 1,
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Account
{
public class Avatar
{
[JsonProperty("gravatar")]
public Gravatar Gravatar { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Account
{
public class Gravatar
{
[JsonProperty("hash")]
public string Hash { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using Newtonsoft.Json;
using TMDbLib.Utilities.Converters;
namespace TMDbLib.Objects.Authentication
{
/// <summary>
/// A guest session can be used to rate movies/tv shows without having a registered TMDb user account.
/// You should only generate a single guest session per user (or device) as you will be able to attach the ratings to a TMDb user account in the future.
/// There is also IP limits in place so you should always make sure it's the end user doing the guest session actions.
/// If a guest session is not used for the first time within 24 hours, it will be automatically discarded.
/// </summary>
public class GuestSession
{
/// <summary>
/// The date / time before which the session must be used for the first time else it will expire. Time is expressed as local time.
/// </summary>
[JsonProperty("expires_at")]
[JsonConverter(typeof(CustomDatetimeFormatConverter))]
public DateTime ExpiresAt { get; set; }
[JsonProperty("guest_session_id")]
public string GuestSessionId { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace TMDbLib.Objects.Authentication
{
public class GuestSessionRequiredException : Exception
{
public GuestSessionRequiredException()
: base("The method you called requires a valid guest or user session to be set on the client object. Please use the 'SetSessionInformation' method to do so.")
{
}
}
}

View File

@ -0,0 +1,9 @@
namespace TMDbLib.Objects.Authentication
{
public enum SessionType
{
Unassigned = 0,
GuestSession = 1,
UserSession = 2
}
}

View File

@ -0,0 +1,31 @@
using System;
using Newtonsoft.Json;
using TMDbLib.Utilities.Converters;
namespace TMDbLib.Objects.Authentication
{
/// <summary>
/// A request token is required in order to request a user authenticated session id.
/// Request tokens will expire after 60 minutes.
/// As soon as a valid session id has been created the token will be useless.
/// </summary>
public class Token
{
// This field is populated by custom code
[JsonIgnore]
public string AuthenticationCallback { get; set; }
/// <summary>
/// The date / time before which the token must be used, else it will expire. Time is expressed as local time.
/// </summary>
[JsonProperty("expires_at")]
[JsonConverter(typeof(CustomDatetimeFormatConverter))]
public DateTime ExpiresAt { get; set; }
[JsonProperty("request_token")]
public string RequestToken { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Authentication
{
/// <summary>
/// Session object that can be retrieved after the user has correctly authenticated himself on the TMDb site. (using the referal url from the token provided previously)
/// </summary>
public class UserSession
{
[JsonProperty("session_id")]
public string SessionId { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace TMDbLib.Objects.Authentication
{
public class UserSessionRequiredException : Exception
{
public UserSessionRequiredException()
: base("The method you called requires a valid user session to be set on the client object. Please use the 'SetSessionInformation' method to do so.")
{
}
}
}

View File

@ -0,0 +1,9 @@
namespace TMDbLib.Objects.Certifications
{
public class CertificationItem
{
public string Certification { get; set; }
public string Meaning { get; set; }
public int Order { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace TMDbLib.Objects.Certifications
{
public class CertificationsContainer
{
public Dictionary<string, List<CertificationItem>> Certifications { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class Change
{
[JsonProperty("items")]
public List<ChangeItemBase> Items { get; set; }
[JsonProperty("key")]
public string Key { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
using TMDbLib.Utilities;
using TMDbLib.Utilities.Converters;
namespace TMDbLib.Objects.Changes
{
[JsonConverter(typeof(EnumStringValueConverter))]
public enum ChangeAction
{
Unknown,
[EnumValue("added")]
Added = 1,
[EnumValue("created")]
Created = 2,
[EnumValue("updated")]
Updated = 3,
[EnumValue("deleted")]
Deleted = 4,
[EnumValue("destroyed")]
Destroyed = 5
}
}

View File

@ -0,0 +1,15 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class ChangeItemAdded : ChangeItemBase
{
public ChangeItemAdded()
{
Action = ChangeAction.Added;
}
[JsonProperty("value")]
public object Value { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using Newtonsoft.Json;
using TMDbLib.Utilities.Converters;
namespace TMDbLib.Objects.Changes
{
public abstract class ChangeItemBase
{
[JsonProperty("action")]
public ChangeAction Action { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// A language code, e.g. en
/// This field is not always set
/// </summary>
[JsonProperty("iso_639_1")]
public string Iso_639_1 { get; set; }
[JsonProperty("time")]
[JsonConverter(typeof(TmdbUtcTimeConverter))]
public DateTime Time { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace TMDbLib.Objects.Changes
{
public class ChangeItemCreated : ChangeItemBase
{
public ChangeItemCreated()
{
Action = ChangeAction.Created;
}
}
}

View File

@ -0,0 +1,15 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class ChangeItemDeleted : ChangeItemBase
{
public ChangeItemDeleted()
{
Action = ChangeAction.Deleted;
}
[JsonProperty("original_value")]
public object OriginalValue { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class ChangeItemDestroyed : ChangeItemBase
{
public ChangeItemDestroyed()
{
Action = ChangeAction.Destroyed;
}
[JsonProperty("value")]
public object Value { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class ChangeItemUpdated : ChangeItemBase
{
public ChangeItemUpdated()
{
Action = ChangeAction.Updated;
}
[JsonProperty("original_value")]
public object OriginalValue { get; set; }
[JsonProperty("value")]
public object Value { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace TMDbLib.Objects.Changes
{
public class ChangesContainer
{
[JsonProperty("changes")]
public List<Change> Changes { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using Newtonsoft.Json;
using TMDbLib.Utilities.Converters;
namespace TMDbLib.Objects.Changes
{
public class ChangesListItem
{
[JsonProperty("adult")]
public bool? Adult { get; set; }
[JsonProperty("id")]
[JsonConverter(typeof(TmdbNullIntAsZero))]
public int Id { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
namespace TMDbLib.Objects.Collections
{
public class Collection
{
[JsonProperty("backdrop_path")]
public string BackdropPath { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("images")]
public Images Images { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("overview")]
public string Overview { get; set; }
[JsonProperty("parts")]
public List<SearchMovie> Parts { get; set; }
[JsonProperty("poster_path")]
public string PosterPath { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using TMDbLib.Utilities;
namespace TMDbLib.Objects.Collections
{
[Flags]
public enum CollectionMethods
{
[EnumValue("Undefined")]
Undefined = 0,
[EnumValue("images")]
Images = 1
}
}

View File

@ -0,0 +1,36 @@
using Newtonsoft.Json;
using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
namespace TMDbLib.Objects.Companies
{
public class Company
{
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("headquarters")]
public string Headquarters { get; set; }
[JsonProperty("homepage")]
public string Homepage { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("logo_path")]
public string LogoPath { get; set; }
[JsonProperty("movies")]
public SearchContainer<SearchMovie> Movies { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("parent_company")]
public SearchCompany ParentCompany { get; set; }
[JsonProperty("origin_country")]
public string OriginCountry { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using TMDbLib.Utilities;
namespace TMDbLib.Objects.Companies
{
[Flags]
public enum CompanyMethods
{
[EnumValue("Undefined")]
Undefined = 0,
[EnumValue("movies")]
Movies = 1
}
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace TMDbLib.Objects.Configuration
{
public class APIConfiguration
{
[JsonProperty("images")]
public APIConfigurationImages Images { get; set; }
[JsonProperty("change_keys")]
public List<string> ChangeKeys { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace TMDbLib.Objects.Configuration
{
public class APIConfigurationImages
{
[JsonProperty("base_url")]
public string BaseUrl { get; set; }
[JsonProperty("secure_base_url")]
public string SecureBaseUrl { get; set; }
[JsonProperty("backdrop_sizes")]
public List<string> BackdropSizes { get; set; }
[JsonProperty("logo_sizes")]
public List<string> LogoSizes { get; set; }
[JsonProperty("poster_sizes")]
public List<string> PosterSizes { get; set; }
[JsonProperty("profile_sizes")]
public List<string> ProfileSizes { get; set; }
[JsonProperty("still_sizes")]
public List<string> StillSizes { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Newtonsoft.Json;
namespace TMDbLib.Objects.Countries
{
public class Country
{
[JsonProperty("iso_3166_1")]
public string Iso_3166_1 { get; set; }
[JsonProperty("english_name")]
public string EnglishName { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using Newtonsoft.Json;
using TMDbLib.Objects.General;
namespace TMDbLib.Objects.Credit
{
public class Credit
{
[JsonProperty("credit_type")]
public CreditType CreditType { get; set; }
[JsonProperty("department")]
public string Department { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("job")]
public string Job { get; set; }
[JsonProperty("media")]
public CreditMedia Media { get; set; }
[JsonProperty("media_type")]
public MediaType MediaType { get; set; }
[JsonProperty("person")]
public CreditPerson Person { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using Newtonsoft.Json;
namespace TMDbLib.Objects.Credit
{
public class CreditEpisode
{
[JsonProperty("air_date")]
public DateTime? AirDate { get; set; }
[JsonProperty("episode_number")]
public int EpisodeNumber { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("overview")]
public string Overview { get; set; }
[JsonProperty("season_number")]
public int SeasonNumber { get; set; }
[JsonProperty("still_path")]
public string StillPath { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace TMDbLib.Objects.Credit
{
public class CreditMedia
{
[JsonProperty("character")]
public string Character { get; set; }
[JsonProperty("episodes")]
public List<CreditEpisode> Episodes { get; set; }
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("original_name")]
public string OriginalName { get; set; }
[JsonProperty("seasons")]
public List<CreditSeason> Seasons { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More