Skip to content

Instantly share code, notes, and snippets.

@lpenaud
Last active January 28, 2025 22:19
Show Gist options
  • Select an option

  • Save lpenaud/d23049fdfb4f4cff70accd64ac8f1004 to your computer and use it in GitHub Desktop.

Select an option

Save lpenaud/d23049fdfb4f4cff70accd64ac8f1004 to your computer and use it in GitHub Desktop.
Create physical link between download and media directory
#!/usr/bin/env -S deno run --allow-read --allow-write
import { walk } from "https://deno.land/[email protected]/fs/walk.ts";
import * as cli from "https://deno.land/[email protected]/cli/mod.ts";
import * as path from "https://deno.land/[email protected]/path/mod.ts";
interface UnixFilesystemOptions {
uid: number | null;
gid: number | null;
filemod: number;
dirmod: number;
}
interface CliArgs {
fs: Filesystem;
indirs: string[];
outdir: string;
}
class DryFilesystem {
async mkdirp(dir: string): Promise<void> {
console.log("Make directory", dir);
}
async ln(infile: string, outfile: string): Promise<void> {
console.log(infile, "->", outfile);
}
}
class Filesystem extends DryFilesystem {
async mkdirp(dir: string): Promise<void> {
await Promise.all([
super.mkdirp(dir),
Deno.mkdir(dir, { recursive: true }),
]);
}
async ln(infile: string, outfile: string): Promise<void> {
await Promise.all([
super.ln(infile, outfile),
Deno.link(infile, outfile),
]);
}
}
class UnixFilesystem extends Filesystem {
#uid: number | null;
#gid: number | null;
#filemod: number;
#dirmod: number;
constructor({ uid, gid, filemod, dirmod }: UnixFilesystemOptions) {
super();
this.#uid = uid;
this.#gid = gid;
this.#filemod = filemod;
this.#dirmod = dirmod;
}
async mkdirp(dir: string): Promise<void> {
await super.mkdirp(dir);
await Deno.chown(dir, this.#uid, this.#gid);
await Deno.chmod(dir, this.#dirmod);
}
async ln(infile: string, outfile: string): Promise<void> {
await super.ln(infile, outfile);
await Deno.chown(outfile, this.#uid, this.#gid);
await Deno.chmod(outfile, this.#filemod);
}
}
function usage(code: number = 0, message?: string): number {
const data: string[] = [
"Usage:",
import.meta.filename ?? import.meta.url,
"[OPTIONS]",
"INDIR",
"OUTDIR",
];
if (code === 0) {
if (message) {
console.log(message);
}
console.log(...data);
return code;
}
if (message) {
console.error(message);
}
console.error(...data);
return code;
}
function printHelp(): number {
usage();
console.log(`
OPTIONS
--uid UID Uid to use (default null)
--gid GID Gid to use (default null)
--dirmod DIRMOD Directory mode (default 775)
--filemod FILEMOD File mode (default 664)
POSITIONALS
INDIR Input directory
OUTDIR Output directory
`);
return 0;
}
function parseArgs(args: string[]): CliArgs | 0 | 1 {
const { uid, gid, filemod, dirmod, help, dryrun, _: extra } = cli.parseArgs(
args,
{
string: ["dirmod", "filemod", "gid", "uid"],
boolean: ["help", "dryrun"],
alias: {
help: "h",
},
default: {
uid: "",
gid: "",
dirmod: "775",
filemod: "664",
},
},
);
if (help) {
return 0;
}
const outdir = extra.pop();
if (outdir === undefined) {
console.error("OUTDIR is required options");
return 1;
}
if (extra.length === 0) {
console.error("INDIR and OUTDIR are required options");
return 1;
}
return {
fs: dryrun
? new DryFilesystem()
: Deno.build.os === "windows"
? new Filesystem()
: new UnixFilesystem({
uid: parseInt(uid, 10) || null,
gid: parseInt(gid, 10) || null,
dirmod: parseInt(dirmod, 8) || 0o775,
filemod: parseInt(filemod, 8) || 0o664,
}),
indirs: extra.map((v) => path.resolve(v.toString())),
outdir: path.resolve(outdir.toString()),
};
}
function* multiMatch(re: RegExp, str: string) {
let m;
while ((m = re.exec(str)) !== null) {
yield m;
}
}
function getPath(outdir: string, { name: src }) {
const prefix: string[] = [];
for (const [word] of multiMatch(/\w+/g, src)) {
const m = /(S\d+)E\d+/.exec(word);
if (m !== null) {
// Remove date to filename
const [ep, season] = m;
const name = (/^\d{4}$/.test(prefix[prefix.length - 1])
? prefix.slice(0, -1)
: prefix)
.concat(ep)
.join(".");
const ext = path.extname(src);
return {
dir: path.join(outdir, prefix.join("."), season),
name,
ext,
};
}
prefix.push(word);
}
return null;
}
async function* tree(indir: string, outdir: string) {
for await (
const entry of walk(indir, {
includeDirs: false,
exts: [".mkv", ".mp4"],
})
) {
const outpath = getPath(outdir, entry);
if (outpath !== null) {
yield {
infile: entry.path,
outpath,
};
}
}
}
async function main(args: string[]): Promise<number> {
const parsed = parseArgs(args);
if (parsed === 0) {
return printHelp();
}
if (parsed === 1) {
return usage(parsed);
}
const { fs, indirs, outdir } = parsed;
const trees = await Promise.all(
indirs.map((v) => Array.fromAsync(tree(v, outdir))),
);
const outfiles = trees.flatMap((v) => v);
const dirs = outfiles
.reduce((s, v) => s.add(v.outpath.dir), new Set<string>());
for (const d of dirs) {
await fs.mkdirp(d);
}
await Promise.all(
outfiles.map(({ infile, outpath }) => fs.ln(infile, path.format(outpath))),
);
return 0;
}
if (import.meta.main) {
main(Deno.args.slice())
.then(Deno.exit)
.catch((err) => {
console.error(err);
Deno.exit(2);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment