mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
fix(playback): ReplayGain prefetch, gapless UI sync, library peak column (#1231)
This commit is contained in:
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compare two environment dumps from dump-environment.sh
|
||||
# Usage: ./scripts/compare-environment.sh FILE_A FILE_B
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 FILE_A FILE_B" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_A="$1"
|
||||
FILE_B="$2"
|
||||
|
||||
for f in "$FILE_A" "$FILE_B"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "Missing file: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
parse_dump() {
|
||||
local file="$1"
|
||||
awk -F= '
|
||||
/^\[/ {
|
||||
section = substr($0, 2, length($0) - 2)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*$/ { next }
|
||||
/^#/ { next }
|
||||
{
|
||||
key = $1
|
||||
$1 = ""
|
||||
sub(/^=/, "", $0)
|
||||
full = (section == "" ? key : section "." key)
|
||||
print full "\t" $0
|
||||
}
|
||||
' "$file" | sort -u
|
||||
}
|
||||
|
||||
TMP_A="$(mktemp)"
|
||||
TMP_B="$(mktemp)"
|
||||
TMP_JOIN="$(mktemp)"
|
||||
trap 'rm -f "$TMP_A" "$TMP_B" "$TMP_JOIN"' EXIT
|
||||
|
||||
parse_dump "$FILE_A" >"$TMP_A"
|
||||
parse_dump "$FILE_B" >"$TMP_B"
|
||||
|
||||
join -t $'\t' -a 1 -a 2 -e '—' -o '0,1.2,2.2' "$TMP_A" "$TMP_B" >"$TMP_JOIN"
|
||||
|
||||
ONLY_A=0
|
||||
ONLY_B=0
|
||||
DIFF=0
|
||||
SAME=0
|
||||
|
||||
echo "Comparing:"
|
||||
echo " A: $FILE_A"
|
||||
echo " B: $FILE_B"
|
||||
echo
|
||||
|
||||
while IFS=$'\t' read -r key val_a val_b; do
|
||||
[[ -z "$key" ]] && continue
|
||||
if [[ "$val_a" == "—" ]]; then
|
||||
ONLY_B=$((ONLY_B + 1))
|
||||
printf ' + %-40s B=%s\n' "$key" "$val_b"
|
||||
elif [[ "$val_b" == "—" ]]; then
|
||||
ONLY_A=$((ONLY_A + 1))
|
||||
printf ' - %-40s A=%s\n' "$key" "$val_a"
|
||||
elif [[ "$val_a" != "$val_b" ]]; then
|
||||
DIFF=$((DIFF + 1))
|
||||
printf ' ≠ %-40s\n A=%s\n B=%s\n' "$key" "$val_a" "$val_b"
|
||||
else
|
||||
SAME=$((SAME + 1))
|
||||
fi
|
||||
done <"$TMP_JOIN"
|
||||
|
||||
echo
|
||||
echo "Summary: same=$SAME different=$DIFF only_in_A=$ONLY_A only_in_B=$ONLY_B"
|
||||
|
||||
# High-signal keys for the common "works on one NixOS box" case.
|
||||
echo
|
||||
echo "High-signal differences (if any):"
|
||||
HIGH_SIGNAL=0
|
||||
while IFS=$'\t' read -r key val_a val_b; do
|
||||
[[ "$val_a" == "$val_b" ]] && continue
|
||||
case "$key" in
|
||||
meta.flake_lock_sha256|meta.nixpkgs_locked_rev|meta.npm_lock_sha256|meta.git_rev|\
|
||||
installed_app.psysonic_realpath|installed_app.psysonic_closure_paths|installed_app.psysonic_drv|\
|
||||
runtime_env.HTTP_PROXY|runtime_env.HTTPS_PROXY|runtime_env.http_proxy|runtime_env.https_proxy|\
|
||||
runtime_env.NO_PROXY|runtime_env.no_proxy|runtime_env.SSL_CERT_FILE|runtime_env.NIX_SSL_CERT_FILE|\
|
||||
toolchain_nix_develop.node_realpath|toolchain_nix_develop.rustc_realpath|\
|
||||
host.nixos_version|host.uname|\
|
||||
app_config_paths.data_dir|app_config_paths.localstorage_db_path|\
|
||||
app_preferences.language|app_servers.active_server_id|app_servers.server_count|\
|
||||
app_servers.server.*.url|app_servers.server.*.alternateUrl|\
|
||||
app_servers.server.*.customHeaders_count|app_servers.server.*.customHeadersApplyTo|\
|
||||
app_servers.server.*.password_sha256|app_servers.server.*.customHeaders.*.name|\
|
||||
app_servers.server.*.customHeaders.*.value_sha256|\
|
||||
app_network_probe.probe.*)
|
||||
HIGH_SIGNAL=$((HIGH_SIGNAL + 1))
|
||||
printf ' ! %s\n A=%s\n B=%s\n' "$key" "$val_a" "$val_b"
|
||||
;;
|
||||
esac
|
||||
done <"$TMP_JOIN"
|
||||
|
||||
if [[ "$HIGH_SIGNAL" -eq 0 && "$DIFF" -eq 0 && "$ONLY_A" -eq 0 && "$ONLY_B" -eq 0 ]]; then
|
||||
echo " (none — environments match on recorded keys)"
|
||||
elif [[ "$HIGH_SIGNAL" -eq 0 ]]; then
|
||||
echo " (no high-signal keys differ; see full list above — may be npm patch-level deps only)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Server / network config differences:"
|
||||
SERVER_DIFF=0
|
||||
while IFS=$'\t' read -r key val_a val_b; do
|
||||
[[ "$val_a" == "$val_b" ]] && continue
|
||||
case "$key" in
|
||||
app_servers.*|app_network_probe.*|app_config_paths.*|app_preferences.*)
|
||||
SERVER_DIFF=$((SERVER_DIFF + 1))
|
||||
printf ' • %s\n A=%s\n B=%s\n' "$key" "$val_a" "$val_b"
|
||||
;;
|
||||
esac
|
||||
done <"$TMP_JOIN"
|
||||
if [[ "$SERVER_DIFF" -eq 0 ]]; then
|
||||
echo " (none — server URLs, headers, and curl probes match)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Reminder: server offline is usually URL/network/headers, not Node patch versions."
|
||||
echo "Next: curl the Navidrome URL from both hosts; compare Settings → Servers side by side."
|
||||
|
||||
if [[ "$DIFF" -gt 0 || "$ONLY_A" -gt 0 || "$ONLY_B" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
Executable
+254
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env bash
|
||||
# Dump Psysonic-related toolchain, Nix closure hints, app config, and env for cross-machine diff.
|
||||
# Re-enters `nix develop` automatically when flake.nix is present (no manual dev shell needed).
|
||||
# Usage: ./scripts/dump-environment.sh [-o FILE]
|
||||
# Compare: ./scripts/compare-environment.sh a.txt b.txt
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$REPO_ROOT/scripts/dump-environment.sh"
|
||||
|
||||
# Bootstrap: run the rest inside the flake dev shell so node/jq match the project.
|
||||
if [[ -z "${PSYSONIC_ENV_DUMP_IN_NIX:-}" ]] && [[ -f "$REPO_ROOT/flake.nix" ]]; then
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
echo "$0: flake.nix found but nix is not on PATH — install Nix or run from a NixOS profile with flakes." >&2
|
||||
exit 1
|
||||
fi
|
||||
export PSYSONIC_ENV_DUMP_IN_NIX=1
|
||||
exec env REPO_ROOT="$REPO_ROOT" nix develop --command bash "$SCRIPT" "$@"
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "$0: node not found after nix develop — check flake.nix devShell." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT_FILE=""
|
||||
while getopts 'o:h' opt; do
|
||||
case "$opt" in
|
||||
o) OUTPUT_FILE="$OPTARG" ;;
|
||||
h)
|
||||
echo "Usage: $0 [-o FILE]" >&2
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [-o FILE]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
emit() {
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
printf '%s\n' "$*" >>"$OUTPUT_FILE"
|
||||
else
|
||||
printf '%s\n' "$*"
|
||||
fi
|
||||
}
|
||||
|
||||
kv() {
|
||||
local key="$1"
|
||||
local value="${2-}"
|
||||
value="${value//$'\n'/\\n}"
|
||||
emit "${key}=${value}"
|
||||
}
|
||||
|
||||
section() {
|
||||
emit ""
|
||||
emit "[$1]"
|
||||
}
|
||||
|
||||
run_optional() {
|
||||
"$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
: >"$OUTPUT_FILE"
|
||||
fi
|
||||
|
||||
section meta
|
||||
kv generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
kv in_nix_dev_shell "${PSYSONIC_ENV_DUMP_IN_NIX:-no}"
|
||||
kv hostname "$(hostname 2>/dev/null || echo unknown)"
|
||||
kv repo_root "$REPO_ROOT"
|
||||
if command -v git >/dev/null 2>&1 && git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
kv git_rev "$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
kv git_branch "$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
||||
kv git_dirty "$(git -C "$REPO_ROOT" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
||||
else
|
||||
kv git_rev "n/a"
|
||||
kv git_branch "n/a"
|
||||
kv git_dirty "n/a"
|
||||
fi
|
||||
if [[ -f "$REPO_ROOT/package.json" ]]; then
|
||||
kv package_json_version "$(node -p "require('./package.json').version" 2>/dev/null || sed -n 's/.*\"version\": \"\\([^\"]*\\)\".*/\\1/p' "$REPO_ROOT/package.json" | head -1)"
|
||||
fi
|
||||
if [[ -f "$REPO_ROOT/flake.lock" ]]; then
|
||||
kv flake_lock_sha256 "$(sha256sum "$REPO_ROOT/flake.lock" | awk '{print $1}')"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
kv nixpkgs_locked_rev "$(jq -r '.nodes.nixpkgs.locked.rev // "unknown"' "$REPO_ROOT/flake.lock" 2>/dev/null)"
|
||||
kv nixpkgs_locked_narHash "$(jq -r '.nodes.nixpkgs.locked.narHash // "unknown"' "$REPO_ROOT/flake.lock" 2>/dev/null)"
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$REPO_ROOT/package-lock.json" ]]; then
|
||||
kv npm_lockfile_version "$(jq -r '.lockfileVersion // "unknown"' "$REPO_ROOT/package-lock.json" 2>/dev/null || echo unknown)"
|
||||
kv npm_lock_sha256 "$(sha256sum "$REPO_ROOT/package-lock.json" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
section host
|
||||
kv uname "$(uname -a 2>/dev/null || echo unknown)"
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
kv os_id "${ID:-unknown}"
|
||||
kv os_version "${VERSION_ID:-unknown}"
|
||||
kv os_pretty "${PRETTY_NAME:-unknown}"
|
||||
fi
|
||||
if command -v nixos-version >/dev/null 2>&1; then
|
||||
kv nixos_version "$(nixos-version 2>/dev/null || echo unknown)"
|
||||
fi
|
||||
|
||||
section nix
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
kv nix_version "$(nix --version 2>/dev/null | head -1)"
|
||||
kv nix_flake_present "$([[ -f "$REPO_ROOT/flake.nix" ]] && echo yes || echo no)"
|
||||
else
|
||||
kv nix_version "not_installed"
|
||||
kv nix_flake_present "$([[ -f "$REPO_ROOT/flake.nix" ]] && echo yes || echo no)"
|
||||
fi
|
||||
|
||||
dump_toolchain_block() {
|
||||
local label="$1"
|
||||
shift
|
||||
section "$label"
|
||||
for tool in node npm rustc cargo clippy jq cmake pkg-config; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
case "$tool" in
|
||||
node) kv node_version "$("$tool" -v 2>/dev/null)" ;;
|
||||
npm) kv npm_version "$("$tool" -v 2>/dev/null)" ;;
|
||||
rustc) kv rustc_version "$("$tool" -V 2>/dev/null | head -1)" ;;
|
||||
cargo) kv cargo_version "$("$tool" -V 2>/dev/null | head -1)" ;;
|
||||
clippy) kv clippy_version "$("$tool" -V 2>/dev/null | head -1)" ;;
|
||||
jq) kv jq_version "$("$tool" --version 2>/dev/null | head -1)" ;;
|
||||
cmake) kv cmake_version "$("$tool" --version 2>/dev/null | head -1)" ;;
|
||||
pkg-config) kv pkg_config_version "$("$tool" --version 2>/dev/null | head -1)" ;;
|
||||
esac
|
||||
kv "${tool}_path" "$(command -v "$tool")"
|
||||
if [[ -L "$(command -v "$tool")" ]] || [[ -e "$(command -v "$tool")" ]]; then
|
||||
kv "${tool}_realpath" "$(readlink -f "$(command -v "$tool")" 2>/dev/null || echo unknown)"
|
||||
fi
|
||||
else
|
||||
kv "${tool}_version" "missing"
|
||||
fi
|
||||
done
|
||||
kv CARGO_TARGET_DIR "${CARGO_TARGET_DIR:-unset}"
|
||||
kv LD_LIBRARY_PATH "${LD_LIBRARY_PATH:-unset}"
|
||||
kv GST_PLUGIN_PATH "${GST_PLUGIN_PATH:-unset}"
|
||||
kv GIO_EXTRA_MODULES "${GIO_EXTRA_MODULES:-unset}"
|
||||
}
|
||||
|
||||
if [[ -n "${PSYSONIC_ENV_DUMP_IN_NIX:-}" ]]; then
|
||||
dump_toolchain_block toolchain_nix_develop
|
||||
elif command -v nix >/dev/null 2>&1 && [[ -f "$REPO_ROOT/flake.nix" ]]; then
|
||||
# No bootstrap (should not happen when flake exists) — capture devShell separately.
|
||||
NIX_DEV_DUMP="$(nix develop --command bash -lc '
|
||||
set +e
|
||||
cd "$REPO_ROOT" || exit 0
|
||||
for tool in node npm rustc cargo clippy jq; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
case "$tool" in
|
||||
node) printf "node_version=%s\n" "$("$tool" -v)" ;;
|
||||
npm) printf "npm_version=%s\n" "$("$tool" -v)" ;;
|
||||
rustc) printf "rustc_version=%s\n" "$("$tool" -V | head -1)" ;;
|
||||
cargo) printf "cargo_version=%s\n" "$("$tool" -V | head -1)" ;;
|
||||
clippy) printf "clippy_version=%s\n" "$("$tool" -V | head -1)" ;;
|
||||
jq) printf "jq_version=%s\n" "$("$tool" --version | head -1)" ;;
|
||||
esac
|
||||
printf "%s_path=%s\n" "$tool" "$(command -v "$tool")"
|
||||
printf "%s_realpath=%s\n" "$tool" "$(readlink -f "$(command -v "$tool")" 2>/dev/null || echo unknown)"
|
||||
else
|
||||
printf "%s_version=missing\n" "$tool"
|
||||
fi
|
||||
done
|
||||
printf "CARGO_TARGET_DIR=%s\n" "${CARGO_TARGET_DIR:-unset}"
|
||||
printf "LD_LIBRARY_PATH=%s\n" "${LD_LIBRARY_PATH:-unset}"
|
||||
printf "GST_PLUGIN_PATH=%s\n" "${GST_PLUGIN_PATH:-unset}"
|
||||
printf "GIO_EXTRA_MODULES=%s\n" "${GIO_EXTRA_MODULES:-unset}"
|
||||
' REPO_ROOT="$REPO_ROOT" 2>/dev/null | tr -d '\r' || true)"
|
||||
if [[ -n "$NIX_DEV_DUMP" ]]; then
|
||||
section toolchain_nix_develop
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" && "$line" == *=* ]] && emit "$line"
|
||||
done <<<"$NIX_DEV_DUMP"
|
||||
else
|
||||
section toolchain_nix_develop
|
||||
kv status "nix develop failed or unavailable"
|
||||
fi
|
||||
else
|
||||
dump_toolchain_block toolchain_ambient
|
||||
fi
|
||||
|
||||
section runtime_env
|
||||
for var in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy GDK_BACKEND PSYSONIC_SKIP_WAYLAND_FONT_TUNING PSYSONIC_ALLOW_NATIVE_GDK SSL_CERT_FILE SSL_CERT_DIR NIX_SSL_CERT_FILE; do
|
||||
kv "$var" "${!var-unset}"
|
||||
done
|
||||
kv navigator_online "n/a (browser-only)"
|
||||
|
||||
section installed_app
|
||||
if command -v psysonic >/dev/null 2>&1; then
|
||||
PSY_PATH="$(command -v psysonic)"
|
||||
kv psysonic_path "$PSY_PATH"
|
||||
kv psysonic_realpath "$(readlink -f "$PSY_PATH" 2>/dev/null || echo unknown)"
|
||||
run_optional kv psysonic_version "$(psysonic --version 2>/dev/null | head -1)"
|
||||
if command -v nix-store >/dev/null 2>&1; then
|
||||
kv psysonic_closure_paths "$(nix-store -qR "$PSY_PATH" 2>/dev/null | wc -l | tr -d ' ')"
|
||||
kv psysonic_drv "$(nix-store -q --deriver "$PSY_PATH" 2>/dev/null | sed 's/\.drv$//' | xargs -r basename 2>/dev/null || echo unknown)"
|
||||
fi
|
||||
else
|
||||
kv psysonic_path "not_in_path"
|
||||
fi
|
||||
|
||||
section npm_dependencies
|
||||
if [[ -f "$REPO_ROOT/package-lock.json" ]] && command -v node >/dev/null 2>&1; then
|
||||
REPO_ROOT="$REPO_ROOT" node <<'NODE' 2>/dev/null | while IFS= read -r line; do emit "$line"; done || true
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const lockPath = path.join(process.env.REPO_ROOT || '.', 'package-lock.json');
|
||||
let lock;
|
||||
try { lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch { process.exit(0); }
|
||||
const root = lock.packages?.['']?.dependencies || {};
|
||||
const deps = Object.keys(root).sort();
|
||||
for (const name of deps) {
|
||||
const entry = lock.packages?.[`node_modules/${name}`] || lock.packages?.[name];
|
||||
const version = entry?.version || 'unknown';
|
||||
console.log(`dep.${name}=${version}`);
|
||||
}
|
||||
NODE
|
||||
else
|
||||
kv status "node or package-lock.json unavailable"
|
||||
fi
|
||||
|
||||
run_app_config_dump() {
|
||||
local extractor="$REPO_ROOT/scripts/lib/extract-app-config.mjs"
|
||||
[[ -f "$extractor" ]] || return 0
|
||||
if node "$extractor" --repo-root "$REPO_ROOT" >>"${OUTPUT_FILE:-/dev/stdout}" 2>/dev/null; then
|
||||
:
|
||||
else
|
||||
section app_config
|
||||
kv status "extract-app-config failed (is Psysonic installed / has it been run once?)"
|
||||
fi
|
||||
}
|
||||
|
||||
run_app_config_dump
|
||||
|
||||
section network_probe_hint
|
||||
kv note_1 "App server profiles and curl probes are in app_servers / app_network_probe sections above."
|
||||
kv note_2 "Passwords and custom header values are redacted; compare password_sha256 and value_sha256 only."
|
||||
kv note_3 "Compare two dumps with: scripts/compare-environment.sh a.txt b.txt"
|
||||
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "Wrote $OUTPUT_FILE" >&2
|
||||
fi
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Read Psysonic app config from WebKit localStorage + XDG dirs.
|
||||
* Emits key=value lines (passwords/secrets redacted; header values hashed).
|
||||
*
|
||||
* Usage: node scripts/lib/extract-app-config.mjs [--app-id ID] [--repo-root PATH]
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { appId: process.env.PSYSONIC_APP_ID || '', repoRoot: '' };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
if (argv[i] === '--app-id' && argv[i + 1]) {
|
||||
out.appId = argv[++i];
|
||||
} else if (argv[i] === '--repo-root' && argv[i + 1]) {
|
||||
out.repoRoot = argv[++i];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function emitSection(name) {
|
||||
process.stdout.write(`\n[${name}]\n`);
|
||||
}
|
||||
|
||||
function emit(key, value) {
|
||||
const v = String(value ?? '').replace(/\n/g, '\\n');
|
||||
process.stdout.write(`${key}=${v}\n`);
|
||||
}
|
||||
|
||||
function readAppIdFromRepo(repoRoot) {
|
||||
const conf = path.join(repoRoot, 'src-tauri', 'tauri.conf.json');
|
||||
if (!fs.existsSync(conf)) return '';
|
||||
try {
|
||||
const j = JSON.parse(fs.readFileSync(conf, 'utf8'));
|
||||
return typeof j.identifier === 'string' ? j.identifier : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function xdgDataHome() {
|
||||
return process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
||||
}
|
||||
|
||||
function xdgConfigHome() {
|
||||
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
||||
}
|
||||
|
||||
function decodeWebKitValue(raw) {
|
||||
if (raw == null) return null;
|
||||
const buf = Buffer.isBuffer(raw) ? raw : raw instanceof Uint8Array ? Buffer.from(raw) : null;
|
||||
if (buf) {
|
||||
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
||||
return new TextDecoder('utf-16le').decode(buf.subarray(2));
|
||||
}
|
||||
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
||||
return new TextDecoder('utf-16be').decode(buf.subarray(2));
|
||||
}
|
||||
// WebKit often stores UTF-16LE without BOM
|
||||
if (buf.length >= 4 && buf[1] === 0 && buf[3] === 0) {
|
||||
return new TextDecoder('utf-16le').decode(buf);
|
||||
}
|
||||
return buf.toString('utf8');
|
||||
}
|
||||
if (typeof raw === 'string') return raw;
|
||||
return String(raw);
|
||||
}
|
||||
|
||||
function readLocalStorageRaw(dbPath, storageKey) {
|
||||
try {
|
||||
const db = new DatabaseSync(dbPath, { readOnly: true });
|
||||
const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(storageKey);
|
||||
db.close();
|
||||
if (!row?.value) return null;
|
||||
return decodeWebKitValue(row.value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalStorageKey(dbPath, storageKey) {
|
||||
const text = readLocalStorageRaw(dbPath, storageKey);
|
||||
if (text == null) return null;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function pickLocalStorageFile(dataDir) {
|
||||
const dir = path.join(dataDir, 'localstorage');
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs
|
||||
.readdirSync(dir)
|
||||
.filter(f => f.endsWith('.localstorage') && !f.includes('-wal') && !f.includes('-shm'))
|
||||
.map(f => path.join(dir, f));
|
||||
if (files.length === 0) return null;
|
||||
|
||||
// Prefer packaged app origin over vite dev (1420) when both exist.
|
||||
const ranked = files.sort((a, b) => {
|
||||
const score = p => {
|
||||
const base = path.basename(p);
|
||||
if (base.includes('tauri_localhost')) return 0;
|
||||
if (base.includes('1420')) return 2;
|
||||
return 1;
|
||||
};
|
||||
return score(a) - score(b);
|
||||
});
|
||||
|
||||
for (const file of ranked) {
|
||||
const auth = readLocalStorageKey(file, 'psysonic-auth');
|
||||
if (auth?.state?.servers?.length) return file;
|
||||
}
|
||||
return ranked[0];
|
||||
}
|
||||
|
||||
function probeHttpReachability(rawUrl) {
|
||||
if (!rawUrl) return 'empty';
|
||||
const url = rawUrl.startsWith('http') ? rawUrl : `http://${rawUrl}`;
|
||||
const r = spawnSync(
|
||||
'curl',
|
||||
['-sS', '-o', '/dev/null', '-w', '%{http_code}', '--connect-timeout', '5', '--max-time', '10', url],
|
||||
{ encoding: 'utf8', timeout: 15000 },
|
||||
);
|
||||
if (r.error) return `error:${r.error.code ?? 'unknown'}`;
|
||||
if (r.status !== 0) return `curl_exit_${r.status}`;
|
||||
return `http_${r.stdout.trim() || '000'}`;
|
||||
}
|
||||
|
||||
function dumpNetworkProbes(servers) {
|
||||
emitSection('app_network_probe');
|
||||
const seen = new Set();
|
||||
servers.forEach((srv, i) => {
|
||||
for (const [kind, raw] of [
|
||||
['url', srv.url],
|
||||
['alternateUrl', srv.alternateUrl],
|
||||
]) {
|
||||
if (!raw || seen.has(raw)) continue;
|
||||
seen.add(raw);
|
||||
emit(`probe.${i}.${kind}`, probeHttpReachability(raw));
|
||||
emit(`probe.${i}.${kind}_target`, raw);
|
||||
}
|
||||
});
|
||||
if (seen.size === 0) emit('probe_status', 'no server URLs configured');
|
||||
}
|
||||
|
||||
function dumpServerProfiles(state) {
|
||||
const servers = state?.servers ?? [];
|
||||
emit('active_server_id', state?.activeServerId ?? '');
|
||||
emit('server_count', servers.length);
|
||||
servers.forEach((srv, i) => {
|
||||
const p = `server.${i}`;
|
||||
emit(`${p}.id`, srv.id ?? '');
|
||||
emit(`${p}.name`, srv.name ?? '');
|
||||
emit(`${p}.url`, srv.url ?? '');
|
||||
emit(`${p}.alternateUrl`, srv.alternateUrl ?? '');
|
||||
emit(`${p}.shareUsesLocalUrl`, srv.shareUsesLocalUrl === true ? 'true' : 'false');
|
||||
emit(`${p}.username`, srv.username ?? '');
|
||||
emit(`${p}.password_set`, srv.password ? 'yes' : 'no');
|
||||
emit(`${p}.password_sha256`, srv.password ? sha256(srv.password) : 'none');
|
||||
emit(`${p}.customHeadersApplyTo`, srv.customHeadersApplyTo ?? 'public');
|
||||
const headers = srv.customHeaders ?? [];
|
||||
emit(`${p}.customHeaders_count`, headers.length);
|
||||
headers.forEach((h, hi) => {
|
||||
emit(`${p}.customHeaders.${hi}.name`, h.name ?? '');
|
||||
emit(`${p}.customHeaders.${hi}.value_sha256`, h.value ? sha256(h.value) : 'empty');
|
||||
});
|
||||
});
|
||||
dumpNetworkProbes(servers);
|
||||
}
|
||||
|
||||
function fileMeta(label, filePath) {
|
||||
if (!filePath || !fs.existsSync(filePath)) {
|
||||
emit(`${label}_exists`, 'no');
|
||||
return;
|
||||
}
|
||||
const st = fs.statSync(filePath);
|
||||
emit(`${label}_exists`, 'yes');
|
||||
emit(`${label}_path`, filePath);
|
||||
emit(`${label}_size`, st.size);
|
||||
emit(`${label}_mtime`, st.mtime.toISOString());
|
||||
}
|
||||
|
||||
function listDir(label, dirPath, max = 12) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
emit(`${label}_exists`, 'no');
|
||||
return;
|
||||
}
|
||||
emit(`${label}_exists`, 'yes');
|
||||
emit(`${label}_path`, dirPath);
|
||||
const entries = fs.readdirSync(dirPath).sort();
|
||||
emit(`${label}_entry_count`, entries.length);
|
||||
entries.slice(0, max).forEach((name, i) => emit(`${label}.entry.${i}`, name));
|
||||
}
|
||||
|
||||
const { appId: appIdArg, repoRoot } = parseArgs(process.argv);
|
||||
let appId = appIdArg || (repoRoot ? readAppIdFromRepo(repoRoot) : '');
|
||||
if (!appId) appId = readAppIdFromRepo(process.cwd()) || 'dev.psysonic.player';
|
||||
|
||||
const dataDir = path.join(xdgDataHome(), appId);
|
||||
const configDir = path.join(xdgConfigHome(), appId);
|
||||
|
||||
emitSection('app_config_paths');
|
||||
emit('app_id', appId);
|
||||
emit('data_dir', dataDir);
|
||||
emit('config_dir', configDir);
|
||||
emit('data_dir_exists', fs.existsSync(dataDir) ? 'yes' : 'no');
|
||||
emit('config_dir_exists', fs.existsSync(configDir) ? 'yes' : 'no');
|
||||
|
||||
const lsFile = pickLocalStorageFile(dataDir);
|
||||
fileMeta('localstorage_db', lsFile);
|
||||
|
||||
emitSection('app_preferences');
|
||||
if (lsFile) {
|
||||
const lang = readLocalStorageRaw(lsFile, 'psysonic_language');
|
||||
emit('language', lang ?? 'unknown');
|
||||
|
||||
const authWrap = readLocalStorageKey(lsFile, 'psysonic-auth');
|
||||
if (authWrap?.state) {
|
||||
emitSection('app_servers');
|
||||
dumpServerProfiles(authWrap.state);
|
||||
} else {
|
||||
emit('app_servers_status', 'psysonic-auth not found or empty');
|
||||
}
|
||||
} else {
|
||||
emit('app_servers_status', 'no localstorage database found');
|
||||
}
|
||||
|
||||
emitSection('app_config_files');
|
||||
for (const rel of ['linux_wayland_text_profile', 'mini_player_pos.json', '.window-state.json']) {
|
||||
fileMeta(rel.replace(/\./g, '_'), path.join(configDir, rel));
|
||||
}
|
||||
|
||||
emitSection('app_data_artifacts');
|
||||
fileMeta('hsts_storage', path.join(dataDir, 'hsts-storage.sqlite'));
|
||||
fileMeta('library_db', path.join(dataDir, 'databases', 'library', 'library.sqlite'));
|
||||
listDir('localstorage_dir', path.join(dataDir, 'localstorage'));
|
||||
Reference in New Issue
Block a user