mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-08-20 13:06:20 -04:00
fix(library): show mapping art and admin sources
This commit is contained in:
@@ -81,13 +81,14 @@ public sealed class PlaylistLinksControllerContractTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceDiscovery_ExcludesNonOperationalAndUnapprovedGlobalPersonalAccounts()
|
||||
public void SourceDiscovery_ExcludesNonOperationalProvidersAndAllowsAdministratorsToManageGlobalAccounts()
|
||||
{
|
||||
var source = File.ReadAllText(FindRepositoryFile(
|
||||
"allstarr", "Controllers", "PlaylistLinksController.cs"));
|
||||
|
||||
Assert.Contains("includeNonOperational: false", source, StringComparison.Ordinal);
|
||||
Assert.Contains("providerPolicy.AllowGlobalPersonalAccounts", source, StringComparison.Ordinal);
|
||||
Assert.Contains("session.IsAdministrator", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -58,12 +58,20 @@ public sealed class PlaylistLinksController(
|
||||
return capability.AllowedAccountScopes.Contains(item.Scope);
|
||||
}).ToArray();
|
||||
var availableAccounts = capableAccounts
|
||||
.Where(item => item.Scope != ProviderAccountScope.Global || providerPolicy.AllowGlobalPersonalAccounts)
|
||||
.Where(item => item.Scope != ProviderAccountScope.Global ||
|
||||
providerPolicy.AllowGlobalPersonalAccounts ||
|
||||
session.IsAdministrator)
|
||||
.ToArray();
|
||||
var blockedAccounts = capableAccounts.Except(availableAccounts).ToArray();
|
||||
return Ok(new
|
||||
{
|
||||
accounts = availableAccounts.Select(item => ToPlaylistSourceAccountDto(item, true, null)),
|
||||
accounts = availableAccounts.Select(item => ToPlaylistSourceAccountDto(
|
||||
item,
|
||||
true,
|
||||
null,
|
||||
item.Scope == ProviderAccountScope.Global &&
|
||||
session.IsAdministrator &&
|
||||
!providerPolicy.AllowGlobalPersonalAccounts)),
|
||||
blockedAccounts = blockedAccounts.Select(item => ToPlaylistSourceAccountDto(
|
||||
item,
|
||||
false,
|
||||
@@ -81,7 +89,8 @@ public sealed class PlaylistLinksController(
|
||||
}),
|
||||
policy = new
|
||||
{
|
||||
allowSharedPlaylistCredentials = providerPolicy.AllowGlobalPersonalAccounts
|
||||
allowSharedPlaylistCredentials = providerPolicy.AllowGlobalPersonalAccounts,
|
||||
administratorCanUseSharedPlaylistCredentials = session.IsAdministrator
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -663,7 +672,8 @@ public sealed class PlaylistLinksController(
|
||||
private static object ToPlaylistSourceAccountDto(
|
||||
ProviderAccountRecord account,
|
||||
bool available,
|
||||
string? reasonCode) => new
|
||||
string? reasonCode,
|
||||
bool administratorAccess = false) => new
|
||||
{
|
||||
id = account.Id,
|
||||
providerId = account.ProviderId,
|
||||
@@ -674,6 +684,7 @@ public sealed class PlaylistLinksController(
|
||||
{
|
||||
ProviderAccountScope.User => "Personal account",
|
||||
ProviderAccountScope.Library => "Library-shared account",
|
||||
ProviderAccountScope.Global when administratorAccess => "Administrator account",
|
||||
_ => "Deployment-shared account"
|
||||
},
|
||||
revision = account.Revision,
|
||||
|
||||
@@ -572,11 +572,13 @@ public sealed class TrackMatchesController(
|
||||
private static string Hash(string value) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
|
||||
|
||||
private static TrackMetadata ToTrackMetadata((string? Title, string? Artist, string? Album) value) => new()
|
||||
private static TrackMetadata ToTrackMetadata(
|
||||
(string? Title, string? Artist, string? Album, string? ArtworkUrl, string? Isrc) value) => new()
|
||||
{
|
||||
Title = value.Title,
|
||||
Artist = value.Artist,
|
||||
Album = value.Album
|
||||
Album = value.Album,
|
||||
ArtworkUrl = value.ArtworkUrl
|
||||
};
|
||||
|
||||
private static MatchRow Row(ExternalMetadataSnapshotRecord snapshot, TrackMatchRecord? decision,
|
||||
@@ -609,6 +611,8 @@ public sealed class TrackMatchesController(
|
||||
title = metadata.Title,
|
||||
artist = metadata.Artist,
|
||||
album = metadata.Album,
|
||||
artworkUrl = metadata.ArtworkUrl,
|
||||
isrc = metadata.Isrc,
|
||||
localTrack = track == null ? null : new { track.Id, track.BackendItemId, track.Title, track.Artist, track.Album },
|
||||
providerIdentities,
|
||||
candidates = ParseCandidates(decision?.CandidateResultsJson),
|
||||
@@ -620,7 +624,7 @@ public sealed class TrackMatchesController(
|
||||
return new(state, $"{metadata.Title} {metadata.Artist} {metadata.Album} {snapshot.ProviderId} {track?.Title} {track?.Artist}", value);
|
||||
}
|
||||
|
||||
private static (string? Title, string? Artist, string? Album) Metadata(string json)
|
||||
private static (string? Title, string? Artist, string? Album, string? ArtworkUrl, string? Isrc) Metadata(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -630,9 +634,15 @@ public sealed class TrackMatchesController(
|
||||
var artist = Text(root, "artist") ?? Text(root, "Artist");
|
||||
if (artist == null && (root.TryGetProperty("artists", out var artists) || root.TryGetProperty("Artists", out artists)) && artists.ValueKind == JsonValueKind.Array)
|
||||
artist = string.Join(", ", artists.EnumerateArray().Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() : Text(item, "name")).Where(item => item != null));
|
||||
return (title, artist, Text(root, "album") ?? Text(root, "Album") ?? Text(root, "albumTitle") ?? Text(root, "AlbumTitle"));
|
||||
return (
|
||||
title,
|
||||
artist,
|
||||
Text(root, "album") ?? Text(root, "Album") ?? Text(root, "albumTitle") ?? Text(root, "AlbumTitle"),
|
||||
Text(root, "artworkUrl") ?? Text(root, "ArtworkUrl") ?? Text(root, "coverUrl") ?? Text(root, "CoverUrl") ??
|
||||
Text(root, "imageUrl") ?? Text(root, "ImageUrl"),
|
||||
Text(root, "isrc") ?? Text(root, "Isrc") ?? Text(root, "ISRC"));
|
||||
}
|
||||
catch (JsonException) { return (null, null, null); }
|
||||
catch (JsonException) { return (null, null, null, null, null); }
|
||||
}
|
||||
|
||||
private static string? Text(JsonElement root, string name) => root.ValueKind == JsonValueKind.Object &&
|
||||
|
||||
@@ -2165,18 +2165,61 @@
|
||||
}
|
||||
|
||||
.mapping-card-title > div {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mapping-card-title strong {
|
||||
font-size: 1.05rem;
|
||||
font-size: 1.12rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.mapping-card-title p {
|
||||
margin: 0.2rem 0 0;
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.mapping-source-label {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mapping-track-artist {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.mapping-track-album {
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.mapping-card-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mapping-card-meta > span {
|
||||
display: inline-flex;
|
||||
gap: 0.32rem;
|
||||
align-items: center;
|
||||
padding: 0.28rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-page);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.mapping-route {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.75fr) auto minmax(0, 1.25fr);
|
||||
|
||||
@@ -3849,10 +3849,14 @@ class AllstarrApp extends LitElement {
|
||||
const providerId = mapping.providerId || "provider";
|
||||
const targetProvider = mapping.localTrack ? String(this.status?.backendType || "jellyfin").toLowerCase() : routes[0]?.providerId;
|
||||
const artwork = mapping.artworkUrl || mapping.ArtworkUrl;
|
||||
const confidence = Number(mapping.confidence ?? mapping.Confidence);
|
||||
const confidenceLabel = Number.isFinite(confidence) ? `${Math.round(confidence * 1000) / 10}% confidence` : "";
|
||||
const isrc = mapping.isrc || mapping.Isrc;
|
||||
return html`<article class="mapping-review-card ${["unresolved", "suggested", "ambiguous", "rejected"].includes(state) ? "needs-attention" : ""}">
|
||||
<div class="mapping-source-mark" aria-hidden="true">${artwork ? html`<img src=${artwork} alt="" loading="lazy" decoding="async">` : this.renderProviderLogo(providerId, "large")}</div>
|
||||
<div class="mapping-track-copy">
|
||||
<div class="mapping-card-title"><div><strong>${display(mapping.title, "Unknown track")}</strong><p>${display(mapping.artist, "Unknown artist")}${mapping.album ? html`<span> · ${mapping.album}</span>` : ""}</p></div><span class="status-pill ${state}">${titleCase(state)}</span></div>
|
||||
<div class="mapping-card-title"><div><span class="mapping-source-label">${this.renderProviderLogo(providerId, "tiny")}${providerDisplayName(providerId, this.schema?.providers)} source</span><strong>${display(mapping.title, "Unknown track")}</strong><p class="mapping-track-artist">${display(mapping.artist, "Unknown artist")}</p>${mapping.album ? html`<p class="mapping-track-album">${mapping.album}</p>` : nothing}</div><span class="status-pill ${state}">${titleCase(state)}</span></div>
|
||||
<div class="mapping-card-meta">${confidenceLabel ? html`<span>${icon("activity", 14)} ${confidenceLabel}</span>` : nothing}${isrc ? html`<span class="mono">ISRC ${isrc}</span>` : nothing}${mapping.decidedAt ? html`<span>${icon("clock", 14)} ${formatRelativeTime(mapping.decidedAt)}</span>` : nothing}</div>
|
||||
<div class="mapping-route">
|
||||
<span class="mapping-route-node">${this.renderProviderLogo(providerId, "tiny")}<span><small>Source</small><strong>${providerDisplayName(providerId, this.schema?.providers)}</strong></span></span>
|
||||
<span class="mapping-route-arrow" aria-hidden="true">${icon("chevronRight", 18)}</span>
|
||||
|
||||
Reference in New Issue
Block a user