Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Music;
|
||||
|
||||
public class AuthController : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("music/auth/state")]
|
||||
async public Task<ActionResult> State()
|
||||
{
|
||||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||||
var result = await MusicAuthService.GetStatesAsync(profileId);
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("music/auth/save")]
|
||||
async public Task<ActionResult> Save(string provider, string payload)
|
||||
{
|
||||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||||
bool result = await MusicAuthService.SaveAsync(provider, payload, profileId);
|
||||
return ContentTo(MusicJson.Serialize(new
|
||||
{
|
||||
provider,
|
||||
saved = result
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("music/auth/logout")]
|
||||
async public Task<ActionResult> Logout(string provider)
|
||||
{
|
||||
string profileId = MusicProfileIdentity.Resolve(requestInfo, Request);
|
||||
await MusicAuthService.LogoutAsync(provider, profileId);
|
||||
return ContentTo(MusicJson.Serialize(new
|
||||
{
|
||||
provider,
|
||||
logged_out = true
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Music;
|
||||
|
||||
public class SearchController : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("music/search")]
|
||||
async public Task<ActionResult> Search(string q, string provider, bool full = false)
|
||||
{
|
||||
var result = await MusicCatalogService.SearchAsync(q, provider, full);
|
||||
result = MusicImageProxyService.Apply(this, result);
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("music/artist")]
|
||||
async public Task<ActionResult> Artist(string id, string provider)
|
||||
{
|
||||
var result = await MusicCatalogService.GetArtistAsync(id, provider);
|
||||
if (result == null)
|
||||
return ContentTo(MusicJson.Serialize(new { available = false, message = "Artist not found." }));
|
||||
|
||||
result = MusicImageProxyService.Apply(this, result);
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("music/artistsection")]
|
||||
async public Task<ActionResult> ArtistSection(string id, string provider, string page, string artist_name, int limit = 20)
|
||||
{
|
||||
var result = await MusicCatalogService.GetArtistSectionAsync(id, provider, page, limit, artist_name);
|
||||
if (result == null)
|
||||
return ContentTo(MusicJson.Serialize(new { available = false, message = "Artist section not found." }));
|
||||
|
||||
result = MusicImageProxyService.Apply(this, result);
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("music/artistimg")]
|
||||
async public Task<ActionResult> ArtistImage(string id, string name, string country, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ContentTo(MusicJson.Serialize(new { available = false, id, images = new List<MusicImage>() }));
|
||||
|
||||
var artist = new MusicArtist
|
||||
{
|
||||
id = id,
|
||||
name = name,
|
||||
country = country
|
||||
};
|
||||
|
||||
artist.images = await DiscogsArtistImageService.ResolveImagesAsync(artist, cancellationToken) ?? new List<MusicImage>();
|
||||
artist = MusicImageProxyService.Apply(this, artist);
|
||||
|
||||
return ContentTo(MusicJson.Serialize(new
|
||||
{
|
||||
available = artist.images.Count > 0,
|
||||
id = id,
|
||||
images = artist.images
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("music/album")]
|
||||
async public Task<ActionResult> Album(string id, string provider)
|
||||
{
|
||||
var result = await MusicCatalogService.GetAlbumAsync(id, provider);
|
||||
if (result == null)
|
||||
return ContentTo(MusicJson.Serialize(new { available = false, message = "Album not found." }));
|
||||
|
||||
result = MusicImageProxyService.Apply(this, result);
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("music/track")]
|
||||
async public Task<ActionResult> Track(string id, string provider)
|
||||
{
|
||||
var result = await MusicCatalogService.GetTrackAsync(id, provider);
|
||||
if (result == null)
|
||||
return ContentTo(MusicJson.Serialize(new { available = false, message = "Track not found." }));
|
||||
|
||||
return ContentTo(MusicJson.Serialize(result));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user