Refactor metadata handling: remove copyright fields from various services and components

This commit is contained in:
2025-11-06 04:21:24 +11:00
parent fd85181983
commit c5185f2c70
8 changed files with 41 additions and 157 deletions

View File

@@ -29,7 +29,7 @@ export class LibraryService {
private readonly metadataFailures = new Set<string>(); private readonly metadataFailures = new Set<string>();
private readonly checksumFailures = new Set<string>(); private readonly checksumFailures = new Set<string>();
private readonly organization: OrganizationService; private readonly organization: OrganizationService;
private metadataSuggestionCache: { authors: Set<string>; copyrights: Set<string> } | null = null; private metadataSuggestionCache: { authors: Set<string> } | null = null;
private readonly waveformPreviewCache = new Map<number, { modifiedAt: number; pointCount: number; samples: number[]; rms: number }>(); private readonly waveformPreviewCache = new Map<number, { modifiedAt: number; pointCount: number; samples: number[]; rms: number }>();
public constructor( public constructor(
@@ -378,7 +378,6 @@ export class LibraryService {
return this.organizeFile(payload.fileId, { return this.organizeFile(payload.fileId, {
customName: updated.customName ?? undefined, customName: updated.customName ?? undefined,
author: metadataSnapshot.author?.trim() || undefined, author: metadataSnapshot.author?.trim() || undefined,
copyright: metadataSnapshot.copyright?.trim() || undefined,
rating: metadataSnapshot.rating ?? undefined rating: metadataSnapshot.rating ?? undefined
}); });
} }
@@ -435,7 +434,7 @@ export class LibraryService {
*/ */
public async organizeFile( public async organizeFile(
fileId: number, fileId: number,
metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number } metadata: { customName?: string | null; author?: string | null; rating?: number }
): Promise<AudioFileSummary> { ): Promise<AudioFileSummary> {
const record = this.database.getFileById(fileId); const record = this.database.getFileById(fileId);
const category = this.organization.getPrimaryCategory(record); const category = this.organization.getPrimaryCategory(record);
@@ -503,9 +502,7 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided) // Merge with new metadata (only update fields that are provided)
const requestedAuthor = this.normaliseMetadataInput(metadata.author); const requestedAuthor = this.normaliseMetadataInput(metadata.author);
const requestedCopyright = this.normaliseMetadataInput(metadata.copyright);
const mergedAuthor = requestedAuthor !== undefined ? requestedAuthor : existing.author ?? null; const mergedAuthor = requestedAuthor !== undefined ? requestedAuthor : existing.author ?? null;
const mergedCopyright = requestedCopyright !== undefined ? requestedCopyright : existing.copyright ?? null;
const mergedRating = metadata.rating !== undefined ? metadata.rating : existing.rating; const mergedRating = metadata.rating !== undefined ? metadata.rating : existing.rating;
this.tagService.writeMetadataOnly(updatedRecord.absolutePath, { this.tagService.writeMetadataOnly(updatedRecord.absolutePath, {
@@ -513,16 +510,12 @@ export class LibraryService {
categories: updatedRecord.categories, categories: updatedRecord.categories,
title: effectiveCustomName, title: effectiveCustomName,
author: mergedAuthor, author: mergedAuthor,
copyright: mergedCopyright,
rating: mergedRating rating: mergedRating
}); });
// Update suggestions cache for any metadata that was provided // Update suggestions cache for any metadata that was provided
if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) { if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) {
this.updateMetadataSuggestionsCache(mergedAuthor, undefined); this.updateMetadataSuggestionsCache(mergedAuthor);
}
if (typeof mergedCopyright === 'string' && mergedCopyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedCopyright);
} }
this.resetMetadataSuggestionsCache(); this.resetMetadataSuggestionsCache();
@@ -602,9 +595,7 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided) // Merge with new metadata (only update fields that are provided)
const movedAuthor = this.normaliseMetadataInput(metadata.author); const movedAuthor = this.normaliseMetadataInput(metadata.author);
const movedCopyright = this.normaliseMetadataInput(metadata.copyright);
const mergedAuthor = movedAuthor !== undefined ? movedAuthor : existing.author ?? null; const mergedAuthor = movedAuthor !== undefined ? movedAuthor : existing.author ?? null;
const mergedCopyright = movedCopyright !== undefined ? movedCopyright : existing.copyright ?? null;
const mergedRating = metadata.rating !== undefined ? metadata.rating : existing.rating; const mergedRating = metadata.rating !== undefined ? metadata.rating : existing.rating;
// Write tags, categories, and additional metadata to the organized file // Write tags, categories, and additional metadata to the organized file
@@ -613,16 +604,12 @@ export class LibraryService {
categories: updated.categories, categories: updated.categories,
title: effectiveCustomName, title: effectiveCustomName,
author: mergedAuthor, author: mergedAuthor,
copyright: mergedCopyright,
rating: mergedRating rating: mergedRating
}); });
// Update suggestions cache for any metadata that was provided // Update suggestions cache for any metadata that was provided
if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) { if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) {
this.updateMetadataSuggestionsCache(mergedAuthor, undefined); this.updateMetadataSuggestionsCache(mergedAuthor);
}
if (typeof mergedCopyright === 'string' && mergedCopyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedCopyright);
} }
this.resetMetadataSuggestionsCache(); this.resetMetadataSuggestionsCache();
@@ -666,20 +653,20 @@ export class LibraryService {
} }
/** /**
* Reads embedded metadata from a WAV file and returns author, copyright, title, and rating. * Reads embedded metadata from a WAV file and returns author, title, and rating.
*/ */
public async readFileMetadata(fileId: number): Promise<{ author?: string; copyright?: string; title?: string; rating?: number }> { public async readFileMetadata(fileId: number): Promise<{ author?: string; title?: string; rating?: number }> {
const record = this.database.getFileById(fileId); const record = this.database.getFileById(fileId);
const metadata = this.tagService.readMetadata(record.absolutePath); const metadata = this.tagService.readMetadata(record.absolutePath);
this.updateMetadataSuggestionsCache(metadata.author, metadata.copyright); this.updateMetadataSuggestionsCache(metadata.author);
return metadata; return metadata;
} }
/** /**
* Updates metadata (author, copyright, rating) without organizing the file. * Updates metadata (author, rating) without organizing the file.
* Only writes to the WAV INFO chunk, doesn't move or rename the file. * Only writes to the WAV INFO chunk, doesn't move or rename the file.
*/ */
public async updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void> { public async updateFileMetadata(fileId: number, metadata: { author?: string | null; rating?: number }): Promise<void> {
const record = this.database.getFileById(fileId); const record = this.database.getFileById(fileId);
// Read existing metadata from the WAV file // Read existing metadata from the WAV file
@@ -687,13 +674,11 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided) // Merge with new metadata (only update fields that are provided)
const requestedAuthor = this.normaliseMetadataInput(metadata.author); const requestedAuthor = this.normaliseMetadataInput(metadata.author);
const requestedCopyright = this.normaliseMetadataInput(metadata.copyright);
const mergedMetadata = { const mergedMetadata = {
tags: record.tags, tags: record.tags,
categories: record.categories, categories: record.categories,
title: record.customName ?? existing.title, title: record.customName ?? existing.title,
author: requestedAuthor !== undefined ? requestedAuthor : existing.author ?? null, author: requestedAuthor !== undefined ? requestedAuthor : existing.author ?? null,
copyright: requestedCopyright !== undefined ? requestedCopyright : existing.copyright ?? null,
rating: metadata.rating !== undefined ? metadata.rating : existing.rating rating: metadata.rating !== undefined ? metadata.rating : existing.rating
}; };
@@ -702,10 +687,7 @@ export class LibraryService {
// Update suggestions cache // Update suggestions cache
if (typeof mergedMetadata.author === 'string' && mergedMetadata.author.length > 0) { if (typeof mergedMetadata.author === 'string' && mergedMetadata.author.length > 0) {
this.updateMetadataSuggestionsCache(mergedMetadata.author, undefined); this.updateMetadataSuggestionsCache(mergedMetadata.author);
}
if (typeof mergedMetadata.copyright === 'string' && mergedMetadata.copyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedMetadata.copyright);
} }
this.waveformPreviewCache.delete(fileId); this.waveformPreviewCache.delete(fileId);
@@ -715,10 +697,9 @@ export class LibraryService {
/** /**
* Returns distinct metadata suggestions aggregated from the library. * Returns distinct metadata suggestions aggregated from the library.
*/ */
public async listMetadataSuggestions(): Promise<{ authors: string[]; copyrights: string[] }> { public async listMetadataSuggestions(): Promise<{ authors: string[] }> {
if (!this.metadataSuggestionCache) { if (!this.metadataSuggestionCache) {
const authors = new Set<string>(); const authors = new Set<string>();
const copyrights = new Set<string>();
const files = this.database.listFiles(); const files = this.database.listFiles();
for (const file of files) { for (const file of files) {
@@ -727,9 +708,6 @@ export class LibraryService {
if (metadata.author) { if (metadata.author) {
this.addSuggestionValue(authors, metadata.author); this.addSuggestionValue(authors, metadata.author);
} }
if (metadata.copyright) {
this.addSuggestionValue(copyrights, metadata.copyright);
}
} catch (error) { } catch (error) {
if (!this.metadataFailures.has(file.absolutePath)) { if (!this.metadataFailures.has(file.absolutePath)) {
this.metadataFailures.add(file.absolutePath); this.metadataFailures.add(file.absolutePath);
@@ -739,12 +717,11 @@ export class LibraryService {
} }
} }
this.metadataSuggestionCache = { authors, copyrights }; this.metadataSuggestionCache = { authors };
} }
return { return {
authors: Array.from(this.metadataSuggestionCache.authors).sort((a, b) => a.localeCompare(b)), authors: Array.from(this.metadataSuggestionCache.authors).sort((a, b) => a.localeCompare(b))
copyrights: Array.from(this.metadataSuggestionCache.copyrights).sort((a, b) => a.localeCompare(b))
}; };
} }
@@ -769,16 +746,13 @@ export class LibraryService {
return trimmed.length > 0 ? trimmed : null; return trimmed.length > 0 ? trimmed : null;
} }
private updateMetadataSuggestionsCache(author?: string | null, copyright?: string | null): void { private updateMetadataSuggestionsCache(author?: string | null): void {
if (!this.metadataSuggestionCache) { if (!this.metadataSuggestionCache) {
return; return;
} }
if (author) { if (author) {
this.addSuggestionValue(this.metadataSuggestionCache.authors, author); this.addSuggestionValue(this.metadataSuggestionCache.authors, author);
} }
if (copyright) {
this.addSuggestionValue(this.metadataSuggestionCache.copyrights, copyright);
}
} }
/** /**

View File

@@ -42,7 +42,6 @@ export class TagService {
*/ */
public readMetadata(filePath: string): { public readMetadata(filePath: string): {
author?: string; author?: string;
copyright?: string;
title?: string; title?: string;
rating?: number; rating?: number;
} { } {
@@ -58,7 +57,6 @@ export class TagService {
return { return {
author: tags.IART?.trim() || undefined, author: tags.IART?.trim() || undefined,
copyright: tags.ICOP?.trim() || undefined,
title: tags.INAM?.trim() || undefined, title: tags.INAM?.trim() || undefined,
rating rating
}; };
@@ -79,7 +77,6 @@ export class TagService {
categories: string[]; categories: string[];
title?: string | null; title?: string | null;
author?: string | null; author?: string | null;
copyright?: string | null;
rating?: number; rating?: number;
} }
): void { ): void {
@@ -96,7 +93,6 @@ export class TagService {
categories: string[]; categories: string[];
title?: string | null; title?: string | null;
author?: string | null; author?: string | null;
copyright?: string | null;
rating?: number; rating?: number;
} }
): void { ): void {
@@ -123,7 +119,6 @@ export class TagService {
IGNR: null, IGNR: null,
INAM: metadata.title?.trim()?.length ? metadata.title.trim() : null, INAM: metadata.title?.trim()?.length ? metadata.title.trim() : null,
IART: metadata.author?.trim()?.length ? metadata.author.trim() : null, IART: metadata.author?.trim()?.length ? metadata.author.trim() : null,
ICOP: metadata.copyright?.trim()?.length ? metadata.copyright.trim() : null,
IRTD: metadata.rating && metadata.rating > 0 ? String(metadata.rating * 2) : null, IRTD: metadata.rating && metadata.rating > 0 ? String(metadata.rating * 2) : null,
ISFT: 'AudioSort' ISFT: 'AudioSort'
}; };
@@ -138,7 +133,6 @@ export class TagService {
categories: metadata.categories.join(', '), categories: metadata.categories.join(', '),
title: metadata.title, title: metadata.title,
author: metadata.author, author: metadata.author,
copyright: metadata.copyright,
rating: metadata.rating rating: metadata.rating
}); });
} catch (error) { } catch (error) {

View File

@@ -10,8 +10,8 @@ export interface FileDetailPanelProps {
categories: CategoryRecord[]; categories: CategoryRecord[];
onRename(newName: string): Promise<void>; onRename(newName: string): Promise<void>;
onMove(targetRelativeDirectory: string): Promise<void>; onMove(targetRelativeDirectory: string): Promise<void>;
/** Organizes a file with optional metadata fields (customName, author, copyright, rating 1-5). */ /** Organizes a file with optional metadata fields (customName, author, rating 1-5). */
onOrganize(metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<void>; onOrganize(metadata: { customName?: string | null; author?: string | null; rating?: number }): Promise<void>;
onUpdateTags(data: { tags: string[]; categories: string[] }): Promise<void>; onUpdateTags(data: { tags: string[]; categories: string[] }): Promise<void>;
onUpdateCustomName(customName: string | null): Promise<void>; onUpdateCustomName(customName: string | null): Promise<void>;
metadataSuggestionsVersion: number; metadataSuggestionsVersion: number;
@@ -29,9 +29,8 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true); const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true);
const [suggestions, setSuggestions] = useState<{ authors: string[] }>({ authors: [] }); const [suggestions, setSuggestions] = useState<{ authors: string[] }>({ authors: [] });
const initialMetadataRef = useRef<{ author: string; copyright: string; rating: number; customName: string }>({ const initialMetadataRef = useRef<{ author: string; rating: number; customName: string }>({
author: '', author: '',
copyright: '',
rating: 0, rating: 0,
customName: '' customName: ''
}); });
@@ -94,7 +93,7 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
setAuthorDraft(''); setAuthorDraft('');
setRatingDraft(0); setRatingDraft(0);
setIsEditingCustomName(false); setIsEditingCustomName(false);
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: '' }; initialMetadataRef.current = { author: '', rating: 0, customName: '' };
return; return;
} }
@@ -102,7 +101,7 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
setCustomNameDraft(file.customName ?? ''); setCustomNameDraft(file.customName ?? '');
setIsEditingCustomName(false); setIsEditingCustomName(false);
const baseCustomName = (file.customName ?? '').trim(); const baseCustomName = (file.customName ?? '').trim();
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: baseCustomName }; initialMetadataRef.current = { author: '', rating: 0, customName: baseCustomName };
void (async () => { void (async () => {
try { try {
@@ -118,7 +117,6 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
setRatingDraft(ratingValue); setRatingDraft(ratingValue);
initialMetadataRef.current = { initialMetadataRef.current = {
author: authorValue, author: authorValue,
copyright: '',
rating: ratingValue, rating: ratingValue,
customName: customNameValue customName: customNameValue
}; };
@@ -129,7 +127,7 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
console.error('Failed to read file metadata:', error); console.error('Failed to read file metadata:', error);
setAuthorDraft(''); setAuthorDraft('');
setRatingDraft(0); setRatingDraft(0);
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: baseCustomName }; initialMetadataRef.current = { author: '', rating: 0, customName: baseCustomName };
} }
})(); })();
}, [appendSuggestion, file?.id]); }, [appendSuggestion, file?.id]);
@@ -206,7 +204,6 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
const trimmedCustomName = (nextCustomNameRaw ?? '').toString().trim(); const trimmedCustomName = (nextCustomNameRaw ?? '').toString().trim();
const comparisonState = { const comparisonState = {
author: trimmedAuthor, author: trimmedAuthor,
copyright: '',
rating: nextRatingValue, rating: nextRatingValue,
customName: trimmedCustomName customName: trimmedCustomName
}; };
@@ -215,7 +212,6 @@ export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize
if ( if (
previous && previous &&
previous.author === comparisonState.author && previous.author === comparisonState.author &&
previous.copyright === comparisonState.copyright &&
previous.rating === comparisonState.rating && previous.rating === comparisonState.rating &&
previous.customName === comparisonState.customName previous.customName === comparisonState.customName
) { ) {

View File

@@ -8,8 +8,8 @@ export interface MultiFileEditorProps {
categories: CategoryRecord[]; categories: CategoryRecord[];
onUpdateTags(fileId: number, data: { tags: string[]; categories: string[] }): Promise<void>; onUpdateTags(fileId: number, data: { tags: string[]; categories: string[] }): Promise<void>;
onUpdateCustomName(fileId: number, customName: string | null): Promise<void>; onUpdateCustomName(fileId: number, customName: string | null): Promise<void>;
onOrganize(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<void>; onOrganize(fileId: number, metadata: { customName?: string | null; author?: string | null; rating?: number }): Promise<void>;
onUpdateMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void>; onUpdateMetadata(fileId: number, metadata: { author?: string | null; rating?: number }): Promise<void>;
metadataSuggestionsVersion: number; metadataSuggestionsVersion: number;
} }
@@ -19,9 +19,8 @@ export interface MultiFileEditorProps {
export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCustomName, onOrganize, onUpdateMetadata, metadataSuggestionsVersion }: MultiFileEditorProps): JSX.Element { export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCustomName, onOrganize, onUpdateMetadata, metadataSuggestionsVersion }: MultiFileEditorProps): JSX.Element {
const [sharedCustomName, setSharedCustomName] = useState(''); const [sharedCustomName, setSharedCustomName] = useState('');
const [sharedAuthor, setSharedAuthor] = useState(''); const [sharedAuthor, setSharedAuthor] = useState('');
const [sharedCopyright, setSharedCopyright] = useState('');
const [sharedRating, setSharedRating] = useState(0); const [sharedRating, setSharedRating] = useState(0);
const [suggestions, setSuggestions] = useState<{ authors: string[]; copyrights: string[] }>({ authors: [], copyrights: [] }); const [suggestions, setSuggestions] = useState<{ authors: string[] }>({ authors: [] });
const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true); const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true);
// Compute aggregated tags and categories for TagEditor // Compute aggregated tags and categories for TagEditor
@@ -53,7 +52,7 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
type SuggestionKey = 'authors' | 'copyrights'; type SuggestionKey = 'authors';
const appendSuggestion = useCallback((value: string, key: SuggestionKey) => { const appendSuggestion = useCallback((value: string, key: SuggestionKey) => {
const trimmed = value.trim(); const trimmed = value.trim();
@@ -84,21 +83,9 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5); return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5);
}, [sharedAuthor, suggestions.authors]); }, [sharedAuthor, suggestions.authors]);
const filteredCopyrightSuggestions = useMemo(() => {
if (suggestions.copyrights.length === 0) {
return [] as string[];
}
const query = sharedCopyright.trim().toLowerCase();
const base = suggestions.copyrights.filter((entry) => entry.toLowerCase() !== query);
if (query.length === 0) {
return base.slice(0, 5);
}
return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5);
}, [sharedCopyright, suggestions.copyrights]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
setSuggestions({ authors: [], copyrights: [] }); setSuggestions({ authors: [] });
void (async () => { void (async () => {
try { try {
const result = await window.api.listMetadataSuggestions(); const result = await window.api.listMetadataSuggestions();
@@ -106,8 +93,7 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
return; return;
} }
setSuggestions({ setSuggestions({
authors: result.authors, authors: result.authors
copyrights: result.copyrights
}); });
} catch (error) { } catch (error) {
if (!cancelled) { if (!cancelled) {
@@ -188,37 +174,6 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
} }
}; };
const handleSharedCopyrightBlur = async () => {
const normalized = sharedCopyright.trim();
if (normalized.length === 0) return;
try {
let failureCount = 0;
for (const file of files) {
try {
if (file.categories.length > 0) {
await onOrganize(file.id, { copyright: normalized });
} else {
await onUpdateMetadata(file.id, { copyright: normalized });
}
} catch (error) {
failureCount += 1;
console.error(`Failed to update copyright for file ${file.id} (${file.fileName}):`, error);
}
}
if (failureCount > 0) {
console.error(`${failureCount} file(s) failed to update copyright`);
}
if (normalized.length > 0) {
appendSuggestion(normalized, 'copyrights');
}
} catch (error) {
console.error('Failed to update copyright:', error);
}
};
const handleSharedRatingChange = async (rating: number) => { const handleSharedRatingChange = async (rating: number) => {
setSharedRating(rating); setSharedRating(rating);
@@ -325,39 +280,6 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
</div> </div>
)} )}
</label> </label>
<label>
<span>Copyright</span>
<input
type="text"
value={sharedCopyright}
onChange={(e) => setSharedCopyright(e.target.value)}
onBlur={handleSharedCopyrightBlur}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
}
}}
placeholder="Copyright information"
/>
{filteredCopyrightSuggestions.length > 0 && (
<div className="metadata-suggestion-list" role="listbox" aria-label="Copyright suggestions">
{filteredCopyrightSuggestions.map((option) => (
<button
key={option}
type="button"
className="metadata-suggestion"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSharedCopyright(option);
void handleSharedCopyrightBlur();
}}
>
{option}
</button>
))}
</div>
)}
</label>
<label> <label>
<span>Rating</span> <span>Rating</span>
<div className="star-rating"> <div className="star-rating">

View File

@@ -8,11 +8,10 @@ import {
type SyntheticEvent type SyntheticEvent
} from 'react'; } from 'react';
type SuggestionType = 'author' | 'copyright'; type SuggestionType = 'author';
interface MetadataSuggestionPayload { interface MetadataSuggestionPayload {
authors: string[]; authors: string[];
copyrights: string[];
} }
export interface SearchBarProps { export interface SearchBarProps {
@@ -30,7 +29,7 @@ export interface SearchBarProps {
export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenSettings, metadataSuggestionsVersion }: SearchBarProps): JSX.Element { export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenSettings, metadataSuggestionsVersion }: SearchBarProps): JSX.Element {
const [draft, setDraft] = useState(value); const [draft, setDraft] = useState(value);
const [caretIndex, setCaretIndex] = useState<number | null>(null); const [caretIndex, setCaretIndex] = useState<number | null>(null);
const [suggestions, setSuggestions] = useState<MetadataSuggestionPayload>({ authors: [], copyrights: [] }); const [suggestions, setSuggestions] = useState<MetadataSuggestionPayload>({ authors: [] });
const [loadingSuggestions, setLoadingSuggestions] = useState(false); const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const [rescanBusy, setRescanBusy] = useState(false); const [rescanBusy, setRescanBusy] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -44,8 +43,7 @@ export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenS
try { try {
const result = await window.api.listMetadataSuggestions(); const result = await window.api.listMetadataSuggestions();
setSuggestions({ setSuggestions({
authors: result.authors, authors: result.authors
copyrights: result.copyrights
}); });
} catch (error) { } catch (error) {
console.error('Failed to load search suggestions', error); console.error('Failed to load search suggestions', error);
@@ -55,7 +53,7 @@ export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenS
}, []); }, []);
useEffect(() => { useEffect(() => {
setSuggestions({ authors: [], copyrights: [] }); setSuggestions({ authors: [] });
void refreshSuggestions(); void refreshSuggestions();
}, [refreshSuggestions, metadataSuggestionsVersion]); }, [refreshSuggestions, metadataSuggestionsVersion]);
@@ -65,7 +63,7 @@ export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenS
}, [caretIndex, draft]); }, [caretIndex, draft]);
const activeToken = useMemo(() => { const activeToken = useMemo(() => {
const match = analysisTarget.match(/(author|copyright):([^\s]*)$/i); const match = analysisTarget.match(/(author):([^\s]*)$/i);
if (!match) { if (!match) {
return null; return null;
} }
@@ -80,7 +78,7 @@ export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenS
if (!activeToken) { if (!activeToken) {
return [] as string[]; return [] as string[];
} }
const pool = activeToken.type === 'author' ? suggestions.authors : suggestions.copyrights; const pool = suggestions.authors;
const trimmedTerm = activeToken.term.trim(); const trimmedTerm = activeToken.term.trim();
if (trimmedTerm.length === 0) { if (trimmedTerm.length === 0) {
return pool.slice(0, 6); return pool.slice(0, 6);

View File

@@ -297,7 +297,7 @@ export class LibraryStore extends EventTarget {
/** /**
* Automatically organizes a file based on its categories. * Automatically organizes a file based on its categories.
*/ */
public async organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<AudioFileSummary> { public async organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; rating?: number }): Promise<AudioFileSummary> {
const currentSelection = this.snapshot.selectedFileId; const currentSelection = this.snapshot.selectedFileId;
const currentFocused = this.snapshot.focusedFile; const currentFocused = this.snapshot.focusedFile;
const updated = await window.api.organizeFile(fileId, metadata); const updated = await window.api.organizeFile(fileId, metadata);
@@ -335,9 +335,9 @@ export class LibraryStore extends EventTarget {
} }
/** /**
* Updates metadata (author, copyright, rating) for a file without organizing it. * Updates metadata (author, rating) for a file without organizing it.
*/ */
public async updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void> { public async updateFileMetadata(fileId: number, metadata: { author?: string | null; rating?: number }): Promise<void> {
await window.api.updateFileMetadata(fileId, metadata); await window.api.updateFileMetadata(fileId, metadata);
// Refresh the file list to get updated metadata // Refresh the file list to get updated metadata

View File

@@ -45,7 +45,7 @@ export interface RendererApi {
/** Moves a file to another subdirectory under the library root. */ /** Moves a file to another subdirectory under the library root. */
moveFile(fileId: number, targetRelativeDirectory: string): Promise<import('./models').AudioFileSummary>; moveFile(fileId: number, targetRelativeDirectory: string): Promise<import('./models').AudioFileSummary>;
/** Automatically organizes a file based on its categories. */ /** Automatically organizes a file based on its categories. */
organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<import('./models').AudioFileSummary>; organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; rating?: number }): Promise<import('./models').AudioFileSummary>;
/** Updates the custom name for a file. */ /** Updates the custom name for a file. */
updateCustomName(fileId: number, customName: string | null): Promise<import('./models').AudioFileSummary>; updateCustomName(fileId: number, customName: string | null): Promise<import('./models').AudioFileSummary>;
/** Opens the file's containing folder in the system file explorer. */ /** Opens the file's containing folder in the system file explorer. */
@@ -63,11 +63,11 @@ export interface RendererApi {
/** Runs fuzzy search returning ranked results. */ /** Runs fuzzy search returning ranked results. */
search(query: string): Promise<import('./models').AudioFileSummary[]>; search(query: string): Promise<import('./models').AudioFileSummary[]>;
/** Reads embedded metadata from a WAV file. */ /** Reads embedded metadata from a WAV file. */
readFileMetadata(fileId: number): Promise<{ author?: string; copyright?: string; title?: string; rating?: number }>; readFileMetadata(fileId: number): Promise<{ author?: string; title?: string; rating?: number }>;
/** Lists distinct metadata values gathered from the library for quick suggestions. */ /** Lists distinct metadata values gathered from the library for quick suggestions. */
listMetadataSuggestions(): Promise<{ authors: string[]; copyrights: string[] }>; listMetadataSuggestions(): Promise<{ authors: string[] }>;
/** Updates metadata (author, copyright, rating) without organizing the file. */ /** Updates metadata (author, rating) without organizing the file. */
updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void>; updateFileMetadata(fileId: number, metadata: { author?: string | null; rating?: number }): Promise<void>;
/** Listens for menu actions from the main process. Returns a cleanup function. */ /** Listens for menu actions from the main process. Returns a cleanup function. */
onMenuAction(channel: string, callback: () => void): () => void; onMenuAction(channel: string, callback: () => void): () => void;
} }

View File

@@ -2,7 +2,7 @@
"extends": "./tsconfig.base.json", "extends": "./tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "Node10", "moduleResolution": "Bundler",
"outDir": "dist/main", "outDir": "dist/main",
"rootDir": "src", "rootDir": "src",
"types": ["node"], "types": ["node"],