mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-19 13:10:13 -07:00
New: Write genres and album art to track file tags
This commit is contained in:
parent
a0a96911f8
commit
a35f965d31
10 changed files with 81 additions and 10 deletions
|
@ -75,7 +75,6 @@ class RetagPreviewModalContentConnector extends Component {
|
||||||
RetagPreviewModalContentConnector.propTypes = {
|
RetagPreviewModalContentConnector.propTypes = {
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
albumId: PropTypes.number,
|
albumId: PropTypes.number,
|
||||||
retagTracks: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
fetchRetagPreview: PropTypes.func.isRequired,
|
fetchRetagPreview: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
@ -7,16 +8,19 @@ import styles from './RetagPreviewRow.css';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
|
||||||
function formatMissing(value) {
|
function formatValue(field, value) {
|
||||||
if (value === undefined || value === 0 || value === '0') {
|
if (value === undefined || value === 0 || value === '0' || value === '') {
|
||||||
return (<Icon name={icons.BAN} size={12} />);
|
return (<Icon name={icons.BAN} size={12} />);
|
||||||
}
|
}
|
||||||
|
if (field === 'Image Size') {
|
||||||
|
return formatBytes(value);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChange(oldValue, newValue) {
|
function formatChange(field, oldValue, newValue) {
|
||||||
return (
|
return (
|
||||||
<div> {formatMissing(oldValue)} <Icon name={icons.ARROW_RIGHT_NO_CIRCLE} size={12} /> {formatMissing(newValue)} </div>
|
<div> {formatValue(field, oldValue)} <Icon name={icons.ARROW_RIGHT_NO_CIRCLE} size={12} /> {formatValue(field, newValue)} </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +82,7 @@ class RetagPreviewRow extends Component {
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
key={field}
|
key={field}
|
||||||
title={field}
|
title={field}
|
||||||
data={formatChange(oldValue, newValue)}
|
data={formatChange(field, oldValue, newValue)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
Binary file not shown.
BIN
src/NzbDrone.Core.Test/Files/Media/nin.png
Normal file
BIN
src/NzbDrone.Core.Test/Files/Media/nin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
|
@ -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[] 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<string, string[]> SkipPropertiesByFile = new Dictionary<string, string[]> {
|
private static readonly Dictionary<string, string[]> SkipPropertiesByFile = new Dictionary<string, string[]> {
|
||||||
{ "nin.mp2", new [] {"OriginalReleaseDate"} }
|
{ "nin.mp2", new [] {"OriginalReleaseDate"} }
|
||||||
};
|
};
|
||||||
|
@ -61,6 +61,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||||
.Setup(x => x.WriteAudioTags)
|
.Setup(x => x.WriteAudioTags)
|
||||||
.Returns(WriteAudioTagsType.Sync);
|
.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
|
// have to manually set the arrays of string parameters and integers to values > 1
|
||||||
testTags = Builder<AudioTag>.CreateNew()
|
testTags = Builder<AudioTag>.CreateNew()
|
||||||
.With(x => x.Track = 2)
|
.With(x => x.Track = 2)
|
||||||
|
@ -73,6 +76,9 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||||
.With(x => x.OriginalYear = 2009)
|
.With(x => x.OriginalYear = 2009)
|
||||||
.With(x => x.Performers = new [] { "Performer1" })
|
.With(x => x.Performers = new [] { "Performer1" })
|
||||||
.With(x => x.AlbumArtists = new [] { "방탄소년단" })
|
.With(x => x.AlbumArtists = new [] { "방탄소년단" })
|
||||||
|
.With(x => x.Genres = new [] { "Genre1", "Genre2" })
|
||||||
|
.With(x => x.ImageFile = imageFile)
|
||||||
|
.With(x => x.ImageSize = imageSize)
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +234,8 @@ namespace NzbDrone.Core.Test.MediaFiles.AudioTagServiceFixture
|
||||||
var tag = Subject.ReadAudioTag(path);
|
var tag = Subject.ReadAudioTag(path);
|
||||||
var expected = new AudioTag() {
|
var expected = new AudioTag() {
|
||||||
Performers = new string[0],
|
Performers = new string[0],
|
||||||
AlbumArtists = new string[0]
|
AlbumArtists = new string[0],
|
||||||
|
Genres = new string[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
VerifySame(tag, expected, skipProperties);
|
VerifySame(tag, expected, skipProperties);
|
||||||
|
|
|
@ -471,6 +471,9 @@
|
||||||
<Content Include="Files\LongOverview.txt">
|
<Content Include="Files\LongOverview.txt">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="Files\Media\nin.png">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="Files\Media\nin.mp2">
|
<Content Include="Files\Media\nin.mp2">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaCover
|
||||||
{
|
{
|
||||||
void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnumerable<MediaCover> covers);
|
void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnumerable<MediaCover> covers);
|
||||||
string GetCoverPath(int entityId, MediaCoverEntity coverEntity, MediaCoverTypes mediaCoverTypes, string extension, int? height = null);
|
string GetCoverPath(int entityId, MediaCoverEntity coverEntity, MediaCoverTypes mediaCoverTypes, string extension, int? height = null);
|
||||||
|
void EnsureAlbumCovers(Album album);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MediaCoverService :
|
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))
|
foreach (var cover in album.Images.Where(e => e.CoverType == MediaCoverTypes.Cover))
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,6 +34,9 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
public uint OriginalYear { get; set; }
|
public uint OriginalYear { get; set; }
|
||||||
public string Publisher { get; set; }
|
public string Publisher { get; set; }
|
||||||
public TimeSpan Duration { 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 MusicBrainzReleaseCountry { get; set; }
|
||||||
public string MusicBrainzReleaseStatus { get; set; }
|
public string MusicBrainzReleaseStatus { get; set; }
|
||||||
public string MusicBrainzReleaseType { get; set; }
|
public string MusicBrainzReleaseType { get; set; }
|
||||||
|
@ -81,6 +84,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
Year = tag.Year;
|
Year = tag.Year;
|
||||||
Publisher = tag.Publisher;
|
Publisher = tag.Publisher;
|
||||||
Duration = file.Properties.Duration;
|
Duration = file.Properties.Duration;
|
||||||
|
Genres = tag.Genres;
|
||||||
|
ImageSize = tag.Pictures.FirstOrDefault()?.Data.Count ?? 0;
|
||||||
MusicBrainzReleaseCountry = tag.MusicBrainzReleaseCountry;
|
MusicBrainzReleaseCountry = tag.MusicBrainzReleaseCountry;
|
||||||
MusicBrainzReleaseStatus = tag.MusicBrainzReleaseStatus;
|
MusicBrainzReleaseStatus = tag.MusicBrainzReleaseStatus;
|
||||||
MusicBrainzReleaseType = tag.MusicBrainzReleaseType;
|
MusicBrainzReleaseType = tag.MusicBrainzReleaseType;
|
||||||
|
@ -315,6 +320,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
// WMA with null performers/albumartists
|
// WMA with null performers/albumartists
|
||||||
Performers = Performers ?? new string[0];
|
Performers = Performers ?? new string[0];
|
||||||
AlbumArtists = AlbumArtists ?? new string[0];
|
AlbumArtists = AlbumArtists ?? new string[0];
|
||||||
|
Genres = Genres ?? new string[0];
|
||||||
|
|
||||||
TagLib.File file = null;
|
TagLib.File file = null;
|
||||||
try
|
try
|
||||||
|
@ -332,6 +338,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
tag.Disc = Disc;
|
tag.Disc = Disc;
|
||||||
tag.DiscCount = DiscCount;
|
tag.DiscCount = DiscCount;
|
||||||
tag.Publisher = Publisher;
|
tag.Publisher = Publisher;
|
||||||
|
tag.Genres = Genres;
|
||||||
tag.MusicBrainzReleaseCountry = MusicBrainzReleaseCountry;
|
tag.MusicBrainzReleaseCountry = MusicBrainzReleaseCountry;
|
||||||
tag.MusicBrainzReleaseStatus = MusicBrainzReleaseStatus;
|
tag.MusicBrainzReleaseStatus = MusicBrainzReleaseStatus;
|
||||||
tag.MusicBrainzReleaseType = MusicBrainzReleaseType;
|
tag.MusicBrainzReleaseType = MusicBrainzReleaseType;
|
||||||
|
@ -341,6 +348,11 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
tag.MusicBrainzReleaseGroupId = MusicBrainzReleaseGroupId;
|
tag.MusicBrainzReleaseGroupId = MusicBrainzReleaseGroupId;
|
||||||
tag.MusicBrainzTrackId = MusicBrainzTrackId;
|
tag.MusicBrainzTrackId = MusicBrainzTrackId;
|
||||||
|
|
||||||
|
if (ImageFile.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
tag.Pictures = new IPicture[1] { new Picture(ImageFile) };
|
||||||
|
}
|
||||||
|
|
||||||
if (file.TagTypes.HasFlag(TagTypes.Id3v2))
|
if (file.TagTypes.HasFlag(TagTypes.Id3v2))
|
||||||
{
|
{
|
||||||
var id3tag = (TagLib.Id3v2.Tag) file.GetTag(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));
|
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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ using NzbDrone.Core.MediaFiles.Events;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using TagLib;
|
using TagLib;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.MediaCover;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles
|
namespace NzbDrone.Core.MediaFiles
|
||||||
{
|
{
|
||||||
|
@ -40,6 +41,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
private readonly IMediaFileService _mediaFileService;
|
private readonly IMediaFileService _mediaFileService;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IArtistService _artistService;
|
private readonly IArtistService _artistService;
|
||||||
|
private readonly IMapCoversToLocal _mediaCoverService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
IMediaFileService mediaFileService,
|
IMediaFileService mediaFileService,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IArtistService artistService,
|
IArtistService artistService,
|
||||||
|
IMapCoversToLocal mediaCoverService,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
|
@ -54,6 +57,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_mediaFileService = mediaFileService;
|
_mediaFileService = mediaFileService;
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_artistService = artistService;
|
_artistService = artistService;
|
||||||
|
_mediaCoverService = mediaCoverService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -76,6 +80,23 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
var albumartist = album.Artist.Value;
|
var albumartist = album.Artist.Value;
|
||||||
var artist = track.ArtistMetadata.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 {
|
return new AudioTag {
|
||||||
Title = track.Title,
|
Title = track.Title,
|
||||||
Performers = new [] { artist.Name },
|
Performers = new [] { artist.Name },
|
||||||
|
@ -91,6 +112,9 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
OriginalReleaseDate = album.ReleaseDate,
|
OriginalReleaseDate = album.ReleaseDate,
|
||||||
OriginalYear = (uint)album.ReleaseDate?.Year,
|
OriginalYear = (uint)album.ReleaseDate?.Year,
|
||||||
Publisher = release.Label.FirstOrDefault(),
|
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,
|
MusicBrainzReleaseCountry = IsoCountries.Find(release.Country.FirstOrDefault())?.TwoLetterCode,
|
||||||
MusicBrainzReleaseStatus = release.Status.ToLower(),
|
MusicBrainzReleaseStatus = release.Status.ToLower(),
|
||||||
MusicBrainzReleaseType = album.AlbumType.ToLower(),
|
MusicBrainzReleaseType = album.AlbumType.ToLower(),
|
||||||
|
|
|
@ -12,6 +12,7 @@ using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Music.Commands;
|
using NzbDrone.Core.Music.Commands;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.History;
|
using NzbDrone.Core.History;
|
||||||
|
using NzbDrone.Core.MediaCover;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Music
|
namespace NzbDrone.Core.Music
|
||||||
{
|
{
|
||||||
|
@ -33,6 +34,7 @@ namespace NzbDrone.Core.Music
|
||||||
private readonly IHistoryService _historyService;
|
private readonly IHistoryService _historyService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed;
|
private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed;
|
||||||
|
private readonly IMapCoversToLocal _mediaCoverService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public RefreshAlbumService(IAlbumService albumService,
|
public RefreshAlbumService(IAlbumService albumService,
|
||||||
|
@ -46,6 +48,7 @@ namespace NzbDrone.Core.Music
|
||||||
IHistoryService historyService,
|
IHistoryService historyService,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed,
|
ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed,
|
||||||
|
IMapCoversToLocal mediaCoverService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(logger, artistMetadataService)
|
: base(logger, artistMetadataService)
|
||||||
{
|
{
|
||||||
|
@ -59,6 +62,7 @@ namespace NzbDrone.Core.Music
|
||||||
_historyService = historyService;
|
_historyService = historyService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed;
|
_checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed;
|
||||||
|
_mediaCoverService = mediaCoverService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +160,13 @@ namespace NzbDrone.Core.Music
|
||||||
result = UpdateResult.None;
|
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.ArtistMetadataId = remote.ArtistMetadata.Value.Id;
|
||||||
local.ForeignAlbumId = remote.ForeignAlbumId;
|
local.ForeignAlbumId = remote.ForeignAlbumId;
|
||||||
local.OldForeignAlbumIds = remote.OldForeignAlbumIds;
|
local.OldForeignAlbumIds = remote.OldForeignAlbumIds;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue