Files
lampac/mods/Music/Controllers/ApiController.cs
T

296 lines
12 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text;
namespace Music;
public class ApiController : BaseController
{
[HttpGet]
[AllowAnonymous]
[Route("music.js")]
[Route("music/js/{token}")]
public ActionResult MusicJS(string token)
{
string plugin = FileCache.ReadAllText($"{ModInit.modpath}/plugin.js", "music.js", false)
.Replace("{localhost}", host)
.Replace("{client_debug_enabled}", ModInit.conf?.client_debug_enabled == true ? "true" : "false")
.Replace("{stats_clear_enabled}", ModInit.conf?.stats_clear_enabled == true ? "true" : "false")
.Replace("{daily_reset_enabled}", ModInit.conf?.daily_reset_enabled == true ? "true" : "false")
.Replace("{spotify_search_fallback_enabled}", ModInit.conf?.spotify_search_fallback_enabled == true ? "true" : "false");
// ETag считаем от ИТОГОВОГО текста: ответ зависит не только от plugin.js,
// но и от host и флагов конфига выше — хэш исходного файла дал бы ложный
// 304 после смены конфигурации и оставил бы клиенту старые значения.
// no-cache (не no-store): браузер хранит копию, но перед использованием
// перепроверяет её условным запросом — 304 без тела при неизменном файле.
string etag = $"\"{Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(plugin)))[..32].ToLowerInvariant()}\"";
Response.Headers["Cache-Control"] = "private, no-cache, must-revalidate";
Response.Headers["ETag"] = etag;
string ifNoneMatch = Request.Headers["If-None-Match"].FirstOrDefault();
if (!string.IsNullOrEmpty(ifNoneMatch))
{
foreach (var rawTag in ifNoneMatch.Split(','))
{
string tag = rawTag.Trim();
if (tag == "*" || tag == etag || tag == $"W/{etag}")
return new StatusCodeResult(304);
}
}
return Content(plugin, "application/javascript; charset=utf-8");
}
[HttpPost]
[AllowAnonymous]
[Route("music/clientlog")]
public ActionResult ClientLog(string event_name, string state, string track_id, string message)
{
static string cut(string value, int max)
{
if (string.IsNullOrWhiteSpace(value))
return "";
value = value.Replace('\r', ' ').Replace('\n', ' ').Trim();
return value.Length <= max ? value : value.Substring(0, max);
}
if (ModInit.conf?.client_debug_enabled != true)
return ContentTo(MusicJson.Serialize(new { ok = true, logged = false }));
Console.WriteLine($"[MusicClient] event={cut(event_name, 64)} track={cut(track_id, 96)} state={cut(state, 6000)} message={cut(message, 400)}");
return ContentTo(MusicJson.Serialize(new { ok = true, logged = true }));
}
[HttpGet]
[Route("music")]
[Route("music/home")]
async public Task<ActionResult> Home(int? daily_salt)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicCatalogService.GetHomeAsync(profileId, daily_salt ?? 0);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpGet]
[Route("music/playlists")]
async public Task<ActionResult> Playlists()
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicUserPlaylistService.ListAsync(profileId, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(new { available = true, playlists = result }));
}
[HttpGet]
[Route("music/playlists/tracks")]
async public Task<ActionResult> PlaylistTracks(string id)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var tracks = await MusicUserPlaylistService.GetTracksAsync(profileId, id, HttpContext.RequestAborted);
tracks = MusicImageProxyService.Apply(this, tracks);
return ContentTo(MusicJson.Serialize(new { available = true, playlist_id = id, tracks }));
}
[HttpPost]
[Route("music/playlists/create")]
async public Task<ActionResult> PlaylistCreate(string title)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
string playlistId = await MusicUserPlaylistService.CreateAsync(profileId, title, HttpContext.RequestAborted);
return ContentTo(MusicJson.Serialize(new { created = playlistId != null, playlist_id = playlistId, title }));
}
[HttpPost]
[Route("music/playlists/delete")]
async public Task<ActionResult> PlaylistDelete(string id)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
bool removed = await MusicUserPlaylistService.DeleteAsync(profileId, id, HttpContext.RequestAborted);
return ContentTo(MusicJson.Serialize(new { removed, playlist_id = id }));
}
[HttpPost]
[Route("music/playlists/import")]
[Route("music/playlists/import/soundcloud")] // legacy-роут для старого клиентского JS
async public Task<ActionResult> PlaylistImport(string url)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicUserPlaylistService.ImportAsync(profileId, url, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpPost]
[Route("music/playlists/sync")]
async public Task<ActionResult> PlaylistSync(string id)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicUserPlaylistService.SyncAsync(profileId, id, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpPost]
[Route("music/playlists/track/add")]
async public Task<ActionResult> PlaylistTrackAdd(string id, string track)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
MusicTrack parsed = null;
try
{
parsed = string.IsNullOrWhiteSpace(track) ? null : MusicJson.Deserialize<MusicTrack>(track);
}
catch
{
}
bool saved = await MusicUserPlaylistService.AddTrackAsync(profileId, id, parsed, HttpContext.RequestAborted);
var playlist = saved ? await MusicUserPlaylistService.GetSummaryAsync(profileId, id, HttpContext.RequestAborted) : null;
if (playlist != null)
playlist = MusicImageProxyService.Apply(this, playlist);
return ContentTo(MusicJson.Serialize(new { saved, playlist_id = id, playlist }));
}
[HttpPost]
[Route("music/playlists/track/remove")]
async public Task<ActionResult> PlaylistTrackRemove(string id, string track_id)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
bool removed = await MusicUserPlaylistService.RemoveTrackAsync(profileId, id, track_id, HttpContext.RequestAborted);
var playlist = removed ? await MusicUserPlaylistService.GetSummaryAsync(profileId, id, HttpContext.RequestAborted) : null;
if (playlist != null)
playlist = MusicImageProxyService.Apply(this, playlist);
return ContentTo(MusicJson.Serialize(new { removed, playlist_id = id, track_id, playlist }));
}
[HttpPost]
[Route("music/playlists/track/move")]
async public Task<ActionResult> PlaylistTrackMove(string id, string track_id, int position)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
bool moved = await MusicUserPlaylistService.MoveTrackAsync(profileId, id, track_id, position, HttpContext.RequestAborted);
var playlist = moved ? await MusicUserPlaylistService.GetSummaryAsync(profileId, id, HttpContext.RequestAborted) : null;
if (playlist != null)
playlist = MusicImageProxyService.Apply(this, playlist);
return ContentTo(MusicJson.Serialize(new { moved, playlist_id = id, track_id, position, playlist }));
}
[HttpGet]
[Route("music/daily")]
async public Task<ActionResult> DailyMix(int day, int? salt)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicDailyMixService.GetMixAsync(profileId, day, salt ?? 0, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpGet]
[Route("music/stats/top")]
async public Task<ActionResult> StatsTop(int? limit)
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicStatsService.GetTopAsync(profileId, limit ?? 30, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpPost]
[Route("music/stats/clear")]
async public Task<ActionResult> StatsClear()
{
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
int removed = await MusicStatsService.ClearAsync(profileId, HttpContext.RequestAborted);
return ContentTo(MusicJson.Serialize(new { cleared = true, removed }));
}
[HttpPost]
[Route("music/radio")]
async public Task<ActionResult> Radio(string seeds, string exclude, int? limit)
{
// защитные капы: сырой JSON режем ДО десериализации (клиент шлёт
// exclude всей очереди ≤500 компакт-треков ≈ 150KB — 400KB с запасом),
// распарсенные списки — до разумных размеров
const int maxRawJsonLength = 400_000;
const int maxSeeds = 10;
const int maxExclude = 1000;
static List<MusicTrack> parseTracks(string value, int maxItems)
{
if (string.IsNullOrWhiteSpace(value) || value.Length > maxRawJsonLength)
return new List<MusicTrack>();
try
{
var parsed = MusicJson.Deserialize<List<MusicTrack>>(value) ?? new List<MusicTrack>();
return parsed.Count > maxItems ? parsed.Take(maxItems).ToList() : parsed;
}
catch
{
return new List<MusicTrack>();
}
}
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
var result = await MusicRadioService.GetAsync(profileId, new MusicRadioRequest
{
seeds = parseTracks(seeds, maxSeeds),
exclude = parseTracks(exclude, maxExclude)
}, limit ?? 20, HttpContext.RequestAborted);
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
[HttpGet]
[Route("music/lyrics")]
async public Task<ActionResult> Lyrics(string title, string artist_name, string album_title, int? duration_ms, string youtube_id)
{
var result = await MusicLyricsService.GetAsync(title, artist_name, album_title, duration_ms, youtube_id, HttpContext.RequestAborted);
return ContentTo(MusicJson.Serialize(result));
}
[HttpGet]
[Route("music/providers")]
public ActionResult Providers()
{
var result = new
{
metadata = MusicProviderRegistry.DescribeMetadata(),
audio = MusicProviderRegistry.DescribeAudio(),
auth = MusicProviderRegistry.DescribeAuth()
};
return ContentTo(MusicJson.Serialize(result));
}
[HttpGet]
[Route("music/section")]
async public Task<ActionResult> Section(string id)
{
var result = await MusicCatalogService.GetBrowseSectionAsync(id);
if (result == null)
return ContentTo(MusicJson.Serialize(new { available = false, message = "Section not found." }));
result = MusicImageProxyService.Apply(this, result);
return ContentTo(MusicJson.Serialize(result));
}
}