import fs from "fs-extra"; import path from "path"; import https from "https"; import inquirer from "inquirer"; import { fileURLToPath } from "url"; import { loadConfig, MEDIA_TYPES } 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 tv show?", choices: MEDIA_TYPES.map(({ key, label }) => ({ name: label, value: key })) }); 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/tv show", "artist", "album", "book"] }); if (type === "artist") { await downloadArtistImage(); } else if (type === "album") { await downloadAlbumImage(); } else if (type === "book") { await downloadBookImage(); } else { await downloadWatchingImages(); } };