| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Globalization; |
| | | 4 | | using System.Linq; |
| | | 5 | | using System.Net.Http; |
| | | 6 | | using System.Threading; |
| | | 7 | | using System.Threading.Tasks; |
| | | 8 | | using Jellyfin.Data.Enums; |
| | | 9 | | using Jellyfin.Extensions; |
| | | 10 | | using MediaBrowser.Common.Net; |
| | | 11 | | using MediaBrowser.Controller.Entities; |
| | | 12 | | using MediaBrowser.Controller.Entities.Movies; |
| | | 13 | | using MediaBrowser.Controller.Library; |
| | | 14 | | using MediaBrowser.Controller.Providers; |
| | | 15 | | using MediaBrowser.Model.Entities; |
| | | 16 | | using MediaBrowser.Model.Providers; |
| | | 17 | | using TMDbLib.Objects.Find; |
| | | 18 | | using TMDbLib.Objects.Search; |
| | | 19 | | |
| | | 20 | | namespace MediaBrowser.Providers.Plugins.Tmdb.Movies |
| | | 21 | | { |
| | | 22 | | /// <summary> |
| | | 23 | | /// Movie provider powered by TMDb. |
| | | 24 | | /// </summary> |
| | | 25 | | public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder |
| | | 26 | | { |
| | | 27 | | private readonly IHttpClientFactory _httpClientFactory; |
| | | 28 | | private readonly ILibraryManager _libraryManager; |
| | | 29 | | private readonly TmdbClientManager _tmdbClientManager; |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Initializes a new instance of the <see cref="TmdbMovieProvider"/> class. |
| | | 33 | | /// </summary> |
| | | 34 | | /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> |
| | | 35 | | /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> |
| | | 36 | | /// <param name="tmdbClientManager">The <see cref="TmdbClientManager"/>.</param> |
| | | 37 | | public TmdbMovieProvider( |
| | | 38 | | ILibraryManager libraryManager, |
| | | 39 | | TmdbClientManager tmdbClientManager, |
| | | 40 | | IHttpClientFactory httpClientFactory) |
| | | 41 | | { |
| | 21 | 42 | | _libraryManager = libraryManager; |
| | 21 | 43 | | _tmdbClientManager = tmdbClientManager; |
| | 21 | 44 | | _httpClientFactory = httpClientFactory; |
| | 21 | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <inheritdoc /> |
| | 0 | 48 | | public int Order => 1; |
| | | 49 | | |
| | | 50 | | /// <inheritdoc /> |
| | 0 | 51 | | public string Name => TmdbUtils.ProviderName; |
| | | 52 | | |
| | | 53 | | /// <inheritdoc /> |
| | | 54 | | public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken canc |
| | | 55 | | { |
| | | 56 | | if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var id)) |
| | | 57 | | { |
| | | 58 | | var movie = await _tmdbClientManager |
| | | 59 | | .GetMovieAsync( |
| | | 60 | | int.Parse(id, CultureInfo.InvariantCulture), |
| | | 61 | | searchInfo.MetadataLanguage, |
| | | 62 | | TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode), |
| | | 63 | | searchInfo.MetadataCountryCode, |
| | | 64 | | cancellationToken) |
| | | 65 | | .ConfigureAwait(false); |
| | | 66 | | |
| | | 67 | | if (movie is not null) |
| | | 68 | | { |
| | | 69 | | var remoteResult = new RemoteSearchResult |
| | | 70 | | { |
| | | 71 | | Name = movie.Title ?? movie.OriginalTitle, |
| | | 72 | | SearchProviderName = Name, |
| | | 73 | | ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath), |
| | | 74 | | Overview = movie.Overview |
| | | 75 | | }; |
| | | 76 | | |
| | | 77 | | if (movie.ReleaseDate is not null) |
| | | 78 | | { |
| | | 79 | | var releaseDate = movie.ReleaseDate.Value.ToUniversalTime(); |
| | | 80 | | remoteResult.PremiereDate = releaseDate; |
| | | 81 | | remoteResult.ProductionYear = releaseDate.Year; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); |
| | | 85 | | remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId); |
| | | 86 | | |
| | | 87 | | return new[] { remoteResult }; |
| | | 88 | | } |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | IReadOnlyList<SearchMovie>? movieResults = null; |
| | | 92 | | if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out id)) |
| | | 93 | | { |
| | | 94 | | var result = await _tmdbClientManager.FindByExternalIdAsync( |
| | | 95 | | id, |
| | | 96 | | FindExternalSource.Imdb, |
| | | 97 | | TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode), |
| | | 98 | | searchInfo.MetadataCountryCode, |
| | | 99 | | cancellationToken).ConfigureAwait(false); |
| | | 100 | | movieResults = result?.MovieResults; |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | if (movieResults is null && searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out id)) |
| | | 104 | | { |
| | | 105 | | var result = await _tmdbClientManager.FindByExternalIdAsync( |
| | | 106 | | id, |
| | | 107 | | FindExternalSource.TvDb, |
| | | 108 | | TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode), |
| | | 109 | | searchInfo.MetadataCountryCode, |
| | | 110 | | cancellationToken).ConfigureAwait(false); |
| | | 111 | | movieResults = result?.MovieResults; |
| | | 112 | | } |
| | | 113 | | |
| | | 114 | | if (movieResults is null) |
| | | 115 | | { |
| | | 116 | | movieResults = await _tmdbClientManager |
| | | 117 | | .SearchMovieAsync(searchInfo.Name, searchInfo.Year ?? 0, searchInfo.MetadataLanguage, searchInfo.Met |
| | | 118 | | .ConfigureAwait(false); |
| | | 119 | | } |
| | | 120 | | |
| | | 121 | | var len = movieResults.Count; |
| | | 122 | | var remoteSearchResults = new RemoteSearchResult[len]; |
| | | 123 | | for (var i = 0; i < len; i++) |
| | | 124 | | { |
| | | 125 | | var movieResult = movieResults[i]; |
| | | 126 | | var remoteSearchResult = new RemoteSearchResult |
| | | 127 | | { |
| | | 128 | | Name = movieResult.Title ?? movieResult.OriginalTitle, |
| | | 129 | | ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath), |
| | | 130 | | Overview = movieResult.Overview, |
| | | 131 | | SearchProviderName = Name |
| | | 132 | | }; |
| | | 133 | | |
| | | 134 | | var releaseDate = movieResult.ReleaseDate?.ToUniversalTime(); |
| | | 135 | | remoteSearchResult.PremiereDate = releaseDate; |
| | | 136 | | remoteSearchResult.ProductionYear = releaseDate?.Year; |
| | | 137 | | |
| | | 138 | | remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCul |
| | | 139 | | remoteSearchResults[i] = remoteSearchResult; |
| | | 140 | | } |
| | | 141 | | |
| | | 142 | | return remoteSearchResults; |
| | | 143 | | } |
| | | 144 | | |
| | | 145 | | /// <inheritdoc /> |
| | | 146 | | public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) |
| | | 147 | | { |
| | | 148 | | var tmdbId = info.GetProviderId(MetadataProvider.Tmdb); |
| | | 149 | | var imdbId = info.GetProviderId(MetadataProvider.Imdb); |
| | | 150 | | var config = Plugin.Instance.Configuration; |
| | | 151 | | |
| | | 152 | | if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId)) |
| | | 153 | | { |
| | | 154 | | // ParseName is required here. |
| | | 155 | | // Caller provides the filename with extension stripped and NOT the parsed filename |
| | | 156 | | var parsedName = _libraryManager.ParseName(info.Name); |
| | | 157 | | var cleanedName = TmdbUtils.CleanName(parsedName.Name); |
| | | 158 | | |
| | | 159 | | var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year |
| | | 160 | | |
| | | 161 | | if (searchResults.Count > 0) |
| | | 162 | | { |
| | | 163 | | tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); |
| | | 164 | | } |
| | | 165 | | } |
| | | 166 | | |
| | | 167 | | if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) |
| | | 168 | | { |
| | | 169 | | var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Im |
| | | 170 | | if (movieResultFromImdbId?.MovieResults.Count > 0) |
| | | 171 | | { |
| | | 172 | | tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); |
| | | 173 | | } |
| | | 174 | | } |
| | | 175 | | |
| | | 176 | | if (string.IsNullOrEmpty(tmdbId)) |
| | | 177 | | { |
| | | 178 | | return new MetadataResult<Movie>(); |
| | | 179 | | } |
| | | 180 | | |
| | | 181 | | var movieResult = await _tmdbClientManager |
| | | 182 | | .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.G |
| | | 183 | | .ConfigureAwait(false); |
| | | 184 | | |
| | | 185 | | if (movieResult is null) |
| | | 186 | | { |
| | | 187 | | return new MetadataResult<Movie>(); |
| | | 188 | | } |
| | | 189 | | |
| | | 190 | | var movie = new Movie |
| | | 191 | | { |
| | | 192 | | Name = movieResult.Title ?? movieResult.OriginalTitle, |
| | | 193 | | OriginalTitle = movieResult.OriginalTitle, |
| | | 194 | | Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), |
| | | 195 | | Tagline = movieResult.Tagline, |
| | | 196 | | ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() |
| | | 197 | | }; |
| | | 198 | | var metadataResult = new MetadataResult<Movie> |
| | | 199 | | { |
| | | 200 | | HasMetadata = true, |
| | | 201 | | ResultLanguage = info.MetadataLanguage, |
| | | 202 | | Item = movie |
| | | 203 | | }; |
| | | 204 | | |
| | | 205 | | movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); |
| | | 206 | | movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); |
| | | 207 | | if (movieResult.BelongsToCollection is not null) |
| | | 208 | | { |
| | | 209 | | movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(Culture |
| | | 210 | | movie.CollectionName = movieResult.BelongsToCollection.Name; |
| | | 211 | | } |
| | | 212 | | |
| | | 213 | | movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage); |
| | | 214 | | |
| | | 215 | | if (movieResult.Releases?.Countries is not null) |
| | | 216 | | { |
| | | 217 | | var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).To |
| | | 218 | | |
| | | 219 | | var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, Stri |
| | | 220 | | |
| | | 221 | | if (ourRelease is not null) |
| | | 222 | | { |
| | | 223 | | movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification |
| | | 224 | | } |
| | | 225 | | else |
| | | 226 | | { |
| | | 227 | | var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.Ordi |
| | | 228 | | if (usRelease is not null) |
| | | 229 | | { |
| | | 230 | | movie.OfficialRating = usRelease.Certification; |
| | | 231 | | } |
| | | 232 | | } |
| | | 233 | | } |
| | | 234 | | |
| | | 235 | | movie.PremiereDate = movieResult.ReleaseDate; |
| | | 236 | | movie.ProductionYear = movieResult.ReleaseDate?.Year; |
| | | 237 | | |
| | | 238 | | if (movieResult.ProductionCompanies is not null) |
| | | 239 | | { |
| | | 240 | | movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name)); |
| | | 241 | | } |
| | | 242 | | |
| | | 243 | | var genres = movieResult.Genres; |
| | | 244 | | |
| | | 245 | | foreach (var genre in genres.Select(g => g.Name).Trimmed()) |
| | | 246 | | { |
| | | 247 | | movie.AddGenre(genre); |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | if (movieResult.Keywords?.Keywords is not null) |
| | | 251 | | { |
| | | 252 | | for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) |
| | | 253 | | { |
| | | 254 | | movie.AddTag(movieResult.Keywords.Keywords[i].Name); |
| | | 255 | | } |
| | | 256 | | } |
| | | 257 | | |
| | | 258 | | if (movieResult.Credits?.Cast is not null) |
| | | 259 | | { |
| | | 260 | | var castQuery = movieResult.Credits.Cast.AsEnumerable(); |
| | | 261 | | |
| | | 262 | | if (config.HideMissingCastMembers) |
| | | 263 | | { |
| | | 264 | | castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath)); |
| | | 265 | | } |
| | | 266 | | |
| | | 267 | | castQuery = castQuery.OrderBy(a => a.Order).Take(config.MaxCastMembers); |
| | | 268 | | |
| | | 269 | | foreach (var actor in castQuery) |
| | | 270 | | { |
| | | 271 | | if (string.IsNullOrWhiteSpace(actor.Name)) |
| | | 272 | | { |
| | | 273 | | continue; |
| | | 274 | | } |
| | | 275 | | |
| | | 276 | | var personInfo = new PersonInfo |
| | | 277 | | { |
| | | 278 | | Name = actor.Name.Trim(), |
| | | 279 | | Role = actor.Character?.Trim() ?? string.Empty, |
| | | 280 | | Type = PersonKind.Actor, |
| | | 281 | | SortOrder = actor.Order |
| | | 282 | | }; |
| | | 283 | | |
| | | 284 | | if (!string.IsNullOrWhiteSpace(actor.ProfilePath)) |
| | | 285 | | { |
| | | 286 | | personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath); |
| | | 287 | | } |
| | | 288 | | |
| | | 289 | | if (actor.Id > 0) |
| | | 290 | | { |
| | | 291 | | personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)) |
| | | 292 | | } |
| | | 293 | | |
| | | 294 | | metadataResult.AddPerson(personInfo); |
| | | 295 | | } |
| | | 296 | | } |
| | | 297 | | |
| | | 298 | | if (movieResult.Credits?.Crew is not null) |
| | | 299 | | { |
| | | 300 | | var crewQuery = movieResult.Credits.Crew |
| | | 301 | | .Select(crewMember => new |
| | | 302 | | { |
| | | 303 | | CrewMember = crewMember, |
| | | 304 | | PersonType = TmdbUtils.MapCrewToPersonType(crewMember) |
| | | 305 | | }) |
| | | 306 | | .Where(entry => |
| | | 307 | | TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || |
| | | 308 | | TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.Ordina |
| | | 309 | | |
| | | 310 | | if (config.HideMissingCrewMembers) |
| | | 311 | | { |
| | | 312 | | crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath)); |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | crewQuery = crewQuery.Take(config.MaxCrewMembers); |
| | | 316 | | |
| | | 317 | | foreach (var entry in crewQuery) |
| | | 318 | | { |
| | | 319 | | var crewMember = entry.CrewMember; |
| | | 320 | | |
| | | 321 | | if (string.IsNullOrWhiteSpace(crewMember.Name)) |
| | | 322 | | { |
| | | 323 | | continue; |
| | | 324 | | } |
| | | 325 | | |
| | | 326 | | var personInfo = new PersonInfo |
| | | 327 | | { |
| | | 328 | | Name = crewMember.Name.Trim(), |
| | | 329 | | Role = crewMember.Job?.Trim() ?? string.Empty, |
| | | 330 | | Type = entry.PersonType |
| | | 331 | | }; |
| | | 332 | | |
| | | 333 | | if (!string.IsNullOrWhiteSpace(crewMember.ProfilePath)) |
| | | 334 | | { |
| | | 335 | | personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath); |
| | | 336 | | } |
| | | 337 | | |
| | | 338 | | if (crewMember.Id > 0) |
| | | 339 | | { |
| | | 340 | | personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCult |
| | | 341 | | } |
| | | 342 | | |
| | | 343 | | metadataResult.AddPerson(personInfo); |
| | | 344 | | } |
| | | 345 | | } |
| | | 346 | | |
| | | 347 | | if (movieResult.Videos?.Results is not null) |
| | | 348 | | { |
| | | 349 | | var trailers = new List<MediaUrl>(); |
| | | 350 | | |
| | | 351 | | var sortedVideos = movieResult.Videos.Results |
| | | 352 | | .OrderByDescending(video => string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase) |
| | | 353 | | |
| | | 354 | | foreach (var video in sortedVideos) |
| | | 355 | | { |
| | | 356 | | if (!TmdbUtils.IsTrailerType(video)) |
| | | 357 | | { |
| | | 358 | | continue; |
| | | 359 | | } |
| | | 360 | | |
| | | 361 | | trailers.Add(new MediaUrl |
| | | 362 | | { |
| | | 363 | | Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.K |
| | | 364 | | Name = video.Name |
| | | 365 | | }); |
| | | 366 | | } |
| | | 367 | | |
| | | 368 | | movie.RemoteTrailers = trailers; |
| | | 369 | | } |
| | | 370 | | |
| | | 371 | | return metadataResult; |
| | | 372 | | } |
| | | 373 | | |
| | | 374 | | /// <inheritdoc /> |
| | | 375 | | public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) |
| | | 376 | | { |
| | 0 | 377 | | return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken); |
| | | 378 | | } |
| | | 379 | | } |
| | | 380 | | } |