Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop

pull/23/head
Tim Turner 8 years ago
commit 96332978a0

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored

@ -127,6 +127,9 @@ bin
obj obj
output/* output/*
#Packages
Radarr_*/
Radarr_*.zip
#OS X metadata files #OS X metadata files
._* ._*

@ -7,3 +7,6 @@ script: # the following commands are just examples, use whatever your build p
install: install:
- sudo apt-get install nodejs - sudo apt-get install nodejs
- sudo apt-get install npm - sudo apt-get install npm
after_success:
- chmod +x package.sh
- ./package.sh

@ -0,0 +1,50 @@
if [ $# -eq 0 ]; then
if [ "$TRAVIS_PULL_REQUEST" != false ]; then
echo "Need to supply version argument" && exit;
fi
fi
if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
VERSION="`date +%H:%M:%S`"
YEAR="`date +%Y`"
MONTH="`date +%m`"
DAY="`date +%d`"
else
VERSION=$1
fi
outputFolder='./_output'
outputFolderMono='./_output_mono'
outputFolderOsx='./_output_osx'
outputFolderOsxApp='./_output_osx_app'
tr -d "\r" < $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr > $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2
rm $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr
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_Mono_$VERSION.zip Radarr_Mono_$VERSION >& /dev/null
zip -r Radarr_OSX_$VERSION.zip Radarr_OSX_$VERSION >& /dev/null
ftp -n ftp.leonardogalli.ch << END_SCRIPT
passive
quote USER $FTP_USER
quote PASS $FTP_PASS
mkdir builds
cd builds
mkdir $YEAR
cd $YEAR
mkdir $MONTH
cd $MONTH
mkdir $DAY
cd $DAY
binary
put Radarr_Windows_$VERSION.zip
put Radarr_Mono_$VERSION.zip
put Radarr_OSX_$VERSION.zip
quit
END_SCRIPT

@ -48,6 +48,7 @@ namespace NzbDrone.Api.Movie
public List<string> Genres { get; set; } public List<string> Genres { get; set; }
public HashSet<int> Tags { get; set; } public HashSet<int> Tags { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public AddMovieOptions AddOptions { get; set; }
public Ratings Ratings { get; set; } public Ratings Ratings { get; set; }
//TODO: Add series statistics as a property of the series (instead of individual properties) //TODO: Add series statistics as a property of the series (instead of individual properties)
@ -110,6 +111,7 @@ namespace NzbDrone.Api.Movie
Genres = model.Genres, Genres = model.Genres,
Tags = model.Tags, Tags = model.Tags,
Added = model.Added, Added = model.Added,
AddOptions = model.AddOptions,
Ratings = model.Ratings Ratings = model.Ratings
}; };
} }
@ -152,6 +154,7 @@ namespace NzbDrone.Api.Movie
Genres = resource.Genres, Genres = resource.Genres,
Tags = resource.Tags, Tags = resource.Tags,
Added = resource.Added, Added = resource.Added,
AddOptions = resource.AddOptions,
Ratings = resource.Ratings Ratings = resource.Ratings
}; };
} }
@ -167,6 +170,7 @@ namespace NzbDrone.Api.Movie
movie.RootFolderPath = resource.RootFolderPath; movie.RootFolderPath = resource.RootFolderPath;
movie.Tags = resource.Tags; movie.Tags = resource.Tags;
movie.AddOptions = resource.AddOptions;
return movie; return movie;
} }

@ -40,7 +40,7 @@ namespace NzbDrone.Automation.Test
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start();
driver.Url = "http://localhost:8989"; driver.Url = "http://localhost:7878";
var page = new PageBase(driver); var page = new PageBase(driver);
page.WaitForNoSpinner(); page.WaitForNoSpinner();

@ -49,7 +49,7 @@ namespace NzbDrone.Common.Test
public void GetValue_Success() public void GetValue_Success()
{ {
const string key = "Port"; const string key = "Port";
const string value = "8989"; const string value = "7878";
var result = Subject.GetValue(key, value); var result = Subject.GetValue(key, value);
@ -60,7 +60,7 @@ namespace NzbDrone.Common.Test
public void GetInt_Success() public void GetInt_Success()
{ {
const string key = "Port"; const string key = "Port";
const int value = 8989; const int value = 7878;
var result = Subject.GetValueInt(key, value); var result = Subject.GetValueInt(key, value);
@ -95,7 +95,7 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void GetPort_Success() public void GetPort_Success()
{ {
const int value = 8989; const int value = 7878;
var result = Subject.Port; var result = Subject.Port;

@ -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: 8989) 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);

@ -133,7 +133,7 @@ namespace NzbDrone.Core.Configuration
} }
} }
public int Port => GetValueInt("Port", 8989); public int Port => GetValueInt("Port", 7878);
public int SslPort => GetValueInt("SslPort", 9898); public int SslPort => GetValueInt("SslPort", 9898);
@ -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);
} }
} }

@ -63,7 +63,8 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("Ratings").AsString().Nullable() .WithColumn("Ratings").AsString().Nullable()
.WithColumn("Genres").AsString().Nullable() .WithColumn("Genres").AsString().Nullable()
.WithColumn("Tags").AsString().Nullable() .WithColumn("Tags").AsString().Nullable()
.WithColumn("Certification").AsString().Nullable(); .WithColumn("Certification").AsString().Nullable()
.WithColumn("AddOptions").AsString().Nullable();
Create.TableForModel("Seasons") Create.TableForModel("Seasons")

@ -21,11 +21,12 @@ namespace NzbDrone.Core.Datastore.Migration
//Add HeldReleases //Add HeldReleases
Create.TableForModel("PendingReleases") Create.TableForModel("PendingReleases")
.WithColumn("SeriesId").AsInt32() .WithColumn("SeriesId").AsInt32().WithDefaultValue(0)
.WithColumn("Title").AsString() .WithColumn("Title").AsString()
.WithColumn("Added").AsDateTime() .WithColumn("Added").AsDateTime()
.WithColumn("ParsedEpisodeInfo").AsString() .WithColumn("ParsedEpisodeInfo").AsString()
.WithColumn("Release").AsString(); .WithColumn("Release").AsString()
.WithColumn("MovieId").AsInt32().WithDefaultValue(0);
} }
} }
} }

@ -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);
}
}
}

@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
sw.Stop(); sw.Stop();
_announcer.ElapsedTime(sw.Elapsed); _announcer.ElapsedTime(sw.Elapsed);
} }
} }

@ -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,7 +27,9 @@ 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 Logger _logger; private readonly Logger _logger;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
@ -35,15 +37,19 @@ 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,
Logger logger) Logger logger)
{ {
_configService = configService; _configService = configService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_historyService = historyService; _historyService = historyService;
_downloadedEpisodesImportService = downloadedEpisodesImportService; _downloadedEpisodesImportService = downloadedEpisodesImportService;
_downloadedMovieImportService = downloadedMovieImportService;
_parsingService = parsingService; _parsingService = parsingService;
_movieService = movieService;
_logger = logger; _logger = logger;
_seriesService = seriesService; _seriesService = seriesService;
} }
@ -61,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;
} }
@ -88,19 +94,31 @@ namespace NzbDrone.Core.Download
return; return;
} }
var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title); var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title);
if (series == null) if (series == null)
{ {
if (historyItem != null) if (historyItem != null)
{ {
series = _seriesService.GetSeries(historyItem.SeriesId); //series = _seriesService.GetSeries(historyItem.SeriesId);
} }
if (series == null) if (series == null)
{ {
trackedDownload.Warn("Series title mismatch, automatic import is not possible."); var movie = _parsingService.GetMovie(trackedDownload.DownloadItem.Title);
return;
if (movie == null)
{
movie = _movieService.GetMovie(historyItem.MovieId);
if (movie == null)
{
trackedDownload.Warn("Movie title mismatch, automatic import is not possible.");
}
}
//trackedDownload.Warn("Series title mismatch, automatic import is not possible.");
//return;
} }
} }
} }
@ -111,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.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) if (importResults.Empty())
{ {
trackedDownload.State = TrackedDownloadStage.Imported; trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); return;
return; }
}
if (importResults.Any(c => c.Result != ImportResultType.Imported)) 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
{ {
var statusMessages = importResults var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) if (importResults.Empty())
.ToArray(); {
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
}
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)
}; };
} }

@ -7,6 +7,7 @@ namespace NzbDrone.Core.Download.Pending
public class PendingRelease : ModelBase public class PendingRelease : ModelBase
{ {
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int MovieId { get; set; }
public string Title { get; set; } public string Title { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
@ -14,5 +15,6 @@ namespace NzbDrone.Core.Download.Pending
//Not persisted //Not persisted
public RemoteEpisode RemoteEpisode { get; set; } public RemoteEpisode RemoteEpisode { get; set; }
public RemoteMovie RemoteMovie { get; set; }
} }
} }

@ -344,7 +344,7 @@ namespace NzbDrone.Core.Download.Pending
public void Handle(MovieGrabbedEvent message) public void Handle(MovieGrabbedEvent message)
{ {
//RemoveGrabbed(message.Movie);
} }
public void Handle(RssSyncCompleteEvent message) public void Handle(RssSyncCompleteEvent message)

@ -32,53 +32,112 @@ namespace NzbDrone.Core.Download
public ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions) public ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions)
{ {
var qualifiedReports = GetQualifiedReports(decisions); //var qualifiedReports = GetQualifiedReports(decisions);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
var grabbed = new List<DownloadDecision>(); var grabbed = new List<DownloadDecision>();
var pending = new List<DownloadDecision>(); var pending = new List<DownloadDecision>();
foreach (var report in prioritizedDecisions) foreach (var report in prioritizedDecisions)
{ {
var remoteEpisode = report.RemoteEpisode;
var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList(); if (report.IsForMovie)
//Skip if already grabbed
if (grabbed.SelectMany(r => r.RemoteEpisode.Episodes)
.Select(e => e.Id)
.ToList()
.Intersect(episodeIds)
.Any())
{ {
continue; var remoteMovie = report.RemoteMovie;
}
if (report.TemporarilyRejected) if (report.TemporarilyRejected)
{ {
_pendingReleaseService.Add(report); _pendingReleaseService.Add(report);
pending.Add(report); pending.Add(report);
continue; continue;
} }
if (pending.SelectMany(r => r.RemoteEpisode.Episodes) if (remoteMovie == null || remoteMovie.Movie == null)
.Select(e => e.Id) {
.ToList() continue;
.Intersect(episodeIds) }
.Any())
{
continue;
}
try List<int> movieIds = new List<int> { remoteMovie.Movie.Id };
{
_downloadService.DownloadReport(remoteEpisode);
grabbed.Add(report); //Skip if already grabbed
if (grabbed.Select(r => r.RemoteMovie.Movie)
.Select(e => e.Id)
.ToList()
.Intersect(movieIds)
.Any())
{
continue;
}
if (pending.Select(r => r.RemoteMovie.Movie)
.Select(e => e.Id)
.ToList()
.Intersect(movieIds)
.Any())
{
continue;
}
try
{
_downloadService.DownloadReport(remoteMovie);
grabbed.Add(report);
}
catch (Exception e)
{
//TODO: support for store & forward
//We'll need to differentiate between a download client error and an indexer error
_logger.Warn(e, "Couldn't add report to download queue. " + remoteMovie);
}
} }
catch (Exception e) else
{ {
//TODO: support for store & forward var remoteEpisode = report.RemoteEpisode;
//We'll need to differentiate between a download client error and an indexer error
_logger.Warn(e, "Couldn't add report to download queue. " + remoteEpisode); if (remoteEpisode == null || remoteEpisode.Episodes == null)
{
continue;
}
var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList();
//Skip if already grabbed
if (grabbed.SelectMany(r => r.RemoteEpisode.Episodes)
.Select(e => e.Id)
.ToList()
.Intersect(episodeIds)
.Any())
{
continue;
}
if (report.TemporarilyRejected)
{
_pendingReleaseService.Add(report);
pending.Add(report);
continue;
}
if (pending.SelectMany(r => r.RemoteEpisode.Episodes)
.Select(e => e.Id)
.ToList()
.Intersect(episodeIds)
.Any())
{
continue;
}
try
{
_downloadService.DownloadReport(remoteEpisode);
grabbed.Add(report);
}
catch (Exception e)
{
//TODO: support for store & forward
//We'll need to differentiate between a download client error and an indexer error
_logger.Warn(e, "Couldn't add report to download queue. " + remoteEpisode);
}
} }
} }

@ -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,11 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.IndexerSearch
{
public class MoviesSearchCommand : Command
{
public int MovieId { get; set; }
public override bool SendUpdatesToClient => true;
}
}

@ -0,0 +1,46 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch
{
public class MovieSearchService : IExecute<MoviesSearchCommand>
{
private readonly IMovieService _seriesService;
private readonly ISearchForNzb _nzbSearchService;
private readonly IProcessDownloadDecisions _processDownloadDecisions;
private readonly Logger _logger;
public MovieSearchService(IMovieService seriesService,
ISearchForNzb nzbSearchService,
IProcessDownloadDecisions processDownloadDecisions,
Logger logger)
{
_seriesService = seriesService;
_nzbSearchService = nzbSearchService;
_processDownloadDecisions = processDownloadDecisions;
_logger = logger;
}
public void Execute(MoviesSearchCommand message)
{
var series = _seriesService.GetMovie(message.MovieId);
var downloadedCount = 0;
if (!series.Monitored)
{
_logger.Debug("Movie {0} is not monitored, skipping search", series.Title);
}
var decisions = _nzbSearchService.MovieSearch(message.MovieId, false);//_nzbSearchService.SeasonSearch(message.MovieId, season.SeasonNumber, false, message.Trigger == CommandTrigger.Manual);
downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count;
_logger.ProgressInfo("Movie search completed. {0} reports downloaded.", downloadedCount);
}
}
}

@ -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"));
}*/ }
//LocalMovie nullMovie = null;
decision = new ImportDecision(null, new Rejection("IMPLEMENTATION MISSING!!!")); //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);
}
} }
} }

@ -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();

@ -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" />
@ -564,6 +565,8 @@
<Compile Include="Http\HttpProxySettingsProvider.cs" /> <Compile Include="Http\HttpProxySettingsProvider.cs" />
<Compile Include="Http\TorcacheHttpInterceptor.cs" /> <Compile Include="Http\TorcacheHttpInterceptor.cs" />
<Compile Include="IndexerSearch\Definitions\MovieSearchCriteria.cs" /> <Compile Include="IndexerSearch\Definitions\MovieSearchCriteria.cs" />
<Compile Include="IndexerSearch\MoviesSearchCommand.cs" />
<Compile Include="IndexerSearch\MoviesSearchService.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTv.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTv.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTvSettings.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTvSettings.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTvRequestGenerator.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTvRequestGenerator.cs" />
@ -698,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" />
@ -760,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" />
@ -873,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" />
@ -1094,6 +1110,7 @@
<Compile Include="Tv\SeriesAddedHandler.cs" /> <Compile Include="Tv\SeriesAddedHandler.cs" />
<Compile Include="Tv\MovieRepository.cs" /> <Compile Include="Tv\MovieRepository.cs" />
<Compile Include="Tv\MovieEditedService.cs" /> <Compile Include="Tv\MovieEditedService.cs" />
<Compile Include="Tv\MovieScannedHandler.cs" />
<Compile Include="Tv\SeriesScannedHandler.cs" /> <Compile Include="Tv\SeriesScannedHandler.cs" />
<Compile Include="Tv\SeriesEditedService.cs" /> <Compile Include="Tv\SeriesEditedService.cs" />
<Compile Include="Tv\SeriesRepository.cs" /> <Compile Include="Tv\SeriesRepository.cs" />

@ -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,7 +15,10 @@ 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);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null); RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
@ -98,13 +101,53 @@ 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);
if (parsedEpisodeInfo == null) if (parsedEpisodeInfo == null)
{ {
return _seriesService.FindByTitle(title); //Here we have a problem since it is not possible for movies to find a scene mapping, so these releases are always rejected :( return _seriesService.FindByTitle(title);
} }
var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
@ -118,6 +161,26 @@ namespace NzbDrone.Core.Parser
return series; return series;
} }
public Movie GetMovie(string title)
{
var parsedEpisodeInfo = Parser.ParseTitle(title);
if (parsedEpisodeInfo == null)
{
return _movieService.FindByTitle(title);
}
var series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
if (series == null)
{
series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear,
parsedEpisodeInfo.SeriesTitleInfo.Year);
}
return series;
}
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null)
{ {
var remoteEpisode = new RemoteEpisode var remoteEpisode = new RemoteEpisode

@ -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
{ {
@ -40,11 +41,18 @@ namespace NzbDrone.Core.Tv
public DateTime? InCinemas { get; set; } public DateTime? InCinemas { get; set; }
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()
{ {
return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe()); return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe());
} }
} }
public class AddMovieOptions : MonitoringOptions
{
public bool SearchForMovie { get; set; }
}
} }

@ -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);
}
} }
} }

@ -0,0 +1,57 @@
using NLog;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Tv
{
public class MovieScannedHandler : IHandle<MovieScannedEvent>,
IHandle<MovieScanSkippedEvent>
{
private readonly IMovieService _movieService;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public MovieScannedHandler( IMovieService movieService,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_movieService = movieService;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
private void HandleScanEvents(Movie movie)
{
if (movie.AddOptions == null)
{
//_episodeAddedService.SearchForRecentlyAdded(movie.Id);
return;
}
_logger.Info("[{0}] was recently added, performing post-add actions", movie.Title);
//_episodeMonitoredService.SetEpisodeMonitoredStatus(movie, movie.AddOptions);
if (movie.AddOptions.SearchForMovie)
{
_commandQueueManager.Push(new MoviesSearchCommand { MovieId = movie.Id});
}
movie.AddOptions = null;
_movieService.RemoveAddOptions(movie);
}
public void Handle(MovieScannedEvent message)
{
HandleScanEvents(message.Movie);
}
public void Handle(MovieScanSkippedEvent message)
{
HandleScanEvents(message.Movie);
}
}
}

@ -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,14 +24,17 @@ 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);
List<Movie> UpdateMovie(List<Movie> movie); List<Movie> UpdateMovie(List<Movie> movie);
bool MoviePathExists(string folder); bool MoviePathExists(string folder);
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;
@ -190,5 +195,28 @@ namespace NzbDrone.Core.Tv
return _movieRepository.MoviePathExists(folder); return _movieRepository.MoviePathExists(folder);
} }
public void RemoveAddOptions(Movie movie)
{
_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();
}
} }
} }

@ -31,9 +31,9 @@ namespace NzbDrone.Host
{ {
if (IsAlreadyRunning()) if (IsAlreadyRunning())
{ {
_logger.Warn("Another instance of Sonarr is already running."); _logger.Warn("Another instance of Sonarr or Radarr is already running.");
_browserService.LaunchWebUI(); _browserService.LaunchWebUI();
throw new TerminateApplicationException("Another instance is already running"); //throw new TerminateApplicationException("Another instance is already running"); TODO: detect only radarr
} }
} }

@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test
public override string SeriesRootFolder => GetTempDirectory("SeriesRootFolder"); public override string SeriesRootFolder => GetTempDirectory("SeriesRootFolder");
protected override string RootUrl => "http://localhost:8989/"; protected override string RootUrl => "http://localhost:7878/";
protected override string ApiKey => _runner.ApiKey; protected override string ApiKey => _runner.ApiKey;

@ -159,7 +159,7 @@ namespace NzbDrone.Integration.Test
protected void ConnectSignalR() protected void ConnectSignalR()
{ {
_signalRReceived = new List<SignalRMessage>(); _signalRReceived = new List<SignalRMessage>();
_signalrConnection = new Connection("http://localhost:8989/signalr"); _signalrConnection = new Connection("http://localhost:7878/signalr");
_signalrConnection.Start(new LongPollingTransport()).ContinueWith(task => _signalrConnection.Start(new LongPollingTransport()).ContinueWith(task =>
{ {
if (task.IsFaulted) if (task.IsFaulted)

@ -22,10 +22,10 @@ namespace NzbDrone.Test.Common
public string AppData { get; private set; } public string AppData { get; private set; }
public string ApiKey { get; private set; } public string ApiKey { get; private set; }
public NzbDroneRunner(Logger logger, int port = 8989) public NzbDroneRunner(Logger logger, int port = 7878)
{ {
_processProvider = new ProcessProvider(logger); _processProvider = new ProcessProvider(logger);
_restClient = new RestClient("http://localhost:8989/api"); _restClient = new RestClient("http://localhost:7878/api");
} }
public void Start() public void Start()

@ -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;

@ -120,16 +120,17 @@ module.exports = Marionette.Layout.extend({
}, },
_onMoviesAdded : function(options) { _onMoviesAdded : function(options) {
if (this.isExisting && options.movies.get('path') === this.model.get('folder').path) { if (this.isExisting && options.movie.get('path') === this.model.get('folder').path) {
this.close(); this.close();
} }
else if (!this.isExisting) { else if (!this.isExisting) {
this.collection.term = ''; this.resultCollectionView.setExisting(options.movie.get('imdbId'))
/*this.collection.term = '';
this.collection.reset(); this.collection.reset();
this._clearResults(); this._clearResults();
this.ui.moviesSearch.val(''); this.ui.moviesSearch.val('');
this.ui.moviesSearch.focus(); this.ui.moviesSearch.focus();*/ //TODO: Maybe add option wheter to clear search result.
} }
}, },
@ -143,6 +144,7 @@ module.exports = Marionette.Layout.extend({
}, },
_clearResults : function() { _clearResults : function() {
if (!this.isExisting) { if (!this.isExisting) {
this.searchResult.show(new EmptyView()); this.searchResult.show(new EmptyView());
} else { } else {

@ -21,9 +21,21 @@ module.exports = Marionette.CollectionView.extend({
return this.showing >= this.collection.length; return this.showing >= this.collection.length;
}, },
setExisting : function(imdbid) {
var movies = this.collection.where({ imdbId : imdbid });
console.warn(movies)
//debugger;
if (movies.length > 0) {
this.children.findByModel(movies[0])._configureTemplateHelpers();
//this.children.findByModel(movies[0])._configureTemplateHelpers();
this.children.findByModel(movies[0]).render();
//this.templateHelpers.existing = existingMovies[0].toJSON();
}
},
appendHtml : function(collectionView, itemView, index) { appendHtml : function(collectionView, itemView, index) {
if (!this.isExisting || index < this.showing || index === 0) { if (!this.isExisting || index < this.showing || index === 0) {
collectionView.$el.append(itemView.el); collectionView.$el.append(itemView.el);
} }
} }
}); });

@ -153,14 +153,14 @@ var view = Marionette.ItemView.extend({
}, },
_addWithoutSearch : function() { _addWithoutSearch : function() {
this._addMovies(true); this._addMovies(false);
}, },
_addAndSearch : function() { _addAndSearch : function() {
this._addMovies(true); this._addMovies(true);
}, },
_addMovies : function(searchForMissingEpisodes) { _addMovies : function(searchForMovie) {
var addButton = this.ui.addButton; var addButton = this.ui.addButton;
var addSearchButton = this.ui.addSearchButton; var addSearchButton = this.ui.addSearchButton;
@ -171,7 +171,8 @@ var view = Marionette.ItemView.extend({
var rootFolderPath = this.ui.rootFolder.children(':selected').text(); var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var options = this._getAddMoviesOptions(); var options = this._getAddMoviesOptions();
options.searchForMissingEpisodes = searchForMissingEpisodes; options.searchForMovie = searchForMovie;
console.warn(searchForMovie);
this.model.set({ this.model.set({
profileId : profile, profileId : profile,
@ -186,7 +187,7 @@ var view = Marionette.ItemView.extend({
console.log(this.model.save); console.log(this.model.save);
console.log(promise); console.log(promise);
if (searchForMissingEpisodes) { if (searchForMovie) {
this.ui.addSearchButton.spinForPromise(promise); this.ui.addSearchButton.spinForPromise(promise);
} }

@ -2,7 +2,11 @@
<div class="row"> <div class="row">
<div class="col-md-2"> <div class="col-md-2">
<a href="{{imdbUrl}}" target="_blank"> <a href="{{imdbUrl}}" target="_blank">
{{#if remotePoster}}
{{remotePoster}}
{{else}}
{{poster}} {{poster}}
{{/if}}
</a> </a>
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
@ -74,7 +78,7 @@
<i class="icon-sonarr-add"></i> <i class="icon-sonarr-add"></i>
</button> </button>
<button class="btn btn-success add x-add-search" title="Add and Search for missing episodes"> <button class="btn btn-success add x-add-search" title="Add and Search for movie">
<i class="icon-sonarr-search"></i> <i class="icon-sonarr-search"></i>
</button> </button>
</div> </div>

@ -86,7 +86,7 @@ var singleton = function() {
} }
} }
}); });
console.warn(options)
options.element.startSpin(); options.element.startSpin();
} }
}; };

@ -19,6 +19,22 @@ Handlebars.registerHelper('poster', function() {
return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder)); return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder));
}); });
Handlebars.registerHelper('remotePoster', function() {
var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png';
var poster = this.remotePoster;
if (poster) {
if (!poster.match(/^https?:\/\//)) {
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster, 250)));
} else {
var url = poster.replace(/^https?\:/, 'https://'); //IMDb posters need https to work, k?
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url)));
}
}
return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder));
})
Handlebars.registerHelper('traktUrl', function() { Handlebars.registerHelper('traktUrl', function() {
return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show';
}); });

@ -32,7 +32,7 @@ module.exports = Marionette.Layout.extend({
edit : '.x-edit', edit : '.x-edit',
refresh : '.x-refresh', refresh : '.x-refresh',
rename : '.x-rename', rename : '.x-rename',
search : '.x-search', searchAuto : '.x-search',
poster : '.x-movie-poster', poster : '.x-movie-poster',
manualSearch : '.x-manual-search', manualSearch : '.x-manual-search',
history : '.x-movie-history', history : '.x-movie-history',
@ -86,8 +86,9 @@ module.exports = Marionette.Layout.extend({
name : 'refreshMovie' name : 'refreshMovie'
} }
}); });
CommandController.bindToCommand({ CommandController.bindToCommand({
element : this.ui.search, element : this.ui.searchAuto,
command : { command : {
name : 'moviesSearch' name : 'moviesSearch'
} }

Loading…
Cancel
Save