foxygit / mkm3u8 Log in
commits tags

/mkm3u8-tui · 11.06 KB

raw
#!/usr/bin/env bash
#
# mkm3u8-tui — ncurses-TUI för att generera Rockbox-vänliga .m3u8-spellistor
#              med device-relativa sökvägar.
#
# Bygger på 'dialog'. Låter dig:
#   * välja device-root (auto-detekterade enheter + inbyggd default)
#   * bläddra fram mappen som ska katalogiseras
#   * sätta spellistans namn
#   * välja var listan sparas
#   * ev. lägga till #EXTINF-metadata via ffprobe
#
# Sökvägarna i listan görs absoluta från device-rooten (mount-prefixet kapas),
# skrivs som UTF-8 + LF — precis vad Rockbox vill ha.

set -uo pipefail

# På en rå VT-konsol (TERM=linux) i UTF-8-läge ritar ncurses ofta ramarnas
# linjer med Unicode-ritteckensnittstecken som konsolfontet inte har på rätt
# bredd — resultatet blir ramar som ser förskjutna/felplacerade ut. Tvinga
# istället klassisk VT100-alt-charset, som konsolen hanterar korrekt.
export NCURSES_NO_UTF8_ACS=1

# --- inbyggda defaults ------------------------------------------------------
DEFAULT_DEVICE_ROOT="/media/mrfox/JENS IPOD"
AUDIO_EXTS="mp3 flac ogg opus m4a aac wav wv ape mpc wma"
BT="mkm3u8  ::  Rockbox playlist generator"

# --- beroendekoll -----------------------------------------------------------
if ! command -v dialog >/dev/null 2>&1; then
    echo "Detta script kräver 'dialog'." >&2
    echo "Installera med:  sudo apt install dialog" >&2
    exit 1
fi

# --- state ------------------------------------------------------------------
DEVICE_ROOT=""
SOURCE_DIR=""
PLAYLIST_NAME=""
PLAYLIST_NAME_AUTO=1   # 1 = följer källmappens namn, 0 = satt manuellt av användaren
SAVE_DIR=""
EXTINF=0

# förvälj inbyggd device-root om den finns mountad
[[ -d "$DEFAULT_DEVICE_ROOT" ]] && DEVICE_ROOT="$DEFAULT_DEVICE_ROOT"

# --- helpers ----------------------------------------------------------------
dlg() { dialog --stdout "$@"; }

# --- mapp-bläddrare -----------------------------------------------------
# Ersätter dialogs inbyggda --dselect, som kräver mellanslag (inte Enter)
# för att kliva ner i en undermapp — mycket förvirrande i praktiken.
# usage: browse_dir <startmapp> [golv]   (golv = hindra uppåtnavigering förbi denna mapp)
# sätter $BROWSE_RESULT, retur 1 om avbrutet
browse_dir() {
    local cur="${1%/}" floor="${2%/}"
    [[ -d "$cur" ]] || cur="/"

    while true; do
        local -a subdirs=() menu=( "." "[ Välj denna mapp ]" )
        [[ "$cur" != "/" && "$cur" != "$floor" ]] && menu+=( ".." "[ .. upp en nivå ]" )

        local d
        while IFS= read -r d; do
            [[ -n "$d" ]] && subdirs+=( "$d" )
        done < <(find "$cur" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort -V)
        for d in "${subdirs[@]}"; do menu+=( "$d" "$d/" ); done

        local choice
        choice="$(dlg --backtitle "$BT" --title "Bläddra" \
                      --menu "$cur" 20 76 12 "${menu[@]}")" || return 1

        case "$choice" in
            .) BROWSE_RESULT="$cur"; return 0 ;;
            ..) cur="$(dirname "$cur")" ;;
            *) cur="$cur/$choice" ;;
        esac
    done
}

# scanna efter mountade enheter
detect_devices() {
    local base d
    for base in "/media/$USER" "/run/media/$USER" /media /mnt; do
        [[ -d "$base" ]] || continue
        for d in "$base"/*; do
            [[ -d "$d" ]] || continue
            printf '%s\n' "$d"
        done
    done
}

# --- steg: device-root ------------------------------------------------------
choose_device_root() {
    local -a cands=("$DEFAULT_DEVICE_ROOT")
    local d
    while IFS= read -r d; do
        [[ -n "$d" ]] || continue
        [[ "$d" == "$DEFAULT_DEVICE_ROOT" ]] && continue
        cands+=("$d")
    done < <(detect_devices)

    local -a menu=()
    local i label
    for i in "${!cands[@]}"; do
        label="${cands[$i]}"
        [[ "$i" -eq 0 ]] && label="$label  [inbyggd]"
        if [[ -d "${cands[$i]}" ]]; then label="$label [monterad]"; else label="$label (ej mountad)"; fi
        menu+=( "$i" "$label" )
    done
    local custom_idx=${#cands[@]}
    menu+=( "$custom_idx" "Egen sökväg (bläddra)..." )

    local choice
    choice="$(dlg --backtitle "$BT" --title "Välj device-root" \
                  --menu "Var är spelaren mountad?" 18 74 10 \
                  "${menu[@]}")" || return 1

    if [[ "$choice" -eq "$custom_idx" ]]; then
        local start="${DEVICE_ROOT:-/media/$USER}"
        [[ -d "$start" ]] || start="/media"
        browse_dir "$start" "" || return 1
        DEVICE_ROOT="$BROWSE_RESULT"
    else
        DEVICE_ROOT="${cands[$choice]%/}"
    fi
}

# --- steg: källmapp ---------------------------------------------------------
choose_source() {
    local start="${SOURCE_DIR:-$DEVICE_ROOT}"
    [[ -d "$start" ]] || start="$DEVICE_ROOT"
    [[ -d "$start" ]] || start="/"
    browse_dir "$start" "$DEVICE_ROOT" || return 1
    SOURCE_DIR="$BROWSE_RESULT"
    [[ $PLAYLIST_NAME_AUTO -eq 1 ]] && PLAYLIST_NAME="$(basename "$SOURCE_DIR").m3u8"
}

# --- steg: namn -------------------------------------------------------------
choose_name() {
    local def="${PLAYLIST_NAME:-playlist.m3u8}"
    local name
    name="$(dlg --backtitle "$BT" --title "Spellistans namn" \
                --inputbox "Filnamn (.m3u8 läggs till automatiskt om det saknas):" \
                10 62 "$def")" || return 1
    [[ -z "$name" ]] && return 0
    [[ "$name" == *.m3u8 ]] || name="$name.m3u8"
    PLAYLIST_NAME="$name"
    # Låt namnet fortsätta följa mappen om det som skrevs in råkar matcha
    # auto-namnet för aktuell källmapp (t.ex. att man bara tryckte OK).
    # Bara ett faktiskt eget namn låser det till manuellt läge.
    if [[ -n "$SOURCE_DIR" && "$name" == "$(basename "$SOURCE_DIR").m3u8" ]]; then
        PLAYLIST_NAME_AUTO=1
    else
        PLAYLIST_NAME_AUTO=0
    fi
}

# --- steg: spara var --------------------------------------------------------
choose_savedir() {
    local pl="$DEVICE_ROOT/Playlists"
    local choice
    choice="$(dlg --backtitle "$BT" --title "Var ska spellistan sparas?" \
                  --menu "" 12 74 5 \
                  1 "$pl  (skapas vid behov)" \
                  2 "$DEVICE_ROOT  (device-root)" \
                  3 "Egen sökväg (bläddra)...")" || return 1
    case "$choice" in
        1) SAVE_DIR="$pl" ;;
        2) SAVE_DIR="$DEVICE_ROOT" ;;
        3) local start="${SAVE_DIR:-$DEVICE_ROOT}"
           [[ -d "$start" ]] || start="$DEVICE_ROOT"
           browse_dir "$start" "$DEVICE_ROOT" || return 1
           SAVE_DIR="$BROWSE_RESULT" ;;
    esac
}

# --- steg: EXTINF -----------------------------------------------------------
toggle_extinf() {
    if [[ $EXTINF -eq 0 ]] && ! command -v ffprobe >/dev/null 2>&1; then
        dlg --backtitle "$BT" --title "ffprobe saknas" \
            --msgbox "ffprobe hittades inte — kan inte lägga till #EXTINF.\n\nInstallera ffmpeg för att få metadata." 9 60
        return 0
    fi
    if [[ $EXTINF -eq 1 ]]; then
        EXTINF=0
    else
        EXTINF=1
    fi
}

# --- generering -------------------------------------------------------------
generate() {
    [[ -z "$DEVICE_ROOT" ]] && { dlg --msgbox "Ingen device-root vald." 7 50; return 1; }
    [[ -z "$SOURCE_DIR"  ]] && { dlg --msgbox "Ingen källmapp vald." 7 50; return 1; }
    [[ -z "$PLAYLIST_NAME" ]] && PLAYLIST_NAME="$(basename "$SOURCE_DIR").m3u8"
    [[ -z "$SAVE_DIR" ]] && SAVE_DIR="$DEVICE_ROOT"

    if ! mkdir -p "$SAVE_DIR" 2>/dev/null; then
        dlg --msgbox "Kunde inte skapa sparmappen:\n$SAVE_DIR" 8 60
        return 1
    fi
    local out="$SAVE_DIR/$PLAYLIST_NAME"

    # bygg find-uttryck
    local -a find_args=()
    local first=1 ext
    for ext in $AUDIO_EXTS; do
        if [[ $first -eq 1 ]]; then find_args+=( -iname "*.$ext" ); first=0
        else find_args+=( -o -iname "*.$ext" ); fi
    done

    # samla filer (naturlig sortering, säker mot mellanslag/nyrader)
    local -a files=()
    mapfile -d '' -t files < <(find "$SOURCE_DIR" -type f \( "${find_args[@]}" \) -print0 | sort -zV)
    local total=${#files[@]}
    if [[ $total -eq 0 ]]; then
        dlg --msgbox "Inga ljudfiler hittades i:\n$SOURCE_DIR" 8 62
        return 1
    fi

    local mroot="${DEVICE_ROOT%/}"
    local tmp; tmp="$(mktemp)"

    # skriv playlist till fd 4 (-> tmp), skicka progress till stdout (-> gauge)
    {
        printf '#EXTM3U\n' >&4
        local n=0 f abs rel pct dur artist title
        for f in "${files[@]}"; do
            abs="$(readlink -f "$f")"
            rel="${abs#"$mroot"}"
            [[ "$rel" == /* ]] || rel="/$rel"

            if [[ $EXTINF -eq 1 ]]; then
                dur="$(ffprobe -v error -show_entries format=duration \
                       -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null || true)"
                dur="${dur%.*}"; dur="${dur:-0}"
                artist="$(ffprobe -v error -show_entries format_tags=artist \
                         -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null || true)"
                title="$(ffprobe -v error -show_entries format_tags=title \
                        -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null || true)"
                if [[ -n "$artist" && -n "$title" ]]; then
                    printf '#EXTINF:%s,%s - %s\n' "$dur" "$artist" "$title" >&4
                elif [[ -n "$title" ]]; then
                    printf '#EXTINF:%s,%s\n' "$dur" "$title" >&4
                else
                    printf '#EXTINF:%s,%s\n' "$dur" "$(basename "$f")" >&4
                fi
            fi

            printf '%s\n' "$rel" >&4

            n=$((n + 1))
            pct=$(( n * 100 / total ))
            printf 'XXX\n%d\n%s\nXXX\n' "$pct" "$n / $total spår"
        done
    } 4>"$tmp" | dialog --backtitle "$BT" --title "Arbetar" \
                        --gauge "Genererar spellista..." 8 76 0

    mv "$tmp" "$out"

    dlg --backtitle "$BT" --title "Klart" \
        --msgbox "$total spår skrevs till:\n$out\n\nKapat device-prefix:\n$mroot" 12 74
}

# --- huvudmeny --------------------------------------------------------------
main_menu() {
    while true; do
        local dr="${DEVICE_ROOT:-<ej vald>}"
        local sd="${SOURCE_DIR:-<ej vald>}"
        local pn="${PLAYLIST_NAME:-<auto>}"
        local sv="${SAVE_DIR:-<device-root>}"
        local ei; [[ $EXTINF -eq 1 ]] && ei="på" || ei="av"

        local choice
        choice="$(dlg --backtitle "$BT" --title "mkm3u8" --cancel-label "Avsluta" \
            --menu "Device-root : $dr\nKällmapp    : $sd\nSpellista   : $pn\nSpara i     : $sv\nEXTINF      : $ei" \
            20 78 6 \
            1 "Välj device-root" \
            2 "Välj mapp att katalogisera" \
            3 "Sätt spellistans namn" \
            4 "Välj var listan sparas" \
            5 "Växla EXTINF-metadata (nu: $ei)" \
            6 "»  Generera spellista")" || break

        case "$choice" in
            1) choose_device_root ;;
            2) [[ -z "$DEVICE_ROOT" ]] && { dlg --msgbox "Välj device-root först." 7 50; continue; }
               choose_source ;;
            3) choose_name ;;
            4) [[ -z "$DEVICE_ROOT" ]] && { dlg --msgbox "Välj device-root först." 7 50; continue; }
               choose_savedir ;;
            5) toggle_extinf ;;
            6) generate ;;
        esac
    done
    clear
}

main_menu