diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 7cf5a1b7e5c..2278aac9a11 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,15 +1,38 @@ -FROM mcr.microsoft.com/devcontainers/base:bookworm +FROM mcr.microsoft.com/devcontainers/base:trixie -# taken from: https://github.com/LMS-Community/slimserver-platforms/blob/public/9.1/Docker/Dockerfile +# Aligned with https://github.com/LMS-Community/slimserver-platforms/blob/public/9.2/Docker/Dockerfile # Set environment variables ENV DEBIAN_FRONTEND=noninteractive ENV LC_ALL="C.UTF-8" LANG="en_US.UTF-8" LANGUAGE="en_US.UTF-8" - +ENV HTTP_PORT=9000 # Install packages -RUN sudo apt-get update -qq && \ - sudo apt-get install --no-install-recommends -qy procps psmisc wget curl perl tzdata libcrypt-blowfish-perl libwww-perl libfont-freetype-perl liblinux-inotify2-perl \ - libdata-dump-perl libio-socket-ssl-perl libnet-ssleay-perl libcrypt-ssleay-perl libcrypt-openssl-rsa-perl libssl-dev libgomp1 libasound2 lame opus-tools && \ - sudo apt-get clean -qy && \ - sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -qy \ + # Lyrion runtime dependencies + procps psmisc wget curl perl tzdata libcrypt-blowfish-perl libwww-perl libfont-freetype-perl liblinux-inotify2-perl \ + libdata-dump-perl libcrypt-ssleay-perl libcrypt-openssl-rsa-perl libssl-dev libgomp1 libasound2 lame opus-tools \ + # Perl Language Server dependencies (for "richterger.perl" VS Code extension) + cpanminus \ + libanyevent-perl libclass-refresh-perl libcompiler-lexer-perl libdata-dump-perl libio-aio-perl libjson-perl libmoose-perl libpadwalker-perl \ + libscalar-list-utils-perl libcoro-perl && \ + apt-get clean -qy && \ + rm -rf /var/lib/apt/lists/* + +# Volume and port setup +# Permissions allow the non-root Dev Container user (vscode) to write here +# when no host folder is mounted (e.g. GitHub Codespaces). +RUN mkdir -p /music /playlist && \ + chmod 777 /music /playlist + +# Install Perl::LanguageServer (for "richterger.perl" VS Code extension) +RUN cpanm --notest --quiet Perl::LanguageServer + +# Re-install SSL packages after cpanm: cpanm may pull in or override Net::SSLeay +# with a CPAN version that doesn't work correctly on all platforms (e.g. ARM64). +# Installing via apt after cpanm ensures the distro-compiled packages win. +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y libio-socket-ssl-perl libnet-ssleay-perl && \ + apt-get clean -qy && \ + rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/clean-folders.sh b/.devcontainer/clean-folders.sh new file mode 100755 index 00000000000..98a83b159ac --- /dev/null +++ b/.devcontainer/clean-folders.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Cleans runtime-generated folders. + +set -euo pipefail + +readonly FORCE="${FORCE:-0}" +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Runtime-generated subdirectories (Cache, Logs, prefs) +readonly FOLDERS_TO_CLEAN=( + "$REPO_ROOT/prefs" + "$REPO_ROOT/Logs" + "$REPO_ROOT/Cache" +) + +if [[ "$FORCE" != "1" ]]; then + echo "[INFO] The following folders would be deleted:" + for folder in "${FOLDERS_TO_CLEAN[@]}"; do + if [[ -d "$folder" ]]; then + echo "[INFO] $folder/ (exists)" + else + echo "[INFO] $folder/ (not found, skipped)" + fi + done + echo "[INFO] Re-run with FORCE=1 to continue" + exit 0 +fi + +DELETED=0 +for folder in "${FOLDERS_TO_CLEAN[@]}"; do + if [[ -d "$folder" ]]; then + echo "[INFO] Deleting $folder/" + rm -rf "$folder" + DELETED=$((DELETED + 1)) + fi +done + +echo "[INFO] Cleanup complete ($DELETED folder(s) removed)" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 711cda1ce87..fd642af82b3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,36 +1,102 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/debian { - "name": "Lyrion Debian devcontainer", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - //"image": "mcr.microsoft.com/devcontainers/base:bookworm", - "dockerComposeFile": "docker-compose.yml", - "service": "lyrion-devcontainer", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 9000, - 3483, - 9090 - ], - "portsAttributes": { - "9000": { - "label": "web interface", - "protocol": "http" - }, - "3483": { - "label": "SlimProto" - }, - "9090": { - "label": "CLI" - } - }, - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "/workspaces/slimserver/.devcontainer/post-create.sh" - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "name": "Lyrion Debian", + + // You can use "Docker Compose file" or "Dockerfile" to build the container, but not both. + // ---------------------------------------------------- + // 1) Docker Compose file + "dockerComposeFile": "docker-compose.yml", + "service": "lyrion-devcontainer", + // ---------------------------------------------------- + // 2) Dockerfile + // Solves cross-drive path problem with Podman and podman compose on Windows: + // When Dev Containers adds features, it generates a temporary Dockerfile on the C: drive. When workspace is on D: drive, podman compose can't resolve relative paths across drives. + // "build": { + // "dockerfile": "Dockerfile", + // "context": "." + // }, + // "runArgs": [ + // "--network=lyrion_bridge", + // "--hostname=lyrion-devcontainer" + // ], + // ---------------------------------------------------- + + "containerEnv": { + "AUTO_START_LMS": "false" // Set to "true" to automatically start Lyrion Media Server when the container starts + // Optional startup variables (uncomment and adjust as needed): + // "LMS_EXTRA_ARGS": "--advertiseaddr=192.168.1.100" + }, + + // Mount host folders into the container. + // Uncomment and adjust the source paths to match your host system before (re)building the container. + // "mounts": [ + // { "type": "bind", "source": "/path/to/your/music", "target": "/music" }, + // { "type": "bind", "source": "/path/to/your/playlist", "target": "/playlist" } + // ], + + // Dev Containers mounts the opened repository under /workspaces/ by default. + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 9000, + 3483, + 9090 + ], + "portsAttributes": { + "9000": { + "label": "Web interface", + "protocol": "http" + }, + "3483": { + "label": "SlimProto" + }, + "9090": { + "label": "CLI" + } + }, + + // initializeCommand runs on the HOST before the container starts (Windows or Unix). + // bash must be available: Git Bash / WSL2 on Windows, or native bash on Unix. + "initializeCommand": "bash .devcontainer/initialize.sh", + + "postCreateCommand": "bash .devcontainer/post-create.sh", + + "postStartCommand": "bash .devcontainer/post-start.sh", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "richterger.perl" + ], + "settings": { + "perl.enable": true, // Set to false to disable the Perl extension and language server features + "perl.env": { + "PERL5OPT": "-Mlib=/workspaces/slimserver/.vscode -MPerlLanguageServerBootstrap", + "PERL5LIB": "/workspaces/slimserver:/workspaces/slimserver/lib" + }, + "perl.perlCmd": "/usr/bin/perl", + "perl.perlArgs": [ + "-Mlib=/workspaces/slimserver/.vscode", + "-MPerlLanguageServerBootstrap" + ], + "perl.logLevel": 0, + "perl.pathMap": [], + "perl.perlInc": [ + "/workspaces/slimserver/.vscode", + "/workspaces/slimserver", + "/workspaces/slimserver/lib" + ] + } + } + } + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 48ec338bdcc..50dc6c7febe 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,16 +1,18 @@ -version: '3.11' - services: lyrion-devcontainer: - # add this line so we can connect to the host machine + hostname: lyrion-devcontainer build: context: . dockerfile: Dockerfile volumes: - - lyrion-dev-volume:/workspace - + # In Compose mode, Dev Containers does not auto-mount the workspace — + # this explicit mount is required. + - ../..:/workspaces:cached # Overrides default command so things don't shut down after the process ends. command: sleep infinity + networks: + - lyrion_bridge -volumes: - lyrion-dev-volume: \ No newline at end of file +networks: + lyrion_bridge: + external: true diff --git a/.devcontainer/engine-utils.sh b/.devcontainer/engine-utils.sh new file mode 100755 index 00000000000..23bbd2f83cc --- /dev/null +++ b/.devcontainer/engine-utils.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Shared helpers to detect a container engine command usable from shell scripts. + +# Finds a container engine (podman or docker), preferring podman. +# Returns the command name or exits with error. +find_container_engine() { + # Native Linux/macOS shell lookup + if command -v podman >/dev/null 2>&1; then + echo "podman" + return 0 + fi + if command -v docker >/dev/null 2>&1; then + echo "docker" + return 0 + fi + + # Windows executables may only be visible as *.exe from Git Bash/WSL + if command -v podman.exe >/dev/null 2>&1; then + echo "podman.exe" + return 0 + fi + if command -v docker.exe >/dev/null 2>&1; then + echo "docker.exe" + return 0 + fi + + # Fallback via where.exe when command -v does not see Windows PATH entries + if command -v where.exe >/dev/null 2>&1; then + local resolved + resolved="$(where.exe podman 2>/dev/null | tr -d '\r' | head -n 1 || true)" + if [[ -n "$resolved" ]]; then + echo "$resolved" + return 0 + fi + resolved="$(where.exe docker 2>/dev/null | tr -d '\r' | head -n 1 || true)" + if [[ -n "$resolved" ]]; then + echo "$resolved" + return 0 + fi + fi + + return 1 +} + +# Finds a container engine that supports the 'compose' subcommand. +# On Windows / Git Bash the plain 'podman' may be a shim without compose support, +# while 'podman.exe' works correctly — this function tests each candidate in order. +find_compose_engine() { + local candidates=() + + # Build the candidate list: prefer plain names, then .exe variants + for name in podman docker; do + command -v "$name" >/dev/null 2>&1 && candidates+=("$name") + command -v "$name.exe" >/dev/null 2>&1 && candidates+=("$name.exe") + done + + # where.exe fallback for Git Bash / MSYS environments + if command -v where.exe >/dev/null 2>&1; then + for name in podman docker; do + local resolved + resolved="$(where.exe "$name" 2>/dev/null | tr -d '\r' | head -n 1 || true)" + [[ -n "$resolved" ]] && candidates+=("$resolved") + done + fi + + for candidate in "${candidates[@]}"; do + if "$candidate" compose version >/dev/null 2>&1; then + echo "$candidate" + return 0 + fi + done + + return 1 +} diff --git a/.devcontainer/initialize.sh b/.devcontainer/initialize.sh new file mode 100755 index 00000000000..75718c6fb39 --- /dev/null +++ b/.devcontainer/initialize.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Creates the shared network for the Dev Container and SoftSqueeze. + +set -euo pipefail + +readonly NETWORK_NAME="lyrion_bridge" +ENGINE="" + +source "$(dirname "$0")/engine-utils.sh" + +ENGINE="$(find_container_engine || true)" + +if [[ -z "$ENGINE" ]]; then + echo "[ERROR] Missing container engine (podman or docker)." >&2 + echo "[ERROR] Make sure it is installed and available in the shell PATH used by initializeCommand." >&2 + exit 1 +fi + +echo "[INFO] Using container engine: $ENGINE" + +if "$ENGINE" network inspect "$NETWORK_NAME" >/dev/null 2>&1; then + echo "[INFO] Network '$NETWORK_NAME' already exists, skipping creation." + exit 0 +fi + +echo "[INFO] Creating network '$NETWORK_NAME'..." +if "$ENGINE" network create "$NETWORK_NAME" --driver=bridge; then + echo "[INFO] Network '$NETWORK_NAME' created successfully." +else + echo "[ERROR] Failed to create network '$NETWORK_NAME'." >&2 + exit 1 +fi diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 9ec2d65a4a9..76080d4b122 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,21 +1,36 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +# Downloads Slim/Utils/OS/Custom.pm from slimserver-platforms. -# get current git branch -BRANCH="$(git rev-parse --abbrev-ref HEAD)" +set -euo pipefail -URL_BRANCH="https://raw.githubusercontent.com/LMS-Community/slimserver-platforms/${BRANCH}/Docker/Slim-Utils-OS-Custom.pm" -URL_HEAD="https://raw.githubusercontent.com/LMS-Community/slimserver-platforms/HEAD/Docker/Slim-Utils-OS-Custom.pm" -TARGET_FILE="/workspaces/slimserver/Slim/Utils/OS/Custom.pm" +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +BRANCH="$(git -c safe.directory="$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)" +BASE_URL="https://raw.githubusercontent.com/LMS-Community/slimserver-platforms" +TARGET_FILE="$REPO_ROOT/Slim/Utils/OS/Custom.pm" -echo "Detected branch: ${BRANCH}" -echo "Trying branch-specific file..." +echo "[INFO] Post-create setup" +git config --global --add safe.directory "$REPO_ROOT" 2>/dev/null || true -# Try downloading branch version -if curl -fSL "$URL_BRANCH" -o "$TARGET_FILE"; then - echo "Downloaded branch version from: $URL_BRANCH" +# Create runtime directories owned by the container user to avoid permission +# issues on macOS/Linux hosts where the workspace bind mount may have a +# different UID than the container user. +mkdir -p "$REPO_ROOT/Cache" "$REPO_ROOT/Logs" "$REPO_ROOT/prefs" + +# Ensure all devcontainer scripts are executable. Git tracks the bit, but some +# host/client combinations (Windows checkout, certain clone modes) lose it. +# New scripts added to .devcontainer/ must also have the bit set in git: +# git update-index --chmod=+x .devcontainer/your-script.sh +find "$REPO_ROOT/.devcontainer" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true + +mkdir -p "$(dirname "$TARGET_FILE")" + +echo "[INFO] Downloading Custom.pm for branch: $BRANCH" +if curl -fsSL "${BASE_URL}/${BRANCH}/Docker/Slim-Utils-OS-Custom.pm" -o "$TARGET_FILE" 2>/dev/null; then + echo "[INFO] Downloaded from branch '$BRANCH'" +elif curl -fsSL "${BASE_URL}/HEAD/Docker/Slim-Utils-OS-Custom.pm" -o "$TARGET_FILE" 2>/dev/null; then + echo "[INFO] Downloaded from fallback 'HEAD'" else - echo "Branch file not found, falling back to HEAD..." - curl -fSL "$URL_HEAD" -o "$TARGET_FILE" - echo "Downloaded latest version from: $URL_HEAD" -fi \ No newline at end of file + echo "[ERROR] Failed to download Custom.pm" >&2 + exit 1 +fi + diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100755 index 00000000000..fc5c872efbd --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Starts LMS when AUTO_START_LMS=true. + +set -euo pipefail + +readonly AUTO_START_LMS="${AUTO_START_LMS:-false}" +readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +if [[ "$AUTO_START_LMS" != "true" ]]; then + echo "[INFO] AUTO_START_LMS=false, skipping LMS startup." + exit 0 +fi + +exec bash "$SCRIPT_DIR/start-lyrion.sh" diff --git a/.devcontainer/softsqueeze/Dockerfile b/.devcontainer/softsqueeze/Dockerfile new file mode 100644 index 00000000000..15c91a0b427 --- /dev/null +++ b/.devcontainer/softsqueeze/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:trixie-slim + +# SoftSqueeze + VNC standalone image +# Connects to LMS running in the Dev Container (or anywhere reachable) + +ENV DEBIAN_FRONTEND=noninteractive +ENV LC_ALL="C.UTF-8" LANG="en_US.UTF-8" LANGUAGE="en_US.UTF-8" +ENV DISPLAY=:99 + +# Install Java, VNC, audio and SoftSqueeze packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -qy \ + curl ca-certificates unzip procps iproute2 \ + default-jre xvfb x11vnc fluxbox novnc websockify xdg-utils \ + pulseaudio pulseaudio-utils libasound2-plugins alsa-utils && \ + mkdir -p /opt/softsqueeze && \ + curl -fsSL https://sourceforge.net/projects/lmsclients/files/softsqueeze/softsqueeze_3.9.3.zip/download -o /tmp/softsqueeze.zip && \ + unzip -q /tmp/softsqueeze.zip -d /opt/softsqueeze && \ + rm /tmp/softsqueeze.zip && \ + apt-get clean -qy && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +EXPOSE 5900 6080 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/.devcontainer/softsqueeze/SOFTSQUEEZE.md b/.devcontainer/softsqueeze/SOFTSQUEEZE.md new file mode 100644 index 00000000000..3bd62f590dc --- /dev/null +++ b/.devcontainer/softsqueeze/SOFTSQUEEZE.md @@ -0,0 +1,111 @@ +# SoftSqueeze — Standalone Container + +[SoftSqueeze](https://lyrion.org/players-and-controllers/softsqueeze/) + VNC runs as a separate optional container and connects to LMS running in the Dev Container. +LMS works normally even when SoftSqueeze is not running. + +> [!NOTE] +> This container is intended primarily for **display simulation** of the SoftSqueeze player. +> +> **Audio is not supported** in this container. SoftSqueeze acts only as a visual client for LMS. +> Container audio uses PulseAudio with a null sink to avoid Java mixer errors and allow player registration. + +## Prerequisites + +- LMS is running (default HTTP port `9000`). +- Podman or Docker is installed on the host. +- Shared network `lyrion_bridge` exists (created by [`.devcontainer/initialize.sh`](../initialize.sh)). + +## Quick Start + +1. Make sure LMS is running. +2. Start SoftSqueeze from your host terminal (not inside the Dev Container) [^1]: + ```bash + bash .devcontainer/softsqueeze/softsqueeze.sh start + ``` + +[^1]: You can also run Compose directly: + + ```bash + podman compose -f .devcontainer/softsqueeze/docker-compose.yml up -d + podman compose -f .devcontainer/softsqueeze/docker-compose.yml down + # or + docker compose -f .devcontainer/softsqueeze/docker-compose.yml up -d + docker compose -f .devcontainer/softsqueeze/docker-compose.yml down + ``` + +3. Open noVNC in your browser [http://localhost:6080/vnc.html](http://localhost:6080/vnc.html) and connect to the VNC server. + +4. SoftSqueeze should appear in VNC and register in LMS as a player. + +## Management + +```bash +# Start (build + run) +bash .devcontainer/softsqueeze/softsqueeze.sh start + +# Stop + remove +bash .devcontainer/softsqueeze/softsqueeze.sh stop + +# Show status +bash .devcontainer/softsqueeze/softsqueeze.sh status + +# Tail logs +bash .devcontainer/softsqueeze/softsqueeze.sh logs +``` + +The launcher auto-detects Podman or Docker. + +## Connection Defaults + +| Environment Variable | Default | Description | +| -------------------- | --------------------- | ----------------------------------------------- | +| `LMS_HOST` | `lyrion-devcontainer` | LMS hostname in shared `lyrion_bridge` network. | +| `LMS_HTTP_PORT` | `9000` | LMS HTTP port | +| `LMS_SLIMPROTO_PORT` | `3483` | SlimProto port | + +Override via environment variables: + +```bash +LMS_HOST=192.168.1.100 bash .devcontainer/softsqueeze/softsqueeze.sh start +``` + +Or edit the environment section in [`.devcontainer/softsqueeze/docker-compose.yml`](docker-compose.yml). + +> [!TIP] +> SoftSqueeze is not limited to the Dev Container's LMS instance. You can connect it to **any Lyrion Music Server** reachable over the network by setting `LMS_HOST` to the external server's IP address or hostname. + +> [!TIP] +> If SoftSqueeze cannot connect after changing host settings, set host back to: +> +> ```bash +> LMS_HOST=lyrion-devcontainer bash .devcontainer/softsqueeze/softsqueeze.sh start +> ``` + +## How It Works + +- The SoftSqueeze container runs Xvfb + fluxbox + x11vnc + noVNC + PulseAudio + SoftSqueeze. +- Exposes port `6080` (noVNC web) and `5900` (VNC). + +## Troubleshooting + +### SoftSqueeze Cannot Connect to LMS + +1. Confirm LMS port `9000` is accessible from host: + +```bash +curl -sS http://localhost:9000/ | head -c 100 +``` + +2. Verify shared network exists: + +```bash +podman network inspect lyrion_bridge +# or +docker network inspect lyrion_bridge +``` + +3. Check SoftSqueeze logs: + +```bash +bash .devcontainer/softsqueeze/softsqueeze.sh logs +``` diff --git a/.devcontainer/softsqueeze/docker-compose.yml b/.devcontainer/softsqueeze/docker-compose.yml new file mode 100644 index 00000000000..c5d69d204c3 --- /dev/null +++ b/.devcontainer/softsqueeze/docker-compose.yml @@ -0,0 +1,22 @@ +# SoftSqueeze + VNC standalone container. + +services: + softsqueeze: + build: + context: . + dockerfile: Dockerfile + container_name: lyrion-softsqueeze + environment: + - LMS_HOST=${LMS_HOST:-lyrion-devcontainer} + - LMS_HTTP_PORT=9000 + - LMS_SLIMPROTO_PORT=3483 + ports: + - "6080:6080" + - "5900:5900" + restart: unless-stopped + networks: + - lyrion_bridge + +networks: + lyrion_bridge: + external: true diff --git a/.devcontainer/softsqueeze/entrypoint.sh b/.devcontainer/softsqueeze/entrypoint.sh new file mode 100755 index 00000000000..f351c67584e --- /dev/null +++ b/.devcontainer/softsqueeze/entrypoint.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Entrypoint for standalone SoftSqueeze + VNC container. +# Starts Xvfb, fluxbox, x11vnc, noVNC, PulseAudio and SoftSqueeze. + +set -euo pipefail + +export DISPLAY=:99 + +LMS_HOST="${LMS_HOST:-lyrion-devcontainer}" +LMS_HTTP_PORT="${LMS_HTTP_PORT:-9000}" +LMS_SLIMPROTO_PORT="${LMS_SLIMPROTO_PORT:-3483}" +SOFTSQUEEZE_HOME="${SOFTSQUEEZE_HOME:-/opt/softsqueeze}" +SOFTSQUEEZE_PREFS_DIR="${SOFTSQUEEZE_PREFS_DIR:-/root/.softsqueeze-prefs}" +SOFTSQUEEZE_JAVA_OPTS="${SOFTSQUEEZE_JAVA_OPTS:--Xms32m -Xmx128m}" + +start_if_missing() { + local pattern="$1" + shift + if ! pgrep -f "$pattern" >/dev/null 2>&1; then + "$@" & + fi +} + +# PulseAudio +pulseaudio --check >/dev/null 2>&1 || pulseaudio --start --exit-idle-time=-1 >/dev/null 2>&1 || true + +if command -v pactl >/dev/null 2>&1; then + sleep 1 + if ! pactl list short sinks 2>/dev/null | grep -q softsqueeze_sink; then + pactl load-module module-null-sink sink_name=softsqueeze_sink \ + sink_properties=device.description=SoftSqueezeSink >/dev/null 2>&1 || true + fi + pactl set-default-sink softsqueeze_sink >/dev/null 2>&1 || true +fi + +cat > /root/.asoundrc <<'EOF' +pcm.!default { + type pulse +} +ctl.!default { + type pulse +} +EOF + +# VNC stack +echo "[INFO] Starting Xvfb" +start_if_missing "Xvfb :99" Xvfb :99 -screen 0 1280x1024x24 +sleep 2 + +echo "[INFO] Starting fluxbox" +start_if_missing "fluxbox" fluxbox + +echo "[INFO] Starting x11vnc" +start_if_missing "x11vnc -display :99" x11vnc -display :99 -forever -shared -nopw -ncache 10 -ncache_cr + +if [[ -x /usr/share/novnc/utils/novnc_proxy ]]; then + echo "[INFO] Starting noVNC" + start_if_missing "novnc_proxy --vnc localhost:5900 --listen 6080" \ + /usr/share/novnc/utils/novnc_proxy --vnc localhost:5900 --listen 6080 +fi + +for ((i = 1; i <= 30; i++)); do + if ss -ltn | grep -q ':6080'; then + echo "[INFO] noVNC ready on port 6080" + break + fi + sleep 1 +done + +# SoftSqueeze +mkdir -p "$SOFTSQUEEZE_PREFS_DIR" + +echo "[INFO] LMS target: ${LMS_HOST}:${LMS_HTTP_PORT}" + +cd "$SOFTSQUEEZE_HOME" + +echo "[INFO] Starting SoftSqueeze" +read -r -a JAVA_OPTS <<< "$SOFTSQUEEZE_JAVA_OPTS" + +java \ + "${JAVA_OPTS[@]}" \ + -Djava.util.prefs.userRoot="$SOFTSQUEEZE_PREFS_DIR" \ + -Dslimserver="$LMS_HOST" \ + -Dhttpport="$LMS_HTTP_PORT" \ + -Dslimport="$LMS_SLIMPROTO_PORT" \ + -Dcom.Ostermiller.util.Browser.open="xdg-open {0}" \ + -jar SoftSqueeze.jar \ + 2>&1 | tee /tmp/softsqueeze.log & + +SQUEEZE_PID=$! + +echo "[INFO] SoftSqueeze PID: $SQUEEZE_PID" +echo "[INFO] noVNC: http://localhost:6080/vnc.html" + +wait "$SQUEEZE_PID" || true + +echo "[WARN] SoftSqueeze process exited. Keeping container alive for noVNC access." +exec tail -f /dev/null diff --git a/.devcontainer/softsqueeze/softsqueeze.sh b/.devcontainer/softsqueeze/softsqueeze.sh new file mode 100755 index 00000000000..8f81b2cce2e --- /dev/null +++ b/.devcontainer/softsqueeze/softsqueeze.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Manage SoftSqueeze + VNC standalone container. + +set -euo pipefail + +readonly SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +COMPOSE_FILE_FOR_ENGINE="$COMPOSE_FILE" +ENGINE="" + +source "$SCRIPT_DIR/../engine-utils.sh" + +log_info() { + echo "[INFO] $*" +} + +log_error() { + echo "[ERROR] $*" >&2 +} + +compose_run() { + "$ENGINE" compose "$@" +} + +ENGINE="$(find_compose_engine || true)" + +if [[ -z "$ENGINE" ]]; then + log_error "No container engine with 'compose' support found (podman or docker)." + log_error "On Windows / Git Bash try running the script directly (without 'bash' prefix)." + exit 1 +fi + +log_info "Using container engine: $ENGINE" + +if [[ "$ENGINE" == *.exe ]]; then + if command -v cygpath >/dev/null 2>&1; then + COMPOSE_FILE_FOR_ENGINE="$(cygpath -w "$COMPOSE_FILE")" + elif command -v wslpath >/dev/null 2>&1; then + COMPOSE_FILE_FOR_ENGINE="$(wslpath -w "$COMPOSE_FILE")" + fi +fi + +# Default to Dev Container hostname on shared lyrion_bridge network. +export LMS_HOST="${LMS_HOST:-lyrion-devcontainer}" + +case "${1:-start}" in + start) + log_info "Building and starting SoftSqueeze container..." + if compose_run -f "$COMPOSE_FILE_FOR_ENGINE" up -d --build; then + log_info "SoftSqueeze started successfully." + log_info "Access points:" + log_info " noVNC: http://localhost:6080/vnc.html" + log_info " VNC: localhost:5900" + log_info "Stop with: $SCRIPT_NAME stop" + else + log_error "Failed to start SoftSqueeze." + exit 1 + fi + ;; + stop) + log_info "Stopping SoftSqueeze container..." + if compose_run -f "$COMPOSE_FILE_FOR_ENGINE" down; then + log_info "SoftSqueeze stopped." + else + log_error "Failed to stop SoftSqueeze." + exit 1 + fi + ;; + status) + log_info "SoftSqueeze container status:" + compose_run -f "$COMPOSE_FILE_FOR_ENGINE" ps + ;; + logs) + log_info "Tailing SoftSqueeze logs (Ctrl+C to exit)..." + compose_run -f "$COMPOSE_FILE_FOR_ENGINE" logs -f + ;; + *) + log_error "Invalid command: ${1:-start}" + log_info "Usage: $SCRIPT_NAME {start|stop|status|logs}" + exit 1 + ;; +esac diff --git a/.devcontainer/start-lyrion.sh b/.devcontainer/start-lyrion.sh new file mode 100755 index 00000000000..f04ac0e5213 --- /dev/null +++ b/.devcontainer/start-lyrion.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Starts LMS. + +set -euo pipefail + +readonly REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +readonly HTTP_PORT="${LMS_HTTP_PORT:-9000}" +readonly LMS_WAIT_TIMEOUT="${LMS_WAIT_TIMEOUT:-90}" +readonly LOGFILE="/tmp/devcontainer-lms.log" +readonly LMS_EXTRA_ARGS="${LMS_EXTRA_ARGS:-${EXTRA_ARGS:-}}" + +is_lms_running() { + pgrep -f 'perl .*slimserver\.pl' >/dev/null 2>&1 +} + +if is_lms_running; then + echo "[INFO] LMS is already running." + exit 0 +fi + +mkdir -p "$REPO_ROOT/Cache" "$REPO_ROOT/Logs" "$REPO_ROOT/prefs" + +declare -a extra_args=() +if [[ -n "$LMS_EXTRA_ARGS" ]]; then + read -r -a extra_args <<< "$LMS_EXTRA_ARGS" +fi + +echo "[INFO] Starting LMS on port $HTTP_PORT" +nohup perl "$REPO_ROOT/slimserver.pl" \ + --cachedir "$REPO_ROOT/Cache" \ + --logdir "$REPO_ROOT/Logs" \ + --prefsdir "$REPO_ROOT/prefs" \ + --httpport "$HTTP_PORT" \ + "${extra_args[@]}" \ + >"$LOGFILE" 2>&1 & + +for _ in $(seq 1 "$LMS_WAIT_TIMEOUT"); do + if curl -fsS "http://127.0.0.1:${HTTP_PORT}/" >/dev/null 2>&1; then + echo "[INFO] LMS reachable at http://127.0.0.1:$HTTP_PORT" + echo "[INFO] Log: $LOGFILE" + exit 0 + fi + sleep 1 +done + +echo "[WARN] LMS did not become reachable in ${LMS_WAIT_TIMEOUT}s" >&2 +echo "[WARN] Check log: $LOGFILE" >&2 +exit 1 diff --git a/.devcontainer/stop-lyrion.sh b/.devcontainer/stop-lyrion.sh new file mode 100755 index 00000000000..b02cae24522 --- /dev/null +++ b/.devcontainer/stop-lyrion.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Stops running LMS process. + +set -euo pipefail + +readonly FORCE="${FORCE:-0}" + +if ! pgrep -f 'perl .*slimserver\.pl' >/dev/null 2>&1; then + echo "[INFO] LMS is not running." + exit 0 +fi + +if [[ "$FORCE" == "1" ]]; then + echo "[INFO] Stopping LMS (SIGKILL)" + pkill -9 -f 'perl .*slimserver\.pl' || true +else + echo "[INFO] Stopping LMS (SIGTERM)" + pkill -15 -f 'perl .*slimserver\.pl' || true + sleep 2 + if pgrep -f 'perl .*slimserver\.pl' >/dev/null 2>&1; then + echo "[WARN] LMS still running, forcing SIGKILL" + pkill -9 -f 'perl .*slimserver\.pl' || true + fi +fi + +if pgrep -f 'perl .*slimserver\.pl' >/dev/null 2>&1; then + echo "[ERROR] Failed to stop LMS" >&2 + exit 1 +fi + +echo "[INFO] LMS stopped" diff --git a/.editorconfig b/.editorconfig index b351693fc5a..5e7067ff8c0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,33 @@ root = true -[*.{html,p[lm]}] -indent_style = tab -indent_size = 8 +[*] +charset = utf-8 +end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true + +[*.{md,yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{sh,js,json,css,html,p[lm]}] +indent_style = tab +indent_size = 4 + +# For "special" HTML files (e.g., linux-update.html.de, remotestreaming.html.de) +[*.html.*] +indent_style = tab +indent_size = 4 + +[**/Dockerfile] +indent_style = space +indent_size = 4 + +[{extensions.json,devcontainer.json,launch.json}] +indent_style = space +indent_size = 4 +insert_final_newline = false + +# Can contain trailing whitespace for some languages +[strings.txt] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..b5ec54a8fec --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# Source files are always stored with LF line endings so that they work +# correctly inside Linux containers regardless of the developer's host OS. + +# Shell scripts +*.sh text eol=lf + +# Perl source +*.pl text eol=lf +*.pm text eol=lf + +# LMS locale strings (parsed by Linux server — CRLF causes parse errors) +strings.txt text eol=lf + +# Web assets +*.js text eol=lf +*.css text eol=lf +*.html text eol=lf +*.xml text eol=lf +*.html.* text eol=lf + +# Config / documentation +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf +*.txt text eol=lf + +# Third-party code — preserve as committed, no line-ending conversion +CPAN/** -text diff --git a/.gitignore b/.gitignore index 7b198c53e5f..15323a8de2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ -# Ignore custom OS module as this is installed in devcontainer and not part of this repo +# Ignore VSCode Perl Language Server settings +.vscode/perl-lang/* + +# Ignore VSCode settings file +.vscode/settings.json + +# Ignore custom OS module as this is installed in Dev Container and not part of this repo Slim/Utils/OS/Custom.pm -# Ignore cache, logs and prefs files +# Ignore cache, logs, prefs, and plugins directories Cache/* Logs/* prefs/* diff --git a/.vscode/PerlLanguageServerBootstrap.pm b/.vscode/PerlLanguageServerBootstrap.pm new file mode 100644 index 00000000000..97789b0a155 --- /dev/null +++ b/.vscode/PerlLanguageServerBootstrap.pm @@ -0,0 +1,218 @@ +package PerlLanguageServerBootstrap; + +# Bootstrap for the Perl Language Server and VS Code debug adapter. +# +# For Language Server (static analysis / syntax checking): +# Replicates the two-pass module loading strategy from Slim::bootstrap::loadModules(): +# 1. Prepend CPAN (incl. arch-specific) paths to @INC +# 2. Try loading known XS modules from bundled CPAN +# 3. If any fail (XS version mismatch), remove CPAN from @INC, retry from system Perl +# 4. Put CPAN paths back so pure-Perl modules (File::Slurp, etc.) remain reachable +# +# For debug adapter (slimserver.pl under -d): +# Slim::bootstrap::loadModules() handles the full two-pass @INC setup itself. +# We only define constants here. CPAN must NOT be in PERL5LIB for debug launches, +# otherwise PERL5DB's DebuggerInterface finds CPAN/JSON/XS.pm (v2.34) early but +# XSLoader picks up the system .so (v2.3) first — version mismatch kills the load, +# and Slim::bootstrap can never recover it. Without CPAN in PERL5LIB, DebuggerInterface +# falls back to JSON::PP harmlessly, and Slim::bootstrap's unshift puts arch paths +# first in @INC so .pm and .so versions match. + +use strict; +use Config; +use Cwd qw(abs_path); +use File::Basename qw(dirname); +use File::Spec::Functions qw(catdir rel2abs); +use Symbol; + +# VS Code Debug Adapter sends SIGINT to pause the debuggee. Slim::bootstrap installs +# custom INT/TERM/QUIT handlers inside loadModules(), which is called from slimserver.pl's +# BEGIN block. This INIT block runs AFTER all BEGIN blocks, so it safely overrides +# those handlers before the main program starts. Use DB::catch for INT (if available), +# otherwise Perl debugger pause/continue can get stuck. +INIT { + if (($ENV{LMS_VSCODE_DEBUG} || '') eq '1') { + if (defined &DB::catch) { + $SIG{'INT'} = \&DB::catch; + } + else { + $SIG{'INT'} = 'DEFAULT'; + } + $SIG{'TERM'} = 'DEFAULT'; + $SIG{'QUIT'} = 'DEFAULT'; + } +} + +# XS modules known to ship in CPAN/arch — same list as Slim::bootstrap's +# @default_required_modules, plus IO::AIO which the Language Server pulls via Coro::AIO. +my @xs_modules = qw(version Time::HiRes DBI EV XML::Parser::Expat HTML::Parser JSON::XS Digest::SHA1 YAML::XS Sub::Name IO::AIO); + +# Guard against double-import (e.g. -MPerlLanguageServerBootstrap on command line AND in PERL5OPT). +my $imported = 0; + +# Normalize arch name and build @SlimINC — same logic as Slim::bootstrap::loadModules(). +# Returns the list of paths (not all may exist on disk). +sub _buildSlimINC { + my ($repoRoot) = @_; + + my $arch = $Config::Config{'archname'}; + $arch =~ s/^i[3456]86-/i386-/; + $arch =~ s/gnu-//; + my $is64bitint = $arch =~ /64int/; + if ($arch =~ /^aarch64.*linux/) { + $arch = 'aarch64-linux-thread-multi'; + } + elsif ($arch =~ /^arm.*linux/) { + $arch = $arch =~ /gnueabihf/ + ? 'arm-linux-gnueabihf-thread-multi' + : 'arm-linux-gnueabi-thread-multi'; + $arch .= '-64int' if $is64bitint; + } + if ($arch =~ /^(?:ppc|powerpc).*linux/) { + $arch = 'powerpc-linux-thread-multi'; + $arch .= '-64int' if $is64bitint; + } + + my $perlmajorversion = $Config{'version'}; + $perlmajorversion =~ s/\.\d+$//; + + return ( + catdir($repoRoot, 'CPAN', 'arch', $perlmajorversion, $arch), + catdir($repoRoot, 'CPAN', 'arch', $perlmajorversion, $arch, 'auto'), + catdir($repoRoot, 'CPAN', 'arch', $Config{'version'}, $Config::Config{'archname'}), + catdir($repoRoot, 'CPAN', 'arch', $Config{'version'}, $Config::Config{'archname'}, 'auto'), + catdir($repoRoot, 'CPAN', 'arch', $perlmajorversion, $Config::Config{'archname'}), + catdir($repoRoot, 'CPAN', 'arch', $perlmajorversion, $Config::Config{'archname'}, 'auto'), + catdir($repoRoot, 'CPAN', 'arch', $Config::Config{'archname'}), + catdir($repoRoot, 'CPAN', 'arch', $perlmajorversion), + catdir($repoRoot, 'lib'), + catdir($repoRoot, 'CPAN'), + $repoRoot, + ); +} + +sub import { + return if $imported; + $imported = 1; + + no strict 'refs'; + + my $bootstrapDir = dirname(__FILE__); + my $repoRoot = abs_path(rel2abs(catdir($bootstrapDir, '..'))) || rel2abs(catdir($bootstrapDir, '..')); + my $isSlimserverRun = (($0 || '') =~ m{(?:^|[\\/])slimserver\.pl$}i) ? 1 : 0; + my %constants = ( + SCANNER => 0, + RESIZER => 0, + ISWINDOWS => 0, + ISMAC => 0, + TRANSCODING => 1, + PERFMON => 0, + DEBUGLOG => 1, + INFOLOG => 1, + STATISTICS => 1, + SB1SLIMP3SYNC => 1, + WEBUI => 1, + NOMYSB => 1, + LOCALFILE => 0, + NOBROWSECACHE => 0, + SLIM_SERVICE => 0, + NOUPNP => 0, + ISACTIVEPERL => 0, + ); + + # Needed by some modules during static analysis, but conflicts with slimserver.pl runtime sub declaration. + $constants{HAS_AIO} = 0 if !$isSlimserverRun; + + for my $name (keys %constants) { + *{"main::$name"} = sub () { $constants{$name} } unless defined &{"main::$name"}; + } + + $main::VERSION ||= '9.2.0'; + @main::argv = @ARGV unless @main::argv; + + if (!$isSlimserverRun) { + # Language Server / static analysis: full two-pass XS loading. + my @SlimINC = _buildSlimINC($repoRoot); + + # Remove any bare CPAN path that crept in via -I flags (perlInc) or PERL5LIB + # BEFORE we do the controlled two-pass load. + my $cpanDir = catdir($repoRoot, 'CPAN'); + @INC = grep { $_ ne $cpanDir } @INC; + + # Prepend our paths (like 'use lib') + unshift @INC, @SlimINC; + + # --- Two-pass XS module loading (Slim::bootstrap strategy) --- + # Pass 1: try loading from bundled CPAN (arch-specific XS binaries) + my @failed = _tryModuleLoad(@xs_modules); + + if (@failed) { + # Pass 2: remove our CPAN paths from @INC so we try system Perl only + splice(@INC, 0, scalar @SlimINC); + _tryModuleLoad(@failed); + # Put CPAN paths back so pure-Perl modules remain reachable + unshift @INC, @SlimINC; + } + + # Perl::LanguageServer expects AnyEvent::CondVar->new, but LMS ships AnyEvent 5.x. + eval { + require AnyEvent; + if (!AnyEvent::CondVar->can('new')) { + no warnings 'redefine'; + *AnyEvent::CondVar::new = sub { + shift; + return AnyEvent->condvar(@_); + }; + } + }; + + my $cacheDir = catdir($repoRoot, 'Cache'); + $main::cachedir = $cacheDir unless defined $main::cachedir; + $main::tmpdir = catdir($cacheDir, 'tmp') unless defined $main::tmpdir; + + eval { + require Slim::Utils::OSDetect; + Slim::Utils::OSDetect::init(); + }; + } +} + +# Two-pass module loader — adapted from Slim::bootstrap::tryModuleLoad(). +# Tries to 'use' each module; on failure, cleans up the symbol table and %INC +# so the module can be retried from a different @INC in the next pass. +sub _tryModuleLoad { + my @modules = @_; + my @failed; + + for my $module (@modules) { + next unless $module; + + my %oldINC = %INC; + + local $^W = 0; # suppress redefined warnings + eval "use $module ()"; + + if ($@) { + push @failed, $module; + + # Clean up partially-loaded symbols (same logic as Slim::bootstrap). + # Note: IO(?!/AIO) skips core IO modules but NOT IO::AIO so it can be + # retried from system Perl in pass 2. + for my $newModule (grep { !$oldINC{$_} } keys %INC) { + next if $newModule =~ /^(?:AutoLoader|DynaLoader|XSLoader|Carp|overload|IO(?!\/AIO)|Fcntl|Socket|FileHandle|SelectSaver)/; + + my $symbol = $newModule; + $symbol =~ s|/|::|g; + $symbol =~ s|\.pm$||; + + if (eval { Symbol::delete_package($symbol) }) { + delete $INC{$newModule}; + } + } + } + } + + return @failed; +} + +1; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..1df2bd44a32 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "richterger.perl", + "ms-vscode-remote.remote-containers" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..8602849d74a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "perl", + "request": "launch", + "name": "LMS: Debug slimserver.pl", + "program": "${workspaceFolder}/slimserver.pl", + "cwd": "${workspaceFolder}", + "args": [ + "--httpport", "9000", + "--cachedir", "${workspaceFolder}/Cache", + "--logdir", "${workspaceFolder}/Logs", + "--prefsdir", "${workspaceFolder}/prefs", + // "--d_startup" // Uncomment for more verbose module loading + ], + "env": { + "LMS_VSCODE_DEBUG": "1", + "PERL5OPT": "-Mlib=${workspaceFolder}/.vscode -MPerlLanguageServerBootstrap", + "PERL5LIB": "${workspaceFolder}:${workspaceFolder}/lib" + }, + "stopOnEntry": false, + "reloadModules": true + } + ] +} diff --git a/DEVCONTAINERS.md b/DEVCONTAINERS.md index 16854d2ca5c..a3d0060d0f2 100644 --- a/DEVCONTAINERS.md +++ b/DEVCONTAINERS.md @@ -1,18 +1,206 @@ # Dev Container Environment -You can run the project either entirely in the cloud using GitHub Codespaces or locally using VS Code with Dev Containers. Both methods provide a fully configured environment without requiring manual setup on your machine. - -## Development Modes - -### 1. GitHub Codespaces -- In Github on the Code tab click the **Code Button** → **Codespaces** → **Create Codespace**. -- Wait for the browser-based VS Code to start. -- Run `slimserver` in the terminal. -- Open the **Ports** tab and click the forwarded port to access the SlimServer web UI. -- Note: Codespaces includes free-tier limitations. - -### 2. Local VS Code with Dev Containers -- Install the Dev Containers extension: `ext install ms-vscode-remote.remote-containers` and make sure Dev Containers work. -- Run **Dev Containers: Clone Repository** command and follow the steps. -- The container builds automatically—no local dependencies required. -- After build completion, the environment is ready to use. +You can run the project either entirely in the cloud using **GitHub Codespaces** or locally using **VS Code with Dev Containers**. Both methods provide a fully configured environment without requiring manual setup on your machine. + +## Quick Start + +Both methods are ready for Perl development, including support for the [Perl Language Server](https://github.com/richterger/Perl-LanguageServer). + +### GitHub Codespaces + +> [!IMPORTANT] +> Codespaces has free tier [limitations](https://docs.github.com/en/billing/concepts/product-billing/github-codespaces). + +1. In GitHub on the **Code** tab, click **Code** button → **Codespaces** → **Create codespace on ...**. +2. Wait for the browser-based VS Code to start. +3. Continue with [Running Lyrion](#running-lyrion). + +### Local VS Code + Dev Containers [^1] + +[^1]: Podman and Docker are both supported. + + For Podman, set [`dev.containers.dockerPath`](vscode://settings/dev.containers.dockerPath) to `podman`, and set [`dev.containers.dockerComposePath`](vscode://settings/dev.containers.dockerComposePath) to `podman compose`. + +> [!TIP] +> It is recommended to use at least 6GB of RAM for the Dev Container, especially if you plan to run SoftSqueeze alongside Lyrion. On Windows, see the `memory` key in [WSL configuration](https://learn.microsoft.com/en-us/windows/wsl/wsl-config#main-wsl-settings) for more details. + +1. Install [Visual Studio Code](https://code.visualstudio.com/). +2. Install the [Dev Containers extension](vscode:extension/ms-vscode-remote.remote-containers) and make sure Dev Containers work on your machine. Follow the **Installation** instructions in the extension documentation. + +> [!TIP] +> In VS Code, open the Command Palette and run [`ext install ms-vscode-remote.remote-containers`](vscode:extension/ms-vscode-remote.remote-containers) to install the extension. + +3. Then you can either: + - Open the cloned repository in VS Code and then open the Command Palette and run [`> Dev Containers: Reopen in Container`](command:remote-containers.reopenInContainer) command. + - Alternatively, in VS Code, open the Command Palette and run [`> Dev Containers: Clone Repository in Container Volume`](command:remote-containers.openRepositoryFromGitWithEditSession) command and follow the steps. + +4. Continue with [Running Lyrion](#running-lyrion). + +#### Supported Modes + +You can run the Dev Container in two ways (see [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json)): + +- **Docker Compose mode** +- **Dockerfile mode** + - Useful if you have issues with Compose or want a minimal setup. + +Both modes mount your project to `/workspaces/slimserver` in the container and provide the same development environment. + +## Running Lyrion + +Lyrion stores its data in the project root: + +| Path | Purpose | +| ------- | ----------- | +| `Cache` | Cache | +| `Logs` | Logs | +| `prefs` | Preferences | + +These directories are listed in `.gitignore` and are not tracked by git. + +### Manual Start + +1. Wait for everything to initialize (Extensions, Perl Language Server, etc.). +2. Open a terminal in VS Code (should be in `/workspaces/slimserver`). +3. Run: + + ```bash + .devcontainer/start-lyrion.sh + ``` + + The script waits until Lyrion is reachable and prints the URL. + + Alternatively, start it directly with Perl for live log output in the console: + + ```bash + perl slimserver.pl + ``` + + > [!TIP] + > Set `LMS_STDIO=1` when running directly (`LMS_STDIO=1 perl slimserver.pl`) to enable interactive CLI input — you can type CLI commands directly in the terminal while LMS is running. + +4. Open the **Ports** tab and click the **Forwarded Address** `Web interface` to access Lyrion web UI [^2]. + [^2]: For local Dev Containers you can also open [http://localhost:9000](http://localhost:9000) directly. + +### Manual Stop + +```bash +.devcontainer/stop-lyrion.sh +# or force-kill +FORCE=1 .devcontainer/stop-lyrion.sh +``` + +### Auto-Start (`AUTO_START_LMS=true`) + +The Dev Container can start Lyrion automatically. This is disabled by default. + +1. Uncomment or set in [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json): + ```json + "containerEnv": { + "AUTO_START_LMS": "true" + } + ``` +2. Rebuild the container. +3. Lyrion will start automatically and listen on port `9000`. + +Optional startup variables: + +- `LMS_HTTP_PORT` — HTTP port (default: `9000`) +- `LMS_WAIT_TIMEOUT` — seconds to wait for startup (default: `90`) +- `LMS_EXTRA_ARGS` / `EXTRA_ARGS` — additional arguments passed to `slimserver.pl` (same as [`EXTRA_ARGS`](https://github.com/LMS-Community/slimserver-platforms/blob/public/9.2/Docker/README.md#passing-additional-launch-arguments) in the official Docker image) + +## SoftSqueeze [^3] + +[^3]: Not supported in Codespaces. SoftSqueeze runs as a separate optional container. + +Use the standalone guide in [.devcontainer/softsqueeze/SOFTSQUEEZE.md](.devcontainer/softsqueeze/SOFTSQUEEZE.md) for setup, runtime requirements, and troubleshooting. + +## Mounting Music and Playlist Folders + +The Dev Container supports these optional host folder mounts: + +- `/music` — music library +- `/playlist` — playlists + +Mounts are **disabled by default** — the container works out of the box in GitHub Codespaces and local Dev Containers without any configuration. `/music` and `/playlist` are created inside the container by the image. + +> [!NOTE] +> Preferences, cache, and logs (`Cache/`, `Logs/`, `prefs/`) are stored in the project root and are already persisted via the workspace bind mount. + +To mount host folders for music and playlists, uncomment and adjust the `mounts` key in [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json) before (re)building the container. The `mounts` key works in **both** Dockerfile and Docker Compose mode: + +```jsonc +"mounts": [ + { "type": "bind", "source": "/path/to/your/music", "target": "/music" }, + { "type": "bind", "source": "/path/to/your/playlist", "target": "/playlist" } +] +``` + +> [!NOTE] +> After mounting, add `/music` and `/playlist` to LMS via **Settings > Basic Settings > Media Folders** in the web UI. + +## Debugging + +### Debug Launch + +1. Set a breakpoint. +2. Press **F5**. +3. Select **LMS: Debug slimserver.pl**. +4. LMS will launch in debug mode on port 9000, using the project root for all data. + +Configuration is in [.vscode/launch.json](.vscode/launch.json). + +> [!NOTE] +> **Debugger Stop Behavior** +> +> Due to the way Perl and LMS handle signals (see the adjustments in [.vscode/PerlLanguageServerBootstrap.pm](.vscode/PerlLanguageServerBootstrap.pm)), the debugger may not immediately respond to the first stop request. If you encounter this behavior, simply click the stop button again to ensure the debugger receives the signal and halts execution as expected. + +## Cleanup + +### Remove Runtime Folders + +```bash +# Preview what would be deleted +.devcontainer/clean-folders.sh +# Actually delete +FORCE=1 .devcontainer/clean-folders.sh +``` + +This removes `Cache/`, `Logs/`, and `prefs/` from the project root. + +## Troubleshooting + +### Port 9000 Not Accessible + +1. Verify Lyrion is running inside the container: + ```bash + ss -ltn | grep 9000 + ``` +2. Check logs: + ```bash + tail -f /tmp/devcontainer-lms.log + ``` +3. Rebuild the Dev Container. + +## Notes + +- Lyrion ports `9000`, `3483`, and `9090` are forwarded by default. +- Lyrion and SoftSqueeze use the `lyrion_bridge` network. + +## Contributing + +### Adding New Scripts + +All `.sh` scripts in `.devcontainer/` must have the executable bit set in git so they run without an explicit `bash` prefix on macOS and Linux: + +```bash +git update-index --chmod=+x .devcontainer/your-script.sh +``` + +The `post-create.sh` script also runs `chmod +x` on all `.devcontainer/**/*.sh` files inside the container as a fallback, but the git bit is the canonical source of truth and should always be set. + +## Further Reading + +- [Dev Containers Documentation](https://containers.dev/) +- [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) +- [Perl Language Server](https://github.com/richterger/Perl-LanguageServer)