Initial commit

This commit is contained in:
kikootwo
2026-01-28 11:41:24 -05:00
commit a3ba192fbd
257 changed files with 89482 additions and 0 deletions
@@ -0,0 +1,191 @@
/**
* Component: Organize Files Job Processor
* Documentation: documentation/phase3/README.md
*/
import { OrganizeFilesPayload, getJobQueueService } from '../services/job-queue.service';
import { prisma } from '../db';
import { getFileOrganizer } from '../utils/file-organizer';
import { createJobLogger } from '../utils/job-logger';
/**
* Process organize files job
* Moves completed downloads to media library in proper directory structure
*/
export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promise<any> {
const { requestId, audiobookId, downloadPath, jobId } = payload;
// Create logger (fallback to console-only if jobId not provided)
const logger = jobId ? createJobLogger(jobId, 'OrganizeFiles') : null;
await logger?.info(`Processing request ${requestId}`);
await logger?.info(`Download path: ${downloadPath}`);
try {
// Update request status to processing
await prisma.request.update({
where: { id: requestId },
data: {
status: 'processing',
progress: 100, // Download is complete, now organizing
updatedAt: new Date(),
},
});
// Get audiobook details
const audiobook = await prisma.audiobook.findUnique({
where: { id: audiobookId },
});
if (!audiobook) {
throw new Error(`Audiobook ${audiobookId} not found`);
}
await logger?.info(`Organizing: ${audiobook.title} by ${audiobook.author}`);
// Get file organizer
const organizer = getFileOrganizer();
// Organize files (pass logger to file organizer)
const result = await organizer.organize(
downloadPath,
{
title: audiobook.title,
author: audiobook.author,
narrator: audiobook.narrator || undefined,
coverArtUrl: audiobook.coverArtUrl || undefined,
},
jobId ? { jobId, context: 'FileOrganizer' } : undefined
);
if (!result.success) {
throw new Error(`File organization failed: ${result.errors.join(', ')}`);
}
await logger?.info(`Successfully moved ${result.filesMovedCount} files to ${result.targetPath}`);
// Update audiobook record with file path and status
await prisma.audiobook.update({
where: { id: audiobookId },
data: {
filePath: result.targetPath,
status: 'completed',
completedAt: new Date(),
updatedAt: new Date(),
},
});
// Update request to downloaded (green status, waiting for Plex scan)
await prisma.request.update({
where: { id: requestId },
data: {
status: 'downloaded',
progress: 100,
completedAt: new Date(),
updatedAt: new Date(),
},
});
await logger?.info(`Request ${requestId} completed successfully - status: downloaded`, {
success: true,
message: 'Files organized successfully',
requestId,
audiobookId,
targetPath: result.targetPath,
filesCount: result.filesMovedCount,
audioFiles: result.audioFiles,
coverArt: result.coverArtFile,
errors: result.errors,
});
return {
success: true,
message: 'Files organized successfully',
requestId,
audiobookId,
targetPath: result.targetPath,
filesCount: result.filesMovedCount,
audioFiles: result.audioFiles,
coverArt: result.coverArtFile,
errors: result.errors,
};
} catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const errorMessage = error instanceof Error ? error.message : 'File organization failed';
// Check if this is a "no files found" error that should be retried
const isNoFilesError = errorMessage.includes('No audiobook files found');
if (isNoFilesError) {
// Get current request to check retry count
const currentRequest = await prisma.request.findUnique({
where: { id: requestId },
select: { importAttempts: true, maxImportRetries: true },
});
if (!currentRequest) {
throw new Error('Request not found');
}
const newAttempts = currentRequest.importAttempts + 1;
if (newAttempts < currentRequest.maxImportRetries) {
// Still have retries left - queue for re-import
await logger?.warn(`No files found for request ${requestId}, queueing for retry (attempt ${newAttempts}/${currentRequest.maxImportRetries})`);
await prisma.request.update({
where: { id: requestId },
data: {
status: 'awaiting_import',
importAttempts: newAttempts,
lastImportAt: new Date(),
errorMessage: `${errorMessage}. Retry ${newAttempts}/${currentRequest.maxImportRetries}`,
updatedAt: new Date(),
},
});
return {
success: false,
message: 'No audiobook files found, queued for re-import',
requestId,
attempts: newAttempts,
maxRetries: currentRequest.maxImportRetries,
};
} else {
// Max retries exceeded - move to warn status
await logger?.warn(`Max retries (${currentRequest.maxImportRetries}) exceeded for request ${requestId}, moving to warn status`);
await prisma.request.update({
where: { id: requestId },
data: {
status: 'warn',
importAttempts: newAttempts,
errorMessage: `${errorMessage}. Max retries (${currentRequest.maxImportRetries}) exceeded. Manual retry available.`,
updatedAt: new Date(),
},
});
return {
success: false,
message: 'Max import retries exceeded, manual intervention required',
requestId,
attempts: newAttempts,
maxRetries: currentRequest.maxImportRetries,
};
}
} else {
// Other error - fail immediately
await prisma.request.update({
where: { id: requestId },
data: {
status: 'failed',
errorMessage,
updatedAt: new Date(),
},
});
throw error;
}
}
}