433 lines
13 KiB
Bash
Executable file
433 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
progname=$(basename $0)
|
|
|
|
read -d '' _usage <<EOUSAGE
|
|
USAGE
|
|
$0 [OPTION] INPUT_DIRECTORY
|
|
|
|
$progname is a script that utilizes FFmpeg to encode AVC that plays on the PSP's
|
|
built-in hardware-accelerated XMB media player in a batch. If running one
|
|
instance of this script doesn't fully-saturate all of your CPU threads, you can
|
|
safely spawn as many instances of $progname are required to fully-saturate all of
|
|
your CPU's threads. Spawning the instances in separate terminal sessions is
|
|
reccommended for logging and sanity purposes.
|
|
|
|
$progname is safe to run across a cluster of computers that all use a shared
|
|
network-attached filesystem. I recommend SSHFS with '-o Compression=no' for
|
|
maximal security and convenience, but there's nothing stoping you from using
|
|
$progname with NFS or SMB shares, as well.
|
|
|
|
$progname expects to find valid video in a given input directory. It then
|
|
encodes the video in an ouput directory. By default, the output directory name
|
|
is INPUT_DIRECTORY with the suffix "_out" appended to the end.
|
|
|
|
If a VARS_FILE is defined with the -I/--ingest-def flag, INPUT_DIRECTORY
|
|
parameter is ignored.
|
|
|
|
OPTIONS
|
|
-e, --extract-fonts
|
|
Extract all MKV attachments (all platforms), then install any TTF fonts
|
|
found (Linux only) and exit.
|
|
|
|
-o OUTPUT_DIRECTORY, --out-dir=OUTPUT_DIRECTORY
|
|
Override the derived output directory name with a custom output path.
|
|
(Ignored if -I is used.)
|
|
|
|
-s, --subtitles
|
|
Bake in subtitles from the source material as hard subtitles in the output
|
|
video track.
|
|
|
|
-n, --no-subtitles
|
|
Force subtitles to be disabled, no matter what.
|
|
|
|
-l LANG, --lang=LANG
|
|
Specify a language to look for when picking audio and subtitle tracks.
|
|
"eng" is the default.
|
|
|
|
-I VARS_FILE, --ingest-def=VARS_FILE
|
|
Source input and output directory names from variables defined in
|
|
VARS_FILE. See VARS_FILE section below for more information.
|
|
|
|
-h, --help
|
|
Display this help.
|
|
|
|
-d, --dry-run
|
|
Do a dry run. Gets -vv info on each file and skips actual processing.
|
|
|
|
-v
|
|
Increase the verbosity level.
|
|
|
|
VARS_FILE
|
|
The VARS_FILE is a simple config file that has variables defined in valid Bash
|
|
shell.
|
|
|
|
So far only two variables are recognized:
|
|
- ingest_dir
|
|
- out_dir (optional)
|
|
|
|
Here's an example config file:
|
|
|
|
ingest_dir='batches/initiald/in'
|
|
out_dir='batches/initiald/out'
|
|
EOUSAGE
|
|
|
|
usage() { cat <<<"$_usage"; }
|
|
|
|
oargs='-o o:sI:l:hvdne --long out-dir:,subtitles,ingest-def:,lang:,help,dry-run,no-subtitles,extract-fonts'
|
|
parsed=$(getopt $oargs -- "$@")
|
|
[ $? -ne 0 ] && (usage >&2)
|
|
eval set -- "$parsed"
|
|
|
|
var_file=0
|
|
do_subs=0
|
|
no_subs=1
|
|
verbose=0
|
|
dry_run=0
|
|
get_fonts=0
|
|
|
|
while :; do
|
|
case "$1" in
|
|
-o | --out-dir)
|
|
if [ $var_file -eq 1 ]; then
|
|
(echo 'Can'\''t use both -I and -o!' >&2)
|
|
(usage >&2)
|
|
exit 1
|
|
fi
|
|
out_dir="$2"
|
|
shift 2
|
|
;;
|
|
-s | --subtitles)
|
|
do_subs=1
|
|
no_subs=0
|
|
shift
|
|
;;
|
|
-n | --no-subtitles)
|
|
do_subs=0
|
|
no_subs=1
|
|
shift
|
|
;;
|
|
-I | --ingest-def)
|
|
. "$2"
|
|
var_file=1
|
|
shift 2
|
|
;;
|
|
-l | --lang)
|
|
lang="$1"
|
|
shift 2
|
|
break
|
|
;;
|
|
-v)
|
|
((verbose++))
|
|
shift
|
|
;;
|
|
-e | --extract-fonts)
|
|
get_fonts=1
|
|
shift
|
|
;;
|
|
-d | --dry-run)
|
|
verbose=2
|
|
dry_run=1
|
|
shift
|
|
;;
|
|
-h | --help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
(usage >&2)
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
redf() { tput setaf 1; }
|
|
normf() { tput setaf 9; }
|
|
|
|
[ $var_file -eq 0 ] && ingest_dir="$1"
|
|
if [ ! -d "$ingest_dir" ]; then
|
|
(echo $(redf)"[ERR]: \"$ingest_dir\" does not exist!" >&2)
|
|
(normf >&2)
|
|
exit 1
|
|
fi
|
|
|
|
[ -z ${out_dir+nz} ] && out_dir="$ingest_dir"'_out'
|
|
if [ ! -d "$out_dir" ]; then
|
|
mkdir "$out_dir"
|
|
mkdir_s=$?
|
|
if [ $mkdir_s -ne 0 ]; then
|
|
(echo $(redf)"[ERR]: Couldn't create output directory named \"$out_dir\"" >&2)
|
|
(normf >&2)
|
|
exit $mkdir_s
|
|
fi
|
|
fi
|
|
|
|
[ -z ${lang+nz} ] && lang='eng'
|
|
|
|
noext() { basename "$@" | sed 's/\.[a-z0-9]\+$//g'; }
|
|
|
|
werker() {
|
|
#proberx='^\s*Stream\s\+#[0-9]\+:[0-9]\+.*:\s*'
|
|
proberx='^\s*Stream\s\+#[0-9]\+:\([0-9]\+\)(\{0,1\}\(.*\))\{0,1\}:\s*'
|
|
langrx='^\s*Stream\s\+#[0-9]\+:\([0-9]\+\)(\{0,1\}\('"$lang"'\))\{0,1\}:\s*'
|
|
undlangrx='^\s*Stream\s\+#[0-9]\+:\([0-9]\+\)(\{0,1\}\(und\))\{0,1\}:\s*'
|
|
|
|
for file in $ingest_dir/*; do
|
|
noextname=$(noext "$file")
|
|
#subfile="$out_dir/PSP_$noextname.ass"
|
|
subfile="$out_dir/$(hostname).$(date +%s).ass"
|
|
avcfile="$out_dir/PSP_$noextname.mp4"
|
|
thmfile="$out_dir/PSP_$noextname.thm"
|
|
|
|
nfo=$(ffprobe "$file" 2>&1)
|
|
nfostat=$?
|
|
if [ $nfostat -ne 0 ]; then
|
|
(echo <<<"$nfo" >&2)
|
|
exit $nfostat
|
|
fi
|
|
unset nfostat
|
|
|
|
[ -e "$thmfile" ] && continue
|
|
[ -e "$avcfile" ] && continue
|
|
|
|
audiotracks=$(grep "$proberx"'Audio' <<<$nfo)
|
|
videotracks=$(grep "$proberx"'Video' <<<$nfo)
|
|
subtracks=$(grep "$proberx"'Subtitle' <<<$nfo)
|
|
unset nfo
|
|
|
|
n_audiotracks=$(wc -l <<<$audiotracks)
|
|
n_videotracks=$(wc -l <<<$videotracks)
|
|
n_subtracks=$(wc -l <<<$subtracks)
|
|
|
|
# Useful for youtube-dl downloads
|
|
if [ $n_audiotracks -eq 0 ]; then
|
|
(echo "[WARN]: Skipping \"$file\" due to missing audio stream..." >&2)
|
|
continue
|
|
elif [ $n_videotracks -eq 0 ]; then
|
|
(echo "[WARN]: Skipping \"$file\" due to missing video stream..." >&2)
|
|
continue
|
|
fi
|
|
[ $verbose -gt 0 ] && (echo -e "[LOG]: Processing file \"$file\"" >&2)
|
|
|
|
foreign_audiotracks=$(grep -v "$langrx"Audio <<<$audiotracks | grep -v "$undlangrx"Audio)
|
|
und_audiotracks=$(grep "$undlangrx"Audio <<<$audiotracks)
|
|
lang_audiotracks=$(grep "$langrx"Audio <<<$audiotracks)
|
|
foreign_audiotracks_byid=$(sed "s/$proberx.*$/\1/" <<<$foreign_audiotracks)
|
|
und_audiotracks_byid=$(sed "s/$proberx.*$/\1/" <<<$und_audiotracks)
|
|
lang_audiotracks_byid=$(sed "s/$langrx.*$/\1/" <<<$lang_audiotracks)
|
|
|
|
foreign_subtracks=$(grep -v "$langrx"Subtitle <<<$subtracks | grep -v "$undlangrx"Subtitle)
|
|
und_subtracks=$(grep "$undlangrx"Subtitle <<<$subtracks)
|
|
lang_subtracks=$(grep "$langrx"Subtitle <<<$subtracks)
|
|
foreign_subtracks_byid=$(sed "s/$proberx.*$/\1/" <<<$foreign_subtracks)
|
|
und_subtracks_byid=$(sed "s/$proberx.*$/\1/" <<<$und_subtracks)
|
|
lang_subtracks_byid=$(sed "s/$langrx.*$/\1/" <<<$lang_subtracks)
|
|
|
|
|
|
if [ $verbose -gt 1 ]; then
|
|
(echo "[DBG]: AUDIO INFO:" >&2)
|
|
(echo "[DBG]: ===========" >&2)
|
|
(echo -e "[DBG]: all audio:\n$audiotracks" >&2)
|
|
(echo -e "[DBG]: foreign audio:\n$foreign_audiotracks" >&2)
|
|
(echo -e "[DBG]: lang audio ($lang):\n$lang_audiotracks" >&2)
|
|
(echo -e "[DBG]: undef audio ($lang):\n$und_audiotracks" >&2)
|
|
(echo -e "[DBG]: [BY ID] lang audio ($lang):\n$lang_audiotracks_byid" >&2)
|
|
(echo -e "[DBG]: [BY ID] undef audio ($lang):\n$und_audiotracks_byid" >&2)
|
|
(echo -e "[DBG]: [BY ID] foreign audio ($lang):\n$foreign_audiotracks_byid" >&2)
|
|
|
|
(echo "[DBG]: SUBTITLE INFO:" >&2)
|
|
(echo "[DBG]: ==============" >&2)
|
|
(echo -e "[DBG]: all subtitle tracks:\n$subtracks" >&2)
|
|
(echo -e "[DBG]: foreign subtitle tracks:\n$foreign_subtracks" >&2)
|
|
(echo -e "[DBG]: lang subtitle tracks ($lang):\n$lang_subtracks" >&2)
|
|
(echo -e "[DBG]: undef subtitle tracks ($lang):\n$und_subtracks" >&2)
|
|
(echo -e "[DBG]: [BY ID] lang subtitle tracks ($lang):\n$lang_subtracks_byid" >&2)
|
|
(echo -e "[DBG]: [BY ID] undef subtitle tracks ($lang):\n$und_subtracks_byid" >&2)
|
|
(echo -e "[DBG]: [BY ID] foreign subtitle tracks ($lang):\n$foreign_subtracks_byid" >&2)
|
|
fi
|
|
|
|
# Set the default audio stream for FFmpeg mapping
|
|
audiostream=$(sed "s/$proberx.*$/\1/" <<<$audiotracks | head -n 1)
|
|
audiostream_offset=$audiostream
|
|
atype='first'
|
|
|
|
# Try to select the correct audio track for the selected language.
|
|
if [ $n_audiotracks -eq 1 ]; then
|
|
(echo "[WARN]: Only one audio track found, using that one..." >&2)
|
|
elif [ -n "$lang_audiotracks" ]; then
|
|
audiostream=$(head -n 1 <<<$lang_audiotracks_byid)
|
|
atype='lang'
|
|
[ $(wc -l <<<$lang_audiotracks) -gt 1 ] && \
|
|
(echo "[WARN]: More than one audio track in \"$lang\" found, using first one found." >&2)
|
|
atype='lang_first'
|
|
elif [ -z "$lang_audiotracks" ]; then
|
|
(echo "[WARN]: No audio track in \"$lang\" found!" >&2)
|
|
if [ -n "$und_audiotracks" ]; then
|
|
audiostream=$(head -n 1 <<<$und_audiotracks_byid)
|
|
(echo "[LOG]: Using first undefined audio track, in hope that it is in the right language." >&2)
|
|
atype='undef'
|
|
fi
|
|
elif [ -n "$foreign_audiotracks" -a "$atype" != "undef" ]; then
|
|
[ $no_subs -gt 0 ] && do_subs=1
|
|
audiostream=$(head -n 1 <<<$foreign_audiotracks_byid)
|
|
atype='foreign'
|
|
fi
|
|
((audiostream -= audiostream_offset))
|
|
[ $verbose -gt 0 ] && (echo "[LOG]: Using audio track $audiostream." >&2)
|
|
|
|
subvf=""
|
|
# Warn for missing subs, when baking is requested.
|
|
if [ $do_subs -eq 1 ]; then
|
|
if [ $n_subtracks -eq 0 ]; then
|
|
(echo "[WARN]: No subtitles found in \"$file\" to bake into video..." >&2)
|
|
else
|
|
subtrack=$(sed "s/$proberx.*$/\1/" <<<$subtracks | head -n 1)
|
|
subtrack_offset=$subtrack
|
|
if [ $n_subtracks -eq 1 ]; then
|
|
(echo "[WARN]: Only one subtitle track found, using that one..." >&2)
|
|
elif [ -n "$lang_subtracks" ]; then
|
|
subtrack=$(head -n 1 <<<$lang_subtracks_byid)
|
|
[ $(wc -l <<<$lang_subtracks) -gt 1 ] && \
|
|
(echo "[WARN]: More than one subtitle track in \"$lang\" found, using first one found." >&2)
|
|
fi
|
|
((subtrack -= subtrack_offset))
|
|
subvf=""
|
|
fi
|
|
fi
|
|
|
|
if [ $dry_run -gt 0 ]; then continue; fi
|
|
|
|
w=160
|
|
h=120
|
|
ffmpeg -i "$file" \
|
|
-vf "scale=$w:$h:force_original_aspect_ratio=decrease,pad=$w:$h:(ow-iw)/2:(oh-ih)/2" \
|
|
-f image2 \
|
|
-vframes 1 \
|
|
-r 1 \
|
|
-s $w'x'$h \
|
|
-an \
|
|
"$thmfile" \
|
|
;
|
|
|
|
[ -e "$avcfile" ] && continue
|
|
|
|
#lnfile=.$(hostname).$(date +%s)
|
|
#ln -s "$file" "$lnfile"
|
|
|
|
if [ $do_subs -gt 0 ]; then
|
|
ffmpeg -i "$file" \
|
|
-an \
|
|
-vn \
|
|
-c:s ass \
|
|
-map 0:s:$subtrack \
|
|
"$subfile" \
|
|
;
|
|
subvf=",subtitles=$subfile:si=0[v]"
|
|
fi
|
|
#continue
|
|
|
|
#w=720
|
|
#h=480
|
|
w=480
|
|
h=270
|
|
ffmpeg -i "$file" \
|
|
-vf "scale=$w:$h:force_original_aspect_ratio=decrease,pad=$w:$h:(ow-iw)/2:(oh-ih)/2$subvf" \
|
|
-c:a aac \
|
|
-b:a 192k \
|
|
-ar 48000 \
|
|
-ac 2 \
|
|
-map 0:a:$audiostream \
|
|
-c:v libx264 \
|
|
-movflags +faststart \
|
|
-bufsize 2M \
|
|
-profile:v main -level 3 -refs 3 -b-pyramid none -tune film -preset veryslow -flags +mv4+aic \
|
|
-pix_fmt yuv420p \
|
|
-b:v 2000k \
|
|
-maxrate 5000k \
|
|
-s $w'x'$h \
|
|
-aspect 16:9 \
|
|
-metadata title="$noextname encoded by tuxlovesyou" \
|
|
-map 0:v:0 \
|
|
"$avcfile" \
|
|
;
|
|
ffstat=$?
|
|
# Haven't gotten this to work yet, had to resort to bash kludge...
|
|
#-map m:language:$lang \
|
|
#-vf "scale=$w:$h:force_original_aspect_ratio=decrease,pad=$w:$h:(ow-iw)/2:(oh-ih)/2,subtitles=$lnfile:si=$subtrack" \
|
|
|
|
if [ $ffstat -eq 0 ]; then
|
|
rm -vf "$subfile"
|
|
continue
|
|
#unlink $file
|
|
#unlink "$lnfile"
|
|
else
|
|
(echo $(redf)"[ERR]: FFmpeg exited with code $ffstat while transforming \"$file\" into \"$avcfile\"" >&2)
|
|
(normf >&2)
|
|
rm -vf "$avcfile"
|
|
rm -vf "$thmfile"
|
|
rm -vf "$subfile"
|
|
#unlink "$lnfile"
|
|
exit $ffstat
|
|
fi
|
|
done
|
|
}
|
|
|
|
extract_fonts() {
|
|
ingest_dir=$(realpath "$ingest_dir")
|
|
out_dir=$(realpath "$out_dir")
|
|
font_dir="$out_dir/fonts"
|
|
|
|
mkdir "$font_dir"
|
|
cd "$font_dir"
|
|
|
|
attachment_rx='^Attachment\s\+ID\s\+\([0-9]\+\):.*$'
|
|
for file in $ingest_dir/*.mkv; do
|
|
n_attach=$(mkvmerge -i "$file" | grep "$attachment_rx" | sed "s/$attachment_rx/\1/" | tail -n 1)
|
|
attachment_ids=$(eval echo {1..$n_attach})
|
|
mkvextract attachments "$file" $attachment_ids
|
|
done
|
|
|
|
ls -1 *.ttf *.TTF *.otf *.OTF | wc -l
|
|
command -v fc-cache
|
|
if [ $? -eq 0 ]; then
|
|
command -v md5sum
|
|
if [ $? -eq 0 ]; then
|
|
md5sum * | \
|
|
sort | \
|
|
awk 'BEGIN{lasthash = ""} $1 == lasthash {print $2} {lasthash = $1}' | \
|
|
xargs rm -v \
|
|
;
|
|
fi
|
|
ls -1 *.ttf *.TTF *.otf *.OTF | wc -l
|
|
|
|
mkdir -p ~/.local/share/fonts/opentype
|
|
cp -v *.ttf *.TTF ~/.local/share/fonts/
|
|
cp -v *.otf *.OTF ~/.local/share/fonts/opentype/
|
|
|
|
fc-cache -f -v
|
|
fi
|
|
|
|
cd -
|
|
}
|
|
|
|
if [ $get_fonts -gt 0 ]; then
|
|
extract_fonts
|
|
cat <<'EODONE'
|
|
.___________________________________________________________________________.
|
|
| _____ _ _ _ _ _ |
|
|
| | ___|__ _ __ | |_ ___ _____ _| |_ _ __ __ _ ___| |_ ___ __| | | |
|
|
| | |_ / _ \| '_ \| __/ __| / _ \ \/ / __| '__/ _` |/ __| __/ _ \/ _` | | |
|
|
| | _| (_) | | | | |_\__ \ | __/> <| |_| | | (_| | (__| || __/ (_| |_| |
|
|
| |_| \___/|_| |_|\__|___/ \___/_/\_\\__|_| \__,_|\___|\__\___|\__,_(_) |
|
|
`==========================================================================="
|
|
|
|
Now time to encode for real!
|
|
EODONE
|
|
exit
|
|
fi
|
|
|
|
werker
|