diff --git a/to-m4b.sh b/to-m4b.sh old mode 100644 new mode 100755 index 658765f..f92d736 --- a/to-m4b.sh +++ b/to-m4b.sh @@ -3,28 +3,126 @@ set -x set -eufo pipefail # CONSTANTS -declare -r m4b-tool="nix run github:sandreas/m4b-tool#m4b-tool-libfdk -- " declare -r script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # VARS LEADING_ZEROES=2 -MAIN_SRC="${SRC:${script_dir}/src}" -MAIN_OUT="${OUT:${script_dir}/out}" +SRC="${script_dir}/src" +OUT="${script_dir}/out" # FUNCTIONS parse-vars() { - # TODO implement parsing the directory structure of the passed directory, to fill in missing vars and return them # Series structure: ${SRC}///Book - - {}/ # Standalone structure: ${SRC}// - {}/ - # Return the wihh leading zeros based on the LEADING_ZEROES variable + # Return variables as key=value lines; zero-padded to LEADING_ZEROES as series_index + local dir="${1%/}" + local leaf="${dir##*/}" + local parent="${dir%/*}" + local grandparent="${parent%/*}" + + local author="" + local series="" + local title="" + local year="" + local narrator="" + local part="" + local series_index="" + local rest="" + + # Helper: trim leading/trailing whitespace + trim() { local s="$1"; s="${s#${s%%[![:space:]]*}}"; s="${s%${s##*[![:space:]]}}"; printf '%s' "$s"; } + + # Detect pattern and extract fields + if [[ "$leaf" =~ ^[Bb]ook[[:space:]]+([0-9]+)[[:space:]]*-[[:space:]]*([0-9]{4})[[:space:]]*-[[:space:]]*(.+)$ ]]; then + part="${BASH_REMATCH[1]}" + year="${BASH_REMATCH[2]}" + rest="${BASH_REMATCH[3]}" + if [[ "$rest" =~ ^(.+)[[:space:]]*\{([^}]*)\}[[:space:]]*$ ]]; then + title="${BASH_REMATCH[1]}" + narrator="${BASH_REMATCH[2]}" + else + title="$rest" + fi + series="${parent##*/}" + author="${grandparent##*/}" + elif [[ "$leaf" =~ ^([0-9]{4})[[:space:]]*-[[:space:]]*(.+)$ ]]; then + year="${BASH_REMATCH[1]}" + rest="${BASH_REMATCH[2]}" + if [[ "$rest" =~ ^(.+)[[:space:]]*\{([^}]*)\}[[:space:]]*$ ]]; then + title="${BASH_REMATCH[1]}" + narrator="${BASH_REMATCH[2]}" + else + title="$rest" + fi + author="${parent##*/}" + else + # Fallback best-effort parsing + rest="$leaf" + if [[ "$rest" =~ \{([^}]*)\}[[:space:]]*$ ]]; then + narrator="${BASH_REMATCH[1]}" + rest="${rest%\{${narrator}\}*}" + fi + if [[ "$rest" =~ ^([0-9]{4})[[:space:]]*-[[:space:]]*(.+)$ ]]; then + year="${BASH_REMATCH[1]}" + title="${BASH_REMATCH[2]}" + else + title="$rest" + fi + author="${parent##*/}" + fi + + # Normalize whitespace + title="$(trim "$title")" + author="$(trim "$author")" + series="$(trim "$series")" + narrator="$(trim "$narrator")" + + # Zero-pad part for series_index if available + if [[ -n "$part" ]]; then + part="${part//[^0-9]/}" + series_index=$(printf "%0*d" "$LEADING_ZEROES" "$part") + fi + + printf 'author=%q\n' "$author" + printf 'series=%q\n' "$series" + printf 'series_index=%q\n' "$series_index" + printf 'title=%q\n' "$title" + printf 'year=%q\n' "$year" + printf 'narrator=%q\n' "$narrator" } get-book-directories() { - # TODO implemet getting the book directoris based on the ${SRC} path - # find the directories that contains the books files and return them as an array - # Example book directory: - # /path/to/src/Author/Series/Book Part - Year - Book {Narrator}/ - # /path/to/src/Author/Year - Book {Narrator}/ + # Discover book directories under ${SRC} (or ${script_dir}/src if unset) + # Matches: + # - ${SRC}///Book - - {<narrator>}/ + # - ${SRC}/<author>/<year> - <title> {<narrator>}/ + # Prints matched directories, one per line + local src_root="${1}" + local -a results=() + local dir leaf + + # Iterate candidate depths using fast globbing (avoids slow full-recursive find) + for dir in "${src_root}"/*/*/ "${src_root}"/*/*/*/; do + [[ -d "$dir" ]] || continue + leaf="${dir%/}"; leaf="${leaf##*/}" + + # Match standalone: "YYYY - Title {Narrator}" or similar + if [[ "$leaf" =~ ^[0-9]{4}[[:space:]]*-[[:space:]]*.+$ ]]; then + : + # Match series: "Book N - YYYY - Title {Narrator}" (Book/book, flexible spaces) + elif [[ "$leaf" =~ ^[Bb]ook[[:space:]]+[0-9]+[[:space:]]*-[[:space:]]*[0-9]{4}[[:space:]]*-[[:space:]]*.+$ ]]; then + : + else + continue + fi + + # Quick check: directory contains at least one likely audio file + if compgen -G "$dir"*.{mp3,m4a,m4b,aac,flac,wav} >/dev/null 2>&1; then + results+=("${dir%/}") + fi + done + + printf '%s\n' "${results[@]}" } m4b-merge() { @@ -38,7 +136,7 @@ m4b-merge() { local series_index=${8} mkdir -p "$(dirname "${output_file}")" - "${m4b-tool}" \ + nix run github:sandreas/m4b-tool#m4b-tool-libfdk -- \ merge \ -v \ --jobs=6 \ @@ -55,7 +153,32 @@ m4b-merge() { } main() { + local src_root="${SRC:-${script_dir}/src}" + local out_root="${OUT:-${script_dir}/out}" + local dir author narrator title year series series_index output_file + mapfile -t dirs < <(get-book-directories "${src_root}") + + for dir in "${dirs[@]}"; do + eval "$(parse-vars "$dir")" + if [[ -z "$title" ]]; then + echo "Skipping '$dir': could not parse title" >&2 + continue + fi + if [[ -z "$author" ]]; then + echo "Skipping '$dir': could not parse author" >&2 + continue + fi + # Construct output path: ${OUT}/<author>/<series or standalone>/<series_index - >title {narrator}.m4b + if [[ -n "$series" ]]; then + output_file="${out_root}/${author}/${series}/${series_index:+${series_index} - }${title}${narrator:+ {${narrator}}}.m4b" + else + output_file="${out_root}/${author}/${year:+${year} - }${title}${narrator:+ {${narrator}}}.m4b" + fi + + echo "Processing '$dir' -> '$output_file'" + # m4b-merge "$output_file" "$dir" "$author" "$narrator" "$title" "$year" "$series" "$series_index" + done } main "$@"