UNPKG

@tiahui/anitorrent-cli

Version:

CLI tool for video management with PeerTube and Storj S3

250 lines (216 loc) 6.74 kB
const fs = require('fs').promises; const path = require('path'); const anitomy = require('anitomyscript'); class FileService { constructor(options = {}) { this.verbose = options.verbose || false; this.quiet = options.quiet || false; } async scanDirectory(dirPath) { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); const directories = []; for (const item of items) { if (item.isDirectory()) { const fullPath = path.join(dirPath, item.name); const files = await this.getFilesInDirectory(fullPath); if (files.length > 0) { directories.push({ name: item.name, path: fullPath, files: files, }); } } } return directories.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }) ); } catch (error) { throw new Error(`Failed to scan directory: ${error.message}`); } } async getFilesInDirectory(dirPath) { try { const items = await fs.readdir(dirPath); const fileExtensions = [ '.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.ass', '.srt', '.vtt', '.sub', '.mp3', '.aac', ]; return items .filter((item) => { const ext = path.extname(item).toLowerCase(); return fileExtensions.includes(ext); }) .map((item) => ({ name: item, path: path.join(dirPath, item), })); } catch (error) { return []; } } async parseEpisodeInfo(filename) { try { const parsed = await anitomy(filename); // Debug logging if (this.verbose) { console.log('Anitomy parsed:', filename, '→', parsed); } return { anime_title: parsed.anime_title || '', anime_season: parsed.anime_season || '', episode_number: parsed.episode_number || '', episode_title: parsed.episode_title || '', file_extension: parsed.file_extension || '', release_group: parsed.release_group || '', video_resolution: parsed.video_resolution || '', audio_term: parsed.audio_term || '', source: parsed.source || '', other: parsed.other || [], }; } catch (error) { if (this.verbose) { console.log('Anitomy parse error:', filename, '→', error.message); } return { anime_title: '', anime_season: '', episode_number: '', episode_title: '', file_extension: '', release_group: '', video_resolution: '', audio_term: '', source: '', other: [], }; } } generateNewFilename(originalFilename, originalInfo, newEpisodeNumber) { // Debug logging if (this.verbose) { console.log('generateNewFilename input:', { originalFilename, originalInfo, newEpisodeNumber }); } if (!originalInfo.episode_number || !originalInfo.anime_title) { if (this.verbose) { console.log('Missing episode_number or anime_title, returning original filename'); } return originalFilename; } const ext = require('path').extname(originalFilename); const animeTitle = originalInfo.anime_title.replace(/\s+/g, '_'); const seasonNumber = parseInt(originalInfo.anime_season) || 1; const episodeNumber = newEpisodeNumber; const seasonStr = seasonNumber < 10 ? `0${seasonNumber}` : seasonNumber.toString(); const episodeStr = episodeNumber < 10 ? `0${episodeNumber}` : episodeNumber.toString(); const newFilename = `${animeTitle}_S${seasonStr}E${episodeStr}${ext}`; if (this.verbose) { console.log('Generated new filename:', newFilename); } return newFilename; } generateNewFolderName(newEpisodeNumber) { return `E${newEpisodeNumber.toString().padStart(2, '0')}`; } async renameFile(oldPath, newPath) { try { await fs.rename(oldPath, newPath); return true; } catch (error) { throw new Error(`Failed to rename file: ${error.message}`); } } async renameFolder(oldPath, newPath) { try { await fs.rename(oldPath, newPath); return true; } catch (error) { throw new Error(`Failed to rename folder: ${error.message}`); } } async createRenamePreview(directories, startEpisode = 1) { const preview = []; let episodeCounter = startEpisode; for (const dir of directories) { const dirPreview = { originalFolder: dir.name, newFolder: this.generateNewFolderName(episodeCounter), files: [], }; for (const file of dir.files) { const episodeInfo = await this.parseEpisodeInfo(file.name); const newFilename = this.generateNewFilename( file.name, episodeInfo, episodeCounter ); dirPreview.files.push({ originalFile: file.name, newFile: newFilename, episodeInfo: episodeInfo, }); } preview.push(dirPreview); episodeCounter++; } return preview; } async executeRename(directories, preview) { const results = { success: [], errors: [], }; for (let i = 0; i < directories.length; i++) { const dir = directories[i]; const previewItem = preview[i]; try { for (let j = 0; j < dir.files.length; j++) { const file = dir.files[j]; const newFileName = previewItem.files[j].newFile; const newFilePath = path.join(dir.path, newFileName); await this.renameFile(file.path, newFilePath); results.success.push({ type: 'file', old: file.name, new: newFileName, folder: dir.name, }); } const newFolderPath = path.join( path.dirname(dir.path), previewItem.newFolder ); await this.renameFolder(dir.path, newFolderPath); results.success.push({ type: 'folder', old: dir.name, new: previewItem.newFolder, }); } catch (error) { results.errors.push({ folder: dir.name, error: error.message, }); } } return results; } } module.exports = FileService;