diff --git a/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs b/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs new file mode 100644 index 000000000..723c5d74b --- /dev/null +++ b/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RenameSeasonCommand : ICommand + { + public int SeriesId { get; private set; } + public int SeasonNumber { get; private set; } + + public RenameSeasonCommand(int seriesId, int seasonNumber) + { + SeriesId = seriesId; + SeasonNumber = seasonNumber; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs b/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs new file mode 100644 index 000000000..7716c43c0 --- /dev/null +++ b/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RenameSeriesCommand : ICommand + { + public int SeriesId { get; private set; } + + public RenameSeriesCommand(int seriesId) + { + SeriesId = seriesId; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index c59df33d5..b050c8add 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.MediaFiles public interface IMoveEpisodeFiles { EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile); + EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series); EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); } @@ -61,6 +62,15 @@ namespace NzbDrone.Core.MediaFiles return episodeFile; } + public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series) + { + var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id); + var newFileName = _buildFileNames.BuildFilename(episodes, series, episodeFile); + var destinationFilename = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); + + return MoveFile(episodeFile, destinationFilename); + } + public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) { var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); diff --git a/NzbDrone.Core/Download/SeriesRenamedEvent.cs b/NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs similarity index 86% rename from NzbDrone.Core/Download/SeriesRenamedEvent.cs rename to NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs index 18c7d4b80..8cfe96b89 100644 --- a/NzbDrone.Core/Download/SeriesRenamedEvent.cs +++ b/NzbDrone.Core/MediaFiles/Events/SeriesRenamedEvent.cs @@ -1,7 +1,7 @@ using NzbDrone.Common.Messaging; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.Download +namespace NzbDrone.Core.MediaFiles.Events { public class SeriesRenamedEvent : IEvent { diff --git a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 2e9415b99..406bd9669 100644 --- a/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles { EpisodeFile GetFileByPath(string path); List GetFilesBySeries(int seriesId); + List GetFilesBySeason(int seriesId, int seasonNumber); bool Exists(string path); } @@ -20,21 +21,26 @@ namespace NzbDrone.Core.MediaFiles { } - public EpisodeFile GetFileByPath(string path) { return Query.SingleOrDefault(c => c.Path == path); } - public bool Exists(string path) + public List GetFilesBySeries(int seriesId) { - return Query.Any(c => c.Path == path); + return Query.Where(c => c.SeriesId == seriesId).ToList(); } - public List GetFilesBySeries(int seriesId) + public List GetFilesBySeason(int seriesId, int seasonNumber) { - return Query.Where(c => c.SeriesId == seriesId).ToList(); + return Query.Where(c => c.SeriesId == seriesId) + .AndWhere(c => c.SeasonNumber == seasonNumber) + .ToList(); } + public bool Exists(string path) + { + return Query.Any(c => c.Path == path); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/MediaFileService.cs b/NzbDrone.Core/MediaFiles/MediaFileService.cs index c6532b3e8..a53b0f85a 100644 --- a/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -17,21 +17,19 @@ namespace NzbDrone.Core.MediaFiles bool Exists(string path); EpisodeFile GetFileByPath(string path); List GetFilesBySeries(int seriesId); - + List GetFilesBySeason(int seriesId, int seasonNumber); List FilterExistingFiles(List files, int seriesId); } public class MediaFileService : IMediaFileService, IHandleAsync { - private readonly IConfigService _configService; private readonly IMessageAggregator _messageAggregator; - private readonly Logger _logger; private readonly IMediaFileRepository _mediaFileRepository; + private readonly Logger _logger; - public MediaFileService(IMediaFileRepository mediaFileRepository, IConfigService configService, IMessageAggregator messageAggregator, Logger logger) + public MediaFileService(IMediaFileRepository mediaFileRepository, IMessageAggregator messageAggregator, Logger logger) { _mediaFileRepository = mediaFileRepository; - _configService = configService; _messageAggregator = messageAggregator; _logger = logger; } diff --git a/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs b/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs new file mode 100644 index 000000000..51cb44e72 --- /dev/null +++ b/NzbDrone.Core/MediaFiles/RenameEpisodeFileService.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IRenameEpisodeFiles + { + + } + + public class RenameEpisodeFileService : IRenameEpisodeFiles, IExecute, IExecute + { + private readonly ISeriesService _seriesService; + private readonly IMediaFileService _mediaFileService; + private readonly IMoveEpisodeFiles _episodeFileMover; + private readonly IMessageAggregator _messageAggregator; + private readonly Logger _logger; + + public RenameEpisodeFileService(ISeriesService seriesService, + IMediaFileService mediaFileService, + IMoveEpisodeFiles episodeFileMover, + IMessageAggregator messageAggregator, + Logger logger) + { + _seriesService = seriesService; + _mediaFileService = mediaFileService; + _episodeFileMover = episodeFileMover; + _messageAggregator = messageAggregator; + _logger = logger; + } + + private void RenameFiles(List episodeFiles, Series series) + { + var renamed = new List(); + + foreach (var file in episodeFiles) + { + var episodeFile = file; + + _logger.Trace("Renaming episode file: {0}", episodeFile); + episodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, series); + + if (episodeFile != null) + { + _mediaFileService.Update(episodeFile); + renamed.Add(episodeFile); + } + + _logger.Trace("Renamed episode file: {0}", episodeFile); + } + + if (renamed.Any()) + { + _messageAggregator.PublishEvent(new SeriesRenamedEvent(series)); + } + } + + public void Execute(RenameSeasonCommand message) + { + var series = _seriesService.GetSeries(message.SeriesId); + var episodeFiles = _mediaFileService.GetFilesBySeason(message.SeriesId, message.SeasonNumber); + + _logger.Info("Renaming {0} files for {1} season {2}", episodeFiles.Count, series.Title, message.SeasonNumber); + RenameFiles(episodeFiles, series); + _logger.Debug("Episode Fies renamed for {0} season {1}", series.Title, message.SeasonNumber); + } + + public void Execute(RenameSeriesCommand message) + { + var series = _seriesService.GetSeries(message.SeriesId); + var episodeFiles = _mediaFileService.GetFilesBySeries(message.SeriesId); + + _logger.Info("Renaming {0} files for {1}", episodeFiles.Count, series.Title); + RenameFiles(episodeFiles, series); + _logger.Debug("Episode Fies renamed for {0}", series.Title); + } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 5e6918d2d..d220524f7 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -270,6 +270,8 @@ + + @@ -281,7 +283,8 @@ - + + diff --git a/UI/Content/icons.less b/UI/Content/icons.less index 1ec48c837..c87e14ac0 100644 --- a/UI/Content/icons.less +++ b/UI/Content/icons.less @@ -54,6 +54,15 @@ color : @errorText; } +.icon-nd-spinner:before { + .icon(@spinner); + .icon-spin; +} + +.icon-nd-rename:before { + .icon(@pencil) +} + .icon-nd-add:before { .icon(@plus); } diff --git a/UI/Series/Details/SeasonLayout.js b/UI/Series/Details/SeasonLayout.js index 9e9c1f6c5..6ed16851f 100644 --- a/UI/Series/Details/SeasonLayout.js +++ b/UI/Series/Details/SeasonLayout.js @@ -15,12 +15,14 @@ define( ui: { seasonSearch : '.x-season-search', - seasonMonitored: '.x-season-monitored' + seasonMonitored: '.x-season-monitored', + seasonRename : '.x-season-rename' }, events: { 'click .x-season-search' : '_seasonSearch', - 'click .x-season-monitored': '_seasonMonitored' + 'click .x-season-monitored': '_seasonMonitored', + 'click .x-season-rename' : '_seasonRename' }, regions: { @@ -151,6 +153,40 @@ define( this.ui.seasonMonitored.addClass('icon-bookmark-empty'); this.ui.seasonMonitored.removeClass('icon-bookmark'); } + }, + + _seasonRename: function () { + var command = 'renameSeason'; + + this.idle = false; + + this.ui.seasonRename.toggleClass('icon-nd-rename icon-nd-spinner'); + + var properties = { + seriesId : this.model.get('seriesId'), + seasonNumber: this.model.get('seasonNumber') + }; + + var self = this; + var commandPromise = CommandController.Execute(command, properties); + + commandPromise.fail(function (options) { + if (options.readyState === 0 || options.status === 0) { + return; + } + + Messenger.show({ + message: 'Season rename failed', + type : 'error' + }); + }); + + commandPromise.always(function () { + if (!self.isClosed) { + self.ui.seasonRename.toggleClass('icon-nd-rename icon-nd-spinner'); + self.idle = true; + } + }); } }); }); diff --git a/UI/Series/Details/SeasonLayoutTemplate.html b/UI/Series/Details/SeasonLayoutTemplate.html index a555d0a49..d6aebcd32 100644 --- a/UI/Series/Details/SeasonLayoutTemplate.html +++ b/UI/Series/Details/SeasonLayoutTemplate.html @@ -2,11 +2,12 @@

{{#if seasonNumber}} - Season {{seasonNumber}} + Season {{seasonNumber}} {{else}} - Specials + Specials {{/if}} +

diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index 07b0a589f..3583613ce 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -24,13 +24,15 @@ define( header : '.x-header', monitored: '.x-monitored', edit : '.x-edit', - refresh : '.x-refresh' + refresh : '.x-refresh', + rename : '.x-rename' }, events: { 'click .x-monitored': '_toggleMonitored', 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshSeries' + 'click .x-refresh' : '_refreshSeries', + 'click .x-rename' : '_renameSeries' }, initialize: function () { @@ -136,6 +138,39 @@ define( if (this.model.get('id') === event.series.get('id')) { App.Router.navigate('/', { trigger: true }); } + }, + + _renameSeries: function () { + var command = 'renameSeries'; + + this.idle = false; + + this.ui.rename.toggleClass('icon-nd-rename icon-nd-spinner'); + + var properties = { + seriesId : this.model.get('id') + }; + + var self = this; + var commandPromise = CommandController.Execute(command, properties); + + commandPromise.fail(function (options) { + if (options.readyState === 0 || options.status === 0) { + return; + } + + Messenger.show({ + message: 'Season rename failed', + type : 'error' + }); + }); + + commandPromise.always(function () { + if (!self.isClosed) { + self.ui.rename.toggleClass('icon-nd-rename icon-nd-spinner'); + self.idle = true; + } + }); } }); }); diff --git a/UI/Series/Details/SeriesDetailsTemplate.html b/UI/Series/Details/SeriesDetailsTemplate.html index 87a79b6f8..4ebc6cef3 100644 --- a/UI/Series/Details/SeriesDetailsTemplate.html +++ b/UI/Series/Details/SeriesDetailsTemplate.html @@ -5,6 +5,7 @@ {{title}} +