Ctrl K
ComunidadGuías…

import_music.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash
#
# Aoede — Bulk Music Import
# ───────────────────────────
# Imports audio files from a host folder into the SHARED POOL only. Tracks
# become available to every user of the instance via search / library views.
# This script does NOT create playlists and does NOT assign tracks to any
# user; it only moves audio into /opt/saas-data/pool, registers each track
# in the DB with metadata, and optionally fetches missing covers via the
# remote APIs already configured by the admin in `Settings > Engine`.
#
# Usage:
#     ./import_music.sh /path/to/music [options]
#
# Options forwarded to the Python importer:
#     --enrich             use Spotify/Last.fm/MusicBrainz APIs for covers + metadata
#                          (uses the keys configured in Settings > Engine)
#     --dry-run            scan and report what would happen, change nothing
#     --workers N          concurrent API calls during enrichment (default 4)
#     --verbose            per-track debug output
#
# Examples:
#     ./import_music.sh /mnt/library
#     ./import_music.sh /mnt/library --enrich
#     ./import_music.sh /tmp/old-music --dry-run
#

set -euo pipefail

# ─── Locate the Compose project ──────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_DIR="$SCRIPT_DIR/Aoede"

if [[ ! -f "$COMPOSE_DIR/docker-compose.yaml" ]]; then
    echo "✗ docker-compose.yaml not found at $COMPOSE_DIR" >&2
    echo "  Run this script from the Aoede repository root." >&2
    exit 1
fi

if ! command -v docker >/dev/null 2>&1 || ! docker compose version >/dev/null 2>&1; then
    echo "✗ docker / docker compose not available on PATH" >&2
    exit 1
fi

# ─── Parse first positional argument: the source folder ──────────────────────
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
    sed -n '2,30p' "$0"
    exit 0
fi

SRC="$1"; shift
if [[ ! -d "$SRC" ]]; then
    echo "✗ source folder does not exist: $SRC" >&2
    exit 2
fi
SRC="$(cd "$SRC" && pwd)"

# ─── Verify the concierge container is up ────────────────────────────────────
cd "$COMPOSE_DIR"
if ! docker compose ps --status running --services 2>/dev/null | grep -qx concierge; then
    echo "✗ the 'concierge' container isn't running. Start the stack first:" >&2
    echo "    cd $COMPOSE_DIR && docker compose up -d" >&2
    exit 3
fi

# ─── Resolve the host path that the container sees as /saas-data/import_stage
DATA_ROOT="${HOST_DATA_ROOT:-/opt/saas-data}"
STAGE_HOST="$DATA_ROOT/import_stage"

if [[ ! -d "$STAGE_HOST" ]]; then
    echo "→ creating staging directory: $STAGE_HOST"
    sudo mkdir -p "$STAGE_HOST"
    sudo chmod 777 "$STAGE_HOST"
fi

# ─── Move audio files into the staging volume ────────────────────────────────
# Strategy: enumerate files with `find -iname` (case-insensitive,
# recursive, doesn't depend on shell globstar or rsync filter rules)
# and move each one preserving its relative subpath under $SRC.
#
# Why not rsync? We used rsync's --include filter previously, which
# DOES recurse but is case-sensitive — files like "Song.Mp3" slipped
# through. `find -iname` matches *.MP3, *.mp3, *.Mp3, *.mP3, etc.,
# eliminating an entire class of "no audio files found" reports.
echo "→ staging audio files from $SRC into $STAGE_HOST"

# Audio extensions we accept. Add new ones here — the find call below
# expands them with -iname so case is irrelevant.
EXTS=(
    mp3 m4a flac wav ogg opus aac wma
    aif aiff alac dsf dsd ape mka mp4
)

# Build `\( -iname '*.mp3' -o -iname '*.flac' -o ... \)` for find.
FIND_FILTER=( '(' )
first=1
for ext in "${EXTS[@]}"; do
    if [[ $first -eq 1 ]]; then
        FIND_FILTER+=( -iname "*.${ext}" )
        first=0
    else
        FIND_FILTER+=( -o -iname "*.${ext}" )
    fi
done
FIND_FILTER+=( ')' )

# Enumerate audio files NUL-separated to survive spaces/newlines in
# filenames. `-print0` + `read -d ''` is the canonical safe loop.
count=0
while IFS= read -r -d '' f; do
    rel="${f#$SRC/}"
    dest="$STAGE_HOST/$rel"
    sudo mkdir -p "$(dirname "$dest")"
    sudo mv "$f" "$dest"
    # `count=$((count+1))` instead of `((count++))` because the latter
    # exits with status 1 when the OLD value was 0, which under
    # `set -e` would abort the script on the very first file.
    count=$((count+1))
done < <(sudo find "$SRC" -type f "${FIND_FILTER[@]}" -print0)

# Clean up empty source directories left behind by the moves so the
# user's original tree ends up tidy (the importer also drops empty
# dirs in the staging volume on its way out).
sudo find "$SRC" -mindepth 1 -depth -type d -empty -delete 2>/dev/null || true

if [[ "$count" -eq 0 ]]; then
    echo "✗ no audio files found under $SRC" >&2
    exit 4
fi
echo "→ staged $count file(s)"

# ─── Make the staged files readable by `appuser` inside the container ───────
# We sudo'd the moves on the host, so the new files end up root-owned both
# on disk and (via the bind mount) inside the container. The importer
# below runs as `appuser` (UID 1000) — the same user as the running
# concierge — so it needs read access to the staged content. A single
# chown over the staging tree is the simplest fix.
sudo chown -R 1000:1000 "$STAGE_HOST" 2>/dev/null || true

# ─── Run the in-container importer ───────────────────────────────────────────
# Run as `appuser`, not the Dockerfile default (root). The running
# concierge process holds SQLite open as appuser; another process
# attempting to open the same WAL DB as a different UID (even root)
# can hit "unable to open database file" because fcntl locks are
# UID-scoped on some bind-mounted filesystems. Matching the UID
# eliminates that whole class of failure.
echo "→ running importer inside the concierge container (as appuser)"
echo
docker compose exec -T --user appuser concierge python importer.py \
    --source /saas-data/import_stage "$@"
RC=$?

# ─── Tidy up: remove now-empty staging directory contents ───────────────────
sudo find "$STAGE_HOST" -mindepth 1 -depth -empty -delete 2>/dev/null || true

exit $RC