1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

56 Commits

Author SHA1 Message Date
Taloth Saldono
b093be3f4e Added additional gdiplus check. 2016-11-05 22:26:30 +01:00
Mark McDowall
3c8b263694 Update CONTRIBUTING.md 2016-11-02 16:56:54 -07:00
Mark McDowall
43c5d03f9a Fix GetAncestorFolders tests under mono 2016-11-02 15:08:39 -07:00
Taloth Saldono
9fbe06ad68 New: Added support to override Copy vs Move import logic for DownloadedEpisodesScan API and Manual Import UI. 2016-11-02 20:28:45 +01:00
Mark McDowall
db899a9bb8 New: Move subtitles/other extra files to Sonarr's Recycle Bin instead of permanently deleting 2016-11-02 11:12:24 -07:00
Mark McDowall
d3890bd712 New: Health check warning for macOS when running from App Translocation folder 2016-11-02 11:12:24 -07:00
Mark McDowall
1a61796092 Fixed: mono 4.4.2 won't trigger mono version error 2016-11-02 11:12:24 -07:00
Taloth Saldono
1251e294cd Saving settings failed if value was null. 2016-11-02 00:02:14 +01:00
Taloth Saldono
0411b82e65 Fixed: Mount handling logic of net namespaces as seen on QNAP.
fixes #1483
2016-11-01 22:53:53 +01:00
Taloth Saldono
9519f3137c Compile error when fixing FileBrowser not displaying drive letters on Windows. 2016-11-01 20:30:58 +01:00
Taloth Saldono
f8d97cac7d Fixed: FileBrowser not displaying drive letters on Windows.
fixes #1535
2016-11-01 20:22:12 +01:00
Mark McDowall
f2ecbe776b Added and fixed qBittorent tests 2016-10-28 13:28:21 -07:00
Mark McDowall
1ac442d0e6 Removed unused disk provider 2016-10-28 13:14:18 -07:00
Mark McDowall
5f2aeb0cea Fixed: Failing database migration of metadata files without extensions 2016-10-28 13:04:22 -07:00
Mark McDowall
2ece05cd1e Fixed: Email connection test reporting success incorrectly
Fixes #1524
2016-10-28 11:02:36 -07:00
Casey Bodley
25a3f83ebc New: Remove completed torrents from qBittorrent
Fixes #1316
2016-10-27 20:07:42 -07:00
Mark McDowall
cdce65a922 Added TODO to remove ToPP SABnzbd status 2016-10-27 18:02:38 -07:00
Mark McDowall
8f73a51522 Fixed: Handling of some really long qBittorrent ETAs 2016-10-27 17:57:18 -07:00
Mark McDowall
bc438a6a63 Fixed: Handle SABnzbd Propagating status 2016-10-26 21:09:53 -07:00
Mark McDowall
4167ffe11a Capture exit code of nunit to avoid using failed tests as exit code 2016-10-25 14:00:27 -07:00
Mark McDowall
6fb1aa85d0 Fixed: Parsing of some poorly named standard episode release names
Closes #1522
2016-10-25 10:16:05 -07:00
Mark McDowall
eb8ef6c337 Fixed: login page being returned instead of unauthorized response 2016-10-24 20:41:19 -07:00
Mark McDowall
c076f1ddb1 Update omgwtfnzbs URL
Closes #1520
2016-10-24 18:49:58 -07:00
karaambaa
eeff79b288 Added Sonarr-icon to Boxcar notification
Now there is a small Sonarr icon next to the Boxcar message.
2016-10-20 09:36:00 -07:00
Mark McDowall
697a62da0a Fixed typo 2016-10-18 13:53:25 -07:00
Mark McDowall
f1a289cc74 Re-added accidentally removed anime parsing Regex 2016-10-18 13:52:15 -07:00
Mark McDowall
c39a26d9e0 Fixed: Parsing of multiple absolute episode number releases 2016-10-17 17:51:55 -07:00
Taloth Saldono
f2ccf94835 Fixed: Updated IPTorrents url validation to changed format.
fixes: #1493
2016-10-10 23:32:03 +02:00
Taloth Saldono
19d625c6c5 Fixed: Changed Quality Parser to avoid matching tags in the Episode title instead of the Quality tags. 2016-10-10 23:32:01 +02:00
Taloth Saldono
cd3b6000a0 Tweaked Nyaa Request Generator to avoid offset=1. 2016-10-10 23:31:58 +02:00
Mark McDowall
ff33f15bac Fixed: Import episodes in season packs in numerical order
Closes #1485
2016-10-04 10:20:48 -07:00
Mark McDowall
50a0e9514e Fixed: Default redirect URL for forms auth will use URL Base if configured 2016-10-01 15:40:04 -07:00
Taloth Saldono
7ef1ca8a00 Fixed: Tweaked ratelimit logic for rarbg api. 2016-09-29 21:42:08 +02:00
Mark McDowall
e0d1e08f94 Upgraded mono version check (3.10 minimum and 4.4.x)
New: Health warning if mono version is less than 3.10
New: Health error if running mono 4.4.x
2016-09-26 18:47:33 -07:00
Taloth Saldono
9fae76015a Fixed: Calendar api again includes series images. (Nzb360)
fixes #1473
2016-09-24 23:08:31 +02:00
Taloth Saldono
17bf438cad New: uTorrent differential api support to handle larger lists of torrents without hogging the api.
Fixes #1109
2016-09-24 23:07:08 +02:00
Chris Heath
c0b0567c23 Join: Device ID entry and better error handling
New: Optionally limit Join notifications to specific devices
Fixes #1455
2016-09-23 16:13:46 -07:00
Taloth Saldono
edc1e0b8d1 Fixed: Filter qbittorrent torrent list on newer versions.
fixes #1470
ref #1347
2016-09-23 21:42:52 +02:00
kaso17
cd79b42f5f add downloadvolumefactor and uploadvolumefactor torznab attributes (#1464)
Using the attributes it's possible to signal states like freeleech/neutral leech/double upload
2016-09-23 21:08:50 +02:00
Taloth Saldono
dc82e66dde Clarified min/max size rejection message by including the episode runtime. 2016-09-23 20:51:37 +02:00
Taloth Saldono
b034d0c1b3 Fixed: Cleanup unused Tags during housekeeping. 2016-09-23 20:51:36 +02:00
Taloth Saldono
36a3e86882 New: Added filter by tag to iCal feed.
closes  #1430
2016-09-23 20:51:34 +02:00
Mark McDowall
e76fb8c90b Fixed: Issue loading settings on some systems 2016-09-20 22:08:04 -07:00
Mark McDowall
e6288148ad Fixed: Suppress warning log messages when unable to parse non-video files 2016-09-20 22:08:04 -07:00
Mark McDowall
bf8d68a873 Fixed: Version check for SABnzbd develop 2016-09-20 22:08:04 -07:00
Taloth Saldono
080e2e9eff New: Added query parameter to ical feed to list premiers only.
fixes #1463
fixes #1442
2016-09-21 00:28:36 +02:00
Taloth Saldono
e3310e590c Fixed: prevents autofill on new password fields in settings. 2016-09-20 22:06:33 +02:00
Taloth Saldono
a0b4d3a38d Removed conflicting criteria from IsProduction check. 2016-09-20 21:30:46 +02:00
Taloth Saldono
a72b856fb8 Fixed: Added config validation to ensure NzbGet KeepHistory isn't set to 0. 2016-09-20 21:30:17 +02:00
Mark McDowall
522ef9d8d5 Don't append the extension when using ParsePath 2016-09-19 21:33:18 -07:00
Mark McDowall
a486bff40b Fixed: Migrations using old SQLite versions (Prior to 3.7.15)
Closes #1466
2016-09-19 16:10:40 -07:00
Mark McDowall
0de1f3f17a Fixed: Ignore extrafanart subfolder when scanning for extra files 2016-09-18 22:24:32 -07:00
Mark McDowall
755fdce227 Fixed: Generating metadata files after importing episode files 2016-09-18 22:19:08 -07:00
Mark McDowall
cd8659e684 Fixed: Store metadata file extensions 2016-09-18 11:30:22 -07:00
Mark McDowall
a621f0d49b Fixed: Prevent duplicate parsing of extra files 2016-09-18 11:04:56 -07:00
Mark McDowall
2e96c4e798 New: Parse existing subtitles and extra files
Towards #459
2016-09-17 01:27:15 -07:00
155 changed files with 3505 additions and 1162 deletions

3
.gitignore vendored
View File

@@ -41,6 +41,9 @@ src/**/[Oo]bj/
_ReSharper*
_dotCover*
# DevExpress CodeRush
src/.cr/
# NCrunch
*.ncrunch*
.*crunch*.local.xml

6
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{Sonarr node_modules}" />
<includedPredefinedLibrary name="ECMAScript 6" />
</component>
</project>

View File

@@ -8,8 +8,8 @@ Setup guides, FAQ, the more information we have on the wiki the better.
## Development ##
### Tools required ###
- Visual Studio 2013
- HTML/Javascript editor of choice (Sublime Text/Webstorm/etc)
- Visual Studio 2015
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
- npm (node package manager)
- git

View File

@@ -75,6 +75,8 @@
<xs:enumeration value="seedtype" /> <!-- TBD, which criteria must be met. was going for 'ratio,seedtime,both' but afaik it's always 'either' -->
<xs:enumeration value="minimumratio" />
<xs:enumeration value="minimumseedtime" />
<xs:enumeration value="downloadvolumefactor" /> <!-- factor for the download volume, in most cases it should be set to 1, if a torrent is set to freeleech set it to 0, if only 50% is counted set it to 0.5 -->
<xs:enumeration value="uploadvolumefactor" /> <!-- factor for the upload volume, in most cases it should be set to 1, if a torrent is set to neutral leech (upload is not counted) set it to 0, if it's set to double upload set it to 2 -->
</xs:restriction>
</xs:simpleType>
<xs:element name="attr">

View File

@@ -198,7 +198,8 @@ namespace Marr.Data.Mapping
{
return AutoMapPropertiesWhere(m =>
m.MemberType == MemberTypes.Property &&
!DataHelper.IsSimpleType((m as PropertyInfo).PropertyType));
!DataHelper.IsSimpleType((m as PropertyInfo).PropertyType) &&
!MapRepository.Instance.TypeConverters.ContainsKey((m as PropertyInfo).PropertyType));
}
/// <summary>

View File

@@ -43,7 +43,7 @@ namespace NzbDrone.Api.Authentication
expiry = DateTime.UtcNow.AddDays(7);
}
return this.LoginAndRedirect(user.Identifier, expiry);
return this.LoginAndRedirect(user.Identifier, expiry, _configFileProvider.UrlBase + "/");
}
private Response Logout()

View File

@@ -77,7 +77,7 @@ namespace NzbDrone.Api.Authentication
if (context.Request.IsApiRequest())
{
if ((context.Response.StatusCode == HttpStatusCode.SeeOther &&
context.Response.Headers["Location"].StartsWith("/login", StringComparison.InvariantCultureIgnoreCase)) ||
context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) ||
context.Response.StatusCode == HttpStatusCode.Unauthorized)
{
context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized);

View File

@@ -5,17 +5,21 @@ using System.Linq;
using DDay.iCal;
using NzbDrone.Core.Tv;
using Nancy.Responses;
using NzbDrone.Core.Tags;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Api.Calendar
{
public class CalendarFeedModule : NzbDroneFeedModule
{
private readonly IEpisodeService _episodeService;
private readonly ITagService _tagService;
public CalendarFeedModule(IEpisodeService episodeService)
public CalendarFeedModule(IEpisodeService episodeService, ITagService tagService)
: base("calendar")
{
_episodeService = episodeService;
_tagService = tagService;
Get["/NzbDrone.ics"] = options => GetCalendarFeed();
}
@@ -23,10 +27,12 @@ namespace NzbDrone.Api.Calendar
private Response GetCalendarFeed()
{
var pastDays = 7;
var futureDays = 28;
var futureDays = 28;
var start = DateTime.Today.AddDays(-pastDays);
var end = DateTime.Today.AddDays(futureDays);
var unmonitored = false;
var premiersOnly = false;
var tags = new List<int>();
// TODO: Remove start/end parameters in v3, they don't work well for iCal
var queryStart = Request.Query.Start;
@@ -34,6 +40,8 @@ namespace NzbDrone.Api.Calendar
var queryPastDays = Request.Query.PastDays;
var queryFutureDays = Request.Query.FutureDays;
var queryUnmonitored = Request.Query.Unmonitored;
var queryPremiersOnly = Request.Query.PremiersOnly;
var queryTags = Request.Query.Tags;
if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value);
if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value);
@@ -55,11 +63,32 @@ namespace NzbDrone.Api.Calendar
unmonitored = bool.Parse(queryUnmonitored.Value);
}
if (queryPremiersOnly.HasValue)
{
premiersOnly = bool.Parse(queryPremiersOnly.Value);
}
if (queryTags.HasValue)
{
var tagInput = (string)queryTags.Value.ToString();
tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
}
var episodes = _episodeService.EpisodesBetweenDates(start, end, unmonitored);
var icalCalendar = new iCalendar();
foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value))
{
if (premiersOnly && (episode.SeasonNumber == 0 || episode.EpisodeNumber != 1))
{
continue;
}
if (tags.Any() && tags.None(episode.Series.Tags.Contains))
{
continue;
}
var occurrence = icalCalendar.Create<Event>();
occurrence.UID = "NzbDrone_episode_" + episode.Id.ToString();
occurrence.Status = episode.HasFile ? EventStatus.Confirmed : EventStatus.Tentative;

View File

@@ -1,5 +1,4 @@
using System;
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation.Paths;

View File

@@ -1,5 +1,4 @@
using System;
using NzbDrone.Api.REST;
using NzbDrone.Api.REST;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
@@ -21,6 +20,7 @@ namespace NzbDrone.Api.Config
public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; }
public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; }
}
@@ -44,7 +44,8 @@ namespace NzbDrone.Api.Config
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks,
EnableMediaInfo = model.EnableMediaInfo,
ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo
};
}
}

View File

@@ -1,5 +1,5 @@
using System;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Extras.Metadata;
namespace NzbDrone.Api.Metadata
{

View File

@@ -12,7 +12,7 @@ namespace NzbDrone.Api.REST
{
get
{
return GetType().Name.ToLower().Replace("resource", "");
return GetType().Name.ToLowerInvariant().Replace("resource", "");
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests.IEnumerableExtensionTests
{
[TestFixture]
public class ExceptByFixture
{
public class Object1
{
public string Prop1 { get; set; }
}
public class Object2
{
public string Prop1 { get; set; }
}
[Test]
public void should_return_empty_when_object_with_property_exists_in_both_lists()
{
var first = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "two" }
};
var second = new List<Object1>
{
new Object1 { Prop1 = "two" },
new Object1 { Prop1 = "one" }
};
first.ExceptBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).Should().BeEmpty();
}
[Test]
public void should_return_objects_that_do_not_have_a_match_in_the_second_list()
{
var first = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "two" }
};
var second = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "four" }
};
var result = first.ExceptBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).ToList();
result.Should().HaveCount(1);
result.First().GetType().Should().Be(typeof (Object1));
result.First().Prop1.Should().Be("two");
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Test.ExtensionTests.IEnumerableExtensionTests
{
[TestFixture]
public class IntersectByFixture
{
public class Object1
{
public string Prop1 { get; set; }
}
public class Object2
{
public string Prop1 { get; set; }
}
[Test]
public void should_return_empty_when_no_intersections()
{
var first = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "two" }
};
var second = new List<Object1>
{
new Object1 { Prop1 = "three" },
new Object1 { Prop1 = "four" }
};
first.IntersectBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).Should().BeEmpty();
}
[Test]
public void should_return_objects_with_intersecting_values()
{
var first = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "two" }
};
var second = new List<Object1>
{
new Object1 { Prop1 = "one" },
new Object1 { Prop1 = "four" }
};
var result = first.IntersectBy(o => o.Prop1, second, o => o.Prop1, StringComparer.InvariantCultureIgnoreCase).ToList();
result.Should().HaveCount(1);
result.First().Prop1.Should().Be("one");
}
}
}

View File

@@ -80,6 +80,8 @@
<Compile Include="EnvironmentTests\EnvironmentProviderTest.cs" />
<Compile Include="EnvironmentTests\StartupArgumentsFixture.cs" />
<Compile Include="ExtensionTests\FromOctalStringFixture.cs" />
<Compile Include="ExtensionTests\IEnumerableExtensionTests\ExceptByFixture.cs" />
<Compile Include="ExtensionTests\IEnumerableExtensionTests\IntersectByFixture.cs" />
<Compile Include="ExtensionTests\Int64ExtensionFixture.cs" />
<Compile Include="Http\HttpClientFixture.cs" />
<Compile Include="Http\HttpRequestBuilderFixture.cs" />

View File

@@ -243,7 +243,7 @@ namespace NzbDrone.Common.Test
}
[Test]
public void Sanbox()
public void Sandbox()
{
GetIAppDirectoryInfo().GetUpdateSandboxFolder().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\".AsOsAgnostic());
}
@@ -271,5 +271,33 @@ namespace NzbDrone.Common.Test
{
GetIAppDirectoryInfo().GetUpdateLogFolder().Should().BeEquivalentTo(@"C:\NzbDrone\UpdateLogs\".AsOsAgnostic());
}
[Test]
public void GetAncestorFolders_should_return_all_ancestors_in_path_Windows()
{
WindowsOnly();
var path = @"C:\Test\TV\Series Title";
var result = path.GetAncestorFolders();
result.Count.Should().Be(4);
result[0].Should().Be(@"C:\");
result[1].Should().Be(@"Test");
result[2].Should().Be(@"TV");
result[3].Should().Be(@"Series Title");
}
[Test]
public void GetAncestorFolders_should_return_all_ancestors_in_path_Linux()
{
MonoOnly();
var path = @"/Test/TV/Series Title";
var result = path.GetAncestorFolders();
result.Count.Should().Be(4);
result[0].Should().Be(@"/");
result[1].Should().Be(@"Test");
result[2].Should().Be(@"TV");
result[3].Should().Be(@"Series Title");
}
}
}

View File

@@ -113,7 +113,7 @@ namespace NzbDrone.Common.Disk
.Select(d => new FileSystemModel
{
Type = FileSystemEntityType.Drive,
Name = d.VolumeLabel,
Name = d.VolumeName,
Path = d.RootDirectory,
LastModified = null
})
@@ -163,7 +163,7 @@ namespace NzbDrone.Common.Disk
return path;
}
private string GetParent(string path)
{
var di = new DirectoryInfo(path);

View File

@@ -13,5 +13,6 @@ namespace NzbDrone.Common.Disk
long TotalFreeSpace { get; }
long TotalSize { get; }
string VolumeLabel { get; }
string VolumeName { get; }
}
}

View File

@@ -22,13 +22,13 @@ namespace NzbDrone.Common.EnvironmentInfo
serviceProvider.ServiceExist(ServiceProvider.NZBDRONE_SERVICE_NAME) &&
serviceProvider.GetStatus(ServiceProvider.NZBDRONE_SERVICE_NAME) == ServiceControllerStatus.StartPending;
//Guarded to avoid issues when running in a non-managed process
//Guarded to avoid issues when running in a non-managed process
var entry = Assembly.GetEntryAssembly();
if (entry != null)
{
ExecutingApplication = entry.Location;
}
}
}
static RuntimeInfoBase()
@@ -69,7 +69,7 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool IsWindowsService { get; private set; }
public bool IsConsole
{
{
get
{
if (OsInfo.IsWindows)
@@ -78,7 +78,7 @@ namespace NzbDrone.Common.EnvironmentInfo
}
return true;
}
}
}
public bool IsRunning { get; set; }
@@ -105,7 +105,7 @@ namespace NzbDrone.Common.EnvironmentInfo
}
catch
{
}
try
@@ -115,15 +115,14 @@ namespace NzbDrone.Common.EnvironmentInfo
}
catch
{
}
string lowerCurrentDir = Directory.GetCurrentDirectory().ToLower();
if (lowerCurrentDir.Contains("teamcity")) return false;
if (lowerCurrentDir.Contains("_output")) return false;
if (lowerCurrentDir.StartsWith("/run/")) return false;
return true;
}
}
}
}

View File

@@ -13,6 +13,44 @@ namespace NzbDrone.Common.Extensions
return source.Where(element => knownKeys.Add(keySelector(element)));
}
public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first, Func<TFirst, TKey> firstKeySelector,
IEnumerable<TSecond> second, Func<TSecond, TKey> secondKeySelector,
IEqualityComparer<TKey> keyComparer)
{
var keys = new HashSet<TKey>(second.Select(secondKeySelector), keyComparer);
foreach (var element in first)
{
var key = firstKeySelector(element);
// Remove the key so we only yield once
if (keys.Remove(key))
{
yield return element;
}
}
}
public static IEnumerable<TFirst> ExceptBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first, Func<TFirst, TKey> firstKeySelector,
IEnumerable<TSecond> second, Func<TSecond, TKey> secondKeySelector,
IEqualityComparer<TKey> keyComparer)
{
var keys = new HashSet<TKey>(second.Select(secondKeySelector), keyComparer);
var matchedKeys = new HashSet<TKey>();
foreach (var element in first)
{
var key = firstKeySelector(element);
if (!keys.Contains(key) && !matchedKeys.Contains(key))
{
// Store the key so we only yield once
matchedKeys.Add(key);
yield return element;
}
}
}
public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item)
{
if (item == null)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using NzbDrone.Common.EnsureThat;
@@ -37,6 +38,11 @@ namespace NzbDrone.Common.Extensions
return info.FullName.TrimEnd('/').Trim('\\', ' ');
}
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
{
return !PathEquals(firstPath, secondPath, comparison);
}
public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
{
if (!comparison.HasValue)
@@ -169,6 +175,21 @@ namespace NzbDrone.Common.Extensions
return Path.Combine(GetProperCapitalization(dirInfo), fileName);
}
public static List<string> GetAncestorFolders(this string path)
{
var directory = new DirectoryInfo(path);
var directories = new List<string>();
while (directory != null)
{
directories.Insert(0, directory.Name);
directory = directory.Parent;
}
return directories;
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{
return appFolderInfo.AppDataFolder;

View File

@@ -22,6 +22,7 @@ namespace NzbDrone.Common.Http
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public TimeSpan RateLimit { get; set; }
public bool LogResponseContent { get; set; }
public NetworkCredential NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
@@ -75,7 +76,7 @@ namespace NzbDrone.Common.Http
protected virtual HttpUri CreateUri()
{
var url = BaseUrl.CombinePath(ResourceUrl).AddQueryParams(QueryParams.Concat(SuffixQueryParams));
if (Segments.Any())
{
var fullUri = url.FullUri;
@@ -103,6 +104,7 @@ namespace NzbDrone.Common.Http
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;
request.ConnectionKeepAlive = ConnectionKeepAlive;
request.RateLimit = RateLimit;
request.LogResponseContent = LogResponseContent;
if (NetworkCredential != null)
@@ -245,6 +247,13 @@ namespace NzbDrone.Common.Http
return this;
}
public virtual HttpRequestBuilder WithRateLimit(double seconds)
{
RateLimit = TimeSpan.FromSeconds(seconds);
return this;
}
public virtual HttpRequestBuilder Post()
{
Method = HttpMethod.POST;
@@ -371,4 +380,4 @@ namespace NzbDrone.Common.Http
}
}
}
}

View File

@@ -108,5 +108,19 @@ namespace NzbDrone.Core.Test.Configuration
keys.Should().OnlyHaveUniqueItems();
}
[Test]
public void should_ignore_null_properties()
{
Mocker.GetMock<IConfigRepository>()
.Setup(v => v.Get("downloadedepisodesfolder"))
.Returns(new Config { Id = 1, Key = "DownloadedEpisodesFolder", Value = @"C:\test".AsOsAgnostic() });
var dict = new Dictionary<string, object>();
dict.Add("DownloadedEpisodesFolder", null);
Subject.SaveConfigDictionary(dict);
Mocker.GetMock<IConfigRepository>().Verify(c => c.Upsert("downloadedepisodesfolder", It.IsAny<string>()), Times.Never());
}
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class metadata_files_extensionFixture : MigrationTest<extra_and_subtitle_files>
{
[Test]
public void should_set_extension_using_relative_path()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("MetadataFiles").Row(new
{
SeriesId = 1,
RelativePath = "banner.jpg",
LastUpdated = "2016-05-30 20:23:02.3725923",
Type = 3,
Consumer = "XbmcMetadata"
});
c.Insert.IntoTable("MetadataFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
EpisodeFileId = 1,
RelativePath = "Series.Title.S01E01.jpg",
LastUpdated = "2016-05-30 20:23:02.3725923",
Type = 5,
Consumer = "XbmcMetadata"
});
c.Insert.IntoTable("MetadataFiles").Row(new
{
SeriesId = 1,
RelativePath = "Series Title",
LastUpdated = "2016-05-30 20:23:02.3725923",
Type = 3,
Consumer = "RoksboxMetadata"
});
});
var items = db.Query<MetadataFile99>("SELECT * FROM MetadataFiles");
items.Should().HaveCount(2);
items.First().Extension.Should().Be(".jpg");
items.Last().Extension.Should().Be(".jpg");
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class fix_metadata_file_extensionsFixture : MigrationTest<fix_metadata_file_extensions>
{
[Test]
public void should_fix_extension_when_relative_path_contained_multiple_periods()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("MetadataFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
EpisodeFileId = 1,
RelativePath = "Series.Title.S01E01.jpg",
LastUpdated = "2016-05-30 20:23:02.3725923",
Type = 5,
Consumer = "XbmcMetadata",
Extension = ".S01E01.jpg"
});
});
var items = db.Query<MetadataFile99>("SELECT * FROM MetadataFiles");
items.Should().HaveCount(1);
items.First().Extension.Should().Be(".jpg");
}
}
}

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.Download
{
Series = new Series(),
Episodes = new List<Episode> { new Episode { Id = 1 } }
};
};
}
@@ -77,11 +77,11 @@ namespace NzbDrone.Core.Test.Download
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns((History.History)null);
}
private void GivenSuccessfulImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }))
@@ -145,7 +145,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoGrabbedHistory();
GivenSeriesMatch();
GivenSuccessfulImport();
Subject.Process(_trackedDownload);
AssertCompletedDownload();
@@ -179,13 +179,13 @@ namespace NzbDrone.Core.Test.Download
public void should_mark_as_imported_if_all_episodes_were_imported()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"}))
@@ -200,13 +200,13 @@ namespace NzbDrone.Core.Test.Download
public void should_not_mark_as_imported_if_all_files_were_rejected()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure")
@@ -224,13 +224,13 @@ namespace NzbDrone.Core.Test.Download
public void should_not_mark_as_imported_if_no_episodes_were_parsed()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, new Rejection("Rejected!")), "Test Failure"),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"},new Rejection("Rejected!")), "Test Failure")
@@ -247,7 +247,7 @@ namespace NzbDrone.Core.Test.Download
public void should_not_mark_as_imported_if_all_files_were_skipped()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure"),
@@ -271,7 +271,7 @@ namespace NzbDrone.Core.Test.Download
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
@@ -294,7 +294,7 @@ namespace NzbDrone.Core.Test.Download
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
@@ -314,7 +314,7 @@ namespace NzbDrone.Core.Test.Download
GivenABadlyNamedDownload();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
@@ -323,7 +323,7 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(It.IsAny<int>()))
.Returns(BuildRemoteEpisode().Series);
Subject.Process(_trackedDownload);
AssertCompletedDownload();
@@ -335,7 +335,7 @@ namespace NzbDrone.Core.Test.Download
GivenABadlyNamedDownload();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
@@ -370,7 +370,7 @@ namespace NzbDrone.Core.Test.Download
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
@@ -408,7 +408,7 @@ namespace NzbDrone.Core.Test.Download
private void AssertNoAttemptedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
AssertNoCompletedDownload();
}
@@ -424,7 +424,7 @@ namespace NzbDrone.Core.Test.Download
private void AssertCompletedDownload()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Once());

View File

@@ -35,6 +35,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences());
}
protected void GivenRedirectToMagnet()
@@ -85,6 +89,17 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
});
}
protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true)
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(new QBittorrentPreferences
{
RemoveOnMaxRatio = removeOnMaxRatio,
MaxRatio = maxRatio
});
}
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
{
if (torrents == null)
@@ -253,13 +268,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<string, Object>();
configItems.Add("save_path", @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
var config = new QBittorrentPreferences
{
SavePath = @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic()
};
Mocker.GetMock<IQBittorrentProxy>()
.Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(configItems);
.Returns(config);
var result = Subject.GetStatus();
@@ -293,5 +309,97 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
id.Should().NotBeNullOrEmpty();
}
[Test]
public void should_be_read_only_if_max_ratio_not_reached()
{
GivenMaxRatio(1.0f);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "uploading",
Label = "",
SavePath = "",
Ratio = 0.5f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
item.IsReadOnly.Should().BeTrue();
}
[Test]
public void should_be_read_only_if_max_ratio_reached_and_not_paused()
{
GivenMaxRatio(1.0f);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "uploading",
Label = "",
SavePath = "",
Ratio = 1.0f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
item.IsReadOnly.Should().BeTrue();
}
[Test]
public void should_be_read_only_if_max_ratio_is_not_set()
{
GivenMaxRatio(1.0f, false);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "uploading",
Label = "",
SavePath = "",
Ratio = 1.0f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
item.IsReadOnly.Should().BeTrue();
}
[Test]
public void should_not_be_read_only_if_max_ratio_reached_and_paused()
{
GivenMaxRatio(1.0f);
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
Label = "",
SavePath = "",
Ratio = 1.0f
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
item.IsReadOnly.Should().BeFalse();
}
}
}

View File

@@ -12,6 +12,7 @@ using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
{
@@ -436,5 +437,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
error.IsValid.Should().Be(expected);
}
[Test]
public void should_test_develop_version_successfully()
{
Mocker.GetMock<ISabnzbdProxy>()
.Setup(v => v.GetVersion(It.IsAny<SabnzbdSettings>()))
.Returns("develop");
var result = new NzbDroneValidationResult(Subject.Test());
result.IsValid.Should().BeTrue();
result.HasWarnings.Should().BeTrue();
}
}
}

View File

@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
DownloadUrl = _downloadUrl,
RootDownloadPath = "somepath"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
@@ -130,8 +130,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
PrepareClientToReturnQueuedItem();
});
}
protected virtual void GivenTorrents(List<UTorrentTorrent> torrents)
protected virtual void GivenTorrents(List<UTorrentTorrent> torrents, string cacheNumber = null)
{
if (torrents == null)
{
@@ -139,13 +139,30 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
}
Mocker.GetMock<IUTorrentProxy>()
.Setup(s => s.GetTorrents(It.IsAny<UTorrentSettings>()))
.Returns(torrents);
.Setup(s => s.GetTorrents(It.IsAny<string>(), It.IsAny<UTorrentSettings>()))
.Returns(new UTorrentResponse { Torrents = torrents, CacheNumber = cacheNumber });
}
protected virtual void GivenDifferentialTorrents(string oldCacheNumber, List<UTorrentTorrent> changed, List<string> removed, string cacheNumber)
{
if (changed == null)
{
changed = new List<UTorrentTorrent>();
}
if (removed == null)
{
removed = new List<string>();
}
Mocker.GetMock<IUTorrentProxy>()
.Setup(s => s.GetTorrents(oldCacheNumber, It.IsAny<UTorrentSettings>()))
.Returns(new UTorrentResponse { TorrentsChanged = changed, TorrentsRemoved = removed, CacheNumber = cacheNumber });
}
protected void PrepareClientToReturnQueuedItem()
{
GivenTorrents(new List<UTorrentTorrent>
GivenTorrents(new List<UTorrentTorrent>
{
_queued
});
@@ -153,7 +170,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
protected void PrepareClientToReturnDownloadingItem()
{
GivenTorrents(new List<UTorrentTorrent>
GivenTorrents(new List<UTorrentTorrent>
{
_downloading
});
@@ -161,7 +178,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
protected void PrepareClientToReturnFailedItem()
{
GivenTorrents(new List<UTorrentTorrent>
GivenTorrents(new List<UTorrentTorrent>
{
_failed
});
@@ -296,13 +313,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<string, string>();
configItems.Add("dir_active_download_flag", "true");
configItems.Add("dir_active_download", @"C:\Downloads\Downloading\utorrent".AsOsAgnostic());
configItems.Add("dir_completed_download", @"C:\Downloads\Finished\utorrent".AsOsAgnostic());
configItems.Add("dir_completed_download_flag", "true");
configItems.Add("dir_add_label", "true");
Mocker.GetMock<IUTorrentProxy>()
.Setup(v => v.GetConfig(It.IsAny<UTorrentSettings>()))
.Returns(configItems);
@@ -353,5 +370,29 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
id.Should().NotBeNullOrEmpty();
}
[Test]
public void GetItems_should_query_with_cache_id_if_available()
{
_downloading.Status = UTorrentTorrentStatus.Started;
GivenTorrents(new List<UTorrentTorrent> { _downloading }, "abc");
var item1 = Subject.GetItems().Single();
Mocker.GetMock<IUTorrentProxy>().Verify(v => v.GetTorrents(null, It.IsAny<UTorrentSettings>()), Times.Once());
GivenTorrents(new List<UTorrentTorrent> { _downloading, _queued }, "abc2");
GivenDifferentialTorrents("abc", new List<UTorrentTorrent> { _queued }, new List<string>(), "abc2");
GivenDifferentialTorrents("abc2", new List<UTorrentTorrent>(), new List<string>(), "abc2");
var item2 = Subject.GetItems().Single();
Mocker.GetMock<IUTorrentProxy>().Verify(v => v.GetTorrents("abc", It.IsAny<UTorrentSettings>()), Times.Once());
var item3 = Subject.GetItems().Single();
Mocker.GetMock<IUTorrentProxy>().Verify(v => v.GetTorrents("abc2", It.IsAny<UTorrentSettings>()), Times.Once());
}
}
}

View File

@@ -7,9 +7,11 @@ using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
@@ -19,7 +21,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[TestFixture]
public class DeleteBadMediaCoversFixture : CoreTest<DeleteBadMediaCovers>
{
private List<MetadataFile> _metaData;
private List<MetadataFile> _metadata;
private List<Series> _series;
[SetUp]
@@ -31,7 +33,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Build().ToList();
_metaData = Builder<MetadataFile>.CreateListOfSize(1)
_metadata = Builder<MetadataFile>.CreateListOfSize(1)
.Build().ToList();
Mocker.GetMock<ISeriesService>()
@@ -41,7 +43,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Mocker.GetMock<IMetadataFileService>()
.Setup(c => c.GetFilesBySeries(_series.First().Id))
.Returns(_metaData);
.Returns(_metadata);
Mocker.GetMock<IConfigService>().SetupGet(c => c.CleanupMetadataImages).Returns(true);
@@ -51,8 +53,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test]
public void should_not_process_non_image_files()
{
_metaData.First().RelativePath = "season\\file.xml".AsOsAgnostic();
_metaData.First().Type = MetadataType.EpisodeMetadata;
_metadata.First().RelativePath = "season\\file.xml".AsOsAgnostic();
_metadata.First().Type = MetadataType.EpisodeMetadata;
Subject.Clean();
@@ -63,7 +65,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test]
public void should_not_process_images_before_tvdb_switch()
{
_metaData.First().LastUpdated = new DateTime(2014, 12, 25);
_metadata.First().LastUpdated = new DateTime(2014, 12, 25);
Subject.Clean();
@@ -89,7 +91,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test]
public void should_set_clean_flag_to_false()
{
_metaData.First().LastUpdated = new DateTime(2014, 12, 25);
_metadata.First().LastUpdated = new DateTime(2014, 12, 25);
Subject.Clean();
@@ -102,9 +104,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic();
_metaData.First().LastUpdated = new DateTime(2014, 12, 29);
_metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
_metaData.First().Type = MetadataType.SeriesImage;
_metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
_metadata.First().Type = MetadataType.SeriesImage;
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath))
@@ -115,7 +117,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Mocker.GetMock<IDiskProvider>().Verify(c => c.DeleteFile(imagePath), Times.Once());
Mocker.GetMock<IMetadataFileService>().Verify(c => c.Delete(_metaData.First().Id), Times.Once());
Mocker.GetMock<IMetadataFileService>().Verify(c => c.Delete(_metadata.First().Id), Times.Once());
}
@@ -124,9 +126,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic();
_metaData.First().LastUpdated = new DateTime(2014, 12, 29);
_metaData.First().Type = MetadataType.SeasonImage;
_metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().Type = MetadataType.SeasonImage;
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath))
@@ -136,7 +138,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Clean();
Mocker.GetMock<IDiskProvider>().Verify(c => c.DeleteFile(imagePath), Times.Once());
Mocker.GetMock<IMetadataFileService>().Verify(c => c.Delete(_metaData.First().Id), Times.Once());
Mocker.GetMock<IMetadataFileService>().Verify(c => c.Delete(_metadata.First().Id), Times.Once());
}
@@ -145,8 +147,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic();
_metaData.First().LastUpdated = new DateTime(2014, 12, 29);
_metaData.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath))

View File

@@ -1,5 +1,4 @@
using System;
using NUnit.Framework;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
@@ -22,76 +21,41 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns(string.Format("{0} (tarball Wed Sep 25 16:35:44 CDT 2013)", version));
}
[Test]
public void should_return_warning_when_mono_3_0()
[TestCase("3.10")]
[TestCase("4.0.0.0")]
[TestCase("4.2")]
[TestCase("4.6")]
[TestCase("4.4.2")]
public void should_return_ok(string version)
{
GivenOutput("3.0.0.1");
GivenOutput(version);
Subject.Check().ShouldBeOk();
}
[TestCase("2.10.2")]
[TestCase("2.10.8.1")]
[TestCase("3.0.0.1")]
[TestCase("3.2.0.1")]
[TestCase("3.2.1")]
[TestCase("3.2.7")]
[TestCase("3.6.1")]
[TestCase("3.8")]
public void should_return_warning(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_when_mono_2_10_8()
[TestCase("4.4.0")]
[TestCase("4.4.1")]
public void should_return_error(string version)
{
GivenOutput("2.10.8.1");
GivenOutput(version);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_when_mono_2_10_2()
{
GivenOutput("2.10.2");
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_ok_when_mono_3_2()
{
GivenOutput("3.2.0.1");
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_when_mono_4_0()
{
GivenOutput("4.0.0.0");
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_when_mono_3_2_7()
{
GivenOutput("3.2.7");
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_when_mono_3_2_1()
{
GivenOutput("3.2.1");
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_when_mono_3_6_1()
{
GivenOutput("3.6.1");
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_ok_when_mono_3_10()
{
GivenOutput("3.10");
Subject.Check().ShouldBeOk();
Subject.Check().ShouldBeError();
}
}
}

View File

@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.HistoryTests
Path = @"C:\Test\Unsorted\Series.s01e01.mkv"
};
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab","abcd"));
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab", "abcd", true));
Mocker.GetMock<IHistoryRepository>()
.Verify(v => v.Insert(It.Is<History.History>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path))));

View File

@@ -2,8 +2,8 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;

View File

@@ -1,9 +1,9 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
@@ -58,7 +58,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer()
{
var file = Builder<MetadataFile>.CreateNew()
.BuildNew();
.BuildNew();
Db.Insert(file);
Subject.Clean();

View File

@@ -1,10 +1,10 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@@ -94,10 +94,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Db.Insert(series);
var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
.With(m => m.SeriesId = series.Id)
.With(m => m.Type = MetadataType.EpisodeMetadata)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
Db.Insert(metadataFile);
Subject.Clean();

View File

@@ -0,0 +1,43 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tags;
using NzbDrone.Core.Restrictions;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupUnusedTagsFixture : DbTest<CleanupUnusedTags, Tag>
{
[Test]
public void should_delete_unused_tags()
{
var tags = Builder<Tag>.CreateListOfSize(2).BuildList();
Db.InsertMany(tags);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_used_tags()
{
var tags = Builder<Tag>.CreateListOfSize(2).BuildList();
Db.InsertMany(tags);
var restrictions = Builder<Restriction>.CreateListOfSize(2)
.All()
.With(v => v.Tags.Add(tags[0].Id))
.BuildList();
Db.InsertMany(restrictions);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
}
}

View File

@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(new List<ImportResult>());
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>());
var downloadItem = Builder<DownloadClientItem>.CreateNew()
@@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Once());
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Once());
}
[Test]
@@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Once());
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Once());
}
[Test]
@@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
}
[Test]
@@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, null, null), Times.Once());
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(_downloadFolder, ImportMode.Auto, null, null), Times.Once());
ExceptionVerification.ExpectedWarns(1);
}
@@ -154,9 +154,19 @@ namespace NzbDrone.Core.Test.MediaFiles
{
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), null, null), Times.Never());
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Auto, null, null), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_override_import_mode()
{
GivenExistingFile(_downloadFile);
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFile, ImportMode = ImportMode.Copy });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessPath(It.IsAny<string>(), ImportMode.Copy, null, null), Times.Once());
}
}
}
}

View File

@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(true);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
}
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(true);
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
VerifyNoImport();
}
@@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public void should_not_delete_folder_if_no_files_were_imported()
{
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@@ -159,7 +159,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<IDetectSample>()
@@ -231,7 +231,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Mocker.GetMock<IDetectSample>()
@@ -342,7 +342,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null))
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Mocker.GetMock<IDetectSample>()
@@ -365,14 +365,14 @@ namespace NzbDrone.Core.Test.MediaFiles
private void VerifyNoImport()
{
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null),
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Times.Never());
}
private void VerifyImport()
{
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null),
Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Times.Once());
}
}
}
}

View File

@@ -67,7 +67,7 @@ namespace NzbDrone.Core.Test.MediaFiles
}
Mocker.GetMock<IUpgradeMediaFiles>()
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), false))
.Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), It.IsAny<bool>()))
.Returns(new EpisodeFileMoveResult());
_downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.MediaFiles
all.AddRange(_approvedDecisions);
var result = Subject.Import(all, false);
result.Should().HaveCount(all.Count);
result.Where(i => i.Result == ImportResultType.Imported).Should().HaveCount(_approvedDecisions.Count);
}
@@ -223,5 +223,23 @@ namespace NzbDrone.Core.Test.MediaFiles
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.LocalEpisode.Size == fileDecision.LocalEpisode.Size);
}
[Test]
public void should_copy_readonly_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true });
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, true), Times.Once());
}
[Test]
public void should_use_override_importmode()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "30.Rock.S01E01", IsReadOnly = true }, ImportMode.Move);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once());
}
}
}
}

View File

@@ -2,8 +2,8 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Consumers.Roksbox;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Consumers.Roksbox;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;

View File

@@ -2,8 +2,8 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Consumers.Wdtv;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Consumers.Wdtv;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;

View File

@@ -119,6 +119,8 @@
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
<Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
<Compile Include="Datastore\Migration\103_fix_metadata_file_extensionsFixture.cs" />
<Compile Include="Datastore\Migration\099_extra_and_subtitle_filesFixture.cs" />
<Compile Include="Datastore\Migration\101_add_ultrahd_quality_in_profilesFixture.cs" />
<Compile Include="Datastore\Migration\071_unknown_quality_in_profileFixture.cs" />
<Compile Include="Datastore\Migration\072_history_downloadIdFixture.cs" />
@@ -222,6 +224,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedIndexerStatusFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItemsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFilesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupUnusedTagsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleasesFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasksFixture.cs" />
<Compile Include="Http\HttpProxySettingsProviderFixture.cs" />

View File

@@ -119,6 +119,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[RlsGrp] Series Title (2010) - S01E01-02-03 - 001-002-003 - Episode Title HDTV-720p v2", "Series Title (2010)", new[] { 1, 2, 3 })]
[TestCase("[RlsGrp] Series Title (2010) - S01E01-02 - 001-002 - Episode Title HDTV-720p v2", "Series Title (2010)", new[] { 1, 2 })]
[TestCase("Series Title (2010) - S01E01-02 (001-002) - Episode Title (1) HDTV-720p v2 [RlsGrp]", "Series Title (2010)", new[] { 1, 2 })]
[TestCase("[HorribleSubs] Haikyuu!! (01-25) [1080p] (Batch)", "Haikyuu!!", new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 })]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int[] absoluteEpisodeNumbers)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -48,8 +48,17 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)]
public void should_parse_language(string postTitle, Language language)
{
var result = Parser.Parser.ParseTitle(postTitle);
result.Language.Should().Be(language);
var result = LanguageParser.ParseLanguage(postTitle);
result.Should().Be(language);
}
[TestCase("2 Broke Girls - S01E01 - Pilot.en.sub", Language.English)]
[TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub", Language.English)]
[TestCase("2 Broke Girls - S01E01 - Pilot.sub", Language.Unknown)]
public void should_parse_subtitle_language(string fileName, Language language)
{
var result = LanguageParser.ParseSubtitleLanguage(fileName);
result.Should().Be(language);
}
}
}

View File

@@ -165,6 +165,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S06E08 1080p WEB h264-EXCLUSIVE", false)]
[TestCase("Series Title S06E08 No One PROPER 1080p WEB DD5 1 H 264-EXCLUSIVE", true)]
[TestCase("Series Title S06E08 No One PROPER 1080p WEB H 264-EXCLUSIVE", true)]
[TestCase("The.Simpsons.S25E21.Pay.Pal.1080p.WEB-DL.DD5.1.H.264-NTb", false)]
public void should_parse_webdl1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);

View File

@@ -123,6 +123,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Red Dwarf - S02 - E06 - Parallel Universe", "Red Dwarf", 2, 6)]
[TestCase("O.J.Simpson.Made.in.America.Part.Two.720p.HDTV.x264-2HD", "O J Simpson Made in America", 1, 2)]
[TestCase("The.100000.Dollar.Pyramid.2016.S01E05.720p.HDTV.x264-W4F", "The 100000 Dollar Pyramid 2016", 1, 5)]
[TestCase("Class S01E02 (22 October 2016) HDTV 720p [Webrip]", "Class", 1, 2)]
//[TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)
{

View File

@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Configuration
{
object currentValue;
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
if (currentValue == null) continue;
if (currentValue == null || configValue.Value == null) continue;
var equal = configValue.Value.ToString().Equals(currentValue.ToString());
@@ -202,6 +202,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableMediaInfo", value); }
}
public string ExtraFileExtensions
{
get { return GetValue("ExtraFileExtensions", ""); }
set { SetValue("ExtraFileExtensions", value); }
}
public bool SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

View File

@@ -35,6 +35,7 @@ namespace NzbDrone.Core.Configuration
bool SkipFreeSpaceCheckWhenImporting { get; set; }
bool CopyUsingHardlinks { get; set; }
bool EnableMediaInfo { get; set; }
string ExtraFileExtensions { get; set; }
//Permissions (Media Management)
bool SetPermissionsLinux { get; set; }

View File

@@ -0,0 +1,89 @@
using System;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(99)]
public class extra_and_subtitle_files : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("ExtraFiles")
.WithColumn("SeriesId").AsInt32().NotNullable()
.WithColumn("SeasonNumber").AsInt32().NotNullable()
.WithColumn("EpisodeFileId").AsInt32().NotNullable()
.WithColumn("RelativePath").AsString().NotNullable()
.WithColumn("Extension").AsString().NotNullable()
.WithColumn("Added").AsDateTime().NotNullable()
.WithColumn("LastUpdated").AsDateTime().NotNullable();
Create.TableForModel("SubtitleFiles")
.WithColumn("SeriesId").AsInt32().NotNullable()
.WithColumn("SeasonNumber").AsInt32().NotNullable()
.WithColumn("EpisodeFileId").AsInt32().NotNullable()
.WithColumn("RelativePath").AsString().NotNullable()
.WithColumn("Extension").AsString().NotNullable()
.WithColumn("Added").AsDateTime().NotNullable()
.WithColumn("LastUpdated").AsDateTime().NotNullable()
.WithColumn("Language").AsInt32().NotNullable();
Alter.Table("MetadataFiles")
.AddColumn("Added").AsDateTime().Nullable()
.AddColumn("Extension").AsString().Nullable();
// Remove Metadata files that don't have an extension
Execute.Sql("DELETE FROM MetadataFiles WHERE RelativePath NOT LIKE '%.%'");
// Set Extension using the extension from RelativePath
Execute.WithConnection(SetMetadataFileExtension);
Alter.Table("MetadataFiles").AlterColumn("Extension").AsString().NotNullable();
}
private void SetMetadataFileExtension(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT Id, RelativePath FROM MetadataFiles";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var relativePath = reader.GetString(1);
var extension = relativePath.Substring(relativePath.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase));
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE MetadataFiles SET Extension = ? WHERE Id = ?";
updateCmd.AddParameter(extension);
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
public class MetadataFile99
{
public int Id { get; set; }
public int SeriesId { get; set; }
public int? EpisodeFileId { get; set; }
public int? SeasonNumber { get; set; }
public string RelativePath { get; set; }
public DateTime Added { get; set; }
public DateTime LastUpdated { get; set; }
public string Extension { get; set; }
public string Hash { get; set; }
public string Consumer { get; set; }
public int Type { get; set; }
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(103)]
public class fix_metadata_file_extensions : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(SetMetadataFileExtension);
}
private void SetMetadataFileExtension(IDbConnection conn, IDbTransaction tran)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT Id, Extension FROM MetadataFiles";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetInt32(0);
var extension = reader.GetString(1);
extension = extension.Substring(extension.LastIndexOf(".", StringComparison.InvariantCultureIgnoreCase));
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE MetadataFiles SET Extension = ? WHERE Id = ?";
updateCmd.AddParameter(extension);
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
}
}

View File

@@ -14,8 +14,6 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Notifications;
@@ -31,6 +29,10 @@ using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Datastore
@@ -92,13 +94,14 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<QualityDefinition>().RegisterModel("QualityDefinitions")
.Ignore(d => d.Weight);
Mapper.Entity<Profile>().RegisterModel("Profiles");
Mapper.Entity<Log>().RegisterModel("Logs");
Mapper.Entity<NamingConfig>().RegisterModel("NamingConfig");
Mapper.Entity<SeasonStatistics>().MapResultSet();
Mapper.Entity<Blacklist>().RegisterModel("Blacklist");
Mapper.Entity<MetadataFile>().RegisterModel("MetadataFiles");
Mapper.Entity<SubtitleFile>().RegisterModel("SubtitleFiles");
Mapper.Entity<OtherExtraFile>().RegisterModel("ExtraFiles");
Mapper.Entity<PendingRelease>().RegisterModel("PendingReleases")
.Ignore(e => e.RemoteEpisode);

View File

@@ -53,8 +53,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
//If the parsed size is smaller than minSize we don't want it
if (subject.Release.Size < minSize)
{
_logger.Debug("Item: {0}, Size: {1:0n} is smaller than minimum allowed size ({2:0}), rejecting.", subject, subject.Release.Size, minSize);
return Decision.Reject("{0} is smaller than minimum allowed: {1}", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix());
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min";
_logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage);
return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage);
}
}
if (!qualityDefinition.MaxSize.HasValue || qualityDefinition.MaxSize.Value == 0)
@@ -95,8 +97,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
//If the parsed size is greater than maxSize we don't want it
if (subject.Release.Size > maxSize)
{
_logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2}), rejecting.", subject, subject.Release.Size, maxSize);
return Decision.Reject("{0} is larger than maximum allowed: {1}", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix());
var runtimeMessage = subject.Episodes.Count == 1 ? $"{subject.Series.Runtime}min" : $"{subject.Episodes.Count}x {subject.Series.Runtime}min";
_logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting.", subject, subject.Release.Size, maxSize, runtimeMessage);
return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), runtimeMessage);
}
}

View File

@@ -272,6 +272,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
failures.AddIfNotNull(TestConnection());
failures.AddIfNotNull(TestCategory());
failures.AddIfNotNull(TestSettings());
}
private ValidationFailure TestConnection()
@@ -315,7 +316,24 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return null;
}
// Javascript doesn't support 64 bit integers natively so json officially doesn't either.
private ValidationFailure TestSettings()
{
var config = _proxy.GetConfig(Settings);
var keepHistory = config.GetValueOrDefault("KeepHistory");
if (keepHistory == "0")
{
return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0")
{
InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port),
DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads."
};
}
return null;
}
// Javascript doesn't support 64 bit integers natively so json officially doesn't either.
// NzbGet api thus sends it in two 32 bit chunks. Here we join the two chunks back together.
// Simplified decimal example: "42" splits into "4" and "2". To join them I shift (<<) the "4" 1 digit to the left = "40". combine it with "2". which becomes "42" again.
private long MakeInt64(uint high, uint low)
@@ -327,4 +345,4 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
return result;
}
}
}
}

View File

@@ -85,7 +85,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
queueItem.RemainingTime = null;
}
else if (sabQueueItem.Status == SabnzbdDownloadStatus.Queued || sabQueueItem.Status == SabnzbdDownloadStatus.Grabbing)
else if (sabQueueItem.Status == SabnzbdDownloadStatus.Queued ||
sabQueueItem.Status == SabnzbdDownloadStatus.Grabbing ||
sabQueueItem.Status == SabnzbdDownloadStatus.Propagating)
{
queueItem.Status = DownloadItemStatus.Queued;
}
@@ -280,15 +282,31 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var version = _proxy.GetVersion(Settings);
var parsed = VersionRegex.Match(version);
int actualMajor;
int actualMinor;
int actualPatch;
string actualCandidate;
if (!parsed.Success)
{
return false;
if (!version.Equals("develop", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
actualMajor = 1;
actualMinor = 1;
actualPatch = 0;
actualCandidate = null;
}
var actualMajor = Convert.ToInt32(parsed.Groups["major"].Value);
var actualMinor = Convert.ToInt32(parsed.Groups["minor"].Value);
var actualPatch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", ""));
var actualCandidate = parsed.Groups["candidate"].Value.ToUpper();
else
{
actualMajor = Convert.ToInt32(parsed.Groups["major"].Value);
actualMinor = Convert.ToInt32(parsed.Groups["minor"].Value);
actualPatch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", ""));
actualCandidate = parsed.Groups["candidate"].Value.ToUpper();
}
if (actualMajor > major)
{
@@ -340,6 +358,15 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
if (!parsed.Success)
{
if (version.Equals("develop", StringComparison.InvariantCultureIgnoreCase))
{
return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 1.1.0 or higher.")
{
IsWarning = true,
DetailedDescription = "Sonarr may not be able to support new features added to SABnzbd when running develop versions."
};
}
return new ValidationFailure("Version", "Unknown Version: " + version);
}

View File

@@ -7,7 +7,7 @@
Paused,
Checking,
Downloading,
ToPP,
ToPP, // TODO: Remove in v3
QuickCheck,
Verifying,
Repairing,
@@ -17,6 +17,7 @@
Running, // Running PP Script
Completed,
Failed,
Deleted
Deleted,
Propagating
}
}

View File

@@ -12,7 +12,6 @@ using FluentValidation.Results;
using System.Net;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
@@ -80,20 +79,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public override ProviderMessage Message
{
get
{
return new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using qBittorrent", ProviderMessageType.Warning);
}
}
public override IEnumerable<DownloadClientItem> GetItems()
{
QBittorrentPreferences config;
List<QBittorrentTorrent> torrents;
try
{
config = _proxy.GetConfig(Settings);
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
@@ -116,11 +109,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
// At the moment there isn't an easy way to detect if the torrent has
// reached the seeding limit, We would need to check the preferences
// and still not be completely sure if that torrent has a limit set for it
item.IsReadOnly = true;
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.IsReadOnly = (config.MaxRatioEnabled && config.MaxRatio > torrent.Ratio) || torrent.State != "pausedUP";
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
{
@@ -178,7 +170,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
var config = _proxy.GetConfig(Settings);
var destDir = new OsPath((string)config.GetValueOrDefault("save_path"));
var destDir = new OsPath(config.SavePath);
return new DownloadClientStatus
{
@@ -227,6 +219,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
DetailedDescription = "Sonarr will not attempt to import completed downloads without a category."
};
}
// Complain if qBittorrent is configured to remove torrents on max ratio
var config = _proxy.GetConfig(Settings);
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
{
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'."
};
}
}
catch (DownloadClientAuthenticationException ex)
{

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// qbittorrent settings from the list returned by /query/preferences
public class QBittorrentPreferences
{
[JsonProperty(PropertyName = "save_path")]
public string SavePath { get; set; } // Default save path for torrents, separated by slashes
[JsonProperty(PropertyName = "max_ratio_enabled")]
public bool MaxRatioEnabled { get; set; } // True if share ratio limit is enabled
[JsonProperty(PropertyName = "max_ratio")]
public float MaxRatio { get; set; } // Get the global share ratio limit
[JsonProperty(PropertyName = "max_ratio_act")]
public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove]
}
}

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public interface IQBittorrentProxy
{
int GetVersion(QBittorrentSettings settings);
Dictionary<string, Object> GetConfig(QBittorrentSettings settings);
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
@@ -47,10 +47,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response;
}
public Dictionary<string, object> GetConfig(QBittorrentSettings settings)
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/query/preferences");
var response = ProcessRequest<Dictionary<string, object>>(request, settings);
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
return response;
}
@@ -58,7 +58,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/query/torrents")
.AddQueryParam("label", settings.TvCategory);
.AddQueryParam("label", settings.TvCategory)
.AddQueryParam("category", settings.TvCategory);
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
@@ -99,7 +100,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.AddFormParameter("hashes", hash)
.AddFormParameter("category", label);
try
{
{
ProcessRequest<object>(setCategoryRequest, settings);
}
catch(DownloadClientException ex)

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public double Progress { get; set; } // Torrent progress (%/100)
public int Eta { get; set; } // Torrent ETA (seconds)
public ulong Eta { get; set; } // Torrent ETA (seconds)
public string State { get; set; } // Torrent state. See possible values here below
@@ -21,5 +21,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[JsonProperty(PropertyName = "save_path")]
public string SavePath { get; set; } // Torrent save path
public float Ratio { get; set; } // Torrent share ratio
}
}

View File

@@ -12,14 +12,17 @@ using FluentValidation.Results;
using System.Net;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Download.Clients.UTorrent
{
public class UTorrent : TorrentClientBase<UTorrentSettings>
{
private readonly IUTorrentProxy _proxy;
private readonly ICached<UTorrentTorrentCache> _torrentCache;
public UTorrent(IUTorrentProxy proxy,
ICacheManager cacheManager,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
@@ -29,6 +32,8 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
_torrentCache = cacheManager.GetCache<UTorrentTorrentCache>(GetType(), "differentialTorrents");
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
@@ -72,12 +77,37 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
}
public override IEnumerable<DownloadClientItem> GetItems()
{
{
List<UTorrentTorrent> torrents;
try
{
torrents = _proxy.GetTorrents(Settings);
var cacheKey = string.Format("{0}:{1}:{2}", Settings.Host, Settings.Port, Settings.TvCategory);
var cache = _torrentCache.Find(cacheKey);
var response = _proxy.GetTorrents(cache == null ? null : cache.CacheID, Settings);
if (cache != null && response.Torrents == null)
{
var removedAndUpdated = new HashSet<string>(response.TorrentsChanged.Select(v => v.Hash).Concat(response.TorrentsRemoved));
torrents = cache.Torrents
.Where(v => !removedAndUpdated.Contains(v.Hash))
.Concat(response.TorrentsChanged)
.ToList();
}
else
{
torrents = response.Torrents;
}
cache = new UTorrentTorrentCache
{
CacheID = response.CacheNumber,
Torrents = torrents
};
_torrentCache.Set(cacheKey, cache, TimeSpan.FromMinutes(15));
}
catch (DownloadClientException ex)
{
@@ -122,7 +152,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
item.Status = DownloadItemStatus.Warning;
item.Message = "uTorrent is reporting an error";
}
else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Loaded) &&
else if (torrent.Status.HasFlag(UTorrentTorrentStatus.Loaded) &&
torrent.Status.HasFlag(UTorrentTorrentStatus.Checked) && torrent.Remaining == 0 && torrent.Progress == 1.0)
{
item.Status = DownloadItemStatus.Completed;
@@ -239,7 +269,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
{
try
{
_proxy.GetTorrents(Settings);
_proxy.GetTorrents(null, Settings);
}
catch (Exception ex)
{

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
{
int GetVersion(UTorrentSettings settings);
Dictionary<string, string> GetConfig(UTorrentSettings settings);
List<UTorrentTorrent> GetTorrents(UTorrentSettings settings);
UTorrentResponse GetTorrents(string cacheID, UTorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings);
void AddTorrentFromFile(string fileName, byte[] fileContent, UTorrentSettings settings);
@@ -70,14 +70,19 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
return configuration;
}
public List<UTorrentTorrent> GetTorrents(UTorrentSettings settings)
public UTorrentResponse GetTorrents(string cacheID, UTorrentSettings settings)
{
var requestBuilder = BuildRequest(settings)
.AddQueryParam("list", 1);
if (cacheID.IsNotNullOrWhiteSpace())
{
requestBuilder.AddQueryParam("cid", cacheID);
}
var result = ProcessRequest(requestBuilder, settings);
return result.Torrents;
return result;
}
public void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings)

View File

@@ -12,6 +12,10 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
public List<object> RssFeeds { get; set; }
public List<object> RssFilters { get; set; }
[JsonProperty(PropertyName = "torrentp")]
public List<UTorrentTorrent> TorrentsChanged { get; set; }
[JsonProperty(PropertyName = "torrentm")]
public List<string> TorrentsRemoved { get; set; }
[JsonProperty(PropertyName = "torrentc")]
public string CacheNumber { get; set; }

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Download.Clients.UTorrent
{
public class UTorrentTorrentCache
{
public string CacheID { get; set; }
public List<UTorrentTorrent> Torrents { get; set; }
}
}

View File

@@ -112,7 +112,7 @@ namespace NzbDrone.Core.Download
private void Import(TrackedDownload trackedDownload)
{
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
if (importResults.Empty())
{

View File

@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras
{
public class ExistingExtraFileService : IHandle<SeriesScannedEvent>
{
private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService;
private readonly List<IImportExistingExtraFiles> _existingExtraFileImporters;
private readonly Logger _logger;
public ExistingExtraFileService(IDiskProvider diskProvider,
IDiskScanService diskScanService,
List<IImportExistingExtraFiles> existingExtraFileImporters,
Logger logger)
{
_diskProvider = diskProvider;
_diskScanService = diskScanService;
_existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList();
_logger = logger;
}
public void Handle(SeriesScannedEvent message)
{
var series = message.Series;
var extraFiles = new List<ExtraFile>();
if (!_diskProvider.FolderExists(series.Path))
{
return;
}
_logger.Debug("Looking for existing extra files in {0}", series.Path);
var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path);
var possibleExtraFiles = _diskScanService.FilterFiles(series, filesOnDisk);
var filteredFiles = possibleExtraFiles;
var importedFiles = new List<string>();
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
{
var imported = existingExtraFileImporter.ProcessFiles(series, filteredFiles, importedFiles);
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
}
_logger.Info("Found {0} extra files", extraFiles.Count);
}
}
}

View File

@@ -0,0 +1,150 @@
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.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras
{
public interface IExtraService
{
void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly);
}
public class ExtraService : IExtraService,
IHandle<MediaCoversUpdatedEvent>,
IHandle<EpisodeFolderCreatedEvent>,
IHandle<SeriesRenamedEvent>
{
private readonly IMediaFileService _mediaFileService;
private readonly IEpisodeService _episodeService;
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly List<IManageExtraFiles> _extraFileManagers;
private readonly Logger _logger;
public ExtraService(IMediaFileService mediaFileService,
IEpisodeService episodeService,
IDiskProvider diskProvider,
IConfigService configService,
List<IManageExtraFiles> extraFileManagers,
Logger logger)
{
_mediaFileService = mediaFileService;
_episodeService = episodeService;
_diskProvider = diskProvider;
_configService = configService;
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
_logger = logger;
}
public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly)
{
var series = localEpisode.Series;
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterEpisodeImport(series, episodeFile);
}
// TODO: Remove
// Not importing files yet, testing that parsing is working properly first
return;
var sourcePath = localEpisode.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly);
var wantedExtensions = _configService.ExtraFileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.'))
.ToList();
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName));
foreach (var matchingFilename in matchingFilenames)
{
var matchingExtension = wantedExtensions.FirstOrDefault(e => matchingFilename.EndsWith(e));
if (matchingExtension == null)
{
continue;
}
try
{
foreach (var extraFileManager in _extraFileManagers)
{
var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, matchingExtension, isReadOnly);
if (extraFile != null)
{
break;
}
}
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to import extra file: {0}", matchingFilename);
}
}
}
public void Handle(MediaCoversUpdatedEvent message)
{
var series = message.Series;
var episodeFiles = GetEpisodeFiles(series.Id);
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterSeriesScan(series, episodeFiles);
}
}
public void Handle(EpisodeFolderCreatedEvent message)
{
var series = message.Series;
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder);
}
}
public void Handle(SeriesRenamedEvent message)
{
var series = message.Series;
var episodeFiles = GetEpisodeFiles(series.Id);
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.MoveFilesAfterRename(series, episodeFiles);
}
}
private List<EpisodeFile> GetEpisodeFiles(int seriesId)
{
var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId);
var episodes = _episodeService.GetEpisodeBySeries(seriesId);
foreach (var episodeFile in episodeFiles)
{
var localEpisodeFile = episodeFile;
episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id));
}
return episodeFiles;
}
}
}

View File

@@ -1,17 +1,16 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Metadata.Files
namespace NzbDrone.Core.Extras.Files
{
public class MetadataFile : ModelBase
public abstract class ExtraFile : ModelBase
{
public int SeriesId { get; set; }
public string Consumer { get; set; }
public MetadataType Type { get; set; }
public string RelativePath { get; set; }
public DateTime LastUpdated { get; set; }
public int? EpisodeFileId { get; set; }
public int? SeasonNumber { get; set; }
public string Hash { get; set; }
public string RelativePath { get; set; }
public DateTime Added { get; set; }
public DateTime LastUpdated { get; set; }
public string Extension { get; set; }
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Files
{
public interface IManageExtraFiles
{
int Order { get; }
IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder);
IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles);
ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
}
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
where TExtraFile : ExtraFile, new()
{
private readonly IConfigService _configService;
private readonly IDiskTransferService _diskTransferService;
private readonly IExtraFileService<TExtraFile> _extraFileService;
public ExtraFileManager(IConfigService configService,
IDiskTransferService diskTransferService,
IExtraFileService<TExtraFile> extraFileService)
{
_configService = configService;
_diskTransferService = diskTransferService;
_extraFileService = extraFileService;
}
public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder);
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles);
public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
{
var newFileName = Path.Combine(series.Path, Path.ChangeExtension(episodeFile.RelativePath, extension));
var transferMode = TransferMode.Move;
if (readOnly)
{
transferMode = _configService.CopyUsingHardlinks ? TransferMode.HardLinkOrCopy : TransferMode.Copy;
}
_diskTransferService.TransferFile(path, newFileName, transferMode, true, false);
return new TExtraFile
{
SeriesId = series.Id,
SeasonNumber = episodeFile.SeasonNumber,
EpisodeFileId = episodeFile.Id,
RelativePath = series.Path.GetRelativePath(newFileName),
Extension = Path.GetExtension(path)
};
}
}
}

View File

@@ -3,22 +3,23 @@ using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Metadata.Files
namespace NzbDrone.Core.Extras.Files
{
public interface IMetadataFileRepository : IBasicRepository<MetadataFile>
public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new()
{
void DeleteForSeries(int seriesId);
void DeleteForSeason(int seriesId, int seasonNumber);
void DeleteForEpisodeFile(int episodeFileId);
List<MetadataFile> GetFilesBySeries(int seriesId);
List<MetadataFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<MetadataFile> GetFilesByEpisodeFile(int episodeFileId);
MetadataFile FindByPath(string path);
List<TExtraFile> GetFilesBySeries(int seriesId);
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
TExtraFile FindByPath(string path);
}
public class MetadataFileRepository : BasicRepository<MetadataFile>, IMetadataFileRepository
public class ExtraFileRepository<TExtraFile> : BasicRepository<TExtraFile>, IExtraFileRepository<TExtraFile>
where TExtraFile : ExtraFile, new()
{
public MetadataFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
public ExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
@@ -38,22 +39,22 @@ namespace NzbDrone.Core.Metadata.Files
Delete(c => c.EpisodeFileId == episodeFileId);
}
public List<MetadataFile> GetFilesBySeries(int seriesId)
public List<TExtraFile> GetFilesBySeries(int seriesId)
{
return Query.Where(c => c.SeriesId == seriesId);
}
public List<MetadataFile> GetFilesBySeason(int seriesId, int seasonNumber)
public List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber)
{
return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber);
}
public List<MetadataFile> GetFilesByEpisodeFile(int episodeFileId)
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId)
{
return Query.Where(c => c.EpisodeFileId == episodeFileId);
}
public MetadataFile FindByPath(string path)
public TExtraFile FindByPath(string path)
{
return Query.Where(c => c.RelativePath == path).SingleOrDefault();
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Extras.Files
{
public interface IExtraFileService<TExtraFile>
where TExtraFile : ExtraFile, new()
{
List<TExtraFile> GetFilesBySeries(int seriesId);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
TExtraFile FindByPath(string path);
void Upsert(TExtraFile extraFile);
void Upsert(List<TExtraFile> extraFiles);
void Delete(int id);
void DeleteMany(IEnumerable<int> ids);
}
public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>,
IHandleAsync<SeriesDeletedEvent>,
IHandleAsync<EpisodeFileDeletedEvent>
where TExtraFile : ExtraFile, new()
{
private readonly IExtraFileRepository<TExtraFile> _repository;
private readonly ISeriesService _seriesService;
private readonly IDiskProvider _diskProvider;
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly Logger _logger;
public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
ISeriesService seriesService,
IDiskProvider diskProvider,
IRecycleBinProvider recycleBinProvider,
Logger logger)
{
_repository = repository;
_seriesService = seriesService;
_diskProvider = diskProvider;
_recycleBinProvider = recycleBinProvider;
_logger = logger;
}
public virtual bool PermanentlyDelete
{
get
{
return false;
}
}
public List<TExtraFile> GetFilesBySeries(int seriesId)
{
return _repository.GetFilesBySeries(seriesId);
}
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId)
{
return _repository.GetFilesByEpisodeFile(episodeFileId);
}
public TExtraFile FindByPath(string path)
{
return _repository.FindByPath(path);
}
public void Upsert(TExtraFile extraFile)
{
Upsert(new List<TExtraFile> { extraFile });
}
public void Upsert(List<TExtraFile> extraFiles)
{
extraFiles.ForEach(m =>
{
m.LastUpdated = DateTime.UtcNow;
if (m.Id == 0)
{
m.Added = m.LastUpdated;
}
});
_repository.InsertMany(extraFiles.Where(m => m.Id == 0).ToList());
_repository.UpdateMany(extraFiles.Where(m => m.Id > 0).ToList());
}
public void Delete(int id)
{
_repository.Delete(id);
}
public void DeleteMany(IEnumerable<int> ids)
{
_repository.DeleteMany(ids);
}
public void HandleAsync(SeriesDeletedEvent message)
{
_logger.Debug("Deleting Extra from database for series: {0}", message.Series);
_repository.DeleteForSeries(message.Series.Id);
}
public void HandleAsync(EpisodeFileDeletedEvent message)
{
var episodeFile = message.EpisodeFile;
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId);
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id))
{
var path = Path.Combine(series.Path, extra.RelativePath);
if (_diskProvider.FileExists(path))
{
if (PermanentlyDelete)
{
_diskProvider.DeleteFile(path);
}
else
{
// Send extra files to the recycling bin so they can be recovered if necessary
_recycleBinProvider.DeleteFile(path);
}
}
}
_logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile);
_repository.DeleteForEpisodeFile(episodeFile.Id);
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras
{
public interface IImportExistingExtraFiles
{
int Order { get; }
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using NzbDrone.Core.Extras.Files;
namespace NzbDrone.Core.Extras
{
public class ImportExistingExtraFileFilterResult<TExtraFile>
where TExtraFile : ExtraFile, new()
{
public ImportExistingExtraFileFilterResult(List<TExtraFile> previouslyImported, List<string> filesOnDisk)
{
PreviouslyImported = previouslyImported;
FilesOnDisk = filesOnDisk;
}
public List<TExtraFile> PreviouslyImported { get; set; }
public List<string> FilesOnDisk { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NzbDrone.Common;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras
{
public abstract class ImportExistingExtraFilesBase<TExtraFile> : IImportExistingExtraFiles
where TExtraFile : ExtraFile, new()
{
private readonly IExtraFileService<TExtraFile> _extraFileService;
public ImportExistingExtraFilesBase(IExtraFileService<TExtraFile> extraFileService)
{
_extraFileService = extraFileService;
}
public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles)
{
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id);
Clean(series, filesOnDisk, importedFiles, seriesFiles);
return Filter(series, filesOnDisk, importedFiles, seriesFiles);
}
private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles)
{
var previouslyImported = seriesFiles.IntersectBy(s => Path.Combine(series.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList();
var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance)
.Except(importedFiles, PathEqualityComparer.Instance)
.ToList();
// Return files that are already imported so they aren't imported again by other importers.
// Filter out files that were previously imported and as well as ones imported by other importers.
return new ImportExistingExtraFileFilterResult<TExtraFile>(previouslyImported, filteredFiles);
}
private void Clean(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles)
{
var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id);
var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id);
_extraFileService.DeleteMany(alreadyImportedFileIds);
_extraFileService.DeleteMany(deletedFiles);
}
}
}

View File

@@ -6,27 +6,20 @@ using System.Text;
using System.Xml;
using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser
namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
{
public class MediaBrowserMetadata : MetadataBase<MediaBrowserMetadataSettings>
{
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public MediaBrowserMetadata(IMapCoversToLocal mediaCoverService,
IDiskProvider diskProvider,
public MediaBrowserMetadata(
Logger logger)
{
_mediaCoverService = mediaCoverService;
_diskProvider = diskProvider;
_logger = logger;
}
@@ -38,13 +31,6 @@ namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser
}
}
public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
{
var updatedMetadataFiles = new List<MetadataFile>();
return updatedMetadataFiles;
}
public override MetadataFile FindMetadataFile(Series series, string path)
{
var filename = Path.GetFileName(path);

View File

@@ -1,10 +1,9 @@
using System;
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser
namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
{
public class MediaBrowserSettingsValidator : AbstractValidator<MediaBrowserMetadataSettings>
{

View File

@@ -9,12 +9,13 @@ using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{
public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings>
{
@@ -42,49 +43,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
}
}
public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
{
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
var updatedMetadataFiles = new List<MetadataFile>();
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
foreach (var episodeFile in episodeFiles)
if (metadataFile.Type == MetadataType.EpisodeImage)
{
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var metadataFile in metadataFiles)
{
string newFilename;
if (metadataFile.Type == MetadataType.EpisodeImage)
{
newFilename = GetEpisodeImageFilename(episodeFile.RelativePath);
}
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
newFilename = GetEpisodeMetadataFilename(episodeFile.RelativePath);
}
else
{
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
continue;
}
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
newFilename = Path.Combine(series.Path, newFilename);
if (!newFilename.PathEquals(existingFilename))
{
_diskProvider.MoveFile(existingFilename, newFilename);
metadataFile.RelativePath = series.Path.GetRelativePath(newFilename);
updatedMetadataFiles.Add(metadataFile);
}
}
return GetEpisodeImageFilename(episodeFilePath);
}
return updatedMetadataFiles;
if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
return GetEpisodeMetadataFilename(episodeFilePath);
}
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath);
}
public override MetadataFile FindMetadataFile(Series series, string path)
@@ -221,7 +195,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
}
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = Path.Combine(series.Path, Path.GetFileName(series.Path) + Path.GetExtension(source));
var destination = Path.GetFileName(series.Path) + Path.GetExtension(source);
return new List<ImageFileResult>{ new ImageFileResult(destination, source) };
}
@@ -246,7 +220,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox
}
var filename = Path.GetFileName(seasonFolder) + ".jpg";
var path = Path.Combine(series.Path, seasonFolder, filename);
var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename));
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
}

View File

@@ -1,10 +1,9 @@
using System;
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox
namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{
public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings>
{

View File

@@ -9,12 +9,13 @@ using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
{
public class WdtvMetadata : MetadataBase<WdtvMetadataSettings>
{
@@ -41,49 +42,23 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv
}
}
public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
{
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
var updatedMetadataFiles = new List<MetadataFile>();
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
foreach (var episodeFile in episodeFiles)
if (metadataFile.Type == MetadataType.EpisodeImage)
{
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var metadataFile in metadataFiles)
{
string newFilename;
if (metadataFile.Type == MetadataType.EpisodeImage)
{
newFilename = GetEpisodeImageFilename(episodeFile.RelativePath);
}
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
newFilename = GetEpisodeMetadataFilename(episodeFile.RelativePath);
}
else
{
_logger.Trace("Unknown episode file metadata: {0}", metadataFile.RelativePath);
continue;
}
var existingPath = Path.Combine(series.Path, metadataFile.RelativePath);
var newPath = Path.Combine(series.Path, newFilename);
if (!newPath.PathEquals(existingPath))
{
_diskProvider.MoveFile(existingPath, newPath);
metadataFile.RelativePath = newFilename;
updatedMetadataFiles.Add(metadataFile);
}
}
return GetEpisodeImageFilename(episodeFilePath);
}
return updatedMetadataFiles;
if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
return GetEpisodeMetadataFilename(episodeFilePath);
}
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath);
}
public override MetadataFile FindMetadataFile(Series series, string path)

View File

@@ -1,10 +1,9 @@
using System;
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv
namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
{
public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings>
{

View File

@@ -9,25 +9,23 @@ using System.Xml.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Xbmc
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
{
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public XbmcMetadata(IMapCoversToLocal mediaCoverService,
IDiskProvider diskProvider,
Logger logger)
{
_mediaCoverService = mediaCoverService;
_diskProvider = diskProvider;
_logger = logger;
}
@@ -43,49 +41,22 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
}
}
public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles)
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
{
var episodeFilesMetadata = existingMetadataFiles.Where(c => c.EpisodeFileId > 0).ToList();
var updatedMetadataFiles = new List<MetadataFile>();
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
foreach (var episodeFile in episodeFiles)
if (metadataFile.Type == MetadataType.EpisodeImage)
{
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var metadataFile in metadataFiles)
{
string newFilename;
if (metadataFile.Type == MetadataType.EpisodeImage)
{
newFilename = GetEpisodeImageFilename(episodeFile.RelativePath);
}
else if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
newFilename = GetEpisodeNfoFilename(episodeFile.RelativePath);
}
else
{
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
continue;
}
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
newFilename = Path.Combine(series.Path, newFilename);
if (!newFilename.PathEquals(existingFilename))
{
_diskProvider.MoveFile(existingFilename, newFilename);
metadataFile.RelativePath = series.Path.GetRelativePath(newFilename);
updatedMetadataFiles.Add(metadataFile);
}
}
return GetEpisodeImageFilename(episodeFilePath);
}
return updatedMetadataFiles;
if (metadataFile.Type == MetadataType.EpisodeMetadata)
{
return GetEpisodeMetadataFilename(episodeFilePath);
}
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath);
}
public override MetadataFile FindMetadataFile(Series series, string path)
@@ -328,7 +299,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
}
}
return new MetadataFileResult(GetEpisodeNfoFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
}
public override List<ImageFileResult> SeriesImages(Series series)
@@ -407,7 +378,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc
}
}
private string GetEpisodeNfoFilename(string episodeFilePath)
private string GetEpisodeMetadataFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "nfo");
}

View File

@@ -1,10 +1,9 @@
using System;
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.Xbmc
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
public class XbmcSettingsValidator : AbstractValidator<XbmcMetadataSettings>
{

View File

@@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Metadata
{
public class ExistingMetadataImporter : ImportExistingExtraFilesBase<MetadataFile>
{
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
private readonly List<IMetadata> _consumers;
public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileService,
IEnumerable<IMetadata> consumers,
IParsingService parsingService,
Logger logger)
: base(metadataFileService)
{
_metadataFileService = metadataFileService;
_parsingService = parsingService;
_logger = logger;
_consumers = consumers.ToList();
}
public override int Order
{
get
{
return 0;
}
}
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
{
_logger.Debug("Looking for existing metadata in {0}", series.Path);
var metadataFiles = new List<MetadataFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
{
// Don't process files that have known Subtitle file extensions (saves a bit of unecessary processing)
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(possibleMetadataFile)))
{
continue;
}
foreach (var consumer in _consumers)
{
var metadata = consumer.FindMetadataFile(series, possibleMetadataFile);
if (metadata == null)
{
continue;
}
if (metadata.Type == MetadataType.EpisodeImage ||
metadata.Type == MetadataType.EpisodeMetadata)
{
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series);
if (localEpisode == null)
{
_logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile);
continue;
}
if (localEpisode.Episodes.Empty())
{
_logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile);
continue;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile);
continue;
}
metadata.SeasonNumber = localEpisode.SeasonNumber;
metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId;
}
metadata.Extension = Path.GetExtension(possibleMetadataFile);
metadataFiles.Add(metadata);
}
}
_logger.Info("Found {0} existing metadata files", metadataFiles.Count);
_metadataFileService.Upsert(metadataFiles);
// Return files that were just imported along with files that were
// previously imported so previously imported files aren't imported twice
return metadataFiles.Concat(filterResult.PreviouslyImported);
}
}
}

View File

@@ -3,22 +3,22 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Files
namespace NzbDrone.Core.Extras.Metadata.Files
{
public interface ICleanMetadataService
{
void Clean(Series series);
}
public class CleanMetadataService : ICleanMetadataService
public class CleanExtraFileService : ICleanMetadataService
{
private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public CleanMetadataService(IMetadataFileService metadataFileService,
IDiskProvider diskProvider,
Logger logger)
public CleanExtraFileService(IMetadataFileService metadataFileService,
IDiskProvider diskProvider,
Logger logger)
{
_metadataFileService = metadataFileService;
_diskProvider = diskProvider;

View File

@@ -1,6 +1,4 @@
using System;
namespace NzbDrone.Core.Metadata.Files
namespace NzbDrone.Core.Extras.Metadata.Files
{
public class ImageFileResult
{

View File

@@ -0,0 +1,11 @@
using NzbDrone.Core.Extras.Files;
namespace NzbDrone.Core.Extras.Metadata.Files
{
public class MetadataFile : ExtraFile
{
public string Hash { get; set; }
public string Consumer { get; set; }
public MetadataType Type { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Extras.Metadata.Files
{
public interface IMetadataFileRepository : IExtraFileRepository<MetadataFile>
{
}
public class MetadataFileRepository : ExtraFileRepository<MetadataFile>, IMetadataFileRepository
{
public MetadataFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
namespace NzbDrone.Core.Metadata.Files
namespace NzbDrone.Core.Extras.Metadata.Files
{
public class MetadataFileResult
{

View File

@@ -0,0 +1,28 @@
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Metadata.Files
{
public interface IMetadataFileService : IExtraFileService<MetadataFile>
{
}
public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService
{
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
{
}
public override bool PermanentlyDelete
{
get
{
return true;
}
}
}
}

View File

@@ -1,21 +1,19 @@
using System.Collections.Generic;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public interface IMetadata : IProvider
{
List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile);
MetadataFile FindMetadataFile(Series series, string path);
MetadataFileResult SeriesMetadata(Series series);
MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
List<ImageFileResult> SeriesImages(Series series);
List<ImageFileResult> SeasonImages(Series series, Season season);
List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
}
}

View File

@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using FluentValidation.Results;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public abstract class MetadataBase<TSettings> : IMetadata where TSettings : IProviderConfig, new()
{
@@ -43,7 +45,15 @@ namespace NzbDrone.Core.Metadata
return new ValidationResult();
}
public abstract List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles);
public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile)
{
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath);
var extension = Path.GetExtension(existingFilename).TrimStart('.');
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
return newFileName;
}
public abstract MetadataFile FindMetadataFile(Series series, string path);
public abstract MetadataFileResult SeriesMetadata(Series series);

View File

@@ -1,6 +1,6 @@
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public class MetadataDefinition : ProviderDefinition
{

View File

@@ -6,7 +6,7 @@ using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public interface IMetadataFactory : IProviderFactory<IMetadata, MetadataDefinition>
{

View File

@@ -2,12 +2,10 @@
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public interface IMetadataRepository : IProviderRepository<MetadataDefinition>
{
}
public class MetadataRepository : ProviderRepository<MetadataDefinition>, IMetadataRepository

View File

@@ -7,159 +7,179 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public class MetadataService : IHandle<MediaCoversUpdatedEvent>,
IHandle<EpisodeImportedEvent>,
IHandle<EpisodeFolderCreatedEvent>,
IHandle<SeriesRenamedEvent>
public class MetadataService : ExtraFileManager<MetadataFile>
{
private readonly IMetadataFactory _metadataFactory;
private readonly IMetadataFileService _metadataFileService;
private readonly ICleanMetadataService _cleanMetadataService;
private readonly IMediaFileService _mediaFileService;
private readonly IEpisodeService _episodeService;
private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider;
private readonly IHttpClient _httpClient;
private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IEventAggregator _eventAggregator;
private readonly IMetadataFileService _metadataFileService;
private readonly Logger _logger;
public MetadataService(IMetadataFactory metadataFactory,
IMetadataFileService metadataFileService,
ICleanMetadataService cleanMetadataService,
IMediaFileService mediaFileService,
IEpisodeService episodeService,
public MetadataService(IConfigService configService,
IDiskTransferService diskTransferService,
IMetadataFactory metadataFactory,
ICleanMetadataService cleanMetadataService,
IDiskProvider diskProvider,
IHttpClient httpClient,
IMediaFileAttributeService mediaFileAttributeService,
IEventAggregator eventAggregator,
IMetadataFileService metadataFileService,
Logger logger)
: base(configService, diskTransferService, metadataFileService)
{
_metadataFactory = metadataFactory;
_metadataFileService = metadataFileService;
_cleanMetadataService = cleanMetadataService;
_mediaFileService = mediaFileService;
_episodeService = episodeService;
_diskTransferService = diskTransferService;
_diskProvider = diskProvider;
_httpClient = httpClient;
_mediaFileAttributeService = mediaFileAttributeService;
_eventAggregator = eventAggregator;
_metadataFileService = metadataFileService;
_logger = logger;
}
public void Handle(MediaCoversUpdatedEvent message)
public override int Order
{
_cleanMetadataService.Clean(message.Series);
get
{
return 0;
}
}
if (!_diskProvider.FolderExists(message.Series.Path))
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
{
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
_cleanMetadataService.Clean(series);
if (!_diskProvider.FolderExists(series.Path))
{
_logger.Info("Series folder does not exist, skipping metadata creation");
return;
return Enumerable.Empty<MetadataFile>();
}
var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id);
var episodeFiles = GetEpisodeFiles(message.Series.Id);
var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled())
{
var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles);
var files = new List<MetadataFile>();
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles));
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
foreach (var episodeFile in episodeFiles)
{
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles));
files.AddRange(ProcessEpisodeImages(consumer, message.Series, episodeFile, consumerFiles));
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles));
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(files));
}
_metadataFileService.Upsert(files);
return files;
}
public void Handle(EpisodeImportedEvent message)
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
{
foreach (var consumer in _metadataFactory.Enabled())
{
var files = new List<MetadataFile>();
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, message.EpisodeInfo.Series, message.ImportedEpisode, new List<MetadataFile>()));
_eventAggregator.PublishEvent(new MetadataFilesUpdated(files));
}
}
public void Handle(EpisodeFolderCreatedEvent message)
{
if (message.SeriesFolder.IsNullOrWhiteSpace() && message.SeasonFolder.IsNullOrWhiteSpace())
{
return;
}
var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id);
var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled())
{
var files = new List<MetadataFile>();
var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles);
if (message.SeriesFolder.IsNotNullOrWhiteSpace())
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>()));
}
_metadataFileService.Upsert(files);
return files;
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
{
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace())
{
return new List<MetadataFile>();
}
var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled())
{
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
if (seriesFolder.IsNotNullOrWhiteSpace())
{
files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles));
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
}
if (message.SeasonFolder.IsNotNullOrWhiteSpace())
if (seasonFolder.IsNotNullOrWhiteSpace())
{
files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
}
_eventAggregator.PublishEvent(new MetadataFilesUpdated(files));
}
_metadataFileService.Upsert(files);
return files;
}
public void Handle(SeriesRenamedEvent message)
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
var seriesMetadata = _metadataFileService.GetFilesBySeries(message.Series.Id);
var episodeFiles = GetEpisodeFiles(message.Series.Id);
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled())
// TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it
// (Xbmc's EpisodeImage is more than just the extension)
foreach (var consumer in _metadataFactory.GetAvailableProviders())
{
var updatedMetadataFiles = consumer.AfterRename(message.Series,
GetMetadataFilesForConsumer(consumer, seriesMetadata),
episodeFiles);
foreach (var episodeFile in episodeFiles)
{
var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
_eventAggregator.PublishEvent(new MetadataFilesUpdated(updatedMetadataFiles));
foreach (var metadataFile in metadataFilesForConsumer)
{
var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile);
var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath);
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
metadataFile.RelativePath = series.Path.GetRelativePath(newFileName);
movedFiles.Add(metadataFile);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move metadata file: {0}", existingFileName);
}
}
}
}
}
_metadataFileService.Upsert(movedFiles);
return movedFiles;
}
private List<EpisodeFile> GetEpisodeFiles(int seriesId)
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
{
var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId);
var episodes = _episodeService.GetEpisodeBySeries(seriesId);
foreach (var episodeFile in episodeFiles)
{
var localEpisodeFile = episodeFile;
episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id));
}
return episodeFiles;
return null;
}
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata)
@@ -205,6 +225,7 @@ namespace NzbDrone.Core.Metadata
metadata.Hash = hash;
metadata.RelativePath = seriesMetadata.RelativePath;
metadata.Extension = Path.GetExtension(fullPath);
return metadata;
}
@@ -226,7 +247,7 @@ namespace NzbDrone.Core.Metadata
if (existingMetadata != null)
{
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!fullPath.PathEquals(existingFullPath))
if (fullPath.PathNotEquals(existingFullPath))
{
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
existingMetadata.RelativePath = episodeMetadata.RelativePath;
@@ -239,10 +260,12 @@ namespace NzbDrone.Core.Metadata
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = episodeFile.SeasonNumber,
EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeMetadata,
RelativePath = episodeMetadata.RelativePath
RelativePath = episodeMetadata.RelativePath,
Extension = Path.GetExtension(fullPath)
};
if (hash == metadata.Hash)
@@ -279,7 +302,8 @@ namespace NzbDrone.Core.Metadata
SeriesId = series.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesImage,
RelativePath = image.RelativePath
RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath)
};
DownloadImage(series, image);
@@ -315,7 +339,8 @@ namespace NzbDrone.Core.Metadata
SeasonNumber = season.SeasonNumber,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeasonImage,
RelativePath = image.RelativePath
RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath)
};
DownloadImage(series, image);
@@ -347,7 +372,7 @@ namespace NzbDrone.Core.Metadata
if (existingMetadata != null)
{
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!fullPath.PathEquals(existingFullPath))
if (fullPath.PathNotEquals(existingFullPath))
{
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
existingMetadata.RelativePath = image.RelativePath;
@@ -360,10 +385,12 @@ namespace NzbDrone.Core.Metadata
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = episodeFile.SeasonNumber,
EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeImage,
RelativePath = image.RelativePath
RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath)
};
DownloadImage(series, image);

View File

@@ -1,4 +1,4 @@
namespace NzbDrone.Core.Metadata
namespace NzbDrone.Core.Extras.Metadata
{
public enum MetadataType
{

View File

@@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Others
{
public class ExistingOtherExtraImporter : ImportExistingExtraFilesBase<OtherExtraFile>
{
private readonly IExtraFileService<OtherExtraFile> _otherExtraFileService;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFileService,
IParsingService parsingService,
Logger logger)
: base(otherExtraFileService)
{
_otherExtraFileService = otherExtraFileService;
_parsingService = parsingService;
_logger = logger;
}
public override int Order
{
get
{
return 2;
}
}
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
{
_logger.Debug("Looking for existing extra files in {0}", series.Path);
var extraFiles = new List<OtherExtraFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleExtraFile in filterResult.FilesOnDisk)
{
var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series);
if (localEpisode == null)
{
_logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
continue;
}
if (localEpisode.Episodes.Empty())
{
_logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile);
continue;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile);
continue;
}
var extraFile = new OtherExtraFile
{
SeriesId = series.Id,
SeasonNumber = localEpisode.SeasonNumber,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId,
RelativePath = series.Path.GetRelativePath(possibleExtraFile),
Extension = Path.GetExtension(possibleExtraFile)
};
extraFiles.Add(extraFile);
}
_logger.Info("Found {0} existing other extra files", extraFiles.Count);
_otherExtraFileService.Upsert(extraFiles);
// Return files that were just imported along with files that were
// previously imported so previously imported files aren't imported twice
return extraFiles.Concat(filterResult.PreviouslyImported);
}
}
}

View File

@@ -0,0 +1,8 @@
using NzbDrone.Core.Extras.Files;
namespace NzbDrone.Core.Extras.Others
{
public class OtherExtraFile : ExtraFile
{
}
}

View File

@@ -0,0 +1,18 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Extras.Others
{
public interface IOtherExtraFileRepository : IExtraFileRepository<OtherExtraFile>
{
}
public class OtherExtraFileRepository : ExtraFileRepository<OtherExtraFile>, IOtherExtraFileRepository
{
public OtherExtraFileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View File

@@ -0,0 +1,20 @@
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Others
{
public interface IOtherExtraFileService : IExtraFileService<OtherExtraFile>
{
}
public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService
{
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger)
{
}
}
}

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