Add ASIN support to file organization and metadata

This update enhances audiobook file organization by including the ASIN in folder names and embedding it as a custom metadata tag in audio files (M4B/M4A/MP3). Documentation is updated to reflect the new folder naming convention and metadata tagging. Additionally, local login and registration can now be disabled via an environment variable, and the interactive torrent search modal allows custom search titles for all modes.
This commit is contained in:
kikootwo
2025-12-24 23:37:40 -05:00
parent 1374e66f13
commit d617e26c92
12 changed files with 145 additions and 51 deletions
+5 -1
View File
@@ -216,7 +216,7 @@ export function useInteractiveSearch() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const searchTorrents = async (requestId: string) => {
const searchTorrents = async (requestId: string, customTitle?: string) => {
if (!accessToken) {
throw new Error('Not authenticated');
}
@@ -227,6 +227,10 @@ export function useInteractiveSearch() {
try {
const response = await fetchWithAuth(`/api/requests/${requestId}/interactive-search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: customTitle ? JSON.stringify({ customTitle }) : undefined,
});
const data = await response.json();
@@ -54,6 +54,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
author: audiobook.author,
narrator: audiobook.narrator || undefined,
coverArtUrl: audiobook.coverArtUrl || undefined,
asin: audiobook.audibleAsin || undefined,
},
jobId ? { jobId, context: 'FileOrganizer' } : undefined
);
+18 -3
View File
@@ -16,6 +16,7 @@ export interface AudiobookMetadata {
narrator?: string;
year?: number;
coverArtUrl?: string;
asin?: string;
}
export interface OrganizationResult {
@@ -110,6 +111,7 @@ export class FileOrganizer {
author: audiobook.author,
narrator: audiobook.narrator,
year: audiobook.year,
asin: audiobook.asin,
});
const successCount = taggingResults.filter((r) => r.success).length;
@@ -153,7 +155,8 @@ export class FileOrganizer {
this.mediaDir,
audiobook.author,
audiobook.title,
audiobook.year
audiobook.year,
audiobook.asin
);
await logger?.info(`Target path: ${targetPath}`);
@@ -359,16 +362,28 @@ export class FileOrganizer {
/**
* Build target path with sanitized names
* Format: Author/Title (Year) ASIN or Author/Title ASIN or Author/Title (Year)
*/
private buildTargetPath(
baseDir: string,
author: string,
title: string,
year?: number
year?: number,
asin?: string
): string {
const authorClean = this.sanitizePath(author);
const titleClean = this.sanitizePath(title);
const folderName = year ? `${titleClean} (${year})` : titleClean;
// Build folder name with optional year and ASIN
let folderName = titleClean;
if (year) {
folderName = `${folderName} (${year})`;
}
if (asin) {
folderName = `${folderName} ${asin}`;
}
return path.join(baseDir, authorClean, folderName);
}
+11
View File
@@ -15,6 +15,7 @@ export interface MetadataTaggingOptions {
author: string;
narrator?: string;
year?: number;
asin?: string;
}
export interface TaggingResult {
@@ -76,6 +77,11 @@ export async function tagAudioFileMetadata(
args.push('-metadata', `date="${metadata.year}"`);
}
if (metadata.asin) {
// Use custom iTunes tag format for M4B/M4A/MP4 files
args.push('-metadata', `----:com.apple.iTunes:ASIN="${escapeMetadata(metadata.asin)}"`);
}
// Explicitly specify output format (fixes .tmp extension issue)
args.push('-f', 'mp4');
}
@@ -97,6 +103,11 @@ export async function tagAudioFileMetadata(
args.push('-metadata', `date="${metadata.year}"`);
}
if (metadata.asin) {
// Use TXXX frame for custom ID3v2 tags in MP3 files
args.push('-metadata', `ASIN="${escapeMetadata(metadata.asin)}"`);
}
// Explicitly specify output format (fixes .tmp extension issue)
args.push('-f', 'mp3');
}