New: Write out .m3u files for spotify playlists

(cherry picked from commit 7aa051caf25031c7af3bdace8f4c5e73a41c5fe5)
This commit is contained in:
ta264 2022-07-25 21:19:02 +01:00
commit 580afe4642
16 changed files with 507 additions and 58 deletions

View file

@ -19,11 +19,11 @@ namespace NzbDrone.Core.Test.ImportListTests
public void should_not_throw_if_playlist_tracks_is_null()
{
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(default(Paging<PlaylistTrack>));
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(default(FullPlaylist));
var result = Subject.Fetch(_api, "playlistid");
@ -39,11 +39,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
var result = Subject.Fetch(_api, "playlistid");
@ -62,11 +62,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
var result = Subject.Fetch(_api, "playlistid");
@ -108,11 +108,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
var result = Subject.Fetch(_api, "playlistid");
@ -155,11 +155,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
var result = Subject.Fetch(_api, "playlistid");
@ -204,11 +204,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
var result = Subject.Fetch(_api, "playlistid");
@ -251,11 +251,11 @@ namespace NzbDrone.Core.Test.ImportListTests
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetPlaylistTracks(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(playlistTracks);
Setup(x => x.GetPlaylist(It.IsAny<SpotifyPlaylist>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<string>(),
It.IsAny<string>()))
.Returns(new FullPlaylist { Tracks = playlistTracks });
Mocker.GetMock<ISpotifyProxy>()
.Setup(x => x.GetNextPage(It.IsAny<SpotifyFollowedArtists>(),

View file

@ -0,0 +1,23 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(48)]
public class add_playlists : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("Playlists")
.WithColumn("ForeignPlaylistId").AsString().Unique()
.WithColumn("Title").AsString()
.WithColumn("OutputFolder").AsString().Nullable();
Create.TableForModel("PlaylistEntries")
.WithColumn("PlaylistId").AsInt32().Indexed()
.WithColumn("Order").AsInt32()
.WithColumn("ForeignAlbumId").AsString().Indexed()
.WithColumn("TrackTitle").AsString();
}
}
}

View file

@ -195,6 +195,11 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<DownloadHistory>("DownloadHistory").RegisterModel();
Mapper.Entity<UpdateHistory>("UpdateHistory").RegisterModel();
Mapper.Entity<Playlist>("Playlists").RegisterModel()
.Ignore(p => p.Items);
Mapper.Entity<PlaylistEntry>("PlaylistEntries").RegisterModel();
}
private static void RegisterMappers()

View file

@ -110,6 +110,8 @@ namespace NzbDrone.Core.ImportLists.Spotify
// map to musicbrainz ids
releases = MapSpotifyReleases(releases);
ProcessMappedReleases(releases);
return CleanupListItems(releases);
}
@ -291,6 +293,11 @@ namespace NzbDrone.Core.ImportLists.Spotify
}
}
protected virtual void ProcessMappedReleases(IList<SpotifyImportListItemInfo> items)
{
return;
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());

View file

@ -5,26 +5,33 @@ using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Enums;
using SpotifyAPI.Web.Models;
namespace NzbDrone.Core.ImportLists.Spotify
{
public class SpotifyPlaylist : SpotifyImportListBase<SpotifyPlaylistSettings>
{
private readonly IPlaylistService _playlistService;
public SpotifyPlaylist(ISpotifyProxy spotifyProxy,
IMetadataRequestBuilder requestBuilder,
IImportListStatusService importListStatusService,
IImportListRepository importListRepository,
IPlaylistService playlistService,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
{
_playlistService = playlistService;
}
public override string Name => "Spotify Playlists";
@ -40,7 +47,9 @@ namespace NzbDrone.Core.ImportLists.Spotify
_logger.Trace($"Processing playlist {playlistId}");
var playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, artists(id, name), album(id, name, release_date, release_date_precision, artists(id, name))))");
var playlist = _spotifyProxy.GetPlaylist(this, api, playlistId, "id, name, tracks.next, tracks.items(track(name, artists(id, name), album(id, album_type, name, release_date, release_date_precision, artists(id, name))))");
var playlistTracks = playlist?.Tracks;
int order = 0;
while (true)
{
@ -51,7 +60,7 @@ namespace NzbDrone.Core.ImportLists.Spotify
foreach (var playlistTrack in playlistTracks.Items)
{
result.AddIfNotNull(ParsePlaylistTrack(playlistTrack));
result.AddIfNotNull(ParsePlaylistTrack(api, playlistTrack, playlistId, playlist.Name, ref order));
}
if (!playlistTracks.HasNextPage())
@ -65,29 +74,87 @@ namespace NzbDrone.Core.ImportLists.Spotify
return result;
}
private SpotifyImportListItemInfo ParsePlaylistTrack(PlaylistTrack playlistTrack)
protected override void ProcessMappedReleases(IList<SpotifyImportListItemInfo> items)
{
var spotifyPlaylistItems = items.Select(x => (SpotifyPlaylistItemInfo)x);
var groups = spotifyPlaylistItems.GroupBy(x => x.ForeignPlaylistId);
foreach (var group in groups)
{
var first = group.First();
var playlistItems = group.Select(x => new PlaylistEntry
{
Order = x.Order,
ForeignAlbumId = x.AlbumMusicBrainzId,
TrackTitle = x.TrackTitle
}).ToList();
var playlist = new Playlist
{
ForeignPlaylistId = first.ForeignPlaylistId,
Title = first.PlaylistTitle,
OutputFolder = Settings.PlaylistFolder,
Items = playlistItems
};
_playlistService.UpdatePlaylist(playlist);
}
}
private SpotifyPlaylistItemInfo ParsePlaylistTrack(SpotifyWebAPI api, PlaylistTrack playlistTrack, string playlistId, string playlistName, ref int order)
{
// From spotify docs: "Note, a track object may be null. This can happen if a track is no longer available."
if (playlistTrack?.Track?.Album != null)
if (playlistTrack?.Track?.Album == null)
{
var album = playlistTrack.Track.Album;
var albumName = album.Name;
var artistName = album.Artists?.FirstOrDefault()?.Name ?? playlistTrack.Track?.Artists?.FirstOrDefault()?.Name;
if (albumName.IsNotNullOrWhiteSpace() && artistName.IsNotNullOrWhiteSpace())
{
return new SpotifyImportListItemInfo
{
Artist = artistName,
Album = album.Name,
AlbumSpotifyId = album.Id,
ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision)
};
}
return null;
}
return null;
var album = playlistTrack.Track.Album;
var trackName = playlistTrack.Track.Name;
var artistName = album.Artists?.FirstOrDefault()?.Name ?? playlistTrack.Track?.Artists?.FirstOrDefault()?.Name;
if (artistName.IsNullOrWhiteSpace())
{
return null;
}
_logger.Trace($"album {album.Name} type: {album.AlbumType}");
if (album.AlbumType == "single")
{
album = GetBestAlbum(api, artistName, trackName) ?? album;
}
_logger.Trace($"revised type: {album.AlbumType}");
var albumName = album.Name;
if (albumName.IsNullOrWhiteSpace())
{
return null;
}
return new SpotifyPlaylistItemInfo
{
ForeignPlaylistId = playlistId,
Artist = artistName,
Album = album.Name,
AlbumSpotifyId = album.Id,
PlaylistTitle = playlistName,
TrackTitle = playlistTrack.Track.Name,
Order = ++order,
ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision)
};
}
private SimpleAlbum GetBestAlbum(SpotifyWebAPI api, string artistName, string trackName)
{
_logger.Trace($"Finding full album for {artistName}: {trackName}");
var search = _spotifyProxy.SearchItems(this, api, $"artist:\"{artistName}\" track:\"{trackName}\"", SearchType.Track);
return search?.Tracks?.Items?.FirstOrDefault(x => x?.Album?.AlbumType == "album" && !(x?.Album?.Artists?.Any(a => a.Name == "Various Artists") ?? false))?.Album;
}
public override object RequestAction(string action, IDictionary<string, string> query)

View file

@ -0,0 +1,10 @@
namespace NzbDrone.Core.ImportLists.Spotify
{
public class SpotifyPlaylistItemInfo : SpotifyImportListItemInfo
{
public string ForeignPlaylistId { get; set; }
public string PlaylistTitle { get; set; }
public string TrackTitle { get; set; }
public int Order { get; set; }
}
}

View file

@ -26,5 +26,8 @@ namespace NzbDrone.Core.ImportLists.Spotify
[FieldDefinition(1, Label = "Playlists", Type = FieldType.Playlist)]
public IEnumerable<string> PlaylistIds { get; set; }
[FieldDefinition(1, Label = "Output folder", HelpText = "Folder for the .m3u files containing tracks of imported playlists", Type = FieldType.Path)]
public string PlaylistFolder { get; set; }
}
}

View file

@ -16,12 +16,14 @@ namespace NzbDrone.Core.ImportLists.Spotify
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<SavedAlbum> GetSavedAlbums<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
FullPlaylist GetPlaylist<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
where TSettings : SpotifySettingsBase<TSettings>, new();
FollowedArtists GetNextPage<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, FollowedArtists item)
where TSettings : SpotifySettingsBase<TSettings>, new();
SearchItem SearchItems<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string query, SearchType type)
where TSettings : SpotifySettingsBase<TSettings>, new();
}
public class SpotifyProxy : ISpotifyProxy
@ -57,10 +59,10 @@ namespace NzbDrone.Core.ImportLists.Spotify
return Execute(list, api, x => x.GetSavedAlbums(50));
}
public Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
public FullPlaylist GetPlaylist<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetPlaylistTracks(id, fields: fields));
return Execute(list, api, x => x.GetPlaylist(id, fields: fields));
}
public Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
@ -75,6 +77,12 @@ namespace NzbDrone.Core.ImportLists.Spotify
return Execute(list, api, (x) => x.GetNextPage<FollowedArtists, FullArtist>(item.Artists));
}
public SearchItem SearchItems<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string query, SearchType type)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, (x) => x.SearchItems(query, type));
}
public T Execute<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Func<SpotifyWebAPI, T> method, bool allowReauth = true)
where T : BasicModel
where TSettings : SpotifySettingsBase<TSettings>, new()

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class Playlist : ModelBase
{
public string ForeignPlaylistId { get; set; }
public string Title { get; set; }
public string OutputFolder { get; set; }
public List<PlaylistEntry> Items { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class PlaylistEntry : ModelBase
{
public int PlaylistId { get; set; }
public int Order { get; set; }
public string ForeignAlbumId { get; set; }
public string TrackTitle { get; set; }
}
}

View file

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Music
{
public interface IPlaylistEntryRepository : IBasicRepository<PlaylistEntry>
{
List<PlaylistEntry> UpsertMany(List<PlaylistEntry> entries);
List<PlaylistEntry> FindByPlaylistId(int playlistId);
List<int> FindPlaylistsByForeignAlbumId(string foreignAlbumId);
}
public class PlaylistEntryRepository : BasicRepository<PlaylistEntry>, IPlaylistEntryRepository
{
public PlaylistEntryRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public List<PlaylistEntry> UpsertMany(List<PlaylistEntry> entries)
{
var playlistIds = entries.Select(x => x.PlaylistId).Distinct();
Delete(x => playlistIds.Contains(x.PlaylistId));
InsertMany(entries);
return entries;
}
public List<PlaylistEntry> FindByPlaylistId(int playlistId)
{
return Query(x => x.PlaylistId == playlistId).ToList();
}
public List<PlaylistEntry> FindByPlaylistId(List<int> playlistIds)
{
return Query(x => playlistIds.Contains(x.Id)).ToList();
}
public List<int> FindPlaylistsByForeignAlbumId(string foreignAlbumId)
{
return Query(x => x.ForeignAlbumId == foreignAlbumId).Select(x => x.PlaylistId).Distinct().ToList();
}
}
}

View file

@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Music
{
public interface IPlaylistRepository : IBasicRepository<Playlist>
{
Playlist GetPlaylist(string foreignPlaylistId);
}
public class PlaylistRepository : BasicRepository<Playlist>, IPlaylistRepository
{
public PlaylistRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public Playlist GetPlaylist(string foreignPlaylistId)
{
return Query(x => x.ForeignPlaylistId == foreignPlaylistId).FirstOrDefault();
}
}
}

View file

@ -10,6 +10,7 @@ namespace NzbDrone.Core.Music
{
List<Track> GetTracks(int artistId);
List<Track> GetTracksByAlbum(int albumId);
List<Track> GetTracksByForeignAlbumId(string foreignAlbumId);
List<Track> GetTracksByRelease(int albumReleaseId);
List<Track> GetTracksByReleases(List<int> albumReleaseIds);
List<Track> GetTracksForRefresh(int albumReleaseId, IEnumerable<string> foreignTrackIds);
@ -47,6 +48,15 @@ namespace NzbDrone.Core.Music
.Where<Album>(x => x.Id == albumId));
}
public List<Track> GetTracksByForeignAlbumId(string foreignAlbumId)
{
return Query(Builder()
.Join<Track, AlbumRelease>((t, r) => t.AlbumReleaseId == r.Id)
.Join<AlbumRelease, Album>((r, a) => r.AlbumId == a.Id)
.Where<AlbumRelease>(r => r.Monitored == true)
.Where<Album>(x => x.ForeignAlbumId == foreignAlbumId));
}
public List<Track> GetTracksByRelease(int albumReleaseId)
{
return Query(t => t.AlbumReleaseId == albumReleaseId).ToList();

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.ImportLists.Exclusions
{
public interface IPlaylistEntryService
{
List<PlaylistEntry> UpsertMany(List<PlaylistEntry> entries);
List<PlaylistEntry> FindByPlaylistId(int playlistId);
List<int> FindPlaylistsByForeignAlbumId(string foreignAlbumId);
}
public class PlaylistEntryService : IPlaylistEntryService
{
private readonly IPlaylistEntryRepository _repo;
public PlaylistEntryService(IPlaylistEntryRepository repo)
{
_repo = repo;
}
public List<PlaylistEntry> FindByPlaylistId(int playlistId)
{
return _repo.FindByPlaylistId(playlistId);
}
public List<PlaylistEntry> UpsertMany(List<PlaylistEntry> entries)
{
return _repo.UpsertMany(entries);
}
public List<int> FindPlaylistsByForeignAlbumId(string foreignAlbumId)
{
return _repo.FindPlaylistsByForeignAlbumId(foreignAlbumId);
}
}
}

View file

@ -0,0 +1,125 @@
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.ImportLists.Exclusions
{
public interface IPlaylistService
{
Playlist UpdatePlaylist(Playlist playlist);
}
public class PlaylistService : IPlaylistService,
IHandleAsync<AlbumImportedEvent>,
IHandleAsync<TrackFileRenamedEvent>
{
private readonly IPlaylistRepository _repo;
private readonly IPlaylistEntryService _playlistEntryService;
private readonly ITrackService _trackService;
private readonly IMediaFileService _mediaFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public PlaylistService(IPlaylistRepository repo,
IPlaylistEntryService playlistEntryService,
ITrackService trackService,
IMediaFileService mediaFileService,
IDiskProvider diskProvider,
Logger logger)
{
_repo = repo;
_playlistEntryService = playlistEntryService;
_trackService = trackService;
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_logger = logger;
}
public Playlist UpdatePlaylist(Playlist playlist)
{
var existing = _repo.GetPlaylist(playlist.ForeignPlaylistId);
if (existing != null)
{
playlist.Id = existing.Id;
}
_repo.Upsert(playlist);
playlist.Items.ForEach(x => x.PlaylistId = playlist.Id);
_playlistEntryService.UpsertMany(playlist.Items);
WritePlaylist(playlist.Id);
return playlist;
}
public void WritePlaylist(int playlistId)
{
var playlist = _repo.Get(playlistId);
_logger.Debug($"Writing playlist {playlist.Title}");
playlist.Items = _playlistEntryService.FindByPlaylistId(playlistId);
_logger.Trace($"Got {playlist.Items.Count} tracks");
if (!playlist.Items.Any() || playlist.OutputFolder.IsNullOrWhiteSpace())
{
return;
}
var sb = new StringBuilder();
bool doWrite = false;
foreach (var item in playlist.Items.OrderBy(x => x.Order))
{
_logger.Trace($"Getting track for {item.ForeignAlbumId} {item.TrackTitle}");
var track = _trackService.FindTrackByTitleInexact(item.ForeignAlbumId, item.TrackTitle);
if (track?.HasFile == true)
{
var file = _mediaFileService.Get(track.TrackFileId);
var relative = Path.GetRelativePath(playlist.OutputFolder, file.Path);
_logger.Debug($"Got track {relative}");
sb.AppendLine(relative);
doWrite = true;
}
}
if (doWrite)
{
var filename = Path.Combine(playlist.OutputFolder, playlist.Title + ".m3u");
_diskProvider.WriteAllText(filename, sb.ToString());
}
}
public void HandleAsync(AlbumImportedEvent message)
{
var playlistIds = _playlistEntryService.FindPlaylistsByForeignAlbumId(message.Album.ForeignAlbumId);
foreach (var id in playlistIds)
{
WritePlaylist(id);
}
}
public void HandleAsync(TrackFileRenamedEvent message)
{
var albumId = message.TrackFile.Album.Value.ForeignAlbumId;
var playlistIds = _playlistEntryService.FindPlaylistsByForeignAlbumId(albumId);
foreach (var id in playlistIds)
{
WritePlaylist(id);
}
}
}
}

View file

@ -1,15 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Music
{
public interface ITrackService
{
Track GetTrack(int id);
Track FindTrackByTitleInexact(string foreignAlbumId, string title);
List<Track> GetTracks(IEnumerable<int> ids);
List<Track> GetTracksByArtist(int artistId);
List<Track> GetTracksByAlbum(int albumId);
@ -46,6 +50,31 @@ namespace NzbDrone.Core.Music
return _trackRepository.Get(id);
}
public Track FindTrackByTitleInexact(string foreignAlbumId, string title)
{
var normalizedTitle = title.NormalizeTrackTitle().Replace(".", " ");
var tracks = _trackRepository.GetTracksByForeignAlbumId(foreignAlbumId);
Func<Func<Track, string, double>, string, Tuple<Func<Track, string, double>, string>> tc = Tuple.Create;
var scoringFunctions = new List<Tuple<Func<Track, string, double>, string>>
{
tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyMatch(t), normalizedTitle),
tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyContains(t), normalizedTitle),
tc((a, t) => t.FuzzyContains(a.Title.NormalizeTrackTitle()), normalizedTitle)
};
foreach (var func in scoringFunctions)
{
var track = FindByStringInexact(tracks, func.Item1, func.Item2);
if (track != null)
{
return track;
}
}
return null;
}
public List<Track> GetTracks(IEnumerable<int> ids)
{
return _trackRepository.Get(ids).ToList();
@ -133,5 +162,32 @@ namespace NzbDrone.Core.Music
_logger.Debug($"Detaching tracks from file {message.TrackFile}");
_trackRepository.DetachTrackFile(message.TrackFile.Id);
}
private Track FindByStringInexact(List<Track> tracks, Func<Track, string, double> scoreFunction, string title)
{
const double fuzzThreshold = 0.7;
const double fuzzGap = 0.2;
var sortedTracks = tracks.Select(s => new
{
MatchProb = scoreFunction(s, title),
Track = s
}).ToList()
.OrderByDescending(s => s.MatchProb)
.ToList();
if (!sortedTracks.Any())
{
return null;
}
if (sortedTracks[0].MatchProb > fuzzThreshold
&& (sortedTracks.Count == 1 || sortedTracks[0].MatchProb - sortedTracks[1].MatchProb > fuzzGap))
{
return sortedTracks[0].Track;
}
return null;
}
}
}