mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 14:19:40 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
8a88838300
commit
c1f632693d
9 changed files with 119 additions and 24 deletions
|
|
@ -19,6 +19,8 @@ export function createMockSong(overrides?: Partial<Song>): Song {
|
|||
genre: 'Rock',
|
||||
trackNumber: 1,
|
||||
year: 2024,
|
||||
month: null,
|
||||
day: null,
|
||||
duration: 240.5,
|
||||
storagePath: 'users/test-user-123/audio.mp3',
|
||||
coverArtPath: null,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {
|
||||
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, {
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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 @@
|
|||
<div>
|
||||
<label
|
||||
for="edit-year"
|
||||
class="block text-xs font-medium text-foreground-secondary mb-1">Year</label
|
||||
class="block text-xs font-medium text-foreground-secondary mb-1">Date</label
|
||||
>
|
||||
<input
|
||||
id="edit-year"
|
||||
type="text"
|
||||
bind:value={year}
|
||||
class="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Year"
|
||||
/>
|
||||
<div class="grid grid-cols-3 gap-1">
|
||||
<input
|
||||
id="edit-year"
|
||||
type="text"
|
||||
bind:value={year}
|
||||
class="w-full px-2 py-2 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Year"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={month}
|
||||
class="w-full px-2 py-2 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Mo"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={day}
|
||||
class="w-full px-2 py-2 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<div>
|
||||
<label
|
||||
for="year-{uf.id}"
|
||||
class="block text-xs font-medium text-foreground-secondary mb-1">Year</label
|
||||
class="block text-xs font-medium text-foreground-secondary mb-1">Date</label
|
||||
>
|
||||
<input
|
||||
id="year-{uf.id}"
|
||||
type="text"
|
||||
bind:value={uf.metadata.year}
|
||||
class="w-full px-3 py-1.5 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Year"
|
||||
/>
|
||||
<div class="grid grid-cols-3 gap-1">
|
||||
<input
|
||||
id="year-{uf.id}"
|
||||
type="text"
|
||||
bind:value={uf.metadata.year}
|
||||
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Year"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={uf.metadata.month}
|
||||
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Mo"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={uf.metadata.day}
|
||||
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded-lg focus:border-primary focus:outline-none"
|
||||
placeholder="Day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue