import fs from 'fs-extra'; import path from 'path'; import https from 'https'; import inquirer from 'inquirer'; import { fileURLToPath } from 'url'; import { loadConfig } from './config.js'; import { sanitizeMediaString } from './sanitize.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const downloadImage = (url, dest) => new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, response => { if (response.statusCode !== 200) return reject(new Error(`Failed to download. Status: ${response.statusCode}`)); response.pipe(file); file.on('finish', () => file.close(resolve)); }).on('error', reject); }); const isValidTMDBUrl = (val) => /^https:\/\/image\.tmdb\.org\/t\/p\//.test(val) || '❌ Must be a valid TMDB image url'; const overwriteImageDownloadPrompt = async (url, finalPath, fileName) => { await fs.ensureDir(path.dirname(finalPath)); if (await fs.pathExists(finalPath)) { const { overwrite } = await inquirer.prompt({ type: 'confirm', name: 'overwrite', message: `${fileName} already exists. Overwrite?`, default: false }); if (!overwrite) { console.log(`⚠️ Skipped existing file: ${fileName}`); return; } } await downloadImage(url, finalPath); console.log(`✅ Saved to: ${finalPath}`); }; export const downloadWatchingImages = async () => { const config = await loadConfig(); const { mediaType } = await inquirer.prompt({ type: 'list', name: 'mediaType', message: 'Movie or a show?', choices: Object.keys(config.mediaPaths) }); const { tmdbId } = await inquirer.prompt({ name: 'tmdbId', message: 'Enter the TMDB ID:' }); if (!tmdbId) { console.warn('⚠️ TMDB ID is required.'); return; } const { posterUrl, backdropUrl } = await inquirer.prompt([{ name: 'posterUrl', message: 'Enter the poster url:', validate: (val) => { if (!val) return true; return isValidTMDBUrl(val); } }, { name: 'backdropUrl', message: 'Enter the backdrop url:', validate: (val) => { if (!val) return true; return isValidTMDBUrl(val); } }]); const types = [{ type: 'poster', url: posterUrl }, { type: 'backdrop', url: backdropUrl }]; for (const { type, url } of types) { if (!url) continue; const ext = path.extname(new URL(url).pathname) || '.jpg'; const fileName = `${type}-${tmdbId}${ext}`; const targetSubPath = config.mediaPaths[mediaType][type]; if (!targetSubPath) { console.error(`❌ Missing path for ${mediaType}/${type}. Check your config.`); continue; } const targetDir = path.join(config.storageDir, targetSubPath); const finalPath = path.join(targetDir, fileName); await overwriteImageDownloadPrompt(url, finalPath, fileName); } } export const downloadArtistImage = async () => { const config = await loadConfig(); const { artistName } = await inquirer.prompt({ name: 'artistName', message: 'Enter the artist name:' }); if (!artistName) { console.warn('⚠️ Artist name is required.'); return; } const { imageUrl } = await inquirer.prompt({ name: 'imageUrl', message: 'Enter the artist image url:', validate: (val) => { try { new URL(val); return true; } catch { return '❌ Must be a valid url.'; } } }); const sanitizedName = sanitizeMediaString(artistName); const ext = path.extname(new URL(imageUrl).pathname) || '.jpg'; const fileName = `${sanitizedName}${ext}`; const targetDir = path.join(config.storageDir, 'artists'); const finalPath = path.join(targetDir, fileName); await overwriteImageDownloadPrompt(imageUrl, finalPath, fileName); } export const downloadAlbumImage = async () => { const config = await loadConfig(); const { artistName } = await inquirer.prompt({ name: 'artistName', message: 'Enter the artist name:' }); if (!artistName) { console.warn('⚠️ Artist name is required.'); return; } const { albumName } = await inquirer.prompt({ name: 'albumName', message: 'Enter the album name:' }); if (!albumName) { console.warn('⚠️ Album name is required.'); return; } const { imageUrl } = await inquirer.prompt({ name: 'imageUrl', message: 'Enter the album image url:', validate: (val) => { try { new URL(val); return true; } catch { return '❌ Must be a valid url.'; } } }); const artistSlug = sanitizeMediaString(artistName); const albumSlug = sanitizeMediaString(albumName); const ext = path.extname(new URL(imageUrl).pathname) || '.jpg'; const fileName = `${artistSlug}-${albumSlug}${ext}`; const targetDir = path.join(config.storageDir, config.albumPath); const finalPath = path.join(targetDir, fileName); await overwriteImageDownloadPrompt(imageUrl, finalPath, fileName); }; export const downloadBookImage = async () => { const config = await loadConfig(); const { isbn } = await inquirer.prompt({ name: 'isbn', message: 'Enter the ISBN (no spaces):', validate: (val) => /^[a-zA-Z0-9-]+$/.test(val) || 'ISBN must contain only letters, numbers, or hyphens' }); const { bookTitle } = await inquirer.prompt({ name: 'bookTitle', message: 'Enter the book title:' }); if (!bookTitle) { console.warn('⚠️ Book title is required.'); return; } const { imageUrl } = await inquirer.prompt({ name: 'imageUrl', message: 'Enter the book cover image URL:', validate: (val) => { try { new URL(val); return true; } catch { return 'Must be a valid URL'; } } }); const titleSlug = sanitizeMediaString(bookTitle); const ext = path.extname(new URL(imageUrl).pathname) || '.jpg'; const fileName = `${isbn}-${titleSlug}${ext}`; const targetDir = path.join(config.storageDir, config.bookPath); const finalPath = path.join(targetDir, fileName); await overwriteImageDownloadPrompt(imageUrl, finalPath, fileName); } export const downloadAsset = async () => { const { type } = await inquirer.prompt({ type: 'list', name: 'type', message: 'What type of asset are you downloading?', choices: ['movie/show', 'artist', 'album', 'book'] }); if (type === 'artist') { await downloadArtistImage(); } else if (type === 'album') { await downloadAlbumImage(); } else if (type === 'book') { await downloadBookImage(); } else { await downloadWatchingImages(); } }