Compare commits

..
2 Commits
Author SHA1 Message Date
mark 11f162c91a ln wrapper 2026-08-08 18:06:50 +02:00
mark 7da0c922ad updates and backlog 2026-08-08 11:58:01 +02:00
11 changed files with 495 additions and 18 deletions
+1
View File
@@ -10,6 +10,7 @@ While created for my own use, they are shared here in case others find them usef
- **check**: Validates script dependencies and configuration
- **e-ink**: Display handler for e-ink displays with image cycling support
- **indeel**: File organization tool that watches a directory and sorts incoming files based on content hashes
- **lnw**: `ln` wrapper that always creates a symbolic link, auto-detects which argument is the existing target, and converts paths to absolute
- **mover**: Monitors a source directory and moves files with specified extensions to a destination directory
- **pushover**: Sends push notifications via Pushover API
- **stats**: HTTP server that provides system statistics (CPU, memory, disk usage, etc.) in JSON format
Executable
+91
View File
@@ -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()
Executable
+140
View File
@@ -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()
Executable
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# ln? pfft
# Ja het was waarschijnlijk minder moeite geweest om het te onthouden
# dan om dit scriptje te schrijven.
set -euo pipefail
usage() {
echo "Ook altijd geen idee hoe ln ook alweer werkt? Geen zorgen."
echo "Doe: $0 <file> <link>" >&2
echo " of: $0 <link> <file>" >&2
exit 1
}
if [ $# -ne 2 ]; then
usage
fi
a="$1"
b="$2"
a_exists=0
b_exists=0
if [ -e "$a" ]; then a_exists=1; fi
if [ -e "$b" ]; then b_exists=1; fi
if [ "$a_exists" -eq 1 ] && [ "$b_exists" -eq 1 ]; then
# is één van de twee al een link? Dan is het goed.
if { [ -L "$a" ] && [ "$(readlink "$a")" = "$(realpath "$b")" ]; } || \
{ [ -L "$b" ] && [ "$(readlink "$b")" = "$(realpath "$a")" ]; }; then
exit 0
fi
echo "[FOUT] deze bestaan al: $a, $b" >&2
exit 1
# Geen van beide bestaat
elif [ "$a_exists" -eq 0 ] && [ "$b_exists" -eq 0 ]; then
echo "[FOUT] allebei niet gevonden: $a, $b" >&2
exit 1
elif [ "$a_exists" -eq 1 ]; then
target="$a"
link="$b"
else
target="$b"
link="$a"
fi
target_abs="$(realpath "$target")"
link_abs="$(realpath -m "$link")"
ln -s "$target_abs" "$link_abs"
Executable
+63
View File
@@ -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 >/dev/null 2>&1 &")
print(scene)
os._exit(0)
if __name__ == '__main__':
main()
Executable
+32
View File
@@ -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
+2 -2
View File
@@ -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])
+1 -1
View File
@@ -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)
+77 -15
View File
@@ -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()
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
MAX=75576
BASE=$1
COUNT=$2
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <base_url> <count>"
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
+16
View File
@@ -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