mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
- qwen-tts : GPU NVIDIA détecté automatiquement (bfloat16), image CUDA 12.8 (RTX 50xx comprises) via TORCH_VARIANT=cu128, docker-compose.gpu.yml dédié (clé obligatoire, choix de la carte) et guide d'installation Unraid. - Paramètres → « Qwen TTS (voix locale) » : adresse + clé du service, test de connexion (modèle et carte graphique affichés), stockés en base (app_config.qwen_tts_url / qwen_tts_api_key) et transmis à ffmpeg-api à chaque voix ; QWEN_TTS_URL reste un défaut. - Le service qwen-tts CPU du docker-compose principal devient optionnel (profil qwen-local). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
import { pool } from "./db";
|
|
|
|
export async function migrate() {
|
|
const client = await pool.connect();
|
|
try {
|
|
console.log("[Migration] Starting safe migration Check...");
|
|
|
|
// 1. Create Enums if not exist and Update Enums
|
|
await client.query(`
|
|
DO $$ BEGIN
|
|
CREATE TYPE "token_status" AS ENUM('valid', 'expiring', 'expired', 'error');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
`);
|
|
|
|
// Add 'reel' to post_type enum if not exists
|
|
await client.query(`ALTER TYPE "post_type" ADD VALUE IF NOT EXISTS 'reel';`);
|
|
|
|
// Add 'tiktok' to platform enum if not exists
|
|
await client.query(`ALTER TYPE "platform" ADD VALUE IF NOT EXISTS 'tiktok';`);
|
|
|
|
// 2. Create Tables if not exist
|
|
|
|
// post_analytics
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "post_analytics" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"post_id" varchar NOT NULL REFERENCES "posts"("id") ON DELETE cascade,
|
|
"fetched_at" timestamp DEFAULT now(),
|
|
"impressions" integer DEFAULT 0,
|
|
"reach" integer DEFAULT 0,
|
|
"engagement" integer DEFAULT 0,
|
|
"reactions" integer DEFAULT 0,
|
|
"comments" integer DEFAULT 0,
|
|
"shares" integer DEFAULT 0,
|
|
"clicks" integer DEFAULT 0,
|
|
"raw_data" jsonb
|
|
);
|
|
`);
|
|
|
|
// page_analytics_history
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "page_analytics_history" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"page_id" varchar NOT NULL REFERENCES "social_pages"("id") ON DELETE cascade,
|
|
"date" timestamp NOT NULL,
|
|
"followers_count" integer DEFAULT 0,
|
|
"page_views" integer DEFAULT 0,
|
|
"page_reach" integer DEFAULT 0,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// 3. Add Columns to existing tables if not exist
|
|
|
|
// social_pages.token_status
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "token_status" "token_status" DEFAULT 'valid';
|
|
`);
|
|
|
|
// social_pages.last_token_check
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "last_token_check" timestamp;
|
|
`);
|
|
|
|
// music_favorites
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "music_favorites" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
"track_id" text NOT NULL,
|
|
"title" text NOT NULL,
|
|
"artist" text NOT NULL,
|
|
"album_name" text,
|
|
"duration" integer NOT NULL,
|
|
"preview_url" text NOT NULL,
|
|
"download_url" text NOT NULL,
|
|
"image_url" text,
|
|
"license" text,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// freesound_config
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "freesound_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
"client_id" text NOT NULL,
|
|
"client_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now(),
|
|
CONSTRAINT freesound_config_user_id_unique UNIQUE ("user_id")
|
|
);
|
|
`);
|
|
|
|
// posts.generation_status (Reel progress tracking)
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_status" text;
|
|
`);
|
|
|
|
// posts.generation_progress
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_progress" integer DEFAULT 0;
|
|
`);
|
|
|
|
// posts.generation_error
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_error" text;
|
|
`);
|
|
|
|
// cloudinary_config.endpoint_url (MinIO internal endpoint URL)
|
|
await client.query(`
|
|
ALTER TABLE "cloudinary_config"
|
|
ADD COLUMN IF NOT EXISTS "endpoint_url" text;
|
|
`);
|
|
|
|
// cloudinary_config.public_url (MinIO public-facing base URL)
|
|
await client.query(`
|
|
ALTER TABLE "cloudinary_config"
|
|
ADD COLUMN IF NOT EXISTS "public_url" text;
|
|
`);
|
|
// page_analytics_history.page_engagement
|
|
await client.query(`
|
|
ALTER TABLE "page_analytics_history"
|
|
ADD COLUMN IF NOT EXISTS "page_engagement" integer DEFAULT 0;
|
|
`);
|
|
|
|
// audio_tracks (bibliothèque audio interne)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "audio_tracks" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar REFERENCES "users"("id") ON DELETE cascade,
|
|
"title" text NOT NULL,
|
|
"file_name" text NOT NULL,
|
|
"url" text NOT NULL,
|
|
"duration" integer DEFAULT 0,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// app_config (global application settings)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "app_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"external_api_key" text,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// app_config.gemini_api_key
|
|
await client.query(`
|
|
ALTER TABLE "app_config"
|
|
ADD COLUMN IF NOT EXISTS "gemini_api_key" text;
|
|
`);
|
|
|
|
// app_config.qwen_tts_url / qwen_tts_api_key (service Qwen3-TTS)
|
|
await client.query(`
|
|
ALTER TABLE "app_config"
|
|
ADD COLUMN IF NOT EXISTS "qwen_tts_url" text,
|
|
ADD COLUMN IF NOT EXISTS "qwen_tts_api_key" text;
|
|
`);
|
|
|
|
// media.thumbnail_url (vignette conservée après suppression de la vidéo)
|
|
await client.query(`
|
|
ALTER TABLE "media"
|
|
ADD COLUMN IF NOT EXISTS "thumbnail_url" text;
|
|
`);
|
|
|
|
// tiktok_config (application développeur TikTok, globale)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "tiktok_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"client_key" text NOT NULL,
|
|
"client_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// social_pages : champs OAuth TikTok
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "refresh_token" text,
|
|
ADD COLUMN IF NOT EXISTS "refresh_token_expires_at" timestamp,
|
|
ADD COLUMN IF NOT EXISTS "scopes" text,
|
|
ADD COLUMN IF NOT EXISTS "avatar_url" text;
|
|
`);
|
|
|
|
// facebook_config (application développeur Facebook, globale)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "facebook_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"app_id" text NOT NULL,
|
|
"app_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// social_pages : renouvellement automatique des tokens Facebook
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "user_access_token" text,
|
|
ADD COLUMN IF NOT EXISTS "user_token_expires_at" timestamp,
|
|
ADD COLUMN IF NOT EXISTS "token_error" text;
|
|
`);
|
|
|
|
// scheduled_posts : suivi de la publication asynchrone TikTok
|
|
await client.query(`
|
|
ALTER TABLE "scheduled_posts"
|
|
ADD COLUMN IF NOT EXISTS "publish_id" text,
|
|
ADD COLUMN IF NOT EXISTS "publish_status" text;
|
|
`);
|
|
|
|
// reel_jobs : file d'attente persistante des rendus de Reels
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "reel_jobs" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
"post_id" varchar REFERENCES "posts"("id") ON DELETE cascade,
|
|
"kind" text NOT NULL,
|
|
"params" jsonb NOT NULL,
|
|
"status" text NOT NULL DEFAULT 'pending',
|
|
"step" text,
|
|
"progress" integer NOT NULL DEFAULT 0,
|
|
"attempts" integer NOT NULL DEFAULT 0,
|
|
"result" jsonb,
|
|
"error" text,
|
|
"heartbeat_at" timestamp,
|
|
"created_at" timestamp NOT NULL DEFAULT now(),
|
|
"updated_at" timestamp NOT NULL DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// 4. Index de performance
|
|
//
|
|
// Postgres n'indexe pas les clés étrangères tout seul : sans ces index, le
|
|
// calendrier, l'historique et le scheduler faisaient des parcours complets
|
|
// de table à chaque appel. Ils existaient dans migration-docker.sql, qui
|
|
// n'est monté nulle part — ce fichier-ci est le seul exécuté au démarrage.
|
|
//
|
|
// Chaque index est créé isolément : une table absente sur une vieille
|
|
// installation ne doit pas empêcher la création des suivants.
|
|
const indexes: [string, string][] = [
|
|
// Filtres par propriétaire
|
|
["idx_posts_user_id", `CREATE INDEX IF NOT EXISTS "idx_posts_user_id" ON "posts" ("user_id")`],
|
|
["idx_media_user_id", `CREATE INDEX IF NOT EXISTS "idx_media_user_id" ON "media" ("user_id")`],
|
|
["idx_social_pages_user_id", `CREATE INDEX IF NOT EXISTS "idx_social_pages_user_id" ON "social_pages" ("user_id")`],
|
|
["idx_ai_generations_user_id", `CREATE INDEX IF NOT EXISTS "idx_ai_generations_user_id" ON "ai_generations" ("user_id")`],
|
|
|
|
// Jointures
|
|
["idx_scheduled_posts_post_id", `CREATE INDEX IF NOT EXISTS "idx_scheduled_posts_post_id" ON "scheduled_posts" ("post_id")`],
|
|
["idx_post_media_post_id", `CREATE INDEX IF NOT EXISTS "idx_post_media_post_id" ON "post_media" ("post_id")`],
|
|
["idx_post_media_media_id", `CREATE INDEX IF NOT EXISTS "idx_post_media_media_id" ON "post_media" ("media_id")`],
|
|
["idx_post_analytics_post_id", `CREATE INDEX IF NOT EXISTS "idx_post_analytics_post_id" ON "post_analytics" ("post_id")`],
|
|
["idx_page_analytics_history_page_date", `CREATE INDEX IF NOT EXISTS "idx_page_analytics_history_page_date" ON "page_analytics_history" ("page_id", "date")`],
|
|
["idx_user_page_permissions_user_id", `CREATE INDEX IF NOT EXISTS "idx_user_page_permissions_user_id" ON "user_page_permissions" ("user_id")`],
|
|
["idx_user_page_permissions_page_id", `CREATE INDEX IF NOT EXISTS "idx_user_page_permissions_page_id" ON "user_page_permissions" ("page_id")`],
|
|
|
|
// Calendrier et historique : « les publications de ces pages, sur cette période »
|
|
["idx_scheduled_posts_page_scheduled_at", `CREATE INDEX IF NOT EXISTS "idx_scheduled_posts_page_scheduled_at" ON "scheduled_posts" ("page_id", "scheduled_at")`],
|
|
|
|
// Scheduler (toutes les minutes) : seules les publications non encore
|
|
// traitées comptent, d'où un index partiel qui reste minuscule.
|
|
["idx_scheduled_posts_pending", `CREATE INDEX IF NOT EXISTS "idx_scheduled_posts_pending" ON "scheduled_posts" ("scheduled_at") WHERE "published_at" IS NULL`],
|
|
|
|
// Suivi asynchrone TikTok (toutes les deux minutes)
|
|
["idx_scheduled_posts_publish_id", `CREATE INDEX IF NOT EXISTS "idx_scheduled_posts_publish_id" ON "scheduled_posts" ("publish_id") WHERE "publish_id" IS NOT NULL`],
|
|
|
|
// Reels en cours de génération
|
|
["idx_reel_jobs_queue", `CREATE INDEX IF NOT EXISTS "idx_reel_jobs_queue" ON "reel_jobs" ("created_at") WHERE "status" IN ('pending', 'processing')`],
|
|
["idx_posts_generation_status", `CREATE INDEX IF NOT EXISTS "idx_posts_generation_status" ON "posts" ("generation_status") WHERE "generation_status" IS NOT NULL`],
|
|
];
|
|
|
|
for (const [name, statement] of indexes) {
|
|
try {
|
|
await client.query(statement);
|
|
} catch (error) {
|
|
console.warn(`[Migration] Index ${name} non créé:`, error instanceof Error ? error.message : error);
|
|
}
|
|
}
|
|
|
|
console.log("[Migration] Safe migration completed.");
|
|
} catch (error) {
|
|
console.error("[Migration] Error during migration:", error);
|
|
// Don't kill the process, let the app try to start even if migration failed
|
|
// (It might have failed because tables already existed in a different state)
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|