Merge branch 'AirDate' of https://github.com/JackDandy/NzbDrone into set-file-date

pull/3113/head
Mark McDowall 10 years ago
commit 8478379ff4

@ -6,6 +6,7 @@ namespace NzbDrone.Api.Config
public class MediaManagementConfigResource : RestResource
{
public Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }
public Boolean FileDateAiredDate { get; set; }
public String RecycleBin { get; set; }
public Boolean AutoDownloadPropers { get; set; }
public Boolean CreateEmptySeriesFolders { get; set; }

@ -35,7 +35,7 @@ namespace NzbDrone.Api.Logs
{
Id = i + 1,
Filename = Path.GetFileName(file),
LastWriteTime = _diskProvider.GetLastFileWrite(file)
LastWriteTime = _diskProvider.GetLastFileWriteUTC(file)
});
}

@ -77,6 +77,20 @@ namespace NzbDrone.Common.Disk
}
public DateTime GetLastFileWrite(string path)
{
PathEnsureFileExists(path);
return new FileInfo(path).LastWriteTime;
}
public DateTime GetLastFileWriteUTC(string path)
{
PathEnsureFileExists(path);
return new FileInfo(path).LastWriteTimeUtc;
}
private void PathEnsureFileExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
@ -84,8 +98,6 @@ namespace NzbDrone.Common.Disk
{
throw new FileNotFoundException("File doesn't exist: " + path);
}
return new FileInfo(path).LastWriteTimeUtc;
}
public void EnsureFolder(string path)
@ -305,6 +317,26 @@ namespace NzbDrone.Common.Disk
Directory.SetLastWriteTimeUtc(path, dateTime);
}
public void FileSetLastWriteTime(string path, DateTime dateTime)
{
Ensure.That(path, () => path).IsValidPath();
File.SetLastWriteTime(path, dateTime);
}
public void FileSetLastAccessTime(string path, DateTime dateTime)
{
Ensure.That(path, () => path).IsValidPath();
File.SetLastAccessTimeUtc(path, dateTime);
}
public void FileSetLastAccessTimeUtc(string path, DateTime dateTime)
{
Ensure.That(path, () => path).IsValidPath();
File.SetLastAccessTimeUtc(path, dateTime);
}
public bool IsFileLocked(string file)
{
try

@ -14,6 +14,7 @@ namespace NzbDrone.Common.Disk
DateTime GetLastFolderWrite(string path);
DateTime GetLastFileWrite(string path);
DateTime GetLastFileWriteUTC(string path);
void EnsureFolder(string path);
bool FolderExists(string path);
bool FileExists(string path);
@ -33,6 +34,8 @@ namespace NzbDrone.Common.Disk
void WriteAllText(string filename, string contents);
void FileSetLastWriteTimeUtc(string path, DateTime dateTime);
void FolderSetLastWriteTimeUtc(string path, DateTime dateTime);
void FileSetLastWriteTime(string path, DateTime dateTime);
void FileSetLastAccessTime(string path, DateTime dateTime);
bool IsFileLocked(string path);
string GetPathRoot(string path);
string GetParentFolder(string path);

@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.MediaCoverTests
new MediaCover.MediaCover {CoverType = MediaCoverTypes.Banner}
};
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetLastFileWrite(It.IsAny<string>()))
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetLastFileWriteUTC(It.IsAny<string>()))
.Returns(new DateTime(1234));
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(It.IsAny<string>()))

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
private void GivenLastWriteTimeUtc(DateTime time)
{
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetLastFileWrite(It.IsAny<string>()))
.Setup(s => s.GetLastFileWriteUTC(It.IsAny<string>()))
.Returns(time);
}

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFolderWrite(It.IsAny<String>()))
.Returns(DateTime.UtcNow.AddDays(-10));
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFileWrite(It.IsAny<String>()))
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFileWriteUTC(It.IsAny<String>()))
.Returns(DateTime.UtcNow.AddDays(-10));
}
@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFolderWrite(It.IsAny<String>()))
.Returns(DateTime.UtcNow.AddDays(-3));
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFileWrite(It.IsAny<String>()))
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetLastFileWriteUTC(It.IsAny<String>()))
.Returns(DateTime.UtcNow.AddDays(-3));
}

@ -86,6 +86,12 @@ namespace NzbDrone.Core.Configuration
set { SetValue("AutoUnmonitorPreviouslyDownloadedEpisodes", value); }
}
public bool FileDateAiredDate
{
get { return GetValueBoolean("FileDateAiredDate"); }
set { SetValue("FileDateAiredDate", value); }
}
public int Retention
{
get { return GetValueInt("Retention", 0); }

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Configuration
//Media Management
Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }
Boolean FileDateAiredDate { get; set; }
String RecycleBin { get; set; }
Boolean AutoDownloadPropers { get; set; }
Boolean CreateEmptySeriesFolders { get; set; }

@ -67,7 +67,7 @@ namespace NzbDrone.Core.MediaCover
if (_diskProvider.FileExists(filePath))
{
var lastWrite = _diskProvider.GetLastFileWrite(filePath);
var lastWrite = _diskProvider.GetLastFileWriteUTC(filePath);
mediaCover.Url += "?lastWrite=" + lastWrite.Ticks;
}
}

@ -0,0 +1,18 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
{
public class AirDateSeriesCommand : Command
{
public List<int> SeriesIds { get; set; }
public override bool SendUpdatesToClient
{
get
{
return true;
}
}
}
}

@ -22,18 +22,21 @@ namespace NzbDrone.Core.MediaFiles
public class EpisodeFileMovingService : IMoveEpisodeFiles
{
private readonly IEpisodeService _episodeService;
private readonly IUpdateEpisodeFileService _updateEpisodeFileService;
private readonly IBuildFileNames _buildFileNames;
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public EpisodeFileMovingService(IEpisodeService episodeService,
IUpdateEpisodeFileService updateEpisodeFileService,
IBuildFileNames buildFileNames,
IDiskProvider diskProvider,
IConfigService configService,
Logger logger)
{
_episodeService = episodeService;
_updateEpisodeFileService = updateEpisodeFileService;
_buildFileNames = buildFileNames;
_diskProvider = diskProvider;
_configService = configService;
@ -102,6 +105,11 @@ namespace NzbDrone.Core.MediaFiles
_diskProvider.MoveFile(episodeFile.Path, destinationFilename);
episodeFile.Path = destinationFilename;
if (_configService.FileDateAiredDate)
{
_updateEpisodeFileService.ChangeFileDateToAirdate(episodeFile, series);
}
try
{
_logger.Trace("Setting last write time on series folder: {0}", series.Path);

@ -42,7 +42,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return false;
}
if (_diskProvider.GetLastFileWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5))
if (_diskProvider.GetLastFileWriteUTC(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5))
{
_logger.Trace("{0} appears to be unpacking still", localEpisode.Path);
return false;

@ -0,0 +1,15 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.Events
{
public class SeriesAirDatedEvent : IEvent
{
public Series Series { get; private set; }
public SeriesAirDatedEvent(Series series)
{
Series = series;
}
}
}

@ -139,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles
foreach (var file in _diskProvider.GetFiles(_configService.RecycleBin, SearchOption.TopDirectoryOnly))
{
if (_diskProvider.GetLastFileWrite(file).AddDays(7) > DateTime.UtcNow)
if (_diskProvider.GetLastFileWriteUTC(file).AddDays(7) > DateTime.UtcNow)
{
logger.Trace("File hasn't expired yet, skipping: {0}", file);
continue;

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles
{
public interface IUpdateEpisodeFileService
{
void ChangeFileDateToAirdate(EpisodeFile episodeFile, Series series);
}
public class UpdateEpisodeFileService : IUpdateEpisodeFileService,
IExecute<AirDateSeriesCommand>,
IHandle<SeriesScannedEvent>
{
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly ISeriesService _seriesService;
private readonly IEpisodeService _episodeService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public UpdateEpisodeFileService(IDiskProvider diskProvider,
IConfigService configService,
ISeriesService seriesService,
IEpisodeService episodeService,
IEventAggregator eventAggregator,
Logger logger)
{
_diskProvider = diskProvider;
_configService = configService;
_seriesService = seriesService;
_episodeService = episodeService;
_eventAggregator = eventAggregator;
_logger = logger;
}
public void ChangeFileDateToAirdate(EpisodeFile episodeFile, Series series)
{
var episode = new Episode();
episode.AirDate = episodeFile.Episodes.Value.First().AirDate;
episode.EpisodeFile = episodeFile;
episode.EpisodeFileId = 1;
var episodes = new List<Episode>();
episodes.Add(episode);
ChangeFileDateToAirdate(episodes, series);
}
private void ChangeFileDateToAirdate(List<Episode> episodes, Series series)
{
if (!episodes.Any())
{
_logger.ProgressDebug("{0} has no media files available to update with air dates", series.Title);
}
else
{
var done = new List<Episode>();
_logger.ProgressDebug("{0} ... checking {1} media file dates match air date", series.Title, episodes.Count);
foreach (var episode in episodes)
{
if (episode.HasFile
&& episode.EpisodeFile.IsLoaded
&& ChangeFileDate(episode.EpisodeFile.Value.Path, episode.AirDate, series.AirTime))
{
done.Add(episode);
}
}
if (done.Any())
{
_eventAggregator.PublishEvent(new SeriesAirDatedEvent(series));
_logger.ProgressDebug("{0} had {1} of {2} media file dates changed to the date and time the episode aired", series.Title, done.Count, episodes.Count);
}
else
{
_logger.ProgressDebug("{0} has all its media file dates matching the date each aired", series.Title);
}
}
}
public void Execute(AirDateSeriesCommand message)
{
var seriesToAirDate = _seriesService.GetSeries(message.SeriesIds);
foreach (var series in seriesToAirDate)
{
var episodes = _episodeService.EpisodesWithFiles(series.Id);
ChangeFileDateToAirdate(episodes, series);
}
}
public void Handle(SeriesScannedEvent message)
{
if (_configService.FileDateAiredDate)
{
var episodes = _episodeService.EpisodesWithFiles(message.Series.Id);
ChangeFileDateToAirdate(episodes, message.Series);
}
}
private bool ChangeFileDate(String filePath, String fileDate, String fileTime)
{
DateTime dateTime, oldDateTime;
bool result = false;
if (DateTime.TryParse(fileDate + ' ' + fileTime, out dateTime))
{
// avoiding false +ve checks and set date skewing by not using UTC (Windows)
oldDateTime = _diskProvider.GetLastFileWrite(filePath);
if (!DateTime.Equals(dateTime, oldDateTime))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, dateTime);
_diskProvider.FileSetLastAccessTime(filePath, dateTime);
_logger.Info("Date of file [{0}] changed from \"{1}\" to \"{2}\"", filePath, oldDateTime, dateTime);
result = true;
}
catch (Exception ex)
{
_logger.WarnException("Unable to set date of file [" + filePath + "]", ex);
}
}
}
else
{
_logger.Warn("Could not create valid date to set [{0}]", filePath);
}
return result;
}
}
}

@ -78,7 +78,7 @@ namespace NzbDrone.Core.MetadataSource
series.Overview = show.overview;
series.Runtime = show.runtime;
series.Network = show.network;
series.AirTime = show.air_time_utc;
series.AirTime = show.air_time;
series.TitleSlug = show.url.ToLower().Replace("http://trakt.tv/show/", "");
series.Status = GetSeriesStatus(show.status, show.ended);
series.Ratings = GetRatings(show.ratings);

@ -308,6 +308,7 @@
<Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\TrimLogCommand.cs" />
<Compile Include="Instrumentation\DeleteLogFilesService.cs" />
<Compile Include="MediaFiles\Commands\AirDateSeriesCommand.cs" />
<Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" />
<Compile Include="MediaFiles\Commands\RescanSeriesCommand.cs" />
<Compile Include="Lifecycle\Commands\ShutdownCommand.cs" />
@ -318,9 +319,11 @@
<Compile Include="MediaFiles\EpisodeFileMoveResult.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesAirDatedEvent.cs" />
<Compile Include="MediaFiles\MediaFileExtensions.cs" />
<Compile Include="MediaFiles\MediaInfo\VideoFileInfoReader.cs" />
<Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" />
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
<Compile Include="Messaging\Commands\CommandExecutor.cs" />
<Compile Include="Messaging\Commands\ICommandExecutor.cs" />
<Compile Include="Messaging\Commands\IExecute.cs" />

@ -10,7 +10,7 @@ define(
'AddSeries/RootFolders/RootFolderCollection',
'Shared/Toolbar/ToolbarLayout',
'AddSeries/RootFolders/RootFolderLayout',
'Series/Editor/Rename/RenameSeriesView',
'Series/Editor/UpdateFiles/UpdateFilesSeriesView',
'Config'
], function (_,
Marionette,
@ -21,26 +21,26 @@ define(
RootFolders,
ToolbarLayout,
RootFolderLayout,
RenameSeriesView,
UpdateFilesSeriesView,
Config) {
return Marionette.ItemView.extend({
template: 'Series/Editor/SeriesEditorFooterViewTemplate',
ui: {
monitored : '.x-monitored',
qualityProfile: '.x-quality-profiles',
seasonFolder : '.x-season-folder',
rootFolder : '.x-root-folder',
selectedCount : '.x-selected-count',
saveButton : '.x-save',
renameButton : '.x-rename',
container : '.series-editor-footer'
monitored : '.x-monitored',
qualityProfile : '.x-quality-profiles',
seasonFolder : '.x-season-folder',
rootFolder : '.x-root-folder',
selectedCount : '.x-selected-count',
saveButton : '.x-save',
updateFilesButton: '.x-update-files',
container : '.series-editor-footer'
},
events: {
'click .x-save' : '_updateAndSave',
'change .x-root-folder': '_rootFolderChanged',
'click .x-rename' : '_rename'
'click .x-update-files': '_updateFiles'
},
templateHelpers: function () {
@ -119,7 +119,7 @@ define(
this.ui.seasonFolder.attr('disabled', '');
this.ui.rootFolder.attr('disabled', '');
this.ui.saveButton.attr('disabled', '');
this.ui.renameButton.attr('disabled', '');
this.ui.updateFilesButton.attr('disabled', '');
}
else {
@ -128,7 +128,7 @@ define(
this.ui.seasonFolder.removeAttr('disabled', '');
this.ui.rootFolder.removeAttr('disabled', '');
this.ui.saveButton.removeAttr('disabled', '');
this.ui.renameButton.removeAttr('disabled', '');
this.ui.updateFilesButton.removeAttr('disabled', '');
}
},
@ -162,12 +162,12 @@ define(
});
},
_rename: function () {
_updateFiles: function () {
var selected = this.editorGrid.getSelectedModels();
var renameSeriesView = new RenameSeriesView({ series: selected });
this.listenToOnce(renameSeriesView, 'seriesRenamed', this._afterSave);
var updateFilesSeriesView = new UpdateFilesSeriesView({ series: selected });
this.listenToOnce(updateFilesSeriesView, 'updatingFiles', this._afterSave);
vent.trigger(vent.Commands.OpenModalCommand, renameSeriesView);
vent.trigger(vent.Commands.OpenModalCommand, updateFilesSeriesView);
}
});
});

@ -45,7 +45,7 @@
<span class="pull-right">
<span class="selected-count x-selected-count">0 series selected</span>
<button class="btn btn-primary x-save">Save</button>
<button class="btn btn-danger x-rename">Rename</button>
<button class="btn btn-danger x-update-files">Update Files</button>
</span>
</div>
</div>

@ -9,10 +9,11 @@ define(
], function (_, vent, Backbone, Marionette, CommandController) {
return Marionette.ItemView.extend({
template: 'Series/Editor/Rename/RenameSeriesViewTemplate',
template: 'Series/Editor/UpdateFiles/UpdateFilesSeriesViewTemplate',
events: {
'click .x-confirm-rename': '_rename'
'click .x-confirm-rename': '_rename',
'click .x-confirm-airdate': '_setFileAirDate'
},
initialize: function (options) {
@ -29,7 +30,19 @@ define(
seriesIds : seriesIds
});
this.trigger('seriesRenamed');
this.trigger('updatingFiles');
vent.trigger(vent.Commands.CloseModalCommand);
},
_setFileAirDate: function () {
var seriesIds = _.pluck(this.series, 'id');
CommandController.Execute('AirDateSeries', {
name: 'AirDateSeries',
seriesIds: seriesIds
});
this.trigger('updatingFiles');
vent.trigger(vent.Commands.CloseModalCommand);
}
});

@ -1,23 +1,24 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Rename Selected Series</h3>
<h3>Update Files of Selected Series</h3>
</div>
<div class="modal-body rename-series-modal">
<div class="modal-body update-files-series-modal">
<div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">&times;</button>
You can use the rename function for an individual series to preview the rename
Tip: To preview a rename... select "Cancel" then any series title and use the <i data-original-title="" class="icon-nd-rename" title=""></i>
</div>
Are you sure you want to rename all files in the {{numberOfSeries}} selected series?
Are you sure you want to update all files in the {{numberOfSeries}} selected series?
{{debug}}
<ul class="selected-series">
{{#each series}}
<li>{{title}}</li>
<li>{{title}}</li>
{{/each}}
</ul>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-danger x-confirm-rename">rename</button>
<button class="btn btn-danger x-confirm-airdate">set file date to air date</button>
</div>

@ -314,9 +314,13 @@
.row {
margin-left: -40px;
}
.span2 {
width: 160px;
}
}
.rename-series-modal {
.update-files-series-modal {
.selected-series {
margin-top: 15px;
}

@ -21,6 +21,23 @@
</div>
</div>
<div class="control-group">
<label class="control-label">Set File Date to Airdate</label>
<div class="controls">
<label class="checkbox toggle well">
<input type="checkbox" name="fileDateAiredDate" />
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button" />
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Adjust added media file dates to the original episode aired date" />
</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Download Propers</label>

Loading…
Cancel
Save