diff --git a/Jellyfin.Plugin.Fanart/Providers/FanArtAlbumProvider.cs b/Jellyfin.Plugin.Fanart/Providers/FanArtAlbumProvider.cs new file mode 100644 index 0000000..ebb740f --- /dev/null +++ b/Jellyfin.Plugin.Fanart/Providers/FanArtAlbumProvider.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Music +{ + public class FanartAlbumProvider : IRemoteImageProvider, IHasOrder + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + + public FanartAlbumProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + } + + public string Name => ProviderName; + + public static string ProviderName => "FanArt"; + + public bool Supports(BaseItem item) + { + return item is MusicAlbum; + } + + public IEnumerable GetSupportedImages(BaseItem item) + { + return new List + { + ImageType.Primary, + ImageType.Disc + }; + } + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var album = (MusicAlbum)item; + + var list = new List(); + + var musicArtist = album.MusicArtist; + + if (musicArtist == null) + { + return list; + } + + var artistMusicBrainzId = musicArtist.GetProviderId(MetadataProviders.MusicBrainzArtist); + + if (!string.IsNullOrEmpty(artistMusicBrainzId)) + { + await FanartArtistProvider.Current.EnsureArtistJson(artistMusicBrainzId, cancellationToken).ConfigureAwait(false); + + var artistJsonPath = FanartArtistProvider.GetArtistJsonPath(_config.CommonApplicationPaths, artistMusicBrainzId); + + var musicBrainzReleaseGroupId = album.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); + + var musicBrainzId = album.GetProviderId(MetadataProviders.MusicBrainzAlbum); + + try + { + AddImages(list, artistJsonPath, musicBrainzId, musicBrainzReleaseGroupId, cancellationToken); + } + catch (FileNotFoundException) + { + + } + catch (IOException) + { + + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + /// + /// Adds the images. + /// + /// The list. + /// The path. + /// The release identifier. + /// The release group identifier. + /// The cancellation token. + private void AddImages(List list, string path, string releaseId, string releaseGroupId, CancellationToken cancellationToken) + { + var obj = _jsonSerializer.DeserializeFromFile(path); + + if (obj.albums != null) + { + var album = obj.albums.FirstOrDefault(i => string.Equals(i.release_group_id, releaseGroupId, StringComparison.OrdinalIgnoreCase)); + + if (album != null) + { + PopulateImages(list, album.albumcover, ImageType.Primary, 1000, 1000); + PopulateImages(list, album.cdart, ImageType.Disc, 1000, 1000); + } + } + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + + if (!string.IsNullOrEmpty(url)) + { + var likesString = i.likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + // After embedded provider + public int Order => 1; + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }); + } + } +} diff --git a/Jellyfin.Plugin.Fanart/Providers/FanArtArtistProvider.cs b/Jellyfin.Plugin.Fanart/Providers/FanArtArtistProvider.cs new file mode 100644 index 0000000..75b4213 --- /dev/null +++ b/Jellyfin.Plugin.Fanart/Providers/FanArtArtistProvider.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.TV; +using MediaBrowser.Providers.TV.FanArt; + +namespace MediaBrowser.Providers.Music +{ + public class FanartArtistProvider : IRemoteImageProvider, IHasOrder + { + internal const string ApiKey = "184e1a2b1fe3b94935365411f919f638"; + private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3.1/music/{1}?api_key={0}"; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _jsonSerializer; + + internal static FanartArtistProvider Current; + + public FanartArtistProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + + Current = this; + } + + public string Name => ProviderName; + + public static string ProviderName => "FanArt"; + + public bool Supports(BaseItem item) + { + return item is MusicArtist; + } + + public IEnumerable GetSupportedImages(BaseItem item) + { + return new List + { + ImageType.Primary, + ImageType.Logo, + ImageType.Art, + ImageType.Banner, + ImageType.Backdrop + }; + } + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var artist = (MusicArtist)item; + + var list = new List(); + + var artistMusicBrainzId = artist.GetProviderId(MetadataProviders.MusicBrainzArtist); + + if (!string.IsNullOrEmpty(artistMusicBrainzId)) + { + await EnsureArtistJson(artistMusicBrainzId, cancellationToken).ConfigureAwait(false); + + var artistJsonPath = GetArtistJsonPath(_config.CommonApplicationPaths, artistMusicBrainzId); + + try + { + AddImages(list, artistJsonPath, cancellationToken); + } + catch (FileNotFoundException) + { + + } + catch (IOException) + { + + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + /// + /// Adds the images. + /// + /// The list. + /// The path. + /// The cancellation token. + private void AddImages(List list, string path, CancellationToken cancellationToken) + { + var obj = _jsonSerializer.DeserializeFromFile(path); + + PopulateImages(list, obj.artistbackground, ImageType.Backdrop, 1920, 1080); + PopulateImages(list, obj.artistthumb, ImageType.Primary, 500, 281); + PopulateImages(list, obj.hdmusiclogo, ImageType.Logo, 800, 310); + PopulateImages(list, obj.musicbanner, ImageType.Banner, 1000, 185); + PopulateImages(list, obj.musiclogo, ImageType.Logo, 400, 155); + PopulateImages(list, obj.hdmusicarts, ImageType.Art, 1000, 562); + PopulateImages(list, obj.musicarts, ImageType.Art, 500, 281); + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + + if (!string.IsNullOrEmpty(url)) + { + var likesString = i.likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order => 0; + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }); + } + + internal Task EnsureArtistJson(string musicBrainzId, CancellationToken cancellationToken) + { + var jsonPath = GetArtistJsonPath(_config.ApplicationPaths, musicBrainzId); + + var fileInfo = _fileSystem.GetFileSystemInfo(jsonPath); + + if (fileInfo.Exists) + { + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + { + return Task.CompletedTask; + } + } + + return DownloadArtistJson(musicBrainzId, cancellationToken); + } + + /// + /// Downloads the artist data. + /// + /// The music brainz id. + /// The cancellation token. + /// Task{System.Boolean}. + internal async Task DownloadArtistJson(string musicBrainzId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var url = string.Format(FanArtBaseUrl, ApiKey, musicBrainzId); + + var clientKey = FanartSeriesProvider.Current.GetFanartOptions().UserApiKey; + if (!string.IsNullOrWhiteSpace(clientKey)) + { + url += "&client_key=" + clientKey; + } + + var jsonPath = GetArtistJsonPath(_config.ApplicationPaths, musicBrainzId); + + Directory.CreateDirectory(Path.GetDirectoryName(jsonPath)); + + try + { + using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = true + + }, "GET").ConfigureAwait(false)) + { + using (var response = httpResponse.Content) + { + using (var saveFileStream = _fileSystem.GetFileStream(jsonPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + { + await response.CopyToAsync(saveFileStream).ConfigureAwait(false); + } + } + } + } + catch (HttpException ex) + { + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + _jsonSerializer.SerializeToFile(new FanartArtistResponse(), jsonPath); + } + else + { + throw; + } + } + } + + /// + /// Gets the artist data path. + /// + /// The application paths. + /// The music brainz artist identifier. + /// System.String. + private static string GetArtistDataPath(IApplicationPaths appPaths, string musicBrainzArtistId) + { + var dataPath = Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId); + + return dataPath; + } + + /// + /// Gets the artist data path. + /// + /// The application paths. + /// System.String. + internal static string GetArtistDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "fanart-music"); + + return dataPath; + } + + internal static string GetArtistJsonPath(IApplicationPaths appPaths, string musicBrainzArtistId) + { + var dataPath = GetArtistDataPath(appPaths, musicBrainzArtistId); + + return Path.Combine(dataPath, "fanart.json"); + } + + + public class FanartArtistImage + { + public string id { get; set; } + public string url { get; set; } + public string likes { get; set; } + public string disc { get; set; } + public string size { get; set; } + public string lang { get; set; } + } + + public class Album + { + public string release_group_id { get; set; } + public List cdart { get; set; } + public List albumcover { get; set; } + } + + public class FanartArtistResponse + { + public string name { get; set; } + public string mbid_id { get; set; } + public List artistthumb { get; set; } + public List artistbackground { get; set; } + public List hdmusiclogo { get; set; } + public List musicbanner { get; set; } + public List musiclogo { get; set; } + public List musicarts { get; set; } + public List hdmusicarts { get; set; } + public List albums { get; set; } + } + } +} diff --git a/Jellyfin.Plugin.Fanart/Providers/FanArtSeasonProvider.cs b/Jellyfin.Plugin.Fanart/Providers/FanArtSeasonProvider.cs new file mode 100644 index 0000000..5835691 --- /dev/null +++ b/Jellyfin.Plugin.Fanart/Providers/FanArtSeasonProvider.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.TV.FanArt +{ + public class FanArtSeasonProvider : IRemoteImageProvider, IHasOrder + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _json; + + public FanArtSeasonProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _json = json; + } + + public string Name => ProviderName; + + public static string ProviderName => "FanArt"; + + public bool Supports(BaseItem item) + { + return item is Season; + } + + public IEnumerable GetSupportedImages(BaseItem item) + { + return new List + { + ImageType.Backdrop, + ImageType.Thumb, + ImageType.Banner, + ImageType.Primary + }; + } + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var list = new List(); + + var season = (Season)item; + var series = season.Series; + + if (series != null) + { + var id = series.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue) + { + // Bad id entered + try + { + await FanartSeriesProvider.Current.EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + var path = FanartSeriesProvider.Current.GetFanartJsonPath(id); + + try + { + AddImages(list, season.IndexNumber.Value, path, cancellationToken); + } + catch (FileNotFoundException) + { + // No biggie. Don't blow up + } + catch (IOException) + { + // No biggie. Don't blow up + } + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + private void AddImages(List list, int seasonNumber, string path, CancellationToken cancellationToken) + { + var root = _json.DeserializeFromFile(path); + + AddImages(list, root, seasonNumber, cancellationToken); + } + + private void AddImages(List list, FanartSeriesProvider.RootObject obj, int seasonNumber, CancellationToken cancellationToken) + { + PopulateImages(list, obj.seasonposter, ImageType.Primary, 1000, 1426, seasonNumber); + PopulateImages(list, obj.seasonbanner, ImageType.Banner, 1000, 185, seasonNumber); + PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281, seasonNumber); + PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, seasonNumber); + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height, + int seasonNumber) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + var season = i.season; + + if (!string.IsNullOrEmpty(url) && + !string.IsNullOrEmpty(season) && + int.TryParse(season, NumberStyles.Integer, _usCulture, out var imageSeasonNumber) && + seasonNumber == imageSeasonNumber) + { + var likesString = i.likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order => 1; + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }); + } + } +} diff --git a/Jellyfin.Plugin.Fanart/Providers/FanartMovieImageProvider.cs b/Jellyfin.Plugin.Fanart/Providers/FanartMovieImageProvider.cs new file mode 100644 index 0000000..70d187b --- /dev/null +++ b/Jellyfin.Plugin.Fanart/Providers/FanartMovieImageProvider.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; +using MediaBrowser.Providers.TV; +using MediaBrowser.Providers.TV.FanArt; + +namespace MediaBrowser.Providers.Movies +{ + public class FanartMovieImageProvider : IRemoteImageProvider, IHasOrder + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _json; + + private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3/movies/{1}?api_key={0}"; + + internal static FanartMovieImageProvider Current; + + public FanartMovieImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _json = json; + + Current = this; + } + + public string Name => ProviderName; + + public static string ProviderName => "FanArt"; + + public bool Supports(BaseItem item) + { + return item is Movie || item is BoxSet || item is MusicVideo; + } + + public IEnumerable GetSupportedImages(BaseItem item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb, + ImageType.Art, + ImageType.Logo, + ImageType.Disc, + ImageType.Banner, + ImageType.Backdrop + }; + } + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var baseItem = item; + var list = new List(); + + var movieId = baseItem.GetProviderId(MetadataProviders.Tmdb); + + if (!string.IsNullOrEmpty(movieId)) + { + // Bad id entered + try + { + await EnsureMovieJson(movieId, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + var path = GetFanartJsonPath(movieId); + + try + { + AddImages(list, path, cancellationToken); + } + catch (FileNotFoundException) + { + // No biggie. Don't blow up + } + catch (IOException) + { + // No biggie. Don't blow up + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0); + } + + private void AddImages(List list, string path, CancellationToken cancellationToken) + { + var root = _json.DeserializeFromFile(path); + + AddImages(list, root, cancellationToken); + } + + private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) + { + PopulateImages(list, obj.hdmovieclearart, ImageType.Art, 1000, 562); + PopulateImages(list, obj.hdmovielogo, ImageType.Logo, 800, 310); + PopulateImages(list, obj.moviedisc, ImageType.Disc, 1000, 1000); + PopulateImages(list, obj.movieposter, ImageType.Primary, 1000, 1426); + PopulateImages(list, obj.movielogo, ImageType.Logo, 400, 155); + PopulateImages(list, obj.movieart, ImageType.Art, 500, 281); + PopulateImages(list, obj.moviethumb, ImageType.Thumb, 1000, 562); + PopulateImages(list, obj.moviebanner, ImageType.Banner, 1000, 185); + PopulateImages(list, obj.moviebackground, ImageType.Backdrop, 1920, 1080); + } + + private void PopulateImages(List list, List images, ImageType type, int width, int height) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + + if (!string.IsNullOrEmpty(url)) + { + var likesString = i.likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url, + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order => 1; + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }); + } + + /// + /// Gets the movie data path. + /// + /// The application paths. + /// The identifier. + /// System.String. + internal static string GetMovieDataPath(IApplicationPaths appPaths, string id) + { + var dataPath = Path.Combine(GetMoviesDataPath(appPaths), id); + + return dataPath; + } + + /// + /// Gets the movie data path. + /// + /// The app paths. + /// System.String. + internal static string GetMoviesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "fanart-movies"); + + return dataPath; + } + + public string GetFanartJsonPath(string id) + { + var movieDataPath = GetMovieDataPath(_config.ApplicationPaths, id); + return Path.Combine(movieDataPath, "fanart.json"); + } + + /// + /// Downloads the movie json. + /// + /// The identifier. + /// The cancellation token. + /// Task. + internal async Task DownloadMovieJson(string id, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, id); + + var clientKey = FanartSeriesProvider.Current.GetFanartOptions().UserApiKey; + if (!string.IsNullOrWhiteSpace(clientKey)) + { + url += "&client_key=" + clientKey; + } + + var path = GetFanartJsonPath(id); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + try + { + using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = true + + }, "GET").ConfigureAwait(false)) + { + using (var response = httpResponse.Content) + { + using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + { + await response.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + } + } + catch (HttpException exception) + { + if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) + { + // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts + _json.SerializeToFile(new RootObject(), path); + + return; + } + + throw; + } + } + + internal Task EnsureMovieJson(string id, CancellationToken cancellationToken) + { + var path = GetFanartJsonPath(id); + + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + { + return Task.CompletedTask; + } + } + + return DownloadMovieJson(id, cancellationToken); + } + + public class Image + { + public string id { get; set; } + public string url { get; set; } + public string lang { get; set; } + public string likes { get; set; } + } + + public class RootObject + { + public string name { get; set; } + public string tmdb_id { get; set; } + public string imdb_id { get; set; } + public List hdmovielogo { get; set; } + public List moviedisc { get; set; } + public List movielogo { get; set; } + public List movieposter { get; set; } + public List hdmovieclearart { get; set; } + public List movieart { get; set; } + public List moviebackground { get; set; } + public List moviebanner { get; set; } + public List moviethumb { get; set; } + } + } +} diff --git a/Jellyfin.Plugin.Fanart/Providers/FanartSeriesProvider.cs b/Jellyfin.Plugin.Fanart/Providers/FanartSeriesProvider.cs new file mode 100644 index 0000000..49cd959 --- /dev/null +++ b/Jellyfin.Plugin.Fanart/Providers/FanartSeriesProvider.cs @@ -0,0 +1,378 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; + +namespace MediaBrowser.Providers.TV.FanArt +{ + public class FanartSeriesProvider : IRemoteImageProvider, IHasOrder + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _json; + + private const string FanArtBaseUrl = "https://webservice.fanart.tv/v3/tv/{1}?api_key={0}"; + + internal static FanartSeriesProvider Current { get; private set; } + + public FanartSeriesProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _json = json; + + Current = this; + } + + public string Name => ProviderName; + + public static string ProviderName => "FanArt"; + + public bool Supports(BaseItem item) + { + return item is Series; + } + + public IEnumerable GetSupportedImages(BaseItem item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb, + ImageType.Art, + ImageType.Logo, + ImageType.Backdrop, + ImageType.Banner + }; + } + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var list = new List(); + + var series = (Series)item; + + var id = series.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(id)) + { + // Bad id entered + try + { + await EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + var path = GetFanartJsonPath(id); + + try + { + AddImages(list, path, cancellationToken); + } + catch (FileNotFoundException) + { + // No biggie. Don't blow up + } + catch (IOException) + { + // No biggie. Don't blow up + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + private void AddImages(List list, string path, CancellationToken cancellationToken) + { + var root = _json.DeserializeFromFile(path); + + AddImages(list, root, cancellationToken); + } + + private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) + { + PopulateImages(list, obj.hdtvlogo, ImageType.Logo, 800, 310); + PopulateImages(list, obj.hdclearart, ImageType.Art, 1000, 562); + PopulateImages(list, obj.clearlogo, ImageType.Logo, 400, 155); + PopulateImages(list, obj.clearart, ImageType.Art, 500, 281); + PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, true); + PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281); + PopulateImages(list, obj.tvthumb, ImageType.Thumb, 500, 281); + PopulateImages(list, obj.tvbanner, ImageType.Banner, 1000, 185); + PopulateImages(list, obj.tvposter, ImageType.Primary, 1000, 1426); + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height, + bool allowSeasonAll = false) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + var season = i.season; + + var isSeasonValid = string.IsNullOrEmpty(season) || + (allowSeasonAll && string.Equals(season, "all", StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(url) && isSeasonValid) + { + var likesString = i.likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase), + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Integer, _usCulture, out var likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order => 1; + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }); + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// The series id. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); + + return seriesDataPath; + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "fanart-tv"); + + return dataPath; + } + + public string GetFanartJsonPath(string tvdbId) + { + var dataPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId); + return Path.Combine(dataPath, "fanart.json"); + } + + private readonly SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); + internal async Task EnsureSeriesJson(string tvdbId, CancellationToken cancellationToken) + { + var path = GetFanartJsonPath(tvdbId); + + // Only allow one thread in here at a time since every season will be calling this method, possibly concurrently + await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) + { + return; + } + } + + await DownloadSeriesJson(tvdbId, cancellationToken).ConfigureAwait(false); + } + finally + { + _ensureSemaphore.Release(); + } + } + + public FanartOptions GetFanartOptions() + { + return _config.GetConfiguration("fanart"); + } + + /// + /// Downloads the series json. + /// + /// The TVDB identifier. + /// The cancellation token. + /// Task. + internal async Task DownloadSeriesJson(string tvdbId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId); + + var clientKey = GetFanartOptions().UserApiKey; + if (!string.IsNullOrWhiteSpace(clientKey)) + { + url += "&client_key=" + clientKey; + } + + var path = GetFanartJsonPath(tvdbId); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + try + { + using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = true + + }, "GET").ConfigureAwait(false)) + { + using (var response = httpResponse.Content) + { + using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + { + await response.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + } + } + catch (HttpException exception) + { + if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) + { + // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts + _json.SerializeToFile(new RootObject(), path); + + return; + } + + throw; + } + } + + public class Image + { + public string id { get; set; } + public string url { get; set; } + public string lang { get; set; } + public string likes { get; set; } + public string season { get; set; } + } + + public class RootObject + { + public string name { get; set; } + public string thetvdb_id { get; set; } + public List clearlogo { get; set; } + public List hdtvlogo { get; set; } + public List clearart { get; set; } + public List showbackground { get; set; } + public List tvthumb { get; set; } + public List seasonposter { get; set; } + public List seasonthumb { get; set; } + public List hdclearart { get; set; } + public List tvbanner { get; set; } + public List characterart { get; set; } + public List tvposter { get; set; } + public List seasonbanner { get; set; } + } + } + + public class FanartConfigStore : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new ConfigurationStore[] + { + new ConfigurationStore + { + Key = "fanart", + ConfigurationType = typeof(FanartOptions) + } + }; + } + } +}