various
This commit is contained in:
parent
0f6a49366e
commit
024ea1168a
|
@ -13,5 +13,5 @@
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
config.pubkeys = import (self + /pubkeys.nix);
|
config.pubkeys = lib.mkForce (import (self + /pubkeys.nix));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
{pkgs, ...}: let
|
{
|
||||||
puter = pkgs.writeShellApplication {
|
pkgs,
|
||||||
name = "puter";
|
self,
|
||||||
runtimeInputs = [
|
...
|
||||||
pkgs.nixos-rebuild
|
}: {
|
||||||
];
|
|
||||||
text = builtins.readFile ./puter.bash;
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
puter
|
self.packages.${pkgs.system}.puter
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
{
|
{
|
||||||
|
languages.python.enable = true;
|
||||||
|
|
||||||
pre-commit.hooks = {
|
pre-commit.hooks = {
|
||||||
# Nix
|
# Nix
|
||||||
alejandra.enable = true;
|
alejandra.enable = true;
|
||||||
|
@ -10,5 +12,10 @@
|
||||||
|
|
||||||
# Shell
|
# Shell
|
||||||
shellcheck.enable = true;
|
shellcheck.enable = true;
|
||||||
|
|
||||||
|
# Python
|
||||||
|
pyright.enable = true;
|
||||||
|
ruff.enable = true;
|
||||||
|
ruff-format.enable = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
11
flake.nix
11
flake.nix
|
@ -48,7 +48,7 @@
|
||||||
devenv.root = let
|
devenv.root = let
|
||||||
devenvRootFileContent = builtins.readFile inputs.devenv-root.outPath;
|
devenvRootFileContent = builtins.readFile inputs.devenv-root.outPath;
|
||||||
in
|
in
|
||||||
pkgs.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent;
|
self.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent;
|
||||||
|
|
||||||
name = "puter";
|
name = "puter";
|
||||||
|
|
||||||
|
@ -61,7 +61,14 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
packages.disk = pkgs.callPackage ./disk {};
|
packages =
|
||||||
|
self.lib.genAttrs [
|
||||||
|
"puter"
|
||||||
|
"disk"
|
||||||
|
"musicomp"
|
||||||
|
] (
|
||||||
|
name: pkgs.callPackage ./packages/${name} {}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
{
|
|
||||||
self,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: let
|
|
||||||
audiocomp = pkgs.writeShellApplication {
|
|
||||||
name = "audiocomp";
|
|
||||||
runtimeInputs = [
|
|
||||||
pkgs.parallel
|
|
||||||
pkgs.rsync
|
|
||||||
pkgs.openssh
|
|
||||||
];
|
|
||||||
text = let
|
|
||||||
remoteDir = self.nixosConfigurations.abacus.config.services.navidrome.settings.MusicFolder;
|
|
||||||
enc = pkgs.writeShellApplication {
|
|
||||||
name = "enc";
|
|
||||||
runtimeInputs = [
|
|
||||||
pkgs.opusTools
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
src=$1
|
|
||||||
dst=$src
|
|
||||||
dst=''${dst%.flac}.opus
|
|
||||||
dst=/srv/compmusic/''${dst#/srv/music/}
|
|
||||||
|
|
||||||
if [[ -f "$dst" ]]; then
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir --parents -- "$(dirname -- "$dst")"
|
|
||||||
|
|
||||||
echo "encoding ''${src@Q} -> ''${dst@Q}" >&2
|
|
||||||
exec opusenc --quiet --bitrate 96.000 -- "$src" "$dst"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
clean = pkgs.writeShellApplication {
|
|
||||||
name = "clean";
|
|
||||||
text = ''
|
|
||||||
del=$1
|
|
||||||
chk=$del
|
|
||||||
chk=''${chk%.opus}.flac
|
|
||||||
chk=/srv/music/''${chk#/srv/compmusic/}
|
|
||||||
|
|
||||||
if [[ ! -f "$chk" ]]; then
|
|
||||||
echo "deleting ''${del@Q}" >&2
|
|
||||||
rm --force -- "$del"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in ''
|
|
||||||
shopt -s globstar nullglob
|
|
||||||
|
|
||||||
find /srv/music -name '*.flac' -print0 | parallel --null -- ${lib.getExe enc} {}
|
|
||||||
|
|
||||||
find /srv/compmusic -name '*.flac' -exec ${clean} {} \;
|
|
||||||
|
|
||||||
echo syncing >&2
|
|
||||||
rsync --verbose --verbose --archive --update --delete --mkpath --exclude lost+found \
|
|
||||||
--rsh 'ssh -i /etc/ssh/ssh_host_ed25519_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' \
|
|
||||||
-- /srv/compmusic/ root@wrz.one:${remoteDir}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
systemd.services.audiocomp = {
|
|
||||||
description = "Compress and sync music";
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
User = "root";
|
|
||||||
Group = "root";
|
|
||||||
ExecStart = lib.getExe audiocomp;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.timers.audiocomp = {
|
|
||||||
description = "Compress and sync music daily";
|
|
||||||
wantedBy = ["timers.target"];
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "*-*-* 03:00:00";
|
|
||||||
Persistent = true;
|
|
||||||
Unit = "audiocomp.service";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
51
hosts/server/vessel/musicomp.nix
Normal file
51
hosts/server/vessel/musicomp.nix
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
services.musicomp.jobs.main = {
|
||||||
|
music = "/srv/music";
|
||||||
|
comp = "/srv/compmusic";
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "daily";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
inhibitsSleep = true;
|
||||||
|
post = let
|
||||||
|
remoteDir = self.nixosConfigurations.abacus.config.services.navidrome.settings.MusicFolder;
|
||||||
|
rsyncExe = lib.getExe pkgs.rsync;
|
||||||
|
in ''
|
||||||
|
${rsyncExe} \
|
||||||
|
--archive \
|
||||||
|
--recursive \
|
||||||
|
--delete \
|
||||||
|
--update \
|
||||||
|
--mkpath \
|
||||||
|
--verbose --verbose \
|
||||||
|
--exclude lost+found \
|
||||||
|
--rsh 'ssh -i /etc/ssh/ssh_host_ed25519_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' \
|
||||||
|
/srv/compmusic/ root@wrz.one:${remoteDir}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.audiocomp = {
|
||||||
|
description = "Compress and sync music";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "root";
|
||||||
|
Group = "root";
|
||||||
|
ExecStart = lib.getExe audiocomp;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.timers.audiocomp = {
|
||||||
|
description = "Compress and sync music daily";
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "*-*-* 03:00:00";
|
||||||
|
Persistent = true;
|
||||||
|
Unit = "audiocomp.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
10
lib.nix
10
lib.nix
|
@ -1,13 +1,13 @@
|
||||||
lib: _: {
|
lib: _: {
|
||||||
findModules = dirs:
|
findModules = paths:
|
||||||
builtins.concatMap (dir:
|
builtins.concatMap (path:
|
||||||
lib.pipe dir [
|
lib.pipe path [
|
||||||
(lib.fileset.fileFilter (
|
(lib.fileset.fileFilter (
|
||||||
file: file.hasExt "nix"
|
file: file.hasExt "nix"
|
||||||
))
|
))
|
||||||
lib.fileset.toList
|
lib.fileset.toList
|
||||||
])
|
])
|
||||||
dirs;
|
paths;
|
||||||
|
|
||||||
formatHostPort = {
|
formatHostPort = {
|
||||||
host,
|
host,
|
||||||
|
@ -30,6 +30,7 @@ lib: _: {
|
||||||
inputs,
|
inputs,
|
||||||
extraModules ? _: [],
|
extraModules ? _: [],
|
||||||
}: let
|
}: let
|
||||||
|
modulesDir = ./modules;
|
||||||
commonDir = ./common;
|
commonDir = ./common;
|
||||||
classesDir = ./classes;
|
classesDir = ./classes;
|
||||||
hostsDir = ./hosts;
|
hostsDir = ./hosts;
|
||||||
|
@ -47,6 +48,7 @@ lib: _: {
|
||||||
|
|
||||||
modules =
|
modules =
|
||||||
(lib.findModules [
|
(lib.findModules [
|
||||||
|
modulesDir
|
||||||
commonDir
|
commonDir
|
||||||
./classes/${class}
|
./classes/${class}
|
||||||
(classesDir + /${class})
|
(classesDir + /${class})
|
||||||
|
|
120
modules/musicomp.nix
Normal file
120
modules/musicomp.nix
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
utils,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib) types;
|
||||||
|
inherit (utils.systemdUtils.unitOptions) unitOption;
|
||||||
|
in {
|
||||||
|
options.services.musicomp.jobs = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Periodic jobs to run with musicomp.
|
||||||
|
'';
|
||||||
|
# type = types.attrsOf (types.submodule ({name, ...}: {
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
music = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
Source directory.
|
||||||
|
'';
|
||||||
|
example = "/srv/music";
|
||||||
|
};
|
||||||
|
|
||||||
|
comp = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
Destination directory for compressed music.
|
||||||
|
'';
|
||||||
|
example = "/srv/comp";
|
||||||
|
};
|
||||||
|
|
||||||
|
post = lib.mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
Shell commands that are run after compression has finished.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
workers = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 0;
|
||||||
|
description = ''
|
||||||
|
Number of workers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
timerConfig = lib.mkOption {
|
||||||
|
type = lib.types.nullOr (lib.types.attrsOf unitOption);
|
||||||
|
default = {
|
||||||
|
OnCalendar = "daily";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
When to run the job.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = lib.mkPackageOption self.packages.${pkgs.system} "musicomp" {};
|
||||||
|
|
||||||
|
inhibitsSleep = lib.mkOption {
|
||||||
|
default = false;
|
||||||
|
type = lib.types.bool;
|
||||||
|
example = true;
|
||||||
|
description = ''
|
||||||
|
Prevents the system from sleeping while running the job.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Periodic compression jobs to run with musicomp.
|
||||||
|
'';
|
||||||
|
default = {};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
systemd.services =
|
||||||
|
lib.mapAttrs'
|
||||||
|
(
|
||||||
|
name: job:
|
||||||
|
lib.nameValuePair "musicomp-jobs-${name}" {
|
||||||
|
restartIfChanged = false;
|
||||||
|
# TODO
|
||||||
|
wants = ["network-online.target"];
|
||||||
|
after = ["network-online.target"];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
${lib.optionalString job.inhibitsSleep ''
|
||||||
|
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
||||||
|
--mode block \
|
||||||
|
--who musicomp \
|
||||||
|
--what sleep \
|
||||||
|
--why ${lib.escapeShellArg "Scheduled musicomp ${name}"}
|
||||||
|
''}
|
||||||
|
|
||||||
|
${lib.getExe job.package} \
|
||||||
|
${lib.optionalString (job.workers > 0) "--workers ${job.workers}"} \
|
||||||
|
-- ${job.music} ${job.comp}
|
||||||
|
'';
|
||||||
|
|
||||||
|
postStop = job.post;
|
||||||
|
|
||||||
|
serviceConfig.Type = "oneshot";
|
||||||
|
}
|
||||||
|
)
|
||||||
|
config.services.musicomp.jobs;
|
||||||
|
|
||||||
|
systemd.timers =
|
||||||
|
lib.mapAttrs'
|
||||||
|
(name: job:
|
||||||
|
lib.nameValuePair "musicomp-jobs-${name}" {
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
inherit (job) timerConfig;
|
||||||
|
})
|
||||||
|
(lib.filterAttrs (_: job: job.timerConfig != null) config.services.musicomp.jobs);
|
||||||
|
};
|
||||||
|
}
|
1
packages/musicomp/README.md
Normal file
1
packages/musicomp/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
19
packages/musicomp/default.nix
Normal file
19
packages/musicomp/default.nix
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
python3Packages,
|
||||||
|
opusTools,
|
||||||
|
}:
|
||||||
|
python3Packages.buildPythonApplication {
|
||||||
|
pname = "musicomp";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
pyproject = true;
|
||||||
|
doCheck = false;
|
||||||
|
build-system = [python3Packages.hatchling];
|
||||||
|
makeWrapperArgs = [
|
||||||
|
"--prefix"
|
||||||
|
"PATH"
|
||||||
|
":"
|
||||||
|
(lib.makeBinPath [opusTools])
|
||||||
|
];
|
||||||
|
}
|
19
packages/musicomp/pyproject.toml
Normal file
19
packages/musicomp/pyproject.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[project]
|
||||||
|
name = "musicomp"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
{name = "Lukas Wurzinger", email = "lukas@wrz.one"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
musicomp = "musicomp.cli:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/musicomp"]
|
3
packages/musicomp/src/musicomp/__main__.py
Normal file
3
packages/musicomp/src/musicomp/__main__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
6
packages/musicomp/src/musicomp/clean.py
Normal file
6
packages/musicomp/src/musicomp/clean.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from os import PathLike
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def clean(dst: str | PathLike[str]) -> None:
|
||||||
|
Path(dst).unlink(missing_ok=True)
|
118
packages/musicomp/src/musicomp/cli.py
Normal file
118
packages/musicomp/src/musicomp/cli.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import sys
|
||||||
|
from multiprocessing import Pool, cpu_count
|
||||||
|
from argparse import ArgumentParser, ArgumentTypeError, Namespace
|
||||||
|
from pathlib import Path
|
||||||
|
from .todo import Todo
|
||||||
|
|
||||||
|
|
||||||
|
args = Namespace()
|
||||||
|
|
||||||
|
|
||||||
|
def task(todo: Todo) -> None:
|
||||||
|
todo.run()
|
||||||
|
if args.verbose:
|
||||||
|
print("finished", todo, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
def workers_type_func(value: object) -> int:
|
||||||
|
try:
|
||||||
|
value = int(value) # pyright: ignore[reportArgumentType]
|
||||||
|
if value <= 0:
|
||||||
|
raise ArgumentTypeError(f"{value} is not a positive integer")
|
||||||
|
except ValueError:
|
||||||
|
raise ArgumentTypeError(f"{value} is not an integer")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
parser = ArgumentParser()
|
||||||
|
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--workers",
|
||||||
|
default=cpu_count(),
|
||||||
|
type=workers_type_func,
|
||||||
|
help="amount of worker processes",
|
||||||
|
)
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--interactive",
|
||||||
|
action="store_true",
|
||||||
|
help="prompt before running",
|
||||||
|
)
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"-k",
|
||||||
|
"--keep",
|
||||||
|
action="store_true",
|
||||||
|
help="whether source files should be kept if both directories are the same",
|
||||||
|
)
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--redo",
|
||||||
|
action="store_true",
|
||||||
|
help="whether everything should be re-encoded regardless of whether they have already been transcoded",
|
||||||
|
)
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="count",
|
||||||
|
default=0,
|
||||||
|
help="verbose output",
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"music",
|
||||||
|
nargs=1,
|
||||||
|
type=Path,
|
||||||
|
help="the source directory",
|
||||||
|
)
|
||||||
|
_ = parser.add_argument(
|
||||||
|
"comp",
|
||||||
|
nargs=1,
|
||||||
|
type=Path,
|
||||||
|
help="the destination directory for compressed files",
|
||||||
|
)
|
||||||
|
|
||||||
|
global args
|
||||||
|
args = parser.parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
assert isinstance(args.workers, int) # pyright: ignore[reportAny]
|
||||||
|
assert isinstance(args.interactive, bool) # pyright: ignore[reportAny]
|
||||||
|
assert isinstance(args.keep, bool) # pyright: ignore[reportAny]
|
||||||
|
assert isinstance(args.redo, bool) # pyright: ignore[reportAny]
|
||||||
|
assert isinstance(args.verbose, int) # pyright: ignore[reportAny]
|
||||||
|
assert isinstance(args.music, list) # pyright: ignore[reportAny]
|
||||||
|
assert len(args.music) == 1 # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
|
||||||
|
assert isinstance(args.music[0], Path) # pyright: ignore[reportUnknownMemberType]
|
||||||
|
assert isinstance(args.comp, list) # pyright: ignore[reportAny]
|
||||||
|
assert len(args.comp) == 1 # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
|
||||||
|
assert isinstance(args.comp[0], Path) # pyright: ignore[reportUnknownMemberType]
|
||||||
|
|
||||||
|
src_dir = args.music[0] # pyright: ignore[reportUnknownMemberType]
|
||||||
|
dst_dir = args.comp[0] # pyright: ignore[reportUnknownMemberType]
|
||||||
|
|
||||||
|
plan = list(
|
||||||
|
Todo.plan(
|
||||||
|
src_dir,
|
||||||
|
dst_dir,
|
||||||
|
replace=src_dir.samefile(dst_dir) and not args.keep,
|
||||||
|
redo=args.redo,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(plan) == 0:
|
||||||
|
print("Nothing to do", file=sys.stderr)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.verbose >= 1 or args.interactive:
|
||||||
|
print("Plan:", file=sys.stderr)
|
||||||
|
for todo in plan:
|
||||||
|
print(todo, file=sys.stderr)
|
||||||
|
|
||||||
|
if args.interactive:
|
||||||
|
result = input("Do you want to continue? [Y/n] ")
|
||||||
|
if result.lower() not in ("", "y", "yes"):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with Pool(args.workers) as pool:
|
||||||
|
_ = pool.map(task, plan)
|
6
packages/musicomp/src/musicomp/replace.py
Normal file
6
packages/musicomp/src/musicomp/replace.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from os import PathLike
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def replace(src: str | PathLike[str]) -> None:
|
||||||
|
Path(src).unlink(missing_ok=True)
|
68
packages/musicomp/src/musicomp/todo.py
Normal file
68
packages/musicomp/src/musicomp/todo.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from os import PathLike
|
||||||
|
from enum import StrEnum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Self, override
|
||||||
|
from collections.abc import Generator
|
||||||
|
from .transcode import transcode as todo_transcode
|
||||||
|
from .clean import clean as todo_clean
|
||||||
|
from .replace import replace as todo_replace
|
||||||
|
|
||||||
|
SRC_SUFFIX = ".flac"
|
||||||
|
DST_SUFFIX = ".opus"
|
||||||
|
|
||||||
|
|
||||||
|
class TodoAct(StrEnum):
|
||||||
|
TRANSCODE = "transcode"
|
||||||
|
CLEAN = "clean"
|
||||||
|
REPLACE = "replace"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Todo:
|
||||||
|
act: TodoAct
|
||||||
|
src: str | PathLike[str]
|
||||||
|
dst: str | PathLike[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def plan(
|
||||||
|
cls: type[Self],
|
||||||
|
src_dir: str | PathLike[str],
|
||||||
|
dst_dir: str | PathLike[str],
|
||||||
|
replace: bool = False,
|
||||||
|
redo: bool = False,
|
||||||
|
) -> Generator[Self]:
|
||||||
|
def list_files(dir: str | PathLike[str], suffix: str) -> list[Path]:
|
||||||
|
files: list[Path] = []
|
||||||
|
for f in Path(dir).rglob("*"):
|
||||||
|
if f.is_file() and f.suffix == suffix:
|
||||||
|
files.append(f)
|
||||||
|
return files
|
||||||
|
|
||||||
|
src_files = list_files(src_dir, SRC_SUFFIX)
|
||||||
|
dst_files = list_files(dst_dir, DST_SUFFIX)
|
||||||
|
|
||||||
|
for f in src_files:
|
||||||
|
e = dst_dir / (f.relative_to(src_dir).with_suffix(DST_SUFFIX))
|
||||||
|
if redo or e not in dst_files:
|
||||||
|
yield cls(TodoAct.TRANSCODE, f, e)
|
||||||
|
if replace:
|
||||||
|
yield cls(TodoAct.REPLACE, f, e)
|
||||||
|
|
||||||
|
for f in dst_files:
|
||||||
|
e = src_dir / (f.relative_to(dst_dir).with_suffix(SRC_SUFFIX))
|
||||||
|
if e not in src_files:
|
||||||
|
yield cls(TodoAct.CLEAN, f, e)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
match self.act:
|
||||||
|
case TodoAct.TRANSCODE:
|
||||||
|
todo_transcode(self.src, self.dst)
|
||||||
|
case TodoAct.CLEAN:
|
||||||
|
todo_clean(self.dst)
|
||||||
|
case TodoAct.REPLACE:
|
||||||
|
todo_replace(self.src)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.act} {self.src} -> {self.dst}"
|
34
packages/musicomp/src/musicomp/transcode.py
Normal file
34
packages/musicomp/src/musicomp/transcode.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import run
|
||||||
|
from os import PathLike
|
||||||
|
|
||||||
|
|
||||||
|
class TranscodingError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def transcode(src: str | PathLike[str], dst: str | PathLike[str]) -> None:
|
||||||
|
dst = Path(dst)
|
||||||
|
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if dst.is_file():
|
||||||
|
dst.unlink()
|
||||||
|
|
||||||
|
opusenc: tuple[str, ...] = (
|
||||||
|
"opusenc",
|
||||||
|
"--quiet",
|
||||||
|
"--bitrate",
|
||||||
|
"96.000",
|
||||||
|
"--music",
|
||||||
|
"--vbr",
|
||||||
|
"--comp",
|
||||||
|
"10",
|
||||||
|
"--",
|
||||||
|
str(src),
|
||||||
|
str(dst),
|
||||||
|
)
|
||||||
|
|
||||||
|
cp = run(opusenc)
|
||||||
|
if cp.returncode != 0:
|
||||||
|
raise TranscodingError(f"opusenc exited with code {cp.returncode}")
|
11
packages/puter/default.nix
Normal file
11
packages/puter/default.nix
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
writeShellApplication,
|
||||||
|
nixos-rebuild,
|
||||||
|
}:
|
||||||
|
writeShellApplication {
|
||||||
|
name = "puter";
|
||||||
|
runtimeInputs = [
|
||||||
|
nixos-rebuild
|
||||||
|
];
|
||||||
|
text = builtins.readFile ./puter.bash;
|
||||||
|
}
|
Loading…
Reference in a new issue