commits
tags
#!/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