mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Add filesystem scan trigger and version badge features
Implements optional filesystem scan triggering for Plex and Audiobookshelf after file organization, with new settings in the admin UI, setup wizard, and API. Updates documentation to reflect scan trigger options and improved file organization/cleanup logic. Refactors dropdown menus to use smart positioning and portals for better UX. Adds a version API route and a VersionBadge component to display build info in the header. Updates Docker build to inject version metadata.
This commit is contained in:
@@ -45,15 +45,19 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
await logger?.info(`Loaded configuration for ${indexerConfigMap.size} indexers`);
|
||||
|
||||
// Find all completed requests + soft-deleted requests (orphaned downloads)
|
||||
// IMPORTANT: Only cleanup requests that are truly complete and not being actively processed
|
||||
// NOTE: Multiple requests can share the same torrent hash (e.g., re-requesting same audiobook)
|
||||
// Before deleting torrent, we check if other active requests are using it
|
||||
const completedRequests = await prisma.request.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
// Active requests with completed downloads
|
||||
// Active requests that are fully available (scanned by Plex/ABS)
|
||||
{
|
||||
status: { in: ['available', 'downloaded'] },
|
||||
status: 'available',
|
||||
deletedAt: null,
|
||||
},
|
||||
// Soft-deleted requests (orphaned downloads still seeding)
|
||||
// Soft-deleted requests (orphaned downloads)
|
||||
// We'll check if torrent is shared with active requests before deletion
|
||||
{
|
||||
deletedAt: { not: null },
|
||||
},
|
||||
@@ -72,7 +76,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
take: 100, // Limit to 100 requests per run
|
||||
});
|
||||
|
||||
await logger?.info(`Found ${completedRequests.length} completed requests to check`);
|
||||
await logger?.info(`Found ${completedRequests.length} requests to check (status: 'available' or soft-deleted)`);
|
||||
|
||||
let cleaned = 0;
|
||||
let skipped = 0;
|
||||
@@ -144,7 +148,36 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
|
||||
await logger?.info(`Torrent ${torrent.name} (${indexerName}) has met seeding requirement (${Math.floor(actualSeedingTime / 60)}/${seedingConfig.seedingTimeMinutes} minutes)`);
|
||||
|
||||
// Delete torrent and files from qBittorrent
|
||||
// CRITICAL: Check if any other active (non-deleted) request is using this same torrent hash
|
||||
// This prevents deleting shared torrents when user re-requests the same audiobook
|
||||
const otherActiveRequests = await prisma.request.findMany({
|
||||
where: {
|
||||
id: { not: request.id }, // Exclude current request
|
||||
deletedAt: null, // Only check active requests
|
||||
downloadHistory: {
|
||||
some: {
|
||||
torrentHash: downloadHistory.torrentHash,
|
||||
selected: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { id: true, status: true },
|
||||
});
|
||||
|
||||
if (otherActiveRequests.length > 0) {
|
||||
await logger?.info(`Skipping torrent deletion - ${otherActiveRequests.length} other active request(s) still using this torrent (IDs: ${otherActiveRequests.map(r => r.id).join(', ')})`);
|
||||
|
||||
// If this is a soft-deleted request, hard delete it but DON'T delete the torrent
|
||||
if (request.deletedAt) {
|
||||
await prisma.request.delete({ where: { id: request.id } });
|
||||
await logger?.info(`Hard-deleted orphaned request ${request.id} (kept shared torrent for active requests)`);
|
||||
}
|
||||
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe to delete - no other active requests using this torrent
|
||||
await qbt.deleteTorrent(downloadHistory.torrentHash, true); // true = delete files
|
||||
|
||||
// If this is a soft-deleted request (orphaned download), hard delete it now
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
|
||||
torrentName: torrent.title,
|
||||
nzbId: downloadClientId, // Store NZB ID
|
||||
torrentSizeBytes: torrent.size,
|
||||
torrentUrl: torrent.guid, // Source URL
|
||||
torrentUrl: torrent.infoUrl || torrent.guid, // Indexer page URL (prefer infoUrl, fallback to guid)
|
||||
magnetLink: torrent.downloadUrl, // Download URL (.nzb file)
|
||||
seeders: torrent.seeders || 0, // Usenet doesn't have seeders, but include for consistency
|
||||
leechers: 0,
|
||||
@@ -130,7 +130,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
|
||||
torrentName: torrent.title,
|
||||
torrentHash: torrent.infoHash || downloadClientId, // Store torrent hash
|
||||
torrentSizeBytes: torrent.size,
|
||||
torrentUrl: torrent.guid,
|
||||
torrentUrl: torrent.infoUrl || torrent.guid, // Indexer page URL (prefer infoUrl, fallback to guid)
|
||||
magnetLink: torrent.downloadUrl,
|
||||
seeders: torrent.seeders || 0,
|
||||
leechers: torrent.leechers || 0,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { OrganizeFilesPayload, getJobQueueService } from '../services/job-queue.
|
||||
import { prisma } from '../db';
|
||||
import { getFileOrganizer } from '../utils/file-organizer';
|
||||
import { createJobLogger } from '../utils/job-logger';
|
||||
import { getLibraryService } from '../services/library';
|
||||
import { getConfigService } from '../services/config.service';
|
||||
|
||||
/**
|
||||
* Process organize files job
|
||||
@@ -99,6 +101,54 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
|
||||
errors: result.errors,
|
||||
});
|
||||
|
||||
// Trigger filesystem scan if enabled (Plex or Audiobookshelf)
|
||||
const configService = getConfigService();
|
||||
const backendMode = await configService.getBackendMode();
|
||||
|
||||
const configKey = backendMode === 'audiobookshelf'
|
||||
? 'audiobookshelf.trigger_scan_after_import'
|
||||
: 'plex.trigger_scan_after_import';
|
||||
|
||||
const scanEnabled = await configService.get(configKey);
|
||||
|
||||
if (scanEnabled === 'true') {
|
||||
try {
|
||||
// Get library service (returns PlexLibraryService or AudiobookshelfLibraryService)
|
||||
const libraryService = await getLibraryService();
|
||||
|
||||
// Get configured library ID (backend-specific config)
|
||||
const libraryId = backendMode === 'audiobookshelf'
|
||||
? await configService.get('audiobookshelf.library_id')
|
||||
: await configService.get('plex_audiobook_library_id');
|
||||
|
||||
if (!libraryId) {
|
||||
throw new Error('Library ID not configured');
|
||||
}
|
||||
|
||||
// Trigger scan (implementation is backend-specific)
|
||||
await libraryService.triggerLibraryScan(libraryId);
|
||||
|
||||
await logger?.info(
|
||||
`Triggered ${backendMode} filesystem scan for library ${libraryId}`
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
// Log error but don't fail the job
|
||||
await logger?.error(
|
||||
`Failed to trigger filesystem scan: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
{
|
||||
error: error instanceof Error ? error.stack : undefined,
|
||||
backend: backendMode
|
||||
}
|
||||
);
|
||||
// Continue - scheduled scans will eventually detect the book
|
||||
}
|
||||
} else {
|
||||
await logger?.info(
|
||||
`${backendMode} filesystem scan trigger disabled (relying on filesystem watcher)`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Files organized successfully',
|
||||
|
||||
Reference in New Issue
Block a user