New: Write metadata to tags, with UI for previewing changes (#633)

This commit is contained in:
ta264
2019-03-15 12:10:45 +00:00
committed by GitHub
parent 6548f4b1b7
commit 072f772dc8
82 changed files with 2938 additions and 358 deletions
Binary file not shown.
@@ -0,0 +1,11 @@
nin.* in this directory are re-encodes of nin.mp3
title : 999,999
artist : Nine Inch Nails
track : 1
album : The Slip
copyright : Attribution-Noncommercial-Share Alike 3.0 United States: http://creativecommons.org/licenses/by-nc-sa/3.0/us/
comment : URL: http://freemusicarchive.org/music/Nine_Inch_Nails/The_Slip/999999
: Comments: http://freemusicarchive.org/
: Curator:
: Copyright: Attribution-Noncommercial-Share Alike 3.0 United States: http://creativecommons.org/licenses/by-nc-sa/3.0/us/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,212 @@
using System.IO;
using NUnit.Framework;
using FluentAssertions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Configuration;
using FizzWare.NBuilder;
using System;
using System.Collections;
using System.Linq;
using NzbDrone.Common.Extensions;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
{
[TestFixture]
public class AudioTagServiceFixture : CoreTest<AudioTagService>
{
public static class TestCaseFactory
{
private static readonly string[] MediaFiles = new [] { "nin.mp2", "nin.mp3", "nin.flac", "nin.m4a", "nin.wma", "nin.ape", "nin.opus" };
private static readonly string[] SkipProperties = new [] { "IsValid", "Duration", "Quality", "MediaInfo" };
private static readonly Dictionary<string, string[]> SkipPropertiesByFile = new Dictionary<string, string[]> {
{ "nin.mp2", new [] {"OriginalReleaseDate"} }
};
public static IEnumerable TestCases
{
get
{
foreach (var file in MediaFiles)
{
var toSkip = SkipProperties;
if (SkipPropertiesByFile.ContainsKey(file))
{
toSkip = toSkip.Union(SkipPropertiesByFile[file]).ToArray();
}
yield return new TestCaseData(file, toSkip).SetName($"{{m}}_{file.Replace("nin.", "")}");
}
}
}
}
private readonly string testdir = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media");
private string copiedFile;
private AudioTag testTags;
[SetUp]
public void Setup()
{
Mocker.GetMock<IConfigService>()
.Setup(x => x.WriteAudioTags)
.Returns(WriteAudioTagsType.Sync);
// have to manually set the arrays of string parameters and integers to values > 1
testTags = Builder<AudioTag>.CreateNew()
.With(x => x.Track = 2)
.With(x => x.TrackCount = 33)
.With(x => x.Disc = 44)
.With(x => x.DiscCount = 55)
.With(x => x.Date = new DateTime(2019, 3, 1))
.With(x => x.Year = 2019)
.With(x => x.OriginalReleaseDate = new DateTime(2009, 4, 1))
.With(x => x.OriginalYear = 2009)
.With(x => x.Performers = new [] { "Performer1" })
.With(x => x.AlbumArtists = new [] { "방탄소년단" })
.Build();
}
[TearDown]
public void Cleanup()
{
if (File.Exists(copiedFile))
{
File.Delete(copiedFile);
}
}
private void GivenFileCopy(string filename)
{
var original = Path.Combine(testdir, filename);
var tempname = $"temp_{Path.GetRandomFileName()}{Path.GetExtension(filename)}";
copiedFile = Path.Combine(testdir, tempname);
File.Copy(original, copiedFile);
}
private void VerifyDifferent(AudioTag a, AudioTag b, string[] skipProperties)
{
foreach (var property in typeof(AudioTag).GetProperties())
{
if (skipProperties.Contains(property.Name))
{
continue;
}
if (property.CanRead)
{
if (property.PropertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEquatable<>)) ||
Nullable.GetUnderlyingType(property.PropertyType) != null)
{
var val1 = property.GetValue(a, null);
var val2 = property.GetValue(b, null);
val1.Should().NotBe(val2, $"{property.Name} should not be equal. Found {val1.NullSafe()} for both tags");
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
var val1 = (IEnumerable) property.GetValue(a, null);
var val2 = (IEnumerable) property.GetValue(b, null);
if (val1 != null && val2 != null)
{
val1.Should().NotBeEquivalentTo(val2, $"{property.Name} should not be equal");
}
}
}
}
}
private void VerifySame(AudioTag a, AudioTag b, string[] skipProperties)
{
foreach (var property in typeof(AudioTag).GetProperties())
{
if (skipProperties.Contains(property.Name))
{
continue;
}
if (property.CanRead)
{
if (property.PropertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEquatable<>)) ||
Nullable.GetUnderlyingType(property.PropertyType) != null)
{
var val1 = property.GetValue(a, null);
var val2 = property.GetValue(b, null);
val1.Should().Be(val2, $"{property.Name} should be equal");
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
var val1 = (IEnumerable) property.GetValue(a, null);
var val2 = (IEnumerable) property.GetValue(b, null);
val1.Should().BeEquivalentTo(val2, $"{property.Name} should be equal");
}
}
}
}
[Test, TestCaseSource(typeof(TestCaseFactory), "TestCases")]
public void should_read_duration(string filename, string[] ignored)
{
var path = Path.Combine(testdir, filename);
var tags = Subject.ReadTags(path);
tags.Duration.Should().BeCloseTo(new TimeSpan(0, 0, 1, 25, 130), 100);
}
[Test, TestCaseSource(typeof(TestCaseFactory), "TestCases")]
public void should_read_write_tags(string filename, string[] skipProperties)
{
GivenFileCopy(filename);
var path = copiedFile;
var initialtags = Subject.ReadAudioTag(path);
VerifyDifferent(initialtags, testTags, skipProperties);
testTags.Write(path);
var writtentags = Subject.ReadAudioTag(path);
VerifySame(writtentags, testTags, skipProperties);
}
[Test, TestCaseSource(typeof(TestCaseFactory), "TestCases")]
public void should_remove_mb_tags(string filename, string[] skipProperties)
{
GivenFileCopy(filename);
var path = copiedFile;
var track = new TrackFile {
Artist = new Artist {
Path = Path.GetDirectoryName(path)
},
RelativePath = Path.GetFileName(path)
};
testTags.Write(path);
var withmb = Subject.ReadAudioTag(path);
VerifySame(withmb, testTags, skipProperties);
Subject.RemoveMusicBrainzTags(track);
var tag = Subject.ReadAudioTag(path);
tag.MusicBrainzReleaseCountry.Should().BeNull();
tag.MusicBrainzReleaseStatus.Should().BeNull();
tag.MusicBrainzReleaseType.Should().BeNull();
tag.MusicBrainzReleaseId.Should().BeNull();
tag.MusicBrainzArtistId.Should().BeNull();
tag.MusicBrainzReleaseArtistId.Should().BeNull();
tag.MusicBrainzReleaseGroupId.Should().BeNull();
tag.MusicBrainzTrackId.Should().BeNull();
tag.MusicBrainzAlbumComment.Should().BeNull();
tag.MusicBrainzReleaseTrackId.Should().BeNull();
}
}
}
@@ -146,8 +146,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByForeignReleaseId(new List<string>{ "xxx" }))
.Returns(new List<AlbumRelease> { release });
.Setup(x => x.GetReleaseByForeignReleaseId("xxx"))
.Returns(release);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
}
@@ -11,6 +11,8 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Test.Common;
using FluentAssertions;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Test.MusicTests
{
@@ -54,13 +56,13 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(_artist);
Mocker.GetMock<IReleaseService>()
.Setup(s => s.GetReleasesByAlbum(album1.Id))
.Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny<IEnumerable<string>>()))
.Returns(new List<AlbumRelease> { release });
Mocker.GetMock<IReleaseService>()
.Setup(s => s.GetReleasesByForeignReleaseId(It.IsAny<List<string>>()))
.Returns(new List<AlbumRelease> { release });
Mocker.GetMock<IArtistMetadataRepository>()
.Setup(s => s.FindById(It.IsAny<List<string>>()))
.Returns(new List<ArtistMetadata>());
Mocker.GetMock<IProvideAlbumInfo>()
.Setup(s => s.GetAlbumInfo(It.IsAny<string>()))
.Callback(() => { throw new AlbumNotFoundException(album1.ForeignAlbumId); });
@@ -80,7 +82,7 @@ namespace NzbDrone.Core.Test.MusicTests
[Test]
public void should_log_error_if_musicbrainz_id_not_found()
{
Subject.RefreshAlbumInfo(_albums, false);
Subject.RefreshAlbumInfo(_albums, false, false);
Mocker.GetMock<IAlbumService>()
.Verify(v => v.UpdateMany(It.IsAny<List<Album>>()), Times.Never());
@@ -97,12 +99,56 @@ namespace NzbDrone.Core.Test.MusicTests
GivenNewAlbumInfo(newAlbumInfo);
Subject.RefreshAlbumInfo(_albums, false);
Subject.RefreshAlbumInfo(_albums, false, false);
Mocker.GetMock<IAlbumService>()
.Verify(v => v.UpdateMany(It.Is<List<Album>>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId)));
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void two_equivalent_releases_should_be_equal()
{
var release = Builder<AlbumRelease>.CreateNew().Build();
var release2 = Builder<AlbumRelease>.CreateNew().Build();
ReferenceEquals(release, release2).Should().BeFalse();
release.Equals(release2).Should().BeTrue();
release.Label?.ToJson().Should().Be(release2.Label?.ToJson());
release.Country?.ToJson().Should().Be(release2.Country?.ToJson());
release.Media?.ToJson().Should().Be(release2.Media?.ToJson());
}
[Test]
public void two_equivalent_tracks_should_be_equal()
{
var track = Builder<Track>.CreateNew().Build();
var track2 = Builder<Track>.CreateNew().Build();
ReferenceEquals(track, track2).Should().BeFalse();
track.Equals(track2).Should().BeTrue();
}
[Test]
public void two_equivalent_metadata_should_be_equal()
{
var meta = Builder<ArtistMetadata>.CreateNew().Build();
var meta2 = Builder<ArtistMetadata>.CreateNew().Build();
ReferenceEquals(meta, meta2).Should().BeFalse();
meta.Equals(meta2).Should().BeTrue();
}
[Test]
public void should_remove_items_from_list()
{
var releases = Builder<AlbumRelease>.CreateListOfSize(2).Build();
var release = releases[0];
releases.Remove(release);
releases.Should().HaveCount(1);
}
}
}
@@ -46,13 +46,9 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(_artist);
Mocker.GetMock<IAlbumService>()
.Setup(s => s.GetAlbumsByArtist(It.IsAny<int>()))
.Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(new List<Album>());
Mocker.GetMock<IAlbumService>()
.Setup(s => s.FindById(It.IsAny<List<string>>()))
.Returns(new List<Album>());
Mocker.GetMock<IProvideArtistInfo>()
.Setup(s => s.GetArtistInfo(It.IsAny<string>(), It.IsAny<int>()))
.Callback(() => { throw new ArtistNotFoundException(_artist.ForeignArtistId); });
@@ -87,6 +87,10 @@
<Reference Include="Prowlin, Version=0.9.4456.26422, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll</HintPath>
</Reference>
<Reference Include="taglib-sharp, Version=2.2.0.0, Culture=neutral, PublicKeyToken=db62eba44689b5b0, processorArchitecture=MSIL">
<HintPath>..\packages\TagLibSharp.2.2.0-beta\lib\netstandard2.0\taglib-sharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
@@ -281,6 +285,7 @@
<Compile Include="MediaCoverTests\CoverExistsSpecificationFixture.cs" />
<Compile Include="MediaCoverTests\ImageResizerFixture.cs" />
<Compile Include="MediaCoverTests\MediaCoverServiceFixture.cs" />
<Compile Include="MediaFiles\AudioTagServiceFixture.cs" />
<Compile Include="MediaFiles\DiskScanServiceTests\ScanFixture.cs" />
<Compile Include="MediaFiles\DownloadedAlbumsCommandServiceFixture.cs" />
<Compile Include="MediaFiles\DownloadedTracksImportServiceFixture.cs" />
@@ -502,7 +507,7 @@
<Content Include="Files\LongOverview.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\H264_sample.mp4">
<Content Include="Files\Media\nin.mp2">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.mp3">
@@ -511,6 +516,18 @@
<Content Include="Files\Media\nin.flac">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.m4a">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.wma">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.ape">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Media\nin.opus">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Files\Nzbget\JsonError.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -183,12 +183,14 @@ namespace NzbDrone.Core.Test.ParserTests
}
[TestCase("", "MPEG-4 Audio (mp4a)", 320)]
[TestCase("", "MPEG-4 Audio (drms)", 320)]
public void should_parse_aac_320_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.AAC_320);
}
[TestCase("", "MPEG-4 Audio (mp4a)", 321)]
[TestCase("", "MPEG-4 Audio (drms)", 321)]
public void should_parse_aac_vbr_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.AAC_VBR);
@@ -196,12 +198,14 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Kirlian Camera - The Ice Curtain - Album 1998 - Ogg-Vorbis Q10", null, 0)]
[TestCase("", "Vorbis Version 0 Audio", 500)]
[TestCase("", "Opus Version 1 Audio", 501)]
public void should_parse_vorbis_q10_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q10);
}
[TestCase("", "Vorbis Version 0 Audio", 320)]
[TestCase("", "Opus Version 1 Audio", 321)]
public void should_parse_vorbis_q9_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q9);
@@ -209,6 +213,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Various Artists - No New York [1978/Ogg/q8]", null, 0)]
[TestCase("", "Vorbis Version 0 Audio", 256)]
[TestCase("", "Opus Version 1 Audio", 257)]
public void should_parse_vorbis_q8_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q8);
@@ -216,18 +221,21 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Masters_At_Work-Nuyorican_Soul-.Talkin_Loud.-1997-OGG.Q7", null, 0)]
[TestCase("", "Vorbis Version 0 Audio", 224)]
[TestCase("", "Opus Version 1 Audio", 225)]
public void should_parse_vorbis_q7_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q7);
}
[TestCase("", "Vorbis Version 0 Audio", 192)]
[TestCase("", "Opus Version 1 Audio", 193)]
public void should_parse_vorbis_q6_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q6);
}
[TestCase("", "Vorbis Version 0 Audio", 160)]
[TestCase("", "Opus Version 1 Audio", 161)]
public void should_parse_vorbis_q5_quality(string title, string desc, int bitrate)
{
ParseAndVerifyQuality(title, desc, bitrate, Quality.VORBIS_Q5);
+3 -2
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AutoMoq" version="1.8.1.0" targetFramework="net461" />
<package id="CommonServiceLocator" version="1.3" targetFramework="net461" />
@@ -14,4 +14,5 @@
<package id="NUnit" version="3.11.0" targetFramework="net461" />
<package id="Prowlin" version="0.9.4456.26422" targetFramework="net461" />
<package id="Unity" version="2.1.505.2" targetFramework="net461" />
</packages>
<package id="TagLibSharp" version="2.2.0-beta" targetFramework="net461" />
</packages>