Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
using Shared.Services.Utilities;
|
||||
|
||||
namespace Music;
|
||||
|
||||
internal sealed class YandexDownloadInfo
|
||||
{
|
||||
public string download_info_url { get; set; }
|
||||
public int? bitrate_kbps { get; set; }
|
||||
public string codec { get; set; }
|
||||
}
|
||||
|
||||
internal static class YandexMusicSupport
|
||||
{
|
||||
public const string ProviderId = "yandexmusic";
|
||||
public const string Salt = "XGRlBW9FXlekgbPrRHuSiA";
|
||||
|
||||
static readonly Dictionary<string, string> KeyMap = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ym_token"] = "ym_token",
|
||||
["token"] = "ym_token",
|
||||
["oauth"] = "ym_token",
|
||||
|
||||
["ym_session_id"] = "ym_session_id",
|
||||
["session_id"] = "ym_session_id",
|
||||
["Session_id"] = "ym_session_id",
|
||||
|
||||
["ym_sessionid2"] = "ym_sessionid2",
|
||||
["sessionid2"] = "ym_sessionid2",
|
||||
|
||||
["ym_yandexuid"] = "ym_yandexuid",
|
||||
["yandexuid"] = "ym_yandexuid",
|
||||
|
||||
["ym_yandex_login"] = "ym_yandex_login",
|
||||
["yandex_login"] = "ym_yandex_login",
|
||||
["login"] = "ym_yandex_login",
|
||||
|
||||
["ym_l_token"] = "ym_l_token",
|
||||
["l_token"] = "ym_l_token",
|
||||
["L"] = "ym_l_token"
|
||||
};
|
||||
|
||||
public static bool HasApiToken(YandexMusicCredentials credentials)
|
||||
=> !string.IsNullOrWhiteSpace(credentials?.ym_token);
|
||||
|
||||
public static bool HasWebAuth(YandexMusicCredentials credentials)
|
||||
{
|
||||
if (credentials == null)
|
||||
return false;
|
||||
|
||||
bool hasSession = !string.IsNullOrWhiteSpace(credentials.ym_session_id) ||
|
||||
!string.IsNullOrWhiteSpace(credentials.ym_sessionid2);
|
||||
|
||||
return hasSession &&
|
||||
!string.IsNullOrWhiteSpace(credentials.ym_yandexuid) &&
|
||||
!string.IsNullOrWhiteSpace(credentials.ym_yandex_login) &&
|
||||
!string.IsNullOrWhiteSpace(credentials.ym_l_token);
|
||||
}
|
||||
|
||||
public static YandexMusicCredentials Normalize(YandexMusicCredentials credentials)
|
||||
{
|
||||
credentials ??= new YandexMusicCredentials();
|
||||
|
||||
credentials.ym_token = NormalizeValue(credentials.ym_token);
|
||||
credentials.ym_session_id = NormalizeValue(credentials.ym_session_id);
|
||||
credentials.ym_sessionid2 = NormalizeValue(credentials.ym_sessionid2);
|
||||
credentials.ym_yandexuid = NormalizeValue(credentials.ym_yandexuid);
|
||||
credentials.ym_yandex_login = NormalizeValue(credentials.ym_yandex_login);
|
||||
credentials.ym_l_token = NormalizeValue(credentials.ym_l_token);
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public static bool IsEmpty(YandexMusicCredentials credentials)
|
||||
{
|
||||
credentials = Normalize(credentials);
|
||||
|
||||
return string.IsNullOrWhiteSpace(credentials.ym_token) &&
|
||||
string.IsNullOrWhiteSpace(credentials.ym_session_id) &&
|
||||
string.IsNullOrWhiteSpace(credentials.ym_sessionid2) &&
|
||||
string.IsNullOrWhiteSpace(credentials.ym_yandexuid) &&
|
||||
string.IsNullOrWhiteSpace(credentials.ym_yandex_login) &&
|
||||
string.IsNullOrWhiteSpace(credentials.ym_l_token);
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> CreateApiHeaders(YandexMusicCredentials credentials)
|
||||
{
|
||||
credentials = Normalize(credentials);
|
||||
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Accept"] = "application/json, text/plain, */*",
|
||||
["Accept-Language"] = "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||
["Referer"] = "https://music.yandex.ru/"
|
||||
};
|
||||
|
||||
if (HasApiToken(credentials))
|
||||
headers["Authorization"] = $"OAuth {credentials.ym_token}";
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> CreateWebHeaders(YandexMusicCredentials credentials)
|
||||
{
|
||||
credentials = Normalize(credentials);
|
||||
if (!HasWebAuth(credentials))
|
||||
return null;
|
||||
|
||||
var cookie = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(credentials.ym_session_id))
|
||||
cookie.Add($"Session_id={credentials.ym_session_id}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(credentials.ym_sessionid2))
|
||||
cookie.Add($"sessionid2={credentials.ym_sessionid2}");
|
||||
|
||||
cookie.Add($"yandexuid={credentials.ym_yandexuid}");
|
||||
cookie.Add($"yandex_login={credentials.ym_yandex_login}");
|
||||
cookie.Add($"L={credentials.ym_l_token}");
|
||||
|
||||
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Accept"] = "*/*",
|
||||
["Accept-Language"] = "ru-RU,ru;q=0.9,en;q=0.8",
|
||||
["Cache-Control"] = "no-cache",
|
||||
["Pragma"] = "no-cache",
|
||||
["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
||||
["Referer"] = "https://music.yandex.ru/",
|
||||
["Cookie"] = string.Join("; ", cookie)
|
||||
};
|
||||
}
|
||||
|
||||
public static void ApplyHeaders(HttpClient http, Dictionary<string, string> headers)
|
||||
{
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
|
||||
foreach (var header in headers)
|
||||
http.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
public static string BuildTrackQuery(MusicTrack track)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(track?.artist_name))
|
||||
parts.Add(track.artist_name);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(track?.title))
|
||||
parts.Add(track.title);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(track?.album_title))
|
||||
parts.Add(track.album_title);
|
||||
|
||||
return string.Join(" ", parts.Where(i => !string.IsNullOrWhiteSpace(i))).Trim();
|
||||
}
|
||||
|
||||
public static List<MusicAudioMatch> RankMatches(MusicTrack query, IEnumerable<MusicAudioMatch> matches)
|
||||
{
|
||||
string queryTitle = NormalizeText(query?.title);
|
||||
string queryArtist = NormalizeText(query?.artist_name);
|
||||
string queryAlbum = NormalizeText(query?.album_title);
|
||||
|
||||
return matches
|
||||
.Select(match =>
|
||||
{
|
||||
int score = 0;
|
||||
string title = NormalizeText(match.title);
|
||||
string album = NormalizeText(match.album_title);
|
||||
var artists = match.artists.Select(NormalizeText).ToList();
|
||||
|
||||
if (title == queryTitle)
|
||||
score += 6;
|
||||
else if (!string.IsNullOrWhiteSpace(queryTitle) && title.Contains(queryTitle))
|
||||
score += 4;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(queryArtist) && artists.Any(a => a == queryArtist))
|
||||
score += 4;
|
||||
else if (!string.IsNullOrWhiteSpace(queryArtist) && artists.Any(a => a.Contains(queryArtist) || queryArtist.Contains(a)))
|
||||
score += 2;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(queryAlbum) && album == queryAlbum)
|
||||
score += 2;
|
||||
|
||||
if (query.duration_ms.HasValue && match.duration_ms.HasValue)
|
||||
{
|
||||
int diff = Math.Abs(query.duration_ms.Value - match.duration_ms.Value);
|
||||
if (diff <= 3000)
|
||||
score += 3;
|
||||
else if (diff <= 10000)
|
||||
score += 1;
|
||||
}
|
||||
|
||||
return new { match, score };
|
||||
})
|
||||
.OrderByDescending(i => i.score)
|
||||
.ThenBy(i => i.match.title)
|
||||
.Select(i => i.match)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<YandexDownloadInfo> ParseDownloadInfos(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return Array.Empty<YandexDownloadInfo>();
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (!doc.RootElement.TryGetProperty("result", out var result) || result.ValueKind != JsonValueKind.Array)
|
||||
return Array.Empty<YandexDownloadInfo>();
|
||||
|
||||
var infos = new List<YandexDownloadInfo>();
|
||||
|
||||
foreach (var item in result.EnumerateArray())
|
||||
{
|
||||
string url = TryGetString(item, "downloadInfoUrl");
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
continue;
|
||||
|
||||
infos.Add(new YandexDownloadInfo
|
||||
{
|
||||
download_info_url = url,
|
||||
bitrate_kbps = TryGetInt(item, "bitrateInKbps"),
|
||||
codec = TryGetString(item, "codec")
|
||||
});
|
||||
}
|
||||
|
||||
return infos
|
||||
.OrderByDescending(i => string.Equals(i.codec, "mp3", StringComparison.OrdinalIgnoreCase))
|
||||
.ThenByDescending(i => i.bitrate_kbps ?? 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string BuildStreamUrl(string host, string path, string ts, string s)
|
||||
{
|
||||
string pathForSign = path?.TrimStart('/') ?? string.Empty;
|
||||
string sign = CrypTo.md5(Salt + pathForSign + s);
|
||||
return $"https://{host}/get-mp3/{sign}/{ts}{path}";
|
||||
}
|
||||
|
||||
public static bool TryParseDownloadPayload(string raw, out string host, out string path, out string ts, out string s)
|
||||
{
|
||||
host = null;
|
||||
path = null;
|
||||
ts = null;
|
||||
s = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return false;
|
||||
|
||||
raw = raw.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
if (raw.StartsWith("{"))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
host = TryGetString(doc.RootElement, "host");
|
||||
path = TryGetString(doc.RootElement, "path");
|
||||
ts = TryGetString(doc.RootElement, "ts");
|
||||
s = TryGetString(doc.RootElement, "s");
|
||||
}
|
||||
else
|
||||
{
|
||||
var xml = XDocument.Parse(raw);
|
||||
host = xml.Root?.Element("host")?.Value;
|
||||
path = xml.Root?.Element("path")?.Value;
|
||||
ts = xml.Root?.Element("ts")?.Value;
|
||||
s = xml.Root?.Element("s")?.Value;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(host) &&
|
||||
!string.IsNullOrWhiteSpace(path) &&
|
||||
!string.IsNullOrWhiteSpace(ts) &&
|
||||
!string.IsNullOrWhiteSpace(s);
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> ParseSavePayload(string payload)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
return result;
|
||||
|
||||
payload = payload.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
if (payload.StartsWith("{"))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(payload);
|
||||
foreach (var property in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!KeyMap.TryGetValue(property.Name, out var key))
|
||||
continue;
|
||||
|
||||
result[key] = property.Value.ValueKind == JsonValueKind.String
|
||||
? property.Value.GetString()
|
||||
: property.Value.ToString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var form = HttpUtility.ParseQueryString(payload
|
||||
.Replace("\r", "&")
|
||||
.Replace("\n", "&")
|
||||
.Replace(";", "&"));
|
||||
foreach (string rawKey in form.AllKeys)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawKey) || !KeyMap.TryGetValue(rawKey, out var key))
|
||||
continue;
|
||||
|
||||
result[key] = form[rawKey];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string BuildStateMessage(bool hasToken, bool hasWeb)
|
||||
{
|
||||
if (hasToken && hasWeb)
|
||||
return "Token и web cookies сохранены.";
|
||||
|
||||
if (hasToken)
|
||||
return "Token сохранён. Web cookies для подборок и web-разделов ещё не заданы.";
|
||||
|
||||
if (hasWeb)
|
||||
return "Web cookies сохранены. OAuth token для API и playback ещё не задан.";
|
||||
|
||||
return "Token and cookies are required for full functionality.";
|
||||
}
|
||||
|
||||
static string NormalizeValue(string value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
static string NormalizeText(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return string.Empty;
|
||||
|
||||
return new string(value
|
||||
.Trim()
|
||||
.ToLowerInvariant()
|
||||
.Where(ch => char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
static string TryGetString(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var property))
|
||||
return null;
|
||||
|
||||
return property.ValueKind == JsonValueKind.String ? property.GetString() : property.ToString();
|
||||
}
|
||||
|
||||
static int? TryGetInt(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var property))
|
||||
return null;
|
||||
|
||||
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var number))
|
||||
return number;
|
||||
|
||||
if (property.ValueKind == JsonValueKind.String && int.TryParse(property.GetString(), out number))
|
||||
return number;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user