coryd.dev/cli/lib/download.js

259 lines
6.6 KiB
JavaScript

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();
}
};