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.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Savers; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Providers.Movies { /// /// Class MovieDbProvider /// public class MovieDbProvider : BaseMetadataProvider, IDisposable { protected static CultureInfo EnUs = new CultureInfo("en-US"); protected readonly IProviderManager ProviderManager; /// /// The movie db /// internal readonly SemaphoreSlim MovieDbResourcePool = new SemaphoreSlim(1, 1); internal static MovieDbProvider Current { get; private set; } /// /// Gets the json serializer. /// /// The json serializer. protected IJsonSerializer JsonSerializer { get; private set; } /// /// Gets the HTTP client. /// /// The HTTP client. protected IHttpClient HttpClient { get; private set; } /// /// Initializes a new instance of the class. /// /// The log manager. /// The configuration manager. /// The json serializer. /// The HTTP client. /// The provider manager. public MovieDbProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, IProviderManager providerManager) : base(logManager, configurationManager) { JsonSerializer = jsonSerializer; HttpClient = httpClient; ProviderManager = providerManager; Current = this; } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { MovieDbResourcePool.Dispose(); } } /// /// Gets the priority. /// /// The priority. public override MetadataProviderPriority Priority { get { return MetadataProviderPriority.Third; } } /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public override bool Supports(BaseItem item) { var trailer = item as Trailer; if (trailer != null) { return !trailer.IsLocalTrailer; } // Don't support local trailers return item is Movie || item is BoxSet || item is MusicVideo; } /// /// Gets a value indicating whether [requires internet]. /// /// true if [requires internet]; otherwise, false. public override bool RequiresInternet { get { return true; } } protected override bool RefreshOnVersionChange { get { return true; } } protected override string ProviderVersion { get { return "3"; } } /// /// The _TMDB settings task /// private TmdbSettingsResult _tmdbSettings; private readonly SemaphoreSlim _tmdbSettingsSemaphore = new SemaphoreSlim(1, 1); /// /// Gets the TMDB settings. /// /// Task{TmdbSettingsResult}. internal async Task GetTmdbSettings(CancellationToken cancellationToken) { if (_tmdbSettings != null) { return _tmdbSettings; } await _tmdbSettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // Check again in case it got populated while we were waiting. if (_tmdbSettings != null) { return _tmdbSettings; } using (var json = await GetMovieDbResponse(new HttpRequestOptions { Url = string.Format(TmdbConfigUrl, ApiKey), CancellationToken = cancellationToken, AcceptHeader = AcceptHeader }).ConfigureAwait(false)) { _tmdbSettings = JsonSerializer.DeserializeFromStream(json); return _tmdbSettings; } } finally { _tmdbSettingsSemaphore.Release(); } } private const string TmdbConfigUrl = "http://api.themoviedb.org/3/configuration?api_key={0}"; private const string Search3 = @"http://api.themoviedb.org/3/search/{3}?api_key={1}&query={0}&language={2}"; private const string GetMovieInfo3 = @"http://api.themoviedb.org/3/movie/{0}?api_key={1}&language={2}&append_to_response=casts,releases,images,keywords,trailers"; private const string GetBoxSetInfo3 = @"http://api.themoviedb.org/3/collection/{0}?api_key={1}&language={2}&append_to_response=images"; internal static string ApiKey = "f6bd687ffa63cd282b6ff2c6877f2669"; internal static string AcceptHeader = "application/json,image/*"; static readonly Regex[] NameMatches = new[] { new Regex(@"(?.*)\((?\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year new Regex(@"(?.*)") // last resort matches the whole string as the name }; protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { if (HasAltMeta(item) && !ConfigurationManager.Configuration.EnableTmdbUpdates) return false; // Boxsets require two passes because we need the children to be refreshed if (item is BoxSet && string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb))) { return true; } return base.NeedsRefreshInternal(item, providerInfo); } protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) { var language = ConfigurationManager.Configuration.PreferredMetadataLanguage; var path = GetDataFilePath(item, language); if (!string.IsNullOrEmpty(path)) { var fileInfo = new FileInfo(path); if (fileInfo.Exists) { return fileInfo.LastWriteTimeUtc > providerInfo.LastRefreshed; } } return base.NeedsRefreshBasedOnCompareDate(item, providerInfo); } /// /// Gets the movie data path. /// /// The app paths. /// if set to true [is box set]. /// The TMDB id. /// System.String. internal static string GetMovieDataPath(IApplicationPaths appPaths, bool isBoxSet, string tmdbId) { var dataPath = isBoxSet ? GetBoxSetsDataPath(appPaths) : GetMoviesDataPath(appPaths); return Path.Combine(dataPath, tmdbId); } internal static string GetMoviesDataPath(IApplicationPaths appPaths) { var dataPath = Path.Combine(appPaths.DataPath, "tmdb-movies"); return dataPath; } internal static string GetBoxSetsDataPath(IApplicationPaths appPaths) { var dataPath = Path.Combine(appPaths.DataPath, "tmdb-collections"); return dataPath; } /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// /// The item. /// if set to true [force]. /// The cancellation token /// Task{System.Boolean}. public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var id = item.GetProviderId(MetadataProviders.Tmdb); if (string.IsNullOrEmpty(id)) { id = item.GetProviderId(MetadataProviders.Imdb); } if (string.IsNullOrEmpty(id)) { id = await FindId(item, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(id)) { item.SetProviderId(MetadataProviders.Tmdb, id); } } if (!string.IsNullOrEmpty(id)) { cancellationToken.ThrowIfCancellationRequested(); await FetchMovieData(item, id, force, cancellationToken).ConfigureAwait(false); } SetLastRefreshed(item, DateTime.UtcNow); return true; } /// /// Determines whether [has alt meta] [the specified item]. /// /// The item. /// true if [has alt meta] [the specified item]; otherwise, false. internal static bool HasAltMeta(BaseItem item) { if (item is BoxSet) { return item.LocationType == LocationType.FileSystem && item.ResolveArgs.ContainsMetaFileByName("collection.xml"); } var path = MovieXmlSaver.GetMovieSavePath(item); if (item.LocationType == LocationType.FileSystem) { // If mixed with multiple movies in one folder, resolve args won't have the file system children return item.ResolveArgs.ContainsMetaFileByName(Path.GetFileName(path)) || File.Exists(path); } return false; } /// /// Parses the name. /// /// The name. /// Name of the just. /// The year. protected void ParseName(string name, out string justName, out int? year) { justName = null; year = null; foreach (var re in NameMatches) { Match m = re.Match(name); if (m.Success) { justName = m.Groups["name"].Value.Trim(); string y = m.Groups["year"] != null ? m.Groups["year"].Value : null; int temp; year = Int32.TryParse(y, out temp) ? temp : (int?)null; break; } } } /// /// Finds the id. /// /// The item. /// The cancellation token /// Task{System.String}. public async Task FindId(BaseItem item, CancellationToken cancellationToken) { int? yearInName; string name = item.Name; ParseName(name, out name, out yearInName); var year = item.ProductionYear ?? yearInName; Logger.Info("MovieDbProvider: Finding id for item: " + name); string language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower(); //if we are a boxset - look at our first child var boxset = item as BoxSet; if (boxset != null) { // See if any movies have a collection id already var collId = boxset.Children.Concat(boxset.GetLinkedChildren()).OfType