use ffmpeg more + workarounds for difficult DVDs
parent
bb1002c8f4
commit
0673926de3
604
backup-disc.sh
604
backup-disc.sh
|
@ -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 # don’t 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
|
||||
|
|
Loading…
Reference in New Issue