foxygit / mkm3u8 Log in
commit 44bf12cbc60e8231c60a0d60a551739505b812f1
Author:     MrJensK <jens.se@icloud.com>
AuthorDate: Wed Aug 12 19:50:01 2026 +0200
Commit:     MrJensK <jens.se@icloud.com>
CommitDate: Wed Aug 12 19:50:01 2026 +0200

    Initial commit
---
 mkm3u8-tui | 300 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 300 insertions(+)

diff --git a/mkm3u8-tui b/mkm3u8-tui
new file mode 100755
index 0000000..760e071
--- /dev/null
+++ b/mkm3u8-tui
@@ -0,0 +1,300 @@
+#!/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