fs/tidy-movies
Usage
npx @lukasbach/scripts fs/tidy-movies
You can call the script directly if you have installed it globally:
npm i -g @lukasbach/scripts
ldo fs/tidy-movies
Options
--api-key
: What is your TMDB API key? (https://www.themoviedb.org/settings/api)--root
: What is the root folder to search?--extensions
: What are the file extensions to search for?--target
: What is the target folder?-v
,--verbose
: Verbose logging
You can also omit options, and will be asked for them interactively.
Add --yes
to skip all confirmations.
Script source
/* eslint-disable @typescript-eslint/no-use-before-define */
/** Tidy a folder of movies */
import * as path from "path";
const apiKey = await ask.text("api-key", "What is your TMDB API key? (https://www.themoviedb.org/settings/api)");
const rootSearch = await ask.text("root", "What is the root folder to search?", ".");
const extensions = await ask.text("extensions", "What are the file extensions to search for?", "mkv,mp4,iso");
const target = await ask.text("target", "What is the target folder?", "dist");
let files = await glob(`**/*.{${extensions}}`, { cwd: rootSearch, absolute: true });
const sampleFiles = files.filter((f) => f.toLowerCase().includes("sample"));
if (sampleFiles.length > 0) {
log.info(`Sample files found: ${sampleFiles.join(", ")}`);
if (await ask.confirm("Do you want to exclude these files?")) {
files = files.filter((f) => !sampleFiles.includes(f));
}
}
for (const file of files) {
log.info(`Processing ${file}`);
const searchString = toSearchString(path.basename(file));
const result = await tryToResolveMovie(searchString, file);
if (result === null) {
continue;
}
const safeTitle = result.title.replace(/:\s+/g, " - ").replace(/[^a-z0-9\- ]/gi, "_");
const dist = path.join(target, `${safeTitle} (${result.release_date})`);
const associatedFiles = (await glob(`${path.dirname(file)}/**/*`, { absolute: true })).filter((f) => f !== file);
let matchedAssociates: string[] = [];
if (associatedFiles.length > 30) {
log.warn(`More than 30 associated files found for ${file}, they will not be included...`);
} else {
matchedAssociates = await ask.multiChoice(
null,
`Found ${associatedFiles.length} associated files, which to include?`,
associatedFiles.map((f) => ({
value: path.relative(path.dirname(file), f),
checked: true,
}))
);
}
await fs.ensureDir(dist);
await fs.move(file, path.join(dist, path.basename(file)));
for (const associate of matchedAssociates) {
const from = path.join(path.dirname(file), associate);
if (fs.lstatSync(from).isDirectory()) {
continue;
}
await fs.move(from, path.join(dist, associate));
}
log.success(`Moved ${file} to ${dist}`);
}
function toSearchString(str: string) {
return path
.basename(str, path.extname(str))
.toLowerCase()
.replace(/[\s.,_-]/g, " ")
.replace(/[^a-z0-9\s]/g, "");
}
async function searchMovie(query: string): Promise<TMDBResult> {
return got(`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&query=${query}`).json();
}
async function tryToResolveMovie(query: string, fileName: string): Promise<TMDBResult["results"][number] | null> {
const result = await searchMovie(query);
if (result.results.length === 0) {
log.warn(`No results found for ${query}`);
return tryToResolveMovie(await ask.text(null, `Search for ${fileName}`), fileName);
}
const askResult = await ask.choice(null, `Found ${result.results.length} results for ${query}`, [
...result.results.slice(0, 5).map((r) => ({ value: r.id, name: `${r.title} (${r.release_date})` })),
{ value: "retry", name: "Search again..." },
{ value: "cancel", name: "Skip file..." },
]);
if (askResult === "retry") {
return tryToResolveMovie(await ask.text(null, `Search for ${fileName}`), fileName);
}
if (askResult === "cancel") {
return null;
}
const chosenResult = result.results.find((r) => `${r.id}` === `${askResult}`);
if (!chosenResult) {
log.error(`Could not find chosen result ${askResult}`);
return tryToResolveMovie(await ask.text(null, `Search for ${fileName}`), fileName);
}
return chosenResult;
}
type TMDBResult = {
results: { backdrop_path: string; title: string; vote_average: number; release_date: string; id: number }[];
};