Release v1.3.0: Multi-source downloads, audio analyzer resilience, mobile improvements
Major Features: - Multi-source download system (Soulseek/Lidarr with fallback) - Configurable enrichment speed control (1-5x) - Mobile touch drag support for seek sliders - iOS PWA media controls (Control Center, Lock Screen) - Artist name alias resolution via Last.fm - Circuit breaker pattern for audio analysis Critical Fixes: - Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM) - Discovery system race conditions and import failures - Radio decade categorization using originalYear - LastFM API response normalization - Mood bucket infinite loop prevention Security: - Bull Board admin authentication - Lidarr webhook signature verification - JWT token expiration and refresh - Encryption key validation on startup Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
-- Rename soulseekFallback to primaryFailureFallback (idempotent)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'SystemSettings' AND column_name = 'soulseekFallback'
|
||||
) THEN
|
||||
ALTER TABLE "SystemSettings" RENAME COLUMN "soulseekFallback" TO "primaryFailureFallback";
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Add tokenVersion to User table (idempotent)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'User')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'User' AND column_name = 'tokenVersion'
|
||||
) THEN
|
||||
ALTER TABLE "User" ADD COLUMN "tokenVersion" INTEGER NOT NULL DEFAULT 0;
|
||||
END IF;
|
||||
END $$;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
-- Create targetMbid index on DownloadJob (idempotent)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'DownloadJob')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE tablename = 'DownloadJob' AND indexname = 'DownloadJob_targetMbid_idx'
|
||||
) THEN
|
||||
CREATE INDEX "DownloadJob_targetMbid_idx" ON "DownloadJob"("targetMbid");
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -19,6 +19,7 @@ CREATE TABLE "User" (
|
||||
"twoFactorSecret" TEXT,
|
||||
"twoFactorRecoveryCodes" TEXT,
|
||||
"moodMixParams" JSONB,
|
||||
"tokenVersion" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
@@ -78,7 +79,7 @@ CREATE TABLE "SystemSettings" (
|
||||
"downloadRetryAttempts" INTEGER NOT NULL DEFAULT 3,
|
||||
"transcodeCacheMaxGb" INTEGER NOT NULL DEFAULT 10,
|
||||
"downloadSource" TEXT NOT NULL DEFAULT 'soulseek',
|
||||
"soulseekFallback" TEXT NOT NULL DEFAULT 'none',
|
||||
"primaryFailureFallback" TEXT NOT NULL DEFAULT 'none',
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -826,6 +827,9 @@ CREATE INDEX "DownloadJob_lidarrRef_idx" ON "DownloadJob"("lidarrRef");
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DownloadJob_artistMbid_idx" ON "DownloadJob"("artistMbid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "DownloadJob_targetMbid_idx" ON "DownloadJob"("targetMbid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ListeningState_userId_idx" ON "ListeningState"("userId");
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SystemSettings" ADD COLUMN "enrichmentConcurrency" INTEGER NOT NULL DEFAULT 1;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Album" ADD COLUMN "displayTitle" TEXT,
|
||||
ADD COLUMN "displayYear" INTEGER,
|
||||
ADD COLUMN "hasUserOverrides" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "userCoverUrl" TEXT,
|
||||
ADD COLUMN "userGenres" JSONB;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Artist" ADD COLUMN "displayName" TEXT,
|
||||
ADD COLUMN "hasUserOverrides" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "userGenres" JSONB,
|
||||
ADD COLUMN "userHeroUrl" TEXT,
|
||||
ADD COLUMN "userSummary" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Track" ADD COLUMN "displayTitle" TEXT,
|
||||
ADD COLUMN "displayTrackNo" INTEGER,
|
||||
ADD COLUMN "hasUserOverrides" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Album_hasUserOverrides_idx" ON "Album"("hasUserOverrides");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Artist_hasUserOverrides_idx" ON "Artist"("hasUserOverrides");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Track_hasUserOverrides_idx" ON "Track"("hasUserOverrides");
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
-- Migration: Add search vector triggers for podcasts and audiobooks
|
||||
-- This migration creates PostgreSQL functions and triggers to automatically
|
||||
-- populate and maintain search vectors for podcast and audiobook content
|
||||
|
||||
-- ============================================================================
|
||||
-- PODCAST SEARCH VECTOR FUNCTION
|
||||
-- ============================================================================
|
||||
-- Function to generate Podcast search vector from title, author, and description
|
||||
CREATE OR REPLACE FUNCTION podcast_search_vector_trigger() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
-- Combine title, author, and description into search vector
|
||||
-- Using setweight: title (A), author (B), description (C)
|
||||
NEW."searchVector" :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.author, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'C');
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to auto-update Podcast search vector
|
||||
DROP TRIGGER IF EXISTS podcast_search_vector_update ON "Podcast";
|
||||
CREATE TRIGGER podcast_search_vector_update
|
||||
BEFORE INSERT OR UPDATE OF title, author, description
|
||||
ON "Podcast"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION podcast_search_vector_trigger();
|
||||
|
||||
-- ============================================================================
|
||||
-- PODCAST EPISODE SEARCH VECTOR FUNCTION
|
||||
-- ============================================================================
|
||||
-- Function to generate PodcastEpisode search vector from title and description
|
||||
CREATE OR REPLACE FUNCTION podcast_episode_search_vector_trigger() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
-- Combine title and description into search vector
|
||||
-- Using setweight: title (A), description (B)
|
||||
NEW."searchVector" :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B');
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to auto-update PodcastEpisode search vector
|
||||
DROP TRIGGER IF EXISTS podcast_episode_search_vector_update ON "PodcastEpisode";
|
||||
CREATE TRIGGER podcast_episode_search_vector_update
|
||||
BEFORE INSERT OR UPDATE OF title, description
|
||||
ON "PodcastEpisode"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION podcast_episode_search_vector_trigger();
|
||||
|
||||
-- ============================================================================
|
||||
-- AUDIOBOOK SEARCH VECTOR FUNCTION
|
||||
-- ============================================================================
|
||||
-- Function to generate Audiobook search vector from title, author, narrator, series, and description
|
||||
CREATE OR REPLACE FUNCTION audiobook_search_vector_trigger() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
-- Combine title, author/narrator/series, and description into search vector
|
||||
-- Using setweight: title (A), author/narrator/series (B), description (C)
|
||||
NEW."searchVector" :=
|
||||
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.author, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.narrator, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.series, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'C');
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trigger to auto-update Audiobook search vector
|
||||
DROP TRIGGER IF EXISTS audiobook_search_vector_update ON "Audiobook";
|
||||
CREATE TRIGGER audiobook_search_vector_update
|
||||
BEFORE INSERT OR UPDATE OF title, author, narrator, series, description
|
||||
ON "Audiobook"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION audiobook_search_vector_trigger();
|
||||
|
||||
-- ============================================================================
|
||||
-- ADD SEARCH VECTOR COLUMNS
|
||||
-- ============================================================================
|
||||
-- Add searchVector column to Podcast table
|
||||
ALTER TABLE "Podcast" ADD COLUMN IF NOT EXISTS "searchVector" tsvector;
|
||||
|
||||
-- Add searchVector column to PodcastEpisode table
|
||||
ALTER TABLE "PodcastEpisode" ADD COLUMN IF NOT EXISTS "searchVector" tsvector;
|
||||
|
||||
-- Add searchVector column to Audiobook table
|
||||
ALTER TABLE "Audiobook" ADD COLUMN IF NOT EXISTS "searchVector" tsvector;
|
||||
|
||||
-- ============================================================================
|
||||
-- CREATE GIN INDEXES
|
||||
-- ============================================================================
|
||||
-- Create GIN index on Podcast search vector
|
||||
CREATE INDEX IF NOT EXISTS "Podcast_searchVector_idx" ON "Podcast" USING GIN ("searchVector");
|
||||
|
||||
-- Create GIN index on PodcastEpisode search vector
|
||||
CREATE INDEX IF NOT EXISTS "PodcastEpisode_searchVector_idx" ON "PodcastEpisode" USING GIN ("searchVector");
|
||||
|
||||
-- Create GIN index on Audiobook search vector
|
||||
CREATE INDEX IF NOT EXISTS "Audiobook_searchVector_idx" ON "Audiobook" USING GIN ("searchVector");
|
||||
|
||||
-- ============================================================================
|
||||
-- POPULATE EXISTING RECORDS
|
||||
-- ============================================================================
|
||||
-- Update all existing Podcasts to populate their search vectors
|
||||
UPDATE "Podcast"
|
||||
SET "searchVector" =
|
||||
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(author, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(description, '')), 'C');
|
||||
|
||||
-- Update all existing PodcastEpisodes to populate their search vectors
|
||||
UPDATE "PodcastEpisode"
|
||||
SET "searchVector" =
|
||||
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(description, '')), 'B');
|
||||
|
||||
-- Update all existing Audiobooks to populate their search vectors
|
||||
UPDATE "Audiobook"
|
||||
SET "searchVector" =
|
||||
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
||||
setweight(to_tsvector('english', COALESCE(author, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(narrator, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(series, '')), 'B') ||
|
||||
setweight(to_tsvector('english', COALESCE(description, '')), 'C');
|
||||
@@ -0,0 +1,32 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "EnrichmentFailure" (
|
||||
"id" TEXT NOT NULL,
|
||||
"entityType" TEXT NOT NULL,
|
||||
"entityId" TEXT NOT NULL,
|
||||
"entityName" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"errorCode" TEXT,
|
||||
"retryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxRetries" INTEGER NOT NULL DEFAULT 3,
|
||||
"firstFailedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastFailedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"skipped" BOOLEAN NOT NULL DEFAULT false,
|
||||
"skippedAt" TIMESTAMP(3),
|
||||
"resolved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"resolvedAt" TIMESTAMP(3),
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "EnrichmentFailure_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "EnrichmentFailure_entityType_resolved_idx" ON "EnrichmentFailure"("entityType", "resolved");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "EnrichmentFailure_skipped_idx" ON "EnrichmentFailure"("skipped");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "EnrichmentFailure_lastFailedAt_idx" ON "EnrichmentFailure"("lastFailedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EnrichmentFailure_entityType_entityId_key" ON "EnrichmentFailure"("entityType", "entityId");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Album" ADD COLUMN "originalYear" INTEGER;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SystemSettings" ADD COLUMN "lidarrWebhookSecret" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Track" ADD COLUMN "analysisStartedAt" TIMESTAMP(3);
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SystemSettings" ADD COLUMN "audioAnalyzerWorkers" INTEGER NOT NULL DEFAULT 2;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SystemSettings" ADD COLUMN "lastfmApiKey" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SystemSettings" ADD COLUMN "soulseekConcurrentDownloads" INTEGER NOT NULL DEFAULT 4;
|
||||
Reference in New Issue
Block a user