Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Music;
|
||||
|
||||
public sealed class MusicPlaylistRequest
|
||||
{
|
||||
public string ids { get; set; }
|
||||
public string provider { get; set; }
|
||||
public string audio_provider { get; set; }
|
||||
public string stream_mode { get; set; }
|
||||
public string playback_mode { get; set; }
|
||||
public string playlist_strategy { get; set; }
|
||||
public string profile_id { get; set; }
|
||||
public string host { get; set; }
|
||||
public string uid { get; set; }
|
||||
public string account_email { get; set; }
|
||||
public CancellationToken cancellation_token { get; set; }
|
||||
}
|
||||
|
||||
public static class MusicPlaylistService
|
||||
{
|
||||
public static async Task<string> BuildAsync(MusicPlaylistRequest request)
|
||||
{
|
||||
request ??= new MusicPlaylistRequest();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ids))
|
||||
return string.Empty;
|
||||
|
||||
// защитный кап: 500 = лимит окна снапшота очереди, длиннее клиент
|
||||
// не шлёт; всё сверх — обрезаем, а не отказываем (устойчивость)
|
||||
const int maxPlaylistIds = 500;
|
||||
const int maxTrackIdLength = 256;
|
||||
|
||||
var trackIds = request.ids
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id) && id.Length <= maxTrackIdLength)
|
||||
.Take(maxPlaylistIds)
|
||||
.ToArray();
|
||||
|
||||
if (trackIds.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
var tracks = await ResolvePlaylistTracksAsync(trackIds, request);
|
||||
|
||||
if (string.Equals(request.audio_provider, SefonSupport.ProviderId, StringComparison.OrdinalIgnoreCase))
|
||||
return await BuildResolvedExternalPlaylistAsync(trackIds, tracks, request);
|
||||
|
||||
if (string.Equals(request.audio_provider, SoundCloudSupport.AudioProviderId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(request.playlist_strategy, "resolved", StringComparison.OrdinalIgnoreCase))
|
||||
return await BuildResolvedExternalPlaylistAsync(trackIds, tracks, request);
|
||||
|
||||
return await BuildHybridSoundCloudExternalPlaylistAsync(trackIds, tracks, request);
|
||||
}
|
||||
|
||||
return BuildInternalPlaylist(trackIds, tracks, request);
|
||||
}
|
||||
|
||||
static async Task<MusicTrack[]> ResolvePlaylistTracksAsync(string[] trackIds, MusicPlaylistRequest request)
|
||||
{
|
||||
const int metadataResolveParallelism = 6;
|
||||
var tracks = new MusicTrack[trackIds.Length];
|
||||
var historyTracks = await MusicPlaybackHistoryService.GetTracksAsync(request.profile_id, trackIds, request.cancellation_token);
|
||||
|
||||
for (int i = 0; i < trackIds.Length; i++)
|
||||
{
|
||||
if (historyTracks.TryGetValue(trackIds[i], out var historyTrack))
|
||||
tracks[i] = historyTrack;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.provider))
|
||||
return tracks;
|
||||
|
||||
using var resolveGate = new SemaphoreSlim(metadataResolveParallelism);
|
||||
var resolveTasks = new List<Task>();
|
||||
|
||||
async Task ResolveTrackAtAsync(int index)
|
||||
{
|
||||
await resolveGate.WaitAsync(request.cancellation_token);
|
||||
|
||||
try
|
||||
{
|
||||
tracks[index] = await MusicCatalogService.GetTrackAsync(trackIds[index], request.provider, request.cancellation_token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
resolveGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < trackIds.Length; i++)
|
||||
{
|
||||
if (tracks[i] == null)
|
||||
resolveTasks.Add(ResolveTrackAtAsync(i));
|
||||
}
|
||||
|
||||
if (resolveTasks.Count > 0)
|
||||
await Task.WhenAll(resolveTasks);
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
static string BuildInternalPlaylist(string[] trackIds, MusicTrack[] tracks, MusicPlaylistRequest request)
|
||||
{
|
||||
var playlist = new StringBuilder();
|
||||
playlist.AppendLine("#EXTM3U");
|
||||
|
||||
for (int i = 0; i < trackIds.Length; i++)
|
||||
{
|
||||
var track = tracks[i];
|
||||
string id = trackIds[i];
|
||||
int position = i + 1;
|
||||
|
||||
string title = BuildTitle(track, position);
|
||||
int seconds = GetSeconds(track);
|
||||
|
||||
playlist.AppendLine($"#EXTINF:{seconds},{EscapePlaylistTitle(title)}");
|
||||
string playlistUrl = BuildPlaylistEntryUrl(track, id, request);
|
||||
if (!string.IsNullOrWhiteSpace(playlistUrl))
|
||||
playlist.AppendLine(playlistUrl);
|
||||
}
|
||||
|
||||
return playlist.ToString();
|
||||
}
|
||||
|
||||
static async Task<string> BuildHybridSoundCloudExternalPlaylistAsync(string[] trackIds, MusicTrack[] tracks, MusicPlaylistRequest request)
|
||||
{
|
||||
var playlist = new StringBuilder();
|
||||
playlist.AppendLine("#EXTM3U");
|
||||
|
||||
const int leadChunkSize = 6;
|
||||
int leadIndex = -1;
|
||||
var leadResolvedEntries = new Dictionary<int, PlaylistResolvedEntry>();
|
||||
|
||||
for (int offset = 0; offset < trackIds.Length && leadIndex < 0; offset += leadChunkSize)
|
||||
{
|
||||
int chunkCount = Math.Min(leadChunkSize, trackIds.Length - offset);
|
||||
var tasks = new Task<PlaylistResolvedEntry>[chunkCount];
|
||||
|
||||
for (int chunkOffset = 0; chunkOffset < chunkCount; chunkOffset++)
|
||||
{
|
||||
int index = offset + chunkOffset;
|
||||
var track = tracks[index];
|
||||
if (track == null)
|
||||
continue;
|
||||
|
||||
tasks[chunkOffset] = ResolvePreferredPlaylistEntryAsync(index, track, request);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.Where(task => task != null));
|
||||
|
||||
for (int chunkOffset = 0; chunkOffset < chunkCount; chunkOffset++)
|
||||
{
|
||||
int index = offset + chunkOffset;
|
||||
var task = tasks[chunkOffset];
|
||||
var entry = task?.Result;
|
||||
if (entry == null || string.IsNullOrWhiteSpace(entry.Url))
|
||||
continue;
|
||||
|
||||
leadResolvedEntries[index] = entry;
|
||||
if (leadIndex < 0)
|
||||
leadIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (leadIndex < 0)
|
||||
return playlist.ToString();
|
||||
|
||||
for (int i = leadIndex; i < trackIds.Length; i++)
|
||||
{
|
||||
var track = tracks[i];
|
||||
string trackId = trackIds[i];
|
||||
if (track == null)
|
||||
continue;
|
||||
|
||||
if (leadResolvedEntries.TryGetValue(i, out var resolvedEntry) && !string.IsNullOrWhiteSpace(resolvedEntry.Url))
|
||||
{
|
||||
playlist.AppendLine($"#EXTINF:{resolvedEntry.Seconds},{EscapePlaylistTitle(resolvedEntry.Title)}");
|
||||
playlist.AppendLine(resolvedEntry.Url);
|
||||
continue;
|
||||
}
|
||||
|
||||
string playlistUrl = BuildPlaylistEntryUrl(track, trackId, request);
|
||||
if (string.IsNullOrWhiteSpace(playlistUrl))
|
||||
continue;
|
||||
|
||||
playlist.AppendLine($"#EXTINF:{GetSeconds(track)},{EscapePlaylistTitle(BuildTitle(track, i + 1))}");
|
||||
playlist.AppendLine(playlistUrl);
|
||||
}
|
||||
|
||||
return playlist.ToString();
|
||||
}
|
||||
|
||||
static async Task<string> BuildResolvedExternalPlaylistAsync(string[] trackIds, MusicTrack[] tracks, MusicPlaylistRequest request)
|
||||
{
|
||||
var entries = new PlaylistResolvedEntry[trackIds.Length];
|
||||
using var concurrency = new SemaphoreSlim(8);
|
||||
|
||||
var tasks = trackIds
|
||||
.Select((trackId, index) => ResolveResolvedPlaylistEntryAsync(index, tracks[index], request, entries, concurrency))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
var playlist = new StringBuilder();
|
||||
playlist.AppendLine("#EXTM3U");
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry == null || string.IsNullOrWhiteSpace(entry.Url))
|
||||
continue;
|
||||
|
||||
playlist.AppendLine($"#EXTINF:{entry.Seconds},{EscapePlaylistTitle(entry.Title)}");
|
||||
playlist.AppendLine(entry.Url);
|
||||
}
|
||||
|
||||
return playlist.ToString();
|
||||
}
|
||||
|
||||
static async Task ResolveResolvedPlaylistEntryAsync(int index, MusicTrack track, MusicPlaylistRequest request, PlaylistResolvedEntry[] entries, SemaphoreSlim concurrency)
|
||||
{
|
||||
await concurrency.WaitAsync(request.cancellation_token);
|
||||
try
|
||||
{
|
||||
if (track == null)
|
||||
return;
|
||||
|
||||
var entry = await ResolvePreferredPlaylistEntryAsync(index, track, request);
|
||||
if (entry != null)
|
||||
entries[index] = entry;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
concurrency.Release();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<PlaylistResolvedEntry> ResolvePreferredPlaylistEntryAsync(int index, MusicTrack track, MusicPlaylistRequest request)
|
||||
{
|
||||
if (track == null)
|
||||
return null;
|
||||
|
||||
var result = await MusicPlaybackService.ResolvePreferredTrackAsync(track, request.audio_provider, request.stream_mode, request.playback_mode, request.profile_id, request.cancellation_token);
|
||||
if (result?.available != true || result.sources == null || result.sources.Count == 0)
|
||||
return null;
|
||||
|
||||
var source = MusicPlaybackService.SelectSource(result.sources, null, request.stream_mode);
|
||||
if (source == null || string.IsNullOrWhiteSpace(source.url))
|
||||
return null;
|
||||
|
||||
string url = source.url;
|
||||
string resolvedAudioProvider = string.IsNullOrWhiteSpace(source.provider_id) ? request.audio_provider : source.provider_id;
|
||||
|
||||
bool canUseDirectExternalUrl =
|
||||
(string.Equals(resolvedAudioProvider, SefonSupport.ProviderId, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(resolvedAudioProvider, SoundCloudSupport.AudioProviderId, StringComparison.OrdinalIgnoreCase))
|
||||
&& (source.headers == null || source.headers.Count == 0);
|
||||
|
||||
if (!canUseDirectExternalUrl)
|
||||
{
|
||||
MusicPlaybackService.PrepareStreamSources(request.host, track, MusicPlaybackService.ResolveTrackProvider(track, request.provider), resolvedAudioProvider, request.stream_mode, request.playback_mode, result);
|
||||
source = MusicPlaybackService.SelectSource(result.sources, null, request.stream_mode);
|
||||
if (source == null || string.IsNullOrWhiteSpace(source.url))
|
||||
return null;
|
||||
|
||||
url = source.url;
|
||||
}
|
||||
|
||||
return new PlaylistResolvedEntry
|
||||
{
|
||||
Title = BuildTitle(track, index + 1),
|
||||
Seconds = GetSeconds(track),
|
||||
Url = url
|
||||
};
|
||||
}
|
||||
|
||||
static string BuildPlaylistEntryUrl(MusicTrack track, string trackId, MusicPlaylistRequest request)
|
||||
{
|
||||
trackId = string.IsNullOrWhiteSpace(trackId) ? track?.id : trackId.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trackId) || string.IsNullOrWhiteSpace(request.host))
|
||||
return null;
|
||||
|
||||
string resolvedProvider = MusicPlaybackService.ResolveTrackProvider(track, request.provider);
|
||||
var fallbackTrack = track ?? new MusicTrack { id = trackId };
|
||||
if (string.IsNullOrWhiteSpace(fallbackTrack.id))
|
||||
fallbackTrack.id = trackId;
|
||||
|
||||
var url = MusicPlaybackService.BuildStreamFallbackParams(
|
||||
fallbackTrack,
|
||||
resolvedProvider,
|
||||
request.audio_provider,
|
||||
request.stream_mode,
|
||||
request.playback_mode,
|
||||
request.uid,
|
||||
request.account_email
|
||||
);
|
||||
|
||||
return $"{request.host}/music/stream?{string.Join("&", url)}";
|
||||
}
|
||||
|
||||
static string BuildTitle(MusicTrack track, int position)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(track?.artist_name)
|
||||
? track?.title ?? $"Track {position}"
|
||||
: $"{track.artist_name} - {track.title}";
|
||||
}
|
||||
|
||||
static int GetSeconds(MusicTrack track)
|
||||
{
|
||||
return track?.duration_ms.HasValue == true
|
||||
? (int)Math.Max(0, Math.Round(track.duration_ms.Value / 1000d))
|
||||
: -1;
|
||||
}
|
||||
|
||||
static string EscapePlaylistTitle(string title)
|
||||
=> (title ?? string.Empty).Replace("\r", " ").Replace("\n", " ");
|
||||
|
||||
sealed class PlaylistResolvedEntry
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public int Seconds { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user