HASH 6448a0f26c91
DATE 2025-09-08
SUBJECT tools: add discordify ffmpeg script
FILES 2 CHANGED
HASH 6448a0f26c91
DATE 2025-09-08
SUBJECT tools: add discordify ffmpeg script
FILES 2 CHANGED
┌─ modules/home/nixos/tools/default.nix ─────────────────────────────────────┐
│ diff --git a/modules/home/nixos/tools/default.nix b/modules/home/nixos/tools/defau │
│ lt.nix │
│ index 55cb2b2..f2c5b77 100644 │
│ --- a/modules/home/nixos/tools/default.nix │
│ +++ b/modules/home/nixos/tools/default.nix │
│ @@ -12,5 +12,6 @@ │
│ ./syncthing-applet.nix │
│ ./nemo.nix │
│ ./qtscrcpy.nix │
│ + ./discordify.nix │
│ ]; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../home/nixos/tools/default.nix ───┐
│ diff --git a/modules/home/nixos/tools/defaul │
│ t.nix b/modules/home/nixos/tools/default.nix │
│ index 55cb2b2..f2c5b77 100644 │
│ --- a/modules/home/nixos/tools/default.nix │
│ +++ b/modules/home/nixos/tools/default.nix │
│ @@ -12,5 +12,6 @@ │
│ ./syncthing-applet.nix │
│ ./nemo.nix │
│ ./qtscrcpy.nix │
│ + ./discordify.nix │
│ ]; │
│ } │
└──────────────────────────────────────────────┘
┌─ modules/home/nixos/tools/discordify.nix ──────────────────────────────────┐
│ diff --git a/modules/home/nixos/tools/discordify.nix b/modules/home/nixos/tools/di │
│ scordify.nix │
│ new file mode 100644 │
│ index 0000000..5a970e7 │
│ --- /dev/null │
│ +++ b/modules/home/nixos/tools/discordify.nix │
│ @@ -0,0 +1,207 @@ │
│ +{ │
│ + lib, │
│ + config, │
│ + pkgs, │
│ + ... │
│ +}: let │
│ + inherit (lib) mkIf; │
│ + discordify = pkgs.writeShellApplication { │
│ + name = "discordify"; │
│ + runtimeInputs = [pkgs.ffmpeg pkgs.bc pkgs.gum]; │
│ + text = │
│ + # bash │
│ + '' │
│ + TARGET_SIZE_MB=15 │
│ + OUTPUT_SUFFIX="_compressed" │
│ + AUDIO_BITRATE="128k" │
│ + verbose="" │
│ + │
│ + log() { │
│ + local type="$1" │
│ + local message="$2" │
│ + if [ "$type" = "verbose" ]; then │
│ + if [ -n "$verbose" ]; then │
│ + gum log --structured --level debug "$message" │
│ + fi │
│ + return │
│ + fi │
│ + if [ "$type" = "abort" ]; then │
│ + gum log --structured --level error "$message" │
│ + exit 1 │
│ + else │
│ + gum log --structured --level "$type" "$message" │
│ + fi │
│ + } │
│ + │
│ + show_usage() { │
│ + echo "Usage: $0 <input_video> [target_size_mb] [output_name]" │
│ + echo "" │
│ + echo "Examples:" │
│ + echo " $0 video.mp4 # Compress to 15MB" │
│ + echo " $0 video.mp4 25 # Compress to 25MB" │
│ + echo " $0 video.mp4 15 my_video.mp4 # Compress to 15MB with custo │
│ m name" │
│ + echo "" │
│ + echo "Options:" │
│ + echo " target_size_mb: Target file size in MB (default: 15)" │
│ + echo " output_name: Custom output filename (default: input_compresse │
│ d.mp4)" │
│ + echo " -v, --verbose: Show debug output" │
│ + } │
│ + │
│ + # parse verbose flag │
│ + while [[ $# -gt 0 ]]; do │
│ + case $1 in │
│ + -v|--verbose) │
│ + verbose="true" │
│ + shift │
│ + ;; │
│ + *) │
│ + break │
│ + ;; │
│ + esac │
│ + done │
│ + │
│ + # check arguments │
│ + if [ $# -lt 1 ]; then │
│ + show_usage │
│ + exit 1 │
│ + fi │
│ + │
│ + INPUT_FILE="$1" │
│ + │
│ + # check if input file exists │
│ + if [ ! -f "$INPUT_FILE" ]; then │
│ + log "abort" "Input file '$INPUT_FILE' not found" │
│ + fi │
│ + │
│ + # parse arguments │
│ + if [ $# -ge 2 ]; then │
│ + TARGET_SIZE_MB="$2" │
│ + fi │
│ + │
│ + if [ $# -ge 3 ]; then │
│ + OUTPUT_FILE="$3" │
│ + else │
│ + # Generate output filename - using different variable names to avoid │
│ nix interpolation │
│ + input_filename=$(basename "$INPUT_FILE") │
│ + file_extension="''${input_filename##*.}" │
│ + file_name="''${input_filename%.*}" │
│ + OUTPUT_FILE="''${file_name}''${OUTPUT_SUFFIX}.''${file_extension}" │
│ + fi │
│ + │
│ + log "info" "Input file: $INPUT_FILE" │
│ + log "info" "Target size: ''${TARGET_SIZE_MB}MB" │
│ + log "info" "Output file: $OUTPUT_FILE" │
│ + │
│ + # Get video duration in seconds │
│ + gum spin --spinner dot --title "Analyzing input video..." -- sh -c " │
│ + DURATION=\$(ffprobe -v error -show_entries format=duration -of default= │
│ noprint_wrappers=1:nokey=1 '$INPUT_FILE' 2>/dev/null) │
│ + echo \"\$DURATION\" > /tmp/discordify_duration_$ │
│ + " │
│ + DURATION=$(cat "/tmp/discordify_duration_$") │
│ + rm -f "/tmp/discordify_duration_$" │
│ + │
│ + if [ -z "$DURATION" ] || [ "$(echo "$DURATION < 0" | bc -l 2>/dev/null)" │
│ = "1" ]; then │
│ + log "abort" "Could not determine video duration" │
│ + fi │
│ + │
│ + # Calculate target bitrate │
│ + # Formula: (target_size_mb * 8192) / duration - audio_bitrate │
│ + # 8192 = 8 * 1024 (convert MB to kilobits) │
│ + AUDIO_BITRATE_NUM="''${AUDIO_BITRATE//k/}" │
│ + TARGET_BITRATE=$(echo "scale=0; ((''${TARGET_SIZE_MB} * 8192) / $DURATION │
│ ) - $AUDIO_BITRATE_NUM" | bc -l) │
│ + │
│ + # Ensure minimum bitrate │
│ + MIN_BITRATE=64 │
│ + if [ "$(echo "$TARGET_BITRATE < $MIN_BITRATE" | bc -l)" = "1" ]; then │
│ + log "warn" "Calculated bitrate is very low (''${TARGET_BITRATE}k). Se │
│ tting to minimum ''${MIN_BITRATE}k" │
│ + TARGET_BITRATE=$MIN_BITRATE │
│ + fi │
│ + │
│ + log "info" "Video duration: ''${DURATION}s" │
│ + log "info" "Calculated video bitrate: ''${TARGET_BITRATE}k" │
│ + │
│ + # Two-pass encoding for better quality at target bitrate │
│ + log "info" "Starting two-pass encoding..." │
│ + │
│ + # Pass 1 - with spinner │
│ + if [ -n "$verbose" ]; then │
│ + log "info" "Pass 1/2: Analyzing video..." │
│ + if ! ffmpeg -y -i "$INPUT_FILE" \ │
│ + -c:v libx264 \ │
│ + -b:v "''${TARGET_BITRATE}k" \ │
│ + -pass 1 \ │
│ + -an \ │
│ + -f null \ │
│ + /dev/null; then │
│ + log "abort" "First pass failed" │
│ + fi │
│ + else │
│ + if ! gum spin --spinner dot --title "Pass 1/2: Analyzing video..." -- s │
│ h -c " │
│ + ffmpeg -y -i \"$INPUT_FILE\" \ │
│ + -c:v libx264 \ │
│ + -b:v \"''${TARGET_BITRATE}k\" \ │
│ + -pass 1 \ │
│ + -an \ │
│ + -f null \ │
│ + /dev/null >/dev/null 2>&1 │
│ + "; then │
│ + log "abort" "First pass failed" │
│ + fi │
│ + fi │
│ + │
│ + # Pass 2 - with spinner │
│ + if [ -n "$verbose" ]; then │
│ + log "info" "Pass 2/2: Encoding video..." │
│ + if ! ffmpeg -i "$INPUT_FILE" \ │
│ + -c:v libx264 \ │
│ + -b:v "''${TARGET_BITRATE}k" \ │
│ + -pass 2 \ │
│ + -c:a aac \ │
│ + -b:a "$AUDIO_BITRATE" \ │
│ + -movflags +faststart \ │
│ + "$OUTPUT_FILE"; then │
│ + log "abort" "Second pass failed" │
│ + fi │
│ + else │
│ + if ! gum spin --spinner dot --title "Pass 2/2: Encoding video..." -- sh │
│ -c " │
│ + ffmpeg -i \"$INPUT_FILE\" \ │
│ + -c:v libx264 \ │
│ + -b:v \"''${TARGET_BITRATE}k\" \ │
│ + -pass 2 \ │
│ + -c:a aac \ │
│ + -b:a \"$AUDIO_BITRATE\" \ │
│ + -movflags +faststart \ │
│ + \"$OUTPUT_FILE\" >/dev/null 2>&1 │
│ + "; then │
│ + log "abort" "Second pass failed" │
│ + fi │
│ + fi │
│ + │
│ + # Clean up pass files │
│ + rm -f ffmpeg2pass-0.log ffmpeg2pass-0.log.mbtree │
│ + │
│ + # Check final file size │
│ + if [ -f "$OUTPUT_FILE" ]; then │
│ + FINAL_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTP │
│ UT_FILE" 2>/dev/null) │
│ + FINAL_SIZE_MB=$(echo "scale=2; $FINAL_SIZE / 1048576" | bc -l) │
│ + │
│ + log "info" "Compression complete!" │
│ + log "info" "Final file size: ''${FINAL_SIZE_MB}MB" │
│ + log "info" "Output saved as: $OUTPUT_FILE" │
│ + │
│ + # Check if we're close to target - escape the hash for nix │
│ + DIFF=$(echo "scale=2; $FINAL_SIZE_MB - ''${TARGET_SIZE_MB}" | bc -l) │
│ + abs_diff="''${DIFF#-}" │
│ + if [ "$(echo "$abs_diff > 1" | bc -l)" = "1" ]; then │
│ + log "warn" "Final size differs from target by ''${DIFF}MB" │
│ + fi │
│ + else │
│ + log "abort" "Output file was not created" │
│ + fi │
│ + ''; │
│ + }; │
│ +in { │
│ + config = { │
│ + home.packages = [discordify]; │
│ + }; │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...me/nixos/tools/discordify.nix ───┐
│ diff --git a/modules/home/nixos/tools/discor │
│ dify.nix b/modules/home/nixos/tools/discordi │
│ fy.nix │
│ new file mode 100644 │
│ index 0000000..5a970e7 │
│ --- /dev/null │
│ +++ b/modules/home/nixos/tools/discordify.ni │
│ x │
│ @@ -0,0 +1,207 @@ │
│ +{ │
│ + lib, │
│ + config, │
│ + pkgs, │
│ + ... │
│ +}: let │
│ + inherit (lib) mkIf; │
│ + discordify = pkgs.writeShellApplication { │
│ + name = "discordify"; │
│ + runtimeInputs = [pkgs.ffmpeg pkgs.bc pk │
│ gs.gum]; │
│ + text = │
│ + # bash │
│ + '' │
│ + TARGET_SIZE_MB=15 │
│ + OUTPUT_SUFFIX="_compressed" │
│ + AUDIO_BITRATE="128k" │
│ + verbose="" │
│ + │
│ + log() { │
│ + local type="$1" │
│ + local message="$2" │
│ + if [ "$type" = "verbose" ]; then │
│ + if [ -n "$verbose" ]; then │
│ + gum log --structured --level │
│ debug "$message" │
│ + fi │
│ + return │
│ + fi │
│ + if [ "$type" = "abort" ]; then │
│ + gum log --structured --level er │
│ ror "$message" │
│ + exit 1 │
│ + else │
│ + gum log --structured --level "$ │
│ type" "$message" │
│ + fi │
│ + } │
│ + │
│ + show_usage() { │
│ + echo "Usage: $0 <input_video> [ │
│ target_size_mb] [output_name]" │
│ + echo "" │
│ + echo "Examples:" │
│ + echo " $0 video.mp4 │
│ # Compress to 15MB" │
│ + echo " $0 video.mp4 25 │
│ # Compress to 25MB" │
│ + echo " $0 video.mp4 15 my_vide │
│ o.mp4 # Compress to 15MB with custom name │
│ " │
│ + echo "" │
│ + echo "Options:" │
│ + echo " target_size_mb: Target │
│ file size in MB (default: 15)" │
│ + echo " output_name: Custom out │
│ put filename (default: input_compressed.mp4) │
│ " │
│ + echo " -v, --verbose: Show deb │
│ ug output" │
│ + } │
│ + │
│ + # parse verbose flag │
│ + while [[ $# -gt 0 ]]; do │
│ + case $1 in │
│ + -v|--verbose) │
│ + verbose="true" │
│ + shift │
│ + ;; │
│ + *) │
│ + break │
│ + ;; │
│ + esac │
│ + done │
│ + │
│ + # check arguments │
│ + if [ $# -lt 1 ]; then │
│ + show_usage │
│ + exit 1 │
│ + fi │
│ + │
│ + INPUT_FILE="$1" │
│ + │
│ + # check if input file exists │
│ + if [ ! -f "$INPUT_FILE" ]; then │
│ + log "abort" "Input file '$INPUT │
│ _FILE' not found" │
│ + fi │
│ + │
│ + # parse arguments │
│ + if [ $# -ge 2 ]; then │
│ + TARGET_SIZE_MB="$2" │
│ + fi │
│ + │
│ + if [ $# -ge 3 ]; then │
│ + OUTPUT_FILE="$3" │
│ + else │
│ + # Generate output filename - us │
│ ing different variable names to avoid nix in │
│ terpolation │
│ + input_filename=$(basename "$INP │
│ UT_FILE") │
│ + file_extension="''${input_filen │
│ ame##*.}" │
│ + file_name="''${input_filename%. │
│ *}" │
│ + OUTPUT_FILE="''${file_name}''${ │
│ OUTPUT_SUFFIX}.''${file_extension}" │
│ + fi │
│ + │
│ + log "info" "Input file: $INPUT_FILE │
│ " │
│ + log "info" "Target size: ''${TARGET │
│ _SIZE_MB}MB" │
│ + log "info" "Output file: $OUTPUT_FI │
│ LE" │
│ + │
│ + # Get video duration in seconds │
│ + gum spin --spinner dot --title "Ana │
│ lyzing input video..." -- sh -c " │
│ + DURATION=\$(ffprobe -v error -sho │
│ w_entries format=duration -of default=noprin │
│ t_wrappers=1:nokey=1 '$INPUT_FILE' 2>/dev/nu │
│ ll) │
│ + echo \"\$DURATION\" > /tmp/discor │
│ dify_duration_$ │
│ + " │
│ + DURATION=$(cat "/tmp/discordify_dur │
│ ation_$") │
│ + rm -f "/tmp/discordify_duration_$" │
│ + │
│ + if [ -z "$DURATION" ] || [ "$(echo │
│ "$DURATION < 0" | bc -l 2>/dev/null)" = "1" │
│ ]; then │
│ + log "abort" "Could not determin │
│ e video duration" │
│ + fi │
│ + │
│ + # Calculate target bitrate │
│ + # Formula: (target_size_mb * 8192) │
│ / duration - audio_bitrate │
│ + # 8192 = 8 * 1024 (convert MB to ki │
│ lobits) │
│ + AUDIO_BITRATE_NUM="''${AUDIO_BITRAT │
│ E//k/}" │
│ + TARGET_BITRATE=$(echo "scale=0; ((' │
│ '${TARGET_SIZE_MB} * 8192) / $DURATION) - $A │
│ UDIO_BITRATE_NUM" | bc -l) │
│ + │
│ + # Ensure minimum bitrate │
│ + MIN_BITRATE=64 │
│ + if [ "$(echo "$TARGET_BITRATE < $MI │
│ N_BITRATE" | bc -l)" = "1" ]; then │
│ + log "warn" "Calculated bitrate │
│ is very low (''${TARGET_BITRATE}k). Setting │
│ to minimum ''${MIN_BITRATE}k" │
│ + TARGET_BITRATE=$MIN_BITRATE │
│ + fi │
│ + │
│ + log "info" "Video duration: ''${DUR │
│ ATION}s" │
│ + log "info" "Calculated video bitrat │
│ e: ''${TARGET_BITRATE}k" │
│ + │
│ + # Two-pass encoding for better qual │
│ ity at target bitrate │
│ + log "info" "Starting two-pass encod │
│ ing..." │
│ + │
│ + # Pass 1 - with spinner │
│ + if [ -n "$verbose" ]; then │
│ + log "info" "Pass 1/2: Analyzing v │
│ ideo..." │
│ + if ! ffmpeg -y -i "$INPUT_FILE" \ │
│ + -c:v libx264 \ │
│ + -b:v "''${TARGET_BITRATE}k" \ │
│ + -pass 1 \ │
│ + -an \ │
│ + -f null \ │
│ + /dev/null; then │
│ + log "abort" "First pass faile │
│ d" │
│ + fi │
│ + else │
│ + if ! gum spin --spinner dot --tit │
│ le "Pass 1/2: Analyzing video..." -- sh -c " │
│ + ffmpeg -y -i \"$INPUT_FILE\" \ │
│ + -c:v libx264 \ │
│ + -b:v \"''${TARGET_BITRATE}k\" │
│ \ │
│ + -pass 1 \ │
│ + -an \ │
│ + -f null \ │
│ + /dev/null >/dev/null 2>&1 │
│ + "; then │
│ + log "abort" "First pass faile │
│ d" │
│ + fi │
│ + fi │
│ + │
│ + # Pass 2 - with spinner │
│ + if [ -n "$verbose" ]; then │
│ + log "info" "Pass 2/2: Encoding vi │
│ deo..." │
│ + if ! ffmpeg -i "$INPUT_FILE" \ │
│ + -c:v libx264 \ │
│ + -b:v "''${TARGET_BITRATE}k" \ │
│ + -pass 2 \ │
│ + -c:a aac \ │
│ + -b:a "$AUDIO_BITRATE" \ │
│ + -movflags +faststart \ │
│ + "$OUTPUT_FILE"; then │
│ + log "abort" "Second pass fail │
│ ed" │
│ + fi │
│ + else │
│ + if ! gum spin --spinner dot --tit │
│ le "Pass 2/2: Encoding video..." -- sh -c " │
│ + ffmpeg -i \"$INPUT_FILE\" \ │
│ + -c:v libx264 \ │
│ + -b:v \"''${TARGET_BITRATE}k\" │
│ \ │
│ + -pass 2 \ │
│ + -c:a aac \ │
│ + -b:a \"$AUDIO_BITRATE\" \ │
│ + -movflags +faststart \ │
│ + \"$OUTPUT_FILE\" >/dev/null 2 │
│ >&1 │
│ + "; then │
│ + log "abort" "Second pass fail │
│ ed" │
│ + fi │
│ + fi │
│ + │
│ + # Clean up pass files │
│ + rm -f ffmpeg2pass-0.log ffmpeg2pass │
│ -0.log.mbtree │
│ + │
│ + # Check final file size │
│ + if [ -f "$OUTPUT_FILE" ]; then │
│ + FINAL_SIZE=$(stat -f%z "$OUTPUT │
│ _FILE" 2>/dev/null || stat -c%s "$OUTPUT_FIL │
│ E" 2>/dev/null) │
│ + FINAL_SIZE_MB=$(echo "scale=2; │
│ $FINAL_SIZE / 1048576" | bc -l) │
│ + │
│ + log "info" "Compression complet │
│ e!" │
│ + log "info" "Final file size: '' │
│ ${FINAL_SIZE_MB}MB" │
│ + log "info" "Output saved as: $O │
│ UTPUT_FILE" │
│ + │
│ + # Check if we're close to targe │
│ t - escape the hash for nix │
│ + DIFF=$(echo "scale=2; $FINAL_SI │
│ ZE_MB - ''${TARGET_SIZE_MB}" | bc -l) │
│ + abs_diff="''${DIFF#-}" │
│ + if [ "$(echo "$abs_diff > 1" | │
│ bc -l)" = "1" ]; then │
│ + log "warn" "Final size diff │
│ ers from target by ''${DIFF}MB" │
│ + fi │
│ + else │
│ + log "abort" "Output file was no │
│ t created" │
│ + fi │
│ + ''; │
│ + }; │
│ +in { │
│ + config = { │
│ + home.packages = [discordify]; │
│ + }; │
│ +} │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET