New: Parse existing subtitles and extra files

Towards #459
pull/4/head
Mark McDowall 8 years ago
parent 816cf608fc
commit 2e96c4e798

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -275,6 +275,7 @@
<Compile Include="Datastore\Migration\093_naming_config_replace_characters.cs" />
<Compile Include="Datastore\Migration\092_add_unverifiedscenenumbering.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\098_remove_titans_of_tv.cs">
<SubType>Code</SubType>
@ -487,6 +488,29 @@
<Compile Include="Exceptions\SeriesNotFoundException.cs" />
<Compile Include="Exceptions\ReleaseDownloadException.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="HealthCheck\CheckHealthCommand.cs" />
<Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" />
@ -757,29 +781,25 @@
<Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" />
<Compile Include="MetadataSource\SearchSeriesComparer.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookException.cs" />
<Compile Include="Metadata\Consumers\MediaBrowser\MediaBrowserMetadata.cs" />
<Compile Include="Metadata\Consumers\MediaBrowser\MediaBrowserMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
<Compile Include="Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
<Compile Include="Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
<Compile Include="Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
<Compile Include="Metadata\ExistingMetadataService.cs" />
<Compile Include="Metadata\Files\CleanMetadataService.cs" />
<Compile Include="Metadata\Files\ImageFileResult.cs" />
<Compile Include="Metadata\Files\MetadataFile.cs" />
<Compile Include="Metadata\Files\MetadataFileRepository.cs" />
<Compile Include="Metadata\Files\MetadataFileResult.cs" />
<Compile Include="Metadata\Files\MetadataFileService.cs" />
<Compile Include="Metadata\Files\MetadataFilesUpdated.cs" />
<Compile Include="Metadata\IMetadata.cs" />
<Compile Include="Metadata\MetadataBase.cs" />
<Compile Include="Metadata\MetadataDefinition.cs" />
<Compile Include="Metadata\MetadataFactory.cs" />
<Compile Include="Metadata\MetadataRepository.cs" />
<Compile Include="Metadata\MetadataService.cs" />
<Compile Include="Metadata\MetadataType.cs" />
<Compile Include="Extras\Metadata\Consumers\MediaBrowser\MediaBrowserMetadata.cs" />
<Compile Include="Extras\Metadata\Consumers\MediaBrowser\MediaBrowserMetadataSettings.cs" />
<Compile Include="Extras\Metadata\Consumers\Roksbox\RoksboxMetadata.cs" />
<Compile Include="Extras\Metadata\Consumers\Roksbox\RoksboxMetadataSettings.cs" />
<Compile Include="Extras\Metadata\Consumers\Wdtv\WdtvMetadata.cs" />
<Compile Include="Extras\Metadata\Consumers\Wdtv\WdtvMetadataSettings.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcMetadata.cs" />
<Compile Include="Extras\Metadata\Consumers\Xbmc\XbmcMetadataSettings.cs" />
<Compile Include="Extras\Metadata\ExistingMetadataImporter.cs" />
<Compile Include="Extras\Metadata\Files\CleanMetadataFileService.cs" />
<Compile Include="Extras\Metadata\Files\ImageFileResult.cs" />
<Compile Include="Extras\Metadata\Files\MetadataFileResult.cs" />
<Compile Include="Extras\Metadata\IMetadata.cs" />
<Compile Include="Extras\Metadata\MetadataBase.cs" />
<Compile Include="Extras\Metadata\MetadataDefinition.cs" />
<Compile Include="Extras\Metadata\MetadataFactory.cs" />
<Compile Include="Extras\Metadata\MetadataRepository.cs" />
<Compile Include="Extras\Metadata\MetadataService.cs" />
<Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="Notifications\Join\JoinAuthException.cs" />
@ -827,6 +847,9 @@
<Compile Include="Notifications\Twitter\Twitter.cs" />
<Compile Include="Notifications\Twitter\TwitterService.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\DelayProfileService.cs" />
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />

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

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

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

@ -248,9 +248,6 @@ namespace NzbDrone.Core.Parser
private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
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})",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -358,7 +355,7 @@ namespace NzbDrone.Core.Parser
result.Special = true;
}
result.Language = ParseLanguage(title);
result.Language = LanguageParser.ParseLanguage(title);
Logger.Debug("Language parsed: {0}", result.Language);
result.Quality = QualityParser.ParseQuality(title);
@ -493,97 +490,7 @@ namespace NzbDrone.Core.Parser
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)
{
var seriesTitleInfo = new SeriesTitleInfo();

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

Loading…
Cancel
Save