playlist generating working without recommendations

This commit is contained in:
Andy Pack 2024-06-14 07:23:25 +01:00
parent e1094b131f
commit 97cf1839c0
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
7 changed files with 79 additions and 30 deletions

View File

@ -16,17 +16,13 @@ class Program
{ {
var repo = new UserRepo(projectId: System.Environment.GetEnvironmentVariable("GOOGLE_CLOUD_PROJECT")); var repo = new UserRepo(projectId: System.Environment.GetEnvironmentVariable("GOOGLE_CLOUD_PROJECT"));
var userContext = await repo.GetUserContext("andy");
Console.WriteLine(userContext.User);
var walker = new PartTreeWalker(repo); var walker = new PartTreeWalker(repo);
var partPlaylists = await walker.GetPlaylistParts("andy", "RAP"); // var partPlaylists = await walker.GetPlaylistParts("andy", "RAP");
var spotifyNetwork = new SpotifyNetworkProvider(repo, null, NullLogger<SpotifyNetworkProvider>.Instance); var spotifyNetwork = new SpotifyNetworkProvider(repo, null, NullLogger<SpotifyNetworkProvider>.Instance);
var generator = new PlaylistGenerator(repo, spotifyNetwork, walker, NullLogger<PlaylistGenerator>.Instance); var generator = new PlaylistGenerator(repo, spotifyNetwork, walker, NullLogger<PlaylistGenerator>.Instance);
await generator.GeneratePlaylist("RAP", "andy"); await generator.GeneratePlaylist("POP", "andy");
} }
} }

View File

@ -0,0 +1,6 @@
namespace Mixonomer.Extensions;
public static class StringExtensions
{
public static string UriToId(this string uri) => uri.Split(':')[2];
}

View File

@ -1,3 +1,4 @@
using System.Collections;
using SpotifyAPI.Web; using SpotifyAPI.Web;
namespace Mixonomer.Playlist; namespace Mixonomer.Playlist;
@ -6,4 +7,7 @@ public class PlaylistGeneratingContext
{ {
public IList<PlaylistTrack<IPlayableItem>> PartTracks { get; set; } public IList<PlaylistTrack<IPlayableItem>> PartTracks { get; set; }
public IList<SavedTrack> LibraryTracks { get; set; } public IList<SavedTrack> LibraryTracks { get; set; }
public IEnumerable<CommonTrack> ToCommonTracks() => PartTracks.Select(x => (CommonTrack)x)
.Concat(LibraryTracks.Select(x => (CommonTrack)x));
} }

View File

@ -1,5 +1,7 @@
using Google.Cloud.Firestore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Mixonomer.Exceptions; using Mixonomer.Exceptions;
using Mixonomer.Extensions;
using Mixonomer.Fire; using Mixonomer.Fire;
using Mixonomer.Fire.Extensions; using Mixonomer.Fire.Extensions;
using Mixonomer.Playlist.Sort; using Mixonomer.Playlist.Sort;
@ -26,6 +28,8 @@ public class PlaylistGenerator
{ {
using var logScope = _logger.BeginScope(new Dictionary<string, string> { {"username", username}, {"playlist", playlistName} }); using var logScope = _logger.BeginScope(new Dictionary<string, string> { {"username", username}, {"playlist", playlistName} });
ArgumentException.ThrowIfNullOrWhiteSpace(username);
var user = await _userRepo.GetUser(username); var user = await _userRepo.GetUser(username);
var dbPlaylist = await _userRepo.GetPlaylists(user).FirstOrDefaultAsync(x => x.name == playlistName); var dbPlaylist = await _userRepo.GetPlaylists(user).FirstOrDefaultAsync(x => x.name == playlistName);
@ -45,14 +49,31 @@ public class PlaylistGenerator
var context = new PlaylistGeneratingContext var context = new PlaylistGeneratingContext
{ {
PartTracks = await GetPlatlistTracks(spotifyClient, partPlaylists).ToListAsync(), PartTracks = await GetPlaylistTracks(spotifyClient, partPlaylists).ToListAsync(),
LibraryTracks = await GetLibraryTracks(spotifyClient, dbPlaylist).ToListAsync() LibraryTracks = await GetLibraryTracks(spotifyClient, dbPlaylist).ToListAsync()
}; };
context = DoPlaylistTypeProcessing(context, user, dbPlaylist); context = DoPlaylistTypeProcessing(context, user, dbPlaylist);
var combinedTracks = CollapseContextToCommonTracks(context); var combinedTracks = CollapseContextToCommonTracks(context);
// var recommender = new SpotifyRecommender(spotifyClient);
// var recommendations = await recommender.GetRecommendations(dbPlaylist, combinedTracks);
//
// combinedTracks = combinedTracks.Concat(recommendations);
// combinedTracks = combinedTracks.DistinctBy(x => (x.TrackName, string.Join(':', x.ArtistNames.Order())));
// combinedTracks = combinedTracks.DistinctBy(x => x.TrackUri);
combinedTracks = combinedTracks.DistinctBy(x => (x.TrackName.ToLower(), string.Concat(x.ArtistNames.Order())));
combinedTracks = SortTracks(combinedTracks, dbPlaylist); combinedTracks = SortTracks(combinedTracks, dbPlaylist);
await ExecutePlaylist(spotifyClient, dbPlaylist, user, combinedTracks, parts);
await dbPlaylist.Reference.SetAsync(new
{
last_updated = DateTime.UtcNow
}, SetOptions.MergeAll);
} }
private async Task<IEnumerable<string>> GetFullPartList(User user, Fire.Playlist playlist) private async Task<IEnumerable<string>> GetFullPartList(User user, Fire.Playlist playlist)
@ -79,32 +100,34 @@ public class PlaylistGenerator
private IEnumerable<FullPlaylist> GetPartPlaylists(Fire.Playlist subjectPlaylist, IEnumerable<FullPlaylist> allPlaylists, IEnumerable<string> 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 playlist in allPlaylists)
foreach (var part in parts)
{ {
if (allPlaylistDict.TryGetValue(part, out var playlist)) foreach (var part in parts)
{ {
if (!subjectPlaylist.include_spotify_owned && if (playlist.Name?.Equals(part, StringComparison.Ordinal) ?? false)
(playlist.Owner?.DisplayName.Contains("spotify", StringComparison.InvariantCultureIgnoreCase) ?? false))
{ {
// skip if (!subjectPlaylist.include_spotify_owned &&
} (playlist.Owner?.DisplayName.Contains("spotify", StringComparison.InvariantCultureIgnoreCase) ??
else false))
{ {
yield return playlist; // skip
}
else
{
yield return playlist;
}
} }
} }
} }
} }
private async IAsyncEnumerable<PlaylistTrack<IPlayableItem>> GetPlatlistTracks(SpotifyClient client, IEnumerable<FullPlaylist> playlists) private async IAsyncEnumerable<PlaylistTrack<IPlayableItem>> GetPlaylistTracks(SpotifyClient client, IEnumerable<FullPlaylist> playlists)
{ {
foreach (var playlist in playlists) foreach (var playlist in playlists)
{ {
if (playlist.Tracks is { } tracks) if (playlist.Tracks is { } tracks)
{ {
foreach (var track in await client.PaginateAll(tracks)) foreach (var track in await client.PaginateAll(await client.Playlists.GetItems(playlist.Id)))
{ {
yield return track; yield return track;
} }
@ -128,11 +151,8 @@ public class PlaylistGenerator
return context; return context;
} }
protected virtual IEnumerable<CommonTrack> CollapseContextToCommonTracks(PlaylistGeneratingContext context) protected virtual IEnumerable<CommonTrack> CollapseContextToCommonTracks(PlaylistGeneratingContext context) =>
{ context.ToCommonTracks();
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) protected virtual IEnumerable<CommonTrack> SortTracks(IEnumerable<CommonTrack> tracks, Fire.Playlist playlist)
{ {
@ -145,4 +165,21 @@ public class PlaylistGenerator
return tracks.OrderByReleaseDate(); return tracks.OrderByReleaseDate();
} }
} }
protected virtual async Task ExecutePlaylist(SpotifyClient client, Fire.Playlist playlist, User user, IEnumerable<CommonTrack> tracks, IEnumerable<string> partList)
{
var chunks = tracks.Select(x => x.TrackUri).Chunk(100);
var playlistId = playlist.uri.UriToId();
if (chunks.FirstOrDefault() is { } chunk)
{
await client.Playlists.ReplaceItems(playlist.uri.UriToId(),
new PlaylistReplaceItemsRequest(chunk.ToList()));
}
foreach (var remainingChunk in chunks.Skip(1))
{
await client.Playlists.AddItems(playlistId, new PlaylistAddItemsRequest(remainingChunk));
}
}
} }

View File

@ -11,8 +11,11 @@ public static class SortExtensions
.ThenBy(x => x.TrackNumber); .ThenBy(x => x.TrackNumber);
public static IOrderedEnumerable<CommonTrack> OrderByReleaseDate(this IEnumerable<CommonTrack> input) => public static IOrderedEnumerable<CommonTrack> OrderByReleaseDate(this IEnumerable<CommonTrack> input) =>
input.OrderByArtistAlbumTrackNumber() input.OrderByDescending(x => x.ReleaseDate)
.ThenByDescending(x => x.ReleaseDate); .ThenBy(x => x.AlbumArtistNames.First())
.ThenBy(x => x.AlbumName)
.ThenBy(x => x.DiscNumber)
.ThenBy(x => x.TrackNumber);
public static IOrderedEnumerable<CommonTrack> Shuffle(this IEnumerable<CommonTrack> input) => public static IOrderedEnumerable<CommonTrack> Shuffle(this IEnumerable<CommonTrack> input) =>
input.OrderBy(x => _rng.Next()); input.OrderBy(x => _rng.Next());

View File

@ -38,7 +38,7 @@ public class SpotifyNetworkProvider
await WriteUserTokenUpdate(user, new await WriteUserTokenUpdate(user, new
{ {
access_token = refreshed.AccessToken, access_token = refreshed.AccessToken,
refresh_token = refreshed.RefreshToken, refresh_token = refreshed.RefreshToken ?? user.refresh_token,
last_refreshed = refreshed.CreatedAt, last_refreshed = refreshed.CreatedAt,
token_expiry = refreshed.ExpiresIn token_expiry = refreshed.ExpiresIn
}); });
@ -58,7 +58,7 @@ public class SpotifyNetworkProvider
await WriteUserTokenUpdate(user, new await WriteUserTokenUpdate(user, new
{ {
access_token = resp.AccessToken, access_token = resp.AccessToken,
refresh_token = resp.RefreshToken, refresh_token = resp.RefreshToken ?? user.refresh_token,
last_refreshed = resp.CreatedAt, last_refreshed = resp.CreatedAt,
token_expiry = resp.ExpiresIn token_expiry = resp.ExpiresIn
}); });

View File

@ -18,7 +18,10 @@ public class SpotifyRecommender: IRecommend
{ {
if (playlist.include_recommendations) if (playlist.include_recommendations)
{ {
var request = new RecommendationsRequest(); var request = new RecommendationsRequest()
{
Limit = playlist.recommendation_sample
};
var response = await _client.Browse.GetRecommendations(request); var response = await _client.Browse.GetRecommendations(request);