using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Metadata { public class MetadataService : IHandle, IHandle, IHandle { private readonly IMetadataFactory _metadataFactory; private readonly IMetadataFileService _metadataFileService; private readonly ICleanMetadataService _cleanMetadataService; private readonly IMediaFileService _mediaFileService; private readonly IEpisodeService _episodeService; private readonly IDiskProvider _diskProvider; private readonly IHttpProvider _httpProvider; private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public MetadataService(IMetadataFactory metadataFactory, IMetadataFileService metadataFileService, ICleanMetadataService cleanMetadataService, IMediaFileService mediaFileService, IEpisodeService episodeService, IDiskProvider diskProvider, IHttpProvider httpProvider, IConfigService configService, IEventAggregator eventAggregator, Logger logger) { _metadataFactory = metadataFactory; _metadataFileService = metadataFileService; _cleanMetadataService = cleanMetadataService; _mediaFileService = mediaFileService; _episodeService = episodeService; _diskProvider = diskProvider; _httpProvider = httpProvider; _configService = configService; _eventAggregator = eventAggregator; _logger = logger; } public void Handle(MediaCoversUpdatedEvent message) { _cleanMetadataService.Clean(message.Series); if (!_diskProvider.FolderExists(message.Series.Path)) { _logger.Info("Series folder does not exist, skipping metadata creation"); return; } var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); var episodeFiles = GetEpisodeFiles(message.Series.Id); foreach (var consumer in _metadataFactory.Enabled()) { var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); var files = new List(); files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); foreach (var episodeFile in episodeFiles) { files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles)); files.AddRange(ProcessEpisodeImages(consumer, message.Series, episodeFile, consumerFiles)); } _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } } public void Handle(EpisodeImportedEvent message) { foreach (var consumer in _metadataFactory.Enabled()) { var files = new List(); files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); files.AddRange(ProcessEpisodeImages(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List())); _eventAggregator.PublishEvent(new MetadataFilesUpdated(files)); } } public void Handle(SeriesRenamedEvent message) { var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id); var episodeFiles = GetEpisodeFiles(message.Series.Id); foreach (var consumer in _metadataFactory.Enabled()) { var updatedMetadataFiles = consumer.AfterRename(message.Series, GetMetadataFilesForConsumer(consumer, seriesMetadata), episodeFiles); _eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles)); } } private List GetEpisodeFiles(int seriesId) { var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); var episodes = _episodeService.GetEpisodeBySeries(seriesId); foreach (var episodeFile in episodeFiles) { var localEpisodeFile = episodeFile; episodeFile.Episodes = new LazyList(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id)); } return episodeFiles; } private List GetMetadataFilesForConsumer(IMetadata consumer, List seriesMetadata) { return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List existingMetadataFiles) { var seriesMetadata = consumer.SeriesMetadata(series); if (seriesMetadata == null) { return null; } var hash = seriesMetadata.Contents.SHA256Hash(); var metadata = existingMetadataFiles.SingleOrDefault(e => e.Type == MetadataType.SeriesMetadata) ?? new MetadataFile { SeriesId = series.Id, Consumer = consumer.GetType().Name, Type = MetadataType.SeriesMetadata, }; if (hash == metadata.Hash) { return null; } var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath); _logger.Debug("Writing Series Metadata to: {0}", fullPath); SaveMetadataFile(fullPath, seriesMetadata.Contents); metadata.Hash = hash; metadata.RelativePath = seriesMetadata.RelativePath; return metadata; } private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) { var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile); if (episodeMetadata == null) { return null; } var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath); var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && c.EpisodeFileId == episodeFile.Id); if (existingMetadata != null) { var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); if (!fullPath.PathEquals(existingFullPath)) { _diskProvider.MoveFile(existingFullPath, fullPath); existingMetadata.RelativePath = episodeMetadata.RelativePath; } } var hash = episodeMetadata.Contents.SHA256Hash(); var metadata = existingMetadata ?? new MetadataFile { SeriesId = series.Id, EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeMetadata, RelativePath = episodeMetadata.RelativePath }; if (hash == metadata.Hash) { return null; } _logger.Debug("Writing Episode Metadata to: {0}", fullPath); SaveMetadataFile(fullPath, episodeMetadata.Contents); metadata.Hash = hash; return metadata; } private List ProcessSeriesImages(IMetadata consumer, Series series, List existingMetadataFiles) { var result = new List(); foreach (var image in consumer.SeriesImages(series)) { if (_diskProvider.FileExists(image.RelativePath)) { _logger.Debug("Series image already exists: {0}", image.RelativePath); continue; } var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage && c.RelativePath == image.RelativePath) ?? new MetadataFile { SeriesId = series.Id, Consumer = consumer.GetType().Name, Type = MetadataType.SeriesImage, RelativePath = image.RelativePath }; _diskProvider.CopyFile(image.Url, image.RelativePath); result.Add(metadata); } return result; } private List ProcessSeasonImages(IMetadata consumer, Series series, List existingMetadataFiles) { var result = new List(); foreach (var season in series.Seasons) { foreach (var image in consumer.SeasonImages(series, season)) { if (_diskProvider.FileExists(image.RelativePath)) { _logger.Debug("Season image already exists: {0}", image.RelativePath); continue; } var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && c.SeasonNumber == season.SeasonNumber && c.RelativePath == image.RelativePath) ?? new MetadataFile { SeriesId = series.Id, SeasonNumber = season.SeasonNumber, Consumer = consumer.GetType().Name, Type = MetadataType.SeasonImage, RelativePath = image.RelativePath }; DownloadImage(series, image.Url, image.RelativePath); result.Add(metadata); } } return result; } private List ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List existingMetadataFiles) { var result = new List(); foreach (var image in consumer.EpisodeImages(series, episodeFile)) { var fullPath = Path.Combine(series.Path, image.RelativePath); if (_diskProvider.FileExists(fullPath)) { _logger.Debug("Episode image already exists: {0}", image.RelativePath); continue; } var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage && c.EpisodeFileId == episodeFile.Id); if (existingMetadata != null) { var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); if (!fullPath.PathEquals(existingFullPath)) { _diskProvider.MoveFile(fullPath, fullPath); existingMetadata.RelativePath = image.RelativePath; return new List{ existingMetadata }; } } var metadata = existingMetadata ?? new MetadataFile { SeriesId = series.Id, EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeImage, RelativePath = image.RelativePath }; DownloadImage(series, image.Url, fullPath); result.Add(metadata); } return result; } private void DownloadImage(Series series, String url, String path) { try { _httpProvider.DownloadFile(url, path); SetFilePermissions(path); } catch (WebException e) { _logger.Warn(string.Format("Couldn't download image {0} for {1}. {2}", url, series, e.Message)); } catch (Exception e) { _logger.ErrorException("Couldn't download image " + url + " for " + series, e); } } private void SaveMetadataFile(String path, String contents) { _diskProvider.WriteAllText(path, contents); SetFilePermissions(path); } private void SetFilePermissions(String path) { if (!_configService.SetPermissionsLinux) { return; } try { _diskProvider.SetPermissions(path, _configService.FileChmod, _configService.ChownUser, _configService.ChownGroup); } catch (Exception ex) { _logger.WarnException("Unable to apply permissions to: " + path, ex); _logger.DebugException(ex.Message, ex); } } } }