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]
|
[Fact]
|
||||||
public void SourceDiscovery_ExcludesNonOperationalAndUnapprovedGlobalPersonalAccounts()
|
public void SourceDiscovery_ExcludesNonOperationalProvidersAndAllowsAdministratorsToManageGlobalAccounts()
|
||||||
{
|
{
|
||||||
var source = File.ReadAllText(FindRepositoryFile(
|
var source = File.ReadAllText(FindRepositoryFile(
|
||||||
"allstarr", "Controllers", "PlaylistLinksController.cs"));
|
"allstarr", "Controllers", "PlaylistLinksController.cs"));
|
||||||
|
|
||||||
Assert.Contains("includeNonOperational: false", source, StringComparison.Ordinal);
|
Assert.Contains("includeNonOperational: false", source, StringComparison.Ordinal);
|
||||||
Assert.Contains("providerPolicy.AllowGlobalPersonalAccounts", source, StringComparison.Ordinal);
|
Assert.Contains("providerPolicy.AllowGlobalPersonalAccounts", source, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("session.IsAdministrator", source, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -58,12 +58,20 @@ public sealed class PlaylistLinksController(
|
|||||||
return capability.AllowedAccountScopes.Contains(item.Scope);
|
return capability.AllowedAccountScopes.Contains(item.Scope);
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
var availableAccounts = capableAccounts
|
var availableAccounts = capableAccounts
|
||||||
.Where(item => item.Scope != ProviderAccountScope.Global || providerPolicy.AllowGlobalPersonalAccounts)
|
.Where(item => item.Scope != ProviderAccountScope.Global ||
|
||||||
|
providerPolicy.AllowGlobalPersonalAccounts ||
|
||||||
|
session.IsAdministrator)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var blockedAccounts = capableAccounts.Except(availableAccounts).ToArray();
|
var blockedAccounts = capableAccounts.Except(availableAccounts).ToArray();
|
||||||
return Ok(new
|
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(
|
blockedAccounts = blockedAccounts.Select(item => ToPlaylistSourceAccountDto(
|
||||||
item,
|
item,
|
||||||
false,
|
false,
|
||||||
@@ -81,7 +89,8 @@ public sealed class PlaylistLinksController(
|
|||||||
}),
|
}),
|
||||||
policy = new
|
policy = new
|
||||||
{
|
{
|
||||||
allowSharedPlaylistCredentials = providerPolicy.AllowGlobalPersonalAccounts
|
allowSharedPlaylistCredentials = providerPolicy.AllowGlobalPersonalAccounts,
|
||||||
|
administratorCanUseSharedPlaylistCredentials = session.IsAdministrator
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -663,7 +672,8 @@ public sealed class PlaylistLinksController(
|
|||||||
private static object ToPlaylistSourceAccountDto(
|
private static object ToPlaylistSourceAccountDto(
|
||||||
ProviderAccountRecord account,
|
ProviderAccountRecord account,
|
||||||
bool available,
|
bool available,
|
||||||
string? reasonCode) => new
|
string? reasonCode,
|
||||||
|
bool administratorAccess = false) => new
|
||||||
{
|
{
|
||||||
id = account.Id,
|
id = account.Id,
|
||||||
providerId = account.ProviderId,
|
providerId = account.ProviderId,
|
||||||
@@ -674,6 +684,7 @@ public sealed class PlaylistLinksController(
|
|||||||
{
|
{
|
||||||
ProviderAccountScope.User => "Personal account",
|
ProviderAccountScope.User => "Personal account",
|
||||||
ProviderAccountScope.Library => "Library-shared account",
|
ProviderAccountScope.Library => "Library-shared account",
|
||||||
|
ProviderAccountScope.Global when administratorAccess => "Administrator account",
|
||||||
_ => "Deployment-shared account"
|
_ => "Deployment-shared account"
|
||||||
},
|
},
|
||||||
revision = account.Revision,
|
revision = account.Revision,
|
||||||
|
|||||||
@@ -572,11 +572,13 @@ public sealed class TrackMatchesController(
|
|||||||
private static string Hash(string value) =>
|
private static string Hash(string value) =>
|
||||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
|
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,
|
Title = value.Title,
|
||||||
Artist = value.Artist,
|
Artist = value.Artist,
|
||||||
Album = value.Album
|
Album = value.Album,
|
||||||
|
ArtworkUrl = value.ArtworkUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
private static MatchRow Row(ExternalMetadataSnapshotRecord snapshot, TrackMatchRecord? decision,
|
private static MatchRow Row(ExternalMetadataSnapshotRecord snapshot, TrackMatchRecord? decision,
|
||||||
@@ -609,6 +611,8 @@ public sealed class TrackMatchesController(
|
|||||||
title = metadata.Title,
|
title = metadata.Title,
|
||||||
artist = metadata.Artist,
|
artist = metadata.Artist,
|
||||||
album = metadata.Album,
|
album = metadata.Album,
|
||||||
|
artworkUrl = metadata.ArtworkUrl,
|
||||||
|
isrc = metadata.Isrc,
|
||||||
localTrack = track == null ? null : new { track.Id, track.BackendItemId, track.Title, track.Artist, track.Album },
|
localTrack = track == null ? null : new { track.Id, track.BackendItemId, track.Title, track.Artist, track.Album },
|
||||||
providerIdentities,
|
providerIdentities,
|
||||||
candidates = ParseCandidates(decision?.CandidateResultsJson),
|
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);
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -630,9 +634,15 @@ public sealed class TrackMatchesController(
|
|||||||
var artist = Text(root, "artist") ?? Text(root, "Artist");
|
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)
|
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));
|
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 &&
|
private static string? Text(JsonElement root, string name) => root.ValueKind == JsonValueKind.Object &&
|
||||||
|
|||||||
@@ -2165,18 +2165,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mapping-card-title > div {
|
.mapping-card-title > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.22rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-card-title strong {
|
.mapping-card-title strong {
|
||||||
font-size: 1.05rem;
|
font-size: 1.12rem;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapping-card-title p {
|
.mapping-card-title p {
|
||||||
margin: 0.2rem 0 0;
|
margin: 0;
|
||||||
color: var(--text-secondary);
|
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 {
|
.mapping-route {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 0.75fr) auto minmax(0, 1.25fr);
|
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 providerId = mapping.providerId || "provider";
|
||||||
const targetProvider = mapping.localTrack ? String(this.status?.backendType || "jellyfin").toLowerCase() : routes[0]?.providerId;
|
const targetProvider = mapping.localTrack ? String(this.status?.backendType || "jellyfin").toLowerCase() : routes[0]?.providerId;
|
||||||
const artwork = mapping.artworkUrl || mapping.ArtworkUrl;
|
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" : ""}">
|
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-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-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">
|
<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-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>
|
<span class="mapping-route-arrow" aria-hidden="true">${icon("chevronRight", 18)}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user