mirror of
https://github.com/Chevron7Locked/kima-hub.git
synced 2026-06-19 07:37:17 +00:00
v1.4.0: sequential enrichment, GPU auto-detection, repo cleanup
- Run audio analysis and vibe embedding phases sequentially to prevent resource contention (CPU/memory) from concurrent analyzers - Auto-detect GPU availability in both audio analyzers (CUDA/ROCm) - Fix false lite mode detection on startup by checking analyzer scripts on disk before falling back to heartbeat/DB checks - Fix Dockerfile NEXT_PUBLIC_BACKEND_URL and frontend rewrite proxy - Route enrichment failures through notification system instead of persistent error banner - Remove playback error banner from player components - Reduce enrichment cycle interval from 6h to 2h - Comprehensive repo cleanup: remove 127 decorative comment dividers across 17 files, clean verbose comments, harden .gitignore, remove tracked docs from git Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+63
-320
@@ -1,404 +1,135 @@
|
||||
# =============================================================================
|
||||
# LIDIFY MONOREPO - .gitignore
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Environment Variables & Secrets
|
||||
# =============================================================================
|
||||
# Environment & Secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.roomodes
|
||||
|
||||
# =============================================================================
|
||||
# Dependencies
|
||||
# =============================================================================
|
||||
# Node modules in all subdirectories
|
||||
**/node_modules/
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Python virtual environments (for soularr, scripts)
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv/
|
||||
**/.venv/
|
||||
|
||||
# =============================================================================
|
||||
# Build
|
||||
# =============================================================================
|
||||
# Frontend (Next.js)
|
||||
# Build Outputs
|
||||
frontend/.next/
|
||||
frontend/out/
|
||||
frontend/build/
|
||||
frontend/dist/
|
||||
|
||||
# Backend (Node.js/TypeScript)
|
||||
backend/dist/
|
||||
backend/build/
|
||||
backend/out/
|
||||
|
||||
# Mobile Application
|
||||
mobile-application/build/
|
||||
mobile-application/dist/
|
||||
mobile-application/.expo/
|
||||
mobile-application/.expo-shared/
|
||||
|
||||
# Soularr
|
||||
soularr/dist/
|
||||
soularr/build/
|
||||
|
||||
# General build outputs
|
||||
**/dist/
|
||||
**/build/
|
||||
**/out/
|
||||
.next
|
||||
.nuxt
|
||||
|
||||
# =============================================================================
|
||||
# Logs
|
||||
# =============================================================================
|
||||
logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
pnpm-debug.log*
|
||||
backend/logs/
|
||||
frontend/logs/
|
||||
|
||||
# =============================================================================
|
||||
# Testing & Coverage
|
||||
# =============================================================================
|
||||
coverage/
|
||||
*.lcov
|
||||
.nyc_output
|
||||
*.tsbuildinfo
|
||||
.cache/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# =============================================================================
|
||||
# Cache Directories
|
||||
# =============================================================================
|
||||
# Temporary & Backup Files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.old
|
||||
|
||||
# Cache
|
||||
.cache
|
||||
.parcel-cache
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
.npm
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
.rpt2_cache_*/
|
||||
.ruff_cache/
|
||||
|
||||
# =============================================================================
|
||||
# Docker & Containers
|
||||
# =============================================================================
|
||||
# Don't ignore docker-compose.yml itself, but ignore local overrides
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
docker-compose.local.yml
|
||||
|
||||
# Docker volumes (if any are stored locally)
|
||||
**/volumes/
|
||||
**/data/
|
||||
|
||||
# =============================================================================
|
||||
# IDEs & Editors
|
||||
# =============================================================================
|
||||
# VSCode
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
.vscode-test
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
.claude/*
|
||||
!.claude/commands/
|
||||
|
||||
# JetBrains IDEs (WebStorm, IntelliJ, etc.)
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
*.code-workspace
|
||||
|
||||
# macOS
|
||||
# OS Files
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# =============================================================================
|
||||
# Runtime & Process Files
|
||||
# =============================================================================
|
||||
pids
|
||||
# Runtime & Process
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# =============================================================================
|
||||
# Database Files (SQLite for local development)
|
||||
# =============================================================================
|
||||
# Database
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Prisma
|
||||
**/prisma/dev.db
|
||||
**/prisma/dev.db-journal
|
||||
|
||||
# =============================================================================
|
||||
# Media & Large Files
|
||||
# =============================================================================
|
||||
# Don't commit large music files (if any test files are added)
|
||||
*.mp3
|
||||
*.flac
|
||||
*.wav
|
||||
*.m4a
|
||||
*.ogg
|
||||
*.opus
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# =============================================================================
|
||||
# Secrets & Key Material
|
||||
# =============================================================================
|
||||
keystore.b64
|
||||
keystore.jks
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
# =============================================================================
|
||||
# Runtime caches (backend)
|
||||
# =============================================================================
|
||||
backend/cache/
|
||||
backend/logs/
|
||||
backend/mullvad/
|
||||
|
||||
# =============================================================================
|
||||
# Test artifacts
|
||||
# =============================================================================
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# Don't commit large images (unless they're assets)
|
||||
# *.jpg
|
||||
# *.jpeg
|
||||
# *.png
|
||||
# *.gif
|
||||
|
||||
# =============================================================================
|
||||
# Temporary Files
|
||||
# =============================================================================
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.old
|
||||
|
||||
# =============================================================================
|
||||
# Package Manager Files
|
||||
# =============================================================================
|
||||
# Yarn
|
||||
# Package Manager
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
yarn-error.log
|
||||
|
||||
# NPM
|
||||
npm-debug.log*
|
||||
|
||||
# PNPM
|
||||
pnpm-lock.yaml
|
||||
.pnpm-debug.log*
|
||||
|
||||
# =============================================================================
|
||||
# Mobile Specific (React Native / Expo)
|
||||
# =============================================================================
|
||||
mobile-application/.expo/
|
||||
mobile-application/.expo-shared/
|
||||
mobile-application/android/app/build/
|
||||
mobile-application/ios/Pods/
|
||||
mobile-application/ios/build/
|
||||
mobile-application/*.jks
|
||||
mobile-application/*.keystore
|
||||
mobile-application/*.p8
|
||||
mobile-application/*.p12
|
||||
mobile-application/*.mobileprovision
|
||||
# Secrets & Keys
|
||||
*.keystore
|
||||
*.jks
|
||||
keystore.b64
|
||||
**/key.txt
|
||||
*.conf
|
||||
lidify.keystore
|
||||
|
||||
# Legacy native leftovers (web app is PWA-first)
|
||||
frontend/android/
|
||||
|
||||
# =============================================================================
|
||||
# Postman (Keep collections, ignore environments with secrets)
|
||||
# =============================================================================
|
||||
postman/*environment*.json
|
||||
postman/*.local.json
|
||||
|
||||
|
||||
|
||||
# BUT allow README.md files (case-insensitive)
|
||||
!README.md
|
||||
!readme.md
|
||||
!Readme.md
|
||||
|
||||
# =============================================================================
|
||||
# TypeScript
|
||||
# =============================================================================
|
||||
*.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# =============================================================================
|
||||
# Miscellaneous
|
||||
# =============================================================================
|
||||
.lock-wscript
|
||||
lib-cov
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
.grunt
|
||||
bower_components
|
||||
.serverless/
|
||||
.fusebox/
|
||||
.dynamodb/
|
||||
.tern-port
|
||||
.docusaurus
|
||||
**/.vitepress/dist
|
||||
**/.vitepress/cache
|
||||
.vuepress/dist
|
||||
.temp
|
||||
*.tgz
|
||||
.node_repl_history
|
||||
|
||||
# =============================================================================
|
||||
# Project Specific
|
||||
# =============================================================================
|
||||
# Development scripts (keep locally, don't commit)
|
||||
reset-and-setup.sh
|
||||
organize-singles.sh
|
||||
|
||||
# AI Context Management (keep locally, don't push to GitHub)
|
||||
context_portal/
|
||||
|
||||
# Internal Development Documentation (keep locally, don't push to GitHub)
|
||||
docs/
|
||||
**/docs/
|
||||
|
||||
|
||||
# Temporary commit messages
|
||||
COMMIT_MESSAGE.txt
|
||||
|
||||
# Backend development logs
|
||||
backend/logs/
|
||||
|
||||
# Backend test cache directories
|
||||
backend/cache/test-*/
|
||||
|
||||
# Backend duplicate/nested directories
|
||||
backend/backend/
|
||||
|
||||
# Frontend Android build artifacts
|
||||
frontend/android/build/
|
||||
frontend/android/app/build/
|
||||
|
||||
# Postman collections (removed from repo)
|
||||
postman/
|
||||
|
||||
# Soularr config (removed from repo)
|
||||
soularr/
|
||||
|
||||
# Legacy React Native files (if re-added)
|
||||
/App.tsx
|
||||
/app.json
|
||||
/src/
|
||||
|
||||
# =============================================================================
|
||||
# IDE & Editor Settings
|
||||
# =============================================================================
|
||||
.claude/
|
||||
**/.claude/
|
||||
.cursor/
|
||||
**/.cursor/
|
||||
.vscode/
|
||||
**/.vscode/
|
||||
.roo/
|
||||
**/.roo/
|
||||
|
||||
# =============================================================================
|
||||
# Android Build Artifacts (contains local paths)
|
||||
# =============================================================================
|
||||
frontend/android/app/build/
|
||||
frontend/android/build/
|
||||
frontend/android/.gradle/
|
||||
frontend/android/app/src/main/assets/capacitor.config.json
|
||||
|
||||
# =============================================================================
|
||||
# Capacitor Generated Files
|
||||
# =============================================================================
|
||||
frontend/android/capacitor-cordova-android-plugins/build/
|
||||
|
||||
# =============================================================================
|
||||
# Cache Files (user-specific data)
|
||||
# =============================================================================
|
||||
# Backend
|
||||
backend/cache/
|
||||
**/cache/covers/
|
||||
**/cache/transcodes/
|
||||
|
||||
# =============================================================================
|
||||
# VPN / Private Configs (NEVER commit these!)
|
||||
# =============================================================================
|
||||
backend/mullvad/
|
||||
**/mullvad/
|
||||
*.conf
|
||||
**/key.txt
|
||||
|
||||
# Android signing
|
||||
lidify.keystore
|
||||
keystore.b64
|
||||
# Frontend
|
||||
frontend/android/
|
||||
frontend/test-*.tsx
|
||||
|
||||
# =============================================================================
|
||||
# AI Tools & Assistants
|
||||
# =============================================================================
|
||||
.claude/
|
||||
**/.claude/
|
||||
!.claude/commands/
|
||||
.cursor/
|
||||
**/.cursor/
|
||||
.roo/
|
||||
**/.roo/
|
||||
.aider*
|
||||
.serena/
|
||||
**/.serena/
|
||||
@@ -408,18 +139,30 @@ keystore.b64
|
||||
**/.vibe/
|
||||
.claudeignore
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
|
||||
# AI-generated planning/reports/documentation (keep locally)
|
||||
# Internal Documentation (public docs are in project root)
|
||||
docs/
|
||||
**/docs/
|
||||
context_portal/
|
||||
issues/
|
||||
plans/
|
||||
systems/
|
||||
|
||||
# Development worktrees (keep locally, don't commit)
|
||||
# Development
|
||||
.worktrees/
|
||||
reset-and-setup.sh
|
||||
organize-singles.sh
|
||||
COMMIT_MESSAGE.txt
|
||||
backend/backend/
|
||||
postman/
|
||||
soularr/
|
||||
|
||||
# =============================================================================
|
||||
# Development/Debug Test Files (keep locally, don't push to GitHub)
|
||||
AGENTS.md
|
||||
# =============================================================================
|
||||
frontend/test-*.tsx
|
||||
frontend/features/library/components/LazyArtistCard.tsx
|
||||
# Legacy (removed from repo)
|
||||
/App.tsx
|
||||
/app.json
|
||||
/src/
|
||||
|
||||
# Allow README files
|
||||
!README.md
|
||||
!readme.md
|
||||
|
||||
@@ -5,6 +5,39 @@ All notable changes to Lidify will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.0] - 2026-02-05
|
||||
|
||||
### Performance
|
||||
|
||||
- **Sequential audio/vibe enrichment:** Vibe phase skips when audio analysis is still running, preventing concurrent CPU-intensive Python analyzers from competing for resources
|
||||
- **Faster enrichment cycles:** Reduced cycle interval from 30s to 5s; the rate limiter already handles API throttling, making the extra delay redundant
|
||||
- **GPU auto-detection (CLAP):** PyTorch-based CLAP vibe embeddings auto-detect and use GPU when available, falling back to CPU
|
||||
- **GPU auto-detection (Essentia):** TensorFlow-based audio analysis detects GPU with memory growth enabled, with device logging on startup
|
||||
|
||||
### Changed
|
||||
|
||||
- **Enrichment orchestration simplified:** Replaced 4 phase functions with duplicated stop/pause handling with a generic `runPhase()` executor and `shouldHaltCycle()` helper
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Docker frontend routing:** Fixed `NEXT_PUBLIC_BACKEND_URL` build-time env var in Dockerfile so the frontend correctly proxies API requests to the backend
|
||||
- **Next.js rewrite proxy:** Updated rewrite config to use `NEXT_PUBLIC_BACKEND_URL` for consistent build-time/runtime behavior
|
||||
- **False lite mode on startup:** Feature detection now checks for analyzer scripts on disk, preventing false "lite mode" display before analyzers send their first heartbeat
|
||||
- **Removed playback error banner:** Removed the red error bar from all player components (FullPlayer, MiniPlayer, OverlayPlayer) that displayed raw Howler.js error codes
|
||||
- **Enrichment failure notifications:** Replaced aggressive per-cycle error banner with a single notification through the notification system when enrichment completes with failures
|
||||
|
||||
## [1.3.9] - 2026-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Audio analysis cleanup:** Fixed race condition in audio analysis cleanup that could reset tracks still being processed
|
||||
|
||||
## [1.3.8] - 2026-02-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Enrichment:** CLAP queue and failure cleanup fixes for enrichment debug mode
|
||||
|
||||
## [1.3.7] - 2026-02-01
|
||||
|
||||
### Added
|
||||
@@ -69,6 +102,34 @@ Automatic detection of available analyzers with graceful degradation.
|
||||
- **Docker Profiles:** Replaced Docker profiles with override file approach for better compatibility
|
||||
- **Mood Columns:** Marked as legacy in schema - may be derived from CLAP embeddings in future
|
||||
|
||||
## [1.3.5] - 2026-01-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Audio preload:** Emit preload 'load' event asynchronously to prevent race condition during gapless playback
|
||||
|
||||
## [1.3.4] - 2026-01-22
|
||||
|
||||
### Added
|
||||
|
||||
- **Gapless playback:** Preload infrastructure and next-track preloading for seamless transitions
|
||||
- **Infinite scroll:** Library artists, albums, and tracks now use infinite query pagination
|
||||
- **CachedImage:** Migrated to Next.js Image component with proper type support
|
||||
|
||||
### Fixed
|
||||
|
||||
- **CSS hover performance:** Fixed hover state performance issues
|
||||
- **Audio analyzer:** Fixed Enhanced mode detection
|
||||
- **Onboarding:** Accessibility improvements
|
||||
- **Audio format detection:** Simplified to prevent wrong decoder attempts
|
||||
- **Audio cleanup:** Improved Howl instance cleanup to prevent memory leaks
|
||||
- **Audio cleanup tracking:** Use Set for pending cleanup tracking
|
||||
- **Redis connections:** Disconnect enrichmentStateService connections on shutdown
|
||||
|
||||
### Changed
|
||||
|
||||
- **Library page:** Optimized data fetching with tab-based queries and memoized delete handlers
|
||||
|
||||
## [1.3.3] - 2026-01-18
|
||||
|
||||
Comprehensive patch release addressing critical stability issues, performance improvements, and production readiness fixes. This release includes community-contributed fixes and extensive internal code quality improvements.
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ RUN npm ci && npm cache clean --force
|
||||
COPY frontend/ ./
|
||||
|
||||
# Build Next.js (production)
|
||||
ENV NEXT_PUBLIC_API_URL=
|
||||
ENV NEXT_PUBLIC_BACKEND_URL=http://127.0.0.1:3006
|
||||
RUN npm run build
|
||||
|
||||
# ============================================
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lidify-backend",
|
||||
"version": "1.3.7",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lidify-backend",
|
||||
"version": "1.3.7",
|
||||
"version": "1.4.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lidify-backend",
|
||||
"version": "1.3.8",
|
||||
"version": "1.4.0",
|
||||
"description": "Lidify backend API server",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
|
||||
@@ -58,10 +58,6 @@ function deezerRadioToUnified(radio: DeezerRadioStation): PlaylistPreview {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Playlist Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/browse/playlists/featured
|
||||
* Get featured/chart playlists from Deezer
|
||||
@@ -138,10 +134,6 @@ router.get("/playlists/:id", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Radio Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/browse/radios
|
||||
* Get all radio stations (mood/theme based mixes)
|
||||
@@ -215,10 +207,6 @@ router.get("/radios/:id", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Genre Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/browse/genres
|
||||
* Get all available genres
|
||||
@@ -296,10 +284,6 @@ router.get("/genres/:id/playlists", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// URL Parsing (supports both Spotify & Deezer)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /api/browse/playlists/parse
|
||||
* Parse a Spotify or Deezer URL and return playlist info
|
||||
@@ -343,10 +327,6 @@ router.post("/playlists/parse", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Combined Browse Endpoint (for frontend convenience)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /api/browse/all
|
||||
* Get a combined view of featured content (playlists, genres)
|
||||
|
||||
@@ -942,9 +942,8 @@ router.get("/artists/:id", async (req, res) => {
|
||||
return res.status(404).json({ error: "Artist not found" });
|
||||
}
|
||||
|
||||
// ========== DISCOGRAPHY HANDLING ==========
|
||||
// For enriched artists with ownedAlbums, skip expensive MusicBrainz calls
|
||||
// Only fetch from MusicBrainz if the artist hasn't been enriched yet
|
||||
// For enriched artists with ownedAlbums, skip expensive MusicBrainz calls.
|
||||
// Only fetch from MusicBrainz if the artist hasn't been enriched yet.
|
||||
let albumsWithOwnership = [];
|
||||
const ownedRgMbids = new Set(artist.ownedAlbums.map((o) => o.rgMbid));
|
||||
const isEnriched =
|
||||
@@ -994,8 +993,7 @@ router.get("/artists/:id", async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ALWAYS include albums from database (actual owned files) ==========
|
||||
// These are albums with actual tracks on disk - they MUST show as owned
|
||||
// Albums from database have actual tracks on disk - they MUST show as owned
|
||||
const dbAlbums = artist.albums.map((album) => ({
|
||||
...album,
|
||||
owned: true, // If it's in the database with tracks, user owns it!
|
||||
@@ -1007,7 +1005,6 @@ router.get("/artists/:id", async (req, res) => {
|
||||
`[Artist] Found ${dbAlbums.length} albums from database (actual owned files)`
|
||||
);
|
||||
|
||||
// ========== Supplement with MusicBrainz discography for "available to download" ==========
|
||||
// Always fetch discography if we have a valid MBID - users need to see what's available
|
||||
const hasDbAlbums = dbAlbums.length > 0;
|
||||
const shouldFetchDiscography =
|
||||
@@ -1286,15 +1283,12 @@ router.get("/artists/:id", async (req, res) => {
|
||||
}));
|
||||
}
|
||||
|
||||
// ========== HERO IMAGE FETCHING ==========
|
||||
// Use DataCacheService: DB -> Redis -> API -> save to both
|
||||
const heroUrl = await dataCacheService.getArtistImage(
|
||||
artist.id,
|
||||
artist.name,
|
||||
effectiveMbid
|
||||
);
|
||||
|
||||
// ========== SIMILAR ARTISTS (from enriched JSON or Last.fm API) ==========
|
||||
let similarArtists: any[] = [];
|
||||
const similarCacheKey = `similar-artists:${artist.id}`;
|
||||
|
||||
|
||||
@@ -440,10 +440,6 @@ router.post("/mood/save-preferences", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// NEW SIMPLIFIED MOOD BUCKET ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /mixes/mood/buckets/presets:
|
||||
|
||||
@@ -132,10 +132,6 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Download History Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /notifications/downloads/history
|
||||
* Get completed/failed downloads that haven't been cleared
|
||||
|
||||
@@ -537,10 +537,6 @@ router.put("/:id/items/reorder", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Pending Tracks (from Spotify imports)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /playlists/:id/pending
|
||||
* Get pending tracks for a playlist (tracks from Spotify that haven't been matched yet)
|
||||
|
||||
@@ -115,9 +115,8 @@ router.get("/for-you", async (req, res) => {
|
||||
albumCounts.map((ac) => [ac.artistId, ac._count.rgMbid])
|
||||
);
|
||||
|
||||
// ========== CACHE-ONLY IMAGE LOOKUP FOR RECOMMENDATIONS ==========
|
||||
// Only use cached data (DB heroUrl or Redis cache) - no API calls during page loads
|
||||
// Background enrichment worker will populate cache over time
|
||||
// Only use cached data (DB heroUrl or Redis cache) - no API calls during page loads.
|
||||
// Background enrichment worker will populate cache over time.
|
||||
const { redisClient } = await import("../utils/redis");
|
||||
|
||||
// Get all cached images in a single Redis call for efficiency
|
||||
|
||||
@@ -21,10 +21,6 @@ import { lastFmService } from "./lastfm";
|
||||
import { AcquisitionError, AcquisitionErrorType } from "./lidarr";
|
||||
import PQueue from "p-queue";
|
||||
|
||||
// ============================================
|
||||
// TYPE DEFINITIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Context for tracking acquisition origin
|
||||
* Used to link download jobs to their source (Discovery batch or Spotify import)
|
||||
@@ -89,10 +85,6 @@ interface DownloadBehavior {
|
||||
fallbackSource: "soulseek" | "lidarr" | null;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ACQUISITION SERVICE
|
||||
// ============================================
|
||||
|
||||
class AcquisitionService {
|
||||
private albumQueue: PQueue;
|
||||
private lastConcurrency: number = 4;
|
||||
|
||||
@@ -118,18 +118,14 @@ export class AudiobookCacheService {
|
||||
* Sync a single audiobook
|
||||
*/
|
||||
private async syncAudiobook(book: any): Promise<void> {
|
||||
// Extract metadata from Audiobookshelf API response structure
|
||||
// The API returns: { id, media: { metadata: { title, author, ... } } }
|
||||
const metadata = book.media?.metadata || book;
|
||||
const title = metadata.title || book.title;
|
||||
|
||||
// Skip if no title (invalid audiobook data)
|
||||
if (!title) {
|
||||
logger.warn(` Skipping audiobook ${book.id} - missing title`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract additional fields from API response
|
||||
const author = metadata.authorName || metadata.author || null;
|
||||
const narrator = metadata.narratorName || metadata.narrator || null;
|
||||
const description = metadata.description || null;
|
||||
@@ -148,22 +144,16 @@ export class AudiobookCacheService {
|
||||
const size = book.size ? BigInt(book.size) : null;
|
||||
const libraryId = book.libraryId || null;
|
||||
|
||||
// Get cover path - Audiobookshelf uses media.coverPath
|
||||
const coverPath = book.media?.coverPath || null;
|
||||
|
||||
// Build full cover URL for download (needs to be absolute URL with base)
|
||||
const coverUrl = coverPath ? `items/${book.id}/cover` : null;
|
||||
|
||||
// Series info - Audiobookshelf returns seriesName as a string like "Series Name #2"
|
||||
// We need to parse this to extract the series name and sequence number
|
||||
// Parse series name and sequence from seriesName string (e.g. "Series Name #2")
|
||||
let series: string | null = null;
|
||||
let seriesSequence: string | null = null;
|
||||
|
||||
if (metadata.seriesName && typeof metadata.seriesName === "string") {
|
||||
const seriesStr = metadata.seriesName.trim();
|
||||
|
||||
// Try to extract sequence from patterns like:
|
||||
// "Series Name #1", "Series Name #2", "Series Name Book 1", "Series Name, Book 1"
|
||||
const sequencePatterns = [
|
||||
/^(.+?)\s*#(\d+(?:\.\d+)?)\s*$/, // "Series Name #1" or "Series Name #1.5"
|
||||
/^(.+?)\s*,?\s*Book\s*(\d+(?:\.\d+)?)\s*$/i, // "Series Name Book 1" or "Series Name, Book 1"
|
||||
@@ -189,7 +179,6 @@ export class AudiobookCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check metadata.series array/object format
|
||||
if (!series) {
|
||||
if (Array.isArray(metadata.series) && metadata.series.length > 0) {
|
||||
series = metadata.series[0]?.name || null;
|
||||
@@ -204,7 +193,6 @@ export class AudiobookCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
// Log series info for debugging (only for first few books)
|
||||
if (series) {
|
||||
logger.debug(
|
||||
` [Series] "${title}" -> "${series}" #${
|
||||
@@ -213,10 +201,8 @@ export class AudiobookCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
// Download cover image if available - need to construct full URL
|
||||
let localCoverPath: string | null = null;
|
||||
if (coverUrl) {
|
||||
// Get the Audiobookshelf base URL from the service
|
||||
const fullCoverUrl = await this.getFullCoverUrl(coverUrl);
|
||||
if (fullCoverUrl) {
|
||||
localCoverPath = await this.downloadCover(
|
||||
|
||||
@@ -11,10 +11,6 @@ import { redisClient } from "../utils/redis";
|
||||
|
||||
const DEEZER_API = "https://api.deezer.com";
|
||||
|
||||
// ============================================
|
||||
// Playlist Types
|
||||
// ============================================
|
||||
|
||||
export interface DeezerTrack {
|
||||
deezerId: string;
|
||||
title: string;
|
||||
@@ -68,10 +64,6 @@ export interface DeezerGenreWithRadios {
|
||||
radios: DeezerRadioStation[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Service Class
|
||||
// ============================================
|
||||
|
||||
class DeezerService {
|
||||
private readonly cachePrefix = "deezer:";
|
||||
private readonly cacheTTL = 86400; // 24 hours
|
||||
@@ -98,10 +90,6 @@ class DeezerService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Image & Preview Methods (existing functionality)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Search for an artist and get their image URL
|
||||
*/
|
||||
@@ -250,10 +238,6 @@ class DeezerService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Playlist Methods (new functionality)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Parse a Deezer URL and extract the type and ID
|
||||
*/
|
||||
@@ -477,10 +461,6 @@ class DeezerService {
|
||||
return this.searchPlaylists(genreName, limit);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Radio Methods
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get all radio stations (mood/theme based mixes)
|
||||
* Cached for 24 hours
|
||||
|
||||
@@ -938,12 +938,6 @@ export class DiscoverWeeklyService {
|
||||
return;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// PLAYLIST COMPOSITION: ALL Discovery + ~20% Anchors
|
||||
// ONE TRACK PER ALBUM - Each album contributes only 1 track
|
||||
// Include ALL successfully downloaded albums!
|
||||
// ==============================================
|
||||
|
||||
// Group tracks by album ID and pick ONE random track per album
|
||||
const tracksByAlbum = new Map<string, typeof allTracks>();
|
||||
for (const track of allTracks) {
|
||||
@@ -2298,9 +2292,6 @@ export class DiscoverWeeklyService {
|
||||
` Total similar artists from all seeds: ${allSimilarArtists.length}`
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// PASS 1: NEW ARTISTS ONLY (true discovery)
|
||||
// ============================================
|
||||
logger.debug(`\n === PASS 1: NEW Artists Only ===`);
|
||||
|
||||
for (const sim of allSimilarArtists) {
|
||||
@@ -2356,9 +2347,6 @@ export class DiscoverWeeklyService {
|
||||
` Pass 1 complete: ${recommendations.length}/${targetCount} from NEW artists`
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// PASS 2: EXISTING ARTISTS (fallback if needed)
|
||||
// ============================================
|
||||
if (
|
||||
recommendations.length < targetCount &&
|
||||
existingArtistsForFallback.length > 0
|
||||
@@ -2609,11 +2597,6 @@ export class DiscoverWeeklyService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MULTI-STRATEGY DISCOVERY ENGINE
|
||||
// Rotates weekly to keep recommendations fresh
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get user's top genres from listening history
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { existsSync } from "fs";
|
||||
import { redisClient } from "../utils/redis";
|
||||
import { prisma } from "../utils/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// Analyzer script paths in the Docker image
|
||||
const ESSENTIA_ANALYZER_PATH = "/app/audio-analyzer/analyzer.py";
|
||||
const CLAP_ANALYZER_PATH = "/app/audio-analyzer-clap/analyzer.py";
|
||||
|
||||
export interface AvailableFeatures {
|
||||
musicCNN: boolean;
|
||||
vibeEmbeddings: boolean;
|
||||
@@ -37,6 +42,11 @@ class FeatureDetectionService {
|
||||
|
||||
private async checkMusicCNN(): Promise<boolean> {
|
||||
try {
|
||||
// Analyzer script bundled in image = feature is available
|
||||
if (existsSync(ESSENTIA_ANALYZER_PATH)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const heartbeat = await redisClient.get("audio:worker:heartbeat");
|
||||
if (heartbeat) {
|
||||
const timestamp = parseInt(heartbeat, 10);
|
||||
@@ -58,6 +68,11 @@ class FeatureDetectionService {
|
||||
|
||||
private async checkCLAP(): Promise<boolean> {
|
||||
try {
|
||||
// Analyzer script bundled in image = feature is available
|
||||
if (existsSync(CLAP_ANALYZER_PATH)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const heartbeat = await redisClient.get("clap:worker:heartbeat");
|
||||
if (heartbeat) {
|
||||
const timestamp = parseInt(heartbeat, 10);
|
||||
|
||||
@@ -4,10 +4,6 @@ import { config } from "../config";
|
||||
import { getSystemSettings } from "../utils/systemSettings";
|
||||
import { stripAlbumEdition } from "../utils/artistNormalization";
|
||||
|
||||
// ============================================
|
||||
// STRUCTURED ERROR TYPES
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Error types for music acquisition failures
|
||||
* Used to determine fallback strategies
|
||||
@@ -1162,9 +1158,6 @@ class LidarrService {
|
||||
// Use the verified album data
|
||||
const updatedAlbum = verifyResponse.data;
|
||||
|
||||
// ============================================================
|
||||
// PHASE 2.1: Proactive anyReleaseOk for edition variants
|
||||
// ============================================================
|
||||
const editionPatterns = [
|
||||
/\(remaster/i,
|
||||
/\(deluxe/i,
|
||||
@@ -1264,9 +1257,6 @@ class LidarrService {
|
||||
);
|
||||
|
||||
if (result.message?.includes("0 reports")) {
|
||||
// ============================================================
|
||||
// PHASE 2.3: Enhanced diagnostics for 0 reports
|
||||
// ============================================================
|
||||
try {
|
||||
const albumDetails = await this.client.get(
|
||||
`/api/v1/album/${updatedAlbum.id}`
|
||||
@@ -1351,9 +1341,6 @@ class LidarrService {
|
||||
);
|
||||
|
||||
if (retryResult.message?.includes("0 reports")) {
|
||||
// ============================================================
|
||||
// PHASE 2.2: Fallback to base album title
|
||||
// ============================================================
|
||||
const baseAlbumTitle = this.extractBaseTitle(albumTitle);
|
||||
|
||||
if (baseAlbumTitle !== albumTitle && baseAlbumTitle.length > 2) {
|
||||
@@ -1797,10 +1784,6 @@ class LidarrService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Tag Management Methods (for discovery tracking)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get all tags from Lidarr
|
||||
*/
|
||||
@@ -2097,10 +2080,6 @@ class LidarrService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Release Iteration Methods (for exhaustive retry)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get all available releases for an album from all indexers
|
||||
* This is what Lidarr's "Interactive Search" uses
|
||||
@@ -2349,10 +2328,6 @@ class LidarrService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BATCH RECONCILIATION METHODS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Fetch all data needed for reconciliation in minimal API calls.
|
||||
* Returns indexed Maps for O(1) lookups against job data.
|
||||
@@ -2558,10 +2533,6 @@ export interface LidarrRelease {
|
||||
|
||||
export const lidarrService = new LidarrService();
|
||||
|
||||
// ============================================
|
||||
// Queue Cleaner Functions
|
||||
// ============================================
|
||||
|
||||
// Types for queue monitoring
|
||||
interface QueueItem {
|
||||
id: number;
|
||||
|
||||
@@ -1513,9 +1513,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AUDIO ANALYSIS-BASED MIXES (Using Essentia features)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Generate "High Energy" mix using audio analysis
|
||||
@@ -2200,9 +2198,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LAST.FM TAG-BASED MIXES
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Generate mix based on Last.fm mood tags
|
||||
@@ -2361,9 +2357,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DAY-OF-WEEK MIXES
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Generate day-specific mix based on the current day
|
||||
@@ -2531,10 +2525,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CURATED VIBE MIXES (Daily, 10 tracks)
|
||||
// These are "mood" mixes based on audio analysis and vibes
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* "Sad Girl Sundays" - Melancholic introspection
|
||||
@@ -3253,9 +3244,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WEEKLY CURATED MIXES (20 tracks)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* "Deep Cuts" - Hidden gems from your library
|
||||
@@ -3574,9 +3563,7 @@ export class ProgrammaticPlaylistService {
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MOOD ON DEMAND - Generate a mix based on specific criteria
|
||||
// ============================================================
|
||||
// MOOD ON DEMAND
|
||||
|
||||
/**
|
||||
* Generate a custom mood mix based on audio feature parameters
|
||||
|
||||
@@ -373,7 +373,6 @@ export async function enrichSimilarArtist(artist: Artist): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// ========== ALBUM COVER ENRICHMENT ==========
|
||||
// Fetch covers for all albums belonging to this artist that don't have covers yet
|
||||
await enrichAlbumCovers(artist.id, localHeroUrl);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import pLimit from "p-limit";
|
||||
// Configuration
|
||||
const ARTIST_BATCH_SIZE = 10;
|
||||
const TRACK_BATCH_SIZE = 20;
|
||||
const ENRICHMENT_INTERVAL_MS = 30 * 1000; // 30 seconds
|
||||
const ENRICHMENT_INTERVAL_MS = 5 * 1000; // 5 seconds - rate limiter handles API limits
|
||||
const MAX_CONSECUTIVE_SYSTEM_FAILURES = 5; // Circuit breaker threshold
|
||||
|
||||
let isRunning = false;
|
||||
@@ -55,6 +55,9 @@ let currentBatchFailures: BatchFailures = {
|
||||
audio: [],
|
||||
};
|
||||
|
||||
// Session-level failure counter (accumulates across cycles, reset on enrichment start)
|
||||
let sessionFailureCount = { artists: 0, tracks: 0, audio: 0 };
|
||||
|
||||
// Mood tags to extract from Last.fm
|
||||
const MOOD_TAGS = new Set([
|
||||
// Energy/Activity
|
||||
@@ -215,8 +218,17 @@ export async function startUnifiedEnrichmentWorker() {
|
||||
logger.debug(` Interval: ${ENRICHMENT_INTERVAL_MS / 1000}s`);
|
||||
logger.debug("");
|
||||
|
||||
// Initialize state
|
||||
await enrichmentStateService.initializeState();
|
||||
// Check if there's existing state that might be problematic
|
||||
const existingState = await enrichmentStateService.getState();
|
||||
|
||||
// Only clear state if it exists and is in a non-idle state
|
||||
// This prevents clearing fresh state from a previous worker instance
|
||||
if (existingState && existingState.status !== "idle") {
|
||||
await enrichmentStateService.clear();
|
||||
}
|
||||
|
||||
// Initialize state
|
||||
await enrichmentStateService.initializeState();
|
||||
|
||||
// Setup control channel subscription
|
||||
await setupControlChannel();
|
||||
@@ -377,88 +389,68 @@ async function runEnrichmentCycle(fullMode: boolean): Promise<{
|
||||
tracks: number;
|
||||
audioQueued: number;
|
||||
}> {
|
||||
// Check if paused
|
||||
const emptyResult = { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
|
||||
// Sync local pause flag with state service
|
||||
if (!isPaused) {
|
||||
const state = await enrichmentStateService.getState();
|
||||
if (state?.status === "paused" || state?.status === "stopping") {
|
||||
isPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// Check state service
|
||||
const state = await enrichmentStateService.getState();
|
||||
if (state?.status === "paused" || state?.status === "stopping") {
|
||||
isPaused = true;
|
||||
return { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
// Skip if already running (unless full mode or immediate request)
|
||||
const bypassRunningCheck = fullMode || immediateEnrichmentRequested;
|
||||
if (isRunning && !bypassRunningCheck) {
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// Allow immediate enrichment requests to bypass the isRunning check
|
||||
// This prevents race conditions when new content is imported
|
||||
if (isRunning && !fullMode && !immediateEnrichmentRequested) {
|
||||
return { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
}
|
||||
|
||||
// Enforce minimum interval between cycles (unless full mode or immediate request)
|
||||
// Enforce minimum interval (unless full mode or immediate request)
|
||||
const now = Date.now();
|
||||
if (
|
||||
!fullMode &&
|
||||
!immediateEnrichmentRequested &&
|
||||
now - lastRunTime < MIN_INTERVAL_MS
|
||||
) {
|
||||
return { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
if (!bypassRunningCheck && now - lastRunTime < MIN_INTERVAL_MS) {
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// Clear the immediate request flag
|
||||
immediateEnrichmentRequested = false;
|
||||
lastRunTime = now;
|
||||
|
||||
isRunning = true;
|
||||
|
||||
let artistsProcessed = 0;
|
||||
let tracksProcessed = 0;
|
||||
let audioQueued = 0;
|
||||
|
||||
try {
|
||||
// Reset system failure counter on successful cycle start
|
||||
consecutiveSystemFailures = 0;
|
||||
try {
|
||||
consecutiveSystemFailures = 0;
|
||||
|
||||
const artistsPhase = await enrichArtistsPhase();
|
||||
if (!artistsPhase.shouldContinue) {
|
||||
return {
|
||||
artists: artistsPhase.result,
|
||||
tracks: 0,
|
||||
audioQueued: 0,
|
||||
};
|
||||
}
|
||||
artistsProcessed = artistsPhase.result;
|
||||
// Run phases sequentially, halting if stopped/paused
|
||||
const artistResult = await runPhase("artists", executeArtistsPhase);
|
||||
if (artistResult === null) {
|
||||
return { artists: 0, tracks: 0, audioQueued: 0 };
|
||||
}
|
||||
artistsProcessed = artistResult;
|
||||
|
||||
const moodTagsPhase = await enrichMoodTagsPhase();
|
||||
if (!moodTagsPhase.shouldContinue) {
|
||||
return {
|
||||
artists: artistsProcessed,
|
||||
tracks: moodTagsPhase.result,
|
||||
audioQueued: 0,
|
||||
};
|
||||
}
|
||||
tracksProcessed = moodTagsPhase.result;
|
||||
const trackResult = await runPhase("tracks", executeMoodTagsPhase);
|
||||
if (trackResult === null) {
|
||||
return { artists: artistsProcessed, tracks: 0, audioQueued: 0 };
|
||||
}
|
||||
tracksProcessed = trackResult;
|
||||
|
||||
const audioPhase = await enrichAudioPhase();
|
||||
if (!audioPhase.shouldContinue) {
|
||||
return {
|
||||
artists: artistsProcessed,
|
||||
tracks: tracksProcessed,
|
||||
audioQueued: audioPhase.result,
|
||||
};
|
||||
}
|
||||
audioQueued = audioPhase.result;
|
||||
const audioResult = await runPhase("audio", executeAudioPhase);
|
||||
if (audioResult === null) {
|
||||
return { artists: artistsProcessed, tracks: tracksProcessed, audioQueued: 0 };
|
||||
}
|
||||
audioQueued = audioResult;
|
||||
|
||||
const vibePhase = await enrichVibePhase();
|
||||
if (!vibePhase.shouldContinue) {
|
||||
return {
|
||||
artists: artistsProcessed,
|
||||
tracks: tracksProcessed,
|
||||
audioQueued: audioQueued,
|
||||
};
|
||||
}
|
||||
|
||||
const features = await featureDetection.getFeatures();
|
||||
const vibeQueued = vibePhase.result;
|
||||
const vibeResult = await runPhase("vibe", executeVibePhase);
|
||||
if (vibeResult === null) {
|
||||
return { artists: artistsProcessed, tracks: tracksProcessed, audioQueued };
|
||||
}
|
||||
const vibeQueued = vibeResult;
|
||||
const features = await featureDetection.getFeatures();
|
||||
|
||||
// Log progress (only if work was done)
|
||||
if (artistsProcessed > 0 || tracksProcessed > 0 || audioQueued > 0 || vibeQueued > 0) {
|
||||
@@ -500,51 +492,18 @@ try {
|
||||
},
|
||||
completionNotificationSent: false, // Reset flag when new work is processed
|
||||
});
|
||||
|
||||
// Reset session failure counter when new work begins
|
||||
sessionFailureCount = { artists: 0, tracks: 0, audio: 0 };
|
||||
}
|
||||
|
||||
// Send failure notification if there were any failures in this batch
|
||||
const totalFailures =
|
||||
currentBatchFailures.artists.length +
|
||||
currentBatchFailures.tracks.length +
|
||||
currentBatchFailures.audio.length;
|
||||
// Accumulate cycle failures into session counter before resetting
|
||||
sessionFailureCount.artists += currentBatchFailures.artists.length;
|
||||
sessionFailureCount.tracks += currentBatchFailures.tracks.length;
|
||||
sessionFailureCount.audio += currentBatchFailures.audio.length;
|
||||
|
||||
if (totalFailures > 0) {
|
||||
try {
|
||||
const failureCounts =
|
||||
await enrichmentFailureService.getFailureCounts();
|
||||
|
||||
const { notificationService } =
|
||||
await import("../services/notificationService");
|
||||
const users = await prisma.user.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
for (const user of users) {
|
||||
await notificationService.create({
|
||||
userId: user.id,
|
||||
type: "error",
|
||||
title: "Enrichment Completed with Errors",
|
||||
message: `${failureCounts.total} items failed enrichment. Click to view and retry.`,
|
||||
metadata: {
|
||||
actionUrl: "/settings#enrichment-failures",
|
||||
actionLabel: "View Failures",
|
||||
failureCounts,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`[Enrichment] Failure notification sent: ${totalFailures} failures in batch`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[Enrichment] Failed to send failure notification:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// Reset batch failures
|
||||
currentBatchFailures = { artists: [], tracks: [], audio: [] };
|
||||
}
|
||||
// Reset batch failures (failures are viewable in Settings > Enrichment)
|
||||
currentBatchFailures = { artists: [], tracks: [], audio: [] };
|
||||
|
||||
// If everything is complete, mark as idle and send notification (only once)
|
||||
const progress = await getEnrichmentProgress();
|
||||
@@ -613,7 +572,26 @@ try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
const totalSessionFailures =
|
||||
sessionFailureCount.artists +
|
||||
sessionFailureCount.tracks +
|
||||
sessionFailureCount.audio;
|
||||
|
||||
for (const user of users) {
|
||||
if (totalSessionFailures > 0) {
|
||||
const parts: string[] = [];
|
||||
if (sessionFailureCount.artists > 0) parts.push(`${sessionFailureCount.artists} artist(s)`);
|
||||
if (sessionFailureCount.tracks > 0) parts.push(`${sessionFailureCount.tracks} track(s)`);
|
||||
if (sessionFailureCount.audio > 0) parts.push(`${sessionFailureCount.audio} audio analysis`);
|
||||
|
||||
await notificationService.create({
|
||||
userId: user.id,
|
||||
type: "error",
|
||||
title: "Enrichment Completed with Errors",
|
||||
message: `${totalSessionFailures} failures: ${parts.join(", ")}. Check Settings > Enrichment for details.`,
|
||||
});
|
||||
}
|
||||
|
||||
await notificationService.notifySystem(
|
||||
user.id,
|
||||
"Enrichment Complete",
|
||||
@@ -1035,63 +1013,52 @@ async function queueVibeEmbeddings(): Promise<number> {
|
||||
return queued;
|
||||
}
|
||||
|
||||
interface EnrichmentPhaseResult {
|
||||
result: number;
|
||||
shouldContinue: boolean;
|
||||
/**
|
||||
* Check if enrichment should stop and handle state cleanup if stopping.
|
||||
* Returns true if cycle should halt (either stopping or paused).
|
||||
*/
|
||||
async function shouldHaltCycle(): Promise<boolean> {
|
||||
if (isStopping) {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "idle",
|
||||
currentPhase: null,
|
||||
});
|
||||
isStopping = false;
|
||||
return true;
|
||||
}
|
||||
return isPaused;
|
||||
}
|
||||
|
||||
async function enrichArtistsPhase(): Promise<EnrichmentPhaseResult> {
|
||||
/**
|
||||
* Run a phase and return result. Returns null if cycle should halt.
|
||||
*/
|
||||
async function runPhase(
|
||||
phaseName: "artists" | "tracks" | "audio" | "vibe",
|
||||
executor: () => Promise<number>,
|
||||
): Promise<number | null> {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "running",
|
||||
currentPhase: "artists",
|
||||
currentPhase: phaseName,
|
||||
});
|
||||
|
||||
const result = await enrichArtistsBatch();
|
||||
const result = await executor();
|
||||
|
||||
if (isStopping) {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "idle",
|
||||
currentPhase: null,
|
||||
});
|
||||
isStopping = false;
|
||||
return { result, shouldContinue: false };
|
||||
if (await shouldHaltCycle()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
return { result, shouldContinue: true };
|
||||
return result;
|
||||
}
|
||||
|
||||
async function enrichMoodTagsPhase(): Promise<EnrichmentPhaseResult> {
|
||||
await enrichmentStateService.updateState({
|
||||
currentPhase: "tracks",
|
||||
});
|
||||
|
||||
const result = await enrichTrackTagsBatch();
|
||||
|
||||
if (isStopping) {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "idle",
|
||||
currentPhase: null,
|
||||
});
|
||||
isStopping = false;
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
return { result, shouldContinue: true };
|
||||
async function executeArtistsPhase(): Promise<number> {
|
||||
return enrichArtistsBatch();
|
||||
}
|
||||
|
||||
async function enrichAudioPhase(): Promise<EnrichmentPhaseResult> {
|
||||
await enrichmentStateService.updateState({
|
||||
currentPhase: "audio",
|
||||
});
|
||||
async function executeMoodTagsPhase(): Promise<number> {
|
||||
return enrichTrackTagsBatch();
|
||||
}
|
||||
|
||||
async function executeAudioPhase(): Promise<number> {
|
||||
const audioCompletedBefore = await prisma.track.count({
|
||||
where: { analysisStatus: "completed" },
|
||||
});
|
||||
@@ -1111,40 +1078,32 @@ async function enrichAudioPhase(): Promise<EnrichmentPhaseResult> {
|
||||
audioAnalysisCleanupService.recordSuccess();
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
if (audioAnalysisCleanupService.isCircuitOpen()) {
|
||||
logger.warn(
|
||||
"[Enrichment] Audio analysis circuit breaker OPEN - skipping queue",
|
||||
);
|
||||
} else {
|
||||
result = await queueAudioAnalysis();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isStopping) {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "idle",
|
||||
currentPhase: null,
|
||||
});
|
||||
isStopping = false;
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
return { result, shouldContinue: true };
|
||||
return queueAudioAnalysis();
|
||||
}
|
||||
|
||||
async function enrichVibePhase(): Promise<EnrichmentPhaseResult> {
|
||||
async function executeVibePhase(): Promise<number> {
|
||||
const features = await featureDetection.getFeatures();
|
||||
if (!features.vibeEmbeddings) {
|
||||
return { result: 0, shouldContinue: true };
|
||||
return 0;
|
||||
}
|
||||
|
||||
await enrichmentStateService.updateState({
|
||||
currentPhase: "vibe",
|
||||
const audioProcessing = await prisma.track.count({
|
||||
where: { analysisStatus: "processing" },
|
||||
});
|
||||
const audioQueue = await getRedis().llen("audio:analysis:queue");
|
||||
if (audioProcessing > 0 || audioQueue > 0) {
|
||||
logger.debug(
|
||||
`[Enrichment] Skipping vibe phase - audio still running (${audioProcessing} processing, ${audioQueue} queued)`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { reset } = await vibeAnalysisCleanupService.cleanupStaleProcessing();
|
||||
if (reset > 0) {
|
||||
@@ -1153,25 +1112,10 @@ async function enrichVibePhase(): Promise<EnrichmentPhaseResult> {
|
||||
|
||||
const result = await queueVibeEmbeddings();
|
||||
if (result > 0) {
|
||||
logger.debug(
|
||||
`[ENRICHMENT] Queued ${result} tracks for vibe embedding`
|
||||
);
|
||||
logger.debug(`[ENRICHMENT] Queued ${result} tracks for vibe embedding`);
|
||||
}
|
||||
|
||||
if (isStopping) {
|
||||
await enrichmentStateService.updateState({
|
||||
status: "idle",
|
||||
currentPhase: null,
|
||||
});
|
||||
isStopping = false;
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
return { result, shouldContinue: false };
|
||||
}
|
||||
|
||||
return { result, shouldContinue: true };
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,6 @@ import {
|
||||
AudioWaveform,
|
||||
} from "lucide-react";
|
||||
|
||||
// ============================================
|
||||
// TYPES
|
||||
// ============================================
|
||||
|
||||
interface TrackFeatures {
|
||||
energy: number;
|
||||
valence: number;
|
||||
@@ -132,9 +128,6 @@ function distanceToSimilarity(distance: number): number {
|
||||
return Math.max(0, 1 - distance / 2);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// COVER IMAGE
|
||||
// ============================================
|
||||
function CoverImage({
|
||||
coverUrl,
|
||||
title,
|
||||
@@ -182,9 +175,6 @@ function CoverImage({
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SIMILARITY BADGE - the signature element
|
||||
// ============================================
|
||||
function SimilarityBadge({ similarity, size = "md" }: { similarity: number; size?: "sm" | "md" | "lg" }) {
|
||||
const percent = Math.round(similarity * 100);
|
||||
const sizeClasses = {
|
||||
@@ -227,9 +217,6 @@ function SimilarityBadge({ similarity, size = "md" }: { similarity: number; size
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FEATURE COMPARISON - side by side bars
|
||||
// ============================================
|
||||
function FeatureComparison({
|
||||
source,
|
||||
match,
|
||||
@@ -278,9 +265,6 @@ function FeatureComparison({
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MOOD GRID - compact mood comparison
|
||||
// ============================================
|
||||
function MoodGrid({ source, match }: { source: TrackData; match: TrackData }) {
|
||||
const validMoods = MOOD_CONFIG.filter(({ key }) => {
|
||||
const sVal = source.features[key as keyof TrackFeatures];
|
||||
@@ -319,9 +303,6 @@ function MoodGrid({ source, match }: { source: TrackData; match: TrackData }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TAG PILLS - shared tags highlighted
|
||||
// ============================================
|
||||
function TagPills({ source, match }: { source: TrackData; match: TrackData }) {
|
||||
const sourceTags = source.lastfmTags || [];
|
||||
const matchTags = match.lastfmTags || [];
|
||||
@@ -360,9 +341,6 @@ function TagPills({ source, match }: { source: TrackData; match: TrackData }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// COMPARISON PANEL - the detailed view
|
||||
// ============================================
|
||||
function ComparisonPanel({
|
||||
source,
|
||||
match,
|
||||
@@ -478,9 +456,6 @@ function ComparisonPanel({
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TRACK ROW
|
||||
// ============================================
|
||||
function TrackRow({
|
||||
track,
|
||||
index,
|
||||
@@ -567,9 +542,6 @@ function TrackRow({
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MAIN PAGE
|
||||
// ============================================
|
||||
export default function VibePage() {
|
||||
const { vibeEmbeddings, loading: featuresLoading } = useFeatures();
|
||||
|
||||
|
||||
@@ -22,9 +22,7 @@ import {
|
||||
RotateCw,
|
||||
Loader2,
|
||||
AudioWaveform,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -251,38 +249,7 @@ export function FullPlayer() {
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0">
|
||||
{/* Error Banner */}
|
||||
{audioError && (
|
||||
<div className="bg-red-500/20 border-t border-red-500/30 px-4 py-1.5 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<span className="text-red-200 text-sm truncate">
|
||||
{audioError}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearAudioError();
|
||||
resume();
|
||||
}}
|
||||
className="px-2 py-1 text-xs text-red-200 hover:text-white hover:bg-red-500/30 transition rounded"
|
||||
aria-label="Retry playback"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAudioError}
|
||||
className="p-1 text-red-300 hover:text-white transition rounded"
|
||||
aria-label="Dismiss error"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn("bg-black border-t border-white/[0.08]", audioError ? "h-20" : "h-24")}>
|
||||
<div className="bg-black border-t border-white/[0.08] h-24">
|
||||
{/* Subtle top glow */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||
<div className="flex items-center h-full px-6 gap-6">
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
AlertTriangle,
|
||||
X,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -208,9 +207,6 @@ export function MiniPlayer() {
|
||||
|
||||
const seekEnabled = hasMedia && canSeek;
|
||||
|
||||
// ============================================
|
||||
// MOBILE/TABLET: Spotify-style compact player
|
||||
// ============================================
|
||||
if (isMobileOrTablet) {
|
||||
// Don't render if no media
|
||||
if (!hasMedia) return null;
|
||||
@@ -502,9 +498,6 @@ export function MiniPlayer() {
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DESKTOP: Full-featured mini player
|
||||
// ============================================
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Collapsible Vibe Panel - slides up from player */}
|
||||
@@ -570,38 +563,6 @@ export function MiniPlayer() {
|
||||
className="absolute top-0 left-0 right-0"
|
||||
/>
|
||||
|
||||
{/* Error Banner */}
|
||||
{audioError && (
|
||||
<div className="bg-red-500/20 border-b border-red-500/30 px-3 py-1.5 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<span className="text-red-200 text-xs truncate">
|
||||
{audioError}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearAudioError();
|
||||
resume();
|
||||
}}
|
||||
className="p-1 text-red-300 hover:text-white transition rounded"
|
||||
aria-label="Retry playback"
|
||||
title="Retry"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAudioError}
|
||||
className="p-1 text-red-300 hover:text-white transition rounded"
|
||||
aria-label="Dismiss error"
|
||||
title="Dismiss"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Player Content */}
|
||||
<div className="px-3 py-2.5 pt-3">
|
||||
|
||||
@@ -19,9 +19,7 @@ import {
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
RotateCw,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { formatTime, clampTime, formatTimeRemaining } from "@/utils/formatTime";
|
||||
import { cn } from "@/utils/cn";
|
||||
@@ -340,24 +338,6 @@ export function OverlayPlayer() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Error Banner */}
|
||||
{audioError && (
|
||||
<div className="mt-3 bg-red-500/20 border border-red-500/30 rounded-lg px-3 py-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<span className="text-red-200 text-sm truncate">
|
||||
{audioError}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearAudioError}
|
||||
className="p-1 text-red-300 hover:text-white transition rounded flex-shrink-0"
|
||||
aria-label="Dismiss error"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
|
||||
@@ -20,12 +20,6 @@ import { useQuery, useInfiniteQuery, useMutation, useQueryClient } from "@tansta
|
||||
import { api } from "@/lib/api";
|
||||
import type { Artist, Album, Track } from "@/features/library/types";
|
||||
|
||||
// ============================================================================
|
||||
// QUERY KEY FACTORIES
|
||||
// ============================================================================
|
||||
// These functions generate consistent query keys for caching
|
||||
// See: https://tanstack.com/query/latest/docs/react/guides/query-keys
|
||||
|
||||
export const queryKeys = {
|
||||
// Artist queries
|
||||
artist: (id: string) => ["artist", id] as const,
|
||||
@@ -102,10 +96,6 @@ export const queryKeys = {
|
||||
browseRadios: (limit?: number) => ["browse", "radios", limit] as const,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ARTIST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch artist data with automatic library/discovery fallback
|
||||
*
|
||||
@@ -174,10 +164,6 @@ export function useArtistDiscoveryQuery(nameOrMbid: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ALBUM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch album data with automatic library/discovery fallback
|
||||
*
|
||||
@@ -266,10 +252,6 @@ export function useAlbumsQuery(params?: {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIBRARY QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch recently listened items (Continue Listening)
|
||||
*
|
||||
@@ -308,10 +290,6 @@ export function useRecentlyAddedQuery(limit: number = 10) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIBRARY PAGE QUERIES (Artists/Albums/Tracks with pagination)
|
||||
// ============================================================================
|
||||
|
||||
export type LibraryFilter = "owned" | "discovery" | "all";
|
||||
export type SortOption = "name" | "name-desc" | "recent" | "tracks";
|
||||
|
||||
@@ -592,10 +570,6 @@ export function useLibraryTracksQuery({
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RECOMMENDATION QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch personalized recommendations
|
||||
*
|
||||
@@ -662,10 +636,6 @@ export function useSimilarAlbumsQuery(
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to search library with debouncing
|
||||
*
|
||||
@@ -722,10 +692,6 @@ export function useDiscoverSearchQuery(
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLAYLIST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all playlists
|
||||
*
|
||||
@@ -765,10 +731,6 @@ export function usePlaylistQuery(id: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MIX QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all mixes (Made For You)
|
||||
*
|
||||
@@ -805,10 +767,6 @@ export function useMixQuery(id: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POPULAR ARTISTS QUERY
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch popular artists from Last.fm
|
||||
*
|
||||
@@ -828,10 +786,6 @@ export function usePopularArtistsQuery(limit: number = 20) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUDIOBOOK QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all audiobooks
|
||||
*
|
||||
@@ -863,10 +817,6 @@ export function useAudiobookQuery(id: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PODCAST QUERIES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Hook to fetch all subscribed podcasts
|
||||
*
|
||||
@@ -930,11 +880,6 @@ export function useTopPodcastsQuery(limit: number = 20, genreId?: number) {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MUTATION HOOKS
|
||||
// ============================================================================
|
||||
// These hooks handle data modifications and automatically invalidate related queries
|
||||
|
||||
/**
|
||||
* Hook to refresh mixes with cache invalidation
|
||||
*
|
||||
@@ -1041,10 +986,6 @@ export function useDeletePlaylistMutation() {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BROWSE QUERIES (Deezer Playlists/Radios)
|
||||
// ============================================================================
|
||||
|
||||
interface PlaylistPreview {
|
||||
id: string;
|
||||
source: string;
|
||||
|
||||
@@ -1702,10 +1702,6 @@ class ApiClient {
|
||||
return this.delete(`/api-keys/${id}`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Notifications & Activity Panel
|
||||
// ============================================
|
||||
|
||||
async getNotifications(): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
|
||||
@@ -86,8 +86,10 @@ const nextConfig: NextConfig = {
|
||||
];
|
||||
},
|
||||
// Proxy API requests to backend (for Docker all-in-one container)
|
||||
// Use NEXT_PUBLIC_BACKEND_URL if set (build-time), otherwise default to localhost:3006
|
||||
// At runtime, Next.js will proxy /api/* requests to the backend
|
||||
async rewrites() {
|
||||
const backendUrl = process.env.BACKEND_URL || "http://127.0.0.1:3006";
|
||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://127.0.0.1:3006";
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
Generated
+63
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lidify-frontend",
|
||||
"version": "1.3.7",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lidify-frontend",
|
||||
"version": "1.3.7",
|
||||
"version": "1.4.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
@@ -1576,6 +1576,66 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
@@ -3673,6 +3733,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lidify-frontend",
|
||||
"version": "1.3.8",
|
||||
"version": "1.4.0",
|
||||
"description": "Lidify web frontend",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
|
||||
Generated
+21
-1
@@ -2,5 +2,25 @@
|
||||
"name": "lidify",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,14 @@ os.environ['NUMEXPR_MAX_THREADS'] = str(THREADS_PER_WORKER)
|
||||
import torch
|
||||
torch.set_num_threads(THREADS_PER_WORKER)
|
||||
|
||||
# Device detection - use GPU if available
|
||||
if torch.cuda.is_available():
|
||||
DEVICE = torch.device('cuda')
|
||||
GPU_NAME = torch.cuda.get_device_name(0)
|
||||
else:
|
||||
DEVICE = torch.device('cpu')
|
||||
GPU_NAME = None
|
||||
|
||||
import redis
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
@@ -104,10 +112,13 @@ class CLAPAnalyzer:
|
||||
)
|
||||
self.model.load_ckpt('/app/models/music_audioset_epoch_15_esc_90.14.pt')
|
||||
|
||||
# Move to CPU explicitly (we don't use GPU in this service)
|
||||
self.model = self.model.eval()
|
||||
# Move to detected device (GPU if available, else CPU)
|
||||
self.model = self.model.to(DEVICE).eval()
|
||||
|
||||
logger.info("CLAP model loaded successfully")
|
||||
if GPU_NAME:
|
||||
logger.info(f"CLAP model loaded successfully on GPU: {GPU_NAME}")
|
||||
else:
|
||||
logger.info("CLAP model loaded successfully on CPU")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load CLAP model: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audio analyzer service - Essentia-based analysis with TensorFlow ML models"""
|
||||
|
||||
# ============================================================================
|
||||
# CRITICAL: TensorFlow threading MUST be configured before any imports
|
||||
# Environment variables are read by TensorFlow C++ runtime before initialization
|
||||
# ============================================================================
|
||||
# CRITICAL: TensorFlow threading MUST be configured before any imports.
|
||||
# Environment variables are read by TensorFlow C++ runtime before initialization.
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -111,19 +109,31 @@ except ImportError as e:
|
||||
|
||||
# TensorFlow models via Essentia
|
||||
TF_MODELS_AVAILABLE = False
|
||||
TF_GPU_AVAILABLE = False
|
||||
TF_GPU_NAME = None
|
||||
TensorflowPredictMusiCNN = None
|
||||
try:
|
||||
import tensorflow as tf
|
||||
# Limit TensorFlow memory usage (CPU & GPU)
|
||||
try:
|
||||
gpus = tf.config.experimental.list_physical_devices('GPU')
|
||||
|
||||
# Detect and configure GPU
|
||||
gpus = tf.config.experimental.list_physical_devices('GPU')
|
||||
if gpus:
|
||||
TF_GPU_AVAILABLE = True
|
||||
TF_GPU_NAME = gpus[0].name
|
||||
# Enable memory growth to prevent TF from allocating all GPU memory
|
||||
for gpu in gpus:
|
||||
tf.config.experimental.set_memory_growth(gpu, True)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
tf.config.experimental.set_memory_growth(gpu, True)
|
||||
except RuntimeError:
|
||||
pass
|
||||
logger.info(f"TensorFlow GPU detected: {TF_GPU_NAME}")
|
||||
else:
|
||||
logger.info("TensorFlow running on CPU")
|
||||
|
||||
from essentia.standard import TensorflowPredictMusiCNN
|
||||
TF_MODELS_AVAILABLE = True
|
||||
logger.info("TensorflowPredictMusiCNN available - Enhanced mode enabled")
|
||||
device_str = f"GPU: {TF_GPU_NAME}" if TF_GPU_AVAILABLE else "CPU"
|
||||
logger.info(f"TensorflowPredictMusiCNN available - Enhanced mode enabled ({device_str})")
|
||||
except ImportError as e:
|
||||
logger.warning(f"TensorflowPredictMusiCNN not available: {e}")
|
||||
logger.info("Falling back to Standard mode")
|
||||
|
||||
Reference in New Issue
Block a user