Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Music;
|
||||
|
||||
public static class DiscogsArtistImageService
|
||||
{
|
||||
static readonly HttpClient httpClient = MusicHttp.CreateClient("discogs");
|
||||
static readonly SemaphoreSlim requestGate = new(1, 1);
|
||||
static readonly ConcurrentDictionary<string, Task<List<MusicImage>>> pendingLookups = new();
|
||||
static DateTime nextRequestAt = DateTime.MinValue;
|
||||
|
||||
static readonly TimeSpan cacheTtl = TimeSpan.FromDays(30);
|
||||
const string cacheVersion = "v6";
|
||||
|
||||
const string baseUrl = "https://api.discogs.com";
|
||||
const string userAgent = "LampacNextgenMusic/0.1 (https://github.com/lampac-nextgen/lampac)";
|
||||
|
||||
static DiscogsArtistImageService()
|
||||
{
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(20);
|
||||
}
|
||||
|
||||
public static async Task EnrichAsync(List<MusicArtist> artists, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (artists == null || artists.Count == 0)
|
||||
return;
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
|
||||
foreach (var artist in artists)
|
||||
{
|
||||
if (artist == null)
|
||||
continue;
|
||||
|
||||
string normalized = NormalizeArtistName(artist.name);
|
||||
if (string.IsNullOrWhiteSpace(normalized) || !seen.Add(normalized))
|
||||
continue;
|
||||
|
||||
await EnrichAsync(artist, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ApplyCachedAsync(List<MusicArtist> artists, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (artists == null || artists.Count == 0)
|
||||
return;
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
|
||||
foreach (var artist in artists)
|
||||
{
|
||||
if (artist == null || !string.IsNullOrWhiteSpace(artist.images?.FirstOrDefault()?.url) || string.IsNullOrWhiteSpace(artist.name))
|
||||
continue;
|
||||
|
||||
string normalized = NormalizeArtistName(artist.name);
|
||||
if (string.IsNullOrWhiteSpace(normalized) || !seen.Add(normalized))
|
||||
continue;
|
||||
|
||||
var images = await GetCachedImagesAsync(artist.name, artist.country, cancellationToken);
|
||||
|
||||
if (images.Count > 0)
|
||||
artist.images = images;
|
||||
}
|
||||
}
|
||||
|
||||
public static void WarmupMissing(List<MusicArtist> artists)
|
||||
{
|
||||
if (artists == null || artists.Count == 0)
|
||||
return;
|
||||
|
||||
var queue = artists
|
||||
.Where(i => i != null && string.IsNullOrWhiteSpace(i.images?.FirstOrDefault()?.url) && !string.IsNullOrWhiteSpace(i.name))
|
||||
.GroupBy(i => BuildCacheKey(i.name, i.country))
|
||||
.Select(i => i.First())
|
||||
.Take(3)
|
||||
.ToList();
|
||||
|
||||
if (queue.Count == 0)
|
||||
return;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
foreach (var artist in queue)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ResolveImagesAsync(artist, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task EnrichAsync(MusicArtist artist, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (artist == null || !string.IsNullOrWhiteSpace(artist.images?.FirstOrDefault()?.url) || string.IsNullOrWhiteSpace(artist.name))
|
||||
return;
|
||||
|
||||
var images = await ResolveImagesAsync(artist, cancellationToken);
|
||||
|
||||
if (images.Count > 0)
|
||||
artist.images = images;
|
||||
}
|
||||
|
||||
public static async Task<List<MusicImage>> ResolveImagesAsync(MusicArtist artist, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (artist == null || string.IsNullOrWhiteSpace(artist.name))
|
||||
return new List<MusicImage>();
|
||||
|
||||
string cacheKey = BuildCacheKey(artist.name, artist.country);
|
||||
var cached = await GetCachedImagesAsync(artist.name, artist.country, cancellationToken);
|
||||
if (cached.Count > 0)
|
||||
return cached;
|
||||
|
||||
var pending = pendingLookups.GetOrAdd(cacheKey, _ => LookupAndCacheAsync(cacheKey, artist.name, artist.country));
|
||||
|
||||
try
|
||||
{
|
||||
return await pending;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pending.IsCompleted)
|
||||
pendingLookups.TryRemove(new KeyValuePair<string, Task<List<MusicImage>>>(cacheKey, pending));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<MusicImage>> GetCachedImagesAsync(string artistName, string country = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artistName))
|
||||
return new List<MusicImage>();
|
||||
|
||||
string cacheKey = BuildCacheKey(artistName, country);
|
||||
var exact = await MusicMetadataCacheService.GetAsync<List<MusicImage>>(
|
||||
"discogs",
|
||||
"artist-image",
|
||||
cacheKey,
|
||||
cancellationToken
|
||||
) ?? new List<MusicImage>();
|
||||
|
||||
if (exact.Count > 0 || string.IsNullOrWhiteSpace(country))
|
||||
return exact;
|
||||
|
||||
return await MusicMetadataCacheService.GetAsync<List<MusicImage>>(
|
||||
"discogs",
|
||||
"artist-image",
|
||||
BuildCacheKey(artistName, null),
|
||||
cancellationToken
|
||||
) ?? new List<MusicImage>();
|
||||
}
|
||||
|
||||
static async Task<List<MusicImage>> LookupAndCacheAsync(string cacheKey, string artistName, string country)
|
||||
{
|
||||
return await MusicMetadataCacheService.GetOrCreateAsync(
|
||||
"discogs",
|
||||
"artist-image",
|
||||
cacheKey,
|
||||
cacheTtl,
|
||||
() => LookupImagesAsync(artistName, country, CancellationToken.None),
|
||||
CancellationToken.None
|
||||
) ?? new List<MusicImage>();
|
||||
}
|
||||
|
||||
static async Task<List<MusicImage>> LookupImagesAsync(string artistName, string country, CancellationToken cancellationToken)
|
||||
{
|
||||
var searchJson = await GetJsonAsync($"database/search?q={HttpUtility.UrlEncode(artistName)}&type=artist&per_page=5", cancellationToken);
|
||||
|
||||
// null = сеть/429 — НЕ кэшируем (GetOrCreateAsync пропускает null),
|
||||
// чтобы сбой Discogs не превращался в «у артиста нет фото»
|
||||
if (searchJson == null)
|
||||
return null;
|
||||
|
||||
var best = SelectBestArtist(searchJson["results"] as JsonArray, artistName, country);
|
||||
if (best == null)
|
||||
return new List<MusicImage>();
|
||||
|
||||
var id = best["id"]?.GetValue<int?>();
|
||||
if (id == null || id <= 0)
|
||||
return new List<MusicImage>();
|
||||
|
||||
var artistJson = await GetJsonAsync($"artists/{id}", cancellationToken);
|
||||
if (artistJson == null)
|
||||
return null;
|
||||
|
||||
return ParseImages(artistJson["images"] as JsonArray);
|
||||
}
|
||||
|
||||
static JsonObject SelectBestArtist(JsonArray results, string artistName, string country)
|
||||
{
|
||||
if (results == null || results.Count == 0)
|
||||
return null;
|
||||
|
||||
string target = NormalizeArtistName(artistName);
|
||||
int bestScore = int.MinValue;
|
||||
JsonObject best = null;
|
||||
|
||||
foreach (var item in results.OfType<JsonObject>())
|
||||
{
|
||||
int score = ScoreArtist(item, target, country);
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = item;
|
||||
}
|
||||
}
|
||||
|
||||
return bestScore > 0 ? best : null;
|
||||
}
|
||||
|
||||
static int ScoreArtist(JsonObject item, string target, string country)
|
||||
{
|
||||
string rawTitle = item["title"]?.GetValue<string>() ?? string.Empty;
|
||||
string title = NormalizeArtistName(rawTitle);
|
||||
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(target))
|
||||
return 0;
|
||||
|
||||
if (title != target)
|
||||
return 0;
|
||||
|
||||
int score = 100;
|
||||
|
||||
if (string.Equals(rawTitle.Trim().ToLowerInvariant(), target, StringComparison.Ordinal))
|
||||
score += 15;
|
||||
|
||||
if (rawTitle.Contains("(")) score -= 10;
|
||||
if (Regex.IsMatch(rawTitle, @"\(\d+\)\s*$")) score -= 10;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(country) && rawTitle.Contains(country, StringComparison.OrdinalIgnoreCase))
|
||||
score += 5;
|
||||
|
||||
string thumb = item["thumb"]?.GetValue<string>() ?? string.Empty;
|
||||
string cover = item["cover_image"]?.GetValue<string>() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(thumb) || !string.IsNullOrWhiteSpace(cover))
|
||||
score += 3;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
static string NormalizeArtistName(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return string.Empty;
|
||||
|
||||
value = Regex.Replace(value, @"\s*\(\d+\)\s*$", string.Empty);
|
||||
value = Regex.Replace(value, @"[^0-9\p{L}\s]+", " ");
|
||||
value = Regex.Replace(value, @"\s+", " ").Trim();
|
||||
return value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
static List<MusicImage> ParseImages(JsonArray items)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new List<MusicImage>();
|
||||
|
||||
var primary = items
|
||||
.OfType<JsonObject>()
|
||||
.OrderByDescending(item => string.Equals(item["type"]?.GetValue<string>(), "primary", StringComparison.OrdinalIgnoreCase))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (primary == null)
|
||||
return new List<MusicImage>();
|
||||
|
||||
string small = primary["uri150"]?.GetValue<string>();
|
||||
string full = primary["uri"]?.GetValue<string>();
|
||||
int width = primary["width"]?.GetValue<int?>() ?? 0;
|
||||
int height = primary["height"]?.GetValue<int?>() ?? 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(small) && string.IsNullOrWhiteSpace(full))
|
||||
return new List<MusicImage>();
|
||||
|
||||
var result = new List<MusicImage>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(small))
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
url = small,
|
||||
width = 150,
|
||||
height = 150
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(full) && width > 0 && height > 0)
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
url = full,
|
||||
width = width,
|
||||
height = height
|
||||
});
|
||||
}
|
||||
|
||||
return result
|
||||
.GroupBy(i => i.url)
|
||||
.Select(i => i.First())
|
||||
.OrderBy(i => i.width)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
static async Task<JsonObject> GetJsonAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
await RespectRateLimitAsync(cancellationToken);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"{baseUrl}/{path}");
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", userAgent);
|
||||
request.Headers.TryAddWithoutValidation("Accept", "application/json");
|
||||
|
||||
using var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonNode.Parse(json) as JsonObject;
|
||||
}
|
||||
|
||||
static async Task RespectRateLimitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await requestGate.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (nextRequestAt > now)
|
||||
await Task.Delay(nextRequestAt - now, cancellationToken);
|
||||
|
||||
// анонимный лимит Discogs — 25 запросов/мин, т.е. не чаще ~2.4s
|
||||
nextRequestAt = DateTime.UtcNow.AddMilliseconds(2500);
|
||||
}
|
||||
finally
|
||||
{
|
||||
requestGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
static string BuildCacheKey(string artistName, string country)
|
||||
=> $"{cacheVersion}|{NormalizeArtistName(artistName)}|{(country ?? string.Empty).Trim().ToLowerInvariant()}";
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
using Shared;
|
||||
using Shared.Models.Base;
|
||||
|
||||
namespace Music;
|
||||
|
||||
public static class MusicImageProxyService
|
||||
{
|
||||
static readonly BaseSettings init = new()
|
||||
{
|
||||
plugin = "Music"
|
||||
};
|
||||
|
||||
// Create a shallow copy without listing every DTO property. The delegate is
|
||||
// bound once; new metadata fields are preserved automatically.
|
||||
static readonly Func<object, object> shallowCopy = typeof(object)
|
||||
.GetMethod("MemberwiseClone", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
|
||||
.CreateDelegate<Func<object, object>>();
|
||||
|
||||
static T Copy<T>(T value) where T : class => (T)shallowCopy(value);
|
||||
|
||||
// Only rewritten image branches are copied. Reused branches stay read-only:
|
||||
// controllers serialize this result without modifying its DTOs or lists.
|
||||
public static T Apply<T>(BaseController controller, T response)
|
||||
{
|
||||
if (controller == null || response == null)
|
||||
return response;
|
||||
|
||||
return (T)Rewrite(controller, response);
|
||||
}
|
||||
|
||||
static object Rewrite(BaseController controller, object value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MusicSearchResponse response => RewriteSearch(controller, response),
|
||||
MusicHomeResponse response => RewriteHome(controller, response),
|
||||
MusicStatsTopResult response => RewriteStats(controller, response),
|
||||
MusicArtist artist => RewriteArtist(controller, artist),
|
||||
MusicAlbum album => RewriteAlbum(controller, album),
|
||||
MusicTrack track => RewriteTrack(controller, track),
|
||||
MusicBrowseSection section => RewriteSection(controller, section),
|
||||
MusicUserPlaylistSummary playlist => RewritePlaylist(controller, playlist),
|
||||
MusicUserPlaylistImportResult response => RewriteImport(controller, response),
|
||||
MusicDailyMixResponse response => RewriteDaily(controller, response),
|
||||
MusicRadioResponse response => RewriteRadio(controller, response),
|
||||
MusicRecentlyPlayedItem item => RewriteHistory(controller, item),
|
||||
MusicStatsTopTrack item => RewriteStatsTrack(controller, item),
|
||||
MusicImage image => RewriteImage(controller, image),
|
||||
List<MusicTrack> tracks => RewriteList(controller, tracks),
|
||||
List<MusicUserPlaylistSummary> playlists => RewriteList(controller, playlists),
|
||||
MusicTrack[] tracks => RewriteArray(controller, tracks),
|
||||
MusicUserPlaylistSummary[] playlists => RewriteArray(controller, playlists),
|
||||
_ => value
|
||||
};
|
||||
}
|
||||
|
||||
static List<T> RewriteList<T>(BaseController controller, List<T> items) where T : class
|
||||
{
|
||||
if (items == null)
|
||||
return null;
|
||||
|
||||
List<T> copy = null;
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = (T)Rewrite(controller, items[i]);
|
||||
if (ReferenceEquals(item, items[i]))
|
||||
continue;
|
||||
|
||||
copy ??= new List<T>(items);
|
||||
copy[i] = item;
|
||||
}
|
||||
|
||||
return copy ?? items;
|
||||
}
|
||||
|
||||
static T[] RewriteArray<T>(BaseController controller, T[] items) where T : class
|
||||
{
|
||||
T[] copy = null;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var item = (T)Rewrite(controller, items[i]);
|
||||
if (ReferenceEquals(item, items[i]))
|
||||
continue;
|
||||
|
||||
copy ??= (T[])items.Clone();
|
||||
copy[i] = item;
|
||||
}
|
||||
|
||||
return copy ?? items;
|
||||
}
|
||||
|
||||
static MusicSearchResponse RewriteSearch(BaseController controller, MusicSearchResponse value)
|
||||
{
|
||||
var artists = RewriteList(controller, value.artists);
|
||||
var albums = RewriteList(controller, value.albums);
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
var search_sections = RewriteList(controller, value.search_sections);
|
||||
|
||||
if (ReferenceEquals(artists, value.artists)
|
||||
&& ReferenceEquals(albums, value.albums)
|
||||
&& ReferenceEquals(tracks, value.tracks)
|
||||
&& ReferenceEquals(search_sections, value.search_sections))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.artists = artists;
|
||||
copy.albums = albums;
|
||||
copy.tracks = tracks;
|
||||
copy.search_sections = search_sections;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicHomeResponse RewriteHome(BaseController controller, MusicHomeResponse value)
|
||||
{
|
||||
var browse_sections = RewriteList(controller, value.browse_sections);
|
||||
var recently_played = RewriteList(controller, value.recently_played);
|
||||
var user_playlists = RewriteList(controller, value.user_playlists);
|
||||
|
||||
if (ReferenceEquals(browse_sections, value.browse_sections)
|
||||
&& ReferenceEquals(recently_played, value.recently_played)
|
||||
&& ReferenceEquals(user_playlists, value.user_playlists))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.browse_sections = browse_sections;
|
||||
copy.recently_played = recently_played;
|
||||
copy.user_playlists = user_playlists;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicStatsTopResult RewriteStats(BaseController controller, MusicStatsTopResult value)
|
||||
{
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicArtist RewriteArtist(BaseController controller, MusicArtist value)
|
||||
{
|
||||
var images = RewriteList(controller, value.images);
|
||||
var albums = RewriteList(controller, value.albums);
|
||||
var sections = RewriteList(controller, value.sections);
|
||||
|
||||
if (ReferenceEquals(images, value.images)
|
||||
&& ReferenceEquals(albums, value.albums)
|
||||
&& ReferenceEquals(sections, value.sections))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.images = images;
|
||||
copy.albums = albums;
|
||||
copy.sections = sections;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicAlbum RewriteAlbum(BaseController controller, MusicAlbum value)
|
||||
{
|
||||
var images = RewriteList(controller, value.images);
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(images, value.images)
|
||||
&& ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.images = images;
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicTrack RewriteTrack(BaseController controller, MusicTrack value)
|
||||
{
|
||||
var images = RewriteList(controller, value.images);
|
||||
|
||||
if (ReferenceEquals(images, value.images))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.images = images;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicBrowseSection RewriteSection(BaseController controller, MusicBrowseSection value)
|
||||
{
|
||||
var albums = RewriteList(controller, value.albums);
|
||||
var artists = RewriteList(controller, value.artists);
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(albums, value.albums)
|
||||
&& ReferenceEquals(artists, value.artists)
|
||||
&& ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.albums = albums;
|
||||
copy.artists = artists;
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicUserPlaylistSummary RewritePlaylist(BaseController controller, MusicUserPlaylistSummary value)
|
||||
{
|
||||
var images = RewriteList(controller, value.images);
|
||||
|
||||
if (ReferenceEquals(images, value.images))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.images = images;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicUserPlaylistImportResult RewriteImport(BaseController controller, MusicUserPlaylistImportResult value)
|
||||
{
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicDailyMixResponse RewriteDaily(BaseController controller, MusicDailyMixResponse value)
|
||||
{
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicRadioResponse RewriteRadio(BaseController controller, MusicRadioResponse value)
|
||||
{
|
||||
var tracks = RewriteList(controller, value.tracks);
|
||||
|
||||
if (ReferenceEquals(tracks, value.tracks))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.tracks = tracks;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicRecentlyPlayedItem RewriteHistory(BaseController controller, MusicRecentlyPlayedItem value)
|
||||
{
|
||||
var track = (MusicTrack)Rewrite(controller, value.track);
|
||||
|
||||
if (ReferenceEquals(track, value.track))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.track = track;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicStatsTopTrack RewriteStatsTrack(BaseController controller, MusicStatsTopTrack value)
|
||||
{
|
||||
var track = (MusicTrack)Rewrite(controller, value.track);
|
||||
|
||||
if (ReferenceEquals(track, value.track))
|
||||
return value;
|
||||
|
||||
var copy = Copy(value);
|
||||
copy.track = track;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static MusicImage RewriteImage(BaseController controller, MusicImage image)
|
||||
{
|
||||
string url = image.url;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return image;
|
||||
|
||||
// Normalize even when proxying is disabled (e.g. file:// TV clients).
|
||||
if (url.StartsWith("//", StringComparison.Ordinal))
|
||||
url = "https:" + url;
|
||||
|
||||
// Repair legacy cached proxy URLs without modifying the cached image.
|
||||
if (TryRewriteProxyHost(url, controller.host, out string rewritten))
|
||||
url = rewritten;
|
||||
else if (NeedProxy(url, controller.host))
|
||||
url = controller.HostImgProxy(init, url);
|
||||
|
||||
if (string.Equals(url, image.url, StringComparison.Ordinal))
|
||||
return image;
|
||||
|
||||
var copy = Copy(image);
|
||||
copy.url = url;
|
||||
return copy;
|
||||
}
|
||||
|
||||
static bool TryRewriteProxyHost(string url, string host, out string rewritten)
|
||||
{
|
||||
rewritten = null;
|
||||
|
||||
int index = url.IndexOf("/proxyimg", StringComparison.OrdinalIgnoreCase);
|
||||
if (index <= 0 || string.IsNullOrWhiteSpace(host))
|
||||
return false;
|
||||
|
||||
string prefix = url.Substring(0, index);
|
||||
if (string.Equals(prefix, host, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// префикс должен быть полноценным origin (а не куском чужого пути)
|
||||
if (!Uri.TryCreate(prefix, UriKind.Absolute, out var prefixUri) ||
|
||||
!prefixUri.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
rewritten = host + url.Substring(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool NeedProxy(string url, string host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return false;
|
||||
|
||||
if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ||
|
||||
url.StartsWith("blob:", StringComparison.OrdinalIgnoreCase) ||
|
||||
url.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
|
||||
if (!uri.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(host) &&
|
||||
Uri.TryCreate(host, UriKind.Absolute, out var hostUri) &&
|
||||
string.Equals(uri.Host, hostUri.Host, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
return !url.Contains("/proxyimg", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user