2021-03-27 21:18:08 +01:00
|
|
|
|
#!/usr/bin/env sh
|
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
# From a single byte in hexadecimal per line to lines ending with 0a
|
|
|
|
|
# (hex for '\n'). Ex: 61 62 63 0a
|
|
|
|
|
# Required to easily match (and remove) multi-byte characters.
|
2021-04-30 01:13:53 +02:00
|
|
|
|
regroup_lines() awk '
|
2021-04-06 05:18:19 +02:00
|
|
|
|
BEGIN {
|
2021-04-06 16:09:15 +02:00
|
|
|
|
line_start=1
|
2021-04-06 05:18:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
2021-04-06 16:09:15 +02:00
|
|
|
|
if (line_start == 1)
|
2021-04-06 05:18:19 +02:00
|
|
|
|
line = $1;
|
|
|
|
|
else
|
|
|
|
|
line = line " " $1;
|
|
|
|
|
|
2021-04-06 16:09:15 +02:00
|
|
|
|
line_start = 0;
|
2021-04-06 05:18:19 +02:00
|
|
|
|
if ($1 == "0a") {
|
|
|
|
|
print line;
|
2021-04-06 16:09:15 +02:00
|
|
|
|
line_start = 1
|
2021-04-06 05:18:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
END {
|
2021-04-06 16:09:15 +02:00
|
|
|
|
if (line_start == 0)
|
2021-04-06 05:18:19 +02:00
|
|
|
|
print line
|
|
|
|
|
}
|
|
|
|
|
'
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2021-04-06 16:09:15 +02:00
|
|
|
|
# From ’ to '
|
2021-04-30 01:13:53 +02:00
|
|
|
|
simple_quote() sed "s/e2 80 99/27/g"
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2021-04-30 01:22:30 +02:00
|
|
|
|
# From / to '-'
|
|
|
|
|
replace_slashes() sed "s/2f/2d/g"
|
2021-04-06 22:55:30 +02:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
remove_backslashes() sed "s/5c//g"
|
|
|
|
|
|
2021-04-30 01:13:53 +02:00
|
|
|
|
remove_multibyte_characters() sed "s/e2 80 .. //g"
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
uppercase() tr "[a-z]" "[A-Z]"
|
|
|
|
|
|
|
|
|
|
# One column decimal to plain text.
|
|
|
|
|
from_dec() awk '{ printf ("%c", $1 + 0) }'
|
|
|
|
|
|
|
|
|
|
# Replace spaces by line returns, outputs a single column.
|
|
|
|
|
spaces_to_line_returns() tr " " "\n"
|
|
|
|
|
|
2021-04-06 16:09:15 +02:00
|
|
|
|
# Convert input into hexadecimal and a single byte per line.
|
2022-01-26 12:27:41 +01:00
|
|
|
|
to_hex_one_column() { od -An -tx1 | awk '{for(i=1;i<=NF;i++){ print $i }}'; }
|
|
|
|
|
|
|
|
|
|
# One column hexa to one column decimal.
|
|
|
|
|
hex_to_dec() { { echo "obase=10;ibase=16;" ; cat ; } | bc ; }
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
# Reverse hexadecimal (with space separators) to original value.
|
|
|
|
|
from_hex() { spaces_to_line_returns | uppercase | hex_to_dec | from_dec; }
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
# Remove non ascii, backslashes and invalid filename characters,
|
|
|
|
|
# convert "’" to "'", "/" to " - ".
|
2021-04-06 05:18:19 +02:00
|
|
|
|
to_ascii(){
|
2022-01-26 12:27:41 +01:00
|
|
|
|
to_hex_one_column | # Input to hexadecimal, 1-byte representation per line.
|
|
|
|
|
regroup_lines | # From 1-byte to x-byte lines with space separators.
|
|
|
|
|
simple_quote | # From "’" to "'".
|
|
|
|
|
replace_slashes | # From / to '-'.
|
2021-04-06 16:09:15 +02:00
|
|
|
|
remove_multibyte_characters | # Remove non ascii values.
|
2022-01-26 12:27:41 +01:00
|
|
|
|
remove_backslashes | # Can mess with the script.
|
|
|
|
|
from_hex # Convert back from hex (x-byte per line, space separator).
|
2021-04-06 05:18:19 +02:00
|
|
|
|
}
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
comp_end_of_tracks() awk -v NONUMBER="$NONUMBER" -v SEPARATOR="$SEPARATOR" '
|
2021-04-06 05:18:19 +02:00
|
|
|
|
BEGIN {
|
|
|
|
|
OFS=" "
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
if (NR > 1) {
|
|
|
|
|
print timestamp, $1, title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timestamp = $1;
|
|
|
|
|
|
|
|
|
|
if (NONUMBER == 1) {
|
|
|
|
|
title = $2
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (NR < 10) {
|
|
|
|
|
title = "0" NR SEPARATOR $2
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
title = NR SEPARATOR $2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (i=3; i <= NF; i++) {
|
|
|
|
|
title = title " " $i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
END {
|
|
|
|
|
print timestamp, "END_OF_FILE", title;
|
|
|
|
|
}
|
|
|
|
|
'
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2021-04-30 01:13:53 +02:00
|
|
|
|
first_column_to_seconds() awk '
|
2021-04-06 05:18:19 +02:00
|
|
|
|
{
|
|
|
|
|
# from 10:30 to 630
|
|
|
|
|
n = split ($1, arr, ":")
|
|
|
|
|
for (i = 0; i <= n; i++) {
|
|
|
|
|
if (i == 0) {
|
|
|
|
|
v = arr[n-i];
|
|
|
|
|
}
|
|
|
|
|
else if (i == 1) {
|
|
|
|
|
v += 60 * arr[n-i];
|
|
|
|
|
}
|
|
|
|
|
else if (i == 2) {
|
|
|
|
|
v += 3600 * arr[n-i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$1 = v;
|
|
|
|
|
print;
|
|
|
|
|
v = 0;
|
|
|
|
|
}
|
|
|
|
|
'
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2021-04-06 05:18:19 +02:00
|
|
|
|
# Get a more usable time representation for the beginning and the end of songs.
|
2022-01-26 12:27:41 +01:00
|
|
|
|
get_timestamps(){ to_ascii | first_column_to_seconds | comp_end_of_tracks; }
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
|
|
|
|
run_ffmpeg(){
|
2021-04-05 01:41:16 +02:00
|
|
|
|
file=$1
|
|
|
|
|
from=$2
|
2021-04-06 17:24:26 +02:00
|
|
|
|
to=$3
|
2021-04-05 01:41:16 +02:00
|
|
|
|
final_title=$4
|
|
|
|
|
|
|
|
|
|
LOG_LEVEL="-loglevel error"
|
|
|
|
|
FROM="-ss $from"
|
2021-04-06 17:24:26 +02:00
|
|
|
|
TO=""
|
|
|
|
|
if [ "$to" != "" ]; then
|
|
|
|
|
TO="-to $to"
|
2021-04-05 02:33:10 +02:00
|
|
|
|
fi
|
2021-04-06 22:55:30 +02:00
|
|
|
|
INPUT_FILE="$file"
|
2021-04-05 01:41:16 +02:00
|
|
|
|
OUTPUT_FILE="$final_title"
|
|
|
|
|
|
|
|
|
|
case "v$VERBOSITY" in
|
|
|
|
|
v0)
|
|
|
|
|
;;
|
|
|
|
|
v1)
|
|
|
|
|
echo "extracting '$final_title'"
|
|
|
|
|
;;
|
|
|
|
|
v2)
|
2022-01-26 20:44:45 +01:00
|
|
|
|
echo "ffmpeg $LOG_LEVEL $FROM $TO -i $INPUT_FILE $FFOPTS '$OUTPUT_FILE'"
|
2021-04-05 01:41:16 +02:00
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo "verbosity is not set properly" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
if [ "$SIMULATION" = "" ]; then
|
2022-01-26 20:44:45 +01:00
|
|
|
|
ffmpeg $LOG_LEVEL $FROM $TO -i "$INPUT_FILE" $FFOPTS "$OUTPUT_FILE"
|
2021-03-27 21:18:08 +01:00
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
extraction(){
|
2021-03-27 21:18:08 +01:00
|
|
|
|
audio_file="$1"
|
|
|
|
|
time_file="$2"
|
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
get_timestamps < "$time_file" | while read LINE; do
|
2021-04-06 17:24:26 +02:00
|
|
|
|
track_start=$(echo $LINE | cut -d ' ' -f 1)
|
|
|
|
|
track_end=$(echo $LINE | cut -d ' ' -f 2)
|
2021-04-05 01:41:16 +02:00
|
|
|
|
track_title=$(echo $LINE | cut -d ' ' -f 3-)
|
2021-04-06 17:24:26 +02:00
|
|
|
|
|
|
|
|
|
if [ "$track_end" = "END_OF_FILE" ]; then
|
|
|
|
|
track_end=""
|
2021-04-05 02:33:10 +02:00
|
|
|
|
fi
|
2021-03-27 21:18:08 +01:00
|
|
|
|
|
2022-01-26 12:27:41 +01:00
|
|
|
|
# Input is /dev/null, otherwise subshells will take the output
|
|
|
|
|
# of "get_timestamps" as input.
|
|
|
|
|
# Be careful: "while read X" is a dangerous shell design.
|
|
|
|
|
< /dev/null run_ffmpeg "${audio_file}" \
|
|
|
|
|
"${track_start}" "${track_end}" \
|
|
|
|
|
"${track_title}.${FORMAT}"
|
2021-03-27 21:18:08 +01:00
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usage(){
|
2022-01-17 12:08:29 +01:00
|
|
|
|
cat <<END
|
|
|
|
|
Get tracks:
|
|
|
|
|
usage: $0 <single-file-playlist> <song-list>
|
|
|
|
|
|
|
|
|
|
Debug mode (displays starting and ending times for each song):
|
|
|
|
|
usage: $0 <song-list>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Format for <song-list>:
|
|
|
|
|
0:00 First track
|
|
|
|
|
1:30 Second track
|
|
|
|
|
|
|
|
|
|
Environment variables:
|
2022-01-26 20:44:45 +01:00
|
|
|
|
- SIMULATION [empty or not] do not invoke ffmpeg
|
|
|
|
|
|
|
|
|
|
- FORMAT [mp3,ogg,opus,…] see ffmpeg documentation
|
|
|
|
|
- FFOPTS (default: '-c:a copy') see ffmpeg documentation
|
|
|
|
|
|
|
|
|
|
- NONUMBER [empty or 1] do not write song numbers
|
2022-01-17 12:08:29 +01:00
|
|
|
|
- SEPARATOR [separator] (default: ' - ')
|
|
|
|
|
separator between number and name
|
|
|
|
|
example with SEPARATOR='_': 01_intro.opus 02_blah.opus…
|
2022-01-26 20:44:45 +01:00
|
|
|
|
|
|
|
|
|
- HEADERS [empty or 1] print env params (verbosity, quality, etc.)
|
2022-01-17 12:08:29 +01:00
|
|
|
|
- VERBOSITY [0-3] (default: 1)
|
|
|
|
|
0: no output except errors from ffmpeg
|
|
|
|
|
1: simple indications on the current track being extracted
|
|
|
|
|
2: print actual ffmpeg commands the script currently runs
|
|
|
|
|
END
|
2021-03-27 21:18:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header(){
|
|
|
|
|
if [ "$HEADERS" = "1" ]; then
|
|
|
|
|
echo $*
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-26 20:44:45 +01:00
|
|
|
|
warning(){
|
|
|
|
|
echo "WARNING: $*"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Default output format is based on the extension of the input audio file.
|
|
|
|
|
if [ $# -eq 2 ]; then
|
|
|
|
|
DEFAULT_FORMAT="$(echo $1 | awk -F . '{print $NF}')"
|
|
|
|
|
else
|
|
|
|
|
header "no default FORMAT selected"
|
|
|
|
|
fi
|
|
|
|
|
|
2021-04-05 01:41:16 +02:00
|
|
|
|
if [ "$FORMAT" = "" ]; then
|
2022-01-26 20:44:45 +01:00
|
|
|
|
FORMAT="$DEFAULT_FORMAT"
|
|
|
|
|
header "default FORMAT: ${FORMAT}"
|
2021-04-06 05:18:19 +02:00
|
|
|
|
else
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "FORMAT: $FORMAT"
|
2021-04-05 01:41:16 +02:00
|
|
|
|
fi
|
|
|
|
|
|
2022-01-26 20:44:45 +01:00
|
|
|
|
# For unexperienced users, print a warning when input and output formats differ.
|
|
|
|
|
# In case FFOPTS is set, encoding is expected to be handled, drop the warning.
|
|
|
|
|
# Example (remove the get-tracks.sh default behavior, perform re-encoding):
|
|
|
|
|
# FFOPTS=" "
|
|
|
|
|
if [ "$FFOPTS" = "" ] && [ "$FORMAT" != "$DEFAULT_FORMAT" ]; then
|
|
|
|
|
warning "input and output formats seem to differ"
|
|
|
|
|
warning "1. re-encoding may be required (through the FFOPTS envvar)"
|
|
|
|
|
warning "2. FFOPTS represents ffmpeg options, directly given to ffmpeg"
|
|
|
|
|
warning ' (default: "-c:a copy" = copy without re-encoding)'
|
|
|
|
|
warning ' You can put FFOPTS=" " if you want to perform re-encoding.'
|
|
|
|
|
fi
|
|
|
|
|
|
2021-04-05 01:41:16 +02:00
|
|
|
|
if [ "$VERBOSITY" = "" ]; then
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "default VERBOSITY: 1"
|
2021-04-05 01:41:16 +02:00
|
|
|
|
VERBOSITY=1
|
2021-04-06 05:18:19 +02:00
|
|
|
|
else
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "VERBOSITY level: $VERBOSITY"
|
2021-04-05 01:41:16 +02:00
|
|
|
|
fi
|
|
|
|
|
|
2021-04-06 05:18:19 +02:00
|
|
|
|
if [ "$NONUMBER" = "" ]; then
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "default NONUMBER: disabled"
|
2021-04-06 05:18:19 +02:00
|
|
|
|
NONUMBER=0
|
|
|
|
|
|
|
|
|
|
# Assume that there should be a separator.
|
|
|
|
|
if [ "$SEPARATOR" = "" ]; then
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "default SEPARATOR: ' - '"
|
2021-04-06 05:18:19 +02:00
|
|
|
|
SEPARATOR=" - "
|
|
|
|
|
else
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "SEPARATOR: '$SEPARATOR'"
|
2021-04-06 05:18:19 +02:00
|
|
|
|
fi
|
|
|
|
|
else
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "NONUMBER: won't prefix tracks"
|
2021-04-06 05:18:19 +02:00
|
|
|
|
SEPARATOR=""
|
2021-04-05 01:41:16 +02:00
|
|
|
|
fi
|
|
|
|
|
|
2022-01-26 20:44:45 +01:00
|
|
|
|
if [ "$FFOPTS" != "" ]; then
|
|
|
|
|
header "FFOPTS envvar is set: ${FFOPTS}."
|
|
|
|
|
else
|
|
|
|
|
FFOPTS="-c:a copy"
|
|
|
|
|
header "default FFOPTS: ${FFOPTS}"
|
|
|
|
|
fi
|
|
|
|
|
|
2021-04-05 01:41:16 +02:00
|
|
|
|
if [ "$SIMULATION" != "" ]; then
|
2021-04-30 01:21:23 +02:00
|
|
|
|
header "SIMULATION envvar is set: this is a simulation."
|
2021-04-05 01:41:16 +02:00
|
|
|
|
fi
|
|
|
|
|
|
2022-01-17 12:08:29 +01:00
|
|
|
|
case $# in
|
2022-01-26 12:27:41 +01:00
|
|
|
|
0) usage; exit 0;;
|
2022-01-26 20:44:45 +01:00
|
|
|
|
1) get_timestamps < "$1";;
|
|
|
|
|
2) extraction "$1" "$2";;
|
|
|
|
|
*) usage 1>&2; exit 1;;
|
2021-03-27 21:18:08 +01:00
|
|
|
|
esac
|