New: Parse existing subtitles and extra files

Towards #459
This commit is contained in:
Mark McDowall 2015-12-25 01:22:00 -08:00
parent 816cf608fc
commit 2e96c4e798
78 changed files with 2013 additions and 678 deletions

6
.idea/encodings.xml 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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptLibraryMappings"> <component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{Sonarr node_modules}" />
<includedPredefinedLibrary name="ECMAScript 6" /> <includedPredefinedLibrary name="ECMAScript 6" />
</component> </component>
</project> </project>

View File

@ -1,5 +1,4 @@
using System; using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Validation.Paths; 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.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -21,6 +20,7 @@ namespace NzbDrone.Api.Config
public bool SkipFreeSpaceCheckWhenImporting { get; set; } public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; } public bool CopyUsingHardlinks { get; set; }
public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; } public bool EnableMediaInfo { get; set; }
} }

View File

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

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\EnvironmentProviderTest.cs" />
<Compile Include="EnvironmentTests\StartupArgumentsFixture.cs" /> <Compile Include="EnvironmentTests\StartupArgumentsFixture.cs" />
<Compile Include="ExtensionTests\FromOctalStringFixture.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="ExtensionTests\Int64ExtensionFixture.cs" />
<Compile Include="Http\HttpClientFixture.cs" /> <Compile Include="Http\HttpClientFixture.cs" />
<Compile Include="Http\HttpRequestBuilderFixture.cs" /> <Compile Include="Http\HttpRequestBuilderFixture.cs" />

View File

@ -13,6 +13,44 @@ namespace NzbDrone.Common.Extensions
return source.Where(element => knownKeys.Add(keySelector(element))); 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) public static void AddIfNotNull<TSource>(this List<TSource> source, TSource item)
{ {
if (item == null) if (item == null)

View File

@ -37,6 +37,11 @@ namespace NzbDrone.Common.Extensions
return info.FullName.TrimEnd('/').Trim('\\', ' '); 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) public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
{ {
if (!comparison.HasValue) if (!comparison.HasValue)

View File

@ -0,0 +1,33 @@
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"
});
});
var items = db.Query<MetadataFile99>("SELECT * FROM MetadataFiles");
items.Should().HaveCount(1);
items.First().Extension.Should().Be(".jpg");
}
}
}

View File

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

View File

@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.HistoryTests
Path = @"C:\Test\Unsorted\Series.s01e01.mkv" 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>() Mocker.GetMock<IHistoryRepository>()
.Verify(v => v.Insert(It.Is<History.History>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path)))); .Verify(v => v.Insert(It.Is<History.History>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path))));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -119,6 +119,7 @@
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" /> <Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
<Compile Include="Datastore\MappingExtentionFixture.cs" /> <Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" /> <Compile Include="Datastore\MarrDataLazyLoadingFixture.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\101_add_ultrahd_quality_in_profilesFixture.cs" />
<Compile Include="Datastore\Migration\071_unknown_quality_in_profileFixture.cs" /> <Compile Include="Datastore\Migration\071_unknown_quality_in_profileFixture.cs" />
<Compile Include="Datastore\Migration\072_history_downloadIdFixture.cs" /> <Compile Include="Datastore\Migration\072_history_downloadIdFixture.cs" />

View File

@ -48,8 +48,17 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)] [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)]
public void should_parse_language(string postTitle, Language language) public void should_parse_language(string postTitle, Language language)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = LanguageParser.ParseLanguage(postTitle);
result.Language.Should().Be(language); 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

@ -202,6 +202,13 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EnableMediaInfo", value); } set { SetValue("EnableMediaInfo", value); }
} }
public string ExtraFileExtensions
{
get { return GetValue("ExtraFileExtensions", ""); }
set { SetValue("ExtraFileExtensions", value); }
}
public bool SetPermissionsLinux public bool SetPermissionsLinux
{ {
get { return GetValueBoolean("SetPermissionsLinux", false); } get { return GetValueBoolean("SetPermissionsLinux", false); }

View File

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

View File

@ -0,0 +1,56 @@
using System;
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();
// Set Extension using the extension from RelativePath
Execute.Sql("UPDATE MetadataFiles SET Extension = substr(RelativePath, instr(RelativePath, '.'));");
Alter.Table("MetadataFiles").AlterColumn("Extension").AsString().NotNullable();
}
}
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

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

View File

@ -0,0 +1,61 @@
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 List<IImportExistingExtraFiles> _existingExtraFileImporters;
private readonly List<IManageExtraFiles> _extraFileManagers;
private readonly Logger _logger;
public ExistingExtraFileService(IDiskProvider diskProvider,
List<IImportExistingExtraFiles> existingExtraFileImporters,
List<IManageExtraFiles> extraFileManagers,
Logger logger)
{
_diskProvider = diskProvider;
_existingExtraFileImporters = existingExtraFileImporters.OrderBy(e => e.Order).ToList();
_extraFileManagers = extraFileManagers.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 = _diskProvider.GetFiles(series.Path, SearchOption.AllDirectories);
var possibleExtraFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) &&
!c.StartsWith(Path.Combine(series.Path, "EXTRAS"))).ToList();
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);
}
}
}

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)
{
// TODO: Remove
// Not importing files yet, testing that parsing is working properly first
return;
var series = localEpisode.Series;
foreach (var extraFileManager in _extraFileManagers)
{
extraFileManager.CreateAfterEpisodeImport(series, episodeFile);
}
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 System;
using NzbDrone.Core.Datastore; 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 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? EpisodeFileId { get; set; }
public int? SeasonNumber { 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.Datastore;
using NzbDrone.Core.Messaging.Events; 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 DeleteForSeries(int seriesId);
void DeleteForSeason(int seriesId, int seasonNumber); void DeleteForSeason(int seriesId, int seasonNumber);
void DeleteForEpisodeFile(int episodeFileId); void DeleteForEpisodeFile(int episodeFileId);
List<MetadataFile> GetFilesBySeries(int seriesId); List<TExtraFile> GetFilesBySeries(int seriesId);
List<MetadataFile> GetFilesBySeason(int seriesId, int seasonNumber); List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<MetadataFile> GetFilesByEpisodeFile(int episodeFileId); List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
MetadataFile FindByPath(string path); 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) : base(database, eventAggregator)
{ {
} }
@ -38,22 +39,22 @@ namespace NzbDrone.Core.Metadata.Files
Delete(c => c.EpisodeFileId == episodeFileId); Delete(c => c.EpisodeFileId == episodeFileId);
} }
public List<MetadataFile> GetFilesBySeries(int seriesId) public List<TExtraFile> GetFilesBySeries(int seriesId)
{ {
return Query.Where(c => c.SeriesId == 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); 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); 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(); 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,53 @@
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 List<string> 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 List<string> Filter(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles)
{
var filteredFiles = filesOnDisk;
filteredFiles = filteredFiles.Except(seriesFiles.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance).ToList();
return filteredFiles.Except(importedFiles, PathEqualityComparer.Instance).ToList();
}
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;
using System.Xml.Linq; using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
{ {
public class MediaBrowserMetadata : MetadataBase<MediaBrowserMetadataSettings> public class MediaBrowserMetadata : MetadataBase<MediaBrowserMetadataSettings>
{ {
private readonly IMapCoversToLocal _mediaCoverService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger; private readonly Logger _logger;
public MediaBrowserMetadata(IMapCoversToLocal mediaCoverService, public MediaBrowserMetadata(
IDiskProvider diskProvider,
Logger logger) Logger logger)
{ {
_mediaCoverService = mediaCoverService;
_diskProvider = diskProvider;
_logger = logger; _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) public override MetadataFile FindMetadataFile(Series series, string path)
{ {
var filename = Path.GetFileName(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.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.MediaBrowser namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
{ {
public class MediaBrowserSettingsValidator : AbstractValidator<MediaBrowserMetadataSettings> public class MediaBrowserSettingsValidator : AbstractValidator<MediaBrowserMetadataSettings>
{ {

View File

@ -9,12 +9,13 @@ using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{ {
public class RoksboxMetadata : MetadataBase<RoksboxMetadataSettings> 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 episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
var updatedMetadataFiles = new List<MetadataFile>();
foreach (var episodeFile in episodeFiles) if (metadataFile.Type == MetadataType.EpisodeImage)
{ {
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); return GetEpisodeImageFilename(episodeFilePath);
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 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) 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.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.Roksbox namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
{ {
public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings> public class RoksboxSettingsValidator : AbstractValidator<RoksboxMetadataSettings>
{ {

View File

@ -9,12 +9,13 @@ using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
{ {
public class WdtvMetadata : MetadataBase<WdtvMetadataSettings> 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 episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
var updatedMetadataFiles = new List<MetadataFile>();
foreach (var episodeFile in episodeFiles) if (metadataFile.Type == MetadataType.EpisodeImage)
{ {
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); return GetEpisodeImageFilename(episodeFilePath);
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 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) 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.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Metadata.Consumers.Wdtv namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
{ {
public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings> public class WdtvSettingsValidator : AbstractValidator<WdtvMetadataSettings>
{ {

View File

@ -9,12 +9,13 @@ using System.Xml.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata.Consumers.Xbmc namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
{ {
public class XbmcMetadata : MetadataBase<XbmcMetadataSettings> 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 episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
var updatedMetadataFiles = new List<MetadataFile>();
foreach (var episodeFile in episodeFiles) if (metadataFile.Type == MetadataType.EpisodeImage)
{ {
var metadataFiles = episodeFilesMetadata.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); return GetEpisodeImageFilename(episodeFilePath);
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 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) 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) 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"); return Path.ChangeExtension(episodeFilePath, "nfo");
} }

View File

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

View File

@ -0,0 +1,96 @@
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.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 filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleMetadataFile in filteredFiles)
{
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 metadataFiles;
}
}
}

View File

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

View File

@ -1,6 +1,4 @@
using System; namespace NzbDrone.Core.Extras.Metadata.Files
namespace NzbDrone.Core.Metadata.Files
{ {
public class ImageFileResult 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.Extras.Metadata.Files
namespace NzbDrone.Core.Metadata.Files
{ {
public class MetadataFileResult 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 System.Collections.Generic;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public interface IMetadata : IProvider 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); MetadataFile FindMetadataFile(Series series, string path);
MetadataFileResult SeriesMetadata(Series series); MetadataFileResult SeriesMetadata(Series series);
MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile);
List<ImageFileResult> SeriesImages(Series series); List<ImageFileResult> SeriesImages(Series series);
List<ImageFileResult> SeasonImages(Series series, Season season); List<ImageFileResult> SeasonImages(Series series, Season season);
List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile); List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
} }
} }

View File

@ -1,12 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using FluentValidation.Results; using FluentValidation.Results;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public abstract class MetadataBase<TSettings> : IMetadata where TSettings : IProviderConfig, new() public abstract class MetadataBase<TSettings> : IMetadata where TSettings : IProviderConfig, new()
{ {
@ -43,7 +45,15 @@ namespace NzbDrone.Core.Metadata
return new ValidationResult(); 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 MetadataFile FindMetadataFile(Series series, string path);
public abstract MetadataFileResult SeriesMetadata(Series series); public abstract MetadataFileResult SeriesMetadata(Series series);

View File

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

View File

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

View File

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

View File

@ -7,159 +7,179 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public class MetadataService : IHandle<MediaCoversUpdatedEvent>, public class MetadataService : ExtraFileManager<MetadataFile>
IHandle<EpisodeImportedEvent>,
IHandle<EpisodeFolderCreatedEvent>,
IHandle<SeriesRenamedEvent>
{ {
private readonly IMetadataFactory _metadataFactory; private readonly IMetadataFactory _metadataFactory;
private readonly IMetadataFileService _metadataFileService;
private readonly ICleanMetadataService _cleanMetadataService; private readonly ICleanMetadataService _cleanMetadataService;
private readonly IMediaFileService _mediaFileService;
private readonly IEpisodeService _episodeService;
private readonly IDiskTransferService _diskTransferService; private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IEventAggregator _eventAggregator; private readonly IMetadataFileService _metadataFileService;
private readonly Logger _logger; private readonly Logger _logger;
public MetadataService(IMetadataFactory metadataFactory, public MetadataService(IConfigService configService,
IMetadataFileService metadataFileService,
ICleanMetadataService cleanMetadataService,
IMediaFileService mediaFileService,
IEpisodeService episodeService,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
IMetadataFactory metadataFactory,
ICleanMetadataService cleanMetadataService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IHttpClient httpClient, IHttpClient httpClient,
IMediaFileAttributeService mediaFileAttributeService, IMediaFileAttributeService mediaFileAttributeService,
IEventAggregator eventAggregator, IMetadataFileService metadataFileService,
Logger logger) Logger logger)
: base(configService, diskTransferService, metadataFileService)
{ {
_metadataFactory = metadataFactory; _metadataFactory = metadataFactory;
_metadataFileService = metadataFileService;
_cleanMetadataService = cleanMetadataService; _cleanMetadataService = cleanMetadataService;
_mediaFileService = mediaFileService;
_episodeService = episodeService;
_diskTransferService = diskTransferService; _diskTransferService = diskTransferService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_httpClient = httpClient; _httpClient = httpClient;
_mediaFileAttributeService = mediaFileAttributeService; _mediaFileAttributeService = mediaFileAttributeService;
_eventAggregator = eventAggregator; _metadataFileService = metadataFileService;
_logger = logger; _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"); _logger.Info("Series folder does not exist, skipping metadata creation");
return; return Enumerable.Empty<MetadataFile>();
} }
var seriesMetadataFiles = _metadataFileService.GetFilesBySeries(message.Series.Id); var files = new List<MetadataFile>();
var episodeFiles = GetEpisodeFiles(message.Series.Id);
foreach (var consumer in _metadataFactory.Enabled()) foreach (var consumer in _metadataFactory.Enabled())
{ {
var consumerFiles = GetMetadataFilesForConsumer(consumer, seriesMetadataFiles); var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
var files = new List<MetadataFile>();
files.AddIfNotNull(ProcessSeriesMetadata(consumer, message.Series, consumerFiles)); files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, message.Series, consumerFiles)); files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, message.Series, consumerFiles)); files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
foreach (var episodeFile in episodeFiles) foreach (var episodeFile in episodeFiles)
{ {
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, message.Series, episodeFile, consumerFiles)); files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles));
files.AddRange(ProcessEpisodeImages(consumer, message.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>();
{
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);
foreach (var consumer in _metadataFactory.Enabled()) 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.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, message.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 metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
var episodeFiles = GetEpisodeFiles(message.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, foreach (var episodeFile in episodeFiles)
GetMetadataFilesForConsumer(consumer, seriesMetadata), {
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); return null;
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;
} }
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata) private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata)
@ -226,7 +246,7 @@ namespace NzbDrone.Core.Metadata
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!fullPath.PathEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
existingMetadata.RelativePath = episodeMetadata.RelativePath; existingMetadata.RelativePath = episodeMetadata.RelativePath;
@ -239,6 +259,7 @@ namespace NzbDrone.Core.Metadata
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, SeriesId = series.Id,
SeasonNumber = episodeFile.SeasonNumber,
EpisodeFileId = episodeFile.Id, EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeMetadata, Type = MetadataType.EpisodeMetadata,
@ -347,7 +368,7 @@ namespace NzbDrone.Core.Metadata
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath);
if (!fullPath.PathEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
existingMetadata.RelativePath = image.RelativePath; existingMetadata.RelativePath = image.RelativePath;
@ -360,6 +381,7 @@ namespace NzbDrone.Core.Metadata
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, SeriesId = series.Id,
SeasonNumber = episodeFile.SeasonNumber,
EpisodeFileId = episodeFile.Id, EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeImage, Type = MetadataType.EpisodeImage,

View File

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

View File

@ -0,0 +1,83 @@
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 filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleExtraFile in filteredFiles)
{
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 extraFiles;
}
}
}

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,89 @@
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 filteredFiles = FilterAndClean(series, filesOnDisk, importedFiles);
foreach (var possibleSubtitleFile in filteredFiles)
{
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 subtitleFiles;
}
}
}

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

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

View File

@ -11,6 +11,7 @@ using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Extras;
namespace NzbDrone.Core.MediaFiles.EpisodeImport namespace NzbDrone.Core.MediaFiles.EpisodeImport
@ -24,18 +25,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{ {
private readonly IUpgradeMediaFiles _episodeFileUpgrader; private readonly IUpgradeMediaFiles _episodeFileUpgrader;
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IExtraService _extraService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader, public ImportApprovedEpisodes(IUpgradeMediaFiles episodeFileUpgrader,
IMediaFileService mediaFileService, IMediaFileService mediaFileService,
IExtraService extraService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
{ {
_episodeFileUpgrader = episodeFileUpgrader; _episodeFileUpgrader = episodeFileUpgrader;
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_extraService = extraService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
@ -98,9 +102,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_mediaFileService.Add(episodeFile); _mediaFileService.Add(episodeFile);
importResults.Add(new ImportResult(importDecision)); importResults.Add(new ImportResult(importDecision));
if (newDownload)
{
_extraService.ImportExtraFiles(localEpisode, episodeFile, downloadClientItem != null && downloadClientItem.IsReadOnly);
}
if (downloadClientItem != null) if (downloadClientItem != null)
{ {
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId)); _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly));
} }
else else
{ {

View File

@ -1,5 +1,4 @@
using System; using NzbDrone.Common.Messaging;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.Events namespace NzbDrone.Core.MediaFiles.Events
@ -11,6 +10,7 @@ namespace NzbDrone.Core.MediaFiles.Events
public bool NewDownload { get; private set; } public bool NewDownload { get; private set; }
public string DownloadClient { get; private set; } public string DownloadClient { get; private set; }
public string DownloadId { get; private set; } public string DownloadId { get; private set; }
public bool IsReadOnly { get; set; }
public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload)
{ {
@ -19,13 +19,14 @@ namespace NzbDrone.Core.MediaFiles.Events
NewDownload = newDownload; NewDownload = newDownload;
} }
public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId) public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId, bool isReadOnly)
{ {
EpisodeInfo = episodeInfo; EpisodeInfo = episodeInfo;
ImportedEpisode = importedEpisode; ImportedEpisode = importedEpisode;
NewDownload = newDownload; NewDownload = newDownload;
DownloadClient = downloadClient; DownloadClient = downloadClient;
DownloadId = downloadId; DownloadId = downloadId;
IsReadOnly = isReadOnly;
} }
} }
} }

View File

@ -1,92 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Files;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Metadata
{
public class ExistingMetadataService : IHandle<SeriesScannedEvent>
{
private readonly IDiskProvider _diskProvider;
private readonly IMetadataFileService _metadataFileService;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
private readonly List<IMetadata> _consumers;
public ExistingMetadataService(IDiskProvider diskProvider,
IEnumerable<IMetadata> consumers,
IMetadataFileService metadataFileService,
IParsingService parsingService,
Logger logger)
{
_diskProvider = diskProvider;
_metadataFileService = metadataFileService;
_parsingService = parsingService;
_logger = logger;
_consumers = consumers.ToList();
}
public void Handle(SeriesScannedEvent message)
{
if (!_diskProvider.FolderExists(message.Series.Path)) return;
_logger.Debug("Looking for existing metadata in {0}", message.Series.Path);
var filesOnDisk = _diskProvider.GetFiles(message.Series.Path, SearchOption.AllDirectories);
var possibleMetadataFiles = filesOnDisk.Where(c => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(c).ToLower()) &&
!c.StartsWith(Path.Combine(message.Series.Path, "EXTRAS"))).ToList();
var filteredFiles = _metadataFileService.FilterExistingFiles(possibleMetadataFiles, message.Series);
var metadataFiles = new List<MetadataFile>();
foreach (var possibleMetadataFile in filteredFiles)
{
foreach (var consumer in _consumers)
{
var metadata = consumer.FindMetadataFile(message.Series, possibleMetadataFile);
if (metadata == null) continue;
if (metadata.Type == MetadataType.EpisodeImage ||
metadata.Type == MetadataType.EpisodeMetadata)
{
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, message.Series);
if (localEpisode == null)
{
_logger.Debug("Unable to parse meta data file: {0}", possibleMetadataFile);
break;
}
if (localEpisode.Episodes.Empty())
{
_logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile);
break;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Metadata file: {0} does not match existing files.", possibleMetadataFile);
break;
}
metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId;
}
metadataFiles.Add(metadata);
}
}
_metadataFileService.Upsert(metadataFiles);
}
}
}

View File

@ -1,113 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common;
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.Metadata.Files
{
public interface IMetadataFileService
{
List<MetadataFile> GetFilesBySeries(int seriesId);
List<MetadataFile> GetFilesByEpisodeFile(int episodeFileId);
MetadataFile FindByPath(string path);
List<string> FilterExistingFiles(List<string> files, Series series);
void Upsert(List<MetadataFile> metadataFiles);
void Delete(int id);
}
public class MetadataFileService : IMetadataFileService,
IHandleAsync<SeriesDeletedEvent>,
IHandleAsync<EpisodeFileDeletedEvent>,
IHandle<MetadataFilesUpdated>
{
private readonly IMetadataFileRepository _repository;
private readonly ISeriesService _seriesService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public MetadataFileService(IMetadataFileRepository repository,
ISeriesService seriesService,
IDiskProvider diskProvider,
Logger logger)
{
_repository = repository;
_seriesService = seriesService;
_diskProvider = diskProvider;
_logger = logger;
}
public List<MetadataFile> GetFilesBySeries(int seriesId)
{
return _repository.GetFilesBySeries(seriesId);
}
public List<MetadataFile> GetFilesByEpisodeFile(int episodeFileId)
{
return _repository.GetFilesByEpisodeFile(episodeFileId);
}
public MetadataFile FindByPath(string path)
{
return _repository.FindByPath(path);
}
public List<string> FilterExistingFiles(List<string> files, Series series)
{
var seriesFiles = GetFilesBySeries(series.Id).Select(f => Path.Combine(series.Path, f.RelativePath)).ToList();
if (!seriesFiles.Any()) return files;
return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList();
}
public void Upsert(List<MetadataFile> metadataFiles)
{
metadataFiles.ForEach(m => m.LastUpdated = DateTime.UtcNow);
_repository.InsertMany(metadataFiles.Where(m => m.Id == 0).ToList());
_repository.UpdateMany(metadataFiles.Where(m => m.Id > 0).ToList());
}
public void Delete(int id)
{
_repository.Delete(id);
}
public void HandleAsync(SeriesDeletedEvent message)
{
_logger.Debug("Deleting Metadata 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 metadata in _repository.GetFilesByEpisodeFile(episodeFile.Id))
{
var path = Path.Combine(series.Path, metadata.RelativePath);
if (_diskProvider.FileExists(path))
{
_diskProvider.DeleteFile(path);
}
}
_logger.Debug("Deleting Metadata from database for episode file: {0}", episodeFile);
_repository.DeleteForEpisodeFile(episodeFile.Id);
}
public void Handle(MetadataFilesUpdated message)
{
Upsert(message.MetadataFiles);
}
}
}

View File

@ -1,15 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Metadata.Files
{
public class MetadataFilesUpdated : IEvent
{
public List<MetadataFile> MetadataFiles { get; set; }
public MetadataFilesUpdated(List<MetadataFile> metadataFiles)
{
MetadataFiles = metadataFiles;
}
}
}

View File

@ -275,6 +275,7 @@
<Compile Include="Datastore\Migration\093_naming_config_replace_characters.cs" /> <Compile Include="Datastore\Migration\093_naming_config_replace_characters.cs" />
<Compile Include="Datastore\Migration\092_add_unverifiedscenenumbering.cs" /> <Compile Include="Datastore\Migration\092_add_unverifiedscenenumbering.cs" />
<Compile Include="Datastore\Migration\100_add_scene_season_number.cs" /> <Compile Include="Datastore\Migration\100_add_scene_season_number.cs" />
<Compile Include="Datastore\Migration\099_extra_and_subtitle_files.cs" />
<Compile Include="Datastore\Migration\094_add_tvmazeid.cs" /> <Compile Include="Datastore\Migration\094_add_tvmazeid.cs" />
<Compile Include="Datastore\Migration\098_remove_titans_of_tv.cs"> <Compile Include="Datastore\Migration\098_remove_titans_of_tv.cs">
<SubType>Code</SubType> <SubType>Code</SubType>
@ -487,6 +488,29 @@
<Compile Include="Exceptions\SeriesNotFoundException.cs" /> <Compile Include="Exceptions\SeriesNotFoundException.cs" />
<Compile Include="Exceptions\ReleaseDownloadException.cs" /> <Compile Include="Exceptions\ReleaseDownloadException.cs" />
<Compile Include="Exceptions\StatusCodeToExceptions.cs" /> <Compile Include="Exceptions\StatusCodeToExceptions.cs" />
<Compile Include="Extras\ExistingExtraFileService.cs" />
<Compile Include="Extras\Files\ExtraFile.cs" />
<Compile Include="Extras\Files\ExtraFileManager.cs" />
<Compile Include="Extras\Files\ExtraFileService.cs" />
<Compile Include="Extras\Files\ExtraFileRepository.cs" />
<Compile Include="Extras\ExtraService.cs" />
<Compile Include="Extras\IImportExistingExtraFiles.cs" />
<Compile Include="Extras\ImportExistingExtraFilesBase.cs" />
<Compile Include="Extras\Metadata\Files\MetadataFile.cs" />
<Compile Include="Extras\Metadata\Files\MetadataFileRepository.cs" />
<Compile Include="Extras\Metadata\Files\MetadataFileService.cs" />
<Compile Include="Extras\Others\ExistingOtherExtraImporter.cs" />
<Compile Include="Extras\Others\OtherExtraFileRepository.cs" />
<Compile Include="Extras\Others\OtherExtraFileService.cs" />
<Compile Include="Extras\Others\OtherExtraFile.cs" />
<Compile Include="Extras\Others\OtherExtraService.cs" />
<Compile Include="Extras\Subtitles\ExistingSubtitleImporter.cs" />
<Compile Include="Extras\Subtitles\SubtitleFileRepository.cs" />
<Compile Include="Extras\Subtitles\SubtitleFileService.cs" />
<Compile Include="Extras\Subtitles\SubtitleFile.cs" />
<Compile Include="Extras\Subtitles\SubtitleFileExtensions.cs" />
<Compile Include="Extras\Subtitles\ImportedSubtitleFiles.cs" />
<Compile Include="Extras\Subtitles\SubtitleService.cs" />
<Compile Include="Fluent.cs" /> <Compile Include="Fluent.cs" />
<Compile Include="HealthCheck\CheckHealthCommand.cs" /> <Compile Include="HealthCheck\CheckHealthCommand.cs" />
<Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" /> <Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" />
@ -757,29 +781,25 @@
<Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" />
<Compile Include="MetadataSource\SearchSeriesComparer.cs" /> <Compile Include="MetadataSource\SearchSeriesComparer.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookException.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookException.cs" />
<Compile Include="Metadata\Consumers\MediaBrowser\MediaBrowserMetadata.cs" /> <Compile Include="Extras\Metadata\Consumers\MediaBrowser\MediaBrowserMetadata.cs" />
<Compile Include="Metadata\Consumers\MediaBrowser\MediaBrowserMetadataSettings.cs" /> <Compile Include="Extras\Metadata\Consumers\MediaBrowser\MediaBrowserMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadata.cs" /> <Compile Include="Extras\Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" /> <Compile Include="Extras\Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadata.cs" /> <Compile Include="Extras\Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" /> <Compile Include="Extras\Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" /> <Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" /> <Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
<Compile Include="Metadata\ExistingMetadataService.cs" /> <Compile Include="Extras\Metadata\ExistingMetadataImporter.cs" />
<Compile Include="Metadata\Files\CleanMetadataService.cs" /> <Compile Include="Extras\Metadata\Files\CleanMetadataFileService.cs" />
<Compile Include="Metadata\Files\ImageFileResult.cs" /> <Compile Include="Extras\Metadata\Files\ImageFileResult.cs" />
<Compile Include="Metadata\Files\MetadataFile.cs" /> <Compile Include="Extras\Metadata\Files\MetadataFileResult.cs" />
<Compile Include="Metadata\Files\MetadataFileRepository.cs" /> <Compile Include="Extras\Metadata\IMetadata.cs" />
<Compile Include="Metadata\Files\MetadataFileResult.cs" /> <Compile Include="Extras\Metadata\MetadataBase.cs" />
<Compile Include="Metadata\Files\MetadataFileService.cs" /> <Compile Include="Extras\Metadata\MetadataDefinition.cs" />
<Compile Include="Metadata\Files\MetadataFilesUpdated.cs" /> <Compile Include="Extras\Metadata\MetadataFactory.cs" />
<Compile Include="Metadata\IMetadata.cs" /> <Compile Include="Extras\Metadata\MetadataRepository.cs" />
<Compile Include="Metadata\MetadataBase.cs" /> <Compile Include="Extras\Metadata\MetadataService.cs" />
<Compile Include="Metadata\MetadataDefinition.cs" /> <Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="Metadata\MetadataFactory.cs" />
<Compile Include="Metadata\MetadataRepository.cs" />
<Compile Include="Metadata\MetadataService.cs" />
<Compile Include="Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" /> <Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" /> <Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" /> <Compile Include="Notifications\Join\JoinAuthException.cs" />
@ -827,6 +847,9 @@
<Compile Include="Notifications\Twitter\Twitter.cs" /> <Compile Include="Notifications\Twitter\Twitter.cs" />
<Compile Include="Notifications\Twitter\TwitterService.cs" /> <Compile Include="Notifications\Twitter\TwitterService.cs" />
<Compile Include="Notifications\Twitter\TwitterSettings.cs" /> <Compile Include="Notifications\Twitter\TwitterSettings.cs" />
<Compile Include="Parser\IsoLanguage.cs" />
<Compile Include="Parser\IsoLanguages.cs" />
<Compile Include="Parser\LanguageParser.cs" />
<Compile Include="Profiles\Delay\DelayProfile.cs" /> <Compile Include="Profiles\Delay\DelayProfile.cs" />
<Compile Include="Profiles\Delay\DelayProfileService.cs" /> <Compile Include="Profiles\Delay\DelayProfileService.cs" />
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" /> <Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />

View File

@ -0,0 +1,16 @@
namespace NzbDrone.Core.Parser
{
public class IsoLanguage
{
public string TwoLetterCode { get; set; }
public string ThreeLetterCode { get; set; }
public Language Language { get; set; }
public IsoLanguage(string twoLetterCode, string threeLetterCode, Language language)
{
TwoLetterCode = twoLetterCode;
ThreeLetterCode = threeLetterCode;
Language = language;
}
}
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.Parser
{
public static class IsoLanguages
{
private static readonly HashSet<IsoLanguage> All = new HashSet<IsoLanguage>
{
new IsoLanguage("en", "eng", Language.English),
new IsoLanguage("fr", "fra", Language.French),
new IsoLanguage("es", "spa", Language.Spanish),
new IsoLanguage("de", "deu", Language.German),
new IsoLanguage("it", "ita", Language.Italian),
new IsoLanguage("da", "dan", Language.Danish),
new IsoLanguage("nl", "nld", Language.Dutch),
new IsoLanguage("ja", "jpn", Language.Japanese),
// new IsoLanguage("", "", Language.Cantonese),
// new IsoLanguage("", "", Language.Mandarin),
new IsoLanguage("ru", "rus", Language.Russian),
new IsoLanguage("pl", "pol", Language.Polish),
new IsoLanguage("vi", "vie", Language.Vietnamese),
new IsoLanguage("sv", "swe", Language.Swedish),
new IsoLanguage("no", "nor", Language.Norwegian),
new IsoLanguage("fi", "fin", Language.Finnish),
new IsoLanguage("tr", "tur", Language.Turkish),
new IsoLanguage("pt", "por", Language.Portuguese),
// new IsoLanguage("nl", "nld", Language.Flemish),
new IsoLanguage("el", "ell", Language.Greek),
new IsoLanguage("ko", "kor", Language.Korean),
new IsoLanguage("hu", "hun", Language.Hungarian)
};
public static IsoLanguage Find(string isoCode)
{
if (isoCode.Length == 2)
{
//Lookup ISO639-1 code
return All.SingleOrDefault(l => l.TwoLetterCode == isoCode);
}
else if (isoCode.Length == 3)
{
//Lookup ISO639-2T code
return All.SingleOrDefault(l => l.ThreeLetterCode == isoCode);
}
return null;
}
public static IsoLanguage Get(Language language)
{
return All.SingleOrDefault(l => l.Language == language);
}
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.Parser
{
public static class LanguageParser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LanguageParser));
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static Language ParseLanguage(string title)
{
var lowerTitle = title.ToLower();
if (lowerTitle.Contains("english"))
return Language.English;
if (lowerTitle.Contains("french"))
return Language.French;
if (lowerTitle.Contains("spanish"))
return Language.Spanish;
if (lowerTitle.Contains("danish"))
return Language.Danish;
if (lowerTitle.Contains("dutch"))
return Language.Dutch;
if (lowerTitle.Contains("japanese"))
return Language.Japanese;
if (lowerTitle.Contains("cantonese"))
return Language.Cantonese;
if (lowerTitle.Contains("mandarin"))
return Language.Mandarin;
if (lowerTitle.Contains("korean"))
return Language.Korean;
if (lowerTitle.Contains("russian"))
return Language.Russian;
if (lowerTitle.Contains("polish"))
return Language.Polish;
if (lowerTitle.Contains("vietnamese"))
return Language.Vietnamese;
if (lowerTitle.Contains("swedish"))
return Language.Swedish;
if (lowerTitle.Contains("norwegian"))
return Language.Norwegian;
if (lowerTitle.Contains("nordic"))
return Language.Norwegian;
if (lowerTitle.Contains("finnish"))
return Language.Finnish;
if (lowerTitle.Contains("turkish"))
return Language.Turkish;
if (lowerTitle.Contains("portuguese"))
return Language.Portuguese;
if (lowerTitle.Contains("hungarian"))
return Language.Hungarian;
var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any())
return Language.Italian;
if (match.Groups["german"].Captures.Cast<Capture>().Any())
return Language.German;
if (match.Groups["flemish"].Captures.Cast<Capture>().Any())
return Language.Flemish;
if (match.Groups["greek"].Captures.Cast<Capture>().Any())
return Language.Greek;
if (match.Groups["french"].Success)
return Language.French;
if (match.Groups["russian"].Success)
return Language.Russian;
if (match.Groups["dutch"].Success)
return Language.Dutch;
if (match.Groups["hungarian"].Success)
return Language.Hungarian;
return Language.English;
}
public static Language ParseSubtitleLanguage(string fileName)
{
try
{
Logger.Debug("Parsing language from subtitlte file: {0}", fileName);
var simpleFilename = Path.GetFileNameWithoutExtension(fileName);
var languageMatch = SubtitleLanguageRegex.Match(simpleFilename);
if (languageMatch.Success)
{
var isoCode = languageMatch.Groups["iso_code"].Value;
var isoLanguage = IsoLanguages.Find(isoCode);
return isoLanguage?.Language ?? Language.Unknown;
}
Logger.Debug("Unable to parse langauge from subtitle file: {0}", fileName);
}
catch (Exception ex)
{
Logger.Debug("Failed parsing langauge from subtitle file: {0}", fileName);
}
return Language.Unknown;
}
}
}

View File

@ -248,9 +248,6 @@ namespace NzbDrone.Core.Parser
private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?<russian>\brus\b)|(?<dutch>nl\W?subs?)|(?<hungarian>\b(?:HUNDUB|HUN)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})", private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -358,7 +355,7 @@ namespace NzbDrone.Core.Parser
result.Special = true; result.Special = true;
} }
result.Language = ParseLanguage(title); result.Language = LanguageParser.ParseLanguage(title);
Logger.Debug("Language parsed: {0}", result.Language); Logger.Debug("Language parsed: {0}", result.Language);
result.Quality = QualityParser.ParseQuality(title); result.Quality = QualityParser.ParseQuality(title);
@ -494,96 +491,6 @@ namespace NzbDrone.Core.Parser
return title; return title;
} }
public static Language ParseLanguage(string title)
{
var lowerTitle = title.ToLower();
if (lowerTitle.Contains("english"))
return Language.English;
if (lowerTitle.Contains("french"))
return Language.French;
if (lowerTitle.Contains("spanish"))
return Language.Spanish;
if (lowerTitle.Contains("danish"))
return Language.Danish;
if (lowerTitle.Contains("dutch"))
return Language.Dutch;
if (lowerTitle.Contains("japanese"))
return Language.Japanese;
if (lowerTitle.Contains("cantonese"))
return Language.Cantonese;
if (lowerTitle.Contains("mandarin"))
return Language.Mandarin;
if (lowerTitle.Contains("korean"))
return Language.Korean;
if (lowerTitle.Contains("russian"))
return Language.Russian;
if (lowerTitle.Contains("polish"))
return Language.Polish;
if (lowerTitle.Contains("vietnamese"))
return Language.Vietnamese;
if (lowerTitle.Contains("swedish"))
return Language.Swedish;
if (lowerTitle.Contains("norwegian"))
return Language.Norwegian;
if (lowerTitle.Contains("nordic"))
return Language.Norwegian;
if (lowerTitle.Contains("finnish"))
return Language.Finnish;
if (lowerTitle.Contains("turkish"))
return Language.Turkish;
if (lowerTitle.Contains("portuguese"))
return Language.Portuguese;
if (lowerTitle.Contains("hungarian"))
return Language.Hungarian;
var match = LanguageRegex.Match(title);
if (match.Groups["italian"].Captures.Cast<Capture>().Any())
return Language.Italian;
if (match.Groups["german"].Captures.Cast<Capture>().Any())
return Language.German;
if (match.Groups["flemish"].Captures.Cast<Capture>().Any())
return Language.Flemish;
if (match.Groups["greek"].Captures.Cast<Capture>().Any())
return Language.Greek;
if (match.Groups["french"].Success)
return Language.French;
if (match.Groups["russian"].Success)
return Language.Russian;
if (match.Groups["dutch"].Success)
return Language.Dutch;
if (match.Groups["hungarian"].Success)
return Language.Hungarian;
return Language.English;
}
private static SeriesTitleInfo GetSeriesTitleInfo(string title) private static SeriesTitleInfo GetSeriesTitleInfo(string title)
{ {
var seriesTitleInfo = new SeriesTitleInfo(); var seriesTitleInfo = new SeriesTitleInfo();

View File

@ -233,7 +233,7 @@ namespace NzbDrone.Core.Parser
info.FullSeason = false; info.FullSeason = false;
info.Quality = QualityParser.ParseQuality(title); info.Quality = QualityParser.ParseQuality(title);
info.ReleaseGroup = Parser.ParseReleaseGroup(title); info.ReleaseGroup = Parser.ParseReleaseGroup(title);
info.Language = Parser.ParseLanguage(title); info.Language = LanguageParser.ParseLanguage(title);
info.Special = true; info.Special = true;
_logger.Debug("Found special episode {0} for title '{1}'", info, title); _logger.Debug("Found special episode {0} for title '{1}'", info, title);