using Jellyfin.Plugin.MetaShark.Api; 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.AspNetCore.Http; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using TMDbLib.Objects.TvShows; using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; namespace Jellyfin.Plugin.MetaShark.Providers { public class SeriesProvider : BaseProvider, IRemoteMetadataProvider { public SeriesProvider(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, ILibraryManager libraryManager, IHttpContextAccessor httpContextAccessor, DoubanApi doubanApi, TmdbApi tmdbApi, OmdbApi omdbApi, ImdbApi imdbApi) : base(httpClientFactory, loggerFactory.CreateLogger(), libraryManager, httpContextAccessor, doubanApi, tmdbApi, omdbApi, imdbApi) { } public string Name => Plugin.PluginName; /// public async Task> GetSearchResults(SeriesInfo info, CancellationToken cancellationToken) { this.Log($"GetSearchResults of [name]: {info.Name}"); var result = new List(); if (string.IsNullOrEmpty(info.Name)) { return result; } // 从douban搜索 var res = await this._doubanApi.SearchTVAsync(info.Name, cancellationToken).ConfigureAwait(false); result.AddRange(res.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x => { return new RemoteSearchResult { // 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一 ProviderIds = new Dictionary { { DoubanProviderId, x.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{x.Sid}" } }, ImageUrl = this.GetProxyImageUrl(x.Img), ProductionYear = x.Year, Name = x.Name, }; })); // 尝试从tmdb搜索 if (this.config.EnableTmdbSearch) { var tmdbList = await this._tmdbApi.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); result.AddRange(tmdbList.Take(Configuration.PluginConfiguration.MAX_SEARCH_RESULT).Select(x => { return new RemoteSearchResult { // 这里 Plugin.ProviderId 的值做这么复杂,是为了和电影保持一致并唯一 ProviderIds = new Dictionary { { MetadataProvider.Tmdb.ToString(), x.Id.ToString(CultureInfo.InvariantCulture) }, { Plugin.ProviderId, $"{MetaSource.Tmdb}_{x.Id}" } }, Name = string.Format("[TMDB]{0}", x.Name ?? x.OriginalName), ImageUrl = this._tmdbApi.GetPosterUrl(x.PosterPath), Overview = x.Overview, ProductionYear = x.FirstAirDate?.Year, }; })); } return result; } /// public async Task> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) { var fileName = this.GetOriginalFileName(info); this.Log($"GetSeriesMetadata of [name]: {info.Name} [fileName]: {fileName} IsAutomated: {info.IsAutomated}"); var result = new MetadataResult(); var sid = info.GetProviderId(DoubanProviderId); var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); var metaSource = info.GetMetaSource(Plugin.ProviderId); // 注意:会存在元数据有tmdbId,但metaSource没值的情况(之前由TMDB插件刮削导致) var hasTmdbMeta = metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId); var hasDoubanMeta = metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid); if (!hasDoubanMeta && !hasTmdbMeta) { // 自动扫描搜索匹配元数据 sid = await this.GuessByDoubanAsync(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 seriesName = RemoveSeasonSuffix(subject.Name); var item = new Series { ProviderIds = new Dictionary { { DoubanProviderId, subject.Sid }, { Plugin.ProviderId, $"{MetaSource.Douban}_{subject.Sid}" } }, Name = seriesName, OriginalTitle = RemoveSeasonSuffix(subject.OriginalName), CommunityRating = subject.Rating, Overview = subject.Intro, ProductionYear = subject.Year, HomePageUrl = "https://www.douban.com", Genres = subject.Genres, PremiereDate = subject.ScreenTime, Tagline = string.Empty, }; // 设置imdb元数据 if (!string.IsNullOrEmpty(subject.Imdb)) { var newImdbId = await this.CheckNewImdbID(subject.Imdb, cancellationToken).ConfigureAwait(false); subject.Imdb = newImdbId; item.SetProviderId(MetadataProvider.Imdb, newImdbId); } // 搜索匹配tmdbId var newTmdbId = await this.FindTmdbId(seriesName, subject.Imdb, subject.Year, info, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(newTmdbId)) { tmdbId = newTmdbId; item.SetProviderId(MetadataProvider.Tmdb, tmdbId); } // 通过imdb获取电影分级信息 if (this.config.EnableTmdbOfficialRating && !string.IsNullOrEmpty(tmdbId)) { var officialRating = await this.GetTmdbOfficialRating(info, tmdbId, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(officialRating)) { item.OfficialRating = officialRating; } } result.Item = item; result.QueriedById = true; result.HasMetadata = true; subject.LimitDirectorCelebrities.Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS).ToList().ForEach(c => result.AddPerson(new PersonInfo { Name = c.Name, Type = c.RoleType, Role = c.Role, ImageUrl = this.GetLocalProxyImageUrl(c.Img), ProviderIds = new Dictionary { { DoubanProviderId, c.Id } }, })); return result; } if (metaSource == MetaSource.Tmdb && !string.IsNullOrEmpty(tmdbId)) { return await this.GetMetadataByTmdb(tmdbId, info, cancellationToken).ConfigureAwait(false); } this.Log($"匹配失败!可检查下年份是否与豆瓣一致,是否需要登录访问. [name]: {info.Name} [year]: {info.Year}"); return result; } private async Task> GetMetadataByTmdb(string? tmdbId, ItemLookupInfo info, CancellationToken cancellationToken) { var result = new MetadataResult(); if (string.IsNullOrEmpty(tmdbId)) { return result; } 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 { 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; } private async Task FindTmdbId(string name, string imdb, int? year, ItemLookupInfo info, CancellationToken cancellationToken) { // 通过imdb获取TMDB id if (!string.IsNullOrEmpty(imdb)) { var tmdbId = await this.GetTmdbIdByImdbAsync(imdb, info.MetadataLanguage, info, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(tmdbId)) { return tmdbId; } else { this.Log($"Can not found tmdb [id] by imdb id: \"{imdb}\""); } } // 尝试通过搜索匹配获取tmdbId if (!string.IsNullOrEmpty(name) && year != null && year > 0) { var tmdbId = await this.GuestByTmdbAsync(name, year, info, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(tmdbId)) { return tmdbId; } else { this.Log($"Can not found tmdb [id] by name: \"{name}\" and year: \"{year}\""); } } return null; } private async Task GetTmdbOfficialRating(ItemLookupInfo info, string tmdbId, CancellationToken cancellationToken) { var tvShow = await _tmdbApi .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, info.MetadataLanguage, cancellationToken) .ConfigureAwait(false); return this.GetTmdbOfficialRatingByData(tvShow, info.MetadataCountryCode); } private String GetTmdbOfficialRatingByData(TvShow? tvShow, string preferredCountryCode) { if (tvShow != null) { var contentRatings = tvShow.ContentRatings.Results ?? new List(); 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) { return ourRelease.Rating; } else if (usRelease != null) { return usRelease.Rating; } else if (minimumRelease != null) { return minimumRelease.Rating; } } return null; } 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 = (float)System.Math.Round(seriesResult.VoteAverage, 2); 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}_{seriesResult.Id}"); series.OfficialRating = this.GetTmdbOfficialRatingByData(seriesResult, preferredCountryCode); return series; } private IEnumerable GetPersons(TvShow seriesResult) { // 演员 if (seriesResult.Credits?.Cast != null) { foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Configuration.PluginConfiguration.MAX_CAST_MEMBERS)) { 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; } } } } }