~kris/dots

srice

ref: 178c9a25a484ead715392d50e21d03df486b8b41 srice/.config/.mksh/cinefile.sh -rw-r--r-- 16.1 KiB
178c9a25 — Kris Yotam conky: sync proper config from moirai (was the greenred variant) 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# 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
}