Working on playlist generator, adding sort and recommender
This commit is contained in:
parent
77c1e3c79e
commit
e1094b131f
@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Mixonomer.Fire.Extensions;
|
||||
using Mixonomer;
|
||||
using Mixonomer.Playlist;
|
||||
|
||||
namespace Mixonomer.CLI;
|
||||
|
||||
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mixonomer.Fire;
|
||||
using Mixonomer.Playlist;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer.Func;
|
||||
|
6
Mixonomer/Exceptions/MixonomerException.cs
Normal file
6
Mixonomer/Exceptions/MixonomerException.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Mixonomer.Exceptions;
|
||||
|
||||
public class MixonomerException: Exception
|
||||
{
|
||||
|
||||
}
|
6
Mixonomer/Exceptions/PlaylistNotFoundException.cs
Normal file
6
Mixonomer/Exceptions/PlaylistNotFoundException.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Mixonomer.Exceptions;
|
||||
|
||||
public class PlaylistNotFoundException: MixonomerException
|
||||
{
|
||||
|
||||
}
|
@ -10,7 +10,7 @@ public class PartTreeWalker
|
||||
|
||||
private readonly HashSet<string> _processedPlaylists = new();
|
||||
public HashSet<string>? SpotifyPlaylistNames { get; private set; }
|
||||
private List<Playlist> _userPlaylists;
|
||||
private List<Fire.Playlist> _userPlaylists;
|
||||
|
||||
public PartTreeWalker(UserRepo userRepo)
|
||||
{
|
||||
|
71
Mixonomer/Playlist/CommonTrack.cs
Normal file
71
Mixonomer/Playlist/CommonTrack.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer.Playlist;
|
||||
|
||||
public class CommonTrack
|
||||
{
|
||||
public string TrackUri { get; set; }
|
||||
public string TrackName { get; set; }
|
||||
public string AlbumName { get; set; }
|
||||
public IEnumerable<string> ArtistNames { get; set; }
|
||||
public IEnumerable<string> AlbumArtistNames { get; set; }
|
||||
public int DiscNumber { get; set; }
|
||||
public int TrackNumber { get; set; }
|
||||
public DateTime? AddedTime { get; set; }
|
||||
public string ReleaseDate { get; set; }
|
||||
|
||||
public static explicit operator CommonTrack(PlaylistTrack<IPlayableItem> track)
|
||||
{
|
||||
if (track.Track is FullTrack fullTrack)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
TrackUri = fullTrack.Uri,
|
||||
TrackName = fullTrack.Name,
|
||||
AlbumName = fullTrack.Album.Name,
|
||||
ArtistNames = fullTrack.Artists.Select(x => x.Name).ToArray(),
|
||||
AlbumArtistNames = fullTrack.Album.Artists.Select(x => x.Name).ToArray(),
|
||||
AddedTime = track.AddedAt,
|
||||
ReleaseDate = fullTrack.Album.ReleaseDate,
|
||||
DiscNumber = fullTrack.DiscNumber,
|
||||
TrackNumber = fullTrack.TrackNumber
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Nested track of type {track.Track.GetType()}");
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator CommonTrack(SavedTrack track)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
TrackUri = track.Track.Uri,
|
||||
TrackName = track.Track.Name,
|
||||
AlbumName = track.Track.Album.Name,
|
||||
ArtistNames = track.Track.Artists.Select(x => x.Name).ToArray(),
|
||||
AlbumArtistNames = track.Track.Album.Artists.Select(x => x.Name).ToArray(),
|
||||
AddedTime = track.AddedAt,
|
||||
ReleaseDate = track.Track.Album.ReleaseDate,
|
||||
DiscNumber = track.Track.DiscNumber,
|
||||
TrackNumber = track.Track.TrackNumber
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator CommonTrack(SimpleTrack track)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
TrackUri = track.Uri,
|
||||
TrackName = track.Name,
|
||||
AlbumName = null,
|
||||
ArtistNames = track.Artists.Select(x => x.Name).ToArray(),
|
||||
AlbumArtistNames = Enumerable.Empty<string>(),
|
||||
AddedTime = null,
|
||||
ReleaseDate = null,
|
||||
DiscNumber = track.DiscNumber,
|
||||
TrackNumber = track.TrackNumber
|
||||
};
|
||||
}
|
||||
}
|
6
Mixonomer/Playlist/IRecommend.cs
Normal file
6
Mixonomer/Playlist/IRecommend.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Mixonomer.Playlist;
|
||||
|
||||
public interface IRecommend
|
||||
{
|
||||
Task<IEnumerable<CommonTrack>> GetRecommendations(Fire.Playlist playlist, IEnumerable<CommonTrack> currentTrackList);
|
||||
}
|
9
Mixonomer/Playlist/PlaylistGeneratingContext.cs
Normal file
9
Mixonomer/Playlist/PlaylistGeneratingContext.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer.Playlist;
|
||||
|
||||
public class PlaylistGeneratingContext
|
||||
{
|
||||
public IList<PlaylistTrack<IPlayableItem>> PartTracks { get; set; }
|
||||
public IList<SavedTrack> LibraryTracks { get; set; }
|
||||
}
|
148
Mixonomer/Playlist/PlaylistGenerator.cs
Normal file
148
Mixonomer/Playlist/PlaylistGenerator.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mixonomer.Exceptions;
|
||||
using Mixonomer.Fire;
|
||||
using Mixonomer.Fire.Extensions;
|
||||
using Mixonomer.Playlist.Sort;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer.Playlist;
|
||||
|
||||
public class PlaylistGenerator
|
||||
{
|
||||
private readonly ILogger<PlaylistGenerator> _logger;
|
||||
protected readonly UserRepo _userRepo;
|
||||
private readonly SpotifyNetworkProvider _spotifyMetworkProvider;
|
||||
protected readonly PartTreeWalker _partTreeWalker;
|
||||
|
||||
public PlaylistGenerator(UserRepo userRepo, SpotifyNetworkProvider spotifyMetworkProvider, PartTreeWalker partTreeWalker, ILogger<PlaylistGenerator> logger)
|
||||
{
|
||||
_userRepo = userRepo;
|
||||
_spotifyMetworkProvider = spotifyMetworkProvider;
|
||||
_logger = logger;
|
||||
_partTreeWalker = partTreeWalker;
|
||||
}
|
||||
|
||||
public async Task GeneratePlaylist(string playlistName, string username)
|
||||
{
|
||||
using var logScope = _logger.BeginScope(new Dictionary<string, string> { {"username", username}, {"playlist", playlistName} });
|
||||
|
||||
var user = await _userRepo.GetUser(username);
|
||||
var dbPlaylist = await _userRepo.GetPlaylists(user).FirstOrDefaultAsync(x => x.name == playlistName);
|
||||
|
||||
if (dbPlaylist is null)
|
||||
{
|
||||
_logger.LogError("Couldn't find playlist in database");
|
||||
throw new PlaylistNotFoundException();
|
||||
}
|
||||
|
||||
var spotifyClient = new SpotifyClient(await _spotifyMetworkProvider.GetUserConfig(user));
|
||||
|
||||
var userPlaylists = await spotifyClient.Playlists.CurrentUsers();
|
||||
var allPlaylists = await spotifyClient.PaginateAll(userPlaylists);
|
||||
|
||||
var parts = await GetFullPartList(user, dbPlaylist);
|
||||
var partPlaylists = GetPartPlaylists(dbPlaylist, allPlaylists, parts);
|
||||
|
||||
var context = new PlaylistGeneratingContext
|
||||
{
|
||||
PartTracks = await GetPlatlistTracks(spotifyClient, partPlaylists).ToListAsync(),
|
||||
LibraryTracks = await GetLibraryTracks(spotifyClient, dbPlaylist).ToListAsync()
|
||||
};
|
||||
|
||||
context = DoPlaylistTypeProcessing(context, user, dbPlaylist);
|
||||
|
||||
var combinedTracks = CollapseContextToCommonTracks(context);
|
||||
combinedTracks = SortTracks(combinedTracks, dbPlaylist);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> GetFullPartList(User user, Fire.Playlist playlist)
|
||||
{
|
||||
var parts = (await _partTreeWalker.GetPlaylistParts(user, playlist.name) ?? Enumerable.Empty<string>()).ToList();
|
||||
|
||||
if (playlist.add_last_month)
|
||||
{
|
||||
parts.Add(Months.LastMonth());
|
||||
}
|
||||
|
||||
if (playlist.add_this_month)
|
||||
{
|
||||
parts.Add(Months.ThisMonth());
|
||||
}
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No spotify playlist parts found");
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private IEnumerable<FullPlaylist> GetPartPlaylists(Fire.Playlist subjectPlaylist, IEnumerable<FullPlaylist> allPlaylists, IEnumerable<string> parts)
|
||||
{
|
||||
var allPlaylistDict = allPlaylists.ToDictionary(p => p.Name ?? "no name");
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (allPlaylistDict.TryGetValue(part, out var playlist))
|
||||
{
|
||||
if (!subjectPlaylist.include_spotify_owned &&
|
||||
(playlist.Owner?.DisplayName.Contains("spotify", StringComparison.InvariantCultureIgnoreCase) ?? false))
|
||||
{
|
||||
// skip
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return playlist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<PlaylistTrack<IPlayableItem>> GetPlatlistTracks(SpotifyClient client, IEnumerable<FullPlaylist> playlists)
|
||||
{
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
if (playlist.Tracks is { } tracks)
|
||||
{
|
||||
foreach (var track in await client.PaginateAll(tracks))
|
||||
{
|
||||
yield return track;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SavedTrack> GetLibraryTracks(SpotifyClient client, Fire.Playlist playlist)
|
||||
{
|
||||
if (playlist.include_library_tracks)
|
||||
{
|
||||
await foreach(var track in client.Paginate(await client.Library.GetTracks()))
|
||||
{
|
||||
yield return track;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual PlaylistGeneratingContext DoPlaylistTypeProcessing(PlaylistGeneratingContext context, User user, Fire.Playlist playlist)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<CommonTrack> CollapseContextToCommonTracks(PlaylistGeneratingContext context)
|
||||
{
|
||||
return context.PartTracks.Select(x => (CommonTrack)x)
|
||||
.Concat(context.LibraryTracks.Select(x => (CommonTrack)x));
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<CommonTrack> SortTracks(IEnumerable<CommonTrack> tracks, Fire.Playlist playlist)
|
||||
{
|
||||
if (playlist.shuffle)
|
||||
{
|
||||
return tracks.Shuffle();
|
||||
}
|
||||
else
|
||||
{
|
||||
return tracks.OrderByReleaseDate();
|
||||
}
|
||||
}
|
||||
}
|
19
Mixonomer/Playlist/SortExtensions.cs
Normal file
19
Mixonomer/Playlist/SortExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace Mixonomer.Playlist.Sort;
|
||||
|
||||
public static class SortExtensions
|
||||
{
|
||||
private static Random _rng = new Random();
|
||||
|
||||
public static IOrderedEnumerable<CommonTrack> OrderByArtistAlbumTrackNumber(this IEnumerable<CommonTrack> input) =>
|
||||
input.OrderBy(x => x.AlbumArtistNames.First())
|
||||
.ThenBy(x => x.AlbumName)
|
||||
.ThenBy(x => x.DiscNumber)
|
||||
.ThenBy(x => x.TrackNumber);
|
||||
|
||||
public static IOrderedEnumerable<CommonTrack> OrderByReleaseDate(this IEnumerable<CommonTrack> input) =>
|
||||
input.OrderByArtistAlbumTrackNumber()
|
||||
.ThenByDescending(x => x.ReleaseDate);
|
||||
|
||||
public static IOrderedEnumerable<CommonTrack> Shuffle(this IEnumerable<CommonTrack> input) =>
|
||||
input.OrderBy(x => _rng.Next());
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mixonomer.Fire;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer;
|
||||
|
||||
public class PlaylistGenerator
|
||||
{
|
||||
private readonly ILogger<PlaylistGenerator> _logger;
|
||||
private readonly UserRepo _userRepo;
|
||||
private readonly SpotifyNetworkProvider _spotifyMetworkProvider;
|
||||
private readonly PartTreeWalker _partTreeWalker;
|
||||
|
||||
public PlaylistGenerator(UserRepo userRepo, SpotifyNetworkProvider spotifyMetworkProvider, PartTreeWalker partTreeWalker, ILogger<PlaylistGenerator> logger)
|
||||
{
|
||||
_userRepo = userRepo;
|
||||
_spotifyMetworkProvider = spotifyMetworkProvider;
|
||||
_logger = logger;
|
||||
_partTreeWalker = partTreeWalker;
|
||||
}
|
||||
|
||||
public async Task GeneratePlaylist(string playlistName, string username)
|
||||
{
|
||||
var user = await _userRepo.GetUser(username);
|
||||
|
||||
var spotifyConfig = await _spotifyMetworkProvider.GetUserConfig(user);
|
||||
var spotifyClient = new SpotifyClient(spotifyConfig);
|
||||
|
||||
var userPlaylists = await spotifyClient.Playlists.CurrentUsers();
|
||||
var allPlaylists = await spotifyClient.PaginateAll(userPlaylists);
|
||||
}
|
||||
}
|
@ -35,6 +35,14 @@ public class SpotifyNetworkProvider
|
||||
var refreshed = await new OAuthClient()
|
||||
.RequestToken(new AuthorizationCodeRefreshRequest(spotifyClientStr, spotifySecretStr, user.refresh_token));
|
||||
|
||||
await WriteUserTokenUpdate(user, new
|
||||
{
|
||||
access_token = refreshed.AccessToken,
|
||||
refresh_token = refreshed.RefreshToken,
|
||||
last_refreshed = refreshed.CreatedAt,
|
||||
token_expiry = refreshed.ExpiresIn
|
||||
});
|
||||
|
||||
var authenticator = new AuthorizationCodeAuthenticator(spotifyClientStr, spotifySecretStr, new()
|
||||
{
|
||||
AccessToken = refreshed.AccessToken,
|
||||
@ -47,21 +55,13 @@ public class SpotifyNetworkProvider
|
||||
|
||||
authenticator.TokenRefreshed += async (sender, resp) =>
|
||||
{
|
||||
try
|
||||
await WriteUserTokenUpdate(user, new
|
||||
{
|
||||
_logger.LogInformation("Token refreshed for [{}], writing to database", user.username);
|
||||
await user.Reference.SetAsync(new
|
||||
{
|
||||
access_token = resp.AccessToken,
|
||||
refresh_token = resp.RefreshToken,
|
||||
last_refreshed = resp.CreatedAt,
|
||||
token_expiry = resp.ExpiresIn
|
||||
}, SetOptions.MergeAll);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to write updated Spotify tokens to database for [{}]", user.username);
|
||||
}
|
||||
access_token = resp.AccessToken,
|
||||
refresh_token = resp.RefreshToken,
|
||||
last_refreshed = resp.CreatedAt,
|
||||
token_expiry = resp.ExpiresIn
|
||||
});
|
||||
};
|
||||
|
||||
var config = SpotifyClientConfig
|
||||
@ -70,4 +70,17 @@ public class SpotifyNetworkProvider
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private async Task WriteUserTokenUpdate(User user, object updates)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Token refreshed for [{}], writing to database", user.username);
|
||||
await user.Reference.SetAsync(updates, SetOptions.MergeAll);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to write updated Spotify tokens to database for [{}]", user.username);
|
||||
}
|
||||
}
|
||||
}
|
30
Mixonomer/Spotify/SpotifyRecommender.cs
Normal file
30
Mixonomer/Spotify/SpotifyRecommender.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Mixonomer.Playlist;
|
||||
using Mixonomer.Playlist.Sort;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace Mixonomer;
|
||||
|
||||
public class SpotifyRecommender: IRecommend
|
||||
{
|
||||
private readonly SpotifyClient _client;
|
||||
|
||||
public SpotifyRecommender(SpotifyClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CommonTrack>> GetRecommendations(Fire.Playlist playlist,
|
||||
IEnumerable<CommonTrack> currentTrackList)
|
||||
{
|
||||
if (playlist.include_recommendations)
|
||||
{
|
||||
var request = new RecommendationsRequest();
|
||||
|
||||
var response = await _client.Browse.GetRecommendations(request);
|
||||
|
||||
return response.Tracks.Select(x => (CommonTrack) x);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<CommonTrack>();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user