Skip to content

Commit 1801033

Browse files
Merge pull request #79 from Jamesllllllllll/codex/issue-72-lyrics-metadata
Stop treating lyrics/vocals as a playable path
2 parents fbfc82e + 8fe5a73 commit 1801033

27 files changed

+230
-83
lines changed

src/components/playlist-management-surface.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ import {
7373
import { getPickNumbersForQueuedItems } from "~/lib/pick-order";
7474
import {
7575
formatPlaylistItemSummaryLine,
76+
getPlaylistDisplayParts,
7677
getResolvedPlaylistCandidates,
78+
playlistDisplayCandidateHasLyrics,
79+
playlistDisplayItemHasLyrics,
7780
} from "~/lib/playlist/management-display";
7881
import {
7982
getPlaylistEndpoint,
@@ -121,6 +124,7 @@ export type PlaylistItem = {
121124
songCreator?: string;
122125
songTuning?: string;
123126
songPartsJson?: string;
127+
songHasLyrics?: boolean | null;
124128
songDurationText?: string;
125129
songUrl?: string;
126130
songSourceUpdatedAt?: number | null;
@@ -152,6 +156,7 @@ export type PlaylistCandidate = {
152156
creator?: string;
153157
tuning?: string;
154158
parts?: string[];
159+
hasLyrics?: boolean;
155160
durationText?: string;
156161
year?: number;
157162
sourceUpdatedAt?: number;
@@ -180,7 +185,8 @@ const PLAYLIST_PREVIEW_CANDIDATES: PlaylistCandidate[] = [
180185
album: "Neon Noir",
181186
creator: "JohnCryx",
182187
tuning: "E Standard | A Standard",
183-
parts: ["lead", "rhythm", "bass", "voice"],
188+
parts: ["lead", "rhythm", "bass"],
189+
hasLyrics: true,
184190
durationText: "3:49",
185191
sourceUpdatedAt: Date.parse("2025-12-08T00:00:00Z"),
186192
downloads: 4284,
@@ -217,7 +223,8 @@ const PLAYLIST_PREVIEW_ITEM: PlaylistItem = {
217223
songAlbum: "Neon Noir",
218224
songCreator: "JohnCryx",
219225
songTuning: "E Standard | A Standard",
220-
songPartsJson: JSON.stringify(["lead", "rhythm", "bass", "voice"]),
226+
songPartsJson: JSON.stringify(["lead", "rhythm", "bass"]),
227+
songHasLyrics: true,
221228
songDurationText: "3:49",
222229
songUrl: "https://customsforge.com/index.php?/customs/99081",
223230
songSourceUpdatedAt: Date.parse("2025-12-08T00:00:00Z"),
@@ -250,6 +257,7 @@ type SearchResponse = {
250257
creator?: string;
251258
tuning?: string;
252259
parts?: string[];
260+
hasLyrics?: boolean;
253261
durationText?: string;
254262
source: string;
255263
sourceUrl?: string;
@@ -974,6 +982,7 @@ export function PlaylistManagementSurface(
974982
const isBlacklistedCharter =
975983
song.authorId != null &&
976984
blacklistedCharterIds.has(song.authorId);
985+
const displaySongParts = getPlaylistDisplayParts(song.parts);
977986

978987
return (
979988
<div
@@ -1019,8 +1028,10 @@ export function PlaylistManagementSurface(
10191028
{song.tuning ?? t("management.manual.noTuningInfo")}
10201029
</p>
10211030
<p className="mt-1 truncate text-sm text-(--muted)">
1022-
{song.parts?.length
1023-
? song.parts.join(", ")
1031+
{displaySongParts.length > 0
1032+
? displaySongParts
1033+
.map((part) => formatPathLabel(part))
1034+
.join(", ")
10241035
: t("management.manual.noPathInfo")}
10251036
</p>
10261037
</div>
@@ -1057,6 +1068,7 @@ export function PlaylistManagementSurface(
10571068
creator: song.creator,
10581069
tuning: song.tuning,
10591070
parts: song.parts ?? [],
1071+
hasLyrics: song.hasLyrics,
10601072
durationText: song.durationText,
10611073
sourceUrl: song.sourceUrl,
10621074
sourceId: song.sourceId,
@@ -2267,6 +2279,7 @@ function PlaylistQueueItem(props: {
22672279
!hasMultipleVersions && resolvedCandidates[0]?.sourceUrl
22682280
? resolvedCandidates[0].sourceUrl
22692281
: null;
2282+
const itemHasLyrics = playlistDisplayItemHasLyrics(props.item);
22702283

22712284
useEffect(() => {
22722285
const element = itemRef.current;
@@ -2526,7 +2539,7 @@ function PlaylistQueueItem(props: {
25262539
unknownArtistLabel: t("management.manual.unknownArtist"),
25272540
})}
25282541
</p>
2529-
{itemDurationText || compactTuning ? (
2542+
{itemDurationText || compactTuning || itemHasLyrics ? (
25302543
<p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm text-(--muted)">
25312544
{itemDurationText ? (
25322545
<>
@@ -2540,6 +2553,12 @@ function PlaylistQueueItem(props: {
25402553
{compactTuning ? (
25412554
<span title={compactTuningTitle}>{compactTuning}</span>
25422555
) : null}
2556+
{(itemDurationText || compactTuning) && itemHasLyrics ? (
2557+
<span aria-hidden="true">·</span>
2558+
) : null}
2559+
{itemHasLyrics ? (
2560+
<span>{t("management.item.lyrics")}</span>
2561+
) : null}
25432562
</p>
25442563
) : null}
25452564
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
@@ -3118,6 +3137,8 @@ function PlaylistVersionsTable(props: {
31183137
const isBlacklistedCharter =
31193138
candidate.authorId != null &&
31203139
props.blacklistedCharterIds.has(candidate.authorId);
3140+
const displayParts = getPlaylistDisplayParts(candidate.parts);
3141+
const hasLyrics = playlistDisplayCandidateHasLyrics(candidate);
31213142

31223143
return (
31233144
<tr
@@ -3154,7 +3175,7 @@ function PlaylistVersionsTable(props: {
31543175
</td>
31553176
<td className="px-4 py-3">
31563177
<div className="flex flex-wrap gap-1">
3157-
{(candidate.parts ?? []).map((part) => (
3178+
{displayParts.map((part) => (
31583179
<span
31593180
key={`${candidate.id}-${part}`}
31603181
className={getPlaylistPathBadgeClass(part)}
@@ -3163,7 +3184,12 @@ function PlaylistVersionsTable(props: {
31633184
{getPathAbbreviation(part)}
31643185
</span>
31653186
))}
3166-
{(candidate.parts ?? []).length === 0 ? (
3187+
{hasLyrics ? (
3188+
<span className="inline-flex h-6 items-center justify-center border border-(--border-strong) bg-(--panel) px-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-(--muted)">
3189+
{t("management.versionsTable.lyrics")}
3190+
</span>
3191+
) : null}
3192+
{displayParts.length === 0 && !hasLyrics ? (
31673193
<span className="text-xs text-(--muted)">
31683194
{t("management.versionsTable.unknown")}
31693195
</span>
@@ -3243,10 +3269,6 @@ function getPathAbbreviation(path: string) {
32433269
return "R";
32443270
case "bass":
32453271
return "B";
3246-
case "lyrics":
3247-
case "voice":
3248-
case "vocals":
3249-
return "V";
32503272
default:
32513273
return path.slice(0, 1).toUpperCase();
32523274
}
@@ -3260,10 +3282,6 @@ function getPlaylistPathBadgeClass(path: string) {
32603282
return "inline-flex h-6 min-w-6 items-center justify-center border border-sky-700/50 bg-sky-950 px-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-sky-100";
32613283
case "bass":
32623284
return "inline-flex h-6 min-w-6 items-center justify-center border border-orange-700/50 bg-orange-950 px-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-orange-100";
3263-
case "lyrics":
3264-
case "voice":
3265-
case "vocals":
3266-
return "inline-flex h-6 min-w-6 items-center justify-center border border-violet-700/50 bg-violet-950 px-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-violet-100";
32673285
default:
32683286
return "inline-flex h-6 min-w-6 items-center justify-center border border-(--border) bg-(--panel-strong) px-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-(--text)";
32693287
}

src/components/song-search-panel.tsx

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
TooltipProvider,
5050
TooltipTrigger,
5151
} from "~/components/ui/tooltip";
52-
import { pathOptions } from "~/lib/channel-options";
52+
import { normalizePathOptions, pathOptions } from "~/lib/channel-options";
5353
import { useAppLocale, useLocaleTranslation } from "~/lib/i18n/client";
5454
import {
5555
formatCompactTuningSummary,
@@ -76,6 +76,7 @@ export interface SearchSong {
7676
downloads?: number;
7777
sourceUpdatedAt?: number;
7878
sourceId?: number;
79+
hasLyrics?: boolean;
7980
source: string;
8081
}
8182

@@ -204,9 +205,6 @@ export function SongSearchPanel(props: {
204205
return t("paths.rhythm");
205206
case "bass":
206207
return t("paths.bass");
207-
case "voice":
208-
case "vocals":
209-
return t("paths.lyrics");
210208
default:
211209
return path;
212210
}
@@ -1082,6 +1080,7 @@ export function SongSearchPanel(props: {
10821080
const compactTuning = formatCompactTuningSummary([
10831081
song.tuning,
10841082
]);
1083+
const displaySongPaths = normalizePathOptions(song.parts);
10851084
const tuningTitle =
10861085
compactTuning && song.tuning
10871086
? (() => {
@@ -1134,35 +1133,27 @@ export function SongSearchPanel(props: {
11341133

11351134
<div className="search-panel__paths min-w-0">
11361135
<div className="flex flex-wrap gap-2">
1137-
{song.parts?.includes("lead") ? (
1136+
{displaySongPaths.includes("lead") ? (
11381137
<PathBadge
11391138
label={t("paths.lead")}
11401139
shortLabel={getPathShortLabel("lead")}
11411140
className="border-emerald-700/50 bg-emerald-950 text-emerald-100 hover:bg-emerald-950"
11421141
/>
11431142
) : null}
1144-
{song.parts?.includes("rhythm") ? (
1143+
{displaySongPaths.includes("rhythm") ? (
11451144
<PathBadge
11461145
label={t("paths.rhythm")}
11471146
shortLabel={getPathShortLabel("rhythm")}
11481147
className="border-sky-700/50 bg-sky-950 text-sky-100 hover:bg-sky-950"
11491148
/>
11501149
) : null}
1151-
{song.parts?.includes("bass") ? (
1150+
{displaySongPaths.includes("bass") ? (
11521151
<PathBadge
11531152
label={t("paths.bass")}
11541153
shortLabel={getPathShortLabel("bass")}
11551154
className="border-orange-700/50 bg-orange-950 text-orange-100 hover:bg-orange-950"
11561155
/>
11571156
) : null}
1158-
{song.parts?.includes("voice") ||
1159-
song.parts?.includes("vocals") ? (
1160-
<PathBadge
1161-
label={t("paths.lyrics")}
1162-
shortLabel={getPathShortLabel("voice")}
1163-
className="border-violet-700/50 bg-violet-950 text-violet-100 hover:bg-violet-950"
1164-
/>
1165-
) : null}
11661157
</div>
11671158
</div>
11681159
</div>
@@ -1206,35 +1197,27 @@ export function SongSearchPanel(props: {
12061197

12071198
<div className="search-panel__paths min-w-0">
12081199
<div className="flex flex-wrap gap-2">
1209-
{song.parts?.includes("lead") ? (
1200+
{displaySongPaths.includes("lead") ? (
12101201
<PathBadge
12111202
label={t("paths.lead")}
12121203
shortLabel={getPathShortLabel("lead")}
12131204
className="border-emerald-700/50 bg-emerald-950 text-emerald-100 hover:bg-emerald-950"
12141205
/>
12151206
) : null}
1216-
{song.parts?.includes("rhythm") ? (
1207+
{displaySongPaths.includes("rhythm") ? (
12171208
<PathBadge
12181209
label={t("paths.rhythm")}
12191210
shortLabel={getPathShortLabel("rhythm")}
12201211
className="border-sky-700/50 bg-sky-950 text-sky-100 hover:bg-sky-950"
12211212
/>
12221213
) : null}
1223-
{song.parts?.includes("bass") ? (
1214+
{displaySongPaths.includes("bass") ? (
12241215
<PathBadge
12251216
label={t("paths.bass")}
12261217
shortLabel={getPathShortLabel("bass")}
12271218
className="border-orange-700/50 bg-orange-950 text-orange-100 hover:bg-orange-950"
12281219
/>
12291220
) : null}
1230-
{song.parts?.includes("voice") ||
1231-
song.parts?.includes("vocals") ? (
1232-
<PathBadge
1233-
label={t("paths.lyrics")}
1234-
shortLabel={getPathShortLabel("voice")}
1235-
className="border-violet-700/50 bg-violet-950 text-violet-100 hover:bg-violet-950"
1236-
/>
1237-
) : null}
12381221
</div>
12391222
</div>
12401223
</button>
@@ -1333,9 +1316,6 @@ function getPathToneByValue(value: string) {
13331316
return "border-sky-700/50 bg-sky-950 text-sky-100";
13341317
case "bass":
13351318
return "border-orange-700/50 bg-orange-950 text-orange-100";
1336-
case "voice":
1337-
case "vocals":
1338-
return "border-violet-700/50 bg-violet-950 text-violet-100";
13391319
default:
13401320
return "border-(--border-strong) bg-(--panel) text-(--text)";
13411321
}

src/extension/panel/app.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
useLocaleTranslation,
7070
} from "~/lib/i18n/client";
7171
import { type AppLocale, localeOptions } from "~/lib/i18n/locales";
72+
import { playlistDisplayItemHasLyrics } from "~/lib/playlist/management-display";
7273
import {
7374
getQueuedPositionsFromRegularOrder,
7475
getUpdatedPositionsAfterSetCurrent,
@@ -2654,6 +2655,15 @@ function PanelPlaylistRow(props: {
26542655
props.dropTargetState?.itemId === props.itemId
26552656
? props.dropTargetState.edge
26562657
: null;
2658+
const itemHasLyrics =
2659+
props.canManagePlaylist &&
2660+
playlistDisplayItemHasLyrics({
2661+
songHasLyrics:
2662+
typeof props.item.songHasLyrics === "boolean"
2663+
? props.item.songHasLyrics
2664+
: null,
2665+
songPartsJson: getString(props.item, "songPartsJson") ?? undefined,
2666+
});
26572667

26582668
useEffect(() => {
26592669
const element = itemRef.current;
@@ -2893,6 +2903,7 @@ function PanelPlaylistRow(props: {
28932903
</p>
28942904
{pickNumber != null ||
28952905
getPanelRequestedPathLabel(props.item) ||
2906+
itemHasLyrics ||
28962907
(isVipRequest && getPanelStoredVipTokenCost(props.item) > 1) ||
28972908
(!isVipRequest &&
28982909
getPanelStoredVipTokenCost(props.item) > 0) ? (
@@ -2905,6 +2916,11 @@ function PanelPlaylistRow(props: {
29052916
{getPanelRequestedPathLabel(props.item)}
29062917
</span>
29072918
) : null}
2919+
{itemHasLyrics ? (
2920+
<span className="inline-flex h-5 items-center border border-(--border-strong) bg-(--panel-soft) px-1.5 text-[9px] leading-none font-medium uppercase tracking-[0.12em] text-(--muted)">
2921+
{t("queue.lyrics")}
2922+
</span>
2923+
) : null}
29082924
{(isVipRequest &&
29092925
getPanelStoredVipTokenCost(props.item) > 1) ||
29102926
(!isVipRequest &&

src/lib/channel-options.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,48 @@ export const tuningOptions = [
3232
"Other",
3333
] as const;
3434

35-
export const pathOptions = ["lead", "rhythm", "bass", "voice"] as const;
35+
export const pathOptions = ["lead", "rhythm", "bass"] as const;
36+
37+
export type PathOption = (typeof pathOptions)[number];
38+
39+
function normalizePathValue(value: string | null | undefined) {
40+
return (value ?? "").trim().toLowerCase();
41+
}
42+
43+
export function normalizePathOptions(
44+
paths: Array<string | null | undefined> | null | undefined
45+
) {
46+
const normalized = new Set<PathOption>();
47+
48+
for (const path of paths ?? []) {
49+
const normalizedPath = normalizePathValue(path);
50+
if (pathOptions.includes(normalizedPath as PathOption)) {
51+
normalized.add(normalizedPath as PathOption);
52+
}
53+
}
54+
55+
return pathOptions.filter((path) => normalized.has(path));
56+
}
57+
58+
export function isLyricsPart(value: string | null | undefined) {
59+
switch (normalizePathValue(value)) {
60+
case "lyrics":
61+
case "voice":
62+
case "vocals":
63+
return true;
64+
default:
65+
return false;
66+
}
67+
}
68+
69+
export function hasLyricsMetadata(input: {
70+
hasLyrics?: boolean | null | undefined;
71+
hasVocals?: boolean | null | undefined;
72+
parts?: Array<string | null | undefined> | null | undefined;
73+
}) {
74+
if (input.hasLyrics || input.hasVocals) {
75+
return true;
76+
}
77+
78+
return (input.parts ?? []).some((part) => isLyricsPart(part));
79+
}

0 commit comments

Comments
 (0)