Skip to content

Commit ca4502d

Browse files
committed
add lyrics field
1 parent 2edc63e commit ca4502d

File tree

4 files changed

+105
-12
lines changed

4 files changed

+105
-12
lines changed

pages/music/validator.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { sumOf } from "@std/collections/sum-of";
22
import { PriFeaArtist, ProWriArtist } from "shared/helper.ts";
33
import { z } from "zod";
44
import { ArtistRef, zArtistRef, zSong } from "../../spec/mod.ts";
5+
import { formatTimedLyrics } from "./views/table.ts";
56

67
const pageOne = z.object({
78
title: z.string().min(1, { message: "Title is required" }).max(100, { message: "Title is too long" }),
@@ -57,10 +58,12 @@ export const pageThree = pageTwo.and(z.object({
5758
.refine((songs) => songs.every(({ artists }) => artists.filter<ProWriArtist>((artist): artist is ProWriArtist => artist.type === "SONGWRITER").every(({ name }) => name.split(" ").length > 1)), { message: "Songwriters must have a first and last name" })
5859
.refine((songs) => songs.every(({ artists }) => artists.filter<ProWriArtist>((artist): artist is ProWriArtist => artist.type === "PRODUCER").every(({ name }) => name.split(" ").length > 1)), { message: "Producers must have a first and last name" })
5960
.refine((songs) => songs.every(({ artists }) => artists.filter<ProWriArtist>((artist): artist is ProWriArtist => artist.type === "SONGWRITER").every(({ name }, index, arr) => arr.findIndex((y) => y.name === name) === index)), { message: "Duplicate Songwriter" })
60-
.refine((songs) => songs.every(({ artists }) => artists.filter<ProWriArtist>((artist): artist is ProWriArtist => artist.type === "PRODUCER").every(({ name }, index, arr) => arr.findIndex((y) => y.name === name) === index)), { message: "Duplicate Producer" }),
61+
.refine((songs) => songs.every(({ artists }) => artists.filter<ProWriArtist>((artist): artist is ProWriArtist => artist.type === "PRODUCER").every(({ name }, index, arr) => arr.findIndex((y) => y.name === name) === index)), { message: "Duplicate Producer" })
62+
.refine((songs) => songs.every(({ lyrics, timedLyrics }) => timedLyrics ? !!lyrics : true), { message: "Lyrics must be provided when Timed Lyrics are specified" })
63+
.refine((songs) => songs.every(({ timedLyrics }) => timedLyrics ? formatTimedLyrics(timedLyrics) === timedLyrics : true), { message: "Timed Lyrics are not properly formatted" }),
6164
uploadingSongs: z.array(z.string()).max(0, { message: "Some uploads are still in progress" }),
6265
}))
6366
.refine((object) => (object.songs.length === 1 && object.songs[0].title === object.title) || object.songs.length > 1, { message: "Drop Title and Song Title must be the same for single song drops", path: ["songs"] })
64-
.refine((object) => (object.songs.length === 1 && object.songs[ 0 ].artists.length === object.artists.length && object.artists.map(normalize).toSorted().every((v, i) => v === object.songs[ 0 ].artists.map(normalize).toSorted()[ i ]) || object.songs.length > 1), { message: "All artists must be the same for single song drops", path: [ "songs" ] })
67+
.refine((object) => (object.songs.length === 1 && object.songs[0].artists.length === object.artists.length && object.artists.map(normalize).toSorted().every((v, i) => v === object.songs[0].artists.map(normalize).toSorted()[i]) || object.songs.length > 1), { message: "All artists must be the same for single song drops", path: ["songs"] })
6568
.refine((object) => (object.songs.length === 1 && object.songs[0].secondaryGenre === object.secondaryGenre) || object.songs.length > 1, { message: "Drop Secondary Genre and Song Secondary Genre must be the same for single song drops", path: ["songs", "secondaryGenre"] });
66-
export const pages = <z.AnyZodObject[]> [pageOne, pageTwo, pageThree];
69+
export const pages = <z.ZodObject[]> [pageOne, pageTwo, pageThree];

pages/music/views/table.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Audio } from "shared/audio.ts";
2-
import { allowedAudioFormats, ExistingSongDialog, saveBlob, sheetStack } from "shared/helper.ts";
2+
import { allowedAudioFormats, ErrorMessage, ExistingSongDialog, saveBlob, sheetStack } from "shared/helper.ts";
33
import { placeholder } from "shared/list.ts";
4-
import { asRef, asRefRecord, Box, Checkbox, createFilePicker, DropDown, Empty, Entry, Grid, Label, List, MaterialIcon, PrimaryButton, ref, RefRecord, SecondaryButton, SheetHeader, TextInput, WriteSignal } from "webgen/mod.ts";
4+
import { asRef, asRefRecord, Box, Checkbox, createFilePicker, DropDown, Empty, Entry, Grid, Label, List, MaterialIcon, PrimaryButton, ref, RefRecord, SecondaryButton, SheetHeader, TextAreaInput, TextInput, WriteSignal } from "webgen/mod.ts";
55
import countries from "../../../data/countries.json" with { type: "json" };
66
import genres from "../../../data/genres.json" with { type: "json" };
77
import languages from "../../../data/language.json" with { type: "json" };
@@ -39,6 +39,50 @@ const songSheet = (song: RefRecord<Song>, save: (song: RefRecord<Song>) => void,
3939
).setTemplateColumns("max-content max-content").setGap().setJustifyItems("center"),
4040
TextInput(song.isrc ?? asRef(""), "ISRC (optional)").setDisabled(disabled),
4141
).setGap().setDynamicColumns(15),
42+
SecondaryButton("Lyrics").setDisabled(song.instrumental).onClick(() => {
43+
if (song.lyrics === undefined) {
44+
song.lyrics = asRef("");
45+
}
46+
if (song.timedLyrics === undefined) {
47+
song.timedLyrics = asRef("");
48+
}
49+
const errorState = asRef<string | undefined>(undefined);
50+
sheetStack.addSheet(
51+
Grid(
52+
SheetHeader("Edit Lyrics", sheetStack),
53+
TextAreaInput(song.lyrics ?? asRef(""), "Lyrics", "change").setDisabled(disabled),
54+
TextAreaInput(song.timedLyrics ?? asRef(""), "Optional Timed Lyrics (LRC Format)", "change").setDisabled(disabled),
55+
ErrorMessage(errorState),
56+
SecondaryButton("Format Timed Lyrics").onClick(() => {
57+
if (song.timedLyrics?.value === "" || song.timedLyrics?.value === undefined) return;
58+
const formatted = formatTimedLyrics(song.timedLyrics.value);
59+
if (formatted === null) {
60+
errorState.setValue("Timed lyrics format is incorrect.");
61+
} else {
62+
song.timedLyrics.set(formatted);
63+
errorState.setValue(undefined);
64+
}
65+
}),
66+
PrimaryButton("Save").setDisabled(disabled).onClick(() => {
67+
if (!(song.timedLyrics?.value === "" || song.timedLyrics?.value === undefined)) {
68+
const formatted = formatTimedLyrics(song.timedLyrics.value);
69+
if (formatted === null) {
70+
song.timedLyrics.set("");
71+
} else {
72+
song.timedLyrics.set(formatted);
73+
}
74+
}
75+
if (song.lyrics?.value === "") {
76+
song.lyrics = undefined;
77+
}
78+
if (song.timedLyrics?.value === "") {
79+
song.timedLyrics = undefined;
80+
}
81+
sheetStack.removeOne();
82+
}),
83+
).setGap(),
84+
);
85+
}),
4286
Box(blobRef.map((blob) =>
4387
blob === undefined
4488
? SecondaryButton("Listen to Song").onClick(() => {
@@ -66,6 +110,16 @@ const songSheet = (song: RefRecord<Song>, save: (song: RefRecord<Song>) => void,
66110
).setGap();
67111
};
68112

113+
export function formatTimedLyrics(lyrics: string): string | null {
114+
const regex = /\[([0-9]{1}|[0-9]{2})\:[0-9]{2}(\.[0-9]{2})?\]([\w:\s]+)/gm;
115+
const matches = lyrics.match(regex);
116+
if (matches === null) {
117+
return null;
118+
} else {
119+
return matches.join("");
120+
}
121+
}
122+
69123
export function ManageSongs(songs: WriteSignal<Song[]>, id: string, provided: WriteSignal<Artist[] | undefined>, disabled: WriteSignal<boolean> = asRef(false)) {
70124
function SongEntry(song: RefRecord<Song>) {
71125
return Grid(

spec/gen/types.gen.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export type Song = {
104104
explicit: boolean;
105105
instrumental: boolean;
106106
file: string;
107+
lyrics?: string;
108+
timedLyrics?: string;
107109
};
108110

109111
export type Drop = {
@@ -393,6 +395,8 @@ export type SingleAdminDrop = {
393395
explicit: boolean;
394396
instrumental: boolean;
395397
file: string;
398+
lyrics?: string;
399+
timedLyrics?: string;
396400
filename: string;
397401
}>;
398402
comments?: string;
@@ -520,6 +524,8 @@ export type SearchReturn = ({
520524
explicit?: boolean;
521525
instrumental?: boolean;
522526
file?: string;
527+
lyrics?: string;
528+
timedLyrics?: string;
523529
};
524530
} | {
525531
_index: 'users';
@@ -588,6 +594,8 @@ export type UpdateDrop = {
588594
language: string;
589595
explicit: boolean;
590596
instrumental: boolean;
597+
lyrics?: string;
598+
timedLyrics?: string;
591599
}>;
592600
comments?: string;
593601
type?: DropType;
@@ -654,6 +662,8 @@ export type GetIdByDropsByAdminResponses = {
654662
explicit: boolean;
655663
instrumental: boolean;
656664
file: string;
665+
lyrics?: string;
666+
timedLyrics?: string;
657667
filename: string;
658668
}>;
659669
comments?: string;
@@ -1170,6 +1180,8 @@ export type PostDropByDropsByMusicResponses = {
11701180
explicit: boolean;
11711181
instrumental: boolean;
11721182
file: string;
1183+
lyrics?: string;
1184+
timedLyrics?: string;
11731185
};
11741186
};
11751187

@@ -1262,6 +1274,8 @@ export type PatchIdByDropsByMusicData = {
12621274
language: string;
12631275
explicit: boolean;
12641276
instrumental: boolean;
1277+
lyrics?: string;
1278+
timedLyrics?: string;
12651279
}>;
12661280
comments?: string;
12671281
type?: DropType;
@@ -1436,6 +1450,8 @@ export type PostSongsByMusicData = {
14361450
explicit: boolean;
14371451
instrumental: boolean;
14381452
file: string;
1453+
lyrics?: string;
1454+
timedLyrics?: string;
14391455
};
14401456
path?: never;
14411457
query?: never;
@@ -1480,6 +1496,8 @@ export type GetIdBySongsByMusicResponses = {
14801496
explicit: boolean;
14811497
instrumental: boolean;
14821498
file: string;
1499+
lyrics?: string;
1500+
timedLyrics?: string;
14831501
};
14841502
};
14851503

spec/gen/zod.gen.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ export const zSong = z.object({
150150
language: z.string(),
151151
explicit: z.boolean(),
152152
instrumental: z.boolean(),
153-
file: z.string()
153+
file: z.string(),
154+
lyrics: z.optional(z.string()),
155+
timedLyrics: z.optional(z.string())
154156
});
155157

156158
export const zDrop = z.object({
@@ -478,6 +480,8 @@ export const zSingleAdminDrop = z.object({
478480
explicit: z.boolean(),
479481
instrumental: z.boolean(),
480482
file: z.string(),
483+
lyrics: z.optional(z.string()),
484+
timedLyrics: z.optional(z.string()),
481485
filename: z.string()
482486
}))),
483487
comments: z.optional(z.string()),
@@ -627,7 +631,9 @@ export const zSearchReturn = z.intersection(z.union([
627631
language: z.optional(z.string()),
628632
explicit: z.optional(z.boolean()),
629633
instrumental: z.optional(z.boolean()),
630-
file: z.optional(z.string())
634+
file: z.optional(z.string()),
635+
lyrics: z.optional(z.string()),
636+
timedLyrics: z.optional(z.string())
631637
})
632638
}),
633639
z.object({
@@ -705,7 +711,9 @@ export const zUpdateDrop = z.object({
705711
country: z.optional(z.string()),
706712
language: z.string(),
707713
explicit: z.boolean(),
708-
instrumental: z.boolean()
714+
instrumental: z.boolean(),
715+
lyrics: z.optional(z.string()),
716+
timedLyrics: z.optional(z.string())
709717
}))),
710718
comments: z.optional(z.string()),
711719
type: z.optional(zDropType)
@@ -765,6 +773,8 @@ export const zGetIdByDropsByAdminResponse = z.object({
765773
explicit: z.boolean(),
766774
instrumental: z.boolean(),
767775
file: z.string(),
776+
lyrics: z.optional(z.string()),
777+
timedLyrics: z.optional(z.string()),
768778
filename: z.string()
769779
}))),
770780
comments: z.optional(z.string()),
@@ -1199,7 +1209,9 @@ export const zPostDropByDropsByMusicResponse = z.object({
11991209
language: z.string(),
12001210
explicit: z.boolean(),
12011211
instrumental: z.boolean(),
1202-
file: z.string()
1212+
file: z.string(),
1213+
lyrics: z.optional(z.string()),
1214+
timedLyrics: z.optional(z.string())
12031215
});
12041216

12051217
export const zGetDownloadByDropByDropsByMusicData = z.object({
@@ -1277,7 +1289,9 @@ export const zPatchIdByDropsByMusicData = z.object({
12771289
country: z.optional(z.string()),
12781290
language: z.string(),
12791291
explicit: z.boolean(),
1280-
instrumental: z.boolean()
1292+
instrumental: z.boolean(),
1293+
lyrics: z.optional(z.string()),
1294+
timedLyrics: z.optional(z.string())
12811295
}))),
12821296
comments: z.optional(z.string()),
12831297
type: z.optional(zDropType)
@@ -1414,7 +1428,9 @@ export const zPostSongsByMusicData = z.object({
14141428
language: z.string(),
14151429
explicit: z.boolean(),
14161430
instrumental: z.boolean(),
1417-
file: z.string()
1431+
file: z.string(),
1432+
lyrics: z.optional(z.string()),
1433+
timedLyrics: z.optional(z.string())
14181434
})),
14191435
path: z.optional(z.never()),
14201436
query: z.optional(z.any())
@@ -1451,7 +1467,9 @@ export const zGetIdBySongsByMusicResponse = z.object({
14511467
language: z.string(),
14521468
explicit: z.boolean(),
14531469
instrumental: z.boolean(),
1454-
file: z.string()
1470+
file: z.string(),
1471+
lyrics: z.optional(z.string()),
1472+
timedLyrics: z.optional(z.string())
14551473
});
14561474

14571475
export const zGetDownloadBySongBySongsByMusicData = z.object({

0 commit comments

Comments
 (0)