Last active
January 28, 2025 22:19
-
-
Save lpenaud/d23049fdfb4f4cff70accd64ac8f1004 to your computer and use it in GitHub Desktop.
Create physical link between download and media directory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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