# 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 <file> <title> <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
}