diff --git a/frontend/src/Retag/RetagPreviewModalContentConnector.js b/frontend/src/Retag/RetagPreviewModalContentConnector.js index 0e7255eb5..ce3a64776 100644 --- a/frontend/src/Retag/RetagPreviewModalContentConnector.js +++ b/frontend/src/Retag/RetagPreviewModalContentConnector.js @@ -75,7 +75,6 @@ class RetagPreviewModalContentConnector extends Component { RetagPreviewModalContentConnector.propTypes = { artistId: PropTypes.number.isRequired, albumId: PropTypes.number, - retagTracks: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired, fetchRetagPreview: PropTypes.func.isRequired, diff --git a/frontend/src/Retag/RetagPreviewRow.js b/frontend/src/Retag/RetagPreviewRow.js index e23fe914c..e02246253 100644 --- a/frontend/src/Retag/RetagPreviewRow.js +++ b/frontend/src/Retag/RetagPreviewRow.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; import { icons } from 'Helpers/Props'; import Icon from 'Components/Icon'; import CheckInput from 'Components/Form/CheckInput'; @@ -7,16 +8,19 @@ import styles from './RetagPreviewRow.css'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; -function formatMissing(value) { - if (value === undefined || value === 0 || value === '0') { +function formatValue(field, value) { + if (value === undefined || value === 0 || value === '0' || value === '') { return (); } + if (field === 'Image Size') { + return formatBytes(value); + } return value; } -function formatChange(oldValue, newValue) { +function formatChange(field, oldValue, newValue) { return ( -
{formatMissing(oldValue)} {formatMissing(newValue)}
+
{formatValue(field, oldValue)} {formatValue(field, newValue)}
); } @@ -78,7 +82,7 @@ class RetagPreviewRow extends Component { ); }) diff --git a/src/NzbDrone.Core.Test/Files/Media/nin.mp3 b/src/NzbDrone.Core.Test/Files/Media/nin.mp3 index 081b897f3..20c5ec4ba 100644 Binary files a/src/NzbDrone.Core.Test/Files/Media/nin.mp3 and b/src/NzbDrone.Core.Test/Files/Media/nin.mp3 differ diff --git a/src/NzbDrone.Core.Test/Files/Media/nin.png b/src/NzbDrone.Core.Test/Files/Media/nin.png new file mode 100644 index 000000000..9b0164982 Binary files /dev/null and b/src/NzbDrone.Core.Test/Files/Media/nin.png differ diff --git a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs index 8a4b43a46..a10476cdb 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/AudioTagServiceFixture.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture { 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 string[] SkipProperties = new [] { "IsValid", "Duration", "Quality", "MediaInfo", "ImageFile" }; private static readonly Dictionary SkipPropertiesByFile = new Dictionary { { "nin.mp2", new [] {"OriginalReleaseDate"} } }; @@ -61,6 +61,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture .Setup(x => x.WriteAudioTags) .Returns(WriteAudioTagsType.Sync); + var imageFile = Path.Combine(testdir, "nin.png"); + var imageSize = _diskProvider.GetFileSize(imageFile); + // have to manually set the arrays of string parameters and integers to values > 1 testTags = Builder.CreateNew() .With(x => x.Track = 2) @@ -73,6 +76,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture .With(x => x.OriginalYear = 2009) .With(x => x.Performers = new [] { "Performer1" }) .With(x => x.AlbumArtists = new [] { "방탄소년단" }) + .With(x => x.Genres = new [] { "Genre1", "Genre2" }) + .With(x => x.ImageFile = imageFile) + .With(x => x.ImageSize = imageSize) .Build(); } @@ -228,7 +234,8 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture var tag = Subject.ReadAudioTag(path); var expected = new AudioTag() { Performers = new string[0], - AlbumArtists = new string[0] + AlbumArtists = new string[0], + Genres = new string[0] }; VerifySame(tag, expected, skipProperties); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index a373e07d7..8b88ba4d4 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -471,6 +471,9 @@ Always + + Always + Always @@ -639,4 +642,4 @@ - \ No newline at end of file + diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index ec90e4cfb..108714142 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaCover { void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnumerable covers); string GetCoverPath(int entityId, MediaCoverEntity coverEntity, MediaCoverTypes mediaCoverTypes, string extension, int? height = null); + void EnsureAlbumCovers(Album album); } public class MediaCoverService : @@ -137,7 +138,7 @@ namespace NzbDrone.Core.MediaCover } } - private void EnsureAlbumCovers(Album album) + public void EnsureAlbumCovers(Album album) { foreach (var cover in album.Images.Where(e => e.CoverType == MediaCoverTypes.Cover)) { diff --git a/src/NzbDrone.Core/MediaFiles/AudioTag.cs b/src/NzbDrone.Core/MediaFiles/AudioTag.cs index c0475a66f..40d7eb996 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTag.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTag.cs @@ -34,6 +34,9 @@ namespace NzbDrone.Core.MediaFiles public uint OriginalYear { get; set; } public string Publisher { get; set; } public TimeSpan Duration { get; set; } + public string[] Genres { get; set; } + public string ImageFile { get; set; } + public long ImageSize { get; set; } public string MusicBrainzReleaseCountry { get; set; } public string MusicBrainzReleaseStatus { get; set; } public string MusicBrainzReleaseType { get; set; } @@ -81,6 +84,8 @@ namespace NzbDrone.Core.MediaFiles Year = tag.Year; Publisher = tag.Publisher; Duration = file.Properties.Duration; + Genres = tag.Genres; + ImageSize = tag.Pictures.FirstOrDefault()?.Data.Count ?? 0; MusicBrainzReleaseCountry = tag.MusicBrainzReleaseCountry; MusicBrainzReleaseStatus = tag.MusicBrainzReleaseStatus; MusicBrainzReleaseType = tag.MusicBrainzReleaseType; @@ -315,6 +320,7 @@ namespace NzbDrone.Core.MediaFiles // WMA with null performers/albumartists Performers = Performers ?? new string[0]; AlbumArtists = AlbumArtists ?? new string[0]; + Genres = Genres ?? new string[0]; TagLib.File file = null; try @@ -332,6 +338,7 @@ namespace NzbDrone.Core.MediaFiles tag.Disc = Disc; tag.DiscCount = DiscCount; tag.Publisher = Publisher; + tag.Genres = Genres; tag.MusicBrainzReleaseCountry = MusicBrainzReleaseCountry; tag.MusicBrainzReleaseStatus = MusicBrainzReleaseStatus; tag.MusicBrainzReleaseType = MusicBrainzReleaseType; @@ -341,6 +348,11 @@ namespace NzbDrone.Core.MediaFiles tag.MusicBrainzReleaseGroupId = MusicBrainzReleaseGroupId; tag.MusicBrainzTrackId = MusicBrainzTrackId; + if (ImageFile.IsNotNullOrWhiteSpace()) + { + tag.Pictures = new IPicture[1] { new Picture(ImageFile) }; + } + if (file.TagTypes.HasFlag(TagTypes.Id3v2)) { var id3tag = (TagLib.Id3v2.Tag) file.GetTag(TagTypes.Id3v2); @@ -524,6 +536,16 @@ namespace NzbDrone.Core.MediaFiles output.Add("Label", Tuple.Create(Publisher, other.Publisher)); } + if (!Genres.SequenceEqual(other.Genres)) + { + output.Add("Genres", Tuple.Create(string.Join(", ", Genres), string.Join(", ", other.Genres))); + } + + if (ImageSize != other.ImageSize) + { + output.Add("Image Size", Tuple.Create(ImageSize.ToString(), other.ImageSize.ToString())); + } + return output; } diff --git a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs index 36f053239..8d660589a 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTagService.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTagService.cs @@ -16,6 +16,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using TagLib; using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaCover; namespace NzbDrone.Core.MediaFiles { @@ -40,6 +41,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; private readonly IArtistService _artistService; + private readonly IMapCoversToLocal _mediaCoverService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -47,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles IMediaFileService mediaFileService, IDiskProvider diskProvider, IArtistService artistService, + IMapCoversToLocal mediaCoverService, IEventAggregator eventAggregator, Logger logger) { @@ -54,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles _mediaFileService = mediaFileService; _diskProvider = diskProvider; _artistService = artistService; + _mediaCoverService = mediaCoverService; _eventAggregator = eventAggregator; _logger = logger; } @@ -76,6 +80,23 @@ namespace NzbDrone.Core.MediaFiles var albumartist = album.Artist.Value; var artist = track.ArtistMetadata.Value; + var cover = album.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover); + string imageFile = null; + long imageSize = 0; + if (cover != null) + { + imageFile = _mediaCoverService.GetCoverPath(album.Id, MediaCoverEntity.Album, cover.CoverType, cover.Extension, null); + var fileInfo = _diskProvider.GetFileInfo(imageFile); + if (fileInfo.Exists) + { + imageSize = fileInfo.Length; + } + else + { + imageFile = null; + } + } + return new AudioTag { Title = track.Title, Performers = new [] { artist.Name }, @@ -91,6 +112,9 @@ namespace NzbDrone.Core.MediaFiles OriginalReleaseDate = album.ReleaseDate, OriginalYear = (uint)album.ReleaseDate?.Year, Publisher = release.Label.FirstOrDefault(), + Genres = album.Genres.Any() ? album.Genres.ToArray() : artist.Genres.ToArray(), + ImageFile = imageFile, + ImageSize = imageSize, MusicBrainzReleaseCountry = IsoCountries.Find(release.Country.FirstOrDefault())?.TwoLetterCode, MusicBrainzReleaseStatus = release.Status.ToLower(), MusicBrainzReleaseType = album.AlbumType.ToLower(), diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs index f643299c0..3efde5a86 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music.Commands; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.History; +using NzbDrone.Core.MediaCover; namespace NzbDrone.Core.Music { @@ -33,6 +34,7 @@ namespace NzbDrone.Core.Music private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed; + private readonly IMapCoversToLocal _mediaCoverService; private readonly Logger _logger; public RefreshAlbumService(IAlbumService albumService, @@ -46,6 +48,7 @@ namespace NzbDrone.Core.Music IHistoryService historyService, IEventAggregator eventAggregator, ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed, + IMapCoversToLocal mediaCoverService, Logger logger) : base(logger, artistMetadataService) { @@ -59,6 +62,7 @@ namespace NzbDrone.Core.Music _historyService = historyService; _eventAggregator = eventAggregator; _checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed; + _mediaCoverService = mediaCoverService; _logger = logger; } @@ -155,6 +159,13 @@ namespace NzbDrone.Core.Music { result = UpdateResult.None; } + + // Force update and fetch covers if images have changed so that we can write them into tags + if (remote.Images.Any() && !local.Images.Select(x => x.Url).SequenceEqual(remote.Images.Select(x => x.Url))) + { + _mediaCoverService.EnsureAlbumCovers(remote); + result = UpdateResult.UpdateTags; + } local.ArtistMetadataId = remote.ArtistMetadata.Value.Id; local.ForeignAlbumId = remote.ForeignAlbumId;