mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-27 22:56:45 -04:00
New: Write metadata to tags, with UI for previewing changes (#633)
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user