#!/usr/bin/env sh # From a single byte in hexadecimal per line # to lines ending with 0a (hex for '\n'). regroup_lines(){ awk ' BEGIN { line_start=1 } { if (line_start == 1) line = $1; else line = line " " $1; line_start = 0; if ($1 == "0a") { print line; line_start = 1 } } END { if (line_start == 0) print line } ' } # From ’ to ' simple_quote(){ sed "s/e2 80 99/27/g" } # From / to ' - ' replace_slashes(){ sed "s/2f/20 2d 20/g" } remove_multibyte_characters(){ sed "s/e2 80 .. //g" } # Convert input into hexadecimal and a single byte per line. to_hex_one_column(){ xxd -p -c 1 } # Reverse hexadecimal to original value. from_hex(){ xxd -p -r } # Remove non ascii characters, convert "’" to "'", or remove invalid filename characters. to_ascii(){ to_hex_one_column | # Convert input into hexadecimal and a single byte per line. regroup_lines | # Required to easily match multi-byte characters. simple_quote | # Convert "’" to "'". replace_slashes | # Replaces / by ' - ', since it isn't a valid filename character. remove_multibyte_characters | # Remove non ascii values. from_hex # Convert back from hex. } process_end_of_songs(){ awk -v NONUMBER="$NONUMBER" -v SEPARATOR="$SEPARATOR" ' 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; } ' } first_column_to_seconds(){ awk ' { # 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; } ' } # Get a more usable time representation for the beginning and the end of songs. process_time_file(){ to_ascii | first_column_to_seconds | process_end_of_songs } run_ffmpeg(){ file=$1 from=$2 to=$3 final_title=$4 LOG_LEVEL="-loglevel error" FROM="-ss $from" TO="" if [ "$to" != "" ]; then TO="-to $to" fi INPUT_FILE="$file" OUTPUT_FILE="$final_title" case "v$VERBOSITY" in v0) ;; v1) echo "extracting '$final_title'" ;; v2) echo "ffmpeg $LOG_LEVEL $FROM $TO -i $INPUT_FILE '$OUTPUT_FILE'" ;; *) echo "verbosity is not set properly" >&2 exit 1 ;; esac if [ "$SIMULATION" = "" ]; then $(< /dev/null ffmpeg $LOG_LEVEL $FROM $TO -i "$INPUT_FILE" "$OUTPUT_FILE") fi } rip(){ audio_file="$1" time_file="$2" process_time_file < "$time_file" | while read LINE; do track_start=$(echo $LINE | cut -d ' ' -f 1) track_end=$(echo $LINE | cut -d ' ' -f 2) track_title=$(echo $LINE | cut -d ' ' -f 3-) if [ "$track_end" = "END_OF_FILE" ]; then track_end="" fi run_ffmpeg "${audio_file}" "${track_start}" "${track_end}" "${track_title}.${FORMAT}" done } usage(){ echo "usage: $0 command" echo "command: show " echo "command: rip " echo echo "song-list line format example: 1:30 My second track of the playlist" echo "show output format: start end title" echo echo "envvar: SIMULATION, if non empty, do not invoke ffmpeg" echo "envvar: NONUMBER, if equals 1, do not write song number" echo "envvar: FORMAT [mp3,ogg,opus,…], see the ffmpeg documentation" echo "envvar: SEPARATOR [separator] (default: ' - '), write song number, with this separator" echo " example with SEPARATOR='_': song names will be 01_song.opus 02_song.opus…" echo "envvar: VERBOSITY [0-3] (default: 1)" echo " verbosity 0: no output exept errors from ffmpeg" echo " verbosity 1: simple indications on the current track being extracted" echo " verbosity 2: print actual ffmpeg commands the script currently runs" } if [ $# -lt 1 ]; then usage exit 0 fi command=$1 shift if [ "$FORMAT" = "" ]; then echo "default FORMAT: opus" FORMAT="opus" else echo "FORMAT: $FORMAT" fi if [ "$VERBOSITY" = "" ]; then echo "default VERBOSITY: 1" VERBOSITY=1 else echo "VERBOSITY level: $VERBOSITY" fi if [ "$NONUMBER" = "" ]; then echo "default NONUMBER: disabled" NONUMBER=0 # Assume that there should be a separator. if [ "$SEPARATOR" = "" ]; then echo "default SEPARATOR: ' - '" SEPARATOR=" - " else echo "SEPARATOR: '$SEPARATOR'" fi else echo "NONUMBER: won't prefix tracks" SEPARATOR="" fi if [ "$SIMULATION" != "" ]; then echo "SIMULATION envvar is set: this is a simulation." fi /dev/null 2>/dev/null if [ $? -ne 0 ]; then echo "xxd: you don't have an xxd program with '-p' option." 1>&2 exit 1 fi /dev/null 2>/dev/null if [ $? -ne 0 ]; then echo "xxd: you don't have an xxd program with '-r' option." 1>&2 exit 1 fi case "x-${command}" in x-show) # Takes the audio file in first parameter if [ $# -ne 1 ]; then echo "Usage: $0 show time-stamps-file" >&2 exit 1 fi process_time_file < "$1" ;; x-rip) # Takes the audio file in first parameter if [ $# -ne 2 ]; then echo "Usage: $0 show music-file time-stamps-file" >&2 exit 1 fi rip "$1" "$2" ;; *) usage 1>&2 exit 1 esac