1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Leonardo Galli 28857e7fac Fixes issue with History table. 2017-01-03 23:59:41 +01:00
Leonardo Galli 6d23fb1b61 Fixes issue with History table not having a movie id field. 2017-01-03 23:57:02 +01:00
Leonardo Galli ad95fbfd4a Implemented importing movies. This is still in early stages, however it should work pretty well. 2017-01-03 23:18:51 +01:00
Leonardo Galli d9d8cbacec Fixes package.sh for OSX builds 2017-01-03 17:35:47 +01:00
Leonardo Galli 80f2adad50 Changes name to Radarr in system tray icon. 2017-01-03 17:35:31 +01:00
Leonardo Galli 7a72f4a05b Fix package.sh 2017-01-03 16:29:49 +01:00
Leonardo Galli 6d7ff628d8 Updated package.sh for Travis 2017-01-03 16:15:13 +01:00
47 changed files with 1700 additions and 70 deletions
Vendored
BIN
View File
Binary file not shown.
+10 -5
View File
@@ -5,8 +5,7 @@ if [ $# -eq 0 ]; then
fi fi
if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
#VERSION="`date +%H:%M:%S`" VERSION="`date +%H:%M:%S`"
VERSION="15-11-15"
YEAR="`date +%Y`" YEAR="`date +%Y`"
MONTH="`date +%m`" MONTH="`date +%m`"
DAY="`date +%d`" DAY="`date +%d`"
@@ -18,15 +17,21 @@ outputFolderMono='./_output_mono'
outputFolderOsx='./_output_osx' outputFolderOsx='./_output_osx'
outputFolderOsxApp='./_output_osx_app' outputFolderOsxApp='./_output_osx_app'
cp -r $outputFolder Radarr_Windows_$VERSION tr -d "\r" < $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr > $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2
cp -r $outputFolderMono Radarr_Mono_$VERSION rm $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr
cp -r $outputFolderOsxApp Radarr_OSX_$VERSION chmod +x $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2
mv $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2 $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr >& error.log
cp -r $outputFolder/ Radarr_Windows_$VERSION
cp -r $outputFolderMono/ Radarr_Mono_$VERSION
cp -r $outputFolderOsxApp/ Radarr_OSX_$VERSION
zip -r Radarr_Windows_$VERSION.zip Radarr_Windows_$VERSION >& /dev/null zip -r Radarr_Windows_$VERSION.zip Radarr_Windows_$VERSION >& /dev/null
zip -r Radarr_Mono_$VERSION.zip Radarr_Mono_$VERSION >& /dev/null zip -r Radarr_Mono_$VERSION.zip Radarr_Mono_$VERSION >& /dev/null
zip -r Radarr_OSX_$VERSION.zip Radarr_OSX_$VERSION >& /dev/null zip -r Radarr_OSX_$VERSION.zip Radarr_OSX_$VERSION >& /dev/null
ftp -n ftp.leonardogalli.ch << END_SCRIPT ftp -n ftp.leonardogalli.ch << END_SCRIPT
passive
quote USER $FTP_USER quote USER $FTP_USER
quote PASS $FTP_PASS quote PASS $FTP_PASS
mkdir builds mkdir builds
+1 -1
View File
@@ -23,7 +23,7 @@ namespace NzbDrone.Console
{ {
System.Console.WriteLine(""); System.Console.WriteLine("");
System.Console.WriteLine(""); System.Console.WriteLine("");
Logger.Fatal(exception.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions"); Logger.Fatal(exception.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions");
System.Console.WriteLine("Press enter to exit..."); System.Console.WriteLine("Press enter to exit...");
System.Console.ReadLine(); System.Console.ReadLine();
Environment.Exit(1); Environment.Exit(1);
@@ -303,12 +303,12 @@ namespace NzbDrone.Core.Configuration
if (contents.IsNullOrWhiteSpace()) if (contents.IsNullOrWhiteSpace())
{ {
throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Sonarr will recreate it."); throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Radarr will recreate it.");
} }
if (contents.All(char.IsControl)) if (contents.All(char.IsControl))
{ {
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it."); throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Radarr will recreate it.");
} }
return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
@@ -323,7 +323,7 @@ namespace NzbDrone.Core.Configuration
catch (XmlException ex) catch (XmlException ex)
{ {
throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Sonarr will recreate it.", ex); throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Radarr will recreate it.", ex);
} }
} }
@@ -0,0 +1,31 @@
using FluentMigrator;
using Marr.Data.Mapping;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Datastore.Extensions;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(104)]
public class add_moviefiles_table : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("MovieFiles")
.WithColumn("MovieId").AsInt32()
.WithColumn("Path").AsString().Unique()
.WithColumn("Quality").AsString()
.WithColumn("Size").AsInt64()
.WithColumn("DateAdded").AsDateTime()
.WithColumn("SceneName").AsString().Nullable()
.WithColumn("MediaInfo").AsString().Nullable()
.WithColumn("ReleaseGroup").AsString().Nullable()
.WithColumn("RelativePath").AsString().Nullable();
Alter.Table("Movies").AddColumn("MovieFileId").AsInt32().WithDefaultValue(0);
}
}
}
@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(105)]
public class fix_history_movieId : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("History")
.AddColumn("MovieId").AsInt32().WithDefaultValue(0);
}
}
}
@@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
sw.Stop(); sw.Stop();
_announcer.ElapsedTime(sw.Elapsed); _announcer.ElapsedTime(sw.Elapsed);
} }
} }
+16 -4
View File
@@ -76,10 +76,7 @@ namespace NzbDrone.Core.Datastore
.Relationship() .Relationship()
.HasOne(s => s.Profile, s => s.ProfileId); .HasOne(s => s.Profile, s => s.ProfileId);
Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath)
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId);
Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles") Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
.Ignore(f => f.Path) .Ignore(f => f.Path)
@@ -89,6 +86,21 @@ namespace NzbDrone.Core.Datastore
query: (db, parent) => db.Query<Episode>().Where(c => c.EpisodeFileId == parent.Id).ToList()) query: (db, parent) => db.Query<Episode>().Where(c => c.EpisodeFileId == parent.Id).ToList())
.HasOne(file => file.Series, file => file.SeriesId); .HasOne(file => file.Series, file => file.SeriesId);
Mapper.Entity<MovieFile>().RegisterModel("MovieFiles")
.Ignore(f => f.Path)
.Relationships.AutoMapICollectionOrComplexProperties()
.For("Movie")
.LazyLoad(condition: parent => parent.Id > 0,
query: (db, parent) => db.Query<Movie>().Where(c => c.MovieFileId == parent.Id).ToList())
.HasOne(file => file.Movie, file => file.MovieId);
Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath)
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId)
.HasOne(m => m.MovieFile, m => m.MovieFileId);
Mapper.Entity<Episode>().RegisterModel("Episodes") Mapper.Entity<Episode>().RegisterModel("Episodes")
.Ignore(e => e.SeriesTitle) .Ignore(e => e.SeriesTitle)
.Ignore(e => e.Series) .Ignore(e => e.Series)
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{ {
DownloadClient = Definition.Name, DownloadClient = Definition.Name,
DownloadId = Definition.Name + "_" + item.DownloadId, DownloadId = Definition.Name + "_" + item.DownloadId,
Category = "sonarr", Category = "Radarr",
Title = item.Title, Title = item.Title,
TotalSize = item.TotalSize, TotalSize = item.TotalSize,
@@ -250,7 +250,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return new NzbDroneValidationFailure("TvCategory", "Category is recommended") return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
{ {
IsWarning = true, IsWarning = true,
DetailedDescription = "Sonarr will not attempt to import completed downloads without a category." DetailedDescription = "Radarr will not attempt to import completed downloads without a category."
}; };
} }
@@ -260,7 +260,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit") return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
{ {
DetailedDescription = "Sonarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." DetailedDescription = "Radarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'."
}; };
} }
} }
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IDownloadedMovieImportService _downloadedMovieImportService;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly Logger _logger; private readonly Logger _logger;
@@ -36,6 +37,7 @@ namespace NzbDrone.Core.Download
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IHistoryService historyService, IHistoryService historyService,
IDownloadedEpisodesImportService downloadedEpisodesImportService, IDownloadedEpisodesImportService downloadedEpisodesImportService,
IDownloadedMovieImportService downloadedMovieImportService,
IParsingService parsingService, IParsingService parsingService,
ISeriesService seriesService, ISeriesService seriesService,
IMovieService movieService, IMovieService movieService,
@@ -45,6 +47,7 @@ namespace NzbDrone.Core.Download
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_historyService = historyService; _historyService = historyService;
_downloadedEpisodesImportService = downloadedEpisodesImportService; _downloadedEpisodesImportService = downloadedEpisodesImportService;
_downloadedMovieImportService = downloadedMovieImportService;
_parsingService = parsingService; _parsingService = parsingService;
_movieService = movieService; _movieService = movieService;
_logger = logger; _logger = logger;
@@ -64,7 +67,7 @@ namespace NzbDrone.Core.Download
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
{ {
trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping."); trackedDownload.Warn("Download wasn't grabbed by Radarr and not in a category, Skipping.");
return; return;
} }
@@ -126,29 +129,59 @@ namespace NzbDrone.Core.Download
private void Import(TrackedDownload trackedDownload) private void Import(TrackedDownload trackedDownload)
{ {
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); if (trackedDownload.RemoteMovie.Movie != null)
if (importResults.Empty())
{ {
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath); var importResults = _downloadedMovieImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem);
return;
if (importResults.Empty())
{
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
}
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= 1)
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
if (importResults.Any(c => c.Result != ImportResultType.Imported))
{
var statusMessages = importResults
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
.ToArray();
trackedDownload.Warn(statusMessages);
}
} }
else
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
{ {
trackedDownload.State = TrackedDownloadStage.Imported; var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
if (importResults.Any(c => c.Result != ImportResultType.Imported)) if (importResults.Empty())
{ {
var statusMessages = importResults trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
.Where(v => v.Result != ImportResultType.Imported) return;
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) }
.ToArray();
trackedDownload.Warn(statusMessages); if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
if (importResults.Any(c => c.Result != ImportResultType.Imported))
{
var statusMessages = importResults
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
.ToArray();
trackedDownload.Warn(statusMessages);
}
} }
} }
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Download
{ {
return new NzbDroneValidationFailure(propertyName, "Folder does not exist") return new NzbDroneValidationFailure(propertyName, "Folder does not exist")
{ {
DetailedDescription = string.Format("The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName) DetailedDescription = string.Format("The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account '{0}', which is used to execute Radarr.", Environment.UserName)
}; };
} }
@@ -142,7 +142,7 @@ namespace NzbDrone.Core.Download
_logger.Error("Folder '{0}' is not writable.", folder); _logger.Error("Folder '{0}' is not writable.", folder);
return new NzbDroneValidationFailure(propertyName, "Unable to write to folder") return new NzbDroneValidationFailure(propertyName, "Unable to write to folder")
{ {
DetailedDescription = string.Format("The folder you specified is not writable. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName) DetailedDescription = string.Format("The folder you specified is not writable. Please verify the folder permissions for the user account '{0}', which is used to execute Radarr.", Environment.UserName)
}; };
} }
@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{ {
_logger.Debug( _logger.Debug(
"{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.",
Definition.Implementation, remoteEpisode.Release.DownloadUrl); Definition.Implementation, remoteEpisode.Release.DownloadUrl);
} }
@@ -302,7 +302,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{ {
_logger.Debug( _logger.Debug(
"{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.",
Definition.Implementation, remoteEpisode.Release.DownloadUrl); Definition.Implementation, remoteEpisode.Release.DownloadUrl);
} }
@@ -371,7 +371,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{ {
_logger.Debug( _logger.Debug(
"{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.",
Definition.Implementation, remoteEpisode.Release.DownloadUrl); Definition.Implementation, remoteEpisode.Release.DownloadUrl);
} }
@@ -402,7 +402,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{ {
_logger.Debug( _logger.Debug(
"{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.",
Definition.Implementation, remoteEpisode.Release.DownloadUrl); Definition.Implementation, remoteEpisode.Release.DownloadUrl);
} }
@@ -0,0 +1,265 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles
{
public interface IDownloadedMovieImportService
{
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
List<ImportResult> ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null);
bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie);
}
public class DownloadedMovieImportService : IDownloadedMovieImportService
{
private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService;
private readonly IMovieService _movieService;
private readonly IParsingService _parsingService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedMovie _importApprovedMovie;
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public DownloadedMovieImportService(IDiskProvider diskProvider,
IDiskScanService diskScanService,
IMovieService movieService,
IParsingService parsingService,
IMakeImportDecision importDecisionMaker,
IImportApprovedMovie importApprovedMovie,
IDetectSample detectSample,
Logger logger)
{
_diskProvider = diskProvider;
_diskScanService = diskScanService;
_movieService = movieService;
_parsingService = parsingService;
_importDecisionMaker = importDecisionMaker;
_importApprovedMovie = importApprovedMovie;
_detectSample = detectSample;
_logger = logger;
}
public List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo)
{
var results = new List<ImportResult>();
foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName))
{
var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null);
results.AddRange(folderResults);
}
foreach (var videoFile in _diskScanService.GetVideoFiles(directoryInfo.FullName, false))
{
var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null);
results.AddRange(fileResults);
}
return results;
}
public List<ImportResult> ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null)
{
if (_diskProvider.FolderExists(path))
{
var directoryInfo = new DirectoryInfo(path);
if (movie == null)
{
return ProcessFolder(directoryInfo, importMode, downloadClientItem);
}
return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem);
}
if (_diskProvider.FileExists(path))
{
var fileInfo = new FileInfo(path);
if (movie == null)
{
return ProcessFile(fileInfo, importMode, downloadClientItem);
}
return ProcessFile(fileInfo, importMode, movie, downloadClientItem);
}
_logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}", path);
return new List<ImportResult>();
}
public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie)
{
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar");
foreach (var videoFile in videoFiles)
{
var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile));
if (episodeParseResult == null)
{
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
return false;
}
var size = _diskProvider.GetFileSize(videoFile);
var quality = QualityParser.ParseQuality(videoFile);
if (!_detectSample.IsSample(movie, quality, videoFile, size, episodeParseResult.IsPossibleSpecialEpisode))
{
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
return false;
}
}
if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes()))
{
_logger.Warn("RAR file detected, will require manual cleanup");
return false;
}
return true;
}
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem)
{
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var movie = _parsingService.GetMovie(cleanedUpName);
if (movie == null)
{
_logger.Debug("Unknown Movie {0}", cleanedUpName);
return new List<ImportResult>
{
UnknownMovieResult("Unknown Movie")
};
}
return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem);
}
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem)
{
if (_movieService.MoviePathExists(directoryInfo.FullName))
{
_logger.Warn("Unable to process folder that is mapped to an existing show");
return new List<ImportResult>();
}
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name);
if (folderInfo != null)
{
_logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality);
}
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
if (downloadClientItem == null)
{
foreach (var videoFile in videoFiles)
{
if (_diskProvider.IsFileLocked(videoFile))
{
return new List<ImportResult>
{
FileIsLockedResult(videoFile)
};
}
}
}
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), movie, folderInfo, true);
var importResults = _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode);
if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) &&
importResults.Any(i => i.Result == ImportResultType.Imported) &&
ShouldDeleteFolder(directoryInfo, movie))
{
_logger.Debug("Deleting folder after importing valid files");
_diskProvider.DeleteFolder(directoryInfo.FullName, true);
}
return importResults;
}
private List<ImportResult> ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem)
{
var movie = _parsingService.GetMovie(Path.GetFileNameWithoutExtension(fileInfo.Name));
if (movie == null)
{
_logger.Debug("Unknown Movie for file: {0}", fileInfo.Name);
return new List<ImportResult>
{
UnknownMovieResult(string.Format("Unknown Movie for file: {0}", fileInfo.Name), fileInfo.FullName)
};
}
return ProcessFile(fileInfo, importMode, movie, downloadClientItem);
}
private List<ImportResult> ProcessFile(FileInfo fileInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem)
{
if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._"))
{
_logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName);
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new Rejection("Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'")
};
}
if (downloadClientItem == null)
{
if (_diskProvider.IsFileLocked(fileInfo.FullName))
{
return new List<ImportResult>
{
FileIsLockedResult(fileInfo.FullName)
};
}
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, movie, null, true);
return _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode);
}
private string GetCleanedUpFolderName(string folder)
{
folder = folder.Replace("_UNPACK_", "")
.Replace("_FAILED_", "");
return folder;
}
private ImportResult FileIsLockedResult(string videoFile)
{
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later");
}
private ImportResult UnknownMovieResult(string message, string videoFile = null)
{
var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile };
return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Movie")), message);
}
}
}
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IDetectSample public interface IDetectSample
{ {
bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial); bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial);
bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial);
} }
public class DetectSample : IDetectSample public class DetectSample : IDetectSample
@@ -79,6 +80,57 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return false; return false;
} }
public bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial)
{
if (isSpecial)
{
_logger.Debug("Special, skipping sample check");
return false;
}
var extension = Path.GetExtension(path);
if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Skipping sample check for .flv file");
return false;
}
if (extension != null && extension.Equals(".strm", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Skipping sample check for .strm file");
return false;
}
try
{
var runTime = _videoFileInfoReader.GetRunTime(path);
var minimumRuntime = GetMinimumAllowedRuntime(movie);
if (runTime.TotalMinutes.Equals(0))
{
_logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path);
return true;
}
if (runTime.TotalSeconds < minimumRuntime)
{
_logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime);
return true;
}
}
catch (DllNotFoundException)
{
_logger.Debug("Falling back to file size detection");
return CheckSize(size, quality);
}
_logger.Debug("Runtime is over 90 seconds");
return false;
}
private bool CheckSize(long size, QualityModel quality) private bool CheckSize(long size, QualityModel quality)
{ {
if (_largeSampleSizeQualities.Contains(quality.Quality)) if (_largeSampleSizeQualities.Contains(quality.Quality))
@@ -99,6 +151,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return false; return false;
} }
private int GetMinimumAllowedRuntime(Movie movie)
{
return 120; //2 minutes
}
private int GetMinimumAllowedRuntime(Series series) private int GetMinimumAllowedRuntime(Series series)
{ {
//Webisodes - 90 seconds //Webisodes - 90 seconds
@@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IImportDecisionEngineSpecification public interface IImportDecisionEngineSpecification
{ {
Decision IsSatisfiedBy(LocalEpisode localEpisode); Decision IsSatisfiedBy(LocalEpisode localEpisode);
Decision IsSatisfiedBy(LocalMovie localMovie);
} }
} }
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Download;
using NzbDrone.Core.Extras;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public interface IImportApprovedMovie
{
List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
}
public class ImportApprovedMovie : IImportApprovedMovie
{
private readonly IUpgradeMediaFiles _episodeFileUpgrader;
private readonly IMediaFileService _mediaFileService;
private readonly IExtraService _extraService;
private readonly IDiskProvider _diskProvider;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public ImportApprovedMovie(IUpgradeMediaFiles episodeFileUpgrader,
IMediaFileService mediaFileService,
IExtraService extraService,
IDiskProvider diskProvider,
IEventAggregator eventAggregator,
Logger logger)
{
_episodeFileUpgrader = episodeFileUpgrader;
_mediaFileService = mediaFileService;
_extraService = extraService;
_diskProvider = diskProvider;
_eventAggregator = eventAggregator;
_logger = logger;
}
public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
{
var qualifiedImports = decisions.Where(c => c.Approved)
.GroupBy(c => c.LocalMovie.Movie.Id, (i, s) => s
.OrderByDescending(c => c.LocalMovie.Quality, new QualityModelComparer(s.First().LocalMovie.Movie.Profile))
.ThenByDescending(c => c.LocalMovie.Size))
.SelectMany(c => c)
.ToList();
var importResults = new List<ImportResult>();
foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalMovie.Size)
.ThenByDescending(e => e.LocalMovie.Size))
{
var localMovie = importDecision.LocalMovie;
var oldFiles = new List<MovieFile>();
try
{
//check if already imported
if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie)
.Select(e => e.Id).Contains(localMovie.Movie.Id))
{
importResults.Add(new ImportResult(importDecision, "Movie has already been imported"));
continue;
}
var episodeFile = new MovieFile();
episodeFile.DateAdded = DateTime.UtcNow;
episodeFile.MovieId = localMovie.Movie.Id;
episodeFile.Path = localMovie.Path.CleanFilePath();
episodeFile.Size = _diskProvider.GetFileSize(localMovie.Path);
episodeFile.Quality = localMovie.Quality;
episodeFile.MediaInfo = localMovie.MediaInfo;
episodeFile.Movie = localMovie.Movie;
episodeFile.ReleaseGroup = localMovie.ParsedEpisodeInfo.ReleaseGroup;
bool copyOnly;
switch (importMode)
{
default:
case ImportMode.Auto:
copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly;
break;
case ImportMode.Move:
copyOnly = false;
break;
case ImportMode.Copy:
copyOnly = true;
break;
}
if (newDownload)
{
episodeFile.SceneName = GetSceneName(downloadClientItem, localMovie);
var moveResult = _episodeFileUpgrader.UpgradeMovieFile(episodeFile, localMovie, copyOnly);
oldFiles = moveResult.OldFiles;
}
else
{
episodeFile.RelativePath = localMovie.Movie.Path.GetRelativePath(episodeFile.Path);
}
_mediaFileService.Add(episodeFile);
importResults.Add(new ImportResult(importDecision));
if (newDownload)
{
//_extraService.ImportExtraFiles(localMovie, episodeFile, copyOnly); TODO update for movie
}
if (downloadClientItem != null)
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly));
}
else
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload));
}
if (newDownload)
{
_eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, episodeFile, oldFiles));
}
}
catch (Exception e)
{
_logger.Warn(e, "Couldn't import episode " + localMovie);
importResults.Add(new ImportResult(importDecision, "Failed to import episode"));
}
}
//Adding all the rejected decisions
importResults.AddRange(decisions.Where(c => !c.Approved)
.Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray())));
return importResults;
}
private string GetSceneName(DownloadClientItem downloadClientItem, LocalMovie localMovie)
{
if (downloadClientItem != null)
{
var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title);
var parsedTitle = Parser.Parser.ParseTitle(title);
if (parsedTitle != null && !parsedTitle.FullSeason)
{
return title;
}
}
var fileName = Path.GetFileNameWithoutExtension(localMovie.Path.CleanFilePath());
if (SceneChecker.IsSceneTitle(fileName))
{
return fileName;
}
return null;
}
}
}
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public class ImportDecision public class ImportDecision
{ {
public LocalEpisode LocalEpisode { get; private set; } public LocalEpisode LocalEpisode { get; private set; }
public LocalMovie LocalMovie { get; private set; }
public IEnumerable<Rejection> Rejections { get; private set; } public IEnumerable<Rejection> Rejections { get; private set; }
public bool Approved => Rejections.Empty(); public bool Approved => Rejections.Empty();
@@ -18,5 +19,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
LocalEpisode = localEpisode; LocalEpisode = localEpisode;
Rejections = rejections.ToList(); Rejections = rejections.ToList();
} }
public ImportDecision(LocalMovie localMovie, params Rejection[] rejections)
{
LocalMovie = localMovie;
Rejections = rejections.ToList();
LocalEpisode = new LocalEpisode
{
Quality = localMovie.Quality,
ExistingFile = localMovie.ExistingFile,
MediaInfo = localMovie.MediaInfo,
ParsedEpisodeInfo = localMovie.ParsedEpisodeInfo,
Path = localMovie.Path,
Size = localMovie.Size
};
}
} }
} }
@@ -98,36 +98,32 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{ {
ImportDecision decision = null; ImportDecision decision = null;
/*try try
{ {
var localEpisode = _parsingService.GetLocalEpisode(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource);
if (localEpisode != null) if (localMovie != null)
{ {
localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, movie); localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie);
localEpisode.Size = _diskProvider.GetFileSize(file); localMovie.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", localEpisode.Size); _logger.Debug("Size: {0}", localMovie.Size);
//TODO: make it so media info doesn't ruin the import process of a new series //TODO: make it so media info doesn't ruin the import process of a new series
if (sceneSource) if (sceneSource)
{ {
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); localMovie.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
} decision = GetDecision(localMovie);
if (localEpisode.Episodes.Empty())
{
decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode"));
} }
else else
{ {
decision = GetDecision(localEpisode); decision = GetDecision(localMovie);
} }
} }
else else
{ {
localEpisode = new LocalEpisode(); var localEpisode = new LocalEpisode();
localEpisode.Path = file; localEpisode.Path = file;
decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file"));
@@ -139,13 +135,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
var localEpisode = new LocalEpisode { Path = file }; var localEpisode = new LocalEpisode { Path = file };
decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file"));
}*/ }
decision = new ImportDecision(null, new Rejection("IMPLEMENTATION MISSING!!!")); //LocalMovie nullMovie = null;
//decision = new ImportDecision(nullMovie, new Rejection("IMPLEMENTATION MISSING!!!"));
return decision; return decision;
} }
private ImportDecision GetDecision(LocalMovie localMovie)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localMovie))
.Where(c => c != null);
return new ImportDecision(localMovie, reasons.ToArray());
}
private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName)
{ {
ImportDecision decision = null; ImportDecision decision = null;
@@ -204,6 +210,33 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return new ImportDecision(localEpisode, reasons.ToArray()); return new ImportDecision(localEpisode, reasons.ToArray());
} }
private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalMovie localMovie)
{
try
{
var result = spec.IsSatisfiedBy(localMovie);
if (!result.Accepted)
{
return new Rejection(result.Reason);
}
}
catch (NotImplementedException e)
{
_logger.Warn(e, "Spec " + spec.ToString() + " currently does not implement evaluation for movies.");
return null;
}
catch (Exception e)
{
//e.Data.Add("report", remoteEpisode.Report.ToJson());
//e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson());
_logger.Error(e, "Couldn't evaluate decision on " + localMovie.Path);
return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));
}
return null;
}
private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode) private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode)
{ {
try try
@@ -292,6 +325,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}) == 1; }) == 1;
} }
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Movie movie)
{
if (UseFolderQuality(folderInfo, fileQuality, movie))
{
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
return folderInfo.Quality;
}
return fileQuality;
}
private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
{ {
if (UseFolderQuality(folderInfo, fileQuality, series)) if (UseFolderQuality(folderInfo, fileQuality, series))
@@ -303,6 +347,31 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return fileQuality; return fileQuality;
} }
private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Movie movie)
{
if (folderInfo == null)
{
return false;
}
if (folderInfo.Quality.Quality == Quality.Unknown)
{
return false;
}
if (fileQuality.QualitySource == QualitySource.Extension)
{
return true;
}
if (new QualityModelComparer(movie.Profile).Compare(folderInfo.Quality, fileQuality) > 0)
{
return true;
}
return false;
}
private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series)
{ {
if (folderInfo == null) if (folderInfo == null)
@@ -63,5 +63,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept(); return Decision.Accept();
} }
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
if (_configService.SkipFreeSpaceCheckWhenImporting)
{
_logger.Debug("Skipping free space check when importing");
return Decision.Accept();
}
try
{
if (localMovie.ExistingFile)
{
_logger.Debug("Skipping free space check for existing episode");
return Decision.Accept();
}
var path = Directory.GetParent(localMovie.Movie.Path);
var freeSpace = _diskProvider.GetAvailableSpace(path.FullName);
if (!freeSpace.HasValue)
{
_logger.Debug("Free space check returned an invalid result for: {0}", path);
return Decision.Accept();
}
if (freeSpace < localMovie.Size + 100.Megabytes())
{
_logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size);
return Decision.Reject("Not enough free space");
}
}
catch (DirectoryNotFoundException ex)
{
_logger.Error("Unable to check free disk space while importing. " + ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to check free disk space while importing: " + localMovie.Path);
}
return Decision.Accept();
}
} }
} }
@@ -17,11 +17,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{ {
if (localEpisode.ParsedEpisodeInfo.FullSeason) if (localEpisode.ParsedEpisodeInfo.FullSeason)
{ {
//_logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah _logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah
//return Decision.Reject("Single episode file contains all episodes in seasons"); return Decision.Reject("Single episode file contains all episodes in seasons");
} }
return Decision.Accept(); return Decision.Accept();
} }
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
} }
} }
@@ -1,4 +1,5 @@
using System.IO; using System;
using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
@@ -14,6 +15,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{ {
_logger = logger; _logger = logger;
} }
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
if (localMovie.ExistingFile)
{
return Decision.Accept();
}
var dirInfo = new FileInfo(localMovie.Path).Directory;
if (dirInfo == null)
{
return Decision.Accept();
}
var folderInfo = Parser.Parser.ParseTitle(dirInfo.Name);
if (folderInfo == null)
{
return Decision.Accept();
}
if (folderInfo.FullSeason)
{
return Decision.Accept();
}
return Decision.Accept();
}
public Decision IsSatisfiedBy(LocalEpisode localEpisode) public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{ {
if (localEpisode.ExistingFile) if (localEpisode.ExistingFile)
@@ -37,5 +37,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept(); return Decision.Accept();
} }
public Decision IsSatisfiedBy(LocalMovie localEpisode)
{
if (localEpisode.ExistingFile)
{
_logger.Debug("Existing file, skipping sample check");
return Decision.Accept();
}
var sample = _detectSample.IsSample(localEpisode.Movie,
localEpisode.Quality,
localEpisode.Path,
localEpisode.Size,
false);
if (sample)
{
return Decision.Reject("Sample");
}
return Decision.Accept();
}
} }
} }
@@ -56,5 +56,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept(); return Decision.Accept();
} }
public Decision IsSatisfiedBy(LocalMovie localEpisode)
{
if (localEpisode.ExistingFile)
{
_logger.Debug("{0} is in series folder, skipping unpacking check", localEpisode.Path);
return Decision.Accept();
}
foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|'))
{
DirectoryInfo parent = Directory.GetParent(localEpisode.Path);
while (parent != null)
{
if (parent.Name.StartsWith(workingFolder))
{
if (OsInfo.IsNotWindows)
{
_logger.Debug("{0} is still being unpacked", localEpisode.Path);
return Decision.Reject("File is still being unpacked");
}
if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5))
{
_logger.Debug("{0} appears to be unpacking still", localEpisode.Path);
return Decision.Reject("File is still being unpacked");
}
}
parent = parent.Parent;
}
}
return Decision.Accept();
}
} }
} }
@@ -1,3 +1,4 @@
using System;
using NLog; using NLog;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -27,5 +28,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger.Debug("Episode file on disk contains more episodes than this file contains"); _logger.Debug("Episode file on disk contains more episodes than this file contains");
return Decision.Reject("Episode file on disk contains more episodes than this file contains"); return Decision.Reject("Episode file on disk contains more episodes than this file contains");
} }
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
} }
} }
@@ -1,4 +1,5 @@
using System.Linq; using System;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger; _logger = logger;
} }
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
public Decision IsSatisfiedBy(LocalEpisode localEpisode) public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{ {
if (localEpisode.ExistingFile) if (localEpisode.ExistingFile)
@@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept(); return Decision.Accept();
} }
public Decision IsSatisfiedBy(LocalMovie localEpisode)
{
return Decision.Accept();
}
} }
} }
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieDownloadedEvent : IEvent
{
public LocalMovie Movie { get; private set; }
public MovieFile MovieFile { get; private set; }
public List<MovieFile> OldFiles { get; private set; }
public MovieDownloadedEvent(LocalMovie episode, MovieFile episodeFile, List<MovieFile> oldFiles)
{
Movie = episode;
MovieFile = episodeFile;
OldFiles = oldFiles;
}
}
}
@@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieFileAddedEvent : IEvent
{
public MovieFile MovieFile { get; private set; }
public MovieFileAddedEvent(MovieFile episodeFile)
{
MovieFile = episodeFile;
}
}
}
@@ -0,0 +1,16 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieFileDeletedEvent : IEvent
{
public MovieFile MovieFile { get; private set; }
public DeleteMediaFileReason Reason { get; private set; }
public MovieFileDeletedEvent(MovieFile episodeFile, DeleteMediaFileReason reason)
{
MovieFile = episodeFile;
Reason = reason;
}
}
}
@@ -0,0 +1,20 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieFolderCreatedEvent : IEvent
{
public Movie Movie { get; private set; }
public MovieFile MovieFile { get; private set; }
public string SeriesFolder { get; set; }
public string SeasonFolder { get; set; }
public string MovieFolder { get; set; }
public MovieFolderCreatedEvent(Movie movie, MovieFile episodeFile)
{
Movie = movie;
MovieFile = episodeFile;
}
}
}
@@ -0,0 +1,32 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieImportedEvent : IEvent
{
public LocalMovie MovieInfo { get; private set; }
public MovieFile ImportedMovie { get; private set; }
public bool NewDownload { get; private set; }
public string DownloadClient { get; private set; }
public string DownloadId { get; private set; }
public bool IsReadOnly { get; set; }
public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload)
{
MovieInfo = episodeInfo;
ImportedMovie = importedMovie;
NewDownload = newDownload;
}
public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload, string downloadClient, string downloadId, bool isReadOnly)
{
MovieInfo = episodeInfo;
ImportedMovie = importedMovie;
NewDownload = newDownload;
DownloadClient = downloadClient;
DownloadId = downloadId;
IsReadOnly = isReadOnly;
}
}
}
@@ -7,16 +7,20 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Common; using NzbDrone.Common;
using System;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
public interface IMediaFileService public interface IMediaFileService
{ {
MovieFile Add(MovieFile episodeFile);
void Update(MovieFile episodeFile);
void Delete(MovieFile episodeFile, DeleteMediaFileReason reason);
EpisodeFile Add(EpisodeFile episodeFile); EpisodeFile Add(EpisodeFile episodeFile);
void Update(EpisodeFile episodeFile); void Update(EpisodeFile episodeFile);
void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason);
List<EpisodeFile> GetFilesBySeries(int seriesId); List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesByMovie(int movieId); List<MovieFile> GetFilesByMovie(int movieId);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber); List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFilesWithoutMediaInfo(); List<EpisodeFile> GetFilesWithoutMediaInfo();
List<string> FilterExistingFiles(List<string> files, Series series); List<string> FilterExistingFiles(List<string> files, Series series);
@@ -30,12 +34,14 @@ namespace NzbDrone.Core.MediaFiles
{ {
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileRepository _mediaFileRepository; private readonly IMediaFileRepository _mediaFileRepository;
private readonly IMovieFileRepository _movieFileRepository;
private readonly Logger _logger; private readonly Logger _logger;
public MediaFileService(IMediaFileRepository mediaFileRepository, IEventAggregator eventAggregator, Logger logger) public MediaFileService(IMediaFileRepository mediaFileRepository, IMovieFileRepository movieFileRepository, IEventAggregator eventAggregator, Logger logger)
{ {
_mediaFileRepository = mediaFileRepository; _mediaFileRepository = mediaFileRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_movieFileRepository = movieFileRepository;
_logger = logger; _logger = logger;
} }
@@ -61,14 +67,24 @@ namespace NzbDrone.Core.MediaFiles
_eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason));
} }
public void Delete(MovieFile episodeFile, DeleteMediaFileReason reason)
{
//Little hack so we have the episodes and series attached for the event consumers
episodeFile.Movie.LazyLoad();
episodeFile.Path = Path.Combine(episodeFile.Movie.Value.Path, episodeFile.RelativePath);
_movieFileRepository.Delete(episodeFile);
_eventAggregator.PublishEvent(new MovieFileDeletedEvent(episodeFile, reason));
}
public List<EpisodeFile> GetFilesBySeries(int seriesId) public List<EpisodeFile> GetFilesBySeries(int seriesId)
{ {
return _mediaFileRepository.GetFilesBySeries(seriesId); return _mediaFileRepository.GetFilesBySeries(seriesId);
} }
public List<EpisodeFile> GetFilesByMovie(int movieId) public List<MovieFile> GetFilesByMovie(int movieId)
{ {
return _mediaFileRepository.GetFilesBySeries(movieId); //TODO: Update implementation for movie files. return _movieFileRepository.GetFilesByMovie(movieId); //TODO: Update implementation for movie files.
} }
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber) public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber)
@@ -114,5 +130,18 @@ namespace NzbDrone.Core.MediaFiles
var files = GetFilesBySeries(message.Series.Id); var files = GetFilesBySeries(message.Series.Id);
_mediaFileRepository.DeleteMany(files); _mediaFileRepository.DeleteMany(files);
} }
public MovieFile Add(MovieFile episodeFile)
{
var addedFile = _movieFileRepository.Insert(episodeFile);
_eventAggregator.PublishEvent(new MovieFileAddedEvent(addedFile));
return addedFile;
}
public void Update(MovieFile episodeFile)
{
_movieFileRepository.Update(episodeFile);
}
} }
} }
+29
View File
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Marr.Data;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.MediaFiles
{
public class MovieFile : ModelBase
{
public int MovieId { get; set; }
public string RelativePath { get; set; }
public string Path { get; set; }
public long Size { get; set; }
public DateTime DateAdded { get; set; }
public string SceneName { get; set; }
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public LazyLoaded<Movie> Movie { get; set; }
public override string ToString()
{
return string.Format("[{0}] {1}", Id, RelativePath);
}
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace NzbDrone.Core.MediaFiles
{
public class MovieFileMoveResult
{
public MovieFileMoveResult()
{
OldFiles = new List<MovieFile>();
}
public MovieFile MovieFile { get; set; }
public List<MovieFile> OldFiles { get; set; }
}
}
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles
{
public interface IMoveMovieFiles
{
MovieFile MoveMovieFile(MovieFile movieFile, Movie movie);
MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie);
MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie);
}
public class MovieFileMovingService : IMoveMovieFiles
{
private readonly IMovieService _movieService;
private readonly IUpdateMovieFileService _updateMovieFileService;
private readonly IBuildFileNames _buildFileNames;
private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider;
private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _configService;
private readonly Logger _logger;
public MovieFileMovingService(IMovieService movieService,
IUpdateMovieFileService updateMovieFileService,
IBuildFileNames buildFileNames,
IDiskTransferService diskTransferService,
IDiskProvider diskProvider,
IMediaFileAttributeService mediaFileAttributeService,
IEventAggregator eventAggregator,
IConfigService configService,
Logger logger)
{
_movieService = movieService;
_updateMovieFileService = updateMovieFileService;
_buildFileNames = buildFileNames;
_diskTransferService = diskTransferService;
_diskProvider = diskProvider;
_mediaFileAttributeService = mediaFileAttributeService;
_eventAggregator = eventAggregator;
_configService = configService;
_logger = logger;
}
public MovieFile MoveMovieFile(MovieFile movieFile, Movie movie)
{
var newFileName = _buildFileNames.BuildFileName(movie, movieFile);
var filePath = _buildFileNames.BuildFilePath(movie, newFileName, Path.GetExtension(movieFile.RelativePath));
EnsureMovieFolder(movieFile, movie, filePath);
_logger.Debug("Renaming movie file: {0} to {1}", movieFile, filePath);
return TransferFile(movieFile, movie, filePath, TransferMode.Move);
}
public MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie)
{
var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile);
var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path));
EnsureMovieFolder(movieFile, localMovie, filePath);
_logger.Debug("Moving movie file: {0} to {1}", movieFile.Path, filePath);
return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Move);
}
public MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie)
{
var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile);
var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path));
EnsureMovieFolder(movieFile, localMovie, filePath);
if (_configService.CopyUsingHardlinks)
{
_logger.Debug("Hardlinking movie file: {0} to {1}", movieFile.Path, filePath);
return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.HardLinkOrCopy);
}
_logger.Debug("Copying movie file: {0} to {1}", movieFile.Path, filePath);
return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Copy);
}
private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinationFilePath, TransferMode mode)
{
Ensure.That(movieFile, () => movieFile).IsNotNull();
Ensure.That(movie,() => movie).IsNotNull();
Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath();
var movieFilePath = movieFile.Path ?? Path.Combine(movie.Path, movieFile.RelativePath);
if (!_diskProvider.FileExists(movieFilePath))
{
throw new FileNotFoundException("Movie file path does not exist", movieFilePath);
}
if (movieFilePath == destinationFilePath)
{
throw new SameFilenameException("File not moved, source and destination are the same", movieFilePath);
}
_diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode);
movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath);
_updateMovieFileService.ChangeFileDateForFile(movieFile, movie);
try
{
_mediaFileAttributeService.SetFolderLastWriteTime(movie.Path, movieFile.DateAdded);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to set last write time");
}
_mediaFileAttributeService.SetFilePermissions(destinationFilePath);
return movieFile;
}
private void EnsureMovieFolder(MovieFile movieFile, LocalMovie localMovie, string filePath)
{
EnsureMovieFolder(movieFile, localMovie.Movie, filePath);
}
private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath)
{
var movieFolder = Path.GetDirectoryName(filePath);
var rootFolder = new OsPath(movieFolder).Directory.FullPath;
if (!_diskProvider.FolderExists(rootFolder))
{
throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder));
}
var changed = false;
var newEvent = new MovieFolderCreatedEvent(movie, movieFile);
if (!_diskProvider.FolderExists(movieFolder))
{
CreateFolder(movieFolder);
newEvent.SeriesFolder = movieFolder;
changed = true;
}
if (changed)
{
_eventAggregator.PublishEvent(newEvent);
}
}
private void CreateFolder(string directoryName)
{
Ensure.That(directoryName, () => directoryName).IsNotNullOrWhiteSpace();
var parentFolder = new OsPath(directoryName).Directory.FullPath;
if (!_diskProvider.FolderExists(parentFolder))
{
CreateFolder(parentFolder);
}
try
{
_diskProvider.CreateFolder(directoryName);
}
catch (IOException ex)
{
_logger.Error(ex, "Unable to create directory: " + directoryName);
}
_mediaFileAttributeService.SetFolderPermissions(directoryName);
}
}
}
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.MediaFiles
{
public interface IMovieFileRepository : IBasicRepository<MovieFile>
{
List<MovieFile> GetFilesByMovie(int movieId);
List<MovieFile> GetFilesWithoutMediaInfo();
}
public class MovieFileRepository : BasicRepository<MovieFile>, IMovieFileRepository
{
public MovieFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<MovieFile> GetFilesByMovie(int movieId)
{
return Query.Where(c => c.MovieId == movieId).ToList();
}
public List<MovieFile> GetFilesWithoutMediaInfo()
{
return Query.Where(c => c.MediaInfo == null).ToList();
}
}
}
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Exceptron;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles
{
public interface IUpdateMovieFileService
{
void ChangeFileDateForFile(MovieFile movieFile, Movie movie);
}
public class UpdateMovieFileService : IUpdateMovieFileService,
IHandle<SeriesScannedEvent>
{
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
public UpdateMovieFileService(IDiskProvider diskProvider,
IConfigService configService,
IMovieService movieService,
Logger logger)
{
_diskProvider = diskProvider;
_configService = configService;
_movieService = movieService;
_logger = logger;
}
public void ChangeFileDateForFile(MovieFile movieFile, Movie movie)
{
ChangeFileDate(movieFile, movie);
}
private bool ChangeFileDate(MovieFile movieFile, Movie movie)
{
var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
return false;
}
public void Handle(SeriesScannedEvent message)
{
if (_configService.FileDate == FileDateType.None)
{
return;
}
/* var movies = _movieService.MoviesWithFiles(message.Series.Id);
var movieFiles = new List<MovieFile>();
var updated = new List<MovieFile>();
foreach (var group in movies.GroupBy(e => e.MovieFileId))
{
var moviesInFile = group.Select(e => e).ToList();
var movieFile = moviesInFile.First().MovieFile;
movieFiles.Add(movieFile);
if (ChangeFileDate(movieFile, message.Series, moviesInFile))
{
updated.Add(movieFile);
}
}
if (updated.Any())
{
_logger.ProgressDebug("Changed file date for {0} files of {1} in {2}", updated.Count, movieFiles.Count, message.Series.Title);
}
else
{
_logger.ProgressDebug("No file dates changed for {0}", message.Series.Title);
}*/
}
private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime)
{
DateTime airDate;
if (DateTime.TryParse(fileDate + ' ' + fileTime, out airDate))
{
// avoiding false +ve checks and set date skewing by not using UTC (Windows)
DateTime oldDateTime = _diskProvider.FileGetLastWrite(filePath);
if (!DateTime.Equals(airDate, oldDateTime))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, airDate);
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldDateTime, airDate);
return true;
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]");
}
}
}
else
{
_logger.Debug("Could not create valid date to change file [{0}]", filePath);
}
return false;
}
private bool ChangeFileDateToUtcAirDate(string filePath, DateTime airDateUtc)
{
DateTime oldLastWrite = _diskProvider.FileGetLastWrite(filePath);
if (!DateTime.Equals(airDateUtc, oldLastWrite))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, airDateUtc);
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDateUtc);
return true;
}
catch (Exception ex)
{
ex.ExceptronIgnoreOnMono();
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]");
}
}
return false;
}
}
}
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles
public interface IUpgradeMediaFiles public interface IUpgradeMediaFiles
{ {
EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false);
MovieFileMoveResult UpgradeMovieFile(MovieFile movieFile, LocalMovie localMovie, bool copyOnly = false);
} }
public class UpgradeMediaFileService : IUpgradeMediaFiles public class UpgradeMediaFileService : IUpgradeMediaFiles
@@ -16,22 +17,59 @@ namespace NzbDrone.Core.MediaFiles
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IMoveEpisodeFiles _episodeFileMover; private readonly IMoveEpisodeFiles _episodeFileMover;
private readonly IMoveMovieFiles _movieFileMover;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly Logger _logger; private readonly Logger _logger;
public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IMoveEpisodeFiles episodeFileMover, IMoveEpisodeFiles episodeFileMover,
IMoveMovieFiles movieFileMover,
IDiskProvider diskProvider, IDiskProvider diskProvider,
Logger logger) Logger logger)
{ {
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_episodeFileMover = episodeFileMover; _episodeFileMover = episodeFileMover;
_movieFileMover = movieFileMover;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_logger = logger; _logger = logger;
} }
public MovieFileMoveResult UpgradeMovieFile(MovieFile episodeFile, LocalMovie localEpisode, bool copyOnly = false)
{
var moveFileResult = new MovieFileMoveResult();
var existingFile = localEpisode.Movie.MovieFile;
if (existingFile.IsLoaded)
{
var file = existingFile.Value;
var episodeFilePath = Path.Combine(localEpisode.Movie.Path, file.RelativePath);
if (_diskProvider.FileExists(episodeFilePath))
{
_logger.Debug("Removing existing episode file: {0}", file);
_recycleBinProvider.DeleteFile(episodeFilePath);
}
moveFileResult.OldFiles.Add(file);
_mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade);
}
if (copyOnly)
{
moveFileResult.MovieFile = _movieFileMover.CopyMovieFile(episodeFile, localEpisode);
}
else
{
moveFileResult.MovieFile= _movieFileMover.MoveMovieFile(episodeFile, localEpisode);
}
return moveFileResult;
}
public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false)
{ {
var moveFileResult = new EpisodeFileMoveResult(); var moveFileResult = new EpisodeFileMoveResult();
+14
View File
@@ -249,6 +249,7 @@
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" /> <Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
<Compile Include="Datastore\Migration\069_quality_proper.cs" /> <Compile Include="Datastore\Migration\069_quality_proper.cs" />
<Compile Include="Datastore\Migration\070_delay_profile.cs" /> <Compile Include="Datastore\Migration\070_delay_profile.cs" />
<Compile Include="Datastore\Migration\104_add_moviefiles_table.cs" />
<Compile Include="Datastore\Migration\096_disable_kickass.cs" /> <Compile Include="Datastore\Migration\096_disable_kickass.cs" />
<Compile Include="Datastore\Migration\095_add_additional_episodes_index.cs" /> <Compile Include="Datastore\Migration\095_add_additional_episodes_index.cs" />
<Compile Include="Datastore\Migration\103_fix_metadata_file_extensions.cs" /> <Compile Include="Datastore\Migration\103_fix_metadata_file_extensions.cs" />
@@ -700,6 +701,17 @@
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" /> <Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" /> <Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
<Compile Include="MediaFiles\Commands\RescanMovieCommand.cs" /> <Compile Include="MediaFiles\Commands\RescanMovieCommand.cs" />
<Compile Include="MediaFiles\DownloadedMovieCommandService.cs" />
<Compile Include="MediaFiles\MovieFileMovingService.cs" />
<Compile Include="MediaFiles\Events\MovieDownloadedEvent.cs" />
<Compile Include="MediaFiles\Events\MovieFileAddedEvent.cs" />
<Compile Include="MediaFiles\Events\MovieFileDeletedEvent.cs" />
<Compile Include="MediaFiles\Events\MovieFolderCreatedEvent.cs" />
<Compile Include="MediaFiles\Events\MovieImportedEvent.cs" />
<Compile Include="MediaFiles\MovieFileRepository.cs" />
<Compile Include="MediaFiles\MovieFileMoveResult.cs" />
<Compile Include="MediaFiles\MovieFile.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportApprovedMovie.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportMode.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportMode.cs" />
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" /> <Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
<Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" /> <Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" />
@@ -762,6 +774,7 @@
<Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" /> <Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" />
<Compile Include="MediaFiles\RenameEpisodeFileService.cs" /> <Compile Include="MediaFiles\RenameEpisodeFileService.cs" />
<Compile Include="MediaFiles\SameFilenameException.cs" /> <Compile Include="MediaFiles\SameFilenameException.cs" />
<Compile Include="MediaFiles\UpdateMovieFileService.cs" />
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" /> <Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" /> <Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" /> <Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
@@ -875,6 +888,7 @@
<Compile Include="Parser\IsoLanguage.cs" /> <Compile Include="Parser\IsoLanguage.cs" />
<Compile Include="Parser\IsoLanguages.cs" /> <Compile Include="Parser\IsoLanguages.cs" />
<Compile Include="Parser\LanguageParser.cs" /> <Compile Include="Parser\LanguageParser.cs" />
<Compile Include="Parser\Model\LocalMovie.cs" />
<Compile Include="Parser\Model\RemoteMovie.cs" /> <Compile Include="Parser\Model\RemoteMovie.cs" />
<Compile Include="Profiles\Delay\DelayProfile.cs" /> <Compile Include="Profiles\Delay\DelayProfile.cs" />
<Compile Include="Profiles\Delay\DelayProfileService.cs" /> <Compile Include="Profiles\Delay\DelayProfileService.cs" />
+92 -1
View File
@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Organizer
public interface IBuildFileNames public interface IBuildFileNames
{ {
string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null);
string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null);
string BuildFilePath(Movie movie, string fileName, string extension);
string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension);
string BuildSeasonPath(Series series, int seasonNumber); string BuildSeasonPath(Series series, int seasonNumber);
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
@@ -137,6 +139,66 @@ namespace NzbDrone.Core.Organizer
return fileName; return fileName;
} }
public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null)
{
if (namingConfig == null)
{
namingConfig = _namingConfigService.GetConfig();
}
if (!namingConfig.RenameEpisodes)
{
return GetOriginalTitle(movieFile);
}
/*if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard)
{
throw new NamingFormatException("Standard episode format cannot be empty");
}
if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily)
{
throw new NamingFormatException("Daily episode format cannot be empty");
}
if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime)
{
throw new NamingFormatException("Anime episode format cannot be empty");
}*/
/*var pattern = namingConfig.StandardEpisodeFormat;
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList();
if (series.SeriesType == SeriesTypes.Daily)
{
pattern = namingConfig.DailyEpisodeFormat;
}
if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue))
{
pattern = namingConfig.AnimeEpisodeFormat;
}
pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig);
pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig);
AddSeriesTokens(tokenHandlers, series);
AddEpisodeTokens(tokenHandlers, episodes);
AddEpisodeFileTokens(tokenHandlers, episodeFile);
AddQualityTokens(tokenHandlers, series, episodeFile);
AddMediaInfoTokens(tokenHandlers, episodeFile);
var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim();
fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString());
fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty);*/
//TODO: Update namingConfig for Movies!
return GetOriginalTitle(movieFile);
}
public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension)
{ {
Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace();
@@ -146,6 +208,15 @@ namespace NzbDrone.Core.Organizer
return Path.Combine(path, fileName + extension); return Path.Combine(path, fileName + extension);
} }
public string BuildFilePath(Movie movie, string fileName, string extension)
{
Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace();
var path = movie.Path;
return Path.Combine(path, fileName + extension);
}
public string BuildSeasonPath(Series series, int seasonNumber) public string BuildSeasonPath(Series series, int seasonNumber)
{ {
var path = series.Path; var path = series.Path;
@@ -246,7 +317,7 @@ namespace NzbDrone.Core.Organizer
public string GetMovieFolder(Movie movie) public string GetMovieFolder(Movie movie)
{ {
return CleanFolderName(Parser.Parser.CleanSeriesTitle(movie.Title)); return CleanFolderName(movie.Title);
} }
public static string CleanTitle(string title) public static string CleanTitle(string title)
@@ -774,6 +845,26 @@ namespace NzbDrone.Core.Organizer
return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); return Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
} }
private string GetOriginalTitle(MovieFile episodeFile)
{
if (episodeFile.SceneName.IsNullOrWhiteSpace())
{
return GetOriginalFileName(episodeFile);
}
return episodeFile.SceneName;
}
private string GetOriginalFileName(MovieFile episodeFile)
{
if (episodeFile.RelativePath.IsNullOrWhiteSpace())
{
return Path.GetFileNameWithoutExtension(episodeFile.Path);
}
return Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
}
} }
internal sealed class TokenMatch internal sealed class TokenMatch
@@ -0,0 +1,29 @@
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Parser.Model
{
public class LocalMovie
{
public LocalMovie()
{
}
public string Path { get; set; }
public long Size { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
public Movie Movie { get; set; }
public QualityModel Quality { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public bool ExistingFile { get; set; }
public override string ToString()
{
return Path;
}
}
}
@@ -15,6 +15,8 @@ namespace NzbDrone.Core.Parser
{ {
LocalEpisode GetLocalEpisode(string filename, Series series); LocalEpisode GetLocalEpisode(string filename, Series series);
LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
LocalMovie GetLocalMovie(string filename, Movie movie);
LocalMovie GetLocalMovie(string filename, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource);
Series GetSeries(string title); Series GetSeries(string title);
Movie GetMovie(string title); Movie GetMovie(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
@@ -99,6 +101,46 @@ namespace NzbDrone.Core.Parser
}; };
} }
public LocalMovie GetLocalMovie(string filename, Movie movie)
{
return GetLocalMovie(filename, movie, null, false);
}
public LocalMovie GetLocalMovie(string filename, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
ParsedEpisodeInfo parsedEpisodeInfo;
if (folderInfo != null)
{
parsedEpisodeInfo = folderInfo.JsonClone();
parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename));
}
else
{
parsedEpisodeInfo = Parser.ParsePath(filename);
}
if (parsedEpisodeInfo == null)
{
if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename)))
{
_logger.Warn("Unable to parse episode info from path {0}", filename);
}
return null;
}
return new LocalMovie
{
Movie = movie,
Quality = parsedEpisodeInfo.Quality,
Path = filename,
ParsedEpisodeInfo = parsedEpisodeInfo,
ExistingFile = movie.Path.IsParentPath(filename)
};
}
public Series GetSeries(string title) public Series GetSeries(string title)
{ {
var parsedEpisodeInfo = Parser.ParseTitle(title); var parsedEpisodeInfo = Parser.ParseTitle(title);
+3
View File
@@ -4,6 +4,7 @@ using Marr.Data;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles;
using NzbDrone.Core.MediaFiles;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@@ -41,6 +42,8 @@ namespace NzbDrone.Core.Tv
public LazyLoaded<Profile> Profile { get; set; } public LazyLoaded<Profile> Profile { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public AddMovieOptions AddOptions { get; set; } public AddMovieOptions AddOptions { get; set; }
public LazyLoaded<MovieFile> MovieFile { get; set; }
public int MovieFileId { get; set; }
public override string ToString() public override string ToString()
{ {
+16 -1
View File
@@ -1,4 +1,6 @@
using System.Linq; using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -11,6 +13,8 @@ namespace NzbDrone.Core.Tv
Movie FindByTitle(string cleanTitle); Movie FindByTitle(string cleanTitle);
Movie FindByTitle(string cleanTitle, int year); Movie FindByTitle(string cleanTitle, int year);
Movie FindByImdbId(string imdbid); Movie FindByImdbId(string imdbid);
List<Movie> GetMoviesByFileId(int fileId);
void SetFileId(int fileId, int movieId);
} }
public class MovieRepository : BasicRepository<Movie>, IMovieRepository public class MovieRepository : BasicRepository<Movie>, IMovieRepository
@@ -46,5 +50,16 @@ namespace NzbDrone.Core.Tv
{ {
return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault(); return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault();
} }
public List<Movie> GetMoviesByFileId(int fileId)
{
return Query.Where(m => m.MovieFileId == fileId).ToList();
}
public void SetFileId(int episodeId, int fileId)
{
SetFields(new Movie { Id = episodeId, MovieFileId = fileId }, movie => movie.MovieFileId);
}
} }
} }
+24 -1
View File
@@ -10,6 +10,8 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
@@ -22,6 +24,7 @@ namespace NzbDrone.Core.Tv
Movie FindByTitle(string title); Movie FindByTitle(string title);
Movie FindByTitle(string title, int year); Movie FindByTitle(string title, int year);
Movie FindByTitleInexact(string title); Movie FindByTitleInexact(string title);
Movie GetMovieByFileId(int fileId);
void DeleteMovie(int movieId, bool deleteFiles); void DeleteMovie(int movieId, bool deleteFiles);
List<Movie> GetAllMovies(); List<Movie> GetAllMovies();
Movie UpdateMovie(Movie movie); Movie UpdateMovie(Movie movie);
@@ -30,7 +33,8 @@ namespace NzbDrone.Core.Tv
void RemoveAddOptions(Movie movie); void RemoveAddOptions(Movie movie);
} }
public class MovieService : IMovieService public class MovieService : IMovieService, IHandle<MovieFileAddedEvent>,
IHandle<MovieFileDeletedEvent>
{ {
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
@@ -195,5 +199,24 @@ namespace NzbDrone.Core.Tv
{ {
_movieRepository.SetFields(movie, s => s.AddOptions); _movieRepository.SetFields(movie, s => s.AddOptions);
} }
public void Handle(MovieFileAddedEvent message)
{
_movieRepository.SetFileId(message.MovieFile.Id, message.MovieFile.Movie.Value.Id);
_logger.Debug("Linking [{0}] > [{1}]", message.MovieFile.RelativePath, message.MovieFile.Movie.Value);
}
public void Handle(MovieFileDeletedEvent message)
{
var movie = _movieRepository.GetMoviesByFileId(message.MovieFile.Id).First();
movie.MovieFileId = 0;
UpdateMovie(movie);
}
public Movie GetMovieByFileId(int fileId)
{
return _movieRepository.GetMoviesByFileId(fileId).First();
}
} }
} }
+1 -1
View File
@@ -38,7 +38,7 @@ namespace NzbDrone.SysTray
_trayMenu.MenuItems.Add("-"); _trayMenu.MenuItems.Add("-");
_trayMenu.MenuItems.Add("Exit", OnExit); _trayMenu.MenuItems.Add("Exit", OnExit);
_trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version); _trayIcon.Text = string.Format("Radarr - {0}", BuildInfo.Version);
_trayIcon.Icon = Properties.Resources.NzbDroneIcon; _trayIcon.Icon = Properties.Resources.NzbDroneIcon;
_trayIcon.ContextMenu = _trayMenu; _trayIcon.ContextMenu = _trayMenu;