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 checksumFailures = new Set<string>();
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 }>();
public constructor(
@@ -378,7 +378,6 @@ export class LibraryService {
return this.organizeFile(payload.fileId, {
customName: updated.customName ?? undefined,
author: metadataSnapshot.author?.trim() || undefined,
copyright: metadataSnapshot.copyright?.trim() || undefined,
rating: metadataSnapshot.rating ?? undefined
});
}
@@ -435,7 +434,7 @@ export class LibraryService {
*/
public async organizeFile(
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> {
const record = this.database.getFileById(fileId);
const category = this.organization.getPrimaryCategory(record);
@@ -503,9 +502,7 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided)
const requestedAuthor = this.normaliseMetadataInput(metadata.author);
const requestedCopyright = this.normaliseMetadataInput(metadata.copyright);
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;
this.tagService.writeMetadataOnly(updatedRecord.absolutePath, {
@@ -513,16 +510,12 @@ export class LibraryService {
categories: updatedRecord.categories,
title: effectiveCustomName,
author: mergedAuthor,
copyright: mergedCopyright,
rating: mergedRating
});
// Update suggestions cache for any metadata that was provided
if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) {
this.updateMetadataSuggestionsCache(mergedAuthor, undefined);
}
if (typeof mergedCopyright === 'string' && mergedCopyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedCopyright);
this.updateMetadataSuggestionsCache(mergedAuthor);
}
this.resetMetadataSuggestionsCache();
@@ -602,9 +595,7 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided)
const movedAuthor = this.normaliseMetadataInput(metadata.author);
const movedCopyright = this.normaliseMetadataInput(metadata.copyright);
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;
// Write tags, categories, and additional metadata to the organized file
@@ -613,16 +604,12 @@ export class LibraryService {
categories: updated.categories,
title: effectiveCustomName,
author: mergedAuthor,
copyright: mergedCopyright,
rating: mergedRating
});
// Update suggestions cache for any metadata that was provided
if (typeof mergedAuthor === 'string' && mergedAuthor.length > 0) {
this.updateMetadataSuggestionsCache(mergedAuthor, undefined);
}
if (typeof mergedCopyright === 'string' && mergedCopyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedCopyright);
this.updateMetadataSuggestionsCache(mergedAuthor);
}
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 metadata = this.tagService.readMetadata(record.absolutePath);
this.updateMetadataSuggestionsCache(metadata.author, metadata.copyright);
this.updateMetadataSuggestionsCache(metadata.author);
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.
*/
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);
// Read existing metadata from the WAV file
@@ -687,13 +674,11 @@ export class LibraryService {
// Merge with new metadata (only update fields that are provided)
const requestedAuthor = this.normaliseMetadataInput(metadata.author);
const requestedCopyright = this.normaliseMetadataInput(metadata.copyright);
const mergedMetadata = {
tags: record.tags,
categories: record.categories,
title: record.customName ?? existing.title,
author: requestedAuthor !== undefined ? requestedAuthor : existing.author ?? null,
copyright: requestedCopyright !== undefined ? requestedCopyright : existing.copyright ?? null,
rating: metadata.rating !== undefined ? metadata.rating : existing.rating
};
@@ -702,10 +687,7 @@ export class LibraryService {
// Update suggestions cache
if (typeof mergedMetadata.author === 'string' && mergedMetadata.author.length > 0) {
this.updateMetadataSuggestionsCache(mergedMetadata.author, undefined);
}
if (typeof mergedMetadata.copyright === 'string' && mergedMetadata.copyright.length > 0) {
this.updateMetadataSuggestionsCache(undefined, mergedMetadata.copyright);
this.updateMetadataSuggestionsCache(mergedMetadata.author);
}
this.waveformPreviewCache.delete(fileId);
@@ -715,10 +697,9 @@ export class LibraryService {
/**
* 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) {
const authors = new Set<string>();
const copyrights = new Set<string>();
const files = this.database.listFiles();
for (const file of files) {
@@ -727,9 +708,6 @@ export class LibraryService {
if (metadata.author) {
this.addSuggestionValue(authors, metadata.author);
}
if (metadata.copyright) {
this.addSuggestionValue(copyrights, metadata.copyright);
}
} catch (error) {
if (!this.metadataFailures.has(file.absolutePath)) {
this.metadataFailures.add(file.absolutePath);
@@ -739,12 +717,11 @@ export class LibraryService {
}
}
this.metadataSuggestionCache = { authors, copyrights };
this.metadataSuggestionCache = { authors };
}
return {
authors: Array.from(this.metadataSuggestionCache.authors).sort((a, b) => a.localeCompare(b)),
copyrights: Array.from(this.metadataSuggestionCache.copyrights).sort((a, b) => a.localeCompare(b))
authors: Array.from(this.metadataSuggestionCache.authors).sort((a, b) => a.localeCompare(b))
};
}
@@ -769,16 +746,13 @@ export class LibraryService {
return trimmed.length > 0 ? trimmed : null;
}
private updateMetadataSuggestionsCache(author?: string | null, copyright?: string | null): void {
private updateMetadataSuggestionsCache(author?: string | null): void {
if (!this.metadataSuggestionCache) {
return;
}
if (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): {
author?: string;
copyright?: string;
title?: string;
rating?: number;
} {
@@ -58,7 +57,6 @@ export class TagService {
return {
author: tags.IART?.trim() || undefined,
copyright: tags.ICOP?.trim() || undefined,
title: tags.INAM?.trim() || undefined,
rating
};
@@ -79,7 +77,6 @@ export class TagService {
categories: string[];
title?: string | null;
author?: string | null;
copyright?: string | null;
rating?: number;
}
): void {
@@ -96,7 +93,6 @@ export class TagService {
categories: string[];
title?: string | null;
author?: string | null;
copyright?: string | null;
rating?: number;
}
): void {
@@ -123,7 +119,6 @@ export class TagService {
IGNR: null,
INAM: metadata.title?.trim()?.length ? metadata.title.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,
ISFT: 'AudioSort'
};
@@ -138,7 +133,6 @@ export class TagService {
categories: metadata.categories.join(', '),
title: metadata.title,
author: metadata.author,
copyright: metadata.copyright,
rating: metadata.rating
});
} catch (error) {

View File

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

View File

@@ -8,8 +8,8 @@ export interface MultiFileEditorProps {
categories: CategoryRecord[];
onUpdateTags(fileId: number, data: { tags: string[]; categories: string[] }): 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>;
onUpdateMetadata(fileId: number, metadata: { 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; rating?: number }): Promise<void>;
metadataSuggestionsVersion: number;
}
@@ -19,9 +19,8 @@ export interface MultiFileEditorProps {
export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCustomName, onOrganize, onUpdateMetadata, metadataSuggestionsVersion }: MultiFileEditorProps): JSX.Element {
const [sharedCustomName, setSharedCustomName] = useState('');
const [sharedAuthor, setSharedAuthor] = useState('');
const [sharedCopyright, setSharedCopyright] = useState('');
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);
// Compute aggregated tags and categories for TagEditor
@@ -53,7 +52,7 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
const listRef = useRef<HTMLDivElement>(null);
type SuggestionKey = 'authors' | 'copyrights';
type SuggestionKey = 'authors';
const appendSuggestion = useCallback((value: string, key: SuggestionKey) => {
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);
}, [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(() => {
let cancelled = false;
setSuggestions({ authors: [], copyrights: [] });
setSuggestions({ authors: [] });
void (async () => {
try {
const result = await window.api.listMetadataSuggestions();
@@ -106,8 +93,7 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
return;
}
setSuggestions({
authors: result.authors,
copyrights: result.copyrights
authors: result.authors
});
} catch (error) {
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) => {
setSharedRating(rating);
@@ -325,39 +280,6 @@ export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCusto
</div>
)}
</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>
<span>Rating</span>
<div className="star-rating">

View File

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

View File

@@ -297,7 +297,7 @@ export class LibraryStore extends EventTarget {
/**
* 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 currentFocused = this.snapshot.focusedFile;
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);
// 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. */
moveFile(fileId: number, targetRelativeDirectory: string): Promise<import('./models').AudioFileSummary>;
/** 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. */
updateCustomName(fileId: number, customName: string | null): Promise<import('./models').AudioFileSummary>;
/** Opens the file's containing folder in the system file explorer. */
@@ -63,11 +63,11 @@ export interface RendererApi {
/** Runs fuzzy search returning ranked results. */
search(query: string): Promise<import('./models').AudioFileSummary[]>;
/** 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. */
listMetadataSuggestions(): Promise<{ authors: string[]; copyrights: string[] }>;
/** Updates metadata (author, copyright, rating) without organizing the file. */
updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void>;
listMetadataSuggestions(): Promise<{ authors: string[] }>;
/** Updates metadata (author, rating) without organizing the file. */
updateFileMetadata(fileId: number, metadata: { author?: string | null; rating?: number }): Promise<void>;
/** Listens for menu actions from the main process. Returns a cleanup function. */
onMenuAction(channel: string, callback: () => void): () => void;
}

View File

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