diff --git a/db/update_db.js b/db/update_db.js index 21dfe6c..d5a3429 100644 --- a/db/update_db.js +++ b/db/update_db.js @@ -52,8 +52,11 @@ for (const file of files) { // Prepare attributes for insertion const fields = Object.entries(jsonData) .filter(([key]) => key in lookup) - // Serialize value if array - .map(([key, value]) => [lookup[key], Array.isArray(value) ? JSON.stringify(value) : value]); + // Serialize value if array or object + .map(([key, value]) => [ + lookup[key], + typeof value === 'object' && value !== null ? JSON.stringify(value) : value + ]); const row = existingRow.get(jsonData.id); if (row) { diff --git a/src/lib/components/WeightedTags.svelte b/src/lib/components/WeightedTags.svelte new file mode 100644 index 0000000..e4ba80e --- /dev/null +++ b/src/lib/components/WeightedTags.svelte @@ -0,0 +1,33 @@ + + +{#each sorted as [tag, weight], i}{i ? ', ' : ''}{tag}{/each} + + diff --git a/src/lib/database.ts b/src/lib/database.ts index 6b84f4c..e55b3c0 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -18,7 +18,7 @@ export function getStreams(): StreamSummary[] { stream_date: Date.parse(stream.stream_date as string), filename: stream.filename as string, title: stream.title as string | null, - tags: (JSON.parse(stream.tags as string) as string[]) ?? [], + tags: stream.tags ? JSON.parse(stream.tags as string) : {}, length_seconds: stream.length_seconds as number })); } @@ -40,7 +40,7 @@ export function getStreamInfo(streamId: string): Stream | null { format: streamData.format as string, title: streamData.title as string | null, description: streamData.description as string | null, - tags: (JSON.parse(streamData.tags as string) as string[]) ?? [], + tags: streamData.tags ? JSON.parse(streamData.tags as string) : {}, length_seconds: streamData.length_seconds as number, tracks: JSON.parse(streamData.tracks as string) }; diff --git a/src/lib/stores.svelte.ts b/src/lib/stores.svelte.ts index 37eb65d..37840a0 100644 --- a/src/lib/stores.svelte.ts +++ b/src/lib/stores.svelte.ts @@ -8,6 +8,8 @@ $effect.root(() => { }); }); +export const MAX_TAG_WEIGHT = 3; + export const tagList = [ 'acoustic', 'electronic', diff --git a/src/lib/types.ts b/src/lib/types.ts index ee55d9f..fdc06d2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,13 +1,16 @@ /** A track entry: [timestamp_seconds, artist, title] */ export type Track = [number, string, string]; +// tag -> weight, 1 (light) to 3 (heavy) +export type TagWeights = Record; + /** The summary shape used in sidebar listings. */ export interface StreamSummary { id: string; stream_date: number; filename: string; title: string | null; - tags: string[]; + tags: TagWeights; length_seconds: number; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 79aafce..73e7626 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -8,6 +8,10 @@ diff --git a/src/routes/streams/[stream_id]/+page.server.ts b/src/routes/streams/[stream_id]/+page.server.ts index aec7384..f89ded7 100644 --- a/src/routes/streams/[stream_id]/+page.server.ts +++ b/src/routes/streams/[stream_id]/+page.server.ts @@ -5,6 +5,7 @@ import { getStreamInfo } from '$lib/database.ts'; import { dev } from '$app/environment'; import { STREAM_JSON_LOCATION } from '$env/static/private'; import type { Actions } from './$types'; +import type { TagWeights } from '$lib/types'; type SideloadJson = { title: unknown; description: unknown; tags: unknown }; @@ -22,7 +23,7 @@ if (dev) { const jsonString = fs.readFileSync(filePath, 'utf8'); return jsonString; } else { - return JSON.stringify({ title: '', description: '', tags: [] }); + return JSON.stringify({ title: '', description: '', tags: {} }); } }; @@ -34,10 +35,12 @@ if (dev) { actions = { default: async ({ params, request }) => { const data = await request.formData(); + const weights = JSON.parse(String(data.get('tags'))) as TagWeights; const newJson: SideloadJson = { title: data.get('title'), description: data.get('description'), - tags: data.getAll('tags') + // weight 0 means the tag is off + tags: Object.fromEntries(Object.entries(weights).filter(([, weight]) => weight > 0)) }; writeSideloadJson(params.stream_id, newJson); } diff --git a/src/routes/streams/[stream_id]/MetadataEditor.svelte b/src/routes/streams/[stream_id]/MetadataEditor.svelte index 63a29bb..049bbb0 100644 --- a/src/routes/streams/[stream_id]/MetadataEditor.svelte +++ b/src/routes/streams/[stream_id]/MetadataEditor.svelte @@ -1,24 +1,20 @@ -{#key $page.url.pathname} +{#key page.url.pathname} @@ -36,17 +32,23 @@ > - + Tags: - + + {#each tagList as tag} - - - {tag} - + {@const weight = weights[tag] ?? 0} + cycle(tag, e.shiftKey)} + > + {tag}{'·'.repeat(weight)} + {/each} - + Submit @@ -58,19 +60,54 @@ margin-left: 10px; } - label { + label, + .tags { display: flex; align-items: center; } - label > p { + label > p, + .tags > p { margin-right: 5px; } - .checkboxContainer { + .tag-pills { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; max-width: 50%; + padding: 4px 8px; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + } + + .tag-pill { + background: none; + border: none; + padding: 2px 0; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; + user-select: none; + } + + .tag-pill.off { + opacity: 0.35; + } + + .tag-pill:hover { + color: var(--accent); + } + + .tag-pill.off:hover { + opacity: 0.65; + color: inherit; + } + + .dots { + margin-left: 2px; + color: var(--accent); + font-weight: bold; } diff --git a/src/routes/streams/[stream_id]/StreamPage.svelte b/src/routes/streams/[stream_id]/StreamPage.svelte index c1e5f40..89b50f0 100644 --- a/src/routes/streams/[stream_id]/StreamPage.svelte +++ b/src/routes/streams/[stream_id]/StreamPage.svelte @@ -2,6 +2,7 @@ import { getStreamContext } from '$lib/streamContext.svelte.ts'; import { shorthandCode, formatTrackTime, formatDate } from '$lib/utils.ts'; import { jumpToTrack } from './Player.svelte'; + import WeightedTags from '$lib/components/WeightedTags.svelte'; const ctx = getStreamContext(); @@ -18,7 +19,7 @@ ID: {shorthandCode(ctx.current.id)} {formattedStreamDate} - Tags: {ctx.current.tags.join(', ')} + Tags:
Tags: