1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-10 15:10:57 -04:00

Compare commits

...

43 Commits

Author SHA1 Message Date
Leonardo Galli
a63587bb19 Fix alternative titles, if there is only one. 2017-01-05 14:54:48 +01:00
Leonardo Galli
eb46343ce8 Fix History not picking up imported movie. 2017-01-05 14:50:12 +01:00
Leonardo Galli
8fa43fb9f7 Fix for Path column in MovieFiles table. 2017-01-05 14:36:58 +01:00
Leonardo Galli
6d5e9ad4a1 Fixed matching wrong movies from database when searching. 2017-01-05 13:23:22 +01:00
Leonardo Galli
70c8228605 Updated Newznab to correctly identify movie search capabilities 2017-01-05 13:16:23 +01:00
Leonardo Galli
82470bd995 Fixed an issue where tracked downloads would not be found in the database due to alternative titles. 2017-01-05 12:39:48 +01:00
Leonardo Galli
a263558383 Fix parsing of special editions without . in title. 2017-01-05 11:36:26 +01:00
Leonardo Galli
7d82e35650 Fixes some strings still saying episode. 2017-01-05 11:28:22 +01:00
Leonardo Galli
8616bcedd4 Fixes Deluge for movies. 2017-01-05 11:04:29 +01:00
Leonardo Galli
2c66322121 Fixes default Category of QBittorent client. 2017-01-05 10:59:37 +01:00
Leonardo Galli
1402bc883e Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2017-01-05 10:50:35 +01:00
Leonardo Galli
1325822798 Added the TMDB Configuration service. This allows Image urls to be dynamically generated! 2017-01-05 10:49:55 +01:00
Mike
7a786d4c0e Fixes all usenet download clients. (#25)
* Remove confusing abstract method from UsenetClientBase.

* Fix Sabnzbd.

* Put back abstract method.

* Fix usenet download clients.
2017-01-05 01:06:18 +01:00
Leonardo Galli
87c7afac16 Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2017-01-04 23:20:52 +01:00
Leonardo Galli
06d39579a5 Fixed some things regarding director's cut / special edition. 2017-01-04 23:20:48 +01:00
Leonardo Galli
c8ea0a73e2 Merge pull request #24 from AeonLucid/develop
Better folder name.
2017-01-04 23:01:44 +01:00
Leonardo Galli
eeb3c88131 Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2017-01-04 22:59:39 +01:00
Leonardo Galli
fd718b61ac Updated Parser to parse movie titles. Should also parse things, such as: Director's Cut, Special Edition, etc. This is then displayed in the manual search UI. Importing is not yet updated for the new parser! 2017-01-04 22:59:34 +01:00
AeonLucid
4a3b2a0014 Better folder name. 2017-01-04 22:54:45 +01:00
Tim Turner
cbd87dcc38 Merge pull request #23 from fedoranimus/develop
Fix newznab indexers and nzbget
2017-01-04 15:55:19 -05:00
Tim Turner
956de03a62 Fixed up Newznab indexers
Hacky way to remove the "tt" from an imdbid - need to come up with a
better way.
2017-01-04 15:48:30 -05:00
Tim Turner
2b74098040 Merge remote-tracking branch 'refs/remotes/origin/develop' into galli-leo/develop 2017-01-04 14:55:25 -05:00
Leonardo Galli
402a9e1ee0 Fixes some issues when adding movies caused by TMDB. 2017-01-04 20:52:59 +01:00
Leonardo Galli
e68653463d Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2017-01-04 20:34:37 +01:00
Leonardo Galli
0715962ec5 TheMovieDB.org is now used as metadata source. 2017-01-04 20:27:14 +01:00
Leonardo Galli
5668a3bcfd Merge pull request #22 from AeonLucid/develop
Replace colon in movie path.
2017-01-04 18:42:01 +01:00
AeonLucid
bd241dcbe0 Replace colon in movie path. 2017-01-04 18:23:54 +01:00
Leonardo Galli
69786b3968 Fixes OSX Application name. 2017-01-04 15:05:14 +01:00
Leonardo Galli
6e2ded5d33 Fixed an issue where sometimes the json returned from IMDb just was not parsed correctly for some misterious reason. 2017-01-04 15:01:38 +01:00
Leonardo Galli
b47d5f7fa1 Update readme.md 2017-01-04 12:15:56 +01:00
Tim Turner
90ff73d45f Update NzbGet & NewznabSettings to support movies 2017-01-03 18:33:16 -05:00
Tim Turner
c2c7015f39 Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop 2017-01-03 18:32:31 -05:00
Tim Turner
579602419e Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop 2017-01-03 18:01:17 -05:00
Leonardo Galli
28857e7fac Fixes issue with History table. 2017-01-03 23:59:41 +01:00
Leonardo Galli
6d23fb1b61 Fixes issue with History table not having a movie id field. 2017-01-03 23:57:02 +01:00
Tim Turner
96332978a0 Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop 2017-01-03 17:42:54 -05:00
Leonardo Galli
ad95fbfd4a Implemented importing movies. This is still in early stages, however it should work pretty well. 2017-01-03 23:18:51 +01:00
Tim Turner
1488c0a0fc Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop 2017-01-03 16:54:20 -05:00
Leonardo Galli
d9d8cbacec Fixes package.sh for OSX builds 2017-01-03 17:35:47 +01:00
Leonardo Galli
80f2adad50 Changes name to Radarr in system tray icon. 2017-01-03 17:35:31 +01:00
Leonardo Galli
7a72f4a05b Fix package.sh 2017-01-03 16:29:49 +01:00
Leonardo Galli
6d7ff628d8 Updated package.sh for Travis 2017-01-03 16:15:13 +01:00
Tim Turner
a3dfa05f25 Merge remote-tracking branch 'refs/remotes/galli-leo/develop' into develop 2017-01-02 12:24:56 -05:00
103 changed files with 2888 additions and 241 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -192,8 +192,8 @@ PackageOsxApp()
rm -rf $outputFolderOsxApp
mkdir $outputFolderOsxApp
cp -r ./osx/Sonarr.app $outputFolderOsxApp
cp -r $outputFolderOsx $outputFolderOsxApp/Sonarr.app/Contents/MacOS
cp -r ./osx/Radarr.app $outputFolderOsxApp
cp -r $outputFolderOsx $outputFolderOsxApp/Radarr.app/Contents/MacOS
echo "##teamcity[progressFinish 'Creating OS X App Package']"
}

View File

@@ -5,8 +5,7 @@ if [ $# -eq 0 ]; then
fi
if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
#VERSION="`date +%H:%M:%S`"
VERSION="15-11-15"
VERSION="`date +%H:%M:%S`"
YEAR="`date +%Y`"
MONTH="`date +%m`"
DAY="`date +%d`"
@@ -18,15 +17,21 @@ outputFolderMono='./_output_mono'
outputFolderOsx='./_output_osx'
outputFolderOsxApp='./_output_osx_app'
cp -r $outputFolder Radarr_Windows_$VERSION
cp -r $outputFolderMono Radarr_Mono_$VERSION
cp -r $outputFolderOsxApp Radarr_OSX_$VERSION
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

View File

@@ -3,19 +3,25 @@
This fork of Sonarr aims to turn it into something like Couchpotato.
## Currently working:
* Adding new movies (Note: Movies are currently added as one series with one season and one episode. This will change in the future)
* Adding new movies
* Manually searching for releases of movies.
* Automatically searching for releases.
* Rarbg.to indexer (Other indexers are coming, I just need to find the right categories)
* Everything that has nothing to do with series from Sonarr should be working as well.
* QBittorrent download client (Other clients are coming)
## Planned Features:
* Scanning PreDB to know when a new release is available.
* Fixing the other Indexers.
* Fixing how movies are stored and displayed.
* Fixing the other Indexers and download clients.
* Fixing how movies are parsed.
* Fixing movie import.
* Importing of Sonarr config.
* New TorrentPotato Indexer.
## Download
The latest precompiled binary versions can be found here: https://github.com/galli-leo/Radarr/releases.
For more up to date versions (but also sometimes broken), daily builds can be found here: https://leonardogalli.ch/radarr/builds/.
## Major Features Include: ##
* Support for major platforms: Windows, Linux, OSX, Raspberry Pi, etc.
@@ -30,13 +36,10 @@ This fork of Sonarr aims to turn it into something like Couchpotato.
* Full support for specials and multi-episode releases
* And a beautiful UI
## Download
The latest precompiled binary versions can be found here: https://github.com/galli-leo/Radarr/releases.
## Configuring Development Environment: ##
### Requirements ###
- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) or Mono
- [Git](http://git-scm.com/downloads)
- [NodeJS](http://nodejs.org/download/)
@@ -52,7 +55,7 @@ The latest precompiled binary versions can be found here: https://github.com/gal
### Development ###
- Open `NzbDrone.sln` in Visual Studio
- Open `NzbDrone.sln` in Visual Studio or run the build.sh script, if Mono is installed.
- Make sure `NzbDrone.Console` is set as the startup project

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Api.Indexers
public string Indexer { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public string Edition { get; set; }
public string Title { get; set; }
public bool FullSeason { get; set; }
public int SeasonNumber { get; set; }
@@ -90,6 +91,55 @@ namespace NzbDrone.Api.Indexers
if (model.IsForMovie)
{
downloadAllowed = model.RemoteMovie.DownloadAllowed;
var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo;
return new ReleaseResource
{
Guid = releaseInfo.Guid,
Quality = parsedMovieInfo.Quality,
//QualityWeight
Age = releaseInfo.Age,
AgeHours = releaseInfo.AgeHours,
AgeMinutes = releaseInfo.AgeMinutes,
Size = releaseInfo.Size,
IndexerId = releaseInfo.IndexerId,
Indexer = releaseInfo.Indexer,
ReleaseGroup = parsedMovieInfo.ReleaseGroup,
ReleaseHash = parsedMovieInfo.ReleaseHash,
Title = releaseInfo.Title,
FullSeason = parsedMovieInfo.FullSeason,
SeasonNumber = parsedMovieInfo.SeasonNumber,
Language = parsedMovieInfo.Language,
AirDate = "",
SeriesTitle = parsedMovieInfo.MovieTitle,
EpisodeNumbers = new int[0],
AbsoluteEpisodeNumbers = new int[0],
Approved = model.Approved,
TemporarilyRejected = model.TemporarilyRejected,
Rejected = model.Rejected,
TvdbId = releaseInfo.TvdbId,
TvRageId = releaseInfo.TvRageId,
Rejections = model.Rejections.Select(r => r.Reason).ToList(),
PublishDate = releaseInfo.PublishDate,
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = downloadAllowed,
//ReleaseWeight
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,
Seeders = torrentInfo.Seeders,
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
Protocol = releaseInfo.DownloadProtocol,
Edition = parsedMovieInfo.Edition,
IsDaily = false,
IsAbsoluteNumbering = false,
IsPossibleSpecialEpisode = false,
Special = parsedMovieInfo.Special,
};
}
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?)

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Api.Movie
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.ImdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator);
PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator);
PutValidator.RuleFor(s => s.Path).IsValidPath();
}

View File

@@ -28,6 +28,7 @@ namespace NzbDrone.Api.Movie
public string Overview { get; set; }
public DateTime? InCinemas { get; set; }
public List<MediaCover> Images { get; set; }
public string Website { get; set; }
public string RemotePoster { get; set; }
public int Year { get; set; }
@@ -42,6 +43,7 @@ namespace NzbDrone.Api.Movie
public DateTime? LastInfoSync { get; set; }
public string CleanTitle { get; set; }
public string ImdbId { get; set; }
public int TmdbId { get; set; }
public string TitleSlug { get; set; }
public string RootFolderPath { get; set; }
public string Certification { get; set; }
@@ -50,6 +52,7 @@ namespace NzbDrone.Api.Movie
public DateTime Added { get; set; }
public AddMovieOptions AddOptions { get; set; }
public Ratings Ratings { get; set; }
public List<string> AlternativeTitles { get; set; }
//TODO: Add series statistics as a property of the series (instead of individual properties)
@@ -79,7 +82,7 @@ namespace NzbDrone.Api.Movie
return new MovieResource
{
Id = model.Id,
TmdbId = model.TmdbId,
Title = model.Title,
//AlternateTitles
SortTitle = model.SortTitle,
@@ -108,10 +111,12 @@ namespace NzbDrone.Api.Movie
TitleSlug = model.TitleSlug,
RootFolderPath = model.RootFolderPath,
Certification = model.Certification,
Website = model.Website,
Genres = model.Genres,
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,
AlternativeTitles = model.AlternativeTitles,
Ratings = model.Ratings
};
}
@@ -123,6 +128,7 @@ namespace NzbDrone.Api.Movie
return new Core.Tv.Movie
{
Id = resource.Id,
TmdbId = resource.TmdbId,
Title = resource.Title,
//AlternateTitles
@@ -151,10 +157,12 @@ namespace NzbDrone.Api.Movie
TitleSlug = resource.TitleSlug,
RootFolderPath = resource.RootFolderPath,
Certification = resource.Certification,
Website = resource.Website,
Genres = resource.Genres,
Tags = resource.Tags,
Added = resource.Added,
AddOptions = resource.AddOptions,
AlternativeTitles = resource.AlternativeTitles,
Ratings = resource.Ratings
};
}
@@ -162,6 +170,7 @@ namespace NzbDrone.Api.Movie
public static Core.Tv.Movie ToModel(this MovieResource resource, Core.Tv.Movie movie)
{
movie.ImdbId = resource.ImdbId;
movie.TmdbId = resource.TmdbId;
movie.Path = resource.Path;
movie.ProfileId = resource.ProfileId;

View File

@@ -6,6 +6,8 @@ namespace NzbDrone.Common.Cloud
{
IHttpRequestBuilderFactory Services { get; }
IHttpRequestBuilderFactory SkyHookTvdb { get; }
IHttpRequestBuilderFactory TMDB { get; }
IHttpRequestBuilderFactory TMDBSingle { get; }
}
public class SonarrCloudRequestBuilder : ISonarrCloudRequestBuilder
@@ -18,10 +20,20 @@ namespace NzbDrone.Common.Cloud
SkyHookTvdb = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/{route}/{language}/")
.SetSegment("language", "en")
.CreateFactory();
TMDB = new HttpRequestBuilder("https://api.themoviedb.org/3/{route}/{id}{secondaryRoute}")
.AddQueryParam("api_key", "1a7373301961d03f97f853a876dd1212")
.CreateFactory();
TMDBSingle = new HttpRequestBuilder("https://api.themoviedb.org/3/{route}")
.AddQueryParam("api_key", "1a7373301961d03f97f853a876dd1212")
.CreateFactory();
}
public IHttpRequestBuilderFactory Services { get; private set; }
public IHttpRequestBuilderFactory SkyHookTvdb { get; private set; }
public IHttpRequestBuilderFactory TMDB { get; private set; }
public IHttpRequestBuilderFactory TMDBSingle { get; private set; }
}
}

View File

@@ -23,7 +23,7 @@ namespace NzbDrone.Console
{
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(exception.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions");
Logger.Fatal(exception.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions");
System.Console.WriteLine("Press enter to exit...");
System.Console.ReadLine();
Environment.Exit(1);

View File

@@ -303,12 +303,12 @@ namespace NzbDrone.Core.Configuration
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))
{
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));
@@ -323,7 +323,7 @@ namespace NzbDrone.Core.Configuration
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);
}
}

View File

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

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(105)]
public class fix_history_movieId : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("History")
.AddColumn("MovieId").AsInt32().WithDefaultValue(0);
}
}
}

View File

@@ -0,0 +1,21 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(106)]
public class add_tmdb_stuff : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Movies")
.AddColumn("TmdbId").AsInt32().WithDefaultValue(0);
Alter.Table("Movies")
.AddColumn("Website").AsString().Nullable();
Alter.Table("Movies")
.AlterColumn("ImdbId").AsString().Nullable();
Alter.Table("Movies")
.AddColumn("AlternativeTitles").AsString().Nullable();
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(107)]
public class fix_movie_files : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("MovieFiles").AlterColumn("Path").AsString().Nullable(); //Should be deleted, but to much work, ¯\_(ツ)_/¯
}
}
}

View File

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

View File

@@ -76,10 +76,7 @@ namespace NzbDrone.Core.Datastore
.Relationship()
.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")
.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())
.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")
.Ignore(e => e.SeriesTitle)
.Ignore(e => e.Series)

View File

@@ -66,9 +66,9 @@ namespace NzbDrone.Core.DecisionEngine
try
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title);
var parsedEpisodeInfo = Parser.Parser.ParseMovieTitle(report.Title);
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace())
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.MovieTitle.IsNullOrWhiteSpace())
{
RemoteMovie remoteEpisode = _parsingService.Map(parsedEpisodeInfo, "", searchCriteria);
remoteEpisode.Release = report;

View File

@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
ScanGracePeriod = TimeSpan.FromSeconds(30);
}
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents)
{
var title = remoteEpisode.Release.Title;
@@ -42,7 +42,25 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
using (var stream = _diskProvider.OpenWriteStream(filepath))
{
stream.Write(fileContent, 0, fileContent.Length);
stream.Write(fileContents, 0, fileContents.Length);
}
_logger.Debug("NZB Download succeeded, saved to: {0}", filepath);
return null;
}
protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
var title = remoteMovie.Release.Title;
title = FileNameBuilder.CleanFileName(title);
var filepath = Path.Combine(Settings.NzbFolder, title + ".nzb");
using (var stream = _diskProvider.OpenWriteStream(filepath))
{
stream.Write(fileContents, 0, fileContents.Length);
}
_logger.Debug("NZB Download succeeded, saved to: {0}", filepath);
@@ -60,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
DownloadClient = Definition.Name,
DownloadId = Definition.Name + "_" + item.DownloadId,
Category = "sonarr",
Category = "Radarr",
Title = item.Title,
TotalSize = item.TotalSize,

View File

@@ -31,6 +31,50 @@ namespace NzbDrone.Core.Download.Clients.Deluge
_proxy = proxy;
}
protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
}
_proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings);
/*var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First)
{
_proxy.MoveTorrentToTopInQueue(actualHash, Settings);
}*/
return actualHash.ToUpper();
}
protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, byte[] fileContent)
{
var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings);
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);
}
_proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings);
/*var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First)
{
_proxy.MoveTorrentToTopInQueue(actualHash, Settings);
}*/
return actualHash.ToUpper();
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
Host = "localhost";
Port = 8112;
Password = "deluge";
TvCategory = "tv-sonarr";
TvCategory = "movie-radarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@@ -40,7 +40,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]

View File

@@ -29,11 +29,25 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
_proxy = proxy;
}
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents)
{
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings);
var response = _proxy.DownloadNzb(fileContent, filename, priority, Settings);
if (response == null)
{
throw new DownloadClientException("Failed to add nzb {0}", filename);
}
return response;
}
protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
var priority = Settings.RecentTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings);
if (response == null)
{

View File

@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
_proxy = proxy;
}
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents)
{
var category = Settings.TvCategory;
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings);
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings);
if (response == null)
{
@@ -44,6 +44,21 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return response;
}
protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
var category = Settings.TvCategory; // TODO: Update this to MovieCategory?
var priority = Settings.RecentTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings);
if(response == null)
{
throw new DownloadClientException("Failed to add nzb {0}", filename);
}
return response;
}
private IEnumerable<DownloadClientItem> GetQueue()
{
NzbgetGlobalStatus globalStatus;
@@ -72,13 +87,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
var queueItem = new DownloadClientItem();
queueItem.DownloadId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
queueItem.Title = item.NzbName;
queueItem.TotalSize = totalSize;
queueItem.Category = item.Category;
queueItem.DownloadClient = Definition.Name;
var queueItem = new DownloadClientItem()
{
DownloadId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(),
Title = item.NzbName,
TotalSize = totalSize,
Category = item.Category,
DownloadClient = Definition.Name
};
if (globalStatus.DownloadPaused || remainingSize == pausedSize && remainingSize != 0)
{
queueItem.Status = DownloadItemStatus.Paused;
@@ -131,17 +147,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
var historyItem = new DownloadClientItem();
historyItem.DownloadClient = Definition.Name;
historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);
historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir));
historyItem.Category = item.Category;
historyItem.Message = string.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus);
historyItem.Status = DownloadItemStatus.Completed;
historyItem.RemainingTime = TimeSpan.Zero;
var historyItem = new DownloadClientItem()
{
DownloadClient = Definition.Name,
DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(),
Title = item.Name,
TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo),
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir)),
Category = item.Category,
Message = string.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus),
Status = DownloadItemStatus.Completed,
RemainingTime = TimeSpan.Zero
};
if (item.DeleteStatus == "MANUAL")
{
continue;

View File

@@ -26,7 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
Host = "localhost";
Port = 6789;
TvCategory = "tv";
TvCategory = "Movies";
Username = "nzbget";
Password = "tegbzn6789";
RecentTvPriority = (int)NzbgetPriority.Normal;
OlderTvPriority = (int)NzbgetPriority.Normal;
}
@@ -44,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }

View File

@@ -250,7 +250,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
{
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")
{
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'."
};
}
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
Host = "localhost";
Port = 9091;
TvCategory = "tv-sonarr";
TvCategory = "movie-radarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@@ -37,9 +37,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
//Todo: update this shit.
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }

View File

@@ -32,12 +32,27 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
// patch can be a number (releases) or 'x' (git)
private static readonly Regex VersionRegex = new Regex(@"(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+|x)(?<candidate>.*)", RegexOptions.Compiled);
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent)
protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents)
{
var category = Settings.TvCategory;
var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings);
var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings);
if (response != null && response.Ids.Any())
{
return response.Ids.First();
}
return null;
}
protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
var category = Settings.TvCategory;
var priority = Settings.RecentTvPriority;
var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings);
if (response != null && response.Ids.Any())
{

View File

@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download
private readonly IEventAggregator _eventAggregator;
private readonly IHistoryService _historyService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IDownloadedMovieImportService _downloadedMovieImportService;
private readonly IParsingService _parsingService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
@@ -36,6 +37,7 @@ namespace NzbDrone.Core.Download
IEventAggregator eventAggregator,
IHistoryService historyService,
IDownloadedEpisodesImportService downloadedEpisodesImportService,
IDownloadedMovieImportService downloadedMovieImportService,
IParsingService parsingService,
ISeriesService seriesService,
IMovieService movieService,
@@ -45,6 +47,7 @@ namespace NzbDrone.Core.Download
_eventAggregator = eventAggregator;
_historyService = historyService;
_downloadedEpisodesImportService = downloadedEpisodesImportService;
_downloadedMovieImportService = downloadedMovieImportService;
_parsingService = parsingService;
_movieService = movieService;
_logger = logger;
@@ -64,7 +67,7 @@ namespace NzbDrone.Core.Download
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;
}
@@ -126,29 +129,59 @@ namespace NzbDrone.Core.Download
private void Import(TrackedDownload trackedDownload)
{
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
if (importResults.Empty())
if (trackedDownload.RemoteMovie.Movie != null)
{
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
var importResults = _downloadedMovieImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem);
if (importResults.Empty())
{
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
}
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= 1)
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
if (importResults.Any(c => c.Result != ImportResultType.Imported))
{
var statusMessages = importResults
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
.ToArray();
trackedDownload.Warn(statusMessages);
}
}
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
else
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
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();
if (importResults.Empty())
{
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);
}
}
}

View File

@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Download
{
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);
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)
};
}

View File

@@ -1,6 +1,5 @@
using System;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Instrumentation.Extensions;

View File

@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{
_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);
}
@@ -302,7 +302,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{
_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);
}
@@ -371,7 +371,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{
_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);
}
@@ -402,7 +402,7 @@ namespace NzbDrone.Core.Download
if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash)
{
_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);
}

View File

@@ -57,12 +57,17 @@ namespace NzbDrone.Core.Download.TrackedDownloads
try
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
var parsedMovieInfo = Parser.Parser.ParseMovieTitle(trackedDownload.DownloadItem.Title);
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId);
if (parsedMovieInfo != null)
{
trackedDownload.RemoteMovie = _parsingService.Map(parsedMovieInfo, "", null);
}
if (parsedEpisodeInfo != null)
{
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
trackedDownload.RemoteMovie = _parsingService.Map(parsedEpisodeInfo, "", null);
}
if (historyItems.Any())

View File

@@ -30,12 +30,9 @@ namespace NzbDrone.Core.Download
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent);
protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents);
protected virtual string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
throw new NotImplementedException();
}
protected abstract string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents);
public override string Download(RemoteEpisode remoteEpisode)
{

View File

@@ -31,6 +31,7 @@ namespace NzbDrone.Core.History
public class HistoryService : IHistoryService,
IHandle<EpisodeGrabbedEvent>,
IHandle<MovieGrabbedEvent>,
IHandle<MovieImportedEvent>,
IHandle<EpisodeImportedEvent>,
IHandle<DownloadFailedEvent>,
IHandle<EpisodeFileDeletedEvent>,
@@ -186,7 +187,7 @@ namespace NzbDrone.Core.History
{
EventType = HistoryEventType.Grabbed,
Date = DateTime.UtcNow,
Quality = message.Movie.ParsedEpisodeInfo.Quality,
Quality = message.Movie.ParsedMovieInfo.Quality,
SourceTitle = message.Movie.Release.Title,
SeriesId = 0,
EpisodeId = 0,
@@ -196,7 +197,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Indexer", message.Movie.Release.Indexer);
history.Data.Add("NzbInfoUrl", message.Movie.Release.InfoUrl);
history.Data.Add("ReleaseGroup", message.Movie.ParsedEpisodeInfo.ReleaseGroup);
history.Data.Add("ReleaseGroup", message.Movie.ParsedMovieInfo.ReleaseGroup);
history.Data.Add("Age", message.Movie.Release.Age.ToString());
history.Data.Add("AgeHours", message.Movie.Release.AgeHours.ToString());
history.Data.Add("AgeMinutes", message.Movie.Release.AgeMinutes.ToString());
@@ -209,9 +210,9 @@ namespace NzbDrone.Core.History
history.Data.Add("TvRageId", message.Movie.Release.TvRageId.ToString());
history.Data.Add("Protocol", ((int)message.Movie.Release.DownloadProtocol).ToString());
if (!message.Movie.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
if (!message.Movie.ParsedMovieInfo.ReleaseHash.IsNullOrWhiteSpace())
{
history.Data.Add("ReleaseHash", message.Movie.ParsedEpisodeInfo.ReleaseHash);
history.Data.Add("ReleaseHash", message.Movie.ParsedMovieInfo.ReleaseHash);
}
var torrentRelease = message.Movie.Release as TorrentInfo;
@@ -264,6 +265,45 @@ namespace NzbDrone.Core.History
}
}
public void Handle(MovieImportedEvent message)
{
if (!message.NewDownload)
{
return;
}
var downloadId = message.DownloadId;
if (downloadId.IsNullOrWhiteSpace())
{
//downloadId = FindDownloadId(message); For now fuck off.
}
var movie = message.MovieInfo.Movie;
var history = new History
{
EventType = HistoryEventType.DownloadFolderImported,
Date = DateTime.UtcNow,
Quality = message.MovieInfo.Quality,
SourceTitle = movie.Title,
SeriesId = 0,
EpisodeId = 0,
DownloadId = downloadId,
MovieId = movie.Id,
};
//Won't have a value since we publish this event before saving to DB.
//history.Data.Add("FileId", message.ImportedEpisode.Id.ToString());
history.Data.Add("DroppedPath", message.MovieInfo.Path);
history.Data.Add("ImportedPath", Path.Combine(movie.Path, message.ImportedMovie.RelativePath));
history.Data.Add("DownloadClient", message.DownloadClient);
_historyRepository.Insert(history);
}
public void Handle(DownloadFailedEvent message)
{
foreach (var episodeId in message.EpisodeIds)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
@@ -249,15 +249,15 @@ namespace NzbDrone.Core.IndexerSearch
private TSpec Get<TSpec>(Series series, List<Episode> episodes, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new()
{
var spec = new TSpec();
spec.Series = series;
spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
var spec = new TSpec()
{
Series = series,
SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList(),
episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList());
spec.Episodes = episodes;
episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList()),
Episodes = episodes
};
spec.SceneTitles.Add(series.Title);
spec.UserInvokedSearch = userInvokedSearch;
@@ -266,18 +266,18 @@ namespace NzbDrone.Core.IndexerSearch
private TSpec Get<TSpec>(Movie movie, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new()
{
var spec = new TSpec();
var spec = new TSpec()
{
Movie = movie,
/*spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList(),
episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList());
spec.Movie = movie;
/*spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList(),
episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList());
spec.Episodes = episodes;
spec.SceneTitles.Add(series.Title);*/
spec.UserInvokedSearch = userInvokedSearch;
spec.Episodes = episodes;
spec.SceneTitles.Add(series.Title);*/
UserInvokedSearch = userInvokedSearch
};
return spec;
}

View File

@@ -106,8 +106,8 @@ namespace NzbDrone.Core.Indexers.Newznab
}
if (capabilities.SupportedTvSearchParameters != null &&
new[] { "q", "tvdbid", "rid" }.Any(v => capabilities.SupportedTvSearchParameters.Contains(v)) &&
new[] { "season", "ep" }.All(v => capabilities.SupportedTvSearchParameters.Contains(v)))
new[] { "q", "imdb" }.Any(v => capabilities.SupportedMovieSearchParamters.Contains(v)) &&
new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParamters.Contains(v)))
{
return null;
}

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public int MaxPageSize { get; set; }
public string[] SupportedSearchParameters { get; set; }
public string[] SupportedTvSearchParameters { get; set; }
public string[] SupportedMovieSearchParamters { get; set; }
public bool SupportsAggregateIdSearch { get; set; }
public List<NewznabCategory> Categories { get; set; }
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.Indexers.Newznab
DefaultPageSize = 100;
MaxPageSize = 100;
SupportedSearchParameters = new[] { "q" };
SupportedMovieSearchParamters = new[] { "q", "imdb", "imdbtitle", "imdbyear" };
SupportedTvSearchParameters = new[] { "q", "rid", "season", "ep" }; // This should remain 'rid' for older newznab installs.
SupportsAggregateIdSearch = false;
Categories = new List<NewznabCategory>();

View File

@@ -30,6 +30,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings)
{
var key = indexerSettings.ToJson();
_capabilitiesCache.Clear();
var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7));
return capabilities;
@@ -98,6 +99,16 @@ namespace NzbDrone.Core.Indexers.Newznab
capabilities.SupportedTvSearchParameters = xmlTvSearch.Attribute("supportedParams").Value.Split(',');
capabilities.SupportsAggregateIdSearch = true;
}
var xmlMovieSearch = xmlSearching.Element("movie-search");
if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes")
{
capabilities.SupportedMovieSearchParamters = null;
}
else if (xmlMovieSearch.Attribute("supportedParams") != null)
{
capabilities.SupportedMovieSearchParamters = xmlMovieSearch.Attribute("supportedParams").Value.Split(',');
capabilities.SupportsAggregateIdSearch = true;
}
}
var xmlCategories = xmlRoot.Element("categories");

View File

@@ -85,6 +85,17 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private bool SupportsMovieSearch
{
get
{
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
return capabilities.SupportedMovieSearchParamters != null &&
capabilities.SupportedMovieSearchParamters.Contains("imdb");
}
}
private bool SupportsAggregatedIdSearch
{
get
@@ -109,6 +120,25 @@ namespace NzbDrone.Core.Indexers.Newznab
return pageableRequests;
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
if(SupportsMovieSearch)
{
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie",
string.Format("&imdbid={0}", searchCriteria.Movie.ImdbId.Substring(2)))); //strip off the "tt" - VERY HACKY
}
else
{
//Let's try anyways with q parameter, worst case nothing found.
pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search",
string.Format("&q={0}", searchCriteria.Movie.Title)));
}
return pageableRequests;
}
public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
@@ -274,10 +304,5 @@ namespace NzbDrone.Core.Indexers.Newznab
{
return title.Replace("+", "%20");
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation;
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Newznab
public NewznabSettings()
{
Categories = new[] { 5030, 5040 };
Categories = new[] { 2030, 2040, 2050 };
AnimeCategories = Enumerable.Empty<int>();
}

View File

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

View File

@@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IDetectSample
{
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
@@ -79,6 +80,57 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
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)
{
if (_largeSampleSizeQualities.Contains(quality.Quality))
@@ -99,6 +151,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return false;
}
private int GetMinimumAllowedRuntime(Movie movie)
{
return 120; //2 minutes
}
private int GetMinimumAllowedRuntime(Series series)
{
//Webisodes - 90 seconds

View File

@@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IImportDecisionEngineSpecification
{
Decision IsSatisfiedBy(LocalEpisode localEpisode);
Decision IsSatisfiedBy(LocalMovie localMovie);
}
}

View File

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

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public class ImportDecision
{
public LocalEpisode LocalEpisode { get; private set; }
public LocalMovie LocalMovie { get; private set; }
public IEnumerable<Rejection> Rejections { get; private set; }
public bool Approved => Rejections.Empty();
@@ -18,5 +19,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
LocalEpisode = localEpisode;
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
};
}
}
}

View File

@@ -98,36 +98,32 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
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);
localEpisode.Size = _diskProvider.GetFileSize(file);
localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie);
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
if (sceneSource)
{
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
}
if (localEpisode.Episodes.Empty())
{
decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode"));
localMovie.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
decision = GetDecision(localMovie);
}
else
{
decision = GetDecision(localEpisode);
decision = GetDecision(localMovie);
}
}
else
{
localEpisode = new LocalEpisode();
var localEpisode = new LocalEpisode();
localEpisode.Path = 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 };
decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file"));
}*/
}
decision = new ImportDecision(null, new Rejection("IMPLEMENTATION MISSING!!!"));
//LocalMovie nullMovie = null;
//decision = new ImportDecision(nullMovie, new Rejection("IMPLEMENTATION MISSING!!!"));
return decision;
}
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)
{
ImportDecision decision = null;
@@ -204,6 +210,33 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
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)
{
try
@@ -292,6 +325,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}) == 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)
{
if (UseFolderQuality(folderInfo, fileQuality, series))
@@ -303,6 +347,31 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
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)
{
if (folderInfo == null)

View File

@@ -63,5 +63,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
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();
}
}
}

View File

@@ -17,11 +17,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
if (localEpisode.ParsedEpisodeInfo.FullSeason)
{
//_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");
_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.Accept();
}
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
}
}

View File

@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
@@ -14,6 +15,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
_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)
{
if (localEpisode.ExistingFile)

View File

@@ -37,5 +37,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
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();
}
}
}

View File

@@ -56,5 +56,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
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();
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using NLog;
using NzbDrone.Core.DecisionEngine;
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");
return Decision.Reject("Episode file on disk contains more episodes than this file contains");
}
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
@@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie)
{
return Decision.Accept();
}
public Decision IsSatisfiedBy(LocalEpisode localEpisode)
{
if (localEpisode.ExistingFile)

View File

@@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept();
}
public Decision IsSatisfiedBy(LocalMovie localEpisode)
{
return Decision.Accept();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,16 +7,20 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Common;
using System;
namespace NzbDrone.Core.MediaFiles
{
public interface IMediaFileService
{
MovieFile Add(MovieFile episodeFile);
void Update(MovieFile episodeFile);
void Delete(MovieFile episodeFile, DeleteMediaFileReason reason);
EpisodeFile Add(EpisodeFile episodeFile);
void Update(EpisodeFile episodeFile);
void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason);
List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesByMovie(int movieId);
List<MovieFile> GetFilesByMovie(int movieId);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFilesWithoutMediaInfo();
List<string> FilterExistingFiles(List<string> files, Series series);
@@ -30,12 +34,14 @@ namespace NzbDrone.Core.MediaFiles
{
private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileRepository _mediaFileRepository;
private readonly IMovieFileRepository _movieFileRepository;
private readonly Logger _logger;
public MediaFileService(IMediaFileRepository mediaFileRepository, IEventAggregator eventAggregator, Logger logger)
public MediaFileService(IMediaFileRepository mediaFileRepository, IMovieFileRepository movieFileRepository, IEventAggregator eventAggregator, Logger logger)
{
_mediaFileRepository = mediaFileRepository;
_eventAggregator = eventAggregator;
_movieFileRepository = movieFileRepository;
_logger = logger;
}
@@ -61,14 +67,24 @@ namespace NzbDrone.Core.MediaFiles
_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)
{
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)
@@ -114,5 +130,18 @@ namespace NzbDrone.Core.MediaFiles
var files = GetFilesBySeries(message.Series.Id);
_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);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Marr.Data;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.MediaFiles
{
public class MovieFile : ModelBase
{
public int MovieId { get; set; }
public string RelativePath { get; set; }
public string Path { get; set; }
public long Size { get; set; }
public DateTime DateAdded { get; set; }
public string SceneName { get; set; }
public string ReleaseGroup { get; set; }
public QualityModel Quality { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public LazyLoaded<Movie> Movie { get; set; }
public override string ToString()
{
return string.Format("[{0}] {1}", Id, RelativePath);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles
public interface IUpgradeMediaFiles
{
EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false);
MovieFileMoveResult UpgradeMovieFile(MovieFile movieFile, LocalMovie localMovie, bool copyOnly = false);
}
public class UpgradeMediaFileService : IUpgradeMediaFiles
@@ -16,22 +17,59 @@ namespace NzbDrone.Core.MediaFiles
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IMediaFileService _mediaFileService;
private readonly IMoveEpisodeFiles _episodeFileMover;
private readonly IMoveMovieFiles _movieFileMover;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider,
IMediaFileService mediaFileService,
IMoveEpisodeFiles episodeFileMover,
IMoveMovieFiles movieFileMover,
IDiskProvider diskProvider,
Logger logger)
{
_recycleBinProvider = recycleBinProvider;
_mediaFileService = mediaFileService;
_episodeFileMover = episodeFileMover;
_movieFileMover = movieFileMover;
_diskProvider = diskProvider;
_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)
{
var moveFileResult = new EpisodeFileMoveResult();

View File

@@ -7,5 +7,6 @@ namespace NzbDrone.Core.MetadataSource
public interface IProvideMovieInfo
{
Movie GetMovieInfo(string ImdbId);
Movie GetMovieInfo(int TmdbId);
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{
public class ConfigResource
{
public Images images { get; set; }
public string[] change_keys { get; set; }
}
public class Images
{
public string base_url { get; set; }
public string secure_base_url { get; set; }
public string[] backdrop_sizes { get; set; }
public string[] logo_sizes { get; set; }
public string[] poster_sizes { get; set; }
public string[] profile_sizes { get; set; }
public string[] still_sizes { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{
public class ImdbResource
{
public int v { get; set; }
public string q { get; set; }
public MovieResource[] d { get; set; }
}
public class MovieResource
{
public string l { get; set; }
public string id { get; set; }
public string s { get; set; }
public int y { get; set; }
public string q { get; set; }
public object[] i { get; set; }
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{
public class MovieSearchRoot
{
public int page { get; set; }
public MovieResult[] results { get; set; }
public int total_results { get; set; }
public int total_pages { get; set; }
}
public class MovieResult
{
public string poster_path { get; set; }
public bool adult { get; set; }
public string overview { get; set; }
public string release_date { get; set; }
public int?[] genre_ids { get; set; }
public int id { get; set; }
public string original_title { get; set; }
public string original_language { get; set; }
public string title { get; set; }
public string backdrop_path { get; set; }
public float popularity { get; set; }
public int vote_count { get; set; }
public bool video { get; set; }
public float vote_average { get; set; }
}
public class MovieResourceRoot
{
public bool adult { get; set; }
public string backdrop_path { get; set; }
public Belongs_To_Collection belongs_to_collection { get; set; }
public int budget { get; set; }
public Genre[] genres { get; set; }
public string homepage { get; set; }
public int id { get; set; }
public string imdb_id { get; set; }
public string original_language { get; set; }
public string original_title { get; set; }
public string overview { get; set; }
public float popularity { get; set; }
public string poster_path { get; set; }
public Production_Companies[] production_companies { get; set; }
public Production_Countries[] production_countries { get; set; }
public string release_date { get; set; }
public int revenue { get; set; }
public int runtime { get; set; }
public Spoken_Languages[] spoken_languages { get; set; }
public string status { get; set; }
public string tagline { get; set; }
public string title { get; set; }
public bool video { get; set; }
public float vote_average { get; set; }
public int vote_count { get; set; }
public AlternativeTitles alternative_titles { get; set; }
}
public class Belongs_To_Collection
{
public int id { get; set; }
public string name { get; set; }
public string poster_path { get; set; }
public string backdrop_path { get; set; }
}
public class Genre
{
public int id { get; set; }
public string name { get; set; }
}
public class Production_Companies
{
public string name { get; set; }
public int id { get; set; }
}
public class Production_Countries
{
public string iso_3166_1 { get; set; }
public string name { get; set; }
}
public class Spoken_Languages
{
public string iso_639_1 { get; set; }
public string name { get; set; }
}
public class AlternativeTitles
{
public List<Title> titles { get; set; }
}
public class Title
{
public string iso_3166_1 { get; set; }
public string title { get; set; }
}
}

View File

@@ -9,6 +9,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Tv;
using Newtonsoft.Json;
@@ -20,11 +21,15 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly Logger _logger;
private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IHttpRequestBuilderFactory _movieBuilder;
private readonly ITmdbConfigService _configService;
public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, Logger logger)
{
_httpClient = httpClient;
_requestBuilder = requestBuilder.SkyHookTvdb;
_movieBuilder = requestBuilder.TMDB;
_configService = configService;
_logger = logger;
}
@@ -58,6 +63,65 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return new Tuple<Series, List<Episode>>(series, episodes.ToList());
}
public Movie GetMovieInfo(int TmdbId)
{
var request = _movieBuilder.Create()
.SetSegment("route", "movie")
.SetSegment("id", TmdbId.ToString())
.SetSegment("secondaryRoute", "")
.AddQueryParam("append_to_response", "alternative_titles")
.AddQueryParam("country", "US")
.Build();
request.AllowAutoRedirect = true;
request.SuppressHttpError = true;
var response = _httpClient.Get<MovieResourceRoot>(request);
var resource = response.Resource;
var movie = new Movie();
movie.TmdbId = TmdbId;
movie.ImdbId = resource.imdb_id;
movie.Title = resource.title;
movie.TitleSlug = movie.Title.ToLower().Replace(" ", "-");
movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title);
movie.Overview = resource.overview;
movie.Website = resource.homepage;
movie.InCinemas = DateTime.Parse(resource.release_date);
movie.Year = movie.InCinemas.Value.Year;
movie.Images.Add(_configService.GetCoverForURL(resource.poster_path, MediaCoverTypes.Poster));//TODO: Update to load image specs from tmdb page!
movie.Images.Add(_configService.GetCoverForURL(resource.backdrop_path, MediaCoverTypes.Banner));
movie.Runtime = resource.runtime;
foreach(Title title in resource.alternative_titles.titles)
{
movie.AlternativeTitles.Add(title.title);
}
movie.Ratings = new Ratings();
movie.Ratings.Votes = resource.vote_count;
movie.Ratings.Value = (decimal)resource.vote_average;
foreach(Genre genre in resource.genres)
{
movie.Genres.Add(genre.name);
}
if (resource.status == "Released")
{
movie.Status = MovieStatusType.Released;
}
else
{
movie.Status = MovieStatusType.Announced;
}
return movie;
}
public Movie GetMovieInfo(string ImdbId)
{
var imdbRequest = new HttpRequest("http://www.omdbapi.com/?i=" + ImdbId + "&plot=full&r=json");
@@ -136,11 +200,22 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}
}
var searchTerm = lowerTitle.Replace("+", "_").Replace(" ", "_");
var searchTerm = lowerTitle.Replace("_", "+").Replace(" ", "+");
var firstChar = searchTerm.First();
var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/" + firstChar + "/" + searchTerm + ".json");
var request = _movieBuilder.Create()
.SetSegment("route", "search")
.SetSegment("id", "movie")
.SetSegment("secondaryRoute", "")
.AddQueryParam("query", searchTerm)
.AddQueryParam("include_adult", false)
.Build();
request.AllowAutoRedirect = true;
request.SuppressHttpError = true;
/*var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/" + firstChar + "/" + searchTerm + ".json");
var response = _httpClient.Get(imdbRequest);
@@ -148,31 +223,42 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")");
dynamic json = JsonConvert.DeserializeObject(responseCleaned);
_logger.Warn("Cleaned response: " + responseCleaned);
ImdbResource json = JsonConvert.DeserializeObject<ImdbResource>(responseCleaned);
_logger.Warn("Json object: " + json);
_logger.Warn("Crash ahead.");*/
var response = _httpClient.Get<MovieSearchRoot>(request);
var movieResults = response.Resource.results;
var imdbMovies = new List<Movie>();
foreach (dynamic entry in json.d)
foreach (MovieResult result in movieResults)
{
var imdbMovie = new Movie();
imdbMovie.ImdbId = entry.id;
imdbMovie.TmdbId = result.id;
try
{
imdbMovie.SortTitle = entry.l;
imdbMovie.Title = entry.l;
string titleSlug = entry.l;
imdbMovie.SortTitle = result.title;
imdbMovie.Title = result.title;
string titleSlug = result.title;
imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-");
imdbMovie.Year = entry.y;
imdbMovie.Year = DateTime.Parse(result.release_date).Year;
imdbMovie.Images = new List<MediaCover.MediaCover>();
imdbMovie.Overview = result.overview;
try
{
string url = entry.i[0];
var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url);
string url = result.poster_path;
var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster);
imdbMovie.Images.Add(imdbPoster);
}
catch (Exception e)
{
_logger.Debug(entry);
_logger.Debug(result);
continue;
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Core.MediaCover;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Http;
using NzbDrone.Common.Cloud;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
namespace NzbDrone.Core.MetadataSource
{
public interface ITmdbConfigService
{
MediaCover.MediaCover GetCoverForURL(string url, MediaCover.MediaCoverTypes type);
}
class TmdbConfigService : ITmdbConfigService
{
private readonly ICached<ConfigResource> _configurationCache;
private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _tmdbBuilder;
public TmdbConfigService(ICacheManager cacheManager, IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder)
{
_configurationCache = cacheManager.GetCache<ConfigResource>(GetType(), "configuration_cache");
_httpClient = httpClient;
_tmdbBuilder = requestBuilder.TMDBSingle;
}
public MediaCover.MediaCover GetCoverForURL(string url, MediaCover.MediaCoverTypes type)
{
if (_configurationCache.Count == 0)
{
RefreshCache();
}
var images = _configurationCache.Find("configuration").images;
var cover = new MediaCover.MediaCover();
cover.CoverType = type;
var realUrl = images.base_url;
switch (type)
{
case MediaCoverTypes.Banner:
realUrl += images.backdrop_sizes.Last();
break;
case MediaCoverTypes.Poster:
realUrl += images.poster_sizes.Last();
break;
default:
realUrl += "original";
break;
}
realUrl += url;
cover.Url = realUrl;
return cover;
}
private void RefreshCache()
{
var request = _tmdbBuilder.Create().SetSegment("route", "configuration").Build();
var response = _httpClient.Get<ConfigResource>(request);
if (response.Resource.images != null)
{
_configurationCache.Set("configuration", response.Resource);
}
}
}
}

View File

@@ -183,6 +183,9 @@
<Compile Include="Datastore\Migration\002_remove_tvrage_imdb_unique_constraint.cs" />
<Compile Include="Datastore\Migration\003_remove_clean_title_from_scene_mapping.cs" />
<Compile Include="Datastore\Migration\004_updated_history.cs" />
<Compile Include="Datastore\Migration\107_fix_movie_files.cs" />
<Compile Include="Datastore\Migration\106_add_tmdb_stuff.cs" />
<Compile Include="Datastore\Migration\105_fix_history_movieId.cs" />
<Compile Include="Datastore\Migration\005_added_eventtype_to_history.cs" />
<Compile Include="Datastore\Migration\006_add_index_to_log_time.cs" />
<Compile Include="Datastore\Migration\007_add_renameEpisodes_to_naming.cs" />
@@ -249,6 +252,7 @@
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
<Compile Include="Datastore\Migration\069_quality_proper.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\095_add_additional_episodes_index.cs" />
<Compile Include="Datastore\Migration\103_fix_metadata_file_extensions.cs" />
@@ -700,6 +704,17 @@
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.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\Commands\RenameFilesCommand.cs" />
<Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" />
@@ -762,6 +777,7 @@
<Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" />
<Compile Include="MediaFiles\RenameEpisodeFileService.cs" />
<Compile Include="MediaFiles\SameFilenameException.cs" />
<Compile Include="MediaFiles\UpdateMovieFileService.cs" />
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
@@ -790,12 +806,15 @@
<Compile Include="MetadataSource\IProvideMovieInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewMovie.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ActorResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ConfigurationResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\EpisodeResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\SeasonResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\MovieResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\ShowResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\TimeOfDayResource.cs" />
<Compile Include="MetadataSource\SkyHook\Resource\TMDBResources.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" />
<Compile Include="MetadataSource\SearchSeriesComparer.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookException.cs" />
@@ -820,6 +839,7 @@
<Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="MetadataSource\TmdbConfigurationService.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" />
<Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" />
<Compile Include="Notifications\Join\JoinResponseModel.cs" />
@@ -875,6 +895,8 @@
<Compile Include="Parser\IsoLanguage.cs" />
<Compile Include="Parser\IsoLanguages.cs" />
<Compile Include="Parser\LanguageParser.cs" />
<Compile Include="Parser\Model\LocalMovie.cs" />
<Compile Include="Parser\Model\ParsedMovieInfo.cs" />
<Compile Include="Parser\Model\RemoteMovie.cs" />
<Compile Include="Profiles\Delay\DelayProfile.cs" />
<Compile Include="Profiles\Delay\DelayProfileService.cs" />

View File

@@ -17,6 +17,8 @@ namespace NzbDrone.Core.Organizer
public interface IBuildFileNames
{
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 BuildSeasonPath(Series series, int seasonNumber);
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
@@ -137,6 +139,66 @@ namespace NzbDrone.Core.Organizer
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)
{
Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace();
@@ -146,6 +208,15 @@ namespace NzbDrone.Core.Organizer
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)
{
var path = series.Path;
@@ -246,7 +317,7 @@ namespace NzbDrone.Core.Organizer
public string GetMovieFolder(Movie movie)
{
return CleanFolderName(Parser.Parser.CleanSeriesTitle(movie.Title));
return CleanFolderName(movie.Title);
}
public static string CleanTitle(string title)
@@ -275,7 +346,9 @@ namespace NzbDrone.Core.Organizer
public static string CleanFolderName(string name)
{
name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString());
return name.Trim(' ', '.');
name = name.Trim(' ', '.');
return CleanFileName(name);
}
private void AddSeriesTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series)
@@ -774,6 +847,26 @@ namespace NzbDrone.Core.Organizer
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

View File

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

View File

@@ -0,0 +1,31 @@
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Parser.Model
{
public class ParsedMovieInfo
{
public string MovieTitle { get; set; }
public SeriesTitleInfo MovieTitleInfo { get; set; }
public QualityModel Quality { get; set; }
public int SeasonNumber { get; set; }
public Language Language { get; set; }
public bool FullSeason { get; set; }
public bool Special { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public string Edition { get; set;}
public int Year { get; set; }
public ParsedMovieInfo()
{
}
public override string ToString()
{
return string.Format("{0} - {1} {2}", MovieTitle, MovieTitleInfo.Year, Quality);
}
}
}

View File

@@ -9,6 +9,7 @@ namespace NzbDrone.Core.Parser.Model
{
public ReleaseInfo Release { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do.
public ParsedMovieInfo ParsedMovieInfo { get; set; }
public Movie Movie { get; set; }
public bool DownloadAllowed { get; set; }

View File

@@ -15,6 +15,26 @@ namespace NzbDrone.Core.Parser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser));
private static readonly Regex[] ReportMovieTitleRegex = new[]
{
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.Special.Edition.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<edition>(\w+\.?edition))\.(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition //TODO: Seems to slow down parsing heavily!
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)(?<edition>((\w+\.?){1,3}edition))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Cut Movies, e.g: Mission.Impossible.3.Directors.Cut.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<edition>(\w+\.?cut))\.(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Cut Movies, e.g: Mission.Impossible.3.2011.Directors.Cut
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)(?<edition>((\w+\.?){1,3}cut))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Normal movie format, e.g: Mission.Impossible.3.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
};
private static readonly Regex[] ReportTitleRegex = new[]
{
//Anime - Absolute Episode Number + Title + Season+Episode
@@ -298,6 +318,96 @@ namespace NzbDrone.Core.Parser
return result;
}
public static ParsedMovieInfo ParseMovieTitle(string title)
{
ParsedMovieInfo realResult = null;
try
{
if (!ValidateBeforeParsing(title)) return null;
title = title.Replace(" ", "."); //TODO: Determine if this breaks something. However, it shouldn't.
Logger.Debug("Parsing string '{0}'", title);
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length);
Logger.Debug("Reversed name detected. Converted to '{0}'", title);
}
var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty);
simpleTitle = RemoveFileExtension(simpleTitle);
// TODO: Quick fix stripping [url] - prefixes.
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty);
foreach (var regex in ReportMovieTitleRegex)
{
var match = regex.Matches(simpleTitle);
if (match.Count != 0)
{
Logger.Trace(regex);
try
{
var result = ParseMovieMatchCollection(match);
if (result != null)
{
result.Language = LanguageParser.ParseLanguage(title);
Logger.Debug("Language parsed: {0}", result.Language);
result.Quality = QualityParser.ParseQuality(title);
Logger.Debug("Quality parsed: {0}", result.Quality);
result.ReleaseGroup = ParseReleaseGroup(title);
var subGroup = GetSubGroup(match);
if (!subGroup.IsNullOrWhiteSpace())
{
result.ReleaseGroup = subGroup;
}
Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup);
result.ReleaseHash = GetReleaseHash(match);
if (!result.ReleaseHash.IsNullOrWhiteSpace())
{
Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash);
}
realResult = result;
return result;
}
}
catch (InvalidDateException ex)
{
Logger.Debug(ex, ex.Message);
break;
}
}
}
}
catch (Exception e)
{
if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc"))
Logger.Error(e, "An error has occurred while trying to parse " + title);
}
Logger.Debug("Unable to parse {0}", title);
return realResult;
}
public static ParsedEpisodeInfo ParseTitle(string title)
{
@@ -528,6 +638,31 @@ namespace NzbDrone.Core.Parser
return seriesTitleInfo;
}
private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection)
{
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');
seriesName = RequestInfoRegex.Replace(seriesName, "").Trim(' ');
int airYear;
int.TryParse(matchCollection[0].Groups["year"].Value, out airYear);
ParsedMovieInfo result;
result = new ParsedMovieInfo { Year = airYear };
if (matchCollection[0].Groups["edition"].Success)
{
result.Edition = matchCollection[0].Groups["edition"].Value.Replace(".", " ");
}
result.MovieTitle = seriesName;
result.MovieTitleInfo = GetSeriesTitleInfo(result.MovieTitle);
Logger.Debug("Movie Parsed. {0}", result);
return result;
}
private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection)
{
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');

View File

@@ -15,11 +15,13 @@ namespace NzbDrone.Core.Parser
{
LocalEpisode GetLocalEpisode(string filename, Series series);
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);
Movie GetMovie(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
RemoteMovie Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null);
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
}
@@ -31,6 +33,20 @@ namespace NzbDrone.Core.Parser
private readonly ISceneMappingService _sceneMappingService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
private readonly Dictionary<string, string> romanNumeralsMapper = new Dictionary<string, string>
{
{ "1", "I"},
{ "2", "II"},
{ "3", "III"},
{ "4", "IV"},
{ "5", "V"},
{ "6", "VI"},
{ "7", "VII"},
{ "8", "VII"},
{ "9", "IX"},
{ "10", "X"},
}; //If a movie has more than 10 parts fuck 'em.
public ParsingService(IEpisodeService episodeService,
ISeriesService seriesService,
@@ -99,6 +115,46 @@ namespace NzbDrone.Core.Parser
};
}
public LocalMovie GetLocalMovie(string filename, Movie movie)
{
return GetLocalMovie(filename, movie, null, false);
}
public LocalMovie GetLocalMovie(string filename, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
ParsedEpisodeInfo parsedEpisodeInfo;
if (folderInfo != null)
{
parsedEpisodeInfo = folderInfo.JsonClone();
parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename));
}
else
{
parsedEpisodeInfo = Parser.ParsePath(filename);
}
if (parsedEpisodeInfo == null)
{
if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename)))
{
_logger.Warn("Unable to parse episode info from path {0}", filename);
}
return null;
}
return new LocalMovie
{
Movie = movie,
Quality = parsedEpisodeInfo.Quality,
Path = filename,
ParsedEpisodeInfo = parsedEpisodeInfo,
ExistingFile = movie.Path.IsParentPath(filename)
};
}
public Series GetSeries(string title)
{
var parsedEpisodeInfo = Parser.ParseTitle(title);
@@ -121,19 +177,19 @@ namespace NzbDrone.Core.Parser
public Movie GetMovie(string title)
{
var parsedEpisodeInfo = Parser.ParseTitle(title);
var parsedEpisodeInfo = Parser.ParseMovieTitle(title);
if (parsedEpisodeInfo == null)
{
return _movieService.FindByTitle(title);
}
var series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
var series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle);
if (series == null)
{
series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear,
parsedEpisodeInfo.SeriesTitleInfo.Year);
series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitleInfo.TitleWithoutYear,
parsedEpisodeInfo.MovieTitleInfo.Year);
}
return series;
@@ -159,11 +215,11 @@ namespace NzbDrone.Core.Parser
return remoteEpisode;
}
public RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null)
public RemoteMovie Map(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null)
{
var remoteEpisode = new RemoteMovie
{
ParsedEpisodeInfo = parsedEpisodeInfo,
ParsedMovieInfo = parsedEpisodeInfo,
};
var movie = GetMovie(parsedEpisodeInfo, imdbId, searchCriteria);
@@ -292,23 +348,56 @@ namespace NzbDrone.Core.Parser
return null;
}
private Movie GetMovie(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria)
private Movie GetMovie(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria)
{
if (searchCriteria != null)
{
if (searchCriteria.Movie.CleanTitle == parsedEpisodeInfo.SeriesTitle.CleanSeriesTitle())
var possibleTitles = new List<string>();
possibleTitles.Add(searchCriteria.Movie.CleanTitle);
foreach (string altTitle in searchCriteria.Movie.AlternativeTitles)
{
return searchCriteria.Movie;
possibleTitles.Add(altTitle.CleanSeriesTitle());
}
if (imdbId.IsNotNullOrWhiteSpace() && imdbId == searchCriteria.Movie.ImdbId)
foreach (string title in possibleTitles)
{
//TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import
return searchCriteria.Movie;
if (title == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
foreach (KeyValuePair<string, string> entry in romanNumeralsMapper)
{
string num = entry.Key;
string roman = entry.Value.ToLower();
if (title.Replace(num, roman) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
if (title.Replace(roman, num) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
}
}
}
Movie movie = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
Movie movie = null;
if (searchCriteria == null)
{
movie = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle); //Todo: same as above!
return movie;
}
if (movie == null && imdbId.IsNotNullOrWhiteSpace())
{
@@ -318,7 +407,7 @@ namespace NzbDrone.Core.Parser
if (movie == null)
{
_logger.Debug("No matching movie {0}", parsedEpisodeInfo.SeriesTitle);
_logger.Debug("No matching movie {0}", parsedEpisodeInfo.MovieTitle);
return null;
}

View File

@@ -64,7 +64,7 @@ namespace NzbDrone.Core.Queue
Id = HashConverter.GetHashInt31(string.Format("trackedDownload-{0}", trackedDownload.DownloadItem.DownloadId)),
Series = null,
Episode = null,
Quality = trackedDownload.RemoteMovie.ParsedEpisodeInfo.Quality,
Quality = trackedDownload.RemoteMovie.ParsedMovieInfo.Quality,
Title = trackedDownload.DownloadItem.Title,
Size = trackedDownload.DownloadItem.TotalSize,
Sizeleft = trackedDownload.DownloadItem.RemainingSize,

View File

@@ -4,6 +4,7 @@ using Marr.Data;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.MediaFiles;
namespace NzbDrone.Core.Tv
{
@@ -15,8 +16,9 @@ namespace NzbDrone.Core.Tv
Genres = new List<string>();
Actors = new List<Actor>();
Tags = new HashSet<int>();
AlternativeTitles = new List<string>();
}
public int TmdbId { get; set; }
public string ImdbId { get; set; }
public string Title { get; set; }
public string CleanTitle { get; set; }
@@ -29,6 +31,7 @@ namespace NzbDrone.Core.Tv
public int Runtime { get; set; }
public List<MediaCover.MediaCover> Images { get; set; }
public string TitleSlug { get; set; }
public string Website { get; set; }
public string Path { get; set; }
public int Year { get; set; }
public Ratings Ratings { get; set; }
@@ -41,7 +44,9 @@ namespace NzbDrone.Core.Tv
public LazyLoaded<Profile> Profile { get; set; }
public HashSet<int> Tags { get; set; }
public AddMovieOptions AddOptions { get; set; }
public LazyLoaded<MovieFile> MovieFile { get; set; }
public int MovieFileId { get; set; }
public List<string> AlternativeTitles { get; set; }
public override string ToString()
{
return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe());

View File

@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
@@ -11,10 +13,27 @@ namespace NzbDrone.Core.Tv
Movie FindByTitle(string cleanTitle);
Movie FindByTitle(string cleanTitle, int year);
Movie FindByImdbId(string imdbid);
List<Movie> GetMoviesByFileId(int fileId);
void SetFileId(int fileId, int movieId);
}
public class MovieRepository : BasicRepository<Movie>, IMovieRepository
{
private readonly Dictionary<string, string> romanNumeralsMapper = new Dictionary<string, string>
{
{ "1", "I"},
{ "2", "II"},
{ "3", "III"},
{ "4", "IV"},
{ "5", "V"},
{ "6", "VI"},
{ "7", "VII"},
{ "8", "VII"},
{ "9", "IX"},
{ "10", "X"},
}; //If a movie has more than 10 parts fuck 'em.
public MovieRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
@@ -29,8 +48,46 @@ namespace NzbDrone.Core.Tv
{
cleanTitle = cleanTitle.ToLowerInvariant();
return Query.Where(s => s.CleanTitle == cleanTitle)
.SingleOrDefault();
var cleanRoman = cleanTitle;
var cleanNum = cleanTitle;
foreach (KeyValuePair<string, string> entry in romanNumeralsMapper)
{
string num = entry.Key;
string roman = entry.Value.ToLower();
cleanRoman = cleanRoman.Replace(num, roman);
cleanNum = cleanNum.Replace(roman, num);
}
var result = Query.Where(s => s.CleanTitle == cleanTitle).SingleOrDefault();
if (result == null)
{
result = Query.Where(s => s.CleanTitle == cleanNum).OrWhere(s => s.CleanTitle == cleanRoman).SingleOrDefault();
if (result == null)
{
var movies = this.All();
result = movies.Where(m => m.AlternativeTitles.Any(t => Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanTitle ||
Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanRoman ||
Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanNum)).SingleOrDefault();
return result;
}
else
{
return result;
}
}
else
{
return result;
}
}
public Movie FindByTitle(string cleanTitle, int year)
@@ -46,5 +103,16 @@ namespace NzbDrone.Core.Tv
{
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);
}
}
}

View File

@@ -10,6 +10,8 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
namespace NzbDrone.Core.Tv
{
@@ -22,6 +24,7 @@ namespace NzbDrone.Core.Tv
Movie FindByTitle(string title);
Movie FindByTitle(string title, int year);
Movie FindByTitleInexact(string title);
Movie GetMovieByFileId(int fileId);
void DeleteMovie(int movieId, bool deleteFiles);
List<Movie> GetAllMovies();
Movie UpdateMovie(Movie movie);
@@ -30,7 +33,8 @@ namespace NzbDrone.Core.Tv
void RemoveAddOptions(Movie movie);
}
public class MovieService : IMovieService
public class MovieService : IMovieService, IHandle<MovieFileAddedEvent>,
IHandle<MovieFileDeletedEvent>
{
private readonly IMovieRepository _movieRepository;
private readonly IEventAggregator _eventAggregator;
@@ -73,7 +77,7 @@ namespace NzbDrone.Core.Tv
_logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path);
newMovie.CleanTitle = newMovie.Title.CleanSeriesTitle();
newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.ImdbId);
newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.TmdbId);
newMovie.Added = DateTime.UtcNow;
_movieRepository.Insert(newMovie);
@@ -195,5 +199,24 @@ namespace NzbDrone.Core.Tv
{
_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();
}
}
}

View File

@@ -4,16 +4,16 @@ namespace NzbDrone.Core.Tv
{
public static class MovieTitleNormalizer
{
private readonly static Dictionary<string, string> PreComputedTitles = new Dictionary<string, string>
private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string>
{
{ "tt_109823457098", "a to z" },
{ 999999999, "a to z" },
};
public static string Normalize(string title, string imdbid)
public static string Normalize(string title, int tmdbid)
{
if (PreComputedTitles.ContainsKey(imdbid))
if (PreComputedTitles.ContainsKey(tmdbid))
{
return PreComputedTitles[imdbid];
return PreComputedTitles[tmdbid];
}
return Parser.Parser.NormalizeTitle(title).ToLower();

View File

@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Tv
try
{
movieInfo = _movieInfo.GetMovieInfo(movie.ImdbId);
movieInfo = _movieInfo.GetMovieInfo(movie.TmdbId);
}
catch (MovieNotFoundException)
{
@@ -59,10 +59,10 @@ namespace NzbDrone.Core.Tv
return;
}
if (movie.ImdbId != movieInfo.ImdbId)
if (movie.TmdbId != movieInfo.TmdbId)
{
_logger.Warn("Movie '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", movie.Title, movie.ImdbId, movieInfo.Title, movieInfo.ImdbId);
movie.ImdbId = movieInfo.ImdbId;
_logger.Warn("Movie '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", movie.Title, movie.TmdbId, movieInfo.Title, movieInfo.TmdbId);
movie.TmdbId = movieInfo.TmdbId;
}
movie.Title = movieInfo.Title;
@@ -80,6 +80,8 @@ namespace NzbDrone.Core.Tv
movie.Genres = movieInfo.Genres;
movie.Certification = movieInfo.Certification;
movie.InCinemas = movieInfo.InCinemas;
movie.Website = movieInfo.Website;
movie.AlternativeTitles = movieInfo.AlternativeTitles;
movie.Year = movieInfo.Year;
try

View File

@@ -18,9 +18,9 @@ namespace NzbDrone.Core.Validation.Paths
{
if (context.PropertyValue == null) return true;
var imdbid = context.PropertyValue.ToString();
int tmdbId = (int)context.PropertyValue;
return (!_seriesService.GetAllMovies().Exists(s => s.ImdbId == imdbid));
return (!_seriesService.GetAllMovies().Exists(s => s.TmdbId == tmdbId));
}
}
}

View File

@@ -38,7 +38,7 @@ namespace NzbDrone.SysTray
_trayMenu.MenuItems.Add("-");
_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.ContextMenu = _trayMenu;

View File

@@ -125,7 +125,7 @@ module.exports = Marionette.Layout.extend({
}
else if (!this.isExisting) {
this.resultCollectionView.setExisting(options.movie.get('imdbId'))
this.resultCollectionView.setExisting(options.movie.get('tmdbId'))
/*this.collection.term = '';
this.collection.reset();
this._clearResults();

View File

@@ -21,8 +21,8 @@ module.exports = Marionette.CollectionView.extend({
return this.showing >= this.collection.length;
},
setExisting : function(imdbid) {
var movies = this.collection.where({ imdbId : imdbid });
setExisting : function(tmdbid) {
var movies = this.collection.where({ tmdbId : tmdbid });
console.warn(movies)
//debugger;
if (movies.length > 0) {

View File

@@ -91,7 +91,7 @@ var view = Marionette.ItemView.extend({
},
_configureTemplateHelpers : function() {
var existingMovies = MoviesCollection.where({ imdbId : this.model.get('imdbId') });
var existingMovies = MoviesCollection.where({ tmdbId : this.model.get('tmdbId') });
console.log(existingMovies)
if (existingMovies.length > 0) {
this.templateHelpers.existing = existingMovies[0].toJSON();

View File

@@ -0,0 +1,41 @@
var Backgrid = require('backgrid');
var Marionette = require('marionette');
require('bootstrap');
module.exports = Backgrid.Cell.extend({
className : 'edition-cell',
//template : 'Cells/EditionCellTemplate',
render : function() {
var edition = this.model.get(this.column.get('name'));
if (!edition) {
return this;
}
var cut = false;
if (edition.toLowerCase().contains("cut")) {
cut = true;
}
//this.templateFunction = Marionette.TemplateCache.get(this.template);
//var html = this.templateFunction(edition);
if (cut) {
this.$el.html('<i class="icon-sonarr-form-cut"/ title="{0}">'.format(edition));
} else {
this.$el.html('<i class="icon-sonarr-form-special"/ title="{0}">'.format(edition));
}
/*this.$el.popover({
content : html,
html : true,
trigger : 'hover',
title : this.column.get('title'),
placement : 'left',
container : this.$el
});*/
return this;
}
});

View File

@@ -0,0 +1,5 @@
<ul>
<li>
{{this}}
</li>
</ul>

View File

@@ -13,23 +13,23 @@ module.exports = NzbDroneCell.extend({
switch (this.cellValue.get('eventType')) {
case 'grabbed':
icon = 'icon-sonarr-downloading';
toolTip = 'Episode grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer);
toolTip = 'Movie grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer);
break;
case 'seriesFolderImported':
icon = 'icon-sonarr-hdd';
toolTip = 'Existing episode file added to library';
toolTip = 'Existing movie file added to library';
break;
case 'downloadFolderImported':
icon = 'icon-sonarr-imported';
toolTip = 'Episode downloaded successfully and picked up from download client';
toolTip = 'Movie downloaded successfully and picked up from download client';
break;
case 'downloadFailed':
icon = 'icon-sonarr-download-failed';
toolTip = 'Episode download failed';
toolTip = 'Movie download failed';
break;
case 'episodeFileDeleted':
icon = 'icon-sonarr-deleted';
toolTip = 'Episode file deleted';
toolTip = 'Movie file deleted';
break;
default:
icon = 'icon-sonarr-unknown';
@@ -41,4 +41,4 @@ module.exports = NzbDroneCell.extend({
return this;
}
});
});

View File

@@ -121,6 +121,14 @@
.fa-icon-color(@brand-danger);
}
.icon-sonarr-form-cut {
.fa-icon-content(@fa-var-scissors);
}
.icon-sonarr-form-special {
.fa-icon-content(@fa-var-exclamation-circle);
}
.icon-sonarr-form-info-link {
.clickable();
.fa-icon-content(@fa-var-info-circle);
@@ -502,4 +510,4 @@
.icon-sonarr-header-rejections {
.fa-icon-content(@fa-var-exclamation-circle);
}
}

View File

@@ -8,6 +8,7 @@ var DownloadReportCell = require('../../Release/DownloadReportCell');
var AgeCell = require('../../Release/AgeCell');
var ProtocolCell = require('../../Release/ProtocolCell');
var PeersCell = require('../../Release/PeersCell');
var EditionCell = require('../../Cells/EditionCell');
module.exports = Marionette.Layout.extend({
template : 'Episode/Search/ManualLayoutTemplate',
@@ -32,6 +33,12 @@ module.exports = Marionette.Layout.extend({
label : 'Title',
cell : ReleaseTitleCell
},
{
name : 'edition',
label : 'Edition',
cell : EditionCell,
title : "Edition"
},
{
name : 'indexer',
label : 'Indexer',
@@ -83,4 +90,4 @@ module.exports = Marionette.Layout.extend({
}));
}
}
});
});

View File

@@ -33,7 +33,7 @@ Handlebars.registerHelper('remotePoster', function() {
}
return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder));
})
});
Handlebars.registerHelper('traktUrl', function() {
return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show';
@@ -47,6 +47,25 @@ Handlebars.registerHelper('tvdbUrl', function() {
return 'http://imdb.com/title/tt' + this.imdbId;
});
Handlebars.registerHelper('tmdbUrl', function() {
return 'https://www.themoviedb.org/movie/' + this.tmdbId;
});
Handlebars.registerHelper('homepage', function() {
return this.website;
});
Handlebars.registerHelper('alternativeTitlesString', function() {
var titles = this.alternativeTitles;
if (titles.length == 0) {
return "";
}
if (titles.length == 1) {
return titles[0];
}
return titles.slice(0,titles.length-1).join(", ") + " and " + titles[titles.length-1];
});
Handlebars.registerHelper('inCinemas', function() {
var monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
@@ -55,7 +74,7 @@ Handlebars.registerHelper('inCinemas', function() {
var year = cinemasDate.getFullYear();
var month = monthNames[cinemasDate.getMonth()];
return "In Cinemas " + month + " " + year;
})
});
Handlebars.registerHelper('tvRageUrl', function() {
return 'http://www.tvrage.com/shows/id-' + this.tvRageId;

View File

@@ -1,5 +1,5 @@
<div class="row">
<div class="col-md-9">
<div class="col-md-8">
{{profile profileId}}
{{#if network}}
@@ -27,11 +27,13 @@
<span class="label label-default">Announced</span>
{{/if_eq}}
</div>
<div class="col-md-3">
<div class="col-md-4">
<span class="series-info-links">
<!--<a href="{{traktUrl}}" class="label label-info">Trakt</a>
<a href="{{tvdbUrl}}" class="label label-info">The TVDB</a>-->
<!--<a href="{{traktUrl}}" class="label label-info">Trakt</a>-->
{{#if website}}
<a href="{{homepage}}" class="label label-info">Homepage</a>
{{/if}}
<a href="{{tmdbUrl}}" class="label label-info">The Movie DB</a>
{{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a>
@@ -40,18 +42,12 @@
</div>
</div>
{{#if alternateTitles}}
{{#if alternativeTitles}}
<div class="row">
<div class="col-md-12">
{{#each alternateTitles}}
{{#if_eq seasonNumber compare="-1"}}
<span class="label label-default">{{title}}</span>
{{/if_eq}}
{{#if_eq sceneSeasonNumber compare="-1"}}
<span class="label label-default">{{title}}</span>
{{/if_eq}}
{{/each}}
<span class="alternative-titles">
Also known as: {{alternativeTitlesString}}.
</span>
</div>
</div>
{{/if}}

View File

@@ -274,7 +274,7 @@ module.exports = Marionette.Layout.extend({
_showBackdrop : function () {
$('body').addClass('backdrop');
var fanArt = this._getImage('fanart');
var fanArt = this._getImage('banner');
if (fanArt) {
this._backstrech = $.backstretch(fanArt);

View File

@@ -36,12 +36,18 @@
</div>
</div>
<div id="movie-info">
<ul class="nav nav-tabs" id="myTab">
<li><a href="#movie-history" class="x-movie-history">History</a></li>
<li><a href="#movie-search" class="x-movie-search">Search</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="movie-history"/>
<div class="tab-pane" id="movie-search"/>
<div class="movie-tabs">
<div>
<div class="movie-tabs-card">
<ul class="nav nav-tabs" id="myTab">
<li><a href="#movie-history" class="x-movie-history">History</a></li>
<li><a href="#movie-search" class="x-movie-search">Search</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="movie-history"/>
<div class="tab-pane" id="movie-search"/>
</div>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More