New: Parse subtitle titles

Closes #5955
pull/6391/head v4.0.1.947
Jendrik Weise 10 months ago committed by GitHub
parent 7be5732a3a
commit 69f99373e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dapper;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class parse_title_from_existing_subtitle_filesFixture : MigrationTest<parse_title_from_existing_subtitle_files>
{
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", "Name (2020)/Season 1/Name (2020).mkv", "testtitle", 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 0)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle - 3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle - 3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle - 3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "testtitle", 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.default.eng.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].3.forced.eng.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.3.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.3.forced.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", null, 3)]
public void should_process_file_with_missing_title(string subtitlePath, string episodePath, string title, int copy)
{
var now = DateTime.UtcNow;
var db = WithDapperMigrationTestDb(c =>
{
c.Insert.IntoTable("SubtitleFiles").Row(new
{
SeriesId = 1,
SeasonNumber = 1,
EpisodeFileId = 1,
RelativePath = subtitlePath,
Added = now,
LastUpdated = now,
Extension = Path.GetExtension(subtitlePath),
Language = 10,
LanguageTags = new List<string> { "sdh" }.ToJson()
});
c.Insert.IntoTable("EpisodeFiles").Row(new
{
Id = 1,
SeriesId = 1,
RelativePath = episodePath,
OriginalFilePath = string.Empty,
Quality = new { }.ToJson(),
Size = 0,
DateAdded = now,
SeasonNumber = 1,
Languages = new List<int> { 1 }.ToJson()
});
});
var files = db.Query<SubtitleFile198>("SELECT * FROM \"SubtitleFiles\"").ToList();
files.Should().HaveCount(1);
files.First().Title.Should().Be(title);
files.First().Copy.Should().Be(copy);
files.First().LanguageTags.Should().NotContain("sdh");
files.First().Language.Should().NotBe(10);
}
}
public class SubtitleFile198
{
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 int Language { get; set; }
public int Copy { get; set; }
public string Title { get; set; }
public List<string> LanguageTags { get; set; }
}
}

@ -0,0 +1,48 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
[TestFixture]
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
{
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
{
var episodeFile = new EpisodeFile
{
RelativePath = relativePath,
OriginalFilePath = originalFilePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
subtitleTitleInfo.Title.Should().BeNull();
subtitleTitleInfo.Copy.Should().Be(0);
}
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass")]
[TestCase("Default (2020)/Season 1/Default (2020) - S01E20 - [AAC 2.0].mkv", "Default (2020) - S01E20 - [AAC 2.0].testtitle.eng.default.ass")]
public void should_not_parse_default(string relativePath, string path)
{
var episodeFile = new EpisodeFile
{
RelativePath = relativePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
}
}
}

@ -428,5 +428,38 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().Contain(Language.Original); result.Languages.Should().Contain(Language.Original);
result.Languages.Should().Contain(Language.English); result.Languages.Should().Contain(Language.English);
} }
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.eng.ass", new[] { "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.forced.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.eng.testtitle.ass", new[] { "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.fra.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.fra.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.forced.fra.ass", new[] { "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].fra.forced.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].forced.fra.testtitle.ass", new[] { "forced" }, "testtitle", "French")]
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
subtitleTitleInfo.LanguageTags.Should().BeEquivalentTo(expectedTags);
subtitleTitleInfo.Title.Should().BeEquivalentTo(expectedTitle);
subtitleTitleInfo.Language.Should().BeEquivalentTo((Language)expectedLanguage);
}
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.forced.ass")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.ass")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ass")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.ass")]
public void should_not_parse_false_title(string postTitle)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(postTitle);
subtitleTitleInfo.Language.Should().Be(Language.Unknown);
subtitleTitleInfo.LanguageTags.Should().BeEmpty();
subtitleTitleInfo.RawTitle.Should().BeNull();
}
} }
} }

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using Dapper;
using FluentMigrator;
using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(198)]
public class parse_title_from_existing_subtitle_files : NzbDroneMigrationBase
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(AggregateSubtitleInfo));
protected override void MainDbUpgrade()
{
Alter.Table("SubtitleFiles").AddColumn("Title").AsString().Nullable();
Alter.Table("SubtitleFiles").AddColumn("Copy").AsInt32().WithDefaultValue(0);
Execute.WithConnection(UpdateTitles);
}
private void UpdateTitles(IDbConnection conn, IDbTransaction tran)
{
var updates = new List<object>();
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = "SELECT \"SubtitleFiles\".\"Id\", \"SubtitleFiles\".\"RelativePath\", \"EpisodeFiles\".\"RelativePath\", \"EpisodeFiles\".\"OriginalFilePath\" FROM \"SubtitleFiles\" JOIN \"EpisodeFiles\" ON \"SubtitleFiles\".\"EpisodeFileId\" = \"EpisodeFiles\".\"Id\"";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(0);
var relativePath = reader.GetString(1);
var episodeFileRelativePath = reader.GetString(2);
var episodeFileOriginalFilePath = reader.GetString(3);
var subtitleTitleInfo = CleanSubtitleTitleInfo(episodeFileRelativePath, episodeFileOriginalFilePath, relativePath);
updates.Add(new
{
Id = id,
Title = subtitleTitleInfo.Title,
Language = subtitleTitleInfo.Language,
LanguageTags = subtitleTitleInfo.LanguageTags,
Copy = subtitleTitleInfo.Copy
});
}
}
var updateSubtitleFilesSql = "UPDATE \"SubtitleFiles\" SET \"Title\" = @Title, \"Copy\" = @Copy, \"Language\" = @Language, \"LanguageTags\" = @LanguageTags, \"LastUpdated\" = CURRENT_TIMESTAMP WHERE \"Id\" = @Id";
conn.Execute(updateSubtitleFilesSql, updates, transaction: tran);
}
private static SubtitleTitleInfo CleanSubtitleTitleInfo(string relativePath, string originalFilePath, string path)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path);
var episodeFileTitle = Path.GetFileNameWithoutExtension(relativePath);
var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(originalFilePath) ?? string.Empty;
if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase)))
{
Logger.Debug("Subtitle title '{0}' is in episode file title '{1}'. Removing from subtitle title.", subtitleTitleInfo.RawTitle, episodeFileTitle);
subtitleTitleInfo = LanguageParser.ParseBasicSubtitle(path);
}
var cleanedTags = subtitleTitleInfo.LanguageTags.Where(t => !episodeFileTitle.Contains(t, StringComparison.OrdinalIgnoreCase)).ToList();
if (cleanedTags.Count != subtitleTitleInfo.LanguageTags.Count)
{
Logger.Debug("Removed language tags '{0}' from subtitle title '{1}'.", string.Join(", ", subtitleTitleInfo.LanguageTags.Except(cleanedTags)), subtitleTitleInfo.RawTitle);
subtitleTitleInfo.LanguageTags = cleanedTags;
}
return subtitleTitleInfo;
}
}
}

@ -5,7 +5,6 @@ using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -71,15 +70,19 @@ namespace NzbDrone.Core.Extras.Subtitles
continue; continue;
} }
var firstEpisode = localEpisode.Episodes.First();
var subtitleFile = new SubtitleFile var subtitleFile = new SubtitleFile
{ {
SeriesId = series.Id, SeriesId = series.Id,
SeasonNumber = localEpisode.SeasonNumber, SeasonNumber = localEpisode.SeasonNumber,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, EpisodeFileId = firstEpisode.EpisodeFileId,
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), RelativePath = series.Path.GetRelativePath(possibleSubtitleFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), Language = localEpisode.SubtitleInfo.Language,
LanguageTags = LanguageParser.ParseLanguageTags(possibleSubtitleFile), LanguageTags = localEpisode.SubtitleInfo.LanguageTags,
Extension = extension Title = localEpisode.SubtitleInfo.Title,
Extension = extension,
Copy = localEpisode.SubtitleInfo.Copy
}; };
subtitleFiles.Add(subtitleFile); subtitleFiles.Add(subtitleFile);

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
@ -13,15 +14,40 @@ namespace NzbDrone.Core.Extras.Subtitles
public Language Language { get; set; } public Language Language { get; set; }
public string AggregateString => Language + LanguageTagsAsString + Extension; public string AggregateString => Language + Title + LanguageTagsAsString + Extension;
public int Copy { get; set; }
public List<string> LanguageTags { get; set; } public List<string> LanguageTags { get; set; }
public string Title { get; set; }
private string LanguageTagsAsString => string.Join(".", LanguageTags); private string LanguageTagsAsString => string.Join(".", LanguageTags);
public override string ToString() public override string ToString()
{ {
return $"[{Id}] {RelativePath} ({Language}{(LanguageTags.Count > 0 ? "." : "")}{LanguageTagsAsString}{Extension})"; var stringBuilder = new StringBuilder();
stringBuilder.AppendFormat("[{0}] ", Id);
stringBuilder.Append(RelativePath);
stringBuilder.Append(" (");
stringBuilder.Append(Language);
if (Title is not null)
{
stringBuilder.Append('.');
stringBuilder.Append(Title);
}
if (LanguageTags.Count > 0)
{
stringBuilder.Append('.');
stringBuilder.Append(LanguageTagsAsString);
}
stringBuilder.Append(Extension);
stringBuilder.Append(')');
return stringBuilder.ToString();
} }
} }
} }

@ -76,16 +76,20 @@ namespace NzbDrone.Core.Extras.Subtitles
foreach (var group in groupedExtraFilesForEpisodeFile) foreach (var group in groupedExtraFilesForEpisodeFile)
{ {
var groupCount = group.Count(); var multipleCopies = group.Count() > 1;
var copy = 1; var orderedGroup = group.OrderBy(s => -s.Copy).ToList();
var copy = group.First().Copy;
foreach (var subtitleFile in group) foreach (var subtitleFile in orderedGroup)
{ {
var suffix = GetSuffix(subtitleFile.Language, copy, subtitleFile.LanguageTags, groupCount > 1); if (multipleCopies && subtitleFile.Copy == 0)
{
subtitleFile.Copy = ++copy;
}
movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix)); var suffix = GetSuffix(subtitleFile.Language, subtitleFile.Copy, subtitleFile.LanguageTags, multipleCopies, subtitleFile.Title);
copy++; movedFiles.AddIfNotNull(MoveFile(series, episodeFile, subtitleFile, suffix));
} }
} }
} }
@ -229,11 +233,22 @@ namespace NzbDrone.Core.Extras.Subtitles
return importedFiles; return importedFiles;
} }
private string GetSuffix(Language language, int copy, List<string> languageTags, bool multipleCopies = false) private string GetSuffix(Language language, int copy, List<string> languageTags, bool multipleCopies = false, string title = null)
{ {
var suffixBuilder = new StringBuilder(); var suffixBuilder = new StringBuilder();
if (multipleCopies) if (title is not null)
{
suffixBuilder.Append('.');
suffixBuilder.Append(title);
if (multipleCopies)
{
suffixBuilder.Append(" - ");
suffixBuilder.Append(copy);
}
}
else if (multipleCopies)
{ {
suffixBuilder.Append('.'); suffixBuilder.Append('.');
suffixBuilder.Append(copy); suffixBuilder.Append(copy);

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -30,7 +31,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation
IConfigService configService, IConfigService configService,
Logger logger) Logger logger)
{ {
_augmenters = augmenters; _augmenters = augmenters.OrderBy(a => a.Order).ToList();
_diskProvider = diskProvider; _diskProvider = diskProvider;
_videoFileInfoReader = videoFileInfoReader; _videoFileInfoReader = videoFileInfoReader;
_configService = configService; _configService = configService;

@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public class AggregateEpisodes : IAggregateLocalEpisode public class AggregateEpisodes : IAggregateLocalEpisode
{ {
public int Order => 1;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
public AggregateEpisodes(IParsingService parsingService) public AggregateEpisodes(IParsingService parsingService)

@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public class AggregateLanguage : IAggregateLocalEpisode public class AggregateLanguage : IAggregateLocalEpisode
{ {
public int Order => 1;
private readonly List<IAugmentLanguage> _augmentLanguages; private readonly List<IAugmentLanguage> _augmentLanguages;
private readonly Logger _logger; private readonly Logger _logger;

@ -10,6 +10,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public class AggregateQuality : IAggregateLocalEpisode public class AggregateQuality : IAggregateLocalEpisode
{ {
public int Order => 1;
private readonly List<IAugmentQuality> _augmentQualities; private readonly List<IAugmentQuality> _augmentQualities;
private readonly Logger _logger; private readonly Logger _logger;

@ -6,6 +6,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public class AggregateReleaseGroup : IAggregateLocalEpisode public class AggregateReleaseGroup : IAggregateLocalEpisode
{ {
public int Order => 1;
public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{ {
// Prefer ReleaseGroup from DownloadClient/Folder if they're not a season pack // Prefer ReleaseGroup from DownloadClient/Folder if they're not a season pack

@ -8,6 +8,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public class AggregateReleaseInfo : IAggregateLocalEpisode public class AggregateReleaseInfo : IAggregateLocalEpisode
{ {
public int Order => 1;
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
public AggregateReleaseInfo(IHistoryService historyService) public AggregateReleaseInfo(IHistoryService historyService)

@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Core.Download;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
public class AggregateSubtitleInfo : IAggregateLocalEpisode
{
public int Order => 2;
private readonly Logger _logger;
public AggregateSubtitleInfo(Logger logger)
{
_logger = logger;
}
public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
var path = localEpisode.Path;
var isSubtitleFile = SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path));
if (!isSubtitleFile)
{
return localEpisode;
}
var firstEpisode = localEpisode.Episodes.First();
var episodeFile = firstEpisode.EpisodeFile.Value;
localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path);
return localEpisode;
}
public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path);
var episodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.OriginalFilePath) ?? string.Empty;
if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase)))
{
_logger.Debug("Subtitle title '{0}' is in episode file title '{1}'. Removing from subtitle title.", subtitleTitleInfo.RawTitle, episodeFileTitle);
subtitleTitleInfo = LanguageParser.ParseBasicSubtitle(path);
}
var cleanedTags = subtitleTitleInfo.LanguageTags.Where(t => !episodeFileTitle.Contains(t, StringComparison.OrdinalIgnoreCase)).ToList();
if (cleanedTags.Count != subtitleTitleInfo.LanguageTags.Count)
{
_logger.Debug("Removed language tags '{0}' from subtitle title '{1}'.", string.Join(", ", subtitleTitleInfo.LanguageTags.Except(cleanedTags)), subtitleTitleInfo.RawTitle);
subtitleTitleInfo.LanguageTags = cleanedTags;
}
return subtitleTitleInfo;
}
}
}

@ -5,6 +5,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
{ {
public interface IAggregateLocalEpisode public interface IAggregateLocalEpisode
{ {
int Order { get; }
LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem);
} }
} }

@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Parser namespace NzbDrone.Core.Parser
{ {
@ -28,7 +29,11 @@ namespace NzbDrone.Core.Parser
private static readonly Regex GermanDualLanguageRegex = new (@"(?<!WEB[-_. ]?)\bDL\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex GermanDualLanguageRegex = new (@"(?<!WEB[-_. ]?)\bDL\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex GermanMultiLanguageRegex = new (@"\bML\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex GermanMultiLanguageRegex = new (@"\bML\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleLanguageRegex = new Regex(".+?[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex SubtitleLanguageRegex = new Regex(".+?([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*[-_. ](?<iso_code>[a-z]{2,3})([-_. ](?<tags>full|forced|foreign|default|cc|psdh|sdh))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleLanguageTitleRegex = new Regex(".+?(\\.((?<tags1>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*\\.(?<title>[^.]*)(\\.((?<tags2>full|forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleTitleRegex = new Regex("((?<title>.+) - )?(?<copy>\\d+)$", RegexOptions.Compiled);
public static List<Language> ParseLanguages(string title) public static List<Language> ParseLanguages(string title)
{ {
@ -249,6 +254,72 @@ namespace NzbDrone.Core.Parser
return Language.Unknown; return Language.Unknown;
} }
public static SubtitleTitleInfo ParseBasicSubtitle(string fileName)
{
return new SubtitleTitleInfo
{
TitleFirst = false,
LanguageTags = ParseLanguageTags(fileName),
Language = ParseSubtitleLanguage(fileName)
};
}
public static SubtitleTitleInfo ParseSubtitleLanguageInformation(string fileName)
{
var simpleFilename = Path.GetFileNameWithoutExtension(fileName);
var matchTitle = SubtitleLanguageTitleRegex.Match(simpleFilename);
if (!matchTitle.Groups["title"].Success || (matchTitle.Groups["iso_code"].Captures.Count is var languageCodeNumber && languageCodeNumber != 1))
{
Logger.Debug("Could not parse a title from subtitle file: {0}. Falling back to parsing without title.", fileName);
return ParseBasicSubtitle(fileName);
}
var isoCode = matchTitle.Groups["iso_code"].Value;
var isoLanguage = IsoLanguages.Find(isoCode.ToLower());
var language = isoLanguage?.Language ?? Language.Unknown;
var languageTags = matchTitle.Groups["tags1"].Captures
.Union(matchTitle.Groups["tags2"].Captures)
.Cast<Capture>()
.Where(tag => !tag.Value.Empty())
.Select(tag => tag.Value.ToLower());
var rawTitle = matchTitle.Groups["title"].Value;
var subtitleTitleInfo = new SubtitleTitleInfo
{
TitleFirst = matchTitle.Groups["tags1"].Captures.Empty(),
LanguageTags = languageTags.ToList(),
RawTitle = rawTitle,
Language = language
};
UpdateTitleAndCopyFromTitle(subtitleTitleInfo);
return subtitleTitleInfo;
}
public static void UpdateTitleAndCopyFromTitle(SubtitleTitleInfo subtitleTitleInfo)
{
if (subtitleTitleInfo.RawTitle is null)
{
subtitleTitleInfo.Title = null;
subtitleTitleInfo.Copy = 0;
}
else if (SubtitleTitleRegex.Match(subtitleTitleInfo.RawTitle) is var match && match.Success)
{
subtitleTitleInfo.Title = match.Groups["title"].Success ? match.Groups["title"].ToString() : null;
subtitleTitleInfo.Copy = int.Parse(match.Groups["copy"].ToString());
}
else
{
subtitleTitleInfo.Title = subtitleTitleInfo.RawTitle;
subtitleTitleInfo.Copy = 0;
}
}
public static List<string> ParseLanguageTags(string fileName) public static List<string> ParseLanguageTags(string fileName)
{ {
try try

@ -44,6 +44,7 @@ namespace NzbDrone.Core.Parser.Model
public bool FileRenamedAfterScriptImport { get; set; } public bool FileRenamedAfterScriptImport { get; set; }
public bool ShouldImportExtras { get; set; } public bool ShouldImportExtras { get; set; }
public List<string> PossibleExtraFiles { get; set; } public List<string> PossibleExtraFiles { get; set; }
public SubtitleTitleInfo SubtitleInfo { get; set; }
public int SeasonNumber public int SeasonNumber
{ {

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Core.Languages;
namespace NzbDrone.Core.Parser.Model
{
public class SubtitleTitleInfo
{
public List<string> LanguageTags { get; set; }
public Language Language { get; set; }
public string RawTitle { get; set; }
public string Title { get; set; }
public int Copy { get; set; }
public bool TitleFirst { get; set; }
}
}
Loading…
Cancel
Save