# cinefile -- cinephile workflow (mpv, ffmpeg, yt-dlp, media management) # Sources: TheFrenchGhosty, fallwith, benjaminburkhardt, dfaust, Zabooby, clavelm # ============================================================================ # aliases # ============================================================================ # mpv playback alias mpv480='mpv --ytdl-format="bestvideo[height<=480]+bestaudio/best[height<=480]"' alias mpv720='mpv --ytdl-format="bestvideo[height<=720]+bestaudio/best[height<=720]"' alias mpv1080='mpv --ytdl-format="bestvideo[ext=mp4][height<=1080]+bestaudio[ext=m4a]"' alias mpv4k='mpv --ytdl-format="bestvideo[height<=2160]+bestaudio/best"' alias cinema='mpv --profile=cinema' alias film='mpv --profile=cinema --sub-font-size=42' alias mpvshuffle='mpv --shuffle' alias mpvloop='mpv --loop' alias mpvnosub='mpv --no-sub' # mpv history / status alias mpvhist='cat ~/.config/mpv/mpvHistory.log 2>/dev/null' alias mpvsearch='grep -i' # yt-dlp film downloads alias ytbest='yt-dlp -f "bestvideo+bestaudio" --merge-output-format mkv' alias ytmovie='yt-dlp -f "bestvideo[height>=1080]+bestaudio" --merge-output-format mkv --embed-subs --embed-thumbnail --embed-metadata' alias yt1080='yt-dlp -f "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080]"' alias yt720='yt-dlp -f "bestvideo[height<=720]+bestaudio/best[height<=720]"' alias ytmp4='yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]" --merge-output-format mp4' alias ytav1='yt-dlp -f "bestvideo[vcodec^=av01]+bestaudio/best"' alias ytvp9='yt-dlp -f "bestvideo[vcodec^=vp9]+bestaudio/best" --merge-output-format mkv' alias ytsubs='yt-dlp --write-sub --write-auto-sub --sub-lang en --convert-subs srt' # ffmpeg quick conversions alias remux='f() { ffmpeg -i "$1" -c copy "${1%.*}.mkv"; }; f' alias mkv2mp4='f() { ffmpeg -i "$1" -c:v copy -c:a aac -c:s mov_text "${1%.*}.mp4"; }; f' alias shrink='f() { ffmpeg -i "$1" -c:v libx265 -crf 28 -preset fast -c:a aac -b:a 128k "${1%.*}_small.mkv"; }; f' # mediainfo / ffprobe alias mi='mediainfo' alias vres='ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0' alias vcodec='ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=nw=1:nk=1' alias vdur='ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1' alias vbitrate='ffprobe -v error -show_entries format=bit_rate -of default=nw=1:nk=1' # subtitles alias subs='subliminal download -l en' alias subsall='find . -name "*.mkv" -exec subliminal download -l en {} \;' # collection browsing alias moviesize='du -sh /media/movies/*/ 2>/dev/null | sort -rh | head -20' # transmission (torrent) alias tr='transmission-remote' alias tra='transmission-remote -a' alias trl='transmission-remote -l' alias trs='transmission-remote -t all -s' alias trp='transmission-remote -t all -S' # plex / jellyfin alias plexstatus='curl -s http://localhost:32400/identity 2>/dev/null && echo "Plex: UP" || echo "Plex: DOWN"' alias jfstatus='curl -s http://localhost:8096/System/Info/Public 2>/dev/null | head -1 && echo "Jellyfin: UP" || echo "Jellyfin: DOWN"' # ============================================================================ # functions # ============================================================================ # move each video file into its own folder file2folder() { dir="${1:-.}" for f in "$dir"/*.mkv "$dir"/*.mp4 "$dir"/*.avi; do [ -f "$f" ] || continue d="${f%.*}" mkdir -p "$d" mv "$f" "$d/" echo " $f -> $d/" done } # rename movie to "Title (Year).ext" format rename_movie() { file="$1"; title="$2"; year="$3" [ -z "$year" ] && { echo "Usage: rename_movie <year>"; return 1; } ext="${file##*.}" newname="${title} (${year}).${ext}" mv -- "$file" "$newname" echo "Renamed: $file -> $newname" } # bulk download subtitles for a directory getsubs() { lang="${1:-en}" dir="${2:-.}" find "$dir" -type f \( -name '*.mkv' -o -name '*.mp4' -o -name '*.avi' \) \ -exec subliminal download -l "$lang" {} \; } # find duplicate videos by hash finddupes() { dir="${1:-.}" find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) \ -print0 | xargs -0 sha256sum | sort | uniq -w64 -dD } # generate minimal Kodi .nfo from IMDB ID mknfo() { imdb_id="$1"; movie_dir="$2" [ -z "$movie_dir" ] && { echo "Usage: mknfo <imdb-id> <movie-dir>"; return 1; } echo "https://www.imdb.com/title/${imdb_id}/" > "$movie_dir/movie.nfo" echo "Created: $movie_dir/movie.nfo" } # play all videos in a directory mpvdir() { dir="${1:-.}" find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \ -o -iname '*.webm' \) | sort | mpv --playlist=- } # shuffle play a directory mpvrand() { dir="${1:-.}" find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' \) | shuf | mpv --playlist=- } # query current mpv playback via IPC socket mpvnow() { socket="${1:-/tmp/mpv-socket}" [ ! -S "$socket" ] && { echo "No mpv instance running"; return 1; } title=$(echo '{ "command": ["get_property", "media-title"] }' | socat - "$socket" | jq -r '.data') pos=$(echo '{ "command": ["get_property", "playback-time"] }' | socat - "$socket" | jq -r '.data') dur=$(echo '{ "command": ["get_property", "duration"] }' | socat - "$socket" | jq -r '.data') printf "Now playing: %s\nPosition: %s / %s\n" "$title" \ "$(date -u -d @${pos%.*} +%H:%M:%S 2>/dev/null || echo "${pos%.*}s")" \ "$(date -u -d @${dur%.*} +%H:%M:%S 2>/dev/null || echo "${dur%.*}s")" } # random unwatched movie picker randmovie() { dir="${1:-/media/movies}" watched="$HOME/.watched" touch "$watched" pick=$(find "$dir" -maxdepth 2 -type f \( -iname '*.mkv' -o -iname '*.mp4' \) | \ grep -vFf "$watched" | shuf -n1) [ -z "$pick" ] && { echo "All watched! Clearing history..."; > "$watched"; pick=$(find "$dir" -maxdepth 2 -type f \( -iname '*.mkv' -o -iname '*.mp4' \) | shuf -n1); } echo "Tonight's pick: $(basename "$pick")" echo "$pick" >> "$watched" } # x265 CRF encoding encode265() { input="$1"; output="${2:-${input%.*}_x265.mkv}"; crf="${3:-20}" [ -z "$input" ] && { echo "Usage: encode265 <input> [output] [crf]"; return 1; } ffmpeg -i "$input" -c:v libx265 -preset slow -crf "$crf" \ -x265-params "aq-mode=3:rd=4" -c:a copy -c:s copy "$output" } # x264 two-pass encoding encode264_2pass() { input="$1"; output="${2:-${input%.*}_x264.mkv}"; bitrate="${3:-8000k}" [ -z "$input" ] && { echo "Usage: encode264_2pass <input> [output] [bitrate]"; return 1; } ffmpeg -y -i "$input" -c:v libx264 -preset slow -b:v "$bitrate" -pass 1 -an -f null /dev/null ffmpeg -i "$input" -c:v libx264 -preset slow -b:v "$bitrate" -pass 2 -c:a aac -b:a 192k "$output" rm -f ffmpeg2pass-0.log ffmpeg2pass-0.log.mbtree } # AV1 encoding with SVT-AV1 encodeav1() { input="$1"; output="${2:-${input%.*}_av1.mkv}"; crf="${3:-30}" [ -z "$input" ] && { echo "Usage: encodeav1 <input> [output] [crf]"; return 1; } ffmpeg -i "$input" -c:v libsvtav1 -preset 6 -crf "$crf" \ -pix_fmt yuv420p10le -c:a libopus -b:a 128k -c:s copy "$output" } # HDR to SDR tonemapping hdr2sdr() { input="$1"; output="${2:-${input%.*}_sdr.mkv}" [ -z "$input" ] && { echo "Usage: hdr2sdr <input> [output]"; return 1; } ffmpeg -i "$input" \ -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \ -c:v libx265 -preset slow -crf 18 -c:a copy -c:s copy "$output" } # burn subtitles into video burnsubs() { input="$1"; srt="$2"; output="${3:-${input%.*}_hardsub.mp4}" [ -z "$srt" ] && { echo "Usage: burnsubs <video> <srt> [output]"; return 1; } ffmpeg -i "$input" -vf "subtitles='${srt}':force_style='FontSize=24'" \ -c:v libx264 -crf 18 -preset slow -c:a copy "$output" } # batch transcode directory to x265 batch_x265() { indir="${1:-.}"; outdir="${2:-./encoded}"; crf="${3:-22}" mkdir -p "$outdir" find "$indir" -type f \( -iname '*.mkv' -o -iname '*.mp4' -o -iname '*.avi' \) | while read -r f; do base=$(basename "$f") out="$outdir/${base%.*}.mkv" [ -f "$out" ] && { echo "Skip: $out exists"; continue; } echo "Encoding: $f" ffmpeg -i "$f" -c:v libx265 -preset medium -crf "$crf" \ -c:a aac -b:a 192k -c:s copy "$out" done } # extract all subtitle tracks from MKV extract_subs() { input="$1" [ -z "$input" ] && { echo "Usage: extract_subs <file>"; return 1; } basename="${input%.*}" ffprobe -v error -select_streams s -show_entries stream=index:stream_tags=language \ -of csv=p=0 "$input" | while IFS=',' read -r idx lang; do lang="${lang:-und}" ffmpeg -y -nostdin -i "$input" -map 0:"$idx" "${basename}_${lang}_${idx}.srt" echo "Extracted: ${basename}_${lang}_${idx}.srt" done } # extract all audio tracks extract_audio() { input="$1" [ -z "$input" ] && { echo "Usage: extract_audio <file>"; return 1; } basename="${input%.*}" ffprobe -v error -select_streams a -show_entries stream=index,codec_name:stream_tags=language \ -of csv=p=0 "$input" | while IFS=',' read -r idx codec lang; do lang="${lang:-und}" case "$codec" in aac) ext="aac" ;; ac3) ext="ac3" ;; eac3) ext="eac3" ;; dts) ext="dts" ;; flac) ext="flac" ;; opus) ext="opus" ;; *) ext="mka" ;; esac ffmpeg -y -i "$input" -map 0:"$idx" -c copy "${basename}_${lang}.${ext}" done } # human-readable duration vlen() { secs=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$1" | cut -d. -f1) printf "%02d:%02d:%02d\n" $((secs/3600)) $(((secs%3600)/60)) $((secs%60)) } # show every stream in a file streams() { ffprobe -v error -show_entries \ stream=index,codec_type,codec_name,width,height,channels:stream_tags=language,title \ -of json "$1" | jq -r '.streams[] | if .codec_type == "video" then " [\(.index)] VIDEO: \(.codec_name) \(.width)x\(.height)" elif .codec_type == "audio" then " [\(.index)] AUDIO: \(.codec_name) \(.channels)ch [\(.tags.language // "und")]" elif .codec_type == "subtitle" then " [\(.index)] SUBS: \(.codec_name) [\(.tags.language // "und")] \(.tags.title // "")" else " [\(.index)] \(.codec_type): \(.codec_name)" end' } # collection stats: codec, resolution, total size collstats() { dir="${1:-.}" echo "=== Collection Stats ===" echo "Total files: $(find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' \) | wc -l)" echo "" echo "By codec:" find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' \) -exec \ ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 {} \; 2>/dev/null | \ sort | uniq -c | sort -rn echo "" echo "By resolution:" find "$dir" -type f \( -iname '*.mkv' -o -iname '*.mp4' \) -exec \ ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 {} \; 2>/dev/null | \ sort -n | uniq -c | sort -rn echo "" echo "Total size: $(du -sh "$dir" 2>/dev/null | cut -f1)" } # generate thumbnail contact sheet contactsheet() { input="$1"; output="${2:-${input%.*}_contact.jpg}" cols="${3:-4}"; rows="${4:-6}" [ -z "$input" ] && { echo "Usage: contactsheet <video> [output] [cols] [rows]"; return 1; } total=$((cols * rows)) dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$input" | cut -d. -f1) interval=$((dur / (total + 1))) ffmpeg -i "$input" \ -vf "fps=1/${interval},scale=320:-1,tile=${cols}x${rows}" \ -frames:v 1 -q:v 2 "$output" echo "Contact sheet: $output" } # animated GIF preview preview_gif() { input="$1"; output="${2:-${input%.*}_preview.gif}" start="${3:-60}"; length="${4:-5}" [ -z "$input" ] && { echo "Usage: preview_gif <video> [output] [start] [length]"; return 1; } ffmpeg -ss "$start" -t "$length" -i "$input" \ -vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \ -loop 0 "$output" } # rip disc with MakeMKV ripdisc() { output="${1:-$HOME/rips}"; minlength="${2:-120}" mkdir -p "$output" echo "[*] Scanning disc..." makemkvcon info disc:0 --minlength="$minlength" --robot 2>/dev/null echo "[*] Ripping..." makemkvcon mkv disc:0 all "$output" --minlength="$minlength" --robot echo "[+] Done. Files in: $output" eject /dev/sr0 2>/dev/null } # batch convert ISOs to MKV iso2mkv() { isodir="${1:-.}"; outdir="${2:-./mkv}" mkdir -p "$outdir" find "$isodir" -iname '*.iso' | while read -r iso; do name=$(basename "$iso" .iso) dest="$outdir/$name" mkdir -p "$dest" echo "Processing: $iso" makemkvcon mkv iso:"$iso" all "$dest" --minlength=120 done } # HandBrake Blu-ray preset hb_bluray() { input="$1"; output="${2:-${input%.*}_hb.mkv}" [ -z "$input" ] && { echo "Usage: hb_bluray <input> [output]"; return 1; } HandBrakeCLI -i "$input" -o "$output" \ --preset="HQ 1080p30 Surround" \ --encoder x265 --encoder-preset slow --quality 20 \ --all-audio --audio-lang-list eng,und --all-subtitles } # search TMDB (requires TMDB_API_KEY env var) tmdb() { [ -z "$TMDB_API_KEY" ] && { echo "Set TMDB_API_KEY first"; return 1; } query=$(echo "$*" | sed 's/ /%20/g') curl -s "https://api.themoviedb.org/3/search/movie?api_key=${TMDB_API_KEY}&query=${query}" | \ jq -r '.results[:5][] | "\(.title) (\(.release_date[:4])) - Rating: \(.vote_average)"' } # OMDB lookup (requires OMDB_API_KEY env var) imdbrating() { [ -z "$OMDB_API_KEY" ] && { echo "Set OMDB_API_KEY first"; return 1; } title=$(echo "$*" | tr ' ' '+') curl -s "http://www.omdbapi.com/?t=${title}&apikey=${OMDB_API_KEY}" | \ jq -r '" \(.Title) (\(.Year)) - IMDb: \(.imdbRating)/10 - Director: \(.Director)"' } # trigger Plex library scan (requires PLEX_TOKEN env var) plexscan() { section="${1:-1}" [ -z "$PLEX_TOKEN" ] && { echo "Set PLEX_TOKEN first"; return 1; } curl -s "http://localhost:32400/library/sections/${section}/refresh?X-Plex-Token=${PLEX_TOKEN}" -X PUT echo "Plex scan triggered for section $section" } # trigger Jellyfin library scan (requires JELLYFIN_API_KEY env var) jfscan() { [ -z "$JELLYFIN_API_KEY" ] && { echo "Set JELLYFIN_API_KEY first"; return 1; } curl -s -X POST "http://localhost:8096/Library/Refresh?api_key=${JELLYFIN_API_KEY}" echo "Jellyfin scan triggered" } # archival-quality download with all metadata ytarchive() { yt-dlp \ -f "bestvideo+bestaudio" \ --merge-output-format mkv \ --write-sub --write-auto-sub \ --sub-lang "en,es,fr,de,ja" \ --convert-subs srt --embed-subs \ --embed-thumbnail --embed-metadata --embed-chapters \ --write-info-json \ --download-archive "$HOME/.ytdl-archive.txt" \ -o "%(title)s (%(upload_date>%Y)s) [%(height)sp].%(ext)s" \ "$@" } # archive entire channel ytchannel() { [ -z "$1" ] && { echo "Usage: ytchannel <channel-url>"; return 1; } yt-dlp \ -f "bestvideo[height>=1080]+bestaudio/bestvideo+bestaudio/best" \ --merge-output-format mkv \ --embed-subs --embed-thumbnail --embed-metadata --embed-chapters \ --download-archive archive.txt \ -o "%(uploader)s/%(upload_date)s - %(title)s [%(id)s].%(ext)s" \ --sponsorblock-mark all "$1" } # ffmpeg cheat sheet ffcheat() { cat << 'EOF' === FFmpeg Cheat Sheet === Remux: ffmpeg -i in.mkv -c copy out.mp4 Scale: ffmpeg -i in.mkv -vf scale=1920:1080 out.mkv Trim: ffmpeg -ss 00:01:00 -t 00:02:00 -i in.mkv -c copy out.mkv Extract audio: ffmpeg -i in.mkv -vn -c:a copy out.aac Strip audio: ffmpeg -i in.mkv -an -c:v copy out.mkv Add subs: ffmpeg -i in.mkv -i subs.srt -c copy -c:s srt out.mkv Concat: ffmpeg -f concat -i list.txt -c copy out.mkv GIF: ffmpeg -ss 5 -t 3 -i in.mkv -vf "fps=10,scale=320:-1" out.gif Thumbnail: ffmpeg -i in.mkv -vf "thumbnail" -frames:v 1 thumb.jpg HDR->SDR: ffmpeg -i hdr.mkv -vf "zscale=t=linear:npl=100,..." sdr.mkv EOF }