1
0
Fork 0
This commit is contained in:
Lukas Wurzinger 2025-01-04 23:51:35 +01:00
parent 0f6a49366e
commit 024ea1168a
No known key found for this signature in database
23 changed files with 485 additions and 101 deletions

19
packages/disk/default.nix Normal file
View file

@ -0,0 +1,19 @@
{
writeShellApplication,
util-linux,
jq,
e2fsprogs,
dosfstools,
}:
writeShellApplication {
name = "disk";
runtimeInputs = [
util-linux
jq
e2fsprogs
dosfstools
];
text = builtins.readFile ./disk.bash;
}

114
packages/disk/disk.bash Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
progname="$0"
error() {
for line in "$@"; do
printf '%s\n' "$progname: $line" 1>&2
done
exit 1
}
args=$(getopt --options r:m:b:l:c: --longoptions=root:,mapping:,boot-label:,main-label:,cryptmain-label: --name "$progname" -- "$@")
eval set -- "$args"
root=/mnt
mapping=main
bootlbl=BOOT
mainlbl=main
cryptmainlbl=cryptmain
while true; do
case "$1" in
(-r | --root)
root=$2
shift 2
;;
(-m | --mapping)
mapping=$2
shift 2
;;
(-b | --boot-label)
bootlbl=${2^^}
shift 2
;;
(-l | --main-label)
mainlbl=$2
shift 2
;;
(-c | --cryptmain-label)
cryptmainlbl=$2
shift 2
;;
(--)
shift
break
;;
esac
done
if (( $# < 1 )); then
error 'an argument specifying the block device is required'
fi
if (( $# > 1 )); then
error 'too many arguments'
fi
blkdev=$1
sfdisk --label gpt --quiet -- "$blkdev" <<EOF
,512M,U;
,,L;
EOF
parts=()
json=$(sfdisk --json -- "$blkdev")
while IFS= read -r k; do
parts+=("$(jq --argjson k "$k" --raw-output '.partitiontable.partitions[$k].node' <<<"$json")")
done < <(jq '.partitiontable.partitions | keys[]' <<<"$json")
bootfs="${parts[0]}"
mainblkdev="${parts[1]}"
mkfs.vfat -F 32 -n "$bootlbl" -- "$bootfs" >/dev/null
while true; do
read -r -p 'Do you want your main partition to be encrypted [y/N]? ' luks
case "$luks" in
([Yy]*)
while true; do
read -r -s -p 'Enter password: ' password
printf '\n'
read -r -s -p 'Re-enter password: ' repassword
printf '\n'
if [[ $password == "$repassword" ]]; then
break
fi
done
cryptsetup luksFormat --batch-mode --label "$cryptmainlbl" "$mainblkdev" <<<"$password"
cryptsetup open "$mainblkdev" "$mapping" <<<"$password"
mainfs=/dev/mapper/$mapping
break
;;
('' | [Nn]*)
mainfs=$mainblkdev
break
;;
(*) printf 'Please answer with yes or no\n' 1>&2 ;;
esac
done
mkfs.ext4 -q -F -L "$mainlbl" -- "$mainfs"
mkdir --parents -- "$root"
mount --options noatime -- "$mainfs" "$root"
mkdir -- "$root/boot"
mount -- "$bootfs" "$root/boot"

View file

@ -0,0 +1 @@

View 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])
];
}

View 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"]

View file

@ -0,0 +1,3 @@
from .cli import main
main()

View 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)

View 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)

View 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)

View 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}"

View 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}")

View file

@ -0,0 +1,11 @@
{
writeShellApplication,
nixos-rebuild,
}:
writeShellApplication {
name = "puter";
runtimeInputs = [
nixos-rebuild
];
text = builtins.readFile ./puter.bash;
}

104
packages/puter/puter.bash Normal file
View file

@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
progname=$0
warn() {
local line
for line in "$@"; do
echo "$progname: $line" 1>&2
done
}
error() {
warn "$@"
exit 1
}
args=$(getopt --options f:o:t:v --longoptions=flake:,on:,to:,verbose --name "$progname" -- "$@")
eval set -- "$args"
host=localhost
flags=(
--refresh
--use-remote-sudo
--no-write-lock-file
)
verbose=false
while true; do
case $1 in
(-f | --flake)
flake=$2
shift 2
;;
(-o | --on)
flags+=(--build-host "$2")
shift 2
;;
(-t | --to)
host=$2
flags+=(--target-host "$host")
shift 2
;;
(-v | --verbose)
flags+=(--verbose)
verbose=true
shift
;;
(--)
shift
break
;;
esac
done
if [[ ! -v flake ]]; then
flake=git+https://forgejo@tea.wrz.one/lukas/puter.git#$(ssh -- "$host" hostname)
fi
flags+=(--flake "$flake")
if (( $# == 0 )); then
error 'a subcommand is required'
fi
run() {
cmd=(nixos-rebuild "${flags[@]}" "$@")
if "$verbose"; then
warn "running ${cmd[*]}"
fi
"${cmd[@]}"
}
sub=$1
case $sub in
(s | switch)
shift
if (( $# > 0 )); then
error 'too many arguments'
fi
run switch
;;
(b | boot)
shift
if (( $# > 0 )); then
error 'too many arguments'
fi
run boot
;;
(*)
error 'invalid subcommand'
;;
esac