/** * Renaming Rule - Defines folder organization logic * * Migrated from meta-mesh to meta-fuse for virtual path computation. * Organizes files into structured folders based on metadata: * - TV Shows/{title}/S01/{filename} for series * - Movies/{title} (year)/{filename} for movies */ /** * Minimal metadata interface for folder organization * Contains only the fields needed for virtual path computation */ export interface MediaMeta { // Title information originalTitle?: string; title?: string; // Series metadata season?: number; episode?: number; extra?: boolean; // Movie metadata movieYear?: number; year?: number; // File type info fileType?: string; // 'video', 'subtitle', 'torrent' extension?: string; // Version/variant version?: string; subtitleLanguage?: string; // Allow additional fields [key: string]: unknown; } /** * Compute the virtual path for a file based on its metadata * Returns the path relative to the VFS root * * @param metadata File metadata with title, season, episode, year, etc. * @param filepath Original file path (used for warnings) * @returns Virtual path (e.g., "TV Shows/Show Name/S01/Episode.mkv") or null if unsupported */ export function renamingRule(metadata: Partial, filepath?: string): string | null { const supported = ['video', 'subtitle', 'torrent']; // Check if filetype is one of the supported ones if (!metadata.fileType || !supported.includes(metadata.fileType)) { return null; } // Extract the file extension const extension = metadata.extension; if (!extension) { console.warn(`[RenamingRule] No extension found for file: ${filepath}`); return null; } // Get title - try various fields // The display name — never the `titles/*` name set (METADATA_KEYS.md §3). const title = metadata.title || metadata.originalTitle; if (!title) { if (metadata.fileType !== 'torrent') { // Don't warn for torrent files because they don't always have a title console.warn(`[RenamingRule] No title found for file: ${filepath}`); } return null; } let newPath = ''; // Build filename components const season = metadata.season ? ('S' + String(metadata.season).padStart(2, '0')) : ''; const seasonSpace = metadata.season ? ` ${season}` : ''; const episode = metadata.episode ? ('E' + String(metadata.episode).padStart(2, '0')) : ''; const version = metadata.version ? ` ${metadata.version}` : ''; const year = metadata.movieYear ? ` (${metadata.movieYear})` : ''; const subtitle = metadata.fileType === 'subtitle' && metadata.subtitleLanguage ? `.${metadata.subtitleLanguage}` : ''; const fileName = `${title}${seasonSpace}${episode}${year}${subtitle}${version}.${extension}`; // Determine structure: TV Shows for series, Movies for standalone content // Note: season can be 0 (special episodes), so check for null/undefined explicitly const hasSeason = metadata.season !== null && metadata.season !== undefined; const hasEpisode = metadata.episode !== null && metadata.episode !== undefined; if (metadata.extra) { newPath = `TV Shows/${title}/extra/${fileName}`; } else if (hasSeason && hasEpisode) { newPath = `TV Shows/${title}/${season}/${fileName}`; } else { newPath = `Movies/${title}${year}/${fileName}`; } return newPath; } /** * Sanitize a path to remove illegal characters * Works for both Windows and Unix-like systems */ export function sanitizePath(newPath: string): string { // Isolate any potential Windows drive letter at the start const driveMatch = newPath.match(/^[a-zA-Z]:/); const drive = driveMatch ? driveMatch[0] : ''; // Define illegal characters for Windows and potentially problematic ones for Unix const illegalChars = /[<>:"|?*]/g; // Replace illegal characters in the path, excluding the drive part const pathWithoutDrive = newPath.slice(drive.length); const sanitizedPath = pathWithoutDrive.replace(illegalChars, ''); // Combine the drive (if any) and the sanitized path const cleanPath = drive + sanitizedPath; // Normalize to forward slashes (works cross-platform) return cleanPath.replace(/\\/g, '/'); }