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