1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-16 21:15:28 -04:00

Compare commits

...

40 Commits

Author SHA1 Message Date
Mark McDowall
aaf2efb45d Fixed: Failing database migration of metadata files without extensions 2016-10-28 13:11:21 -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
128 changed files with 3022 additions and 1043 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

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

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

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

@@ -37,6 +37,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)

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

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

@@ -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,40 @@ 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")]
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

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

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

@@ -17,6 +17,7 @@
Running, // Running PP Script
Completed,
Failed,
Deleted
Deleted,
Propagating
}
}

View File

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

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

@@ -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,118 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
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 Logger _logger;
public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
ISeriesService seriesService,
IDiskProvider diskProvider,
Logger logger)
{
_repository = repository;
_seriesService = seriesService;
_diskProvider = diskProvider;
_logger = logger;
}
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))
{
_diskProvider.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,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.Xbmc
namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings>
{
@@ -43,49 +44,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 +302,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 +381,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,19 @@
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
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, Logger logger)
: base(repository, seriesService, diskProvider, logger)
{
}
}
}

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,19 @@
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
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, Logger logger)
: base(repository, seriesService, diskProvider, logger)
{
}
}
}

View File

@@ -0,0 +1,112 @@
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.Extras.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Others
{
public class OtherExtraService : ExtraFileManager<OtherExtraFile>
{
private readonly IOtherExtraFileService _otherExtraFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public OtherExtraService(IConfigService configService,
IDiskTransferService diskTransferService,
IOtherExtraFileService otherExtraFileService,
IDiskProvider diskProvider,
Logger logger)
: base(configService, diskTransferService, otherExtraFileService)
{
_otherExtraFileService = otherExtraFileService;
_diskProvider = diskProvider;
_logger = logger;
}
public override int Order
{
get
{
return 2;
}
}
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
{
return Enumerable.Empty<ExtraFile>();
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
{
return Enumerable.Empty<ExtraFile>();
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
{
return Enumerable.Empty<ExtraFile>();
}
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
// TODO: Remove
// We don't want to move files after rename yet.
return Enumerable.Empty<ExtraFile>();
var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<OtherExtraFile>();
foreach (var episodeFile in episodeFiles)
{
var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList();
foreach (var extraFile in extraFilesForEpisodeFile)
{
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath);
var extension = Path.GetExtension(existingFileName).TrimStart('.');
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
movedFiles.Add(extraFile);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move extra file: {0}", existingFileName);
}
}
}
}
_otherExtraFileService.Upsert(movedFiles);
return movedFiles;
}
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
{
// If the extension is .nfo we need to change it to .nfo-orig
if (Path.GetExtension(path).Equals(".nfo"))
{
extension += "-orig";
}
var extraFile = ImportFile(series, episodeFile, path, extension, readOnly);
_otherExtraFileService.Upsert(extraFile);
return extraFile;
}
}
}

View File

@@ -0,0 +1,92 @@
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.Subtitles
{
public class ExistingSubtitleImporter : ImportExistingExtraFilesBase<SubtitleFile>
{
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileService,
IParsingService parsingService,
Logger logger)
: base (subtitleFileService)
{
_subtitleFileService = subtitleFileService;
_parsingService = parsingService;
_logger = logger;
}
public override int Order
{
get
{
return 1;
}
}
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
{
_logger.Debug("Looking for existing subtitle files in {0}", series.Path);
var subtitleFiles = new List<SubtitleFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
{
var extension = Path.GetExtension(possibleSubtitleFile);
if (SubtitleFileExtensions.Extensions.Contains(extension))
{
var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series);
if (localEpisode == null)
{
_logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile);
continue;
}
if (localEpisode.Episodes.Empty())
{
_logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile);
continue;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile);
continue;
}
var subtitleFile = new SubtitleFile
{
SeriesId = series.Id,
SeasonNumber = localEpisode.SeasonNumber,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId,
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
Extension = extension
};
subtitleFiles.Add(subtitleFile);
}
}
_logger.Info("Found {0} existing subtitle files", subtitleFiles.Count);
_subtitleFileService.Upsert(subtitleFiles);
// Return files that were just imported along with files that were
// previously imported so previously imported files aren't imported twice
return subtitleFiles.Concat(filterResult.PreviouslyImported);
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using NzbDrone.Core.Extras.Files;
namespace NzbDrone.Core.Extras.Subtitles
{
public class ImportedSubtitleFiles
{
public List<string> SourceFiles { get; set; }
public List<ExtraFile> SubtitleFiles { get; set; }
public ImportedSubtitleFiles()
{
SourceFiles = new List<string>();
SubtitleFiles = new List<ExtraFile>();
}
}
}

View File

@@ -0,0 +1,10 @@
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Extras.Subtitles
{
public class SubtitleFile : ExtraFile
{
public Language Language { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Extras.Subtitles
{
public static class SubtitleFileExtensions
{
private static HashSet<string> _fileExtensions;
static SubtitleFileExtensions()
{
_fileExtensions = new HashSet<string>
{
".aqt",
".ass",
".idx",
".jss",
".psb",
".rt",
".smi",
".srt",
".ssa",
".sub",
".txt",
".utf",
".utf8",
".utf-8"
};
}
public static HashSet<string> Extensions
{
get { return _fileExtensions; }
}
}
}

View File

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

View File

@@ -0,0 +1,19 @@
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Subtitles
{
public interface ISubtitleFileService : IExtraFileService<SubtitleFile>
{
}
public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService
{
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, Logger logger)
: base(repository, seriesService, diskProvider, logger)
{
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Extras.Subtitles
{
public class SubtitleService : ExtraFileManager<SubtitleFile>
{
private readonly ISubtitleFileService _subtitleFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public SubtitleService(IConfigService configService,
IDiskTransferService diskTransferService,
ISubtitleFileService subtitleFileService,
IDiskProvider diskProvider,
Logger logger)
: base(configService, diskTransferService, subtitleFileService)
{
_subtitleFileService = subtitleFileService;
_diskProvider = diskProvider;
_logger = logger;
}
public override int Order
{
get
{
return 1;
}
}
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles)
{
return Enumerable.Empty<SubtitleFile>();
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
{
return Enumerable.Empty<SubtitleFile>();
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
{
return Enumerable.Empty<SubtitleFile>();
}
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
// TODO: Remove
// We don't want to move files after rename yet.
return Enumerable.Empty<ExtraFile>();
var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<SubtitleFile>();
foreach (var episodeFile in episodeFiles)
{
var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id)
.GroupBy(s => s.Language + s.Extension).ToList();
foreach (var group in groupedExtraFilesForEpisodeFile)
{
var groupCount = group.Count();
var copy = 1;
if (groupCount > 1)
{
_logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath));
}
foreach (var extraFile in group)
{
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath);
var extension = GetExtension(extraFile, existingFileName, copy, groupCount > 1);
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
movedFiles.Add(extraFile);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move subtitle file: {0}", existingFileName);
}
}
copy++;
}
}
}
_subtitleFileService.Upsert(movedFiles);
return movedFiles;
}
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly)
{
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
{
var subtitleFile = ImportFile(series, episodeFile, path, extension, readOnly);
subtitleFile.Language = LanguageParser.ParseSubtitleLanguage(path);
_subtitleFileService.Upsert(subtitleFile);
return subtitleFile;
}
return null;
}
private string GetExtension(SubtitleFile extraFile, string existingFileName, int copy, bool multipleCopies = false)
{
var fileExtension = Path.GetExtension(existingFileName);
var extensionBuilder = new StringBuilder();
if (multipleCopies)
{
extensionBuilder.Append(copy);
extensionBuilder.Append(".");
}
if (extraFile.Language != Language.Unknown)
{
extensionBuilder.Append(IsoLanguages.Get(extraFile.Language).TwoLetterCode);
extensionBuilder.Append(".");
}
extensionBuilder.Append(fileExtension.TrimStart('.'));
return extensionBuilder.ToString();
}
}
}

View File

@@ -39,14 +39,20 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, "your mono version 3.4.0 has a critical bug, you should upgrade to a higher version");
}
if (version >= new Version(3, 2))
if (version >= new Version(4, 4, 0) && version < new Version(4, 5))
{
_logger.Debug("mono version is 3.2 or better: {0}", version.ToString());
_logger.Debug("mono version {0}", version);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"your mono version {version} has a bug that causes issues connecting to indexers/download clients");
}
if (version >= new Version(3, 10))
{
_logger.Debug("mono version is 3.10 or better: {0}", version.ToString());
return new HealthCheck(GetType());
}
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, "mono version is less than 3.2, upgrade for improved stability");
return new HealthCheck(GetType(), HealthCheckResult.Warning, "mono version is less than 3.10, upgrade for improved stability");
}
public override bool CheckOnConfigChange

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Marr.Data;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class CleanupUnusedTags : IHousekeepingTask
{
private readonly IMainDatabase _database;
public CleanupUnusedTags(IMainDatabase database)
{
_database = database;
}
public void Clean()
{
var mapper = _database.GetDataMapper();
var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "Restrictions" }
.SelectMany(v => GetUsedTags(v, mapper))
.Distinct()
.ToArray();
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
mapper.ExecuteNonQuery($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})");
}
private int[] GetUsedTags(string table, IDataMapper mapper)
{
return mapper.ExecuteReader($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'", reader => reader.GetString(0))
.SelectMany(Json.Deserialize<List<int>>)
.Distinct()
.ToArray();
}
}
}

View File

@@ -4,23 +4,27 @@ using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Housekeeping.Housekeepers
{
public class DeleteBadMediaCovers : IHousekeepingTask
{
private readonly IMetadataFileService _metaFileService;
private readonly ISeriesService _seriesService;
private readonly IMetadataFileService _metadataFileService;
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public DeleteBadMediaCovers(ISeriesService seriesService, IMetadataFileService metadataFileService, IDiskProvider diskProvider, IConfigService configService, Logger logger)
public DeleteBadMediaCovers(IMetadataFileService metaFileService,
ISeriesService seriesService,
IDiskProvider diskProvider,
IConfigService configService,
Logger logger)
{
_metaFileService = metaFileService;
_seriesService = seriesService;
_metadataFileService = metadataFileService;
_diskProvider = diskProvider;
_configService = configService;
_logger = logger;
@@ -34,7 +38,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
foreach (var show in series)
{
var images = _metadataFileService.GetFilesBySeries(show.Id)
var images = _metaFileService.GetFilesBySeries(show.Id)
.Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase));
foreach (var image in images)
@@ -61,7 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
private void DeleteMetadata(int id, string path)
{
_metadataFileService.Delete(id);
_metaFileService.Delete(id);
_diskProvider.DeleteFile(path);
}

View File

@@ -16,8 +16,8 @@ namespace NzbDrone.Core.Indexers.IPTorrents
RuleFor(c => c.Url).Matches(@"/rss\?.+$");
RuleFor(c => c.Url).Matches(@"/rss\?.+;download$")
.WithMessage("Use Direct Download Url")
RuleFor(c => c.Url).Matches(@"/rss\?.+;download(?:;|$)")
.WithMessage("Use Direct Download Url (;download)")
.When(v => v.Url.IsNotNullOrWhiteSpace() && Regex.IsMatch(v.Url, @"/rss\?.+$"));
}
}
@@ -38,4 +38,4 @@ namespace NzbDrone.Core.Indexers.IPTorrents
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
}

View File

@@ -51,17 +51,11 @@ namespace NzbDrone.Core.Indexers.Nyaa
{
var searchTitle = PrepareQuery(queryTitle);
pageableRequests.Add(GetPagedRequests(MaxPages,
string.Format("&term={0}+{1:0}",
searchTitle,
searchCriteria.AbsoluteEpisodeNumber)));
pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:0}"));
if (searchCriteria.AbsoluteEpisodeNumber < 10)
{
pageableRequests.Add(GetPagedRequests(MaxPages,
string.Format("&term={0}+{1:00}",
searchTitle,
searchCriteria.AbsoluteEpisodeNumber)));
pageableRequests.Add(GetPagedRequests(MaxPages, $"{searchTitle}+{searchCriteria.AbsoluteEpisodeNumber:00}"));
}
}
@@ -82,19 +76,26 @@ namespace NzbDrone.Core.Indexers.Nyaa
return pageableRequests;
}
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, string searchParameters)
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, string term)
{
var baseUrl = string.Format("{0}/?page=rss{1}", Settings.BaseUrl.TrimEnd('/'), Settings.AdditionalParameters);
if (term != null)
{
baseUrl += "&term=" + term;
}
if (PageSize == 0)
{
yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, searchParameters), HttpAccept.Rss);
yield return new IndexerRequest(baseUrl, HttpAccept.Rss);
}
else
{
for (var page = 0; page < maxPages; page++)
yield return new IndexerRequest(baseUrl, HttpAccept.Rss);
for (var page = 1; page < maxPages; page++)
{
yield return new IndexerRequest(string.Format("{0}&offset={1}{2}", baseUrl, page + 1, searchParameters), HttpAccept.Rss);
yield return new IndexerRequest($"{baseUrl}&offset={page + 1}", HttpAccept.Rss);
}
}
}

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