From c1f632693d25dd4194ebbcadd3dff7fd1b498c4d Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 19 Mar 2026 15:31:50 +0100 Subject: [PATCH] feat(mukke): add month/day fields and auto-set date from file timestamp Adds month and day columns to songs schema. On upload, extracts the file's lastModified date to pre-fill year/month/day. Upload form and SongEditor now show all three date fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/__tests__/utils/mock-factories.ts | 2 + .../backend/src/db/schema/songs.schema.ts | 2 + .../apps/backend/src/song/dto/song.dto.ts | 28 ++++++++++++++ .../apps/backend/src/song/song.controller.ts | 6 ++- .../apps/backend/src/song/song.service.ts | 22 ++++++++--- .../web/src/lib/components/SongEditor.svelte | 36 ++++++++++++++---- .../apps/web/src/lib/stores/library.svelte.ts | 5 ++- .../web/src/routes/(app)/upload/+page.svelte | 38 +++++++++++++++---- apps/mukke/packages/shared/src/types/song.ts | 4 ++ 9 files changed, 119 insertions(+), 24 deletions(-) diff --git a/apps/mukke/apps/backend/src/__tests__/utils/mock-factories.ts b/apps/mukke/apps/backend/src/__tests__/utils/mock-factories.ts index d49e4cef4..36141ccc8 100644 --- a/apps/mukke/apps/backend/src/__tests__/utils/mock-factories.ts +++ b/apps/mukke/apps/backend/src/__tests__/utils/mock-factories.ts @@ -19,6 +19,8 @@ export function createMockSong(overrides?: Partial): Song { genre: 'Rock', trackNumber: 1, year: 2024, + month: null, + day: null, duration: 240.5, storagePath: 'users/test-user-123/audio.mp3', coverArtPath: null, diff --git a/apps/mukke/apps/backend/src/db/schema/songs.schema.ts b/apps/mukke/apps/backend/src/db/schema/songs.schema.ts index 1cd85e3ee..02fad2c84 100644 --- a/apps/mukke/apps/backend/src/db/schema/songs.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/songs.schema.ts @@ -22,6 +22,8 @@ export const songs = pgTable( genre: varchar('genre', { length: 100 }), trackNumber: integer('track_number'), year: integer('year'), + month: integer('month'), + day: integer('day'), duration: real('duration'), storagePath: text('storage_path').notNull(), coverArtPath: text('cover_art_path'), diff --git a/apps/mukke/apps/backend/src/song/dto/song.dto.ts b/apps/mukke/apps/backend/src/song/dto/song.dto.ts index 781b53e10..d9a96b8bb 100644 --- a/apps/mukke/apps/backend/src/song/dto/song.dto.ts +++ b/apps/mukke/apps/backend/src/song/dto/song.dto.ts @@ -39,6 +39,18 @@ export class CreateSongDto { @IsOptional() year?: number; + @IsInt() + @IsOptional() + @Min(1) + @Max(12) + month?: number; + + @IsInt() + @IsOptional() + @Min(1) + @Max(31) + day?: number; + @IsNumber() @IsOptional() @Min(1) @@ -81,6 +93,18 @@ export class UpdateSongDto { @IsOptional() year?: number; + @IsInt() + @IsOptional() + @Min(1) + @Max(12) + month?: number; + + @IsInt() + @IsOptional() + @Min(1) + @Max(31) + day?: number; + @IsNumber() @IsOptional() @Min(0) @@ -103,4 +127,8 @@ export class SongUploadDto { @IsNotEmpty() @MaxLength(255) filename!: string; + + @IsNumber() + @IsOptional() + fileLastModified?: number; } diff --git a/apps/mukke/apps/backend/src/song/song.controller.ts b/apps/mukke/apps/backend/src/song/song.controller.ts index 096853d16..939d27591 100644 --- a/apps/mukke/apps/backend/src/song/song.controller.ts +++ b/apps/mukke/apps/backend/src/song/song.controller.ts @@ -21,7 +21,11 @@ export class SongController { @Post('upload') async createUploadUrl(@CurrentUser() user: CurrentUserData, @Body() dto: SongUploadDto) { - const result = await this.songService.createUploadUrl(user.userId, dto.filename); + const result = await this.songService.createUploadUrl( + user.userId, + dto.filename, + dto.fileLastModified + ); return { song: result.song, uploadUrl: result.uploadUrl }; } diff --git a/apps/mukke/apps/backend/src/song/song.service.ts b/apps/mukke/apps/backend/src/song/song.service.ts index f8a79b8af..c6c886fed 100644 --- a/apps/mukke/apps/backend/src/song/song.service.ts +++ b/apps/mukke/apps/backend/src/song/song.service.ts @@ -23,7 +23,8 @@ export class SongService { async createUploadUrl( userId: string, - filename: string + filename: string, + fileLastModified?: number ): Promise<{ song: Song; uploadUrl: string }> { const key = generateUserFileKey(userId, filename); const contentType = getContentType(filename); @@ -32,13 +33,22 @@ export class SongService { throw new BadRequestException('Invalid file type. Only audio files are allowed.'); } + const values: Record = { + userId, + title: filename.replace(/\.[^/.]+$/, ''), + storagePath: key, + }; + + if (fileLastModified) { + const date = new Date(fileLastModified); + values.year = date.getFullYear(); + values.month = date.getMonth() + 1; + values.day = date.getDate(); + } + const [song] = await this.db .insert(songs) - .values({ - userId, - title: filename.replace(/\.[^/.]+$/, ''), - storagePath: key, - }) + .values(values as typeof songs.$inferInsert) .returning(); const uploadUrl = await this.storage.getUploadUrl(key, { diff --git a/apps/mukke/apps/web/src/lib/components/SongEditor.svelte b/apps/mukke/apps/web/src/lib/components/SongEditor.svelte index 80f56bcef..7e08aa3f0 100644 --- a/apps/mukke/apps/web/src/lib/components/SongEditor.svelte +++ b/apps/mukke/apps/web/src/lib/components/SongEditor.svelte @@ -17,6 +17,8 @@ let genre = $state(''); let trackNumber = $state(''); let year = $state(''); + let month = $state(''); + let day = $state(''); let bpm = $state(''); let coverUrl = $state(null); let saving = $state(false); @@ -35,6 +37,8 @@ genre = song.genre ?? ''; trackNumber = song.trackNumber ? String(song.trackNumber) : ''; year = song.year ? String(song.year) : ''; + month = song.month ? String(song.month) : ''; + day = song.day ? String(song.day) : ''; bpm = song.bpm ? String(song.bpm) : ''; error = null; success = null; @@ -66,6 +70,8 @@ genre: genre || undefined, trackNumber: trackNumber ? parseInt(trackNumber) : undefined, year: year ? parseInt(year) : undefined, + month: month ? parseInt(month) : undefined, + day: day ? parseInt(day) : undefined, bpm: bpm ? parseFloat(bpm) : undefined, }); success = 'Metadata saved'; @@ -211,15 +217,29 @@
Date - +
+ + + +
diff --git a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts index e200c7fe6..0f09ccf45 100644 --- a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts @@ -229,7 +229,10 @@ function createLibraryStore() { async uploadSong(file: File) { const uploadData = await fetchApi<{ song: Song; uploadUrl: string }>('/songs/upload', { method: 'POST', - body: JSON.stringify({ filename: file.name }), + body: JSON.stringify({ + filename: file.name, + fileLastModified: file.lastModified || undefined, + }), }); await fetch(uploadData.uploadUrl, { diff --git a/apps/mukke/apps/web/src/routes/(app)/upload/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/upload/+page.svelte index 197c3bcae..055d39957 100644 --- a/apps/mukke/apps/web/src/routes/(app)/upload/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/upload/+page.svelte @@ -14,6 +14,8 @@ album: string; genre: string; year: string; + month: string; + day: string; }; } @@ -65,6 +67,8 @@ album: '', genre: '', year: '', + month: '', + day: '', }, })); files = [...files, ...newFiles]; @@ -95,6 +99,8 @@ if (extracted.album) files[index].metadata.album = extracted.album ?? ''; if (extracted.genre) files[index].metadata.genre = extracted.genre ?? ''; if (extracted.year) files[index].metadata.year = String(extracted.year); + if (extracted.month) files[index].metadata.month = String(extracted.month); + if (extracted.day) files[index].metadata.day = String(extracted.day); } catch { // Non-fatal: user can still edit metadata manually } @@ -115,6 +121,8 @@ album: uf.metadata.album || undefined, genre: uf.metadata.genre || undefined, year: uf.metadata.year ? parseInt(uf.metadata.year) : undefined, + month: uf.metadata.month ? parseInt(uf.metadata.month) : undefined, + day: uf.metadata.day ? parseInt(uf.metadata.day) : undefined, }; await libraryStore.updateSongMetadata(uf.songId, data); } catch (e) { @@ -307,15 +315,29 @@
Date - +
+ + + +
diff --git a/apps/mukke/packages/shared/src/types/song.ts b/apps/mukke/packages/shared/src/types/song.ts index 23f933456..ab39ed706 100644 --- a/apps/mukke/packages/shared/src/types/song.ts +++ b/apps/mukke/packages/shared/src/types/song.ts @@ -8,6 +8,8 @@ export interface Song { genre: string | null; trackNumber: number | null; year: number | null; + month: number | null; + day: number | null; duration: number | null; storagePath: string; coverArtPath: string | null; @@ -59,6 +61,8 @@ export interface CreateSongDto { genre?: string; trackNumber?: number; year?: number; + month?: number; + day?: number; bpm?: number; }