mirror of
https://github.com/litruv/AudioSort.git
synced 2026-07-24 02:36:01 +10:00
Refactor metadata handling: remove copyright fields from various services and components
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
Reference in New Issue
Block a user