216 lines
8.8 KiB
C#
216 lines
8.8 KiB
C#
using System.Text;
|
|
|
|
namespace Music;
|
|
|
|
public static class MusicPlaybackService
|
|
{
|
|
public static async Task<MusicTrack> ResolveRequestTrackAsync(string id, string provider, string title, string artistName, string albumId, string albumTitle, int? durationMs, string date, string isrc)
|
|
{
|
|
string normalizedIsrc = MusicIsrc.Normalize(isrc);
|
|
MusicTrack track = null;
|
|
|
|
// Spotify search отдаёт точный track/album id, но без duration/isrc.
|
|
// Достаём трек из его родного queryAlbum (кэшируется каталогом), вместо
|
|
// заведомо бесполезного поиска spotify:track:* через MusicBrainz.
|
|
if (!string.IsNullOrWhiteSpace(id)
|
|
&& id.StartsWith("spotify:track:", StringComparison.OrdinalIgnoreCase)
|
|
&& !durationMs.HasValue
|
|
&& !string.IsNullOrWhiteSpace(albumId)
|
|
&& albumId.StartsWith("spotify:album:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var albumTask = MusicCatalogService.GetAlbumAsync(albumId, SpotifySupport.ProviderId);
|
|
if (await Task.WhenAny(albumTask, Task.Delay(2500)) == albumTask)
|
|
{
|
|
var album = await albumTask;
|
|
track = album?.tracks?.FirstOrDefault(i => string.Equals(i?.id, id, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
track = await MusicCatalogService.GetTrackAsync(id, provider);
|
|
}
|
|
|
|
if (track != null)
|
|
{
|
|
track = CopyTrack(track);
|
|
track.isrc = MusicIsrc.Normalize(track.isrc) ?? normalizedIsrc;
|
|
return track;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(title))
|
|
return null;
|
|
|
|
return new MusicTrack
|
|
{
|
|
id = string.IsNullOrWhiteSpace(id) ? BuildInlineTrackId(title, artistName, albumTitle, durationMs, normalizedIsrc) : id,
|
|
title = title,
|
|
artist_name = artistName,
|
|
album_id = albumId,
|
|
album_title = albumTitle,
|
|
isrc = normalizedIsrc,
|
|
duration_ms = durationMs,
|
|
date = date
|
|
};
|
|
}
|
|
|
|
static MusicTrack CopyTrack(MusicTrack track)
|
|
{
|
|
return new MusicTrack
|
|
{
|
|
id = track.id,
|
|
title = track.title,
|
|
artist_id = track.artist_id,
|
|
artist_name = track.artist_name,
|
|
artists = track.artists?.ToList() ?? new List<string>(),
|
|
album_id = track.album_id,
|
|
album_title = track.album_title,
|
|
isrc = track.isrc,
|
|
duration_ms = track.duration_ms,
|
|
track_number = track.track_number,
|
|
disc_number = track.disc_number,
|
|
date = track.date,
|
|
search_score = track.search_score,
|
|
images = track.images?.ToList() ?? new List<MusicImage>(),
|
|
provider_refs = track.provider_refs?.ToList() ?? new List<MusicProviderRef>(),
|
|
auto_radio = track.auto_radio
|
|
};
|
|
}
|
|
|
|
public static async Task<MusicPlayResponse> ResolveTrackAsync(MusicTrack track, string audioProvider, string streamMode, string playbackMode, string profileId, CancellationToken cancellationToken = default, bool refreshSources = false)
|
|
{
|
|
var result = await MusicResolver.ResolveTrackAsync(track, audioProvider, playbackMode, profileId, cancellationToken, refreshSources);
|
|
if (result?.sources != null && result.sources.Count > 0)
|
|
result.sources = MusicStreamModeService.Order(result.sources, streamMode);
|
|
|
|
return result;
|
|
}
|
|
|
|
public static async Task<MusicPlayResponse> ResolvePreferredTrackAsync(MusicTrack track, string audioProvider, string streamMode, string playbackMode, string profileId, CancellationToken cancellationToken = default)
|
|
{
|
|
var result = await MusicResolver.ResolvePreferredTrackAsync(track, audioProvider, playbackMode, profileId, cancellationToken);
|
|
if (result?.sources != null && result.sources.Count > 0)
|
|
result.sources = MusicStreamModeService.Order(result.sources, streamMode);
|
|
|
|
return result;
|
|
}
|
|
|
|
public static void PrepareStreamSources(string host, MusicTrack track, string provider, string audioProvider, string streamMode, string playbackMode, MusicPlayResponse result)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(host) || track == null || result?.sources == null || result.sources.Count == 0)
|
|
return;
|
|
|
|
string resolvedProvider = ResolveTrackProvider(track, provider);
|
|
var fallbackParams = BuildStreamFallbackParams(track, resolvedProvider, audioProvider, streamMode, playbackMode, null, null);
|
|
|
|
foreach (var source in result.sources)
|
|
{
|
|
if (source == null || string.IsNullOrWhiteSpace(source.url))
|
|
continue;
|
|
|
|
source.external_url = source.url;
|
|
string ticket = MusicStreamTicketService.Create(source);
|
|
if (!string.IsNullOrWhiteSpace(ticket))
|
|
{
|
|
var query = new List<string> { $"ticket={Uri.EscapeDataString(ticket)}" };
|
|
query.AddRange(fallbackParams);
|
|
if (!string.IsNullOrWhiteSpace(source.quality))
|
|
query.Add($"quality={Uri.EscapeDataString(source.quality)}");
|
|
|
|
source.url = $"{host}/music/stream?{string.Join("&", query)}";
|
|
}
|
|
|
|
source.headers = new Dictionary<string, string>();
|
|
}
|
|
}
|
|
|
|
public static MusicPlaybackSource SelectSource(List<MusicPlaybackSource> sources, string quality, string streamMode)
|
|
{
|
|
if (sources == null || sources.Count == 0)
|
|
return null;
|
|
|
|
if (!string.IsNullOrWhiteSpace(quality))
|
|
{
|
|
var exact = sources.FirstOrDefault(i => string.Equals(i.quality, quality, StringComparison.OrdinalIgnoreCase));
|
|
if (exact != null)
|
|
return exact;
|
|
}
|
|
|
|
return MusicStreamModeService.Order(sources, streamMode).FirstOrDefault();
|
|
}
|
|
|
|
public static string ResolveTrackProvider(MusicTrack track, string fallbackProvider)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(fallbackProvider))
|
|
return fallbackProvider;
|
|
|
|
if (track?.provider_refs == null || track.provider_refs.Count == 0)
|
|
return null;
|
|
|
|
foreach (var providerRef in track.provider_refs)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(providerRef?.provider))
|
|
return providerRef.provider.Trim();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static List<string> BuildStreamFallbackParams(MusicTrack track, string provider, string audioProvider, string streamMode, string playbackMode, string uid, string accountEmail)
|
|
{
|
|
var url = new List<string>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.id))
|
|
url.Add($"id={Uri.EscapeDataString(track.id)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(provider))
|
|
url.Add($"provider={Uri.EscapeDataString(provider)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(audioProvider))
|
|
url.Add($"audio_provider={Uri.EscapeDataString(audioProvider)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(streamMode))
|
|
url.Add($"stream_mode={Uri.EscapeDataString(streamMode)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(playbackMode))
|
|
url.Add($"playback_mode={Uri.EscapeDataString(playbackMode)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.title))
|
|
url.Add($"title={Uri.EscapeDataString(track.title)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.artist_name))
|
|
url.Add($"artist_name={Uri.EscapeDataString(track.artist_name)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.album_id))
|
|
url.Add($"album_id={Uri.EscapeDataString(track.album_id)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.album_title))
|
|
url.Add($"album_title={Uri.EscapeDataString(track.album_title)}");
|
|
|
|
string isrc = MusicIsrc.Normalize(track?.isrc);
|
|
if (isrc != null)
|
|
url.Add($"isrc={Uri.EscapeDataString(isrc)}");
|
|
|
|
if (track?.duration_ms.HasValue == true)
|
|
url.Add($"duration_ms={track.duration_ms.Value}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(track?.date))
|
|
url.Add($"date={Uri.EscapeDataString(track.date)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(uid))
|
|
url.Add($"uid={Uri.EscapeDataString(uid)}");
|
|
|
|
if (!string.IsNullOrWhiteSpace(accountEmail))
|
|
url.Add($"account_email={Uri.EscapeDataString(accountEmail)}");
|
|
|
|
return url;
|
|
}
|
|
|
|
static string BuildInlineTrackId(string title, string artistName, string albumTitle, int? durationMs, string isrc)
|
|
{
|
|
string key = $"{artistName}::{title}::{albumTitle}::{durationMs}::{isrc}";
|
|
using var md5 = System.Security.Cryptography.MD5.Create();
|
|
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(key));
|
|
return "inline:" + Convert.ToHexString(bytes).ToLowerInvariant();
|
|
}
|
|
}
|