Esempio n. 1
0
        public override async Task CollectFanArtAsync(Guid mediaItemId, IDictionary <Guid, IList <MediaItemAspect> > aspects)
        {
            IResourceLocator mediaItemLocator = null;

            if (!BaseInfo.IsVirtualResource(aspects))
            {
                mediaItemLocator = GetResourceLocator(aspects);
            }

            if (!aspects.ContainsKey(EpisodeAspect.ASPECT_ID) || mediaItemLocator == null)
            {
                return;
            }

            IFanArtCache fanArtCache = ServiceRegistration.Get <IFanArtCache>();

            using (IResourceAccessor mediaItemAccessor = mediaItemLocator.CreateAccessor())
            {
                EpisodeInfo episodeInfo = new EpisodeInfo();
                if (!episodeInfo.FromMetadata(aspects))
                {
                    return;
                }

                //Episode fanart
                if (AddToCache(mediaItemId))
                {
                    var existingThumbs = fanArtCache.GetFanArtFiles(mediaItemId, FanArtTypes.Thumbnail);
                    int?season         = episodeInfo.SeasonNumber;
                    int?episode        = episodeInfo.EpisodeNumbers != null && episodeInfo.EpisodeNumbers.Any() ? episodeInfo.EpisodeNumbers.First() : (int?)null;
                    if (!existingThumbs.Any()) //Only get thumb if needed for better performance
                    {
                        NfoSeriesEpisodeReader episodeReader = await SERIES_EXTRACTOR.TryGetNfoSeriesEpisodeReaderAsync(mediaItemAccessor, season, episode, true).ConfigureAwait(false);

                        if (episodeReader != null)
                        {
                            var stubs    = episodeReader.GetEpisodeStubs();
                            var mainStub = stubs?.FirstOrDefault();
                            if (mainStub?.Thumb != null)
                            {
                                await fanArtCache.TrySaveFanArt(mediaItemId, episodeInfo.ToString(), FanArtTypes.Thumbnail, p => TrySaveFileImage(mainStub.Thumb, p, "Thumb", "Nfo.")).ConfigureAwait(false);
                            }
                        }
                    }
                }

                //Series fanart
                if (RelationshipExtractorUtils.TryGetLinkedId(SeriesAspect.ROLE_SERIES, aspects, out Guid seriesMediaItemId))
                {
                    IList <Tuple <Guid, string> > actors = GetActors(aspects);
                    RelationshipExtractorUtils.TryGetLinkedId(SeasonAspect.ROLE_SEASON, aspects, out Guid seasonMediaItemId);

                    //Check if loading nfo is needed
                    if ((actors?.All(a => IsInCache(a.Item1)) ?? true) && IsInCache(seriesMediaItemId) && (seasonMediaItemId == Guid.Empty || IsInCache(seasonMediaItemId)))
                    {
                        return; //Everything was already saved
                    }
                    NfoSeriesReader seriesNfoReader = await SERIES_EXTRACTOR.TryGetNfoSeriesReaderAsync(mediaItemAccessor, true).ConfigureAwait(false);

                    if (seriesNfoReader != null)
                    {
                        var stubs    = seriesNfoReader.GetSeriesStubs();
                        var mainStub = stubs?.FirstOrDefault();
                        if (AddToCache(seriesMediaItemId))
                        {
                            var series = episodeInfo.CloneBasicInstance <SeriesInfo>();
                            if (mainStub?.Thumbs?.Count > 0)
                            {
                                await TrySaveThumbStubs(fanArtCache, mainStub.Thumbs, null, seriesMediaItemId, series.ToString());
                            }
                        }

                        if (seasonMediaItemId != Guid.Empty && episodeInfo.SeasonNumber.HasValue && AddToCache(seasonMediaItemId))
                        {
                            var season = episodeInfo.CloneBasicInstance <SeasonInfo>();
                            if (mainStub?.Thumbs?.Count > 0)
                            {
                                await TrySaveThumbStubs(fanArtCache, mainStub.Thumbs, episodeInfo.SeasonNumber, seasonMediaItemId, season.ToString());
                            }
                        }


                        //Actor fanart
                        //We only want the series actors because thumb loading is disabled on episode actors for performance reasons, so we might need to
                        //load the series nfo multiple time before we have all actors depending on what actors are in the episode
                        foreach (var actor in actors)
                        {
                            if (!IsInCache(actor.Item1))
                            {
                                var existingThumbs = fanArtCache.GetFanArtFiles(actor.Item1, FanArtTypes.Thumbnail);
                                var actorStub      = mainStub?.Actors?.FirstOrDefault(a => string.Equals(a?.Name, actor.Item2, StringComparison.InvariantCultureIgnoreCase));
                                if (actorStub != null || existingThumbs.Any()) //We have a thumb already or no thumb is available, so no need to check again
                                {
                                    AddToCache(actor.Item1);
                                }

                                if (actorStub?.Thumb != null)
                                {
                                    await fanArtCache.TrySaveFanArt(actor.Item1, actor.Item2, FanArtTypes.Thumbnail, p => TrySaveFileImage(actorStub.Thumb, p, "Thumb", "Nfo.")).ConfigureAwait(false);
                                }
                            }
                        }
                    }
                }
            }
        }
        /// <summary>
        /// Asynchronously tries to extract episode metadata for the given <param name="mediaItemAccessor"></param>
        /// </summary>
        /// <param name="mediaItemAccessor">Points to the resource for which we try to extract metadata</param>
        /// <param name="extractedAspectData">Dictionary of <see cref="MediaItemAspect"/>s with the extracted metadata</param>
        /// <param name="forceQuickMode">If <c>true</c>, nothing is downloaded from the internet</param>
        /// <returns><c>true</c> if metadata was found and stored into <param name="extractedAspectData"></param>, else <c>false</c></returns>
        private async Task <bool> TryExtractEpsiodeMetadataAsync(IResourceAccessor mediaItemAccessor, IDictionary <Guid, IList <MediaItemAspect> > extractedAspectData, bool forceQuickMode)
        {
            // Get a unique number for this call to TryExtractMetadataAsync. We use this to make reading the debug log easier.
            // This MetadataExtractor is called in parallel for multiple MediaItems so that the respective debug log entries
            // for one call are not contained one after another in debug log. We therefore prepend this number before every log entry.
            var  miNumber = Interlocked.Increment(ref _lastMediaItemNumber);
            bool isStub   = extractedAspectData.ContainsKey(StubAspect.ASPECT_ID);

            try
            {
                _debugLogger.Info("[#{0}]: Start extracting metadata for resource '{1}' (forceQuickMode: {2})", miNumber, mediaItemAccessor, forceQuickMode);

                // This MetadataExtractor only works for MediaItems accessible by an IFileSystemResourceAccessor.
                // Otherwise it is not possible to find a nfo-file in the MediaItem's directory or parent directory.
                if (!(mediaItemAccessor is IFileSystemResourceAccessor))
                {
                    _debugLogger.Info("[#{0}]: Cannot extract metadata; mediaItemAccessor is not an IFileSystemResourceAccessor", miNumber);
                    return(false);
                }

                // We only extract metadata with this MetadataExtractor, if another MetadataExtractor that was applied before
                // has identified this MediaItem as a video and therefore added a VideoAspect.
                if (!extractedAspectData.ContainsKey(VideoAspect.ASPECT_ID))
                {
                    _debugLogger.Info("[#{0}]: Cannot extract metadata; this resource is not a video", miNumber);
                    return(false);
                }

                // Here we try to find an IFileSystemResourceAccessor pointing to the episode nfo-file.
                // If we don't find one, we cannot extract any metadata.
                IFileSystemResourceAccessor episodeNfoFsra;
                NfoSeriesEpisodeReader      episodeNfoReader = null;
                bool episodeDetailsFound = false;
                if (TryGetEpisodeNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out episodeNfoFsra))
                {
                    episodeDetailsFound = true;
                    // Now we (asynchronously) extract the metadata into a stub object.
                    // If no metadata was found, nothing can be stored in the MediaItemAspects.
                    episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, forceQuickMode, isStub, _httpClient, _settings);
                    using (episodeNfoFsra)
                    {
                        if (!await episodeNfoReader.TryReadMetadataAsync(episodeNfoFsra).ConfigureAwait(false))
                        {
                            _debugLogger.Warn("[#{0}]: No valid metadata found in episode nfo-file", miNumber);
                            return(false);
                        }
                    }
                }

                // Then we try to find an IFileSystemResourceAccessor pointing to the series nfo-file.
                IFileSystemResourceAccessor seriesNfoFsra;
                if (TryGetSeriesNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out seriesNfoFsra))
                {
                    // If we found one, we (asynchronously) extract the metadata into a stub object and, if metadata was found,
                    // we store it into the episodeNfoReader so that the latter can store metadata from series and episode level into the MediaItemAspects.
                    var seriesNfoReader = new NfoSeriesReader(_debugLogger, miNumber, forceQuickMode, !episodeDetailsFound, isStub, _httpClient, _settings);
                    using (seriesNfoFsra)
                    {
                        if (await seriesNfoReader.TryReadMetadataAsync(seriesNfoFsra).ConfigureAwait(false))
                        {
                            //Check reimport
                            if (extractedAspectData.ContainsKey(ReimportAspect.ASPECT_ID))
                            {
                                SeriesInfo reimport = new SeriesInfo();
                                reimport.FromMetadata(extractedAspectData);
                                if (!VerifySeriesReimport(seriesNfoReader, reimport))
                                {
                                    ServiceRegistration.Get <ILogger>().Info("NfoSeriesMetadataExtractor: Nfo series metadata from resource '{0}' ignored because it does not match reimport {1}", mediaItemAccessor, reimport);
                                    return(false);
                                }
                            }

                            Stubs.SeriesStub series = seriesNfoReader.GetSeriesStubs().FirstOrDefault();

                            // Check if episode should be found
                            if (isStub || !episodeDetailsFound)
                            {
                                if (series != null && series.Episodes?.Count > 0)
                                {
                                    List <Stubs.SeriesEpisodeStub> episodeStubs = null;
                                    if (extractedAspectData.ContainsKey(EpisodeAspect.ASPECT_ID))
                                    {
                                        int?        seasonNo = 0;
                                        IEnumerable episodes;
                                        if (MediaItemAspect.TryGetAttribute(extractedAspectData, EpisodeAspect.ATTR_SEASON, out seasonNo) && MediaItemAspect.TryGetAttribute(extractedAspectData, EpisodeAspect.ATTR_EPISODE, out episodes))
                                        {
                                            List <int> episodeNos = new List <int>();
                                            CollectionUtils.AddAll(episodeNos, episodes.Cast <int>());

                                            if (seasonNo.HasValue && episodeNos.Count > 0)
                                            {
                                                episodeStubs = series.Episodes.Where(e => e.Season == seasonNo.Value && episodeNos.Intersect(e.Episodes).Any()).ToList();
                                            }
                                        }
                                    }
                                    else
                                    {
                                        string title = null;
                                        if (MediaItemAspect.TryGetAttribute(extractedAspectData, MediaAspect.ATTR_TITLE, out title))
                                        {
                                            Regex regex = new Regex(@"(?<series>[^\\]+).S(?<seasonnum>\d+)[\s|\.|\-|_]{0,1}E((?<episodenum>\d+)_?)+(?<episode>.*)");
                                            Match match = regex.Match(title);

                                            if (match.Success && match.Groups["seasonnum"].Length > 0 && match.Groups["episodenum"].Length > 0)
                                            {
                                                episodeStubs = series.Episodes.Where(e => e.Season == Convert.ToInt32(match.Groups["seasonnum"].Value) && e.Episodes.Contains(Convert.ToInt32(match.Groups["episodenum"].Value))).ToList();
                                            }
                                        }
                                    }
                                    if (episodeStubs != null && episodeStubs.Count > 0)
                                    {
                                        Stubs.SeriesEpisodeStub mergedEpisode = null;
                                        if (isStub)
                                        {
                                            if (episodeStubs.Count == 1)
                                            {
                                                mergedEpisode = episodeStubs.First();
                                            }
                                            else
                                            {
                                                Stubs.SeriesEpisodeStub episode = episodeStubs.First();
                                                mergedEpisode                      = new Stubs.SeriesEpisodeStub();
                                                mergedEpisode.Actors               = episode.Actors;
                                                mergedEpisode.Aired                = episode.Aired;
                                                mergedEpisode.Credits              = episode.Credits;
                                                mergedEpisode.Director             = episode.Director;
                                                mergedEpisode.DisplayEpisode       = episode.DisplayEpisode;
                                                mergedEpisode.DisplaySeason        = episode.DisplaySeason;
                                                mergedEpisode.EpBookmark           = episode.EpBookmark;
                                                mergedEpisode.FileInfo             = episode.FileInfo;
                                                mergedEpisode.LastPlayed           = episode.LastPlayed;
                                                mergedEpisode.Mpaa                 = episode.Mpaa;
                                                mergedEpisode.PlayCount            = episode.PlayCount;
                                                mergedEpisode.Premiered            = episode.Premiered;
                                                mergedEpisode.ProductionCodeNumber = episode.ProductionCodeNumber;
                                                mergedEpisode.ResumePosition       = episode.ResumePosition;
                                                mergedEpisode.Season               = episode.Season;
                                                mergedEpisode.Sets                 = episode.Sets;
                                                mergedEpisode.ShowTitle            = episode.ShowTitle;
                                                mergedEpisode.Status               = episode.Status;
                                                mergedEpisode.Studio               = episode.Studio;
                                                mergedEpisode.Tagline              = episode.Tagline;
                                                mergedEpisode.Thumb                = episode.Thumb;
                                                mergedEpisode.Top250               = episode.Top250;
                                                mergedEpisode.Trailer              = episode.Trailer;
                                                mergedEpisode.Watched              = episode.Watched;
                                                mergedEpisode.Year                 = episode.Year;
                                                mergedEpisode.Id                   = episode.Id;
                                                mergedEpisode.UniqueId             = episode.UniqueId;

                                                //Merge episodes
                                                mergedEpisode.Title   = string.Join("; ", episodeStubs.OrderBy(e => e.Episodes.First()).Select(e => e.Title).ToArray());
                                                mergedEpisode.Rating  = episodeStubs.Where(e => e.Rating.HasValue).Sum(e => e.Rating.Value) / episodeStubs.Where(e => e.Rating.HasValue).Count(); // Average rating
                                                mergedEpisode.Votes   = episodeStubs.Where(e => e.Votes.HasValue).Sum(e => e.Votes.Value) / episodeStubs.Where(e => e.Votes.HasValue).Count();
                                                mergedEpisode.Runtime = TimeSpan.FromSeconds(episodeStubs.Where(e => e.Runtime.HasValue).Sum(e => e.Runtime.Value.TotalSeconds));
                                                mergedEpisode.Plot    = string.Join("\r\n\r\n", episodeStubs.OrderBy(e => e.Episodes.First()).
                                                                                    Select(e => string.Format("{0,02}) {1}", e.Episodes.First(), e.Plot)).ToArray());
                                                mergedEpisode.Outline = string.Join("\r\n\r\n", episodeStubs.OrderBy(e => e.Episodes.First()).
                                                                                    Select(e => string.Format("{0,02}) {1}", e.Episodes.First(), e.Outline)).ToArray());
                                                mergedEpisode.Episodes    = new HashSet <int>(episodeStubs.SelectMany(x => x.Episodes).ToList());
                                                mergedEpisode.DvdEpisodes = new HashSet <decimal>(episodeStubs.SelectMany(x => x.DvdEpisodes).ToList());
                                            }

                                            IList <MultipleMediaItemAspect> providerResourceAspects;
                                            if (MediaItemAspect.TryGetAspects(extractedAspectData, ProviderResourceAspect.Metadata, out providerResourceAspects))
                                            {
                                                MultipleMediaItemAspect providerResourceAspect = providerResourceAspects.First(pa => pa.GetAttributeValue <int>(ProviderResourceAspect.ATTR_TYPE) == ProviderResourceAspect.TYPE_STUB);
                                                string mime = null;
                                                if (mergedEpisode.FileInfo != null && mergedEpisode.FileInfo.Count > 0)
                                                {
                                                    mime = MimeTypeDetector.GetMimeTypeFromExtension("file" + mergedEpisode.FileInfo.First().Container);
                                                }
                                                if (mime != null)
                                                {
                                                    providerResourceAspect.SetAttribute(ProviderResourceAspect.ATTR_MIME_TYPE, mime);
                                                }
                                            }

                                            MediaItemAspect.SetAttribute(extractedAspectData, MediaAspect.ATTR_TITLE, string.Format("{0} S{1:00}{2} {3}", series.ShowTitle, mergedEpisode.Season, mergedEpisode.Episodes.Select(e => "E" + e.ToString("00")), mergedEpisode.Title));
                                            MediaItemAspect.SetAttribute(extractedAspectData, MediaAspect.ATTR_SORT_TITLE, BaseInfo.GetSortTitle(mergedEpisode.Title));
                                            MediaItemAspect.SetAttribute(extractedAspectData, MediaAspect.ATTR_RECORDINGTIME, mergedEpisode.Premiered.HasValue ? mergedEpisode.Premiered.Value : mergedEpisode.Year.HasValue ? mergedEpisode.Year.Value : (DateTime?)null);

                                            if (mergedEpisode.FileInfo != null && mergedEpisode.FileInfo.Count > 0)
                                            {
                                                extractedAspectData.Remove(VideoStreamAspect.ASPECT_ID);
                                                extractedAspectData.Remove(VideoAudioStreamAspect.ASPECT_ID);
                                                extractedAspectData.Remove(SubtitleAspect.ASPECT_ID);
                                                StubParser.ParseFileInfo(extractedAspectData, mergedEpisode.FileInfo, mergedEpisode.Title);
                                            }
                                        }

                                        episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, forceQuickMode, isStub, _httpClient, _settings);
                                        episodeNfoReader.SetEpisodeStubs(new List <Stubs.SeriesEpisodeStub> {
                                            mergedEpisode
                                        });
                                    }
                                }
                            }
                            if (series != null)
                            {
                                if (episodeNfoReader != null)
                                {
                                    episodeNfoReader.SetSeriesStubs(new List <Stubs.SeriesStub> {
                                        series
                                    });

                                    // Then we store the found metadata in the MediaItemAspects. If we only found metadata that is
                                    // not (yet) supported by our MediaItemAspects, this MetadataExtractor returns false.
                                    if (!episodeNfoReader.TryWriteMetadata(extractedAspectData))
                                    {
                                        _debugLogger.Warn("[#{0}]: No metadata was written into MediaItemsAspects", miNumber);
                                        return(false);
                                    }
                                }
                                else
                                {
                                    EpisodeInfo episode = new EpisodeInfo();
                                    if (series.Id.HasValue)
                                    {
                                        episode.SeriesTvdbId = series.Id.Value;
                                    }
                                    if (series.Premiered.HasValue)
                                    {
                                        episode.SeriesFirstAired = series.Premiered.Value;
                                    }
                                    episode.SeriesName = series.ShowTitle;
                                    episode.SetMetadata(extractedAspectData);
                                }
                            }
                        }
                        else
                        {
                            _debugLogger.Warn("[#{0}]: No valid metadata found in series nfo-file", miNumber);
                        }
                    }
                }
                else if (episodeNfoReader != null)
                {
                    //Check reimport
                    if (extractedAspectData.ContainsKey(ReimportAspect.ASPECT_ID))
                    {
                        EpisodeInfo reimport = new EpisodeInfo();
                        reimport.FromMetadata(extractedAspectData);
                        if (!VerifyEpisodeReimport(episodeNfoReader, reimport))
                        {
                            ServiceRegistration.Get <ILogger>().Info("NfoSeriesMetadataExtractor: Nfo episode metadata from resource '{0}' ignored because it does not match reimport {1}", mediaItemAccessor, reimport);
                            return(false);
                        }
                    }

                    // Then we store the found metadata in the MediaItemAspects. If we only found metadata that is
                    // not (yet) supported by our MediaItemAspects, this MetadataExtractor returns false.
                    if (!episodeNfoReader.TryWriteMetadata(extractedAspectData))
                    {
                        _debugLogger.Warn("[#{0}]: No metadata was written into MediaItemsAspects", miNumber);
                        return(false);
                    }
                }

                _debugLogger.Info("[#{0}]: Successfully finished extracting metadata", miNumber);
                ServiceRegistration.Get <ILogger>().Debug("NfoSeriesMetadataExtractor: Assigned nfo episode metadata for resource '{0}'", mediaItemAccessor);
                return(true);
            }
            catch (Exception e)
            {
                ServiceRegistration.Get <ILogger>().Warn("NfoSeriesMetadataExtractor: Exception while extracting metadata for resource '{0}'; enable debug logging for more details.", mediaItemAccessor);
                _debugLogger.Error("[#{0}]: Exception while extracting metadata", e, miNumber);
                return(false);
            }
        }
    /// <summary>
    /// Asynchronously tries to extract metadata for the given <param name="mediaItemAccessor"></param>
    /// </summary>
    /// <param name="mediaItemAccessor">Points to the resource for which we try to extract metadata</param>
    /// <param name="extractedAspectData">Dictionary of <see cref="MediaItemAspect"/>s with the extracted metadata</param>
    /// <param name="forceQuickMode">If <c>true</c>, nothing is downloaded from the internet</param>
    /// <returns><c>true</c> if metadata was found and stored into <param name="extractedAspectData"></param>, else <c>false</c></returns>
    private async Task<bool> TryExtractMetadataAsync(IResourceAccessor mediaItemAccessor, IDictionary<Guid, MediaItemAspect> extractedAspectData, bool forceQuickMode)
    {
      // Get a unique number for this call to TryExtractMetadataAsync. We use this to make reading the debug log easier.
      // This MetadataExtractor is called in parallel for multiple MediaItems so that the respective debug log entries
      // for one call are not contained one after another in debug log. We therefore prepend this number before every log entry.
      var miNumber = Interlocked.Increment(ref _lastMediaItemNumber);
      try
      {
        _debugLogger.Info("[#{0}]: Start extracting metadata for resource '{1}' (forceQuickMode: {2})", miNumber, mediaItemAccessor, forceQuickMode);

        // We only extract metadata with this MetadataExtractor, if another MetadataExtractor that was applied before
        // has identified this MediaItem as a video and therefore added a VideoAspect.
        if (!extractedAspectData.ContainsKey(VideoAspect.ASPECT_ID))
        {
          _debugLogger.Info("[#{0}]: Cannot extract metadata; this resource is not a video", miNumber);
          return false;
        }

        // This MetadataExtractor only works for MediaItems accessible by an IFileSystemResourceAccessor.
        // Otherwise it is not possible to find a nfo-file in the MediaItem's directory or parent directory.
        if (!(mediaItemAccessor is IFileSystemResourceAccessor))
        {
          _debugLogger.Info("[#{0}]: Cannot extract metadata; mediaItemAccessor is not an IFileSystemResourceAccessor", miNumber);
          return false;
        }

        // Here we try to find an IFileSystemResourceAccessor pointing to the episode nfo-file.
        // If we don't find one, we cannot extract any metadata.
        IFileSystemResourceAccessor episodeNfoFsra;
        if (!TryGetEpisodeNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out episodeNfoFsra))
          return false;

        // Now we (asynchronously) extract the metadata into a stub object.
        // If no metadata was found, nothing can be stored in the MediaItemAspects.
        var episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, forceQuickMode, _httpClient, _settings);
        using (episodeNfoFsra)
        {
          if (!await episodeNfoReader.TryReadMetadataAsync(episodeNfoFsra).ConfigureAwait(false))
          {
            _debugLogger.Warn("[#{0}]: No valid metadata found in episode nfo-file", miNumber);
            return false;
          }
        }

        // Then we try to find an IFileSystemResourceAccessor pointing to the series nfo-file.
        IFileSystemResourceAccessor seriesNfoFsra;
        if (TryGetSeriesNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out seriesNfoFsra))
        {
          // If we found one, we (asynchronously) extract the metadata into a stub object and, if metadata was found,
          // we store it into the episodeNfoReader so that the latter can store metadata from series and episode level into the MediaItemAspects.
          var seriesNfoReader = new NfoSeriesReader(_debugLogger, miNumber, forceQuickMode, _httpClient, _settings);
          using (seriesNfoFsra)
          {
            if (await seriesNfoReader.TryReadMetadataAsync(seriesNfoFsra).ConfigureAwait(false))
              episodeNfoReader.SetSeriesStubs(seriesNfoReader.GetSeriesStubs());
            else
              _debugLogger.Warn("[#{0}]: No valid metadata found in series nfo-file", miNumber);
          }
        }

        // Then we store the found metadata in the MediaItemAspects. If we only found metadata that is
        // not (yet) supported by our MediaItemAspects, this MetadataExtractor returns false.
        if (!episodeNfoReader.TryWriteMetadata(extractedAspectData))
        {
          _debugLogger.Warn("[#{0}]: No metadata was written into MediaItemsAspects", miNumber);
          return false;
        }

        _debugLogger.Info("[#{0}]: Successfully finished extracting metadata", miNumber);
        return true;
      }
      catch (Exception e)
      {
        ServiceRegistration.Get<ILogger>().Warn("NfoSeriesMetadataExtractor: Exception while extracting metadata for resource '{0}'; enable debug logging for more details.", mediaItemAccessor);
        _debugLogger.Error("[#{0}]: Exception while extracting metadata", e, miNumber);
        return false;
      }
    }
        /// <summary>
        /// Asynchronously creates an <see cref="NfoSeriesEpisodeReader"/> for the given <param name="mediaItemAccessor"></param>
        /// </summary>
        /// <param name="mediaItemAccessor">Points to the resource for which we try to create an NfoSeriesEpisodeReader</param>
        /// <param name="season">Season number of the episode to create an NfoSeriesEpisodeReader for</param>
        /// <param name="episode">Episode number of the episode to create an NfoSeriesEpisodeReader for</param>
        /// <returns>An NfoSeriesEpisodeReader if an nfo file was found, else <c>null</c></returns>
        protected async Task <NfoSeriesEpisodeReader> TryGetNfoSeriesEpisodeReaderAsync(IResourceAccessor mediaItemAccessor, int?season, int?episode, bool includeFanart)
        {
            // Get a unique number for this call to TryExtractMetadataAsync. We use this to make reading the debug log easier.
            // This MetadataExtractor is called in parallel for multiple MediaItems so that the respective debug log entries
            // for one call are not contained one after another in debug log. We therefore prepend this number before every log entry.
            var miNumber = Interlocked.Increment(ref _lastMediaItemNumber);

            try
            {
                _debugLogger.Info("[#{0}]: Start extracting metadata for resource '{1}'", miNumber, mediaItemAccessor);

                // This MetadataExtractor only works for MediaItems accessible by an IFileSystemResourceAccessor.
                // Otherwise it is not possible to find a nfo-file in the MediaItem's directory or parent directory.
                if (!(mediaItemAccessor is IFileSystemResourceAccessor))
                {
                    _debugLogger.Info("[#{0}]: Cannot extract metadata; mediaItemAccessor is not an IFileSystemResourceAccessor", miNumber);
                    return(null);
                }

                // Here we try to find an IFileSystemResourceAccessor pointing to the episode nfo-file.
                // If we don't find one, we cannot extract any metadata.
                IFileSystemResourceAccessor episodeNfoFsra;
                NfoSeriesEpisodeReader      episodeNfoReader = null;
                bool episodeDetailsFound = false;
                if (TryGetEpisodeNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out episodeNfoFsra))
                {
                    episodeDetailsFound = true;
                    // Now we (asynchronously) extract the metadata into a stub object.
                    // If no metadata was found, nothing can be stored in the MediaItemAspects.
                    episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, false, false, _httpClient, _settings, includeFanart);
                    using (episodeNfoFsra)
                    {
                        if (!await episodeNfoReader.TryReadMetadataAsync(episodeNfoFsra).ConfigureAwait(false))
                        {
                            _debugLogger.Warn("[#{0}]: No valid metadata found in episode nfo-file", miNumber);
                            return(null);
                        }
                    }
                }

                // Then we try to find an IFileSystemResourceAccessor pointing to the series nfo-file.
                IFileSystemResourceAccessor seriesNfoFsra;
                if (TryGetSeriesNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out seriesNfoFsra))
                {
                    // If we found one, we (asynchronously) extract the metadata into a stub object and, if metadata was found,
                    // we store it into the episodeNfoReader so that the latter can store metadata from series and episode level into the MediaItemAspects.
                    var seriesNfoReader = new NfoSeriesReader(_debugLogger, miNumber, false, !episodeDetailsFound, false, _httpClient, _settings, includeFanart);
                    using (seriesNfoFsra)
                    {
                        if (await seriesNfoReader.TryReadMetadataAsync(seriesNfoFsra).ConfigureAwait(false))
                        {
                            Stubs.SeriesStub series = seriesNfoReader.GetSeriesStubs().FirstOrDefault();
                            if (series != null)
                            {
                                if (!episodeDetailsFound && series.Episodes != null && season.HasValue && episode.HasValue)
                                {
                                    Stubs.SeriesEpisodeStub episodeStub = series.Episodes.FirstOrDefault(e => e.Season == season && e.Episodes != null && e.Episodes.Contains(episode.Value));
                                    episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, false, false, _httpClient, _settings, includeFanart);
                                    episodeNfoReader.SetEpisodeStubs(new List <Stubs.SeriesEpisodeStub>(new[] { episodeStub }));
                                }

                                if (episodeNfoReader != null)
                                {
                                    episodeNfoReader.SetSeriesStubs(new List <Stubs.SeriesStub> {
                                        series
                                    });
                                }
                            }
                            else
                            {
                                _debugLogger.Warn("[#{0}]: No valid metadata found in series nfo-file", miNumber);
                            }
                        }
                    }
                }

                if (episodeNfoReader != null)
                {
                    return(episodeNfoReader);
                }

                _debugLogger.Warn("[#{0}]: No valid nfo-file found", miNumber);
            }
            catch (Exception e)
            {
                ServiceRegistration.Get <ILogger>().Warn("NfoSeriesMetadataExtractor: Exception while extracting metadata for resource '{0}'; enable debug logging for more details.", mediaItemAccessor);
                _debugLogger.Error("[#{0}]: Exception while extracting metadata", e, miNumber);
            }
            return(null);
        }
        /// <summary>
        /// Asynchronously tries to extract metadata for the given <param name="mediaItemAccessor"></param>
        /// </summary>
        /// <param name="mediaItemAccessor">Points to the resource for which we try to extract metadata</param>
        /// <param name="extractedAspectData">Dictionary of <see cref="MediaItemAspect"/>s with the extracted metadata</param>
        /// <param name="forceQuickMode">If <c>true</c>, nothing is downloaded from the internet</param>
        /// <returns><c>true</c> if metadata was found and stored into <param name="extractedAspectData"></param>, else <c>false</c></returns>
        private async Task <bool> TryExtractMetadataAsync(IResourceAccessor mediaItemAccessor, IDictionary <Guid, MediaItemAspect> extractedAspectData, bool forceQuickMode)
        {
            // Get a unique number for this call to TryExtractMetadataAsync. We use this to make reading the debug log easier.
            // This MetadataExtractor is called in parallel for multiple MediaItems so that the respective debug log entries
            // for one call are not contained one after another in debug log. We therefore prepend this number before every log entry.
            var miNumber = Interlocked.Increment(ref _lastMediaItemNumber);

            try
            {
                _debugLogger.Info("[#{0}]: Start extracting metadata for resource '{1}' (forceQuickMode: {2})", miNumber, mediaItemAccessor, forceQuickMode);

                // We only extract metadata with this MetadataExtractor, if another MetadataExtractor that was applied before
                // has identified this MediaItem as a video and therefore added a VideoAspect.
                if (!extractedAspectData.ContainsKey(VideoAspect.ASPECT_ID))
                {
                    _debugLogger.Info("[#{0}]: Cannot extract metadata; this resource is not a video", miNumber);
                    return(false);
                }

                // This MetadataExtractor only works for MediaItems accessible by an IFileSystemResourceAccessor.
                // Otherwise it is not possible to find a nfo-file in the MediaItem's directory or parent directory.
                if (!(mediaItemAccessor is IFileSystemResourceAccessor))
                {
                    _debugLogger.Info("[#{0}]: Cannot extract metadata; mediaItemAccessor is not an IFileSystemResourceAccessor", miNumber);
                    return(false);
                }

                // Here we try to find an IFileSystemResourceAccessor pointing to the episode nfo-file.
                // If we don't find one, we cannot extract any metadata.
                IFileSystemResourceAccessor episodeNfoFsra;
                if (!TryGetEpisodeNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out episodeNfoFsra))
                {
                    return(false);
                }

                // Now we (asynchronously) extract the metadata into a stub object.
                // If no metadata was found, nothing can be stored in the MediaItemAspects.
                var episodeNfoReader = new NfoSeriesEpisodeReader(_debugLogger, miNumber, forceQuickMode, _httpClient, _settings);
                using (episodeNfoFsra)
                {
                    if (!await episodeNfoReader.TryReadMetadataAsync(episodeNfoFsra).ConfigureAwait(false))
                    {
                        _debugLogger.Warn("[#{0}]: No valid metadata found in episode nfo-file", miNumber);
                        return(false);
                    }
                }

                // Then we try to find an IFileSystemResourceAccessor pointing to the series nfo-file.
                IFileSystemResourceAccessor seriesNfoFsra;
                if (TryGetSeriesNfoSResourceAccessor(miNumber, mediaItemAccessor as IFileSystemResourceAccessor, out seriesNfoFsra))
                {
                    // If we found one, we (asynchronously) extract the metadata into a stub object and, if metadata was found,
                    // we store it into the episodeNfoReader so that the latter can store metadata from series and episode level into the MediaItemAspects.
                    var seriesNfoReader = new NfoSeriesReader(_debugLogger, miNumber, forceQuickMode, _httpClient, _settings);
                    using (seriesNfoFsra)
                    {
                        if (await seriesNfoReader.TryReadMetadataAsync(seriesNfoFsra).ConfigureAwait(false))
                        {
                            episodeNfoReader.SetSeriesStubs(seriesNfoReader.GetSeriesStubs());
                        }
                        else
                        {
                            _debugLogger.Warn("[#{0}]: No valid metadata found in series nfo-file", miNumber);
                        }
                    }
                }

                // Then we store the found metadata in the MediaItemAspects. If we only found metadata that is
                // not (yet) supported by our MediaItemAspects, this MetadataExtractor returns false.
                if (!episodeNfoReader.TryWriteMetadata(extractedAspectData))
                {
                    _debugLogger.Warn("[#{0}]: No metadata was written into MediaItemsAspects", miNumber);
                    return(false);
                }

                _debugLogger.Info("[#{0}]: Successfully finished extracting metadata", miNumber);
                return(true);
            }
            catch (Exception e)
            {
                ServiceRegistration.Get <ILogger>().Warn("NfoSeriesMetadataExtractor: Exception while extracting metadata for resource '{0}'; enable debug logging for more details.", mediaItemAccessor);
                _debugLogger.Error("[#{0}]: Exception while extracting metadata", e, miNumber);
                return(false);
            }
        }