working version based on ffmpeg

Yves G. 2024-04-08 19:50:54 +02:00
parent e2152a1281
commit 768e5e11cb
1 changed files with 309 additions and 375 deletions

View File

@ -6,13 +6,12 @@
# Dependencies (needed in PATH):
# — isosize (part of util-linux) : get sector size and count of the disc (unless: -n)
# — dd, ddrescue : copy image of disc (unless: -n)
# — HandBrakeCLI (part of HandBrake) : get disc metadata and rip chapters of each title to MKV
# — (mplayer) : detect black borders of each title
# — ffmpeg, ffprobe (part of FFMpeg) : extract single frames from an MKV, and manipulate streams
# — HandBrakeCLI (part of HandBrake) : get disc metadata
# — ffmpeg, ffprobe (part of FFMpeg) : extract single frames and rip to MKV
# — spumux (part of dvdauthor) : generate missing VOBSUB streams to align all tracks
# — identify, montage, convert (part of ImageMagick): get extracted frame metadata, create visual timelines
# — czkawka_cli (part of Czkawka) : compare timelines and detect duplicate chapters
# — (mkvextract,) mkvmerge (part of MKVToolNix) : get MKV metadata and assemble chapters into a final MKV
# — mkvmerge (part of MKVToolNix) : get MKV metadata and assemble chapters into a final MKV
# — sed, gawk, grep, sort, uniq, head, cut, tr, wc : data manipulation
# — dirname, tee, ln, mktemp, cat, touch, mv, rm : file manipulation
# — jq : for parsing JSON and extracting metadata
@ -37,27 +36,6 @@
# — https://www.reddit.com/r/mkvtoolnix/comments/11nbfy0/tutorial_mediumlinked_tiny_segmented_mkvs_with/
# — https://www.reddit.com/r/handbrake/comments/bhqxve/handbrake_settings_explained/
# FIXME: HOW TO DETECT USELESS SUBTITLES? E.G.
# 3|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → Normal
# 5|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → ??
# 6|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → Commentary
# ⇓
# (about 4min for full movie for each subtitle track)
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 3 --subtitle-burned=none -i .iso -o sub3.mkv
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 5 --subtitle-burned=none -i .iso -o sub5.mkv
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 6 --subtitle-burned=none -i .iso -o sub6.mkv
# (instantaneous)
# $ mkvextract sub3.mkv tracks --raw 1:sub3.sub
# $ mkvextract sub5.mkv tracks --raw 1:sub5.sub
# $ mkvextract sub6.mkv tracks --raw 1:sub6.sub
# $ ls -l sub*
# -rw-r--r-- 1 yves yves 287386078 8 mars 18:54 sub3.mkv
# -rw-r--r-- 1 yves yves 1058252 8 mars 19:35 sub3.sub
# -rw-r--r-- 1 yves yves 286304465 8 mars 19:07 sub5.mkv
# -rw-r--r-- 1 yves yves 2268 8 mars 19:35 sub5.sub
# -rw-r--r-- 1 yves yves 289029155 8 mars 19:12 sub6.mkv
# -rw-r--r-- 1 yves yves 2684952 8 mars 19:35 sub6.sub
trap 'exit 100' SIGINT
# read command-line arguments
@ -84,7 +62,7 @@ KEEP_ALL=${BUILD:+true}
BUILD="${BUILD:-$(mktemp -d /tmp/bdvdrip-XXXXXX)}"
[ -d "$BUILD" ] && [ -w "$BUILD" ] || { echo "$BUILD is not a writeable directory"; exit 1; }
DVD="${DVD:-/dev/sr0}"
[ -r "$DVD" ] && ! [ -d "$DVD" ] || [ -f "$BUILD/.iso" ] || { echo "$DVD is not a useable readable input"; exit 1; }
[ -r "$DVD" ] && ! [ -d "$DVD" ] || [ -e "$BUILD/.iso" ] || { echo "$DVD is not a useable readable input"; exit 1; }
TARGET="${TARGET:-./output.mkv}"
[ -d "$(dirname "$TARGET")" ] && [ -w "$(dirname "$TARGET")" ] || { echo "$(dirname "$TARGET") is not a writeable directory"; exit 1; }
if ! [[ "$MAXW" =~ ^[0-9]+$ ]]; then MAXW=1280; fi
@ -92,7 +70,7 @@ if ! [[ "$MAXH" =~ ^[0-9]+$ ]]; then MAXH=720; fi
exec 3>&2
function log_and_run() {
local getOutput
local getOutput output
if [ "$1" == '-o' ]; then
getOutput=1; shift
fi
@ -101,16 +79,21 @@ function log_and_run() {
printf '\n>> %s\n>> %s\n' "$step" "${*@Q}" >&3
fi
case "$getOutput.$DEBUG" in
1.debug) "$@" 1> >(tee >(cat >&3)) 2> >(tee >(cat >&3) >&2) ;;
1.*) "$@" ;;
.debug) "$@" 1>&3 2>&3 ;;
.*) "$@" &>/dev/null ;;
1.debug) "$@" 1> >(tee >(cat >&3)) 2> >(tee >(cat >&3) >&2) ;; #OK
1.*) F=/tmp/fifo.$$.outerr; mkfifo $F; "$@" 1> >(tee -a $F) 2> >(tee -a $F >&2) & pid=$!; output="$(cat $F)"; rm -f $F; wait $pid ;;
.debug) "$@" 1>&3 2>&3 ;; #OK
.*) output="$("$@" 2>&1)" ;;
esac
excode=$?
if [ $excode -ne 0 ] && [ "$DEBUG" != quiet ]; then
printf '%s>>> ERROR: EXIT CODE %s\n\n' "${output:+"$output"$'\n'}" "$excode" >&3
fi
return $excode
}
# copy DVD
if [ ! -f "$BUILD/.iso" ]; then
if ! [ -f "$BUILD/.iso" ]; then
if [ -n "$NO_DD" ]; then
RM_ISO=${SHOW:+true}
ln -s "$DVD" "$BUILD/.iso"
@ -118,7 +101,7 @@ if [ ! -f "$BUILD/.iso" ]; then
read -r sectcount sectsize < <(LANG=C isosize -x "$DVD" | sed -r 's/.*: (.*), .*: (.*)$/\1 \2/')
log_and_run 'Dump disc to ISO image' \
dd -bs $sectsize count=$sectcount if="$DVD" of="$BUILD/.iso" || {
dd bs=$sectsize count=$sectcount if="$DVD" of="$BUILD/.iso" || {
log_and_run 'Dump disc to ISO image (slower because of media errors)' \
ddrescue -n -b$sectsize "$DVD" "$BUILD/.iso" "$BUILD/.iso.mapfile"
log_and_run 'Try to recover damaged sectors if needed' \
@ -129,14 +112,14 @@ fi
# fetch metadata
if [ ! -f "$BUILD/.json" ]; then
if ! [ -f "$BUILD/.json" ]; then
log_and_run -o 'Read disc metadata (tracks, chapters…)' \
HandBrakeCLI --json -t 0 --min-duration 5 -i "$BUILD/.iso" 2>/dev/null \
| sed $'1i \\\n{\n1,/JSON Title Set:/d' >"$BUILD/.json"
fi
ALL_META="$(
# log_and_run -o 'Parse disc metadata' \
log_and_run -o 'Parse metadata' \
jq -r ".TitleList[] | .Index as \$tnum | \
\"TITLE \(\$tnum) (\(.Duration.Hours):\(.Duration.Minutes):\(.Duration.Seconds))\", \
\"\tVideo (index, codec, geometry, frame-rate, bit-depth, chroma-subsampling, pixel aspect ratio, top:bottom:left:right borders, is interlaced)\", \
@ -153,7 +136,7 @@ ALL_META="$(
<"$BUILD/.json"
)"
# show information then exit if requested
# show information then exit, if requested
if [ -n "$SHOW" ]; then
echo "$ALL_META"
@ -161,15 +144,23 @@ if [ -n "$SHOW" ]; then
exit 0
fi
# parse information
# parse general information
declare TITLE_LIST=$(sed -rn 's/^TITLE (.*) .*/\1/p' <<<"$ALL_META")
declare VFORMAT=$(awk -F$'\t' '$5~"x480$"{print "ntsc";exit}' <<<"$ALL_META")
declare -A VSECONDS
declare -A CHAPTERS
declare -A CHNAMES
declare -A INTERLV
while read -r tnum h m s; do VSECONDS[$tnum]=$((3600*h+60*m+s)); done < <(
sed -nr 's/^TITLE (.*) \((.*):(.*):(.*)\)/\1 \2 \3 \4/p' <<<"$ALL_META")
for tnum in $TITLE_LIST; do CHAPTERS[$tnum]=$(
sed -rn "s/^C-$tnum\\t\\t([^\\t]*)\\t.*/\\1/p" <<<"$ALL_META"); done
while read -r tnum chnum name; do CHNAMES[$tnum.$chnum]="$name"; done < <(
sed -nr 's/^C-([0-9]+)\t\t([0-9]+)\t[0-9:]*\t(.*)/\1 \2 \3/p' <<<"$ALL_META")
while read -r tnum interl; do INTERLV[$tnum]="$interl"; done < <(
sed -nr 's/^V-([0-9]+)\t.*\t([^\t]*)$/\1 \2/p' <<<"$ALL_META")
# compute final geometry
@ -186,10 +177,10 @@ function scale_round() {
printf '%d %d %d %d %d %d' $adj1 $adj2 $dim1 $dim2 $5 $6
}
declare -A SRC_RAW SRC_LTRB SRC_WH2WH DAR_COUNT RIP_VIDEO
declare -A SRC_RAW SRC_LTRB SRC_WH2WH DAR_COUNT
# → compute optimal crop and sizes, to target a pixel aspect ratio of 1:1, with width and height being multiples of 16
for tnum in "${!VSECONDS[@]}"; do
for tnum in $TITLE_LIST; do
read -r raww rawh parn pard cropt cropb cropl cropr < <(sed -nr "
s/^V-$tnum\t(\t[^\t]*){2}\t([0-9]+)x([0-9]+)/\2 \3/; T
s#(\t[^\t]*){3}\t([0-9]+)/([0-9]+)\t([0-9]+):([0-9]+):([0-9]+):([0-9]+)\t.*# \2 \3 \4 \5 \6 \7#p
@ -215,6 +206,8 @@ for tnum in "${!VSECONDS[@]}"; do
fi
fi
fi
[ $srcw -le $raww ] || srcw=$raww
[ $srch -le $rawh ] || srch=$rawh
SRC_LTRB[$tnum]="$cropl $cropt $cropr $cropb"
SRC_WH2WH[$tnum]="$srcw $srch $finalw $finalh $scalen $scaled"
SRC_RAW[$tnum]="$raww $rawh $parn $pard"
@ -227,7 +220,7 @@ read -r maxw maxh x < <(
unset DAR_COUNT
# → rescale each title according to the chosen width and height
for tnum in "${!VSECONDS[@]}"; do
for tnum in $TITLE_LIST; do
read -r sw sh fw fh scn scd <<<"${SRC_WH2WH[$tnum]}"
read -r rw rh parn pard <<<"${SRC_RAW[$tnum]}"
# already OK ⇒ skip
@ -248,65 +241,14 @@ for tnum in "${!VSECONDS[@]}"; do
fi
SRC_WH2WH[$tnum]="$newsrcw $newsrch $neww $newh $newscalen $newscaled"
done
# → resize crop-box of titles when it can avoid double-encoding
for tnum in "${!VSECONDS[@]}"; do
read -r sw sh fw fh scn scd <<<"${SRC_WH2WH[$tnum]}"
read -r cl ct cr cb <<<"${SRC_LTRB[$tnum]}"
# already OK ⇒ skip
if [ $fw -eq $maxw ] && [ $fh -eq $maxh ]; then
RIP_VIDEO[$tnum]="--crop $ct:$cb:$cl:$cr --width $fw --height $fh --pixel-aspect 1:1 -e x265_10bit --encoder-profile main10 --vfr -q 18"
continue
fi
# else expand if it can reach the target geometry
read -r rw rh parn pard <<<"${SRC_RAW[$tnum]}"
if [ $parn -gt $pard ]; then
newsrcw=$((maxw*scd*pard/parn/scn))
newsrch=$((maxh*scd/scn))
maxraww=$((4*pard/parn+rw)) # same ¾-down/¼-up rounding rule
maxrawh=$((4+rh))
else
newsrcw=$((maxw*scd/scn))
newsrch=$((maxh*scd*parn/pard/scn))
maxraww=$((4+rw)) # same ¾-down/¼-up rounding rule
maxrawh=$((4*parn/pard+rh))
fi
if [ $newsrcw -le $maxraww ] && [ $newsrch -le $maxrawh ]; then
if [ $newsrcw -ge $maxraww ]; then
newcl=0; newcr=0; newsrcw=$rw; newsrch=$rh
else
newcl=$(( (rw-newsrcw)/2 )); newcr=$(( rw-newsrcw-newcl ))
fi
if [ $newsrch -ge $maxrawh ]; then
newct=0; newcb=0
else
newct=$(( (rh-newsrch)/2 )); newcb=$(( rh-newsrch-newct ))
fi
SRC_WH2WH[$tnum]="$newsrcw $newsrch $maxw $maxh $scn $scd"
RIP_VIDEO[$tnum]="--crop $newct:$newcb:$newcl:$newcr --width $maxw --height $maxh --pixel-aspect 1:1 -e x265_10bit --encoder-profile main10 --vfr -q 18"
else
RIP_VIDEO[$tnum]="--crop $ct:$cb:$cl:$cr --width $fw --height $fh --pixel-aspect 1:1 -e x264_10bit --encoder-profile auto --vfr -q 15"
fi
done
unset SRC_LTRB
unset SRC_RAW
## $1×$2, $3×$4: with×height of video, width×height of enclosing box
## &1: "<left border> <top border> <right border> <bottom border>"
#function compute_borders() {
# local top=$(( ($4-$2)/2 )) # ½Δ
# local left=$(( ($3-$1)/2 )) # ½Δ
# local bottom=$(( $4-$2-top ))
# local right=$(( $3-$1-left ))
# printf '%d %d %d %d' $left $top $right $bottom
#}
# compute final audio streams
declare -A SRC_AUDIO CATEG_COUNT RIP_AUDIO
declare -A SRC_AUDIO CATEG_COUNT
# → categorize all audio streams
for tnum in "${!VSECONDS[@]}"; do
for tnum in $TITLE_LIST; do
allaudio="$(awk -F$'\t' -vT=$tnum '$1=="A-" T{print T, $3, $5, $7, $8, $11, $4, $6, $10, $12}' <<<"$ALL_META")"
# (technical info:) <1:title number> <2:stream number> <3:codec> <4:Hz> <5:bit/s> <6:secondary?>
# (category:) <7:lang> <8:channels count> <9:commentary?> <10:visu.impaired?> <11:priority>
@ -318,8 +260,8 @@ for tnum in "${!VSECONDS[@]}"; do
done
)"
while read -r x x cod freq bps x lng cnt cmt blind prio; do
k="$cod $freq $bps $lng $cnt $cmt $blind $prio"
CATEG_COUNT["$k"]=$(awk -vN=${CATEG_COUNT["$k"]:-0} "/C-$tnum\t/{N++};END{print N}" <<<"$ALL_META")
x="$cod $freq $bps $lng $cnt $cmt $blind $prio"
CATEG_COUNT["$x"]=$(awk -vN=${CATEG_COUNT["$x"]:-0} "/C-$tnum\t/{N++};END{print N}" <<<"$ALL_META")
done <<<"${SRC_AUDIO[$tnum]}"
done
@ -331,49 +273,39 @@ allaudio="$(export IFS=$'\n'; echo "${SRC_AUDIO[*]}")"
MKV_AUDIO="$(
for lng in ${ALL_LANGS//,/ }; do
while read -r x x cod freq bps x lng cnt cmt blind prio; do
k="$cod $freq $bps $lng $cnt $cmt $blind $prio"
x="$cod $freq $bps $lng $cnt $cmt $blind $prio"
name="${lng^^}${expo[$prio]} ${cnt}🕩"
[ "$cmt" == true ] && name+=" 🗩"
[ "$blind" == true ] && name+=" 🙈"
echo "${CATEG_COUNT["$k"]} $cod $freq $bps $lng $cnt $cmt $blind $prio $name"
echo "${CATEG_COUNT["$x"]} $cod $freq $bps $lng $cnt $cmt $blind $prio $name"
done < <(awk -vL=$lng '$7==L{print}' <<<"$allaudio") \
| sort -k7,7 -k8,8 -k6,6nr -k9,9n -k1,1nr \
| uniq -f5
done \
| nl -nln -s' ' -w1 | cut -d' ' -f1,3-
)"
unset expo
unset expo allaudio
unset CATEG_COUNT
# → set audio ripping parameters to reach the target
declare -A count2mix=(["1"]=mono ["2"]=dpl2 ["3"]=dpl2 ["4"]=dpl2 ["5"]=dpl2 ["7"]=6point1 ["8"]=7point1)
for tnum in "${!VSECONDS[@]}"; do
declare -a nums=() cods=() mixs=() rates=() names=()
while read -r x anum cod freq bps x lng cnt cmt blind prio; do
read -r num targetc targetf targetr x x x x x name < <(
awk -vL=$lng -vC=$cnt -vM=$cmt -vB=$blind -vP=$prio '$5==L && $6==C && $7==M && $8==B && $9==P{print}' <<<"$MKV_AUDIO")
nums+=($anum)
rates+=(auto)
names+=("$num:${name// / }")
if [[ $cod =~ ^(aac|ac3|eac3|truehd|dts|dtshd|mp2|mp3|flac|opus)$ ]]; then
cods+=("copy:$cod"); mixs+=("${count2mix[$cnt]:-5point1}")
else
cods+=("flac16"); mixs+=("${count2mix[$cnt]:-5point1}")
fi
done <<<"${SRC_AUDIO[$tnum]}"
RIP_AUDIO["$tnum"]="$(export IFS=,
printf -- '-a %s -E %s --mixdown %s -R %s -A %s' \
"${nums[*]:-none}" "${cods[*]:-none}" "${mixs[*]:-none}" "${rates[*]:-none}" "${names[*]:-none}"
)"
unset nums cods mixs rates names
done
# (technical info:) <1:stream number> <2:MKV codec> <3:FFMpeg index> <4:FFMpeg codec> <5:FFMpeg layout> <6:Hz> <7:bit/s>
# (category:) <8:lang> <9:channels count> <10:commentary?> <11:visu.impaired?> <12:priority> <13:name>
MKV_AUDIO="$(
while read -r num cod freq bps lng count cmt blind prio name; do
# take example on an existing stream
read -r ffnum t2 < <(export IFS=$'\n'; echo "${SRC_AUDIO[*]}" \
| awk '$1!=T{T=$1;C=0};{print ++C, $0}' | sed -nr "s|^([^ ]+) ([^ ]+) [^ ]+ $cod $freq $bps [^ ]+ $lng $count $cmt $blind $prio\$|\\1 \\2|p;T;q")
read -r ffcod ffchanlay < <(ffprobe -hide_banner -output_format json -show_streams -select_streams a -f dvdvideo -title $t2 "$BUILD/.iso" 2>/dev/null \
| jq -r --argjson I $ffnum '.streams[] | select(.index == $I) | "\(.codec_name) \(.channel_layout)"')
echo "$num $cod $ffnum $ffcod $ffchanlay $freq $bps $lng $count $cmt $blind $prio $name"
done <<<"$MKV_AUDIO"
)"
# compute final subtitle streams
declare -A SRC_SUB RIP_SUB
declare -A SRC_SUB
# → categorize all subtitle streams
for tnum in "${!VSECONDS[@]}"; do
for tnum in $TITLE_LIST; do
allsubs="$(awk -F$'\t' -vT=$tnum '$1=="S-" T{print $3, $8, $5, $4, $7, $9}' <<<"$ALL_META")"
# (technical info:) <1:stream number> <2:forced?>
# (category:) <3:codec> <4:lang> <5:commentary?> <6:hear.impaired?> <7:priority>
@ -404,55 +336,9 @@ MKV_SUB="$(
)"
unset expo
# → set subtitle ripping parameters to reach the target
declare -A count2mix=(["1"]=mono ["2"]=dpl2 ["3"]=dpl2 ["4"]=dpl2 ["5"]=dpl2 ["7"]=6point1 ["8"]=7point1)
for tnum in "${!VSECONDS[@]}"; do
declare -a nums=() names=()
while read -r snum x cod lng cmt deaf prio; do
name="$(sed -nr "s/^([^ ]+) $cod $lng $cmt $deaf $prio /\1:/p" <<<"$MKV_SUB")"
nums+=($snum)
names+=("${name// / }")
done <<<"${SRC_SUB[$tnum]}"
RIP_SUB["$tnum"]="$(export IFS=,; printf -- '-s %s -S %s' "${nums[*]:-none}" "${names[*]:-none}")"
unset nums names
done
# rip chapters
#2024-03-20 19:01 <Marth64> yrc: with ffmpeg DVD demuxer you can extract the chapters in one shot now if that helps
#2024-03-20 19:01 <Marth64> ffmpeg -f dvdvideo -chapter_start 5 -chapter_end 5 -i DVD_INPUT -map 0 -c copy Chapter5.mkv
#2024-03-20 19:02 <Marth64> it will also apply dispositions for commentary, karaoke, AD audio if dvd had tagged them correctly
if [ -z "$(ls "$BUILD/"tmp.*.ch.*.mkv 2>/dev/null)" ]; then
for chapter in "${!CHNAMES[@]}"; do
tnum=${chapter%.*}
chnum=${chapter#*.}
log_and_run "Rip track $tnum chapter $chnum (${CHNAMES[$chapter]})" \
HandBrakeCLI -t $tnum --min-duration 5 -c $chnum --no-markers \
${RIP_VIDEO[$tnum]} ${RIP_AUDIO[$tnum]} ${RIP_SUB["$tnum"]} --subtitle-burned=none \
--non-anamorphic --no-comb-detect --no-decomb --no-detelecine \
--no-hqdn3d --no-nlmeans --no-chroma-smooth --no-unsharp --no-lapsharp --no-deblock \
-i "$BUILD/.iso" -o "$BUILD/tmp.$tnum.ch.$chnum.mkv" </dev/null
done
fi
# (technical info:) <1:stream number> <2:MKV codec> <3:FFMpeg index> <4:FFMpeg codec> <5:FFMpeg layout> <6:Hz> <7:bit/s>
# (category:) <8:lang> <9:channels count> <10:commentary?> <11:visu.impaired?> <12:priority> <13:name>
MKV_AUDIO="$(
while read -r num cod freq bps lng count cmt blind prio name; do
# take example on an existing stream
read -r ffnum t2 < <(export IFS=$'\n'; echo "${SRC_AUDIO[*]}" \
| awk '$1!=T{T=$1;C=0};{print ++C, $0}' | sed -nr "s|^([^ ]+) ([^ ]+) [^ ]+ $cod $freq $bps [^ ]+ $lng $count $cmt $blind $prio\$|\\1 \\2|p;T;q")
read -r ffcod ffchanlay < <(ffprobe -hide_banner -select_streams a -output_format json -show_streams "$BUILD/tmp.$t2.ch.1.mkv" 2>/dev/null \
| jq -r --argjson I $ffnum '.streams[] | select(.index == $I) | "\(.codec_name) \(.channel_layout)"')
echo "$num $cod $ffnum $ffcod $ffchanlay $freq $bps $lng $count $cmt $blind $prio $name"
done <<<"$MKV_AUDIO"
)"
# generate per-chapter timeline snapshots
if [ -z "$(ls "$BUILD/"tmp.*.ch.*.png 2>/dev/null)" ]; then
if [ -z "$(ls "$BUILD/"t.*.ch.*.png 2>/dev/null)" ]; then
[ -d "$BUILD/.tsnap" ] || mkdir "$BUILD/.tsnap"
while read -r tnum chnum x; do
seconds=$(awk -F$'\t' -vT=$tnum -vC=$chnum '$3==C && $1=="C-" T{FS=":"; $0=$4; print 3600*$1+60*$2+$3}' <<<"$ALL_META")
@ -464,7 +350,8 @@ if [ -z "$(ls "$BUILD/"tmp.*.ch.*.png 2>/dev/null)" ]; then
while [ $snapsec -le $seconds ]; do
target="$BUILD/.tsnap/t.$tnum.ch.$chnum.s.$snapsec.png"
log_and_run "Make snapshot @${snapsec}s of track $tnum chapter $chnum" \
ffmpeg -hide_banner -i "$BUILD/tmp.$tnum.ch.$chnum.mkv" -ss ${snapsec}s -vf scale=w=256:h=144:force_original_aspect_ratio=decrease -vframes 1 "$target%1d.png" </dev/null
ffmpeg -hide_banner -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum -i "$BUILD/.iso" \
-ss ${snapsec}s -vf scale=w=256:h=144:force_original_aspect_ratio=decrease -vframes 1 "$target%1d.png" </dev/null
mv "$target"*.png "$target"
snapsec=$((snapsec+5))
snapcount=$((snapcount+1))
@ -478,7 +365,7 @@ if [ -z "$(ls "$BUILD/"tmp.*.ch.*.png 2>/dev/null)" ]; then
log_and_run "Annotate timeline snapshot of track $tnum chapter $chnum with audio and subtitles metadata" \
convert -fill red -gravity SouthEast -annotate +0+0 \
"$(export IFS=+; echo "${audio[*]}")|$(export IFS=+; echo "${subs[*]}")" \
"$BUILD/.tsnap/t.$tnum.ch.$chnum.png" "$BUILD/tmp.$tnum.ch.$chnum.png" </dev/null
"$BUILD/.tsnap/t.$tnum.ch.$chnum.png" "$BUILD/t.$tnum.ch.$chnum.png" </dev/null
unset audio subs
done < <(
# restrict to chapters having common durations
@ -499,10 +386,24 @@ function isepisode() {
echo $?
}
if [ ! -f "$BUILD/.duplicates" ]; then
declare -A STREAMS_IDS
declare -A SUBST
for tnum in $TITLE_LIST; do
strid=
while read -r x x x x x x lng cnt cmt blind prio; do
strid+=",${lng}${prio}x${cnt}(${cmt}/${blind})"
done <<<"${SRC_AUDIO[$tnum]}"
while read -r x x cod lng cmt deaf prio; do
strid+=",${lng}${prio}:${cod}(${cmt}/${deaf})"
done <<<"${SRC_SUB[$tnum]}"
STREAMS_IDS[$tnum]="$strid"
done
if ! [ -f "$BUILD/.duplicates" ]; then
log_and_run -o "Detect duplicates among all chapters of all titles" \
czkawka_cli image -f "$BUILD/.raw_dedup" -s High -m 1 -R -d "$BUILD/" \
| sed -rn 's#^/.*/t\.([0-9]+)\.ch\.([0-9]+)\.png.*#\1 \2#p' \
| sed -rn 's#^"?/.*/t\.([0-9]+)\.ch\.([0-9]+)\.png.*#\1 \2#p' \
| while read -r tnum1 chnum1; read -r tnum2 chnum2; do
if [ "${STREAMS_IDS[$tnum1]}" == "${STREAMS_IDS[$tnum2]}" ]; then
# long film first, else episode first, else lower title number first, else lower chapter number first
@ -514,204 +415,218 @@ if [ ! -f "$BUILD/.duplicates" ]; then
done >"$BUILD/.duplicates"
fi
# fill-in missing streams
if [ -z "$(ls "$BUILD/"t.*.ch.*.mkv 2>/dev/null)" ]; then
for tnum in "${!VSECONDS[@]}"; do
read -r x x tw th x <<<"${SRC_WH2WH[$tnum]}"
while read -r chnum chtimeceil; do
declare -A blank=()
ffcmd=(ffmpeg -hide_banner -i "$BUILD/tmp.$tnum.ch.$chnum.mkv")
ffmap=(-map_chapters -1 -map 0:V:0)
if [ $tw -eq $maxw ] && [ $th -eq $maxh ]; then
ffenc=(-c:v:0 copy -copyinkf:v:0 -copyts:v:0)
else
# https://superuser.com/a/991412
ffenc=(-c:v:0 hevc -hwaccel:v:0 auto -profile:v:0 main10 -fps_mode:v:0 vfr -qscale:v:0 18 -preset:v:0 slower -pix_fmt:v:0 + -enc_time_base:v:0 demux -vf:v:0 "scale=w=${maxw}:h=${maxh}:force_original_aspect_ratio=1,pad=${maxw}:${maxh}:(ow-iw)/2:(oh-ih)/2")
fi
# audio streams
while read -r mkvnum mkvcod ffnum ffcod ffchanlay mkvfreq mkvrate lng count cmt blind prio name; do
read -r x num cod freq rate x < <(
awk -vL=$lng -vC=$count -vM=$cmt -vB=$blind -vP=$prio '$7==L && $8==C && $9==M && $10==B && $11==P{print}' <<<"${SRC_AUDIO[$tnum]}")
if [ -z "$num" ]; then
# no such stream in this title
f="$BUILD/tmp.empty_a.$ffchanlay.$mkvfreq.$mkvrate.$chtimeceil.$ffcod"
if ! [ -f "$f" ]; then
log_and_run "For title $tnum chapter $chnum, create missing audio stream for ${chtimeceil}s of ${mkvnum}: ${name}" \
ffmpeg -hide_banner -lavfi anullsrc=channel_layout="${ffchanlay}":sample_rate=${mkvfreq}:duration=${chtimeceil} -c:a ${ffcod} -b:a ${mkvrate} "$f" </dev/null
ffcmd+=(-i "$f")
blank["$f"]=$((${#blank[*]}+1))
fi
ffmap+=(-map ${blank["$f"]}:a:0)
ffenc+=(-c:a:$((mkvnum-1)) copy)
elif [ "$cod,$freq,$rate" == "$mkvcod,$mkvfreq,$mkvrate" ]; then
# exact match in this title
ffmap+=(-map 0:a:$((num-1)))
ffenc+=(-c:a:$((mkvnum-1)) copy)
else
# steam has different characteristics
ffmap+=(-map 0:a:$((num-1)))
ffenc+=(-c:a:$((mkvnum-1)) $ffcod -ar:a:$((mkvnum-1)) $mkvfreq -b:a:$((mkvnum-1)) $mkvrate)
fi
done <<<"$MKV_AUDIO"
# subtitle streams
while read -r mkvnum mkvcod lng cmt deaf prio name; do
read -r num x < <(
awk -vC=$mkvcod -vL=$lng -vM=$cmt -vD=$deaf -vP=$prio '$3==C && $4==L && $5==M && $6==D && $11==P{print}' <<<"${SRC_SUB[$tnum]}")
if [ -n "$num" ]; then
# exact match in this title
ffmap+=(-map 0:s:$((num-1)))
ffenc+=(-c:s:$((mkvnum-1)) copy)
elif [ "$mkvcod" != 'VOBSUB:bitmap' ]; then
# unsupported type!
printf 'ERROR: UNSUPPORTED MISSING SUBTITLE STREAM FOR TITLE %d CHAPTER %d OF TYPE %s\n' $tnum $chnum "$mkvcod" >&2
exit 1
else
# no such stream in this title
f="$BUILD/tmp.empty_s.$chtimeceil.mpeg2"
if ! [ -f "$f" ]; then
if ! [ -f "$BUILD/tmp.empty_pixel.png" ]; then
log_and_run "Generate a transparent pixel for use as an empty subtitle stream" \
convert -size 1x1 'xc:rgba(0,0,0,0)' "$BUILD/tmp.empty_pixel.png"
fi
log_and_run -o "Generate XML description for ${chtimeceil}s of empty subtitle" \
echo "<subpictures format=\"PAL\"><stream><spu start=\"00:00:00.00\" end=\"$(date -u -d@${chtimeceil} +%T.00)\" image=\"$BUILD/tmp.empty_pixel.png\"/></stream></subpictures>" >"$BUILD/tmp.empty_s.xml"
log_and_run -o "For title $tnum chapter $chnum, create missing subtitle stream for ${chtimeceil}s of ${mkvnum}: ${name}" \
spumux -m dvd --nomux --nodvdauthor-data "$BUILD/tmp.empty_s.xml" </dev/null >"$f"
ffcmd+=(-i "$f")
blank["$f"]=$((${#blank[*]}+1))
fi
ffmap+=(-map ${blank["$f"]}:s:0)
ffenc+=(-c:s:$((mkvnum-1)) copy)
fi
done <<<"$MKV_SUB"
log_and_run "Adapt title $tnum chapter $chnum to final MKV streams" \
"${ffcmd[@]}" "${ffmap[@]}" "${ffenc[@]}" -shortest "$BUILD/t.$tnum.ch.$chnum.mkv"
unset blank ffcmd ffmap ffenc
rm -f "$BUILD/tmp.empty_s.xml" "$BUILD/tmp.empty_pixel.png"
done < <(awk -F$'\t' -vT=$tnum '$1=="C-" T{bFS=FS; n=$3; FS=":"; $0=$4; print n, 3600*$1+60*$2+$3+1; FS=bFS}' <<<"$ALL_META")
done
fi
exit
# fetch chapter metadata
if [ -z "$(ls "$BUILD/"t.*.ch.*.json 2>/dev/null)" ]; then
for mkv in "$BUILD/"t.*.ch.*.mkv; do
log_and_run "Read metadata (length, codecs…) for $mkv" \
mkvmerge -F json -i "$mkv" -r "${mkv%mkv}json"
done
fi
declare -A STREAMS_IDS
declare -A CHAPTERS
jqscript='['
jqscript+='([.tracks[] | [select(.type=="video").properties] | sort_by(.number)[] | "\(.number),\(.codec_id),\(.pixel_dimensions)"] | join("+"))'
jqscript+=',([.tracks[] | [select(.type=="audio").properties] | sort_by(.number)[] | "\(.number),\(.codec_id),\(.audio_channels)x\(.audio_sampling_frequency),\(.language)"] | join("+"))'
jqscript+=',([.tracks[] | [select(.type=="subtitles").properties] | sort_by(.number)[] | "\(.number),\(.codec_id),\(.language)"] | join("+"))'
jqscript+='] | join("|")'
TITLE_LIST=''
while read -r tnum chnum; do
TITLE_LIST="${TITLE_LIST% $tnum} $tnum"
CHAPTERS[$tnum]="${CHAPTERS[$tnum]} $chnum"
done < <(ls -1 "$BUILD/"t.*.ch.*.json | sed -r 's#.*/t\.(.*)\.ch\.(.*)\.json#\1 \2#' | sort -k1,1n -k2,2n)
for tnum in $TITLE_LIST; do
read chnum x <<<"${CHAPTERS[$tnum]}"
STREAMS_IDS[$tnum]="$(jq -r "$jqscript" <"$BUILD/t.$tnum.ch.$chnum.json")"
done
# detect source tracks that will fit together within the same end-result track
if [ ! -f "$BUILD/.titlelist" ]; then
for tnum in $TITLE_LIST; do
strid="${STREAMS_IDS[$tnum]}"
lowestwithid=$(for i in "${!STREAMS_IDS[@]}"; do printf '%d\t%s\t\n' $i "${STREAMS_IDS[$i]}"; done \
| grep -F $'\t'"$strid"$'\t' | sort -t$'\n' -k1,1n | head -n 1 | cut -f1)
printf '%d %d %d %d\n' $(islongfilm $tnum) $(isepisode $tnum) $lowestwithid $tnum
done \
| sort -k1,1n -k2,2n -k3,3n -k4,4n | awk '
$1=="0"{print $4; next}
($2 $3)!=streams{if(streams!=""){printf("\n")};streams=($2 $3);printf("%d",$4);next}
{printf(" %d",$4)}
END{printf("\n")}
' >"$BUILD/.titlelist"
fi
# compute chapters timestamps, UIDs, and substitutions + prepare merging
declare -A TIMESTAMPS
declare -a STREAMS_LIST
declare -A STREAMS_TO_REF
declare -A STREAMS_TO_FILES
declare -A CHAPTERS_IDS
#declare -A UIDS
declare -A SUBST
declare -a MKVMRGARGS
while read -r t1 ch1 t2 ch2; do
SUBST[$t2.$ch2]="$t1.$ch1"
done <"$BUILD/.duplicates"
while read -r tlist; do
strid="${STREAMS_IDS[${tlist%% *}]}"
if printf '%s\n' "${STREAMS_LIST[@]}" | grep -qxF "$strid"; then
ref=${STREAMS_TO_REF[$strid]}
else
ref=0
STREAMS_LIST+=("$strid")
fi
for tnum in $tlist; do
for chnum in ${CHAPTERS[$tnum]}; do
tch=$tnum.$chnum
while [ -n "${SUBST[$tch]}" ]; do tch=${SUBST[$tch]}; done
if [ $tch == $tnum.$chnum ]; then
# compute milliseconds
start=$ref
stop=$(bc -lq <<<"$ref+$(jq -r '.container.properties | .duration/.timestamp_scale' <"$BUILD/t.$tnum.ch.$chnum.json")")
ref=$stop
TIMESTAMPS[$tch]="$start $stop"
#UIDS[$tch]="$(jq -r .container.properties.segment_uid <"$BUILD/t.$tnum.ch.$chnum.json")"
STREAMS_TO_FILES[$strid]="${STREAMS_TO_FILES[$strid]}|$BUILD/t.$tnum.ch.$chnum.mkv"
else
TIMESTAMPS[$tnum.$chnum]="${TIMESTAMPS[$tch]}"
#UIDS[$tnum.$chnum]="${UIDS[$tch]}"
fi
CHAPTERS_IDS[$tnum.$chnum]="$(jq -r .container.properties.segment_uid <"$BUILD/t.$tnum.ch.$chnum.json" \
| tr '[a-f]' '[A-F]' | bc -q <<<"ibase=16;$(cat)" | head -c 10)"
done
done
STREAMS_TO_REF[$strid]=$ref
done <"$BUILD/.titlelist"
# rip chapters and fill-in missing streams
declare -A STREAMS_TO_OFFSET
declare -A USED_A USED_S
for tnum in $TITLE_LIST; do
read -r sw sh fw fh scn scd <<<"${SRC_WH2WH[$tnum]}"
read -r cropl cropt cropr cropb <<<"${SRC_LTRB[$tnum]}"
firstch=true
while read -r chnum chtimefloor; do
[ -z "${SUBST[$tnum.$chnum]}" ] || continue
declare -A blank=()
# video stream
ffcmd=(
ffmpeg -hide_banner -f dvdvideo -preindex 1 -trim false
-title $tnum -chapter_start $chnum -chapter_end $chnum -i "$BUILD/.iso"
)
ffmap=(-map_chapters -1 -map 0:V:0)
ffenc=()
filter=
if [ "${INTERLV[$tnum]}" == true ]; then
filter+=",bwdif"
fi
filter+=",crop=x=$cropl:y=$cropt:w=$((sw/2*2)):h=$((sh/2*2)):exact=1"
if [ $sw -ne $fw ] || [ $sh -ne $fh ]; then
ffenc+=(-sws_flags lanczos+accurate_rnd)
filter+=",scale=w=$fw:h=$fh:force_original_aspect_ratio=disable,setsar=1/1"
fi
if [ $fw -ne $maxw ] || [ $fh -ne $maxh ]; then
filter+=",pad=w=$maxw:h=$maxh:x=-1:y=-1"
fi
ffenc+=(-filter:v:0 "${filter:1}" -enc_time_base:v:0 demux)
ffenc+=(-c:v:0 libx265 -x265-params:v:0 profile=main10:preset=slower:crf=18:sar=1:videoformat=${VFORMAT:-pal}:rc-lookahead=120:bframes=12:ref=6:subme=7:aq-mode=3)
# audio streams
while read -r mkvnum mkvcod ffnum ffcod ffchanlay mkvfreq mkvrate lng count cmt blind prio name; do if [ -n "$mkvnum" ]; then
read -r x num cod freq rate x < <(
awk -vL=$lng -vC=$count -vM=$cmt -vB=$blind -vP=$prio '$7==L && $8==C && $9==M && $10==B && $11==P{print}' <<<"${SRC_AUDIO[$tnum]}")
if [ -z "$num" ]; then
# no such stream in this title
f="$BUILD/tmp.empty_a.$ffchanlay.$mkvfreq.$mkvrate.$chtimefloor.$ffcod"
if ! [ -f "$f" ] && ! [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then
log_and_run "For title $tnum chapter $chnum, create missing audio stream for ${chtimefloor}s of ${mkvnum}: ${name}" \
ffmpeg -hide_banner -strict -2 -lavfi anullsrc=channel_layout="${ffchanlay}":sample_rate=${mkvfreq}:duration=${chtimefloor} -c:a ${ffcod} -b:a ${mkvrate} "$f" </dev/null
fi
if [ -z "${blank["$f"]}" ]; then
ffcmd+=(-i "$f")
blank["$f"]=$((${#blank[*]}+1))
fi
ffmap+=(-map ${blank["$f"]}:a:0)
ffenc+=(-c:a:$((mkvnum-1)) copy)
elif [ "$cod,$freq,$rate" == "$mkvcod,$mkvfreq,$mkvrate" ]; then
# exact match in this title
[ -n "$firstch" ] && USED_A[$tnum]="${USED_A[$tnum]} $mkvnum"
ffmap+=(-map 0:a:$((num-1)))
ffenc+=(-c:a:$((mkvnum-1)) copy -map_metadata:s:a:$((mkvnum-1)) 0:s:a:$((num-1)))
else
# steam has different characteristics
[ -n "$firstch" ] && USED_A[$tnum]="${USED_A[$tnum]} $mkvnum"
ffmap+=(-map 0:a:$((num-1)))
ffenc+=(-c:a:$((mkvnum-1)) $ffcod -ar:a:$((mkvnum-1)) $mkvfreq -b:a:$((mkvnum-1)) $mkvrate -map_metadata:s:a:$((mkvnum-1)) 0:s:a:$((num-1)))
fi
ffenc+=(-metadata:s:a:$((mkvnum-1)) language=$lng -metadata:s:a:$((mkvnum-1)) title="$mkvnum: $name")
fi; done <<<"$MKV_AUDIO"
# subtitle streams
while read -r mkvnum mkvcod lng cmt deaf prio name; do if [ -n "$mkvnum" ]; then
read -r num x < <(
awk -vC=$mkvcod -vL=$lng -vM=$cmt -vD=$deaf -vP=$prio '$3==C && $4==L && $5==M && $6==D && $7==P{print}' <<<"${SRC_SUB[$tnum]}")
if [ -n "$num" ]; then
# exact match in this title
[ -n "$firstch" ] && USED_S[$tnum]="${USED_S[$tnum]} $mkvnum"
ffmap+=(-map 0:s:$((num-1)))
ffenc+=(-c:s:$((mkvnum-1)) copy -map_metadata:s:s:$((mkvnum-1)) 0:s:s:$((num-1)))
elif [ "$mkvcod" != 'VOBSUB:bitmap' ]; then
# unsupported type!
printf 'ERROR: UNSUPPORTED MISSING SUBTITLE STREAM FOR TITLE %d CHAPTER %d OF TYPE %s\n' $tnum $chnum "$mkvcod" >&2
exit 1
else
# no such stream in this title
f="$BUILD/tmp.empty_s.${VFORMAT:-pal}.$chtimefloor.mpeg2"
if ! [ -f "$f" ] && ! [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then
if ! [ -f "$BUILD/tmp.empty_pixel.png" ]; then
log_and_run "Generate a transparent pixel for use as an empty subtitle stream" \
convert -size 1x1 'xc:rgba(0,0,0,0)' "$BUILD/tmp.empty_pixel.png"
fi
log_and_run -o "Generate XML description for ${chtimefloor}s of empty subtitle" \
echo "<subpictures format=\"$([ "$VFORMAT" == ntsc ] && echo NTSC || echo PAL)\"><stream><spu start=\"00:00:00.00\" end=\"$(date -u -d@${chtimefloor} +%T.00)\" image=\"$BUILD/tmp.empty_pixel.png\"/></stream></subpictures>" >"$BUILD/tmp.empty_s.xml"
log_and_run -o "For title $tnum chapter $chnum, create missing subtitle stream for ${chtimefloor}s of ${mkvnum}: ${name}" \
spumux -m dvd --nomux --nodvdauthor-data "$BUILD/tmp.empty_s.xml" </dev/null >"$f"
fi
if [ -z "${blank["$f"]}" ]; then
ffcmd+=(-i "$f")
blank["$f"]=$((${#blank[*]}+1))
fi
ffmap+=(-map ${blank["$f"]}:s:0)
ffenc+=(-c:s:$((mkvnum-1)) copy)
fi
ffenc+=(-metadata:s:s:$((mkvnum-1)) language=$lng -metadata:s:s:$((mkvnum-1)) title="$mkvnum: $name")
fi; done <<<"$MKV_SUB"
firstch=
if ! [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then
log_and_run "Adapt title $tnum chapter $chnum to final MKV streams" \
"${ffcmd[@]}" "${ffmap[@]}" "${ffenc[@]}" "$BUILD/t.$tnum.ch.$chnum.mkv" </dev/null
fi
unset blank ffcmd ffmap ffenc
rm -f "$BUILD/tmp.empty_s.xml" "$BUILD/tmp.empty_pixel.png"
done < <(awk -F$'\t' -vT=$tnum '$1=="C-" T{bFS=FS; n=$3; FS=":"; $0=$4; print n, 3600*$1+60*$2+$3; FS=bFS}' <<<"$ALL_META")
done
for substid in "${!SUBST[@]}"; do
tch=$substid
while [ -n "${SUBST[$tch]}" ]; do tch=${SUBST[$tch]}; done
[ -n "${USED_A[${substid%.*}]}" ] || USED_A[${substid%.*}]="${USED_A[${tch%.*}]}"
[ -n "${USED_S[${substid%.*}]}" ] || USED_S[${substid%.*}]="${USED_S[${tch%.*}]}"
done
unset firstch INTERLV SRC_LTRB SRC_WH2WH
# assemble source tracks into end-result tracks
if ! [ -f "$BUILD/.titlelist" ]; then
tcategs="$(
for tnum in $TITLE_LIST; do
printf '%d %d %d %d\n' $(islongfilm $tnum) $(isepisode $tnum) $tnum $(ls -1 "$BUILD"/t.$tnum.ch.*.mkv | wc -l)
done \
| grep -v ' 0$'
)"
{
awk '$1==0 {printf("🎥 %d\t%d\n",$3,$3)}' <<<"$tcategs"
awk '$1==1 && $2==0 {L=L " " $3}; END {if(L!="")printf("📹%s\t%s\n",gensub(" ","","g",L),gensub(" ","",1,L))}' <<<"$tcategs"
awk '$1==1 && $2==1 {L=L " " $3}; END {if(L!="")printf("🖼%s\t%s\n",gensub(" ","","g",L),gensub(" ","",1,L))}' <<<"$tcategs"
awk '{L=L " " $3}; END {if(L!="")printf("…🎞🎞🎞…\t%s\n",gensub(" ","",1,L))}' <<<"$tcategs"
{
if [ $(grep '^1 0 ' <<<"$tcategs" | wc -l) -gt 1 ]; then
awk '$1==1 && $2==0 {printf("🎞 %d\t%d\n",$3,$3)}' <<<"$tcategs"
fi
if [ $(grep '^1 1 ' <<<"$tcategs" | wc -l) -gt 1 ]; then
awk '$1==1 && $2==1 {printf("🎞 %d\t%d\n",$3,$3)}' <<<"$tcategs"
fi
} | sort -t$'\t' -k2,2n
} >"$BUILD/.titlelist"
unset tcategs
fi
# fetch chapter metadata
for mkv in "$BUILD/"t.*.ch.*.mkv; do
if ! [ -f "${mkv%mkv}json" ]; then
log_and_run "Read metadata (length, ids…) for $mkv" \
mkvmerge -F json -i "$mkv" -r "${mkv%mkv}json"
fi
done
# compute chapters timestamps, UIDs, and substitutions + prepare merging
declare -A TIMESTAMPS
declare -A CHAPTERS_IDS
declare -A UIDS
declare -a MKVMRGARGS=()
default=
strnum=0
while read -r x x x x x x x lng x cmt blind x; do if [ -n "$lng" ]; then
strnum=$((strnum+1))
if [ "$default$cmt$blind${lng,,}" == "falsefalse${DEFAULT_LANG,,}" ]; then
default=found
MKVMRGARGS+=(--default-track-flag $strnum:1)
else
MKVMRGARGS+=(--default-track-flag $strnum:0)
fi
fi; done <<<"$MKV_AUDIO"
while read -r x x lng cmt deaf x; do if [ -n "$lng" ]; then
strnum=$((strnum+1))
if [ "$default$cmt$blind" == "falsefalse" ] && grep -qiF ",${lng,,}," <<<",$ALL_LANGS,"; then
default=found
MKVMRGARGS+=(--default-track-flag $strnum:1)
else
MKVMRGARGS+=(--default-track-flag $strnum:0)
fi
fi; done <<<"$MKV_SUB"
unset default strnum
ref=0
for strid in "${STREAMS_LIST[@]}"; do
STREAMS_TO_OFFSET[$strid]=$ref
file="${STREAMS_TO_FILES[$strid]##*|}"
tnum=$(sed -r 's#.*/t.([0-9]+).ch.[0-9]+.mkv#\1#' <<<"$file")
streamcount=$(jq -r '.tracks | length' <"${file%mkv}json")
for ((i=0; i<streamcount; i++)); do
MKVMRGARGS+=('--sync' $i:$ref --default-track-flag)
if [ $i == "${ADEFAULT[$tnum]}" ] || [ $i == "${SDEFAULT[$tnum]}" ]; then
MKVMRGARGS+=($i:1)
else
MKVMRGARGS+=($i:0)
fi
done
MKVMRGARGS+=('--no-chapters' '--no-global-tags' '[')
IFS='|' read -r -a files <<<"${STREAMS_TO_FILES[$strid]#|}"
MKVMRGARGS+=("${files[@]}")
MKVMRGARGS+=(']')
for file in "${files[@]}"; do
ref=$(bc -lq <<<"$ref+$(jq -r '.container.properties | .duration/.timestamp_scale' <"${file%mkv}json")+1")
done
MKVMRGARGS+=('--no-chapters' '--no-global-tags' '[')
while read -r tnum; do
for chnum in ${CHAPTERS[$tnum]}; do if [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then
[ -z "${SUBST[$tnum.$chnum]}" ] || continue
MKVMRGARGS+=("$BUILD/t.$tnum.ch.$chnum.mkv")
# compute milliseconds
start=$ref
stop=$(bc -lq <<<"$ref+$(jq -r '.container.properties | .duration/.timestamp_scale' <"$BUILD/t.$tnum.ch.$chnum.json")")
ref=$stop
TIMESTAMPS[$tnum.$chnum]="$start $stop"
CHAPTERS_IDS[$tnum.$chnum]="$tnum$chnum$(jq -r .container.properties.segment_uid <"$BUILD/t.$tnum.ch.$chnum.json" \
| tr '[a-f]' '[A-F]' | bc -q <<<"ibase=16;$(cat)" | head -c 8)"
UIDS[$tnum.$chnum]="$(jq -r .container.properties.segment_uid <"$BUILD/t.$tnum.ch.$chnum.json")"
fi; done
done < <(cut -d$'\t' -f2 <"$BUILD/.titlelist" | tr ' ' '\n' | awk '{if($0 in seen)next};{print;seen[$0]=1}')
for substid in "${!SUBST[@]}"; do
tch=$substid
while [ -n "${SUBST[$tch]}" ]; do tch=${SUBST[$tch]}; done
TIMESTAMPS[$substid]=${TIMESTAMPS[$tch]}
CHAPTERS_IDS[$substid]="${substid//.}$(jq -r .container.properties.segment_uid <"$BUILD/t.${tch//./.ch.}.json" \
| tr '[a-f]' '[A-F]' | bc -q <<<"ibase=16;$(cat)" | head -c 8)"
UIDS[$substid]="${UIDS[$tch]}"
done
MKVMRGARGS+=(']')
unset ref
# generate MKV editions XML file
@ -723,7 +638,7 @@ function formatTimestamp() {
fi
}
if [ ! -f "$BUILD/mkv-editions.xml" ]; then
if ! [ -f "$BUILD/mkv-editions.xml" ]; then
{
cat <<ENDOFXML
<?xml version="1.0" encoding="UTF-8"?>
@ -733,9 +648,7 @@ ENDOFXML
edflagdefault=1
titlenum=1
while read -r titlelist; do
strid="${STREAMS_IDS[${titlelist%% *}]}"
offset=${STREAMS_TO_OFFSET[$strid]}
while IFS=$'\t' read -r label titlelist; do
cat <<ENDOFXML
<EditionEntry>
@ -743,24 +656,24 @@ ENDOFXML
<EditionFlagDefault>$edflagdefault</EditionFlagDefault>
<EditionFlagOrdered>1</EditionFlagOrdered>
<EditionDisplay>
<EditionString>$(printf '%03d' $titlenum): $titlelist ($(tr '|+' ' ' <<<"$strid"))</EditionString>
<EditionString>$(printf '%03d%s' $titlenum "$label")</EditionString>
</EditionDisplay>
ENDOFXML
chflaghidden=0
for tnum in $titlelist; do
for chnum in ${CHAPTERS[$tnum]}; do
for chnum in ${CHAPTERS[$tnum]}; do if [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then
cat <<ENDOFXML
<ChapterAtom>
<ChapterUID>${CHAPTERS_IDS[$tnum.$chnum]}</ChapterUID>
<ChapterTimeStart>$(formatTimestamp $(printf '%s+%s\n' $offset ${TIMESTAMPS[$tnum.$chnum]% *} | bc -q))</ChapterTimeStart>
<ChapterTimeEnd>$(formatTimestamp $(printf '%s+%s\n' $offset ${TIMESTAMPS[$tnum.$chnum]#* } | bc -q))</ChapterTimeEnd>
<ChapterTimeStart>$(formatTimestamp ${TIMESTAMPS[$tnum.$chnum]% *})</ChapterTimeStart>
<ChapterTimeEnd>$(formatTimestamp ${TIMESTAMPS[$tnum.$chnum]#* })</ChapterTimeEnd>
<ChapterFlagHidden>$chflaghidden</ChapterFlagHidden>
<ChapterFlagEnabled>1</ChapterFlagEnabled>
<!-- ChapterSegmentUID format="hex">$ - {UIDS[$tnum.$chnum]} - </ChapterSegmentUID -->
<!-- ChapterSegmentUID format="hex">${UIDS[$tnum.$chnum]}</ChapterSegmentUID -->
<ChapterDisplay>
<ChapterString>$tnum.$chnum${CHNAMES[$tnum.$chnum]}</ChapterString>
<ChapterString>$tnum.$chnum (A${USED_A[$tnum]:-} - S${USED_S[$tnum]:-}) ${CHNAMES[$tnum.$chnum]}</ChapterString>
</ChapterDisplay>
</ChapterAtom>
ENDOFXML
@ -768,7 +681,7 @@ ENDOFXML
if [ "${titlelist/ }" != "$titlelist" ]; then
chflaghidden=1
fi
done
fi; done
chflaghidden=0
done
@ -789,7 +702,7 @@ fi
# merge all chapters of all titles
if [ ! -f "$TARGET" ]; then
if ! [ -f "$TARGET" ]; then
log_and_run 'Build final MKV file' \
mkvmerge --disable-track-statistics-tags --append-mode file --chapters "$BUILD/mkv-editions.xml" -o "$TARGET" "${MKVMRGARGS[@]}"
fi
@ -797,3 +710,24 @@ fi
if [ $? -eq 0 ] && [ -z "$KEEP_ALL" ]; then
rm -rf "$BUILD"
fi
# FIXME: HOW TO DETECT USELESS SUBTITLES? E.G.
# 3|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → Normal
# 5|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → ??
# 6|fra,VOBSUB:bitmap,false,false,false Francais (Wide Screen) [VOBSUB] → Commentary
# ⇓
# (about 4min for full movie for each subtitle track)
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 3 --subtitle-burned=none -i .iso -o sub3.mkv
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 5 --subtitle-burned=none -i .iso -o sub5.mkv
# $ HandBrakeCLI -t 1 -e x265_10bit --encoder-profile main10 -q 1 --vfr -X 128 -Y 96 -a none -s 6 --subtitle-burned=none -i .iso -o sub6.mkv
# (instantaneous)
# $ mkvextract sub3.mkv tracks --raw 1:sub3.sub
# $ mkvextract sub5.mkv tracks --raw 1:sub5.sub
# $ mkvextract sub6.mkv tracks --raw 1:sub6.sub
# $ ls -l sub*
# -rw-r--r-- 1 yves yves 287386078 8 mars 18:54 sub3.mkv
# -rw-r--r-- 1 yves yves 1058252 8 mars 19:35 sub3.sub
# -rw-r--r-- 1 yves yves 286304465 8 mars 19:07 sub5.mkv
# -rw-r--r-- 1 yves yves 2268 8 mars 19:35 sub5.sub
# -rw-r--r-- 1 yves yves 289029155 8 mars 19:12 sub6.mkv
# -rw-r--r-- 1 yves yves 2684952 8 mars 19:35 sub6.sub