Skip to content

Commit

Permalink
Enable LovedTracks import via scheduled task
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseward committed Aug 2, 2020
1 parent 70ec987 commit 294ce1a
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 115 deletions.
6 changes: 4 additions & 2 deletions Jellyfin.Plugin.Lastfm/Api/LastfmApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,19 @@ public async Task<bool> UnloveTrack(Audio item, LastfmUser user)
return await LoveTrack(item, user, false);
}

public async Task<LovedTracksResponse> GetLovedTracks(LastfmUser user)
public async Task<LovedTracksResponse> GetLovedTracks(LastfmUser user, CancellationToken cancellationToken, int page)
{
var request = new GetLovedTracksRequest
{
User = user.Username,
ApiKey = Strings.Keys.LastfmApiKey,
Method = Strings.Methods.GetLovedTracks,
Limit = 1000, // {"error":6,"message":"limit param out of bounds (1-1000)"}
Page = page,
Secure = true
};

return await Get<GetLovedTracksRequest, LovedTracksResponse>(request);
return await Get<GetLovedTracksRequest, LovedTracksResponse>(request, cancellationToken);
}

public async Task<GetTracksResponse> GetTracks(LastfmUser user, MusicArtist artist, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
public class GetLovedTracksRequest : BaseRequest
{
public string User { get; set; }
public int Limit { get; set; }
public int Page { get; set; }

public override Dictionary<string, string> ToDictionary()
{
return new Dictionary<string, string>(base.ToDictionary())
{
{ "user", User }
{ "user", User },
{ "limit" , Limit.ToString() },
{ "page" , Page.ToString() }
};
}
}
Expand Down
27 changes: 25 additions & 2 deletions Jellyfin.Plugin.Lastfm/Models/Responses/LovedTracksResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[DataContract]
public class LovedTracksResponse : BaseResponse
{
[DataMember(Name="lovedtracks")]
[DataMember(Name = "lovedtracks")]
public LovedTracks LovedTracks { get; set; }

public bool HasLovedTracks()
Expand All @@ -20,5 +20,28 @@ public class LovedTracks
{
[DataMember(Name = "track")]
public List<LastfmLovedTrack> Tracks { get; set; }


[DataMember(Name = "@attr")]
public LovedTracksMeta Metadata { get; set; }
}


[DataContract]
public class LovedTracksMeta
{
[DataMember(Name = "totalPages")]
public int TotalPages { get; set; }

[DataMember(Name = "total")]
public int TotalTracks { get; set; }

[DataMember(Name = "page")]
public int Page { get; set; }

public bool IsLastPage()
{
return Page.Equals(TotalPages);
}
}
}
}
181 changes: 72 additions & 109 deletions Jellyfin.Plugin.Lastfm/ScheduledTasks/ImportLastfmData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,47 @@
using Utils;
using Microsoft.Extensions.Logging;

class ImportLastfmData : IScheduledTask
/// <summary>
/// Task that will sync each users LastFM loved songs with their local library.
/// </summary>
public class ImportLastfmData : IScheduledTask
{
private readonly IUserManager _userManager;
private readonly LastfmApiClient _apiClient;
private readonly IUserDataManager _userDataManager;
private ILibraryManager _libraryManager;
private readonly ILogger<ImportLastfmData> _logger;
private readonly LastfmApiClient _apiClient;

public ImportLastfmData(IHttpClient httpClient, IJsonSerializer jsonSerializer, IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, ILoggerFactory loggerFactory)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<ImportLastfmData>();

_apiClient = new LastfmApiClient(httpClient, jsonSerializer, _logger);
_apiClient = new LastfmApiClient(httpClient, jsonSerializer, loggerFactory.CreateLogger<ImportLastfmData>());
}

public string Name
{
get { return "Import Last.fm Data"; }
}
public string Name => "Import Last.fm Loved Tracks";

public string Category
{
get { return "Last.fm"; }
}
public string Category => "Last.fm";

public string Key
{
get { return "ImportLastfmData"; }
}
public string Key => "ImportLastfmData";

public string Description
{
get { return "Import play counts and favourite tracks for each user with Last.fm accounted configured"; }
}
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();

public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new TaskTriggerInfo[]
{
//new WeeklyTrigger { DayOfWeek = DayOfWeek.Sunday, TimeOfDay = TimeSpan.FromHours(3) }
};
}
public string Description => "Import favourite tracks for each user with Last.fm accounted configured";

/// <summary>
/// Gather users information and calls <see cref="SyncDataforUserByArtistBulk"/>
/// </summary>
/// <param name="cancellationToken"></param>
/// <param name="progress"></param>
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
//Get all users
var users = _userManager.Users.Where(u =>
{
var user = UserHelpers.GetUser(u);
return user != null && !String.IsNullOrWhiteSpace(user.SessionKey);
}).ToList();

Expand Down Expand Up @@ -99,141 +87,116 @@ public async Task Execute(CancellationToken cancellationToken, IProgress<double>
Plugin.Syncing = false;
}


private async Task SyncDataforUserByArtistBulk(User user, IProgress<double> progress, CancellationToken cancellationToken, double maxProgress, double progressOffset)
{
var artists = _libraryManager.GetArtists(new InternalItemsQuery(user))

LastfmUser lastFmUser = UserHelpers.GetUser(user);
if (!lastFmUser.Options.SyncFavourites)
{
return;
}

_logger.LogInformation("Syncing LastFM favourties for {0}", user.Username);

List<MusicArtist> artists = _libraryManager.GetArtists(new InternalItemsQuery(user))
.Items
.Select(i => i.Item1)
.Cast<MusicArtist>()
.ToList();

var lastFmUser = UserHelpers.GetUser(user);

var totalSongs = 0;
var matchedSongs = 0;
int matchedSongs = 0;

//Get loved tracks
var lovedTracksReponse = await _apiClient.GetLovedTracks(lastFmUser).ConfigureAwait(false);
var hasLovedTracks = lovedTracksReponse.HasLovedTracks();
// Fetch the user's loved tracks from LastFM API.
List<LastfmLovedTrack> lovedTracks = await GetLovedTracksLibrary(lastFmUser, progress, cancellationToken, maxProgress, progressOffset);

//Get entire library
var usersTracks = await GetUsersLibrary(lastFmUser, progress, cancellationToken, maxProgress, progressOffset);

if (usersTracks.Count == 0)
if (lovedTracks.Count == 0)
{
_logger.LogInformation("User {0} has no tracks in last.fm", user.Username);
_logger.LogInformation("User {0} has no loved tracks in last.fm", user.Username);
return;
}

//Group the library by artist
var userLibrary = usersTracks.GroupBy(t => t.Artist.MusicBrainzId).ToList();
// Group the list of loved tracks by artist
List<IGrouping<string, LastfmLovedTrack>> groupedLovedTracks = lovedTracks.GroupBy(t => t.Artist.MusicBrainzId).ToList();

//Loop through each artist
foreach (var artist in artists)
// Iterate over each artist in user's library
// iterate over each song by artist
// for each song, compare against the list of song/track in the lastfm loved track list
foreach (MusicArtist artist in artists)
{
cancellationToken.ThrowIfCancellationRequested();

//Get all the tracks by the current artist
var artistMBid = Helpers.GetMusicBrainzArtistId(artist);

string artistMBid = Helpers.GetMusicBrainzArtistId(artist);
if (artistMBid == null)
continue;

//Get the tracks from lastfm for the current artist
var artistTracks = userLibrary.FirstOrDefault(t => t.Key.Equals(artistMBid));

if (artistTracks == null || !artistTracks.Any())
if (groupedLovedTracks.FirstOrDefault(t => t.Key.Equals(artistMBid)) == null || !groupedLovedTracks.FirstOrDefault(t => t.Key.Equals(artistMBid)).Any())
{
_logger.LogInformation("{0} has no tracks in last.fm library for {1}", user.Username, artist.Name);
continue;
}

var artistTracksList = artistTracks.ToList();

_logger.LogInformation("Found {0} tracks in last.fm library for {1}", artistTracksList.Count, artist.Name);

//Loop through each song
foreach (var song in artist.GetRecursiveChildren().OfType<Audio>())
_logger.LogDebug("Found {0} LastFM lovedtracks for {1}",
groupedLovedTracks.FirstOrDefault(t => t.Key.Equals(artistMBid)).ToList().Count,
artist.Name);
// Loop through each song
foreach (Audio song in artist.GetTaggedItems(new InternalItemsQuery(user)
{
totalSongs++;

var matchedSong = Helpers.FindMatchedLastfmSong(artistTracksList, song);
IncludeItemTypes = new[] { "Audio" },
EnableTotalRecordCount = false
}).OfType<Audio>().ToList())
{
LastfmLovedTrack matchedSong = Helpers.FindMatchedLastfmSong(groupedLovedTracks.FirstOrDefault(t => t.Key.Equals(artistMBid)).ToList(), song);

if (matchedSong == null)
continue;

//We have found a match
// We have found a match
matchedSongs++;

_logger.LogDebug("Found match for {0} = {1}", song.Name, matchedSong.Name);

var userData = _userDataManager.GetUserData(user, song);

//Check if its a favourite track
if (hasLovedTracks && lastFmUser.Options.SyncFavourites)
{
//Use MBID if set otherwise match on song name
var favourited = lovedTracksReponse.LovedTracks.Tracks.Any(
t => String.IsNullOrWhiteSpace(t.MusicBrainzId)
? StringHelper.IsLike(t.Name, matchedSong.Name)
: t.MusicBrainzId.Equals(matchedSong.MusicBrainzId)
);

userData.IsFavorite = favourited;

_logger.LogDebug("{0} Favourite: {1}", song.Name, favourited);
}

//Update the play count
if (matchedSong.PlayCount > 0)
{
userData.Played = true;
userData.PlayCount = Math.Max(userData.PlayCount, matchedSong.PlayCount);
}
else
{
userData.Played = false;
userData.PlayCount = 0;
userData.LastPlayedDate = null;
}

_userDataManager.SaveUserData(userData.UserId, song, userData, UserDataSaveReason.UpdateUserRating, cancellationToken);
userData.IsFavorite = true;
_userDataManager.SaveUserData(user, song, userData, UserDataSaveReason.UpdateUserRating, cancellationToken);
_logger.LogDebug("Found library match for {0} = {1}", song.Name, matchedSong.Name);
}
}

//The percentage might not actually be correct but I'm pretty tired and don't want to think about it
_logger.LogInformation("Finished import Last.fm library for {0}. Local Songs: {1} | Last.fm Songs: {2} | Matched Songs: {3} | {4}% match rate",
user.Username, totalSongs, usersTracks.Count, matchedSongs, Math.Round(((double)matchedSongs / Math.Min(usersTracks.Count, totalSongs)) * 100));
_logger.LogInformation("Finished Last.fm lovedTracks sync for {0}. Matched Songs: {2}",user.Username, matchedSongs);
}

private async Task<List<LastfmTrack>> GetUsersLibrary(LastfmUser lastfmUser, IProgress<double> progress, CancellationToken cancellationToken, double maxProgress, double progressOffset)
/// <summary>
/// Returns a list of a target user's loved tracks from the Last.FM API. See https://www.last.fm/api/show/user.getLovedTracks
/// </summary>
/// <param name="lastfmUser"></param>
/// <param name="progress"></param>
/// <param name="cancellationToken"></param>
/// <param name="maxProgress"></param>
/// <param name="progressOffset"></param>
private async Task<List<LastfmLovedTrack>> GetLovedTracksLibrary(LastfmUser lastfmUser, IProgress<double> progress, CancellationToken cancellationToken, double maxProgress, double progressOffset)
{
var tracks = new List<LastfmTrack>();
var page = 1; //Page 0 = 1
var tracks = new List<LastfmLovedTrack>();
int page = 1;
bool moreTracks;

do
{
cancellationToken.ThrowIfCancellationRequested();

var response = await _apiClient.GetTracks(lastfmUser, cancellationToken, page++).ConfigureAwait(false);
var response = await _apiClient.GetLovedTracks(lastfmUser, cancellationToken, page++).ConfigureAwait(false);

if (response == null || !response.HasTracks())
if (response == null || !response.HasLovedTracks())
break;

tracks.AddRange(response.Tracks.Tracks);
tracks.AddRange(response.LovedTracks.Tracks);

moreTracks = !response.Tracks.Metadata.IsLastPage();
moreTracks = !response.LovedTracks.Metadata.IsLastPage();

//Only report progress in download because it will be 90% of the time taken
var currentProgress = ((double)response.Tracks.Metadata.Page / response.Tracks.Metadata.TotalPages) * (maxProgress - progressOffset) + progressOffset;
// Only report progress in download because it will be 90% of the time taken
var currentProgress = ((double)response.LovedTracks.Metadata.Page / response.LovedTracks.Metadata.TotalPages) * (maxProgress - progressOffset) + progressOffset;

_logger.LogDebug("Progress: " + currentProgress * 100);

progress.Report(currentProgress * 100);
} while (moreTracks);

_logger.LogInformation("Retrieved {0} lovedTracks from LastFM for user {1}", tracks.Count(), lastfmUser.Username);
return tracks;
}
}
Expand Down
2 changes: 1 addition & 1 deletion Jellyfin.Plugin.Lastfm/Utils/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public static string GetMusicBrainzArtistId(MusicArtist artist)
return null;
}

public static LastfmTrack FindMatchedLastfmSong(List<LastfmTrack> tracks, Audio song)
public static LastfmLovedTrack FindMatchedLastfmSong(List<LastfmLovedTrack> tracks, Audio song)
{
return tracks.FirstOrDefault(lastfmTrack => StringHelper.IsLike(song.Name, lastfmTrack.Name));
}
Expand Down

0 comments on commit 294ce1a

Please sign in to comment.