From 03b9c957b81f83559d095f5ab915f5d357f3b4a6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 26 Oct 2024 14:20:55 -0700 Subject: [PATCH] New: Episode mappings in .plexmatch metadata files Closes #5784 --- src/NzbDrone.Core/Extras/ExtraService.cs | 31 ++++++++++++++++- .../Extras/Files/ExtraFileManager.cs | 2 ++ .../Consumers/Kometa/KometaMetadata.cs | 2 +- .../Metadata/Consumers/Plex/PlexMetadata.cs | 31 ++++++++++++++++- .../Consumers/Plex/PlexMetadataSettings.cs | 3 ++ .../Consumers/Roksbox/RoksboxMetadata.cs | 2 +- .../Metadata/Consumers/Wdtv/WdtvMetadata.cs | 2 +- .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 7 +++- .../Extras/Metadata/IMetadata.cs | 11 +++++-- .../Extras/Metadata/MetadataBase.cs | 2 +- .../Extras/Metadata/MetadataService.cs | 33 ++++++++++++++++--- .../Extras/Others/OtherExtraService.cs | 5 +++ .../Extras/Subtitles/SubtitleService.cs | 5 +++ src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 14 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index b29fa6d17..5742b7610 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -5,6 +5,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; @@ -25,13 +26,15 @@ public class ExtraService : IExtraService, IHandle, IHandle, IHandle, - IHandle + IHandle, + IHandle { private readonly IMediaFileService _mediaFileService; private readonly IEpisodeService _episodeService; private readonly IDiskProvider _diskProvider; private readonly IConfigService _configService; private readonly List _extraFileManagers; + private readonly Dictionary _seriesWithImportedFiles; public ExtraService(IMediaFileService mediaFileService, IEpisodeService episodeService, @@ -45,6 +48,7 @@ public ExtraService(IMediaFileService mediaFileService, _diskProvider = diskProvider; _configService = configService; _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); + _seriesWithImportedFiles = new Dictionary(); } public void ImportEpisode(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) @@ -100,6 +104,11 @@ private void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile private void CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { + lock (_seriesWithImportedFiles) + { + _seriesWithImportedFiles.TryAdd(series.Id, series); + } + foreach (var extraFileManager in _extraFileManagers) { extraFileManager.CreateAfterEpisodeImport(series, episodeFile); @@ -161,6 +170,26 @@ public void Handle(SeriesRenamedEvent message) } } + public void Handle(DownloadsProcessedEvent message) + { + var allSeries = new List(); + + lock (_seriesWithImportedFiles) + { + allSeries.AddRange(_seriesWithImportedFiles.Values); + + _seriesWithImportedFiles.Clear(); + } + + foreach (var series in allSeries) + { + foreach (var extraFileManager in _extraFileManagers) + { + extraFileManager.CreateAfterEpisodesImported(series); + } + } + } + private List GetEpisodeFiles(int seriesId) { var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs index 4ea272f67..1b9179689 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileManager.cs @@ -17,6 +17,7 @@ public interface IManageExtraFiles int Order { get; } IEnumerable CreateAfterMediaCoverUpdate(Series series); IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); + IEnumerable CreateAfterEpisodesImported(Series series); IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); IEnumerable CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); @@ -46,6 +47,7 @@ public ExtraFileManager(IConfigService configService, public abstract int Order { get; } public abstract IEnumerable CreateAfterMediaCoverUpdate(Series series); public abstract IEnumerable CreateAfterSeriesScan(Series series, List episodeFiles); + public abstract IEnumerable CreateAfterEpisodesImported(Series series); public abstract IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); public abstract IEnumerable CreateAfterEpisodeFolder(Series series, string seriesFolder, string seasonFolder); public abstract IEnumerable MoveFilesAfterRename(Series series, List episodeFiles); diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs index 4a0bd6c2f..7085b3ddf 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -92,7 +92,7 @@ public override MetadataFile FindMetadataFile(Series series, string path) return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { return null; } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs index bfe8e7220..df56a72af 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadata.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; @@ -10,6 +11,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex { public class PlexMetadata : MetadataBase { + private readonly IEpisodeService _episodeService; + private readonly IMediaFileService _mediaFileService; + + public PlexMetadata(IEpisodeService episodeService, IMediaFileService mediaFileService) + { + _episodeService = episodeService; + _mediaFileService = mediaFileService; + } + public override string Name => "Plex"; public override MetadataFile FindMetadataFile(Series series, string path) @@ -37,7 +47,7 @@ public override MetadataFile FindMetadataFile(Series series, string path) return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { if (!Settings.SeriesPlexMatchFile) { @@ -51,6 +61,25 @@ public override MetadataFileResult SeriesMetadata(Series series) content.AppendLine($"TvdbId: {series.TvdbId}"); content.AppendLine($"ImdbId: {series.ImdbId}"); + if (Settings.EpisodeMappings) + { + var episodes = _episodeService.GetEpisodeBySeries(series.Id); + var episodeFiles = _mediaFileService.GetFilesBySeries(series.Id); + + foreach (var episodeFile in episodeFiles) + { + var episodesInFile = episodes.Where(e => e.EpisodeFileId == episodeFile.Id); + var episodeFormat = $"S{episodeFile.SeasonNumber:00}{string.Join("-", episodesInFile.Select(e => $"E{e.EpisodeNumber:00}"))}"; + + if (episodeFile.SeasonNumber == 0) + { + episodeFormat = $"SP{episodesInFile.First():00}"; + } + + content.Append($"Episode: {episodeFormat}: {episodeFile.RelativePath}"); + } + } + return new MetadataFileResult(".plexmatch", content.ToString()); } diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs index de94f5153..3558290c5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Plex/PlexMetadataSettings.cs @@ -21,6 +21,9 @@ public PlexMetadataSettings() [FieldDefinition(0, Label = "MetadataPlexSettingsSeriesPlexMatchFile", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataPlexSettingsSeriesPlexMatchFileHelpText")] public bool SeriesPlexMatchFile { get; set; } + [FieldDefinition(0, Label = "MetadataPlexSettingsEpisodeMappings", Type = FieldType.Checkbox, Section = MetadataSectionType.Metadata, HelpText = "MetadataPlexSettingsEpisodeMappingsHelpText")] + public bool EpisodeMappings { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs index 095225fd6..ee98606a9 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadata.cs @@ -124,7 +124,7 @@ public override MetadataFile FindMetadataFile(Series series, string path) return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { // Series metadata is not supported return null; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs index 3888d15f3..429bb808b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Wdtv/WdtvMetadata.cs @@ -113,7 +113,7 @@ public override MetadataFile FindMetadataFile(Series series, string path) return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { // Series metadata is not supported return null; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index a65168bdf..5450a16f3 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -137,8 +137,13 @@ public override MetadataFile FindMetadataFile(Series series, string path) return null; } - public override MetadataFileResult SeriesMetadata(Series series) + public override MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason) { + if (reason == SeriesMetadataReason.EpisodesImported) + { + return null; + } + var xmlResult = string.Empty; if (Settings.SeriesMetadata) diff --git a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs index b631425e6..b9232fa6e 100644 --- a/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/IMetadata.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.ThingiProvider; @@ -10,10 +10,17 @@ public interface IMetadata : IProvider { string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); MetadataFile FindMetadataFile(Series series, string path); - MetadataFileResult SeriesMetadata(Series series); + MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason); MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); List SeriesImages(Series series); List SeasonImages(Series series, Season season); List EpisodeImages(Series series, EpisodeFile episodeFile); } + + public enum SeriesMetadataReason + { + Scan, + EpisodeFolderCreated, + EpisodesImported + } } diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs index ecfa7b855..369c9fe76 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataBase.cs @@ -38,7 +38,7 @@ public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFil public abstract MetadataFile FindMetadataFile(Series series, string path); - public abstract MetadataFileResult SeriesMetadata(Series series); + public abstract MetadataFileResult SeriesMetadata(Series series, SeriesMetadataReason reason); public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); public abstract List SeriesImages(Series series); public abstract List SeasonImages(Series series, Season season); diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 1cf253e81..5b08aa384 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -99,7 +99,7 @@ public override IEnumerable CreateAfterSeriesScan(Series series, List { var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.Scan)); files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles)); @@ -115,6 +115,31 @@ public override IEnumerable CreateAfterSeriesScan(Series series, List return files; } + public override IEnumerable CreateAfterEpisodesImported(Series series) + { + var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); + _cleanMetadataService.Clean(series); + + if (!_diskProvider.FolderExists(series.Path)) + { + _logger.Info("Series folder does not exist, skipping metadata creation"); + return Enumerable.Empty(); + } + + var files = new List(); + + foreach (var consumer in _metadataFactory.Enabled()) + { + var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); + + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.EpisodesImported)); + } + + _metadataFileService.Upsert(files); + + return files; + } + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { var files = new List(); @@ -147,7 +172,7 @@ public override IEnumerable CreateAfterEpisodeFolder(Series series, s if (seriesFolder.IsNotNullOrWhiteSpace()) { - files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); + files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles, SeriesMetadataReason.EpisodeFolderCreated)); files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles)); } @@ -218,9 +243,9 @@ private List GetMetadataFilesForConsumer(IMetadata consumer, List< return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); } - private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List existingMetadataFiles) + private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List existingMetadataFiles, SeriesMetadataReason reason) { - var seriesMetadata = consumer.SeriesMetadata(series); + var seriesMetadata = consumer.SeriesMetadata(series, reason); if (seriesMetadata == null) { diff --git a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs index 1a1d167c9..4567dbe08 100644 --- a/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs +++ b/src/NzbDrone.Core/Extras/Others/OtherExtraService.cs @@ -46,6 +46,11 @@ public override IEnumerable CreateAfterSeriesScan(Series series, List return Enumerable.Empty(); } + public override IEnumerable CreateAfterEpisodesImported(Series series) + { + return Enumerable.Empty(); + } + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { return Enumerable.Empty(); diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs index 465783f16..18e4cbdca 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -53,6 +53,11 @@ public override IEnumerable CreateAfterSeriesScan(Series series, List return Enumerable.Empty(); } + public override IEnumerable CreateAfterEpisodesImported(Series series) + { + return Enumerable.Empty(); + } + public override IEnumerable CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) { return Enumerable.Empty(); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f9d6383e4..5c8616c83 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1151,6 +1151,8 @@ "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", + "MetadataPlexSettingsEpisodeMappings": "Episode Mappings", + "MetadataPlexSettingsEpisodeMappingsHelpText": "Include episode mappings for all files in .plexmatch file", "MetadataPlexSettingsSeriesPlexMatchFile": "Series Plex Match File", "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Creates a .plexmatch file in the series folder", "MetadataProvidedBy": "Metadata is provided by {provider}",