170 lines
8.6 KiB
C#
170 lines
8.6 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
||
|
||
namespace Music;
|
||
|
||
public class PlaybackController : BaseController
|
||
{
|
||
[HttpGet]
|
||
[Route("music/matches")]
|
||
async public Task<ActionResult> Matches(string id, string provider, string audio_provider, string playback_mode, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc, string query)
|
||
{
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
|
||
var result = string.IsNullOrWhiteSpace(query)
|
||
? await MusicResolver.GetMatchesAsync(track, audio_provider, playback_mode, profileId)
|
||
: await MusicResolver.GetMatchesByQueryAsync(track, audio_provider, query, playback_mode);
|
||
|
||
return ContentTo(MusicJson.Serialize(result));
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("music/match/select")]
|
||
async public Task<ActionResult> SelectMatch(string id, string provider, string audio_provider, string playback_mode, string match_id, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc, string query)
|
||
{
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
bool saved = await MusicResolver.SelectMatchAsync(track, audio_provider, match_id, playback_mode, profileId, query);
|
||
|
||
return ContentTo(MusicJson.Serialize(new
|
||
{
|
||
saved,
|
||
track_id = id,
|
||
audio_provider,
|
||
match_id
|
||
}));
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("music/match/reset")]
|
||
async public Task<ActionResult> ResetMatch(string id, string provider, string playback_mode, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc)
|
||
{
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
bool reset = track != null && await MusicSourceMatchService.DeleteAsync(track.id, playback_mode);
|
||
|
||
return ContentTo(MusicJson.Serialize(new
|
||
{
|
||
reset,
|
||
track_id = id
|
||
}));
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("music/play")]
|
||
async public Task<ActionResult> Play(string id, string provider, string audio_provider, string stream_mode, string playback_mode, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc)
|
||
{
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
var result = await MusicPlaybackService.ResolveTrackAsync(track, audio_provider, stream_mode, playback_mode, profileId, HttpContext.RequestAborted);
|
||
MusicPlaybackService.PrepareStreamSources(host, track, provider, audio_provider, stream_mode, playback_mode, result);
|
||
return ContentTo(MusicJson.Serialize(result));
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("music/stream")]
|
||
async public Task<ActionResult> Stream(string ticket, string id, string provider, string audio_provider, string quality, string stream_mode, string playback_mode, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc)
|
||
{
|
||
if (MusicStreamTicketService.TryGet(ticket, out var ticketSource))
|
||
{
|
||
// тикет жив, но upstream-ссылка внутри могла умереть (403/404/410/416 —
|
||
// googlevideo TTL/IP-привязка): не транслируем смерть клиенту, а
|
||
// падаем в пере-резолв по fallback-параметрам ниже
|
||
var relayed = await MusicStreamRelayService.RelayAsync(HttpContext, ticketSource, failOnUpstreamError: !string.IsNullOrWhiteSpace(id));
|
||
if (relayed != null)
|
||
return relayed;
|
||
}
|
||
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
// upstream-ссылка умерла (403/410/416 в relay) — кэш sources для этого
|
||
// трека может держать те же мёртвые URL: пере-резолв идёт с refreshSources
|
||
var result = await MusicPlaybackService.ResolveTrackAsync(track, audio_provider, stream_mode, playback_mode, profileId, HttpContext.RequestAborted, refreshSources: true);
|
||
|
||
if (track == null || result?.available != true || result.sources.Count == 0)
|
||
return StatusCode(404);
|
||
|
||
var source = MusicPlaybackService.SelectSource(result.sources, quality, stream_mode);
|
||
if (source == null || string.IsNullOrWhiteSpace(source.url))
|
||
return StatusCode(404);
|
||
|
||
return await MusicStreamRelayService.RelayAsync(HttpContext, source);
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("music/history/mark")]
|
||
async public Task<ActionResult> MarkHistory(string id, string provider, string title, string artist_name, string album_id, string album_title, int? duration_ms, string date, string isrc, bool count_play = false, long? played_ms = null, string image = null)
|
||
{
|
||
var track = await MusicPlaybackService.ResolveRequestTrackAsync(id, provider, title, artist_name, album_id, album_title, duration_ms, date, isrc);
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
|
||
if (track != null)
|
||
{
|
||
image = image?.Trim();
|
||
if (image?.StartsWith("//", StringComparison.Ordinal) == true)
|
||
image = "https:" + image;
|
||
|
||
if (track.images?.Any(i => !string.IsNullOrWhiteSpace(i?.url)) != true
|
||
&& !string.IsNullOrWhiteSpace(image) && image.Length <= 8192
|
||
&& Uri.TryCreate(image, UriKind.Absolute, out var imageUri)
|
||
&& (imageUri.Scheme == Uri.UriSchemeHttp || imageUri.Scheme == Uri.UriSchemeHttps))
|
||
{
|
||
// Client artwork belongs to this history entry, not the shared metadata cache.
|
||
track = MusicJson.Deserialize<MusicTrack>(MusicJson.Serialize(track));
|
||
track.images = new List<MusicImage> { new() { url = imageUri.AbsoluteUri } };
|
||
}
|
||
|
||
await MusicPlaybackHistoryService.SaveAsync(profileId, track);
|
||
|
||
// статистику инкрементим только по честному прослушиванию:
|
||
// mark дёргается и для обновления payload (смена источника),
|
||
// без флага это накручивало бы счётчики
|
||
if (count_play || played_ms > 0)
|
||
await MusicStatsService.RecordPlayAsync(profileId, track, count_play, played_ms, HttpContext.RequestAborted);
|
||
}
|
||
|
||
return ContentTo(MusicJson.Serialize(new
|
||
{
|
||
saved = track != null,
|
||
track_id = id
|
||
}));
|
||
}
|
||
|
||
[HttpPost]
|
||
[Route("music/history/remove")]
|
||
async public Task<ActionResult> RemoveHistory(string id)
|
||
{
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
bool removed = await MusicPlaybackHistoryService.RemoveAsync(profileId, id);
|
||
|
||
return ContentTo(MusicJson.Serialize(new
|
||
{
|
||
removed,
|
||
track_id = id
|
||
}));
|
||
}
|
||
|
||
[HttpGet]
|
||
[Route("music/playlist.m3u")]
|
||
async public Task<ActionResult> Playlist(string ids, string provider, string audio_provider, string stream_mode, string playback_mode, string playlist_strategy)
|
||
{
|
||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||
|
||
string playlist = await MusicPlaylistService.BuildAsync(new MusicPlaylistRequest
|
||
{
|
||
ids = ids,
|
||
provider = provider,
|
||
audio_provider = audio_provider,
|
||
stream_mode = stream_mode,
|
||
playback_mode = playback_mode,
|
||
playlist_strategy = playlist_strategy,
|
||
profile_id = profileId,
|
||
host = host,
|
||
uid = Request.Query["uid"].FirstOrDefault(),
|
||
account_email = Request.Query["account_email"].FirstOrDefault(),
|
||
cancellation_token = HttpContext.RequestAborted
|
||
});
|
||
|
||
return Content(playlist, "audio/x-mpegurl; charset=utf-8");
|
||
}
|
||
}
|