diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index 844be997..fcf33415 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -22,7 +22,7 @@ - + diff --git a/Kyoo.Tests/Kyoo.Tests.csproj b/Kyoo.Tests/Kyoo.Tests.csproj index 120f8ba7..eeaf81f9 100644 --- a/Kyoo.Tests/Kyoo.Tests.csproj +++ b/Kyoo.Tests/Kyoo.Tests.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,7 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Kyoo.TheMovieDb/Convertors.cs b/Kyoo.TheMovieDb/Convertors.cs new file mode 100644 index 00000000..02497549 --- /dev/null +++ b/Kyoo.TheMovieDb/Convertors.cs @@ -0,0 +1,264 @@ +using System.Linq; +using Kyoo.Models; +using TMDbLib.Objects.General; +using TMDbLib.Objects.Movies; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; +using Genre = Kyoo.Models.Genre; +using TvCast = TMDbLib.Objects.TvShows.Cast; +using MovieCast = TMDbLib.Objects.Movies.Cast; + +namespace Kyoo.TheMovieDb +{ + public static class Convertors + { + /// + /// Convert a into a . + /// + /// The movie to convert. + /// The provider representing TheMovieDb. + /// The converted movie as a . + public static Show ToShow(this Movie movie, Provider provider) + { + return new() + { + Slug = Utility.ToSlug(movie.Title), + Title = movie.Title, + Aliases = movie.AlternativeTitles.Titles.Select(x => x.Title).ToArray(), + Overview = movie.Overview, + TrailerUrl = movie.Videos?.Results.Where(x => x.Type is "Trailer" or "Teaser" && x.Site == "YouTube") + .Select(x => "https://www.youtube.com/watch?v=" + x.Key).FirstOrDefault(), + Status = movie.Status == "Released" ? Status.Finished : Status.Planned, + StartAir = movie.ReleaseDate, + EndAir = movie.ReleaseDate, + Poster = movie.PosterPath != null + ? $"https://image.tmdb.org/t/p/original{movie.PosterPath}" + : null, + Backdrop = movie.BackdropPath != null + ? $"https://image.tmdb.org/t/p/original{movie.BackdropPath}" + : null, + Genres = movie.Genres.Select(x => new Genre(x.Name)).ToArray(), + Studio = !string.IsNullOrEmpty(movie.ProductionCompanies.FirstOrDefault()?.Name) + ? new Studio(movie.ProductionCompanies.First().Name) + : null, + IsMovie = true, + People = movie.Credits.Cast + .Select(x => x.ToPeople(provider)) + .Concat(movie.Credits.Crew.Select(x => x.ToPeople(provider))) + .ToArray(), + ExternalIDs = new [] + { + new MetadataID + { + Second = provider, + Link = $"https://www.themoviedb.org/movie/{movie.Id}", + DataID = movie.Id.ToString() + } + } + }; + } + + /// + /// Convert a to a . + /// + /// The show to convert. + /// The provider representing TheMovieDb. + /// A converted as a . + public static Show ToShow(this TvShow tv, Provider provider) + { + return new() + { + Slug = Utility.ToSlug(tv.Name), + Title = tv.Name, + Aliases = tv.AlternativeTitles.Results.Select(x => x.Title).ToArray(), + Overview = tv.Overview, + TrailerUrl = tv.Videos?.Results.Where(x => x.Type is "Trailer" or "Teaser" && x.Site == "YouTube") + .Select(x => "https://www.youtube.com/watch?v=" + x.Key).FirstOrDefault(), + Status = tv.Status == "Ended" ? Status.Finished : Status.Planned, + StartAir = tv.FirstAirDate, + EndAir = tv.LastAirDate, + Poster = tv.PosterPath != null + ? $"https://image.tmdb.org/t/p/original{tv.PosterPath}" + : null, + Backdrop = tv.BackdropPath != null + ? $"https://image.tmdb.org/t/p/original{tv.BackdropPath}" + : null, + Genres = tv.Genres.Select(x => new Genre(x.Name)).ToArray(), + Studio = !string.IsNullOrEmpty(tv.ProductionCompanies.FirstOrDefault()?.Name) + ? new Studio(tv.ProductionCompanies.First().Name) + : null, + IsMovie = true, + People = tv.Credits.Cast + .Select(x => x.ToPeople(provider)) + .Concat(tv.Credits.Crew.Select(x => x.ToPeople(provider))) + .ToArray(), + ExternalIDs = new [] + { + new MetadataID + { + Second = provider, + Link = $"https://www.themoviedb.org/movie/{tv.Id}", + DataID = tv.Id.ToString() + } + } + }; + } + + /// + /// Convert a into a . + /// + /// The movie to convert. + /// The provider representing TheMovieDb. + /// The converted movie as a . + public static Show ToShow(this SearchMovie movie, Provider provider) + { + return new() + { + Slug = Utility.ToSlug(movie.Title), + Title = movie.Title, + Overview = movie.Overview, + StartAir = movie.ReleaseDate, + EndAir = movie.ReleaseDate, + Poster = movie.PosterPath != null + ? $"https://image.tmdb.org/t/p/original{movie.PosterPath}" + : null, + Backdrop = movie.BackdropPath != null + ? $"https://image.tmdb.org/t/p/original{movie.BackdropPath}" + : null, + IsMovie = true, + ExternalIDs = new [] + { + new MetadataID + { + Second = provider, + Link = $"https://www.themoviedb.org/movie/{movie.Id}", + DataID = movie.Id.ToString() + } + } + }; + } + + /// + /// Convert a to a . + /// + /// The show to convert. + /// The provider representing TheMovieDb. + /// A converted as a . + public static Show ToShow(this SearchTv tv, Provider provider) + { + return new() + { + Slug = Utility.ToSlug(tv.Name), + Title = tv.Name, + Overview = tv.Overview, + StartAir = tv.FirstAirDate, + Poster = tv.PosterPath != null + ? $"https://image.tmdb.org/t/p/original{tv.PosterPath}" + : null, + Backdrop = tv.BackdropPath != null + ? $"https://image.tmdb.org/t/p/original{tv.BackdropPath}" + : null, + IsMovie = true, + ExternalIDs = new [] + { + new MetadataID + { + Second = provider, + Link = $"https://www.themoviedb.org/movie/{tv.Id}", + DataID = tv.Id.ToString() + } + } + }; + } + + /// + /// Convert a to a . + /// + /// An internal TheMovieDB cast. + /// The provider that represent TheMovieDB inside Kyoo. + /// A representing the movie cast. + public static PeopleRole ToPeople(this MovieCast cast, Provider provider) + { + return new() + { + People = new People + { + Slug = Utility.ToSlug(cast.Name), + Name = cast.Name, + Poster = cast.ProfilePath != null ? $"https://image.tmdb.org/t/p/original{cast.ProfilePath}" : null, + ExternalIDs = new[] + { + new MetadataID + { + Second = provider, + DataID = cast.Id.ToString(), + Link = $"https://www.themoviedb.org/person/{cast.Id}" + } + } + }, + Type = "Actor", + Role = cast.Character + }; + } + + /// + /// Convert a to a . + /// + /// An internal TheMovieDB cast. + /// The provider that represent TheMovieDB inside Kyoo. + /// A representing the movie cast. + public static PeopleRole ToPeople(this TvCast cast, Provider provider) + { + return new() + { + People = new People + { + Slug = Utility.ToSlug(cast.Name), + Name = cast.Name, + Poster = cast.ProfilePath != null ? $"https://image.tmdb.org/t/p/original{cast.ProfilePath}" : null, + ExternalIDs = new[] + { + new MetadataID + { + Second = provider, + DataID = cast.Id.ToString(), + Link = $"https://www.themoviedb.org/person/{cast.Id}" + } + } + }, + Type = "Actor", + Role = cast.Character + }; + } + + /// + /// Convert a to a . + /// + /// An internal TheMovieDB crew member. + /// The provider that represent TheMovieDB inside Kyoo. + /// A representing the movie crew. + public static PeopleRole ToPeople(this Crew crew, Provider provider) + { + return new() + { + People = new People + { + Slug = Utility.ToSlug(crew.Name), + Name = crew.Name, + Poster = crew.ProfilePath != null ? $"https://image.tmdb.org/t/p/original{crew.ProfilePath}" : null, + ExternalIDs = new[] + { + new MetadataID + { + Second = provider, + DataID = crew.Id.ToString(), + Link = $"https://www.themoviedb.org/person/{crew.Id}" + } + } + }, + Type = crew.Department, + Role = crew.Job + }; + } + } +} \ No newline at end of file diff --git a/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj b/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj new file mode 100644 index 00000000..3f50e6b3 --- /dev/null +++ b/Kyoo.TheMovieDb/Kyoo.TheMovieDb.csproj @@ -0,0 +1,34 @@ + + + net5.0 + + SDG + Zoe Roux + https://github.com/AnonymusRaccoon/Kyoo + default + Kyoo.TheMovieDb + + + + ../Kyoo/bin/$(Configuration)/$(TargetFramework)/plugins/the-moviedb + false + false + false + false + true + + + + + + + + + + + all + false + runtime + + + diff --git a/Kyoo.TheMovieDb/PluginTmdb.cs b/Kyoo.TheMovieDb/PluginTmdb.cs new file mode 100644 index 00000000..aaf5aa9c --- /dev/null +++ b/Kyoo.TheMovieDb/PluginTmdb.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using Autofac; +using Kyoo.Controllers; +using Kyoo.Models.Attributes; +using Kyoo.TheMovieDb.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.TheMovieDb +{ + /// + /// A plugin that add a for TheMovieDB. + /// + public class PluginTmdb : IPlugin + { + /// + public string Slug => "the-moviedb"; + + /// + public string Name => "TheMovieDb Provider"; + + /// + public string Description => "A metadata provider for TheMovieDB."; + + /// + public ICollection Provides => new [] + { + typeof(IMetadataProvider) + }; + + /// + public ICollection ConditionalProvides => ArraySegment.Empty; + + /// + public ICollection Requires => ArraySegment.Empty; + + + /// + /// The configuration to use. + /// + private readonly IConfiguration _configuration; + + /// + /// The configuration manager used to register typed/untyped implementations. + /// + [Injected] public IConfigurationManager ConfigurationManager { private get; set; } + + + /// + /// Create a new tmdb module instance and use the given configuration. + /// + /// The configuration to use + public PluginTmdb(IConfiguration configuration) + { + _configuration = configuration; + } + + + /// + public void Configure(ContainerBuilder builder) + { + builder.RegisterProvider(); + } + + /// + public void Configure(IServiceCollection services, ICollection availableTypes) + { + services.Configure(_configuration.GetSection(TheMovieDbOptions.Path)); + } + + /// + public void ConfigureAspNet(IApplicationBuilder app) + { + ConfigurationManager.AddTyped(TheMovieDbOptions.Path); + } + } +} \ No newline at end of file diff --git a/Kyoo.TheMovieDb/ProviderTmdb.cs b/Kyoo.TheMovieDb/ProviderTmdb.cs new file mode 100644 index 00000000..aedfeea6 --- /dev/null +++ b/Kyoo.TheMovieDb/ProviderTmdb.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Controllers; +using Kyoo.Models; +using Kyoo.TheMovieDb.Models; +using Microsoft.Extensions.Options; +using TMDbLib.Client; +using TMDbLib.Objects.Movies; +using TMDbLib.Objects.Search; +using TMDbLib.Objects.TvShows; + +namespace Kyoo.TheMovieDb +{ + /// + /// A metadata provider for TheMovieDb. + /// + public class TheMovieDbProvider : IMetadataProvider + { + /// + /// The API key used to authenticate with TheMovieDb API. + /// + private readonly IOptions _apiKey; + + /// + public Provider Provider => new() + { + Slug = "the-moviedb", + Name = "TheMovieDB", + LogoExtension = "svg", + Logo = "https://www.themoviedb.org/assets/2/v4/logos/v2/blue_short-8e7b30f73a4020692ccca9c88bafe5dcb6f8a62a4c6bc55cd9ba82bb2cd95f6c.svg" + }; + + /// + /// Create a new using the given api key. + /// + /// The api key + public TheMovieDbProvider(IOptions apiKey) + { + _apiKey = apiKey; + } + + + /// + public Task Get(T item) + where T : class, IResource + { + return item switch + { + Show show => _GetShow(show) as Task, + _ => null + }; + } + + /// + /// Get a show using it's id, if the id is not present in the show, fallback to a title search. + /// + /// The show to search for + /// A show containing metadata from TheMovieDb + private async Task _GetShow(Show show) + { + if (!int.TryParse(show.GetID(Provider.Name), out int id)) + return (await _SearchShows(show.Title ?? show.Slug)).FirstOrDefault(); + TMDbClient client = new(_apiKey.Value.ApiKey); + + if (show.IsMovie) + { + return (await client + .GetMovieAsync(id, MovieMethods.AlternativeTitles | MovieMethods.Videos | MovieMethods.Credits)) + ?.ToShow(Provider); + } + + return (await client + .GetTvShowAsync(id, TvShowMethods.AlternativeTitles | TvShowMethods.Videos | TvShowMethods.Credits)) + ?.ToShow(Provider); + } + + + /// + public async Task> Search(string query) + where T : class, IResource + { + if (typeof(T) == typeof(Show)) + return (await _SearchShows(query) as ICollection)!; + return ArraySegment.Empty; + } + + /// + /// Search for a show using it's name as a query. + /// + /// The query to search for + /// A show containing metadata from TheMovieDb + private async Task> _SearchShows(string query) + { + TMDbClient client = new(_apiKey.Value.ApiKey); + return (await client.SearchMultiAsync(query)) + .Results + .Select(x => + { + return x switch + { + SearchTv tv => tv.ToShow(Provider), + SearchMovie movie => movie.ToShow(Provider), + _ => null + }; + }) + .Where(x => x != null) + .ToArray(); + } + + // public async Task GetSeason(Show show, int seasonNumber) + // { + // string id = show?.GetID(Provider.Name); + // if (id == null) + // return await Task.FromResult(null); + // TMDbClient client = new TMDbClient(APIKey); + // TvSeason season = await client.GetTvSeasonAsync(int.Parse(id), seasonNumber); + // if (season == null) + // return null; + // return new Season(show.ID, + // seasonNumber, + // season.Name, + // season.Overview, + // season.AirDate?.Year, + // season.PosterPath != null ? "https://image.tmdb.org/t/p/original" + season.PosterPath : null, + // new[] {new MetadataID(Provider, $"{season.Id}", $"https://www.themoviedb.org/tv/{id}/season/{season.SeasonNumber}")}); + // } + // + // public async Task GetEpisode(Show show, int seasonNumber, int episodeNumber, int absoluteNumber) + // { + // if (seasonNumber == -1 || episodeNumber == -1) + // return await Task.FromResult(null); + // + // string id = show?.GetID(Provider.Name); + // if (id == null) + // return await Task.FromResult(null); + // TMDbClient client = new(APIKey); + // TvEpisode episode = await client.GetTvEpisodeAsync(int.Parse(id), seasonNumber, episodeNumber); + // if (episode == null) + // return null; + // return new Episode(seasonNumber, episodeNumber, absoluteNumber, + // episode.Name, + // episode.Overview, + // episode.AirDate, + // 0, + // episode.StillPath != null ? "https://image.tmdb.org/t/p/original" + episode.StillPath : null, + // new [] + // { + // new MetadataID(Provider, $"{episode.Id}", $"https://www.themoviedb.org/tv/{id}/season/{episode.SeasonNumber}/episode/{episode.EpisodeNumber}") + // }); + // } + } +} \ No newline at end of file diff --git a/Kyoo.TheMovieDb/TheMovieDbOptions.cs b/Kyoo.TheMovieDb/TheMovieDbOptions.cs new file mode 100644 index 00000000..2387f553 --- /dev/null +++ b/Kyoo.TheMovieDb/TheMovieDbOptions.cs @@ -0,0 +1,18 @@ +namespace Kyoo.TheMovieDb.Models +{ + /// + /// The option containing the api key for TheMovieDb. + /// + public class TheMovieDbOptions + { + /// + /// The path to get this option from the root configuration. + /// + public const string Path = "the-moviedb"; + + /// + /// The api key of TheMovieDb. + /// + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index 55aeb44c..fbec126b 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.SqLite", "Kyoo.SqLite\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheTvdb", "Kyoo.TheTvdb\Kyoo.TheTvdb.csproj", "{D06BF829-23F5-40F3-A62D-627D9F4B4D6C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.TheMovieDb", "Kyoo.TheMovieDb\Kyoo.TheMovieDb.csproj", "{BAB270D4-E0EA-4329-BA65-512FDAB01001}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,5 +55,9 @@ Global {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D06BF829-23F5-40F3-A62D-627D9F4B4D6C}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB270D4-E0EA-4329-BA65-512FDAB01001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB270D4-E0EA-4329-BA65-512FDAB01001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB270D4-E0EA-4329-BA65-512FDAB01001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB270D4-E0EA-4329-BA65-512FDAB01001}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo/settings.json b/Kyoo/settings.json index 66ff8566..8529a35a 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -70,5 +70,8 @@ "tvdb": { "apiKey": "REDACTED" + }, + "the-moviedb": { + "apiKey": "REDACTED" } }