From 7da0c922add672b7c71638b532e82a60ebbab0d3 Mon Sep 17 00:00:00 2001 From: Mark Hoekveen Date: Sat, 8 Aug 2026 11:58:01 +0200 Subject: [PATCH] updates and backlog --- bin/avif | 91 ++++++++++++++++++++++++++++++ bin/dctr | 140 ++++++++++++++++++++++++++++++++++++++++++++++ bin/pmvg | 63 +++++++++++++++++++++ bin/quitif | 32 +++++++++++ bin/tclean | 4 +- bin/vc | 2 +- bin/vcmp | 92 +++++++++++++++++++++++++----- bin/vids | 22 ++++++++ utils/pwm.service | 16 ++++++ 9 files changed, 444 insertions(+), 18 deletions(-) create mode 100755 bin/avif create mode 100755 bin/dctr create mode 100755 bin/pmvg create mode 100755 bin/quitif create mode 100755 bin/vids create mode 100644 utils/pwm.service diff --git a/bin/avif b/bin/avif new file mode 100755 index 0000000..76f765f --- /dev/null +++ b/bin/avif @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import re +import shutil +import tempfile +from pathlib import Path +from typing import Optional, Dict, Set + +def check_dependencies() -> bool: + """Check if required commands are available.""" + for cmd in ['file', 'avifenc']: + if not shutil.which(cmd): + print(f"Error: {cmd} command not found") + return False + return True + +def convert_file(filepath: Path, output_path: Path, quality: int = 60) -> bool: + """Convert file to AVIF using avifenc""" + try: + subprocess.run(['avifenc', '-q', str(quality), str(filepath), str(output_path)], check=True) + return True + except subprocess.CalledProcessError as e: + print(f"Error converting {filepath} to AVIF: {e}") + return False + +def delete_file(filepath: Path) -> bool: + """Delete file""" + try: + filepath.unlink() + return True + except OSError as e: + print(f"Error deleting {filepath}: {e}") + return False + +def process_directory(directory: Path): + """Process a single directory.""" + print(f"Processing directory: {directory}") + + # Get all image files in directory + image_files = sorted( + path for path in directory.iterdir() + if path.is_file() and path.suffix.lower() in {'.jpg', '.jpeg', '.png'} + ) + + for filepath in image_files: + print(f"Processing: {filepath.name}") + + # Convert to avif + if convert_file(filepath, filepath.with_suffix('.avif')): + print(f"Converted {filepath.name} to AVIF") + if delete_file(filepath): + print(f"Deleted {filepath.name}") + else: + print(f"Failed to delete {filepath.name}") + else: + print(f"Failed to convert {filepath.name} to AVIF") + + print(f"Done! Processed {len(image_files)} files in {directory}") + +def main(): + if not check_dependencies(): + sys.exit(1) + + # Get directories to process + if len(sys.argv) == 1: + directories = [Path('.')] + else: + recursive = sys.argv[1] in {'-r', '--recursive'} + start_idx = 2 if recursive else 1 + + if recursive: + directories = [] + for dir_arg in sys.argv[start_idx:]: + path = Path(dir_arg) + if path.is_dir(): + directories.extend(p for p in path.rglob('.') if p.is_dir()) + else: + directories = [Path(dir_arg) for dir_arg in sys.argv[start_idx:] if Path(dir_arg).is_dir()] + + # Process each directory + for directory in directories: + try: + process_directory(directory) + except Exception as e: + print(f"Error processing directory {directory}: {e}") + +if __name__ == "__main__": + main() diff --git a/bin/dctr b/bin/dctr new file mode 100755 index 0000000..70e1166 --- /dev/null +++ b/bin/dctr @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import re +import shutil +import tempfile +from pathlib import Path +from typing import Optional, Dict, Set + +def check_dependencies() -> bool: + """Check if required commands are available.""" + for cmd in ['file', 'dctfilename']: + if not shutil.which(cmd): + print(f"Error: {cmd} command not found") + return False + return True + +def is_processed_file(filename: str) -> bool: + """Check if filename matches our format (NNNN_HASH.ext).""" + pattern = r'^[0-9]{4}_[0-9a-f]{16}\.(jpg|jpeg|png)$' + return bool(re.match(pattern, filename.lower())) + +def extract_hash(filename: str) -> Optional[str]: + """Extract hash from processed filename.""" + match = re.match(r'^[0-9]{4}_([0-9a-f]{16})\.[^.]+$', filename.lower()) + return match.group(1) if match else None + +def get_file_hash(filepath: Path) -> str: + """Get hash for a file using dctfilename.""" + try: + # Create a temporary file for processing + with tempfile.NamedTemporaryFile(suffix=filepath.suffix) as tmp: + temp_path = Path(tmp.name) + try: + # Try hardlink first + os.link(str(filepath), tmp.name) + except OSError: + # Fall back to copy if hardlink fails + shutil.copy2(str(filepath), tmp.name) + + result = subprocess.run(['dctfilename', tmp.name], + capture_output=True, text=True, check=True) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error getting hash for {filepath}: {e}") + return "" + except Exception as e: + print(f"Unexpected error processing {filepath}: {e}") + return "" + +def process_directory(directory: Path): + """Process a single directory.""" + print(f"Processing directory: {directory}") + + # Track files and their information + seen_hashes: Dict[str, Path] = {} # hash -> filepath + hash_sizes: Dict[str, int] = {} # hash -> filesize + + # Get all image files in directory + image_files = sorted( + path for path in directory.iterdir() + if path.is_file() and path.suffix.lower() in {'.jpg', '.jpeg', '.png'} + ) + + # First pass: gather information and handle duplicates + kept_files = [] # Files to keep and rename + for filepath in image_files: + print(f"Processing: {filepath.name}") + + # Get hash (either from filename or calculate) + if is_processed_file(filepath.name): + file_hash = extract_hash(filepath.name) + print(f"Using existing hash from filename: {file_hash}") + else: + file_hash = get_file_hash(filepath) + print(f"Calculated new hash: {file_hash}") + + if not file_hash: + print(f"Skipping {filepath.name} due to hash error") + continue + + filesize = filepath.stat().st_size + + if file_hash in seen_hashes: + # Found duplicate + if filesize > hash_sizes[file_hash]: + print(f"Found larger duplicate: {filepath.name} replaces {seen_hashes[file_hash].name}") + seen_hashes[file_hash].unlink() # Remove smaller file + seen_hashes[file_hash] = filepath + hash_sizes[file_hash] = filesize + kept_files.append((filepath, file_hash)) + else: + print(f"Removing smaller duplicate: {filepath.name}") + filepath.unlink() + else: + seen_hashes[file_hash] = filepath + hash_sizes[file_hash] = filesize + kept_files.append((filepath, file_hash)) + + # Second pass: rename files + for index, (filepath, file_hash) in enumerate(sorted(kept_files, key=lambda x: x[0].name), 1): + new_name = filepath.parent / f"{index:04d}_{file_hash}{filepath.suffix.lower()}" + + if filepath != new_name: + print(f"Renaming: {filepath.name} -> {new_name.name}") + filepath.rename(new_name) + + print(f"Done! Processed {len(kept_files)} files in {directory}") + +def main(): + if not check_dependencies(): + sys.exit(1) + + # Get directories to process + if len(sys.argv) == 1: + directories = [Path('.')] + else: + recursive = sys.argv[1] in {'-r', '--recursive'} + start_idx = 2 if recursive else 1 + + if recursive: + directories = [] + for dir_arg in sys.argv[start_idx:]: + path = Path(dir_arg) + if path.is_dir(): + directories.extend(p for p in path.rglob('.') if p.is_dir()) + else: + directories = [Path(dir_arg) for dir_arg in sys.argv[start_idx:] if Path(dir_arg).is_dir()] + + # Process each directory + for directory in directories: + try: + process_directory(directory) + except Exception as e: + print(f"Error processing directory {directory}: {e}") + +if __name__ == "__main__": + main() diff --git a/bin/pmvg b/bin/pmvg new file mode 100755 index 0000000..049c520 --- /dev/null +++ b/bin/pmvg @@ -0,0 +1,63 @@ +#!/usr/bin/python + +import argparse +import shutil +import sqlite3 +import tempfile +import os + +def main(): + parser = argparse.ArgumentParser(description='Command line arguments') + parser.add_argument('-d', '--db', required=True, help='database location') + parser.add_argument('-t', '--tag', type=int, required=False, help='tag id') + parser.add_argument('-n', '--number', type=int, default=4, help='number') + args = parser.parse_args() + + # Copy db to temp location + temp_dir = tempfile.gettempdir() + temp_db = os.path.join(temp_dir, 'pmvg.db') + if not os.path.exists(temp_db): + shutil.copy2(args.db, temp_db) + + # Open temp db with sqlite + conn = sqlite3.connect(temp_db) + cursor = conn.cursor() + + # Print names of all tables + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = cursor.fetchall() + print("Tables:") + for table in tables: + print(table[0]) + print() + + # Print schema of scenes_tags table + cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='scenes_tags'") + schema = cursor.fetchone() + print("Schema for scenes_tags:") + print(schema[0]) + print() + + # Print data of tags table if tag id not provided + if not args.tag: + cursor.execute("SELECT * FROM tags") + tags = cursor.fetchall() + print("Table: tags") + for tag in tags: + print(tag) + print("Select a tag with -t") + exit() + + # Get random scenes based on tag id + cursor.execute(f"SELECT * FROM scenes_tags WHERE tag_id = {args.tag} ORDER BY RANDOM() LIMIT {args.number}") + scenes = cursor.fetchall() + print(f"\nRandom {args.number} scenes with tag_id {args.tag}:") + for scene in scenes: + scene_id = scene[0] + url = f"http://localhost:9999/scene/{scene_id}/stream" + os.system(f"nohup mpv --volume=0 {url} /dev/null 2>&1 &") + print(scene) + os._exit(0) + +if __name__ == '__main__': + main() diff --git a/bin/quitif b/bin/quitif new file mode 100755 index 0000000..96a73df --- /dev/null +++ b/bin/quitif @@ -0,0 +1,32 @@ +#!/bin/bash + +SHUTDOWN_DELAY=60 + +while getopts "d:" opt; do + case $opt in + d) SHUTDOWN_DELAY="$OPTARG";; + \?) echo "Invalid option: -$OPTARG" >&2; exit 1;; + esac +done + +# Shift past the options to get positional argument +shift $((OPTIND-1)) + +if [ $# -ne 1 ]; then + echo "Usage: $0 [-d delay_seconds] process_name" + exit 1 +fi + +# Configuration +PROCESS_NAME="$1" # Name of the process to monitor + +# Check if process is running +while true; do + if ! pgrep -x "$PROCESS_NAME" > /dev/null; then + echo "Process $PROCESS_NAME is no longer running" + echo "System will shutdown in $SHUTDOWN_DELAY seconds" + sleep $SHUTDOWN_DELAY + shutdown -h now + fi + sleep 5 +done diff --git a/bin/tclean b/bin/tclean index f2d6a81..a836f4f 100755 --- a/bin/tclean +++ b/bin/tclean @@ -141,8 +141,8 @@ def get_deletion_candidates(torrents, limit=10): for torrent, score in sorted_torrents[:limit]: now = datetime.now() - age_days = (now - torrent['date_added']).days - inactive_days = (now - torrent['latest_activity']).days + age_days = (now - torrent.get('date_added', now)).days + inactive_days = (now - torrent.get('latest_activity', now)).days size_str = torrent['size'] size_num = float(size_str.split()[0]) diff --git a/bin/vc b/bin/vc index 4fe4b08..2a13f13 100755 --- a/bin/vc +++ b/bin/vc @@ -52,7 +52,7 @@ def get_video_filter(crop_arg: str | None) -> str: Rescales video height to 720p or original height if smaller, maintaining aspect ratio. Supports standard, percentage-based, and edge-based cropping formats.""" - vfilter = "scale=-2:'min(720,ih)'" + vfilter = "scale=-2:'min(2160,ih)'" if crop_arg: if crop_arg.endswith('%'): # Percentage-based centered cropping (single percentage value) diff --git a/bin/vcmp b/bin/vcmp index a900328..1b85da2 100755 --- a/bin/vcmp +++ b/bin/vcmp @@ -13,6 +13,7 @@ import psutil from tqdm import tqdm video_extensions: list[str] = ["mp4", "mkv", "avi", "wmv", "mov", "m4v", "ts", "flv", "mpg", "webm"] +embedded_sub_extensions: list[str] = ["mkv"] total_files: int = 0 @@ -111,6 +112,38 @@ def ffmpeg_command(input: pathlib.Path, output: pathlib.Path, nice: int = 20) -> str(output) ] +def extract_sub_command(input: pathlib.Path, output: pathlib.Path) -> list[str]: + return [ + "ffmpeg", + "-progress", "pipe:1", # Send progress info to stdout + "-i", str(input), + "-map", "0:s:0", + str(output) + ] + +def has_subs(input: pathlib.Path) -> bool: + cmd = [ + "ffprobe", + "-loglevel", "error", + "-select_streams", "s", + "-show_entries", "stream=index:stream_tags=language", + "-of", "csv=p=0", + str(input) + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + print(input) + print(result.stdout) + print("---") + if result.stdout: + return True + return False + except (subprocess.CalledProcessError, ValueError) as e: + logging.error(f"Error getting subs for {input}: {e}") + return False + except: + return False + def clean_directory(directory_path: pathlib.Path) -> None: """Yeet all files and directories from a directory.""" for item in directory_path.iterdir(): @@ -122,6 +155,9 @@ def clean_directory(directory_path: pathlib.Path) -> None: def is_video(path: pathlib.Path) -> bool: return path.suffix.lower()[1:] in video_extensions +def is_video_with_embedded_subs(path: pathlib.Path) -> bool: + return path.suffix.lower()[1:] in embedded_sub_extensions + def get_video_duration(input_file: pathlib.Path) -> float: cmd = [ "ffprobe", @@ -313,6 +349,26 @@ def process_directory( logging.info(f"Processed {count} files, {successful} successful") return count +def process_subtitles( + *, + source_dir: pathlib.Path, + destination_dir: pathlib.Path, + temp_dir: pathlib.Path): + for dirpath, _dirnames, filenames in os.walk(source_dir): + for filename in filenames: + filepath = pathlib.Path(dirpath) / filename + # File might have been deleted since starting script + if not filepath.exists() or not is_video_with_embedded_subs(filepath): + logging.debug(f"Skipping {filepath}: not a video file or doesn't exist") + continue + if has_subs(filepath): + target_path = destination_dir / filepath.relative_to(source_dir) + target_path = target_path.with_suffix(".cmp.en.srt") + if target_path.exists(): + logging.debug(f"Skipping {filepath}: target already exists") + continue + cmd = extract_sub_command(filepath, target_path) + result = subprocess.run(cmd, capture_output=True, text=True, check=True) def main(): parser = argparse.ArgumentParser( @@ -332,6 +388,7 @@ def main(): help='Remove source files after successful compression') parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging output') + parser.add_argument('--sub', action='store_true', help='Process files for subtitle extraction first') parser.add_argument('-n', '--nice', type=lambda x: int(x) if 0 <= int(x) <= 39 else exec('raise ValueError("Nice value must be between 0 and 39")'), default=20, help='Nice value for process priority (default: 20). Valid values are 0 (highest priority) to 39 (lowest priority). Lower values use more system resources.') @@ -352,22 +409,27 @@ def main(): global ignore_busy ignore_busy = bool(args.ignore_busy) - logging.info(f"Starting video compression from {source_dir} to {destination_dir}") - - global total_files - total_files = process_directory( - source_dir=source_dir, + if args.sub: + process_subtitles(source_dir=source_dir, destination_dir=destination_dir, - temp_dir=temp_dir, - count_only=True - ) + temp_dir=temp_dir) + else: + logging.info(f"Starting video compression from {source_dir} to {destination_dir}") - process_directory( - source_dir=source_dir, - destination_dir=destination_dir, - temp_dir=temp_dir, - remove_source=args.remove_source, - nice=args.nice - ) + global total_files + total_files = process_directory( + source_dir=source_dir, + destination_dir=destination_dir, + temp_dir=temp_dir, + count_only=True + ) + + process_directory( + source_dir=source_dir, + destination_dir=destination_dir, + temp_dir=temp_dir, + remove_source=args.remove_source, + nice=args.nice + ) if __name__ == "__main__": main() diff --git a/bin/vids b/bin/vids new file mode 100755 index 0000000..0d0eb72 --- /dev/null +++ b/bin/vids @@ -0,0 +1,22 @@ +#!/bin/bash + +MAX=75576 +BASE=$1 +COUNT=$2 + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + exit 1 +fi + +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/mpv-urls" +mkdir -p "$STATE_DIR" +OUTPUT_FILE="$STATE_DIR/urls-$(date +%Y%m%d-%H%M%S).txt" + +for ((i=1; i<=COUNT; i++)); do + URL="$BASE/$(( RANDOM % MAX + 1 ))/stream" + echo "$URL" >> "$OUTPUT_FILE" + mpv "$URL" & +done + +wait diff --git a/utils/pwm.service b/utils/pwm.service new file mode 100644 index 0000000..b1ada29 --- /dev/null +++ b/utils/pwm.service @@ -0,0 +1,16 @@ +[Unit] +Description=Fan PWM Control +After=multi-user.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/bin/bash -c 'echo 0 > /sys/class/pwm/pwmchip0/export && \ + echo 40000 > /sys/class/pwm/pwmchip0/pwm0/period && \ + echo 28000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle && \ + echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable' +ExecStop=/bin/bash -c 'echo 0 > /sys/class/pwm/pwmchip0/pwm0/enable && \ + echo 0 > /sys/class/pwm/pwmchip0/unexport' + +[Install] +WantedBy=multi-user.target