Files
lampac/mods/Music/Services/Playback/MusicPlaybackHistoryService.cs
T

198 lines
7.5 KiB
C#

using Microsoft.Data.Sqlite;
namespace Music;
public static class MusicPlaybackHistoryService
{
public static async Task SaveAsync(string profileId, MusicTrack track, CancellationToken cancellationToken = default)
{
profileId = NormalizeProfileId(profileId);
if (track == null || string.IsNullOrWhiteSpace(track.id) || string.IsNullOrWhiteSpace(profileId))
return;
var semaphore = new SemaphorManager(MusicContext.semaphoreKey, TimeSpan.FromSeconds(20));
try
{
if (!await semaphore.WaitAsync())
return;
await using var connection = new SqliteConnection(MusicContext.ConnectionString);
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO playback_history (profile_id, track_id, payload, updated)
VALUES ($profile_id, $track_id, $payload, $updated)
ON CONFLICT(profile_id, track_id) DO UPDATE SET
payload = excluded.payload,
updated = excluded.updated;
""";
command.Parameters.AddWithValue("$profile_id", profileId);
command.Parameters.AddWithValue("$track_id", track.id);
command.Parameters.AddWithValue("$payload", MusicJson.Serialize(track));
command.Parameters.AddWithValue("$updated", DateTime.UtcNow);
await command.ExecuteNonQueryAsync(cancellationToken);
}
finally
{
semaphore.Release();
}
}
public static async Task<List<MusicRecentlyPlayedItem>> GetRecentAsync(string profileId, int limit = 12, CancellationToken cancellationToken = default)
{
profileId = NormalizeProfileId(profileId);
var items = new List<MusicRecentlyPlayedItem>();
if (string.IsNullOrWhiteSpace(profileId))
return items;
await using var connection = new SqliteConnection(MusicContext.ConnectionString);
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = """
SELECT payload, updated
FROM playback_history
WHERE profile_id = $profile_id
ORDER BY updated DESC
LIMIT $limit;
""";
command.Parameters.AddWithValue("$profile_id", profileId);
command.Parameters.AddWithValue("$limit", Math.Max(1, limit));
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var payload = reader.IsDBNull(0) ? null : reader.GetString(0);
if (string.IsNullOrWhiteSpace(payload))
continue;
var track = MusicJson.Deserialize<MusicTrack>(payload);
if (track == null)
continue;
items.Add(new MusicRecentlyPlayedItem
{
track = track,
played_at = reader.IsDBNull(1) ? DateTime.UtcNow : reader.GetDateTime(1)
});
}
return items;
}
public static async Task<MusicTrack> GetTrackAsync(string profileId, string trackId, CancellationToken cancellationToken = default)
{
profileId = NormalizeProfileId(profileId);
trackId = string.IsNullOrWhiteSpace(trackId) ? null : trackId.Trim();
if (string.IsNullOrWhiteSpace(profileId) || string.IsNullOrWhiteSpace(trackId))
return null;
await using var connection = new SqliteConnection(MusicContext.ConnectionString);
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = """
SELECT payload
FROM playback_history
WHERE profile_id = $profile_id
AND track_id = $track_id
LIMIT 1;
""";
command.Parameters.AddWithValue("$profile_id", profileId);
command.Parameters.AddWithValue("$track_id", trackId);
var payload = await command.ExecuteScalarAsync(cancellationToken) as string;
return string.IsNullOrWhiteSpace(payload) ? null : MusicJson.Deserialize<MusicTrack>(payload);
}
public static async Task<Dictionary<string, MusicTrack>> GetTracksAsync(string profileId, IEnumerable<string> trackIds, CancellationToken cancellationToken = default)
{
profileId = NormalizeProfileId(profileId);
if (string.IsNullOrWhiteSpace(profileId) || trackIds == null)
return new Dictionary<string, MusicTrack>(StringComparer.Ordinal);
var normalizedIds = trackIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
if (normalizedIds.Count == 0)
return new Dictionary<string, MusicTrack>(StringComparer.Ordinal);
await using var connection = new SqliteConnection(MusicContext.ConnectionString);
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
var placeholders = new List<string>(normalizedIds.Count);
for (int i = 0; i < normalizedIds.Count; i++)
{
string parameterName = $"$track_id_{i}";
placeholders.Add(parameterName);
command.Parameters.AddWithValue(parameterName, normalizedIds[i]);
}
command.CommandText = $"""
SELECT track_id, payload
FROM playback_history
WHERE profile_id = $profile_id
AND track_id IN ({string.Join(", ", placeholders)});
""";
command.Parameters.AddWithValue("$profile_id", profileId);
var result = new Dictionary<string, MusicTrack>(normalizedIds.Count, StringComparer.Ordinal);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var trackId = reader.IsDBNull(0) ? null : reader.GetString(0);
var payload = reader.IsDBNull(1) ? null : reader.GetString(1);
if (string.IsNullOrWhiteSpace(trackId) || string.IsNullOrWhiteSpace(payload))
continue;
var track = MusicJson.Deserialize<MusicTrack>(payload);
if (track != null)
result[trackId] = track;
}
return result;
}
public static async Task<bool> RemoveAsync(string profileId, string trackId, CancellationToken cancellationToken = default)
{
profileId = NormalizeProfileId(profileId);
trackId = string.IsNullOrWhiteSpace(trackId) ? null : trackId.Trim();
if (string.IsNullOrWhiteSpace(profileId) || string.IsNullOrWhiteSpace(trackId))
return false;
await using var connection = new SqliteConnection(MusicContext.ConnectionString);
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = """
DELETE FROM playback_history
WHERE profile_id = $profile_id
AND track_id = $track_id;
""";
command.Parameters.AddWithValue("$profile_id", profileId);
command.Parameters.AddWithValue("$track_id", trackId);
return await command.ExecuteNonQueryAsync(cancellationToken) > 0;
}
static string NormalizeProfileId(string profileId)
=> string.IsNullOrWhiteSpace(profileId) ? null : profileId.Trim().ToLowerInvariant();
}