use ffmpeg more + workarounds for difficult DVDs

dev
Yves G. 2024-04-25 16:56:14 +02:00
parent bb1002c8f4
commit 0673926de3
1 changed files with 374 additions and 230 deletions

View File

@ -6,8 +6,9 @@
# 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
# — ffmpeg, ffprobe (part of FFMpeg) : extract single frames and rip to MKV
# — lsdvd : get DVD metadata
# — mplayer : rip raw DVD streams in case ffmpeg is unable to rip
# — ffmpeg, ffprobe (part of FFMpeg) : probe, rip and encode to HEVC/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
@ -21,7 +22,7 @@
# Options:
# [-h : show this help ]
# [-? : display metadata and exit (implies: -n -v quiet) ]
# [-l lang1,lang2,… : all languages to retain for audio and subtitles, with preferred first; default: fra,eng]
# [-l lang1,lang2,… : all languages to retain for audio and subtitles, with preferred first; default: fre,eng]
# [-v debug|info|quiet : verbosity; default: info ]
# [-t temp_path : location with at least 2×disc size, to store temporary files; default: /tmp/bdvdrip-xxx]
# [-i input_path : path of the disc device or ISO; default: /dev/sr0 ]
@ -56,10 +57,10 @@ while [ $# -gt 0 ]; do
shift
done
ALL_LANGS=${ALL_LANGS:-fra,eng}
ALL_LANGS=${ALL_LANGS:-fre,eng}
DEFAULT_LANG=${ALL_LANGS%%,*}
KEEP_ALL=${BUILD:+true}
BUILD="${BUILD:-$(mktemp -d /tmp/bdvdrip-XXXXXX)}"
BUILD="${BUILD:-$(mktemp --tmpdir -d bdvdrip-XXXXXX)}"
[ -d "$BUILD" ] && [ -w "$BUILD" ] || { echo "$BUILD is not a writeable directory"; exit 1; }
DVD="${DVD:-/dev/sr0}"
[ -r "$DVD" ] && ! [ -d "$DVD" ] || [ -e "$BUILD/.iso" ] || { echo "$DVD is not a useable readable input"; exit 1; }
@ -70,7 +71,7 @@ if ! [[ "$MAXH" =~ ^[0-9]+$ ]]; then MAXH=720; fi
exec 3>&2
function log_and_run() {
local getOutput output
local getOutput output O E excode
if [ "$1" == '-o' ]; then
getOutput=1; shift
fi
@ -79,14 +80,15 @@ 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) ;; #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
1.debug) "$@" 1> >(tee >(cat >&3)) 2> >(tee >(cat >&3) >&2) ;;
1.*) O="$(mktemp --tmpdir XXX.out)"; E="$(mktemp --tmpdir XXX.err)"; "$@" 1>"$O" 2>"$E"; excode=$?; output="$(cat "$O" "$E")"; cat "$O"; cat "$E" >&2; rm -f "$O" "$E" ;;
.debug) "$@" 1>&3 2>&3 ;;
.*) output="$("$@" 2>&1)" ;;
esac
excode=$?
excode=${excode:-$?}
if [ $excode -ne 0 ] && [ "$DEBUG" != quiet ]; then
printf '%s>>> ERROR: EXIT CODE %s\n\n' "${output:+"$output"$'\n'}" "$excode" >&3
[ -n "$output" ] && echo "$output"
printf '>>> ERROR: EXIT CODE %s\n\n' "$excode" >&3
fi
return $excode
}
@ -110,29 +112,118 @@ if ! [ -f "$BUILD/.iso" ]; then
fi
fi
# fetch metadata
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"
meta='{}'
for tnum in $(
log_and_run -o 'Read disc track list' \
lsdvd -Ox "$BUILD/.iso" | grep -o '<ix>[^<]</ix>' | tr -dc '0-9\n'
); do
tmeta='{}'
# general
data="$(
log_and_run -o "Read track $tnum general metadata" \
ffprobe -hide_banner -output_format json -show_format -f dvdvideo -trim false -title $tnum "$BUILD/.iso" 2>/dev/null \
| jq -r "select(.format?.duration? | tonumber? > 5)"
)"
[ -n "$data" ] && x="$(jq --argjson D "$data" '.format = $D.format' <<<"$tmeta")" && tmeta="$x" || continue
# video
data="$(
log_and_run -o "Read track $tnum video metadata" \
ffprobe -hide_banner -output_format json -show_streams -select_streams V -f dvdvideo -trim false -title $tnum "$BUILD/.iso" 2>/dev/null | jq
)"
[[ "$data" =~ [[:blank:]]\{ ]] && x="$(jq --argjson D "$data" '.video = $D.streams[0]' <<<"$tmeta")" && tmeta="$x" || continue
# chapters
# ⚠ With `-preindex 1`: chapter list may be truncated (end is missing)
# without `-preindex 1`: chapter list may be empty (all is missing)
# ⚠ Chapters <1s do not get merged correctly in the end.
# ⚠ Single-chapter titles may appear as having none.
chapters="$(
log_and_run -o "Merge two versions of track $tnum chapters metadata" \
jq -sr '{chapters: [[.[] | if (.chapters|length == 0) then {} else [.chapters[] | {"\(.id)": .}] | add end] | add | to_entries[] | select((.value.end_time|tonumber)-(.value.start_time|tonumber)>1) | .value]}' \
<(log_and_run -o "Read track $tnum chapters metadata (with \`-preindex 1\`)" \
ffprobe -hide_banner -output_format json -show_chapters -f dvdvideo -preindex 1 -trim false -title $tnum "$BUILD/.iso" 2>/dev/null) \
<(log_and_run -o "Read track $tnum chapters metadata (without \`-preindex 1\`)" \
ffprobe -hide_banner -output_format json -show_chapters -f dvdvideo -trim false -title $tnum "$BUILD/.iso" 2>/dev/null)
)"
[[ "$chapters" =~ \"chapters\":\ (null|\[\]) ]] && chapters="$(
jq --argjson T "$tmeta" '{chapters: [{id: 0, start_time: "0.000000", end_time: $T.format.duration}]}' <<<"{}"
)"
while read index chnum startpos; do
if ! [ -f "$BUILD/.t.$tnum.ch.$chnum.sample.mpeg" ]; then
log_and_run "Extract a sample of title $tnum chapter $chnum" \
mplayer dvd://$tnum -chapter $chnum-$chnum -endpos $(bc -lq <<<"6+$startpos") -dumpstream -dumpfile "$BUILD/.t.$tnum.ch.$chnum.sample.mpeg" -dvd-device "$BUILD/.iso" </dev/null
fi
for t in a:audio s:subtitles; do
if ffprobe -hide_banner -output_format json -show_streams -select_streams ${t%:*} "$BUILD/.t.$tnum.ch.$chnum.sample.mpeg" \
</dev/null 2>/dev/null | jq | grep -qF '[]'; then
data="$(
ffprobe -hide_banner -output_format json -show_streams -select_streams ${t%:*} -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum "$BUILD/.iso" 2>/dev/null \
| jq -r 'if (.streams? == null) then [] else [.streams[] | select(.codec_type != "audio" or .bit_rate != null)] end'
)"
else
ref="$(
ffprobe -hide_banner -output_format json -show_streams -select_streams ${t%:*} -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum "$BUILD/.iso" </dev/null 2>/dev/null \
| jq -r '[.streams[] | {key: .id, value: {idx: .index, lng: (if (.tags?.language == null) then "und" else .tags.language end)}}] | from_entries'
)"
data="$(
log_and_run -o "Probe track $tnum chapter $chnum ${t#*:} metadata from sample" \
ffprobe -hide_banner -output_format json -show_streams -select_streams ${t%:*} "$BUILD/.t.$tnum.ch.$chnum.sample.mpeg" </dev/null 2>/dev/null \
| jq -r --argjson R "$ref" "\
if (.streams? == null) then [] else [ \
.streams[] | \$R.[\"\\(.id)\"] as \$ref \
| select(\$ref != null and (.codec_type != \"audio\" or .bit_rate != null)) \
| .index = \$ref.idx \
| .tags = {language: \$ref.lng} \
] end"
)"
fi
x="$(jq --argjson D "$data" ".chapters[$index].${t#*:} = \$D" <<<"$chapters")" && chapters="$x"
done
done < <(jq -r '.chapters | to_entries[] | "\(.key) \(.value.id+1) \(.value.start_time)"' <<<"$chapters")
x="$(jq --argjson C "$chapters" '.chapters = $C.chapters' <<<"$tmeta")" && tmeta="$x"
x="$(jq --arg N $tnum --argjson T "$tmeta" '.["\($N)"] = $T' <<<"$meta")" && meta="$x"
done
# filter-out titles with no audio
jq -r '[to_entries[] | select(([.value.chapters[].audio[]] | length) > 0)] | from_entries' <<<"$meta" >"$BUILD/.json"
# detect crop
list="$(jq -r 'to_entries[] | "\(.key) \((.value.format.duration / ".")[0])"' <"$BUILD/.json")"
while read tnum vseconds; do
case $vseconds in
?|??) start=0; duration=$vseconds ;;
*) start=$(($vseconds/3)); duration=60 ;;
esac
x="$(jq ".\"$tnum\".video.crop = \"$(
log_and_run -o "Detect track $tnum picture borders" \
ffmpeg -hide_banner -f dvdvideo -preindex 1 -trim false -title $tnum -i "$BUILD/.iso" \
-ss $start -t $duration -vf cropdetect=limit=24:round=2:reset=0:skip=0,metadata=mode=print -f null - </dev/null 2>&1 \
| grep -o 'crop=[0-9:]*' | sort | uniq -c | sort -k1,1nr | head -n 1 | sed 's/.*=//'
)\"" <"$BUILD/.json")" \
&& printf '%s' "$x" >"$BUILD/.json"
done <<<"$list"
fi
ALL_META="$(
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)\", \
\"V-\(\$tnum)\t\t0\t\(.VideoCodec)\t\(.Geometry.Width)x\(.Geometry.Height)\t\(.FrameRate.Num/.FrameRate.Den)\t\(.Color.BitDepth)\t\(.Color.ChromaSubsampling)\t\(.Geometry.PAR.Num)/\(.Geometry.PAR.Den)\t\(.Crop[0]):\(.Crop[1]):\(.Crop[2]):\(.Crop[3])\t\(.InterlaceDetected)\", \
\"\tAudio (index, language, codec, channels, sample-rate, bit/s, is default, is commentary, is secondary, for visually impaired, description)\", \
(.AudioList[] | \
\"A-\(\$tnum)\t\t\(.TrackNumber)\t\(.LanguageCode)\t\(.CodecName)\t\(.ChannelCount)\t\(.SampleRate)\t\(.BitRate)\t\(.Attributes.Default)\t\(.Attributes.Commentary or .Attributes.AltCommentary)\t\(.Attributes.Secondary or .Attributes.AltCommentary)\t\(.Attributes.VisuallyImpaired)\t\(.Description)\"), \
\"\tSubtitles (index, language, codec, is default, is commentary, is forced, for hearing impaired, description)\", \
(.SubtitleList[] | \
\"S-\(\$tnum)\t\t\(.TrackNumber)\t\(.LanguageCode)\t\(.SourceName):\(.Format)\t\(.Attributes.Default)\t\(.Attributes.Commentary)\t\(.Attributes.Forced)\t\(.Attributes.ClosedCaption)\t\(.Language)\"), \
\"\tChapters (index, duration, name)\", \
(.ChapterList | to_entries[] | \
\"C-\(\$tnum)\t\t\(.key+1)\t\(.value.Duration.Hours):\(.value.Duration.Minutes):\(.value.Duration.Seconds)\t\(.value.Name)\")" \
jq -r "to_entries[] | .key as \$tnum | .value | \
\"TITLE \(\$tnum) (\(.format.duration)s)\", \
\"\tVideo (index, codec, geometry, frame-rate, chroma-subsampling, pixel aspect ratio, crop width:height:left:top, is interlaced, id)\", \
(.video | \
\"V-\(\$tnum)\t\t\(.index)\t\(.codec_name)\t\(.width)x\(.height)\t\(.avg_frame_rate)\t\(.pix_fmt)\t\(.sample_aspect_ratio | sub(\":\"; \"/\"))\t\(.crop)\t\(\",\(.field_order),\" | test(\",(bt|tb|tt),\"))\t\(.id)\"), \
\"\tChapters (index, begin-end timestamps, name)\", \
(.chapters[] | (.id+1) as \$chnum | ( \
\"C-\(\$tnum)\t\(\$chnum)\t\t\(.start_time)s-\(.end_time)s\tCH.\(\$chnum)\", \
(select (.audio|length > 0) | \"\t Audio (index, language, codec, channels, layout, sample-rate, bit/s, is default, is commentary, is secondary, for visually impaired, id, description)\"), \
(.audio[] | \
\"A-\(\$tnum)\t\(\$chnum)\t\(.index)\t\(if (.tags?.language == null) then \"und\" else .tags.language end)\t\(.codec_name)\t\(.channels)\t\(.channel_layout)\t\(.sample_rate)\t\(.bit_rate)\t\(.disposition.default != 0)\t\(.disposition.comment != 0 or .disposition.descriptions != 0)\tfalse\t\(.disposition.visual_impaired != 0)\t\(.id)\t\(if (.tags?.language? != null) then \"\(.tags.language | ascii_upcase) \" else \"UND \" end)\(.channel_layout)\"), \
(select (.subtitles|length > 0) | \"\t Subtitles (index, language, codec, is default, is commentary, is forced, for hearing impaired, id, description)\"), \
(.subtitles[] | \
\"S-\(\$tnum)\t\(\$chnum)\t\(.index)\t\(if (.tags?.language == null) then \"und\" else .tags.language end)\t\(.codec_name)\t\(.disposition.default != 0)\t\(.disposition.comment != 0 or .disposition.descriptions != 0)\t\(.disposition.forced != 0)\t\(.disposition.hearing_impaired != 0 or .disposition.captions != 0)\t\(.id)\t\(if (.tags?.language? != null) then .tags.language | ascii_upcase else \"UND\" end)\"), \
\"-\" \
))" \
<"$BUILD/.json"
)"
@ -148,19 +239,22 @@ fi
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 SRC_VIDEO
declare -A VSECONDS
declare -A CHAPTERS
declare -A CSECONDS
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")
while read -r tnum vnum; do SRC_VIDEO[$tnum]="$vnum"; done < <(
sed -nr 's/^V-([0-9]+)\t\t([0-9]+)\t.*/\1 \2/p' <<<"$ALL_META")
while read -r tnum s; do VSECONDS[$tnum]=$s; done < <(
sed -nr 's/^TITLE (.*) \(([0-9]+)(\..*)?s\)/\1 \2/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
sed -rn "s/^C-$tnum\\t([^\\t]*)\\t.*/\\1/p" <<<"$ALL_META"); done
while read -r tnum chnum from to; do CSECONDS[$tnum.$chnum]=$(bc -lq <<<"$to-$from"); done < <(
sed -nr 's/^C-([0-9]+)\t([0-9]+)\t\t([0-9.]*)s-([0-9.]*)s\t.*/\1 \2 \3 \4/p' <<<"$ALL_META")
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")
sed -nr 's/^C-([0-9]+)\t([0-9]+)\t\t[-0-9.s]*\t(.*)/\1 \2 \3/p' <<<"$ALL_META")
# compute final geometry
@ -177,16 +271,14 @@ 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
declare -A SRC_RAW SRC_WHLT SRC_WH2WH CATEG_TIME
# → 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 $TITLE_LIST; do
read -r raww rawh parn pard cropt cropb cropl cropr < <(sed -nr "
read -r raww rawh parn pard cropw croph cropl cropt interl id < <(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
s#(\t[^\t]*){2}\t([0-9]+)/([0-9]+)\t([0-9]+):([0-9]+):([0-9]+):([0-9]+)\t([^\t]*)\t([^\t]*)# \2 \3 \4 \5 \6 \7 \8 \9#p
" <<<"$ALL_META")
cropw=$((raww-cropl-cropr))
croph=$((rawh-cropt-cropb))
if [ $parn -gt $pard ]; then # stretch horizontally
read -r srch srcw finalh finalw scalen scaled < <(scale_round $croph $cropw $parn $pard 1 1)
if [ $finalw -gt $MAXW ] || [ $finalh -gt $MAXH ]; then
@ -208,21 +300,21 @@ for tnum in $TITLE_LIST; do
fi
[ $srcw -le $raww ] || srcw=$raww
[ $srch -le $rawh ] || srch=$rawh
SRC_LTRB[$tnum]="$cropl $cropt $cropr $cropb"
SRC_WHLT[$tnum]="$cropw $croph $cropl $cropt"
SRC_WH2WH[$tnum]="$srcw $srch $finalw $finalh $scalen $scaled"
SRC_RAW[$tnum]="$raww $rawh $parn $pard"
DAR_COUNT["$finalw $finalh"]=$(awk -vN=${DAR_COUNT["$finalw $finalh"]:-0} "/C-$tnum\t/{N++};END{print N}" <<<"$ALL_META")
SRC_RAW[$tnum]="$raww $rawh $parn $pard $interl $id"
CATEG_TIME["$finalw $finalh"]=$(bc -q <<<"${CATEG_TIME["$finalw $finalh"]:-0}+${VSECONDS[$tnum]}")
done
# → identify the width and height most often used in all chapters
read -r maxw maxh x < <(
for wh in "${!DAR_COUNT[@]}"; do printf '%s %d\n' "$wh" ${DAR_COUNT[$wh]}; done | sort -k3,3nr -k1,1nr -k2,2nr | head -n 1)
unset DAR_COUNT
for wh in "${!CATEG_TIME[@]}"; do printf '%s %d\n' "$wh" ${CATEG_TIME[$wh]}; done | sort -k3,3nr -k1,1nr -k2,2nr | head -n 1)
unset CATEG_TIME
# → rescale each title according to the chosen width and height
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]}"
read -r rw rh parn pard x <<<"${SRC_RAW[$tnum]}"
# already OK ⇒ skip
if { [ $fw -eq $maxw ] || [ $fh -eq $maxh ]; } && [ $fw -le $maxw ] && [ $fh -le $maxh ]; then
continue
@ -241,77 +333,63 @@ for tnum in $TITLE_LIST; do
fi
SRC_WH2WH[$tnum]="$newsrcw $newsrch $neww $newh $newscalen $newscaled"
done
unset SRC_RAW
# compute final audio streams
declare -A SRC_AUDIO CATEG_COUNT
declare -A SRC_AUDIO CATEG_TIME
# → categorize all audio streams
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>
SRC_AUDIO[$tnum]="$(
for tch in "${!CSECONDS[@]}"; do
allaudio="$(awk -F$'\t' -vT=${tch%.*} -vC=${tch#*.} '$1=="A-" T && $2==C{print T "." C, $3, $14, $5, $8, $9, $12, $4, $6, $7, $11, $13}' <<<"$ALL_META")"
# (technical info:) <1:title.chapter number> <2:stream number> <3:id> <4:codec> <5:Hz> <6:bit/s> <7:secondary?>
# (category:) <8:lang> <9:channels count> <10:layout> <11:commentary?> <12:visu.impaired?> <13:priority>
SRC_AUDIO[$tch]="$(
for lng in ${ALL_LANGS//,/ }; do
awk -vL=$lng '$7==L{print}' <<<"$allaudio" \
| sort -k9,9 -k10,10 -k6,6 -k8,8nr -k2,2n \
| awk '($8 $9 $10)!=ref{ref=($8 $9 $10);c=0};{c++; print $0, c}'
awk -vL=$lng '$8==L{print}' <<<"$allaudio" \
| sort -k11,11 -k12,12 -k7,7 -k9,9nr -k10,10 -k2,2n \
| awk '($9 $10 $11 $12)!=ref{ref=($9 $10 $11 $12);c=0};{c++; print $0, c}'
done
)"
while read -r x x cod freq bps x lng cnt cmt blind prio; do
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]}"
while read -r x x x cod freq bps x lng cnt lay cmt blind prio; do
x="$cod $freq $bps $lng $cnt $lay $cmt $blind $prio"
CATEG_TIME["$x"]=$(bc -q <<<"${CATEG_TIME["$x"]:-0}+${CSECONDS[$tch]%.*}")
done <<<"${SRC_AUDIO[$tch]}"
done
# → identify for each category the audio settings most often used in all chapters, with preferred language on top
declare -A expo=(["1"]='' ["2"]=² ["3"]=³ ["4"]=["5"]=["6"]=["7"]=["8"]=["9"]=)
allaudio="$(export IFS=$'\n'; echo "${SRC_AUDIO[*]}")"
# (technical info:) <1:stream number> <2:MKV codec> <3:Hz> <4:bit/s>
# (category:) <5:lang> <6:channels count> <7:commentary?> <8:visu.impaired?> <9:priority> <10:name>
# (technical info:) <1:stream number> <2:codec> <3:Hz> <4:bit/s>
# (category:) <5:lang> <6:channels count> <7:layout> <8:commentary?> <9:visu.impaired?> <10:priority> <11:name>
MKV_AUDIO="$(
for lng in ${ALL_LANGS//,/ }; do
while read -r x x cod freq bps x lng cnt cmt blind prio; do
x="$cod $freq $bps $lng $cnt $cmt $blind $prio"
name="${lng^^}${expo[$prio]} ${cnt}🕩"
[ "$cmt" == true ] && name+=" 🗩"
while read -r x x x cod freq bps x lng cnt lay cmt blind prio; do
x="$cod $freq $bps $lng $cnt $lay $cmt $blind $prio"
name="${lng^^} ${cnt}🕩 $lay"
[ "$blind" == true ] && 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 \
[ "$cmt" == true ] && name+=" 🗩"
echo "${CATEG_TIME["$x"]} $cod $freq $bps $lng $cnt $lay $cmt $blind $prio $name${expo[$prio]}"
done < <(awk -vL=$lng '$8==L{print}' <<<"$allaudio") \
| sort -k8,8 -k9,9 -k6,6nr -k7,7 -k10,10n -k1,1nr \
| uniq -f5
done \
| nl -nln -s' ' -w1 | cut -d' ' -f1,3-
)"
unset expo allaudio
unset CATEG_COUNT
# (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"
)"
unset CATEG_TIME
# compute final subtitle streams
declare -A SRC_SUB
# → categorize all subtitle streams
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>
SRC_SUB[$tnum]="$(
for tch in "${!CSECONDS[@]}"; do
allsubs="$(awk -F$'\t' -vT=${tch%.*} -vC=${tch#*.} '$1=="S-" T && $2==C{print $3, $10, $8, $5, $4, $7, $9}' <<<"$ALL_META")"
# (technical info:) <1:stream number> <2:id> <3:forced?>
# (category:) <4:codec> <5:lang> <6:commentary?> <7:hear.impaired?> <8:priority>
SRC_SUB[$tch]="$(
for lng in ${ALL_LANGS//,/ }; do
awk -vL=$lng '$4==L{print}' <<<"$allsubs" | sort -k5,5 -k6,6 -k2,2r -k3,3 -k1,1n | awk '($3 $5 $6)!=ref{ref=($3 $5 $6);c=0};{c++; print $0, c}'
awk -vL=$lng '$5==L{print}' <<<"$allsubs" | sort -k6,6 -k7,7 -k3,3r -k4,4 -k1,1n | awk '($4 $6 $7)!=ref{ref=($4 $6 $7);c=0};{c++; print $0, c}'
done
)"
done
@ -320,15 +398,15 @@ done
declare -A expo=(["1"]='' ["2"]=² ["3"]=³ ["4"]=["5"]=["6"]=["7"]=["8"]=["9"]=)
allsubs="$(export IFS=$'\n'; echo "${SRC_SUB[*]}")"
# (technical info:) <1:stream number>
# (category:) <2:MKV codec> <3:lang> <4:commentary?> <5:hear.impaired?> <6:priority> <7:name>
# (category:) <2:codec> <3:lang> <4:commentary?> <5:hear.impaired?> <6:priority> <7:name>
MKV_SUB="$(
for lng in ${ALL_LANGS//,/ }; do
while read -r x x cod lng cmt deaf prio; do
name="${lng^^}${expo[$prio]}"
[ "$cmt" == true ] && name+=" 🗩"
while read -r x x x cod lng cmt deaf prio; do
name="${lng^^}"
[ "$deaf" == true ] && name+=" 🙉"
echo "$cod $lng $cmt $deaf $prio $name"
done < <(awk -vL=$lng '$4==L{print}' <<<"$allsubs") \
[ "$cmt" == true ] && name+=" 🗩"
echo "$cod $lng $cmt $deaf $prio $name${expo[$prio]}"
done < <(awk -vL=$lng '$5==L{print}' <<<"$allsubs") \
| sort -k3,3 -k4,4 -k1,1 -k5,5n \
| uniq
done \
@ -338,41 +416,28 @@ unset expo
# generate per-chapter timeline snapshots
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")
audio=( $(awk -vOFS=, '{print $7, $8, $9, $10, $11}' <<<"${SRC_AUDIO[$tnum]}") )
subs=( $(awk -vOFS=, '{print $3, $4, $5, $6, $7}' <<<"${SRC_SUB[$tnum]}") )
montagecmd=(montage -background black)
snapsec=0
snapcount=0
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 -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))
montagecmd+=("$target")
done
cellsize=$(identify "$BUILD/.tsnap/t.$tnum.ch.$chnum.s.5.png" </dev/null | sed -nr 's/.* ([0-9]+x[0-9]+(\+[0-9]+){2}).*/\1/p')
gridsize=1
while [ $((gridsize*gridsize)) -lt $snapcount ]; do gridsize=$((gridsize+1)); done
[ -d "$BUILD/.tsnap" ] || mkdir "$BUILD/.tsnap"
while read -r tnum chnum seconds; do
if ! [ -f "$BUILD/t.$tnum.ch.$chnum.png" ]; then
audio=( $(awk -vOFS=, '{print $8, $9, $10, $11, $12, $13}' <<<"${SRC_AUDIO[$tnum.$chnum]}") )
subs=( $(awk -vOFS=, '{print $4, $5, $6, $7, $8}' <<<"${SRC_SUB[$tnum.$chnum]}") )
gridsize=$(($(bc -lq <<<"sqrt($seconds/5)" | sed 's/\.0*$//;s/\..*/+1/')))
log_and_run "Make timeline snapshot of track $tnum chapter $chnum" \
"${montagecmd[@]}" -tile ${gridsize}x${gridsize} -geometry $cellsize -trim "$BUILD/.tsnap/t.$tnum.ch.$chnum.png" </dev/null
ffmpeg -hide_banner -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum -i "$BUILD/.iso" \
-map 0:v:0 -frames:v 1 -filter:v \
"select='bitor(gte(t-prev_selected_t,5),isnan(prev_selected_t))',scale=w=256:h=144:force_original_aspect_ratio=decrease,tile=${gridsize}x${gridsize}" \
-fps_mode passthrough "$BUILD/.tsnap/t.$tnum.ch.$chnum.png" </dev/null
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/t.$tnum.ch.$chnum.png" </dev/null
unset audio subs
done < <(
# restrict to chapters having common durations
jq -r '.TitleList[] | .Index as $tnum | .ChapterList | to_entries | .[] | .key as $chnum | .value.Duration | "\($tnum) \(1+$chnum) \(.Hours).\(.Minutes).\(.Seconds).\(.Ticks)"' <"$BUILD/.json" \
| sort -k3,3V | uniq -f2 -D # keep only duplicates in the duration column
)
fi
fi
done < <(
# restrict to chapters having common durations
for tch in ${!CSECONDS[@]}; do printf '%s %s\n' "${tch/./ }" ${CSECONDS[$tch]}; done \
| sort -k3,3nr | uniq -f2 -D # keep only duplicates in the duration column
)
# detect duplicates
@ -387,17 +452,16 @@ function isepisode() {
}
declare -A STREAMS_IDS
declare -A SUBST
for tnum in $TITLE_LIST; do
for tch in "${!CSECONDS[@]}"; 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
while read -r x x x x x x x lng x lay cmt blind prio; do
strid+=",${lng}${prio}:${lay}(${cmt}/${blind})"
done <<<"${SRC_AUDIO[$tch]}"
while read -r x x x cod lng cmt deaf prio; do
strid+=",${lng}${prio}:${cod}(${cmt}/${deaf})"
done <<<"${SRC_SUB[$tnum]}"
STREAMS_IDS[$tnum]="$strid"
done <<<"${SRC_SUB[$tch]}"
STREAMS_IDS[$tch]="$strid"
done
if ! [ -f "$BUILD/.duplicates" ]; then
@ -405,7 +469,7 @@ if ! [ -f "$BUILD/.duplicates" ]; then
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' \
| while read -r tnum1 chnum1; read -r tnum2 chnum2; do
if [ "${STREAMS_IDS[$tnum1]}" == "${STREAMS_IDS[$tnum2]}" ]; then
if [ "${STREAMS_IDS[$tnum1.$chnum1]}" == "${STREAMS_IDS[$tnum2.$chnum2]}" ]; then
# long film first, else episode first, else lower title number first, else lower chapter number first
printf '%d %d %d %d %d %d\n%d %d %d %d %d %d\n' \
$(islongfilm $tnum1) $(isepisode $tnum1) $tnum1 $tnum2 $chnum1 $chnum2 \
@ -414,6 +478,9 @@ if ! [ -f "$BUILD/.duplicates" ]; then
fi
done >"$BUILD/.duplicates"
fi
unset STREAMS_IDS
declare -A SUBST
while read -r t1 ch1 t2 ch2; do
SUBST[$t2.$ch2]="$t1.$ch1"
@ -421,121 +488,193 @@ done <"$BUILD/.duplicates"
# rip chapters and fill-in missing streams
function effectivePlayTime() {
ffmpeg -hide_banner "$@" -f null /dev/null </dev/null \
|& grep -o 'time=[^ ]*' | tail -n 1 | tr '=.' : | awk -F: '{printf("%d.%s",3600*$2+60*$3+$4,$5)}'
}
function within1s() {
[ -z "$(bc -q <<<"(${1:-0}-${2:-0})^2" | cut -d. -f1)" ]
}
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=()
# $1,2,3: tnum, chnum, chtime
# $5, $6: (optional) alt. raw input, id-to-alt. stream number mapping ('id num\nid num\n…')
function ripchapter() {
local tnum=$1 chnum=$2 chtime=$3 altinput="$4" altmap="$5"
local -A blank
local -a ffcmd ffinput ffmap ffenc
if [ -n "$altinput" ]; then
ffcmd=(ffmpeg -hide_banner)
ffinput=(-i "$altinput")
else
ffcmd=(ffmpeg -hide_banner -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum)
ffinput=(-i "$BUILD/.iso")
fi
ffmap=(-map_chapters -1)
ffenc=()
# 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=()
# video stream
if [ -n "${SRC_VIDEO[$tnum]}" ]; then
read -r x x x x interl id <<<"${SRC_RAW[$tnum]}"
num=${SRC_VIDEO[$tnum]}
if [ -n "$altinput" ]; then
num=$(awk -vI="$id" '$1==I{print $2}' <<<"$altmap")
if [ -z "$num" ]; then
printf 'ERROR: COULD NOT RETRIEVE VIDEO OF TITLE %d CHAPTER %d\n' $tnum $chnum >&2
exit 1
fi
fi
read -r sw sh fw fh x <<<"${SRC_WH2WH[$tnum]}"
read -r x x cropl cropt <<<"${SRC_WHLT[$tnum]}"
ffmap+=(-map 0:$num)
filter=
if [ "${INTERLV[$tnum]}" == true ]; then
if [ "$interl" == 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"
filter+=",scale=w=$fw:h=$fh:force_original_aspect_ratio=disable"
fi
filter+=',setsar=1/1'
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)
fi
# 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)))
# audio streams
while read -r mkvnum ffcod mkvfreq mkvrate lng count ffchanlay cmt blind prio name; do if [ -n "$mkvnum" ]; then
read -r x num id cod freq rate x < <(
awk -vL=$lng -vC=$count -vY=$ffchanlay -vM=$cmt -vB=$blind -vP=$prio '$8==L && $9==C && $10==Y && $11==M && $12==B && $13==P{print}' <<<"${SRC_AUDIO[$tnum.$chnum]}")
[ -n "$altinput" ] && num=$(awk -vI="$id" '$1==I{print $2}' <<<"$altmap")
if [ -z "$num" ]; then
# no such stream in this title
f="$BUILD/tmp.empty_a.$ffchanlay.$mkvfreq.$mkvrate.$chtime.$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 ${chtime}s of ${mkvnum}: ${name}" \
ffmpeg -hide_banner -strict -2 -lavfi anullsrc=channel_layout="${ffchanlay}":sample_rate=${mkvfreq}:duration=${chtime} -c:a ${ffcod} -b:a ${mkvrate} "$f" </dev/null
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)
if [ -z "${blank["$f"]}" ]; then
ffinput+=(-i "$f")
blank["$f"]=$((${#blank[*]}+1))
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
ffmap+=(-map ${blank["$f"]}:a:0)
ffenc+=(-c:a:$((mkvnum-1)) copy)
elif [ "$cod,$freq,$rate" == "$ffcod,$mkvfreq,$mkvrate" ]; then
# exact match in this title
grep -qF " $mkvnum " <<<"${USED_A[$tnum]} " || USED_A[$tnum]="${USED_A[$tnum]} $mkvnum"
ffmap+=(-map 0:$num)
ffenc+=(-c:a:$((mkvnum-1)) copy -map_metadata:s:a:$((mkvnum-1)) 0:s:$num)
else
# steam has different characteristics
grep -qF " $mkvnum " <<<"${USED_A[$tnum]} " || USED_A[$tnum]="${USED_A[$tnum]} $mkvnum"
ffmap+=(-map 0:$num)
ffenc+=(-c:a:$((mkvnum-1)) $ffcod -ar:a:$((mkvnum-1)) $mkvfreq -b:a:$((mkvnum-1)) $mkvrate -map_metadata:s:a:$((mkvnum-1)) 0:s:$num)
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
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 ffcod lng cmt deaf prio name; do if [ -n "$mkvnum" ]; then
read -r num id x < <(
awk -vC=$ffcod -vL=$lng -vM=$cmt -vD=$deaf -vP=$prio '$4==C && $5==L && $6==M && $7==D && $8==P{print}' <<<"${SRC_SUB[$tnum.$chnum]}")
[ -n "$altinput" ] && num=$(awk -vI="$id" '$1==I{print $2}' <<<"$altmap")
if [ -n "$num" ]; then
# exact match in this title
grep -qF " $mkvnum " <<<"${USED_S[$tnum]} " || USED_S[$tnum]="${USED_S[$tnum]} $mkvnum"
ffmap+=(-map 0:$num)
ffenc+=(-c:s:$((mkvnum-1)) copy -map_metadata:s:s:$((mkvnum-1)) 0:s:$num)
elif [ "$ffcod" != dvd_subtitle ]; then
# unsupported type!
printf 'ERROR: UNSUPPORTED MISSING SUBTITLE STREAM FOR TITLE %d CHAPTER %d OF TYPE %s\n' $tnum $chnum "$ffcod" >&2
exit 1
else
# no such stream in this title
f="$BUILD/tmp.empty_s.${VFORMAT:-pal}.$chtime.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 ${chtime}s of empty subtitle" \
echo "<subpictures format=\"$([ "$VFORMAT" == ntsc ] && echo NTSC || echo PAL)\"><stream><spu start=\"00:00:00.00\" end=\"$(date -u -d@${chtime} +%T.${chtime#*.})\" 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 ${chtime}s of ${mkvnum}: ${name}" \
spumux -m dvd --nomux --nodvdauthor-data "$BUILD/tmp.empty_s.xml" </dev/null >"$f"
fi
if [ -z "${blank["$f"]}" ]; then
ffinput+=(-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"
# encode
if ! [ -f "$BUILD/t.$tnum.ch.$chnum.mkv" ]; then (
if [ -n "$altinput" ]; then
log_and_run "Adapt title $tnum chapter $chnum to final MKV streams (from raw extract)" \
"${ffcmd[@]}" "${ffinput[@]}" "${ffmap[@]}" "${ffenc[@]}" "$BUILD/t.$tnum.ch.$chnum.mkv" </dev/null \
&& exit 0
printf 'ERROR: UNABLE TO RIP AND ENCODE TITLE %d CHAPTER %d\n' $tnum $chnum >&2
exit 1
fi
duration=$(effectivePlayTime -f dvdvideo -preindex 1 -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum -i "$BUILD/.iso")
if within1s $chtime $duration; then
log_and_run "Adapt title $tnum chapter $chnum to final MKV streams" \
"${ffcmd[@]}" -preindex 1 "${ffinput[@]}" "${ffmap[@]}" "${ffenc[@]}" "$BUILD/t.$tnum.ch.$chnum.mkv" </dev/null \
&& exit 0
fi
duration=$(effectivePlayTime -f dvdvideo -trim false -title $tnum -chapter_start $chnum -chapter_end $chnum -i "$BUILD/.iso")
if within1s $chtime $duration; then
log_and_run "Adapt title $tnum chapter $chnum to final MKV streams (degraded)" \
"${ffcmd[@]}" "${ffinput[@]}" "${ffmap[@]}" "${ffenc[@]}" "$BUILD/t.$tnum.ch.$chnum.mkv" </dev/null \
&& exit 0
fi
if ! [ -f "$BUILD/t.$tnum.ch.$chnum.mpeg" ]; then
log_and_run "Extract title $tnum chapter $chnum raw data" \
mplayer dvd://${tnum} -chapter ${chnum}-${chnum} -dumpstream -dumpfile "$BUILD/t.$tnum.ch.$chnum.mpeg" -dvd-device "$BUILD/.iso" </dev/null
fi
duration=$(effectivePlayTime -i "$BUILD/t.$tnum.ch.$chnum.mpeg")
if within1s $chtime $duration || [ $(tail -n 1 <<<"${CHAPTERS[$tnum]}") == $chnum ]; then
altmap="$(ffprobe -hide_banner -output_format json -show_streams -analyzeduration "$(bc -q <<<"$duration*1000000" | cut -d. -f1)" \
"$BUILD/t.$tnum.ch.$chnum.mpeg" 2>/dev/null | jq -r '.streams[] |"\(.id) \(.index)"')"
ripchapter $tnum $chnum $chtime "$BUILD/t.$tnum.ch.$chnum.mpeg" "$altmap" \
&& exit 0
fi
if [ ${duration%.*}0 -lt 600 ]; then # <60s
printf '\nWARNING: Cannot read the exact length of title %d chapter %d (%s over %s); skipping…\n' $tnum $chnum $duration $chtime >&2
exit 0 # dont rip; it is probably a subtitle/menu chapter
fi
printf '\nERROR: CANNOT READ THE EXACT LENGTH OF TITLE %d CHAPTER %d\n' $tnum $chnum >&2
exit 1
); fi \
&& rm -f "$BUILD/tmp.empty_s.xml" "$BUILD/tmp.empty_pixel.png"
}
while read -r tnum chnum chtime; do
[ -z "${SUBST[$tnum.$chnum]}" ] || continue
[ ${chtime:0:1} == . ] && chtime=0$chtime
ripchapter $tnum $chnum $chtime || exit 1
if [ -f "$BUILD/t.$tnum.ch.$chnum.mpeg" ] && [ $(tail -n 1 <<<"${CHAPTERS[$tnum]}") == $chnum ]; then
CSECONDS[$tnum.$chnum]=$(effectivePlayTime -i "$BUILD/t.$tnum.ch.$chnum.mkv")
fi
done < <(for tch in ${!CSECONDS[@]}; do printf '%s %s\n' "${tch/./ }" ${CSECONDS[$tch]}; done | sort -k1,1n -k2,2n)
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
unset SRC_VIDEO SRC_RAW SRC_WHLT SRC_WH2WH SRC_AUDIO SRC_SUB
# assemble source tracks into end-result tracks
@ -563,7 +702,7 @@ if ! [ -f "$BUILD/.titlelist" ]; then
unset tcategs
fi
# fetch chapter metadata
# fetch chapters MKV metadata
for mkv in "$BUILD/"t.*.ch.*.mkv; do
if ! [ -f "${mkv%mkv}json" ]; then
@ -581,7 +720,7 @@ 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
while read -r x x x x lng x x cmt blind x; do if [ -n "$lng" ]; then
strnum=$((strnum+1))
if [ "$default$cmt$blind${lng,,}" == "falsefalse${DEFAULT_LANG,,}" ]; then
default=found
@ -589,6 +728,8 @@ while read -r x x x x x x x lng x cmt blind x; do if [ -n "$lng" ]; then
else
MKVMRGARGS+=(--default-track-flag $strnum:0)
fi
MKVMRGARGS+=(--visual-impaired-flag $strnum:$([ "$blind" == true ] && printf 1 || printf 0))
MKVMRGARGS+=(--commentary-flag $strnum:$([ "$cmt" == true ] && printf 1 || printf 0))
fi; done <<<"$MKV_AUDIO"
while read -r x x lng cmt deaf x; do if [ -n "$lng" ]; then
strnum=$((strnum+1))
@ -598,6 +739,8 @@ while read -r x x lng cmt deaf x; do if [ -n "$lng" ]; then
else
MKVMRGARGS+=(--default-track-flag $strnum:0)
fi
MKVMRGARGS+=(--hearing-impaired-flag $strnum:$([ "$deaf" == true ] && printf 1 || printf 0))
MKVMRGARGS+=(--commentary-flag $strnum:$([ "$cmt" == true ] && printf 1 || printf 0))
fi; done <<<"$MKV_SUB"
unset default strnum
@ -607,9 +750,9 @@ 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
# compute floating-point seconds
start=$ref
stop=$(bc -lq <<<"$ref+$(jq -r '.container.properties | .duration/.timestamp_scale' <"$BUILD/t.$tnum.ch.$chnum.json")")
stop=$(bc -lq <<<"$ref+$(jq -r '.container.properties | .duration/.timestamp_scale/1000' <"$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" \
@ -634,7 +777,7 @@ function formatTimestamp() {
if [ $1 == 0 ]; then
echo '00:00:00.000'
else
date -u -d@${1:0:-3} +%T.${1: -3:3}
date -u -d@${1%.*} +%T.${1#*.}
fi
}
@ -704,7 +847,8 @@ fi
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[@]}"
mkvmerge --attachment-description 'MKV Ordered-Chapters Editions' --attachment-mime-type text/xml --attach-file "$BUILD/mkv-editions.xml" \
--disable-track-statistics-tags --append-mode file --chapters "$BUILD/mkv-editions.xml" -o "$TARGET" "${MKVMRGARGS[@]}"
fi
if [ $? -eq 0 ] && [ -z "$KEEP_ALL" ]; then