// server/_core/createApp.ts
import "dotenv/config";
import express2 from "express";
import { createServer } from "http";
import { createExpressMiddleware } from "@trpc/server/adapters/express";

// shared/const.ts
var COOKIE_NAME = "app_session_id";
var ONE_YEAR_MS = 1e3 * 60 * 60 * 24 * 365;
var UNAUTHED_ERR_MSG = "Please login (10001)";
var NOT_ADMIN_ERR_MSG = "You do not have required permission (10002)";

// server/_core/uploads.ts
import { parse as parseCookieHeader } from "cookie";
import express from "express";
import fs from "fs";
import multer from "multer";
import { nanoid as nanoid2 } from "nanoid";
import path from "path";

// server/_core/auth.ts
import bcrypt from "bcryptjs";
import { SignJWT, jwtVerify } from "jose";

// server/_core/env.ts
var ENV = {
  cookieSecret: process.env.JWT_SECRET ?? "",
  databaseUrl: process.env.DATABASE_URL ?? "",
  isProduction: process.env.NODE_ENV === "production",
  // Sub-path under which this app is served (e.g. "/app" when the cPanel
  // "Application URL" is "seu-dominio.com.br/app"). Leave empty when the
  // app is served from the domain root. Trailing slashes are stripped.
  basePath: (process.env.BASE_PATH ?? "").trim().replace(/\/+$/, "")
};

// server/_core/auth.ts
var BCRYPT_ROUNDS = 10;
async function hashPassword(password) {
  return bcrypt.hash(password, BCRYPT_ROUNDS);
}
async function verifyPassword(password, hash) {
  if (!hash) return false;
  try {
    return await bcrypt.compare(password, hash);
  } catch {
    return false;
  }
}
function getSecretKey() {
  if (!ENV.cookieSecret) {
    throw new Error(
      "JWT_SECRET n\xE3o est\xE1 configurado. Defina a vari\xE1vel de ambiente JWT_SECRET."
    );
  }
  return new TextEncoder().encode(ENV.cookieSecret);
}
async function createSessionToken(userId, expiresInMs = ONE_YEAR_MS) {
  const issuedAt = Date.now();
  const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1e3);
  return new SignJWT({ uid: userId }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setExpirationTime(expirationSeconds).sign(getSecretKey());
}
async function verifySessionToken(token) {
  if (!token) return null;
  try {
    const { payload } = await jwtVerify(token, getSecretKey(), {
      algorithms: ["HS256"]
    });
    const uid = payload.uid;
    if (typeof uid !== "number") return null;
    return { uid };
  } catch {
    return null;
  }
}

// server/db.ts
import { eq, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import { nanoid } from "nanoid";

// drizzle/schema.ts
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, tinyint } from "drizzle-orm/mysql-core";
import { relations } from "drizzle-orm";
var users = mysqlTable("users", {
  id: int("id").autoincrement().primaryKey(),
  openId: varchar("openId", { length: 64 }).notNull().unique(),
  name: varchar("name", { length: 255 }),
  email: varchar("email", { length: 320 }).unique(),
  password: varchar("password", { length: 255 }),
  loginMethod: varchar("loginMethod", { length: 64 }).default("local"),
  role: mysqlEnum("role", ["user", "admin", "editor", "operator", "analyst"]).default("user").notNull(),
  createdAt: timestamp("createdAt").defaultNow().notNull(),
  updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
  lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull()
});
var podcastApresentadores = mysqlTable("podcast_apresentadores", {
  id: int("id").autoincrement().primaryKey(),
  nome: varchar("nome", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  bio: text("bio"),
  foto: varchar("foto", { length: 500 }),
  reproducoes: int("reproducoes").default(0),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var podcastProgramas = mysqlTable("podcast_programas", {
  id: int("id").autoincrement().primaryKey(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  descricao: text("descricao"),
  capa: varchar("capa", { length: 500 }),
  ano: int("ano"),
  genero: varchar("genero", { length: 100 }),
  categoria: varchar("categoria", { length: 100 }).default("podcast"),
  apresentadorId: int("apresentador_id"),
  status: mysqlEnum("status", ["rascunho", "publicado", "arquivado"]).default("publicado"),
  configAutodj: tinyint("config_autodj").default(1),
  configSequencial: tinyint("config_sequencial").default(1),
  configVinheta: tinyint("config_vinheta").default(0),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var podcastEpisodios = mysqlTable("podcast_episodios", {
  id: int("id").autoincrement().primaryKey(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  descricao: text("descricao"),
  arquivo: varchar("arquivo", { length: 500 }),
  // HUB-INTEGRATION-POINT (Fase 3/4): tipoMidia e videoUrl são a base
  // para suporte a rádio (stream) e audiolivros (audio_video com capítulos).
  // Quando PlayableItem (Fase 1) for implementado, estes campos alimentam
  // o kind e streamUrl do tipo genérico.
  tipoMidia: mysqlEnum("tipo_midia", ["audio", "video", "audio_video"]).default("audio").notNull(),
  videoUrl: varchar("video_url", { length: 500 }),
  capa: varchar("capa", { length: 500 }),
  duracao: int("duracao"),
  reproducoes: int("reproducoes").default(0),
  numeroEpisodio: int("numero_episodio"),
  ordem: int("ordem").default(0),
  tags: text("tags"),
  transcricao: text("transcricao"),
  apresentadorId: int("apresentador_id"),
  programaId: int("programa_id"),
  status: mysqlEnum("status", ["rascunho", "publicado", "agendado"]).default("publicado"),
  dataPublicacao: timestamp("data_publicacao").defaultNow(),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var podcastVinhetas = mysqlTable("podcast_vinhetas", {
  id: int("id").autoincrement().primaryKey(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  arquivo: varchar("arquivo", { length: 500 }).notNull(),
  categoria: mysqlEnum("categoria", ["abertura", "transicao", "encerramento"]).default("transicao"),
  ativo: tinyint("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow()
});
var userFavoritos = mysqlTable("user_favoritos", {
  id: int("id").autoincrement().primaryKey(),
  userId: int("user_id").notNull(),
  episodioId: int("episodio_id").notNull(),
  criadoEm: timestamp("criado_em").defaultNow()
});
var userHistorico = mysqlTable("user_historico", {
  id: int("id").autoincrement().primaryKey(),
  userId: int("user_id").notNull(),
  episodioId: int("episodio_id").notNull(),
  tocadoEm: timestamp("tocado_em").defaultNow()
});
var userPlaylists = mysqlTable("user_playlists", {
  id: int("id").autoincrement().primaryKey(),
  userId: int("user_id").notNull(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  descricao: text("descricao"),
  capa: varchar("capa", { length: 500 }),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var userPlaylistItems = mysqlTable("user_playlist_items", {
  id: int("id").autoincrement().primaryKey(),
  playlistId: int("playlist_id").notNull(),
  episodioId: int("episodio_id").notNull(),
  ordem: int("ordem").default(0),
  adicionadoEm: timestamp("adicionado_em").defaultNow()
});
var podcastProgramasRelations = relations(podcastProgramas, ({ one, many }) => ({
  apresentador: one(podcastApresentadores, {
    fields: [podcastProgramas.apresentadorId],
    references: [podcastApresentadores.id]
  }),
  episodios: many(podcastEpisodios)
}));
var podcastEpisodiosRelations = relations(podcastEpisodios, ({ one }) => ({
  programa: one(podcastProgramas, {
    fields: [podcastEpisodios.programaId],
    references: [podcastProgramas.id]
  }),
  apresentador: one(podcastApresentadores, {
    fields: [podcastEpisodios.apresentadorId],
    references: [podcastApresentadores.id]
  })
}));
var musicaArtistas = mysqlTable("musica_artistas", {
  id: int("id").autoincrement().primaryKey(),
  nome: varchar("nome", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  bio: text("bio"),
  foto: varchar("foto", { length: 500 }),
  reproducoes: int("reproducoes").default(0),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var musicaAlbuns = mysqlTable("musica_albuns", {
  id: int("id").autoincrement().primaryKey(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  descricao: text("descricao"),
  capa: varchar("capa", { length: 500 }),
  anoLancamento: int("ano_lancamento"),
  genero: varchar("genero", { length: 100 }),
  artistaId: int("artista_id"),
  status: mysqlEnum("status", ["rascunho", "publicado", "arquivado"]).default("publicado"),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var musicaFaixas = mysqlTable("musica_faixas", {
  id: int("id").autoincrement().primaryKey(),
  titulo: varchar("titulo", { length: 255 }).notNull(),
  slug: varchar("slug", { length: 255 }).unique(),
  descricao: text("descricao"),
  arquivo: varchar("arquivo", { length: 500 }),
  // HUB-INTEGRATION-POINT (Fase 3): mesmo padrão de tipoMidia usado em
  // podcast_episodios — mantém a porta aberta para versões com vídeo
  // (clipes) sem precisar de nova tabela.
  tipoMidia: mysqlEnum("tipo_midia", ["audio", "video", "audio_video"]).default("audio").notNull(),
  videoUrl: varchar("video_url", { length: 500 }),
  capa: varchar("capa", { length: 500 }),
  duracao: int("duracao"),
  reproducoes: int("reproducoes").default(0),
  numeroFaixa: int("numero_faixa"),
  ordem: int("ordem").default(0),
  letra: text("letra"),
  tags: text("tags"),
  artistaId: int("artista_id"),
  albumId: int("album_id"),
  status: mysqlEnum("status", ["rascunho", "publicado", "agendado"]).default("publicado"),
  dataPublicacao: timestamp("data_publicacao").defaultNow(),
  ativo: int("ativo").default(1),
  criadoEm: timestamp("criado_em").defaultNow(),
  atualizadoEm: timestamp("atualizado_em").defaultNow().onUpdateNow()
});
var userFavoritosMusica = mysqlTable("user_favoritos_musica", {
  id: int("id").autoincrement().primaryKey(),
  userId: int("user_id").notNull(),
  faixaId: int("faixa_id").notNull(),
  criadoEm: timestamp("criado_em").defaultNow()
});
var userHistoricoMusica = mysqlTable("user_historico_musica", {
  id: int("id").autoincrement().primaryKey(),
  userId: int("user_id").notNull(),
  faixaId: int("faixa_id").notNull(),
  tocadoEm: timestamp("tocado_em").defaultNow()
});
var userPlaylistItemsMusica = mysqlTable("user_playlist_items_musica", {
  id: int("id").autoincrement().primaryKey(),
  playlistId: int("playlist_id").notNull(),
  faixaId: int("faixa_id").notNull(),
  ordem: int("ordem").default(0),
  adicionadoEm: timestamp("adicionado_em").defaultNow()
});
var musicaAlbunsRelations = relations(musicaAlbuns, ({ one, many }) => ({
  artista: one(musicaArtistas, {
    fields: [musicaAlbuns.artistaId],
    references: [musicaArtistas.id]
  }),
  faixas: many(musicaFaixas)
}));
var musicaFaixasRelations = relations(musicaFaixas, ({ one }) => ({
  album: one(musicaAlbuns, {
    fields: [musicaFaixas.albumId],
    references: [musicaAlbuns.id]
  }),
  artista: one(musicaArtistas, {
    fields: [musicaFaixas.artistaId],
    references: [musicaArtistas.id]
  })
}));

// server/db.ts
var _db = null;
async function getDb() {
  if (!_db && process.env.DATABASE_URL) {
    try {
      _db = drizzle(process.env.DATABASE_URL);
    } catch (error) {
      console.warn("[Database] Failed to connect:", error);
      _db = null;
    }
  }
  return _db;
}
async function getUserById(id) {
  const db = await getDb();
  if (!db) {
    console.warn("[Database] Cannot get user: database not available");
    return void 0;
  }
  const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
  return result.length > 0 ? result[0] : void 0;
}
async function getUserByEmail(email) {
  const db = await getDb();
  if (!db) {
    console.warn("[Database] Cannot get user: database not available");
    return void 0;
  }
  const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
  return result.length > 0 ? result[0] : void 0;
}
async function countUsers() {
  const db = await getDb();
  if (!db) return 0;
  const result = await db.select({ count: sql`count(*)` }).from(users);
  return Number(result[0]?.count || 0);
}
async function atualizarUltimoLogin(userId) {
  const db = await getDb();
  if (!db) return;
  await db.update(users).set({ lastSignedIn: /* @__PURE__ */ new Date() }).where(eq(users.id, userId));
}
async function createLocalUser(data) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const openId = `local_${nanoid(20)}`;
  await db.insert(users).values({
    openId,
    name: data.name,
    email: data.email,
    password: data.passwordHash,
    loginMethod: "local",
    role: data.role,
    lastSignedIn: /* @__PURE__ */ new Date()
  });
  const created = await getUserByEmail(data.email);
  if (!created) {
    throw new Error("Failed to create user");
  }
  return created;
}

// server/_core/uploads.ts
var UPLOADS_DIR = path.resolve(process.cwd(), "uploads");
var AUDIO_DIR = path.join(UPLOADS_DIR, "audio");
var IMAGES_DIR = path.join(UPLOADS_DIR, "images");
for (const dir of [AUDIO_DIR, IMAGES_DIR]) {
  fs.mkdirSync(dir, { recursive: true });
}
function sanitizeFilename(name) {
  const base = path.basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
  return base.length > 0 ? base.slice(-120) : "arquivo";
}
var audioStorage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, AUDIO_DIR),
  filename: (_req, file, cb) => cb(null, `${nanoid2(10)}-${sanitizeFilename(file.originalname)}`)
});
var imageStorage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, IMAGES_DIR),
  filename: (_req, file, cb) => cb(null, `${nanoid2(10)}-${sanitizeFilename(file.originalname)}`)
});
var audioUpload = multer({
  storage: audioStorage,
  limits: { fileSize: 300 * 1024 * 1024 },
  // 300MB — sermões/podcasts longos
  fileFilter: (_req, file, cb) => {
    const okMime = /^audio\//.test(file.mimetype);
    const okExt = /\.(mp3|wav|m4a|ogg|aac|flac)$/i.test(file.originalname);
    if (okMime || okExt) return cb(null, true);
    cb(new Error("Tipo de arquivo de \xE1udio inv\xE1lido"));
  }
});
var imageUpload = multer({
  storage: imageStorage,
  limits: { fileSize: 10 * 1024 * 1024 },
  // 10MB
  fileFilter: (_req, file, cb) => {
    if (/^image\//.test(file.mimetype)) return cb(null, true);
    cb(new Error("Tipo de arquivo de imagem inv\xE1lido"));
  }
});
async function requireAdmin(req, res, next) {
  try {
    const cookies = parseCookieHeader(req.headers.cookie ?? "");
    const session = await verifySessionToken(cookies[COOKIE_NAME]);
    if (!session) {
      res.status(401).json({ error: "N\xE3o autenticado" });
      return;
    }
    const user = await getUserById(session.uid);
    if (!user || user.role !== "admin") {
      res.status(403).json({ error: "Acesso restrito a administradores" });
      return;
    }
    next();
  } catch {
    res.status(401).json({ error: "N\xE3o autenticado" });
  }
}
function registerUploadRoutes(app) {
  app.use("/uploads", express.static(UPLOADS_DIR));
  app.post(
    "/api/upload/audio",
    requireAdmin,
    audioUpload.single("file"),
    (req, res) => {
      if (!req.file) {
        res.status(400).json({ error: "Nenhum arquivo enviado" });
        return;
      }
      res.json({ url: `/uploads/audio/${req.file.filename}` });
    }
  );
  app.post(
    "/api/upload/image",
    requireAdmin,
    imageUpload.single("file"),
    (req, res) => {
      if (!req.file) {
        res.status(400).json({ error: "Nenhum arquivo enviado" });
        return;
      }
      res.json({ url: `/uploads/images/${req.file.filename}` });
    }
  );
  app.use(
    "/api/upload",
    (err, _req, res, next) => {
      if (!err) return next();
      const message = err instanceof Error ? err.message : "Erro no upload";
      res.status(400).json({ error: message });
    }
  );
}

// server/routers.ts
import { TRPCError as TRPCError2 } from "@trpc/server";
import { z as z2 } from "zod";

// server/_core/cookies.ts
function isSecureRequest(req) {
  if (req.protocol === "https") return true;
  const forwardedProto = req.headers["x-forwarded-proto"];
  if (!forwardedProto) return false;
  const protoList = Array.isArray(forwardedProto) ? forwardedProto : forwardedProto.split(",");
  return protoList.some((proto) => proto.trim().toLowerCase() === "https");
}
function getSessionCookieOptions(req) {
  return {
    httpOnly: true,
    path: "/",
    sameSite: "lax",
    secure: isSecureRequest(req)
  };
}

// server/_core/systemRouter.ts
import { z } from "zod";

// server/_core/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
var t = initTRPC.context().create({
  transformer: superjson
});
var router = t.router;
var publicProcedure = t.procedure;
var requireUser = t.middleware(async (opts) => {
  const { ctx, next } = opts;
  if (!ctx.user) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: UNAUTHED_ERR_MSG });
  }
  return next({
    ctx: {
      ...ctx,
      user: ctx.user
    }
  });
});
var protectedProcedure = t.procedure.use(requireUser);
var adminProcedure = t.procedure.use(
  t.middleware(async (opts) => {
    const { ctx, next } = opts;
    if (!ctx.user || ctx.user.role !== "admin") {
      throw new TRPCError({ code: "FORBIDDEN", message: NOT_ADMIN_ERR_MSG });
    }
    return next({
      ctx: {
        ...ctx,
        user: ctx.user
      }
    });
  })
);

// server/_core/systemRouter.ts
var systemRouter = router({
  health: publicProcedure.input(
    z.object({
      timestamp: z.number().min(0, "timestamp cannot be negative")
    })
  ).query(() => ({
    ok: true
  }))
});

// server/podcast-db.ts
import { eq as eq2, and, desc, asc, like, sql as sql2, inArray } from "drizzle-orm";
async function getEpisodios(page = 1, limit = 30, filtros) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const where = [eq2(podcastEpisodios.ativo, 1)];
  if (filtros?.apresentadorId) {
    where.push(eq2(podcastEpisodios.apresentadorId, filtros.apresentadorId));
  }
  if (filtros?.programaId) {
    where.push(eq2(podcastEpisodios.programaId, filtros.programaId));
  }
  const episodios = await db.select().from(podcastEpisodios).where(and(...where)).orderBy(desc(podcastEpisodios.reproducoes), desc(podcastEpisodios.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastEpisodios).where(and(...where));
  return {
    episodios,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getEpisodioById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const episodio = await db.select().from(podcastEpisodios).where(and(eq2(podcastEpisodios.id, id), eq2(podcastEpisodios.ativo, 1))).limit(1);
  return episodio[0] || null;
}
async function getEpisodiosRelacionados(episodio, limit = 6) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const where = episodio.programaId ? and(
    eq2(podcastEpisodios.programaId, episodio.programaId),
    eq2(podcastEpisodios.ativo, 1),
    sql2`${podcastEpisodios.id} != ${episodio.id}`
  ) : episodio.apresentadorId ? and(
    eq2(podcastEpisodios.apresentadorId, episodio.apresentadorId),
    eq2(podcastEpisodios.ativo, 1),
    sql2`${podcastEpisodios.id} != ${episodio.id}`
  ) : and(eq2(podcastEpisodios.ativo, 1), sql2`${podcastEpisodios.id} != ${episodio.id}`);
  return await db.select().from(podcastEpisodios).where(where).orderBy(asc(podcastEpisodios.numeroEpisodio), asc(podcastEpisodios.id)).limit(limit);
}
async function getTopEpisodios(limit = 20) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  return await db.select().from(podcastEpisodios).where(eq2(podcastEpisodios.ativo, 1)).orderBy(desc(podcastEpisodios.reproducoes), desc(podcastEpisodios.id)).limit(limit);
}
async function incrementarReproducoes(episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.update(podcastEpisodios).set({ reproducoes: sql2`${podcastEpisodios.reproducoes} + 1` }).where(eq2(podcastEpisodios.id, episodioId));
  const episodio = await getEpisodioById(episodioId);
  if (episodio?.apresentadorId) {
    await db.update(podcastApresentadores).set({ reproducoes: sql2`${podcastApresentadores.reproducoes} + 1` }).where(eq2(podcastApresentadores.id, episodio.apresentadorId));
  }
}
function slugify(value) {
  return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 250);
}
async function criarEpisodio(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify(input.titulo);
  const result = await db.insert(podcastEpisodios).values({
    titulo: input.titulo,
    slug,
    descricao: input.descricao ?? null,
    arquivo: input.arquivo ?? null,
    tipoMidia: input.tipoMidia ?? "audio",
    videoUrl: input.videoUrl ?? null,
    capa: input.capa ?? null,
    duracao: input.duracao ?? null,
    numeroEpisodio: input.numeroEpisodio ?? null,
    ordem: input.ordem ?? 0,
    tags: input.tags ?? null,
    transcricao: input.transcricao ?? null,
    apresentadorId: input.apresentadorId ?? null,
    programaId: input.programaId ?? null,
    status: input.status ?? "publicado",
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarEpisodio(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(podcastEpisodios).set(values).where(eq2(podcastEpisodios.id, id));
}
async function deletarEpisodio(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(podcastEpisodios).where(eq2(podcastEpisodios.id, id));
}
async function getEpisodiosAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const episodios = await db.select().from(podcastEpisodios).orderBy(desc(podcastEpisodios.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastEpisodios);
  return {
    episodios,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getApresentadores(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const apresentadores = await db.select().from(podcastApresentadores).where(eq2(podcastApresentadores.ativo, 1)).orderBy(desc(podcastApresentadores.reproducoes), desc(podcastApresentadores.nome)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastApresentadores).where(eq2(podcastApresentadores.ativo, 1));
  return {
    apresentadores,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getApresentadorById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const apresentador = await db.select().from(podcastApresentadores).where(
    and(
      eq2(podcastApresentadores.id, id),
      eq2(podcastApresentadores.ativo, 1)
    )
  ).limit(1);
  return apresentador[0] || null;
}
async function getApresentadoresDestaque(limit = 8) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  return await db.select().from(podcastApresentadores).where(eq2(podcastApresentadores.ativo, 1)).orderBy(desc(podcastApresentadores.reproducoes)).limit(limit);
}
async function criarApresentador(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify(input.nome);
  const result = await db.insert(podcastApresentadores).values({
    nome: input.nome,
    slug,
    bio: input.bio ?? null,
    foto: input.foto ?? null,
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarApresentador(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(podcastApresentadores).set(values).where(eq2(podcastApresentadores.id, id));
}
async function deletarApresentador(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(podcastApresentadores).where(eq2(podcastApresentadores.id, id));
}
async function getApresentadoresAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const apresentadores = await db.select().from(podcastApresentadores).orderBy(desc(podcastApresentadores.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastApresentadores);
  return {
    apresentadores,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getProgramas(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const programas = await db.select().from(podcastProgramas).where(eq2(podcastProgramas.ativo, 1)).orderBy(desc(podcastProgramas.ano), desc(podcastProgramas.titulo)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastProgramas).where(eq2(podcastProgramas.ativo, 1));
  return {
    programas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getProgramaById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const programa = await db.select().from(podcastProgramas).where(and(eq2(podcastProgramas.id, id), eq2(podcastProgramas.ativo, 1))).limit(1);
  return programa[0] || null;
}
async function getProgramasRecentes(limit = 8) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  return await db.select().from(podcastProgramas).where(eq2(podcastProgramas.ativo, 1)).orderBy(desc(podcastProgramas.ano), desc(podcastProgramas.titulo)).limit(limit);
}
async function criarPrograma(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify(input.titulo);
  const result = await db.insert(podcastProgramas).values({
    titulo: input.titulo,
    slug,
    descricao: input.descricao ?? null,
    capa: input.capa ?? null,
    ano: input.ano ?? null,
    genero: input.genero ?? null,
    categoria: input.categoria ?? "podcast",
    apresentadorId: input.apresentadorId ?? null,
    status: input.status ?? "publicado",
    configAutodj: input.configAutodj ?? 1,
    configSequencial: input.configSequencial ?? 1,
    configVinheta: input.configVinheta ?? 0,
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarPrograma(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(podcastProgramas).set(values).where(eq2(podcastProgramas.id, id));
}
async function deletarPrograma(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(podcastProgramas).where(eq2(podcastProgramas.id, id));
}
async function getProgramasAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const programas = await db.select().from(podcastProgramas).orderBy(desc(podcastProgramas.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(podcastProgramas);
  return {
    programas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getProximoConteudoAutoDJ(programaId, episodioAtualId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const programa = await getProgramaById(programaId);
  if (!programa || !programa.configAutodj) return null;
  const episodiosDoPrograma = await db.select().from(podcastEpisodios).where(
    and(eq2(podcastEpisodios.programaId, programaId), eq2(podcastEpisodios.ativo, 1))
  ).orderBy(
    asc(podcastEpisodios.numeroEpisodio),
    asc(podcastEpisodios.ordem),
    asc(podcastEpisodios.id)
  );
  if (episodiosDoPrograma.length <= 1) return null;
  let proximoEpisodio = null;
  if (programa.configSequencial) {
    const idx = episodiosDoPrograma.findIndex((ep) => ep.id === episodioAtualId);
    const nextIdx = idx === -1 ? 0 : (idx + 1) % episodiosDoPrograma.length;
    proximoEpisodio = episodiosDoPrograma[nextIdx] ?? null;
  } else {
    const candidatos = episodiosDoPrograma.filter((ep) => ep.id !== episodioAtualId);
    const pool = candidatos.length > 0 ? candidatos : episodiosDoPrograma;
    proximoEpisodio = pool[Math.floor(Math.random() * pool.length)] ?? null;
  }
  let vinhetas = [];
  if (programa.configVinheta) {
    vinhetas = await getProximasVinhetas(2);
  }
  return { vinhetas, proximoEpisodio };
}
async function getVinhetas(categoria) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const where = categoria ? and(eq2(podcastVinhetas.ativo, 1), eq2(podcastVinhetas.categoria, categoria)) : eq2(podcastVinhetas.ativo, 1);
  return await db.select().from(podcastVinhetas).where(where).orderBy(desc(podcastVinhetas.criadoEm));
}
var vinhetaRotationIndex = 0;
async function getProximasVinhetas(quantidade = 2, categoria) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const where = categoria ? and(eq2(podcastVinhetas.ativo, 1), eq2(podcastVinhetas.categoria, categoria)) : eq2(podcastVinhetas.ativo, 1);
  const vinhetas = await db.select().from(podcastVinhetas).where(where).orderBy(asc(podcastVinhetas.id));
  if (vinhetas.length === 0) return [];
  const resultado = [];
  for (let i = 0; i < quantidade; i++) {
    resultado.push(vinhetas[vinhetaRotationIndex % vinhetas.length]);
    vinhetaRotationIndex++;
  }
  return resultado;
}
async function criarVinheta(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const result = await db.insert(podcastVinhetas).values({
    titulo: input.titulo,
    arquivo: input.arquivo,
    categoria: input.categoria ?? "transicao",
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarVinheta(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(podcastVinhetas).set(values).where(eq2(podcastVinhetas.id, id));
}
async function deletarVinheta(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(podcastVinhetas).where(eq2(podcastVinhetas.id, id));
}
async function getVinhetasAdmin() {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  return await db.select().from(podcastVinhetas).orderBy(desc(podcastVinhetas.id));
}
async function buscar(query, limit = 15) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const searchPattern = `%${query}%`;
  const [episodios, apresentadores, programas] = await Promise.all([
    db.select().from(podcastEpisodios).where(
      and(
        eq2(podcastEpisodios.ativo, 1),
        like(podcastEpisodios.titulo, searchPattern)
      )
    ).limit(limit),
    db.select().from(podcastApresentadores).where(
      and(
        eq2(podcastApresentadores.ativo, 1),
        like(podcastApresentadores.nome, searchPattern)
      )
    ).limit(limit),
    db.select().from(podcastProgramas).where(
      and(
        eq2(podcastProgramas.ativo, 1),
        like(podcastProgramas.titulo, searchPattern)
      )
    ).limit(limit)
  ]);
  return { episodios, apresentadores, programas };
}
async function adicionarFavorito(userId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.insert(userFavoritos).values({ userId, episodioId });
}
async function removerFavorito(userId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(userFavoritos).where(
    and(
      eq2(userFavoritos.userId, userId),
      eq2(userFavoritos.episodioId, episodioId)
    )
  );
}
async function isFavorito(userId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const result = await db.select().from(userFavoritos).where(
    and(
      eq2(userFavoritos.userId, userId),
      eq2(userFavoritos.episodioId, episodioId)
    )
  ).limit(1);
  return result.length > 0;
}
async function getFavoritosComEpisodios(userId, page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const rows = await db.select({ episodio: podcastEpisodios, favoritadoEm: userFavoritos.criadoEm }).from(userFavoritos).innerJoin(podcastEpisodios, eq2(userFavoritos.episodioId, podcastEpisodios.id)).where(eq2(userFavoritos.userId, userId)).orderBy(desc(userFavoritos.criadoEm)).limit(limit).offset(offset);
  const total = await db.select({ count: sql2`count(*)` }).from(userFavoritos).where(eq2(userFavoritos.userId, userId));
  return {
    episodios: rows.map((row) => ({ ...row.episodio, favoritadoEm: row.favoritadoEm })),
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function adicionarAoHistorico(userId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.insert(userHistorico).values({ userId, episodioId });
}
async function getHistoricoComEpisodios(userId, limit = 50) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const rows = await db.select({ episodio: podcastEpisodios, tocadoEm: userHistorico.tocadoEm }).from(userHistorico).innerJoin(podcastEpisodios, eq2(userHistorico.episodioId, podcastEpisodios.id)).where(eq2(userHistorico.userId, userId)).orderBy(desc(userHistorico.tocadoEm)).limit(limit * 3);
  const vistos = /* @__PURE__ */ new Set();
  const episodios = [];
  for (const row of rows) {
    if (vistos.has(row.episodio.id)) continue;
    vistos.add(row.episodio.id);
    episodios.push({ ...row.episodio, tocadoEm: row.tocadoEm });
    if (episodios.length >= limit) break;
  }
  return episodios;
}
async function criarPlaylist(userId, titulo, descricao) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const result = await db.insert(userPlaylists).values({
    userId,
    titulo,
    descricao
  });
  return { id: Number(result.insertId ?? 0) };
}
async function getPlaylistById(playlistId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const playlist = await db.select().from(userPlaylists).where(eq2(userPlaylists.id, playlistId)).limit(1);
  return playlist[0] || null;
}
async function atualizarPlaylist(playlistId, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(userPlaylists).set(values).where(eq2(userPlaylists.id, playlistId));
}
async function deletarPlaylist(playlistId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(userPlaylists).where(eq2(userPlaylists.id, playlistId));
}
async function adicionarAPlaylist(playlistId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const maxOrdem = await db.select({ max: sql2`MAX(ordem)` }).from(userPlaylistItems).where(eq2(userPlaylistItems.playlistId, playlistId));
  const ordem = (maxOrdem[0]?.max || 0) + 1;
  await db.insert(userPlaylistItems).values({
    playlistId,
    episodioId,
    ordem
  });
}
async function removerDaPlaylist(playlistId, episodioId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(userPlaylistItems).where(
    and(
      eq2(userPlaylistItems.playlistId, playlistId),
      eq2(userPlaylistItems.episodioId, episodioId)
    )
  );
}
async function getPlaylistItemsComEpisodios(playlistId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const rows = await db.select({ episodio: podcastEpisodios, ordem: userPlaylistItems.ordem }).from(userPlaylistItems).innerJoin(podcastEpisodios, eq2(userPlaylistItems.episodioId, podcastEpisodios.id)).where(eq2(userPlaylistItems.playlistId, playlistId)).orderBy(asc(userPlaylistItems.ordem));
  return rows.map((row) => row.episodio);
}
async function getPlaylistsComContagem(userId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const playlists = await db.select().from(userPlaylists).where(eq2(userPlaylists.userId, userId)).orderBy(desc(userPlaylists.criadoEm));
  if (playlists.length === 0) return [];
  const ids = playlists.map((p) => p.id);
  const counts = await db.select({
    playlistId: userPlaylistItems.playlistId,
    total: sql2`count(*)`
  }).from(userPlaylistItems).where(inArray(userPlaylistItems.playlistId, ids)).groupBy(userPlaylistItems.playlistId);
  const countMap = new Map(counts.map((c) => [c.playlistId, Number(c.total)]));
  return playlists.map((p) => ({ ...p, totalEpisodios: countMap.get(p.id) ?? 0 }));
}

// server/musica-db.ts
import { eq as eq3, and as and2, desc as desc2, sql as sql3 } from "drizzle-orm";
async function getArtistas(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const artistas = await db.select().from(musicaArtistas).where(eq3(musicaArtistas.ativo, 1)).orderBy(desc2(musicaArtistas.reproducoes), desc2(musicaArtistas.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaArtistas).where(eq3(musicaArtistas.ativo, 1));
  return {
    artistas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getArtistaById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const artista = await db.select().from(musicaArtistas).where(and2(eq3(musicaArtistas.id, id), eq3(musicaArtistas.ativo, 1))).limit(1);
  if (!artista[0]) return null;
  const albuns = await db.select().from(musicaAlbuns).where(and2(eq3(musicaAlbuns.artistaId, id), eq3(musicaAlbuns.ativo, 1))).orderBy(desc2(musicaAlbuns.anoLancamento));
  const faixas = await db.select().from(musicaFaixas).where(and2(eq3(musicaFaixas.artistaId, id), eq3(musicaFaixas.ativo, 1))).orderBy(desc2(musicaFaixas.reproducoes), desc2(musicaFaixas.id));
  return { artista: artista[0], albuns, faixas };
}
async function getAlbuns(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const albuns = await db.select().from(musicaAlbuns).where(eq3(musicaAlbuns.ativo, 1)).orderBy(desc2(musicaAlbuns.anoLancamento), desc2(musicaAlbuns.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaAlbuns).where(eq3(musicaAlbuns.ativo, 1));
  return {
    albuns,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getFaixas(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const faixas = await db.select().from(musicaFaixas).where(eq3(musicaFaixas.ativo, 1)).orderBy(desc2(musicaFaixas.reproducoes), desc2(musicaFaixas.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaFaixas).where(eq3(musicaFaixas.ativo, 1));
  return {
    faixas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function getAlbumById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const album = await db.select().from(musicaAlbuns).where(and2(eq3(musicaAlbuns.id, id), eq3(musicaAlbuns.ativo, 1))).limit(1);
  if (!album[0]) return null;
  const artista = album[0].artistaId ? (await db.select().from(musicaArtistas).where(eq3(musicaArtistas.id, album[0].artistaId)).limit(1))[0] ?? null : null;
  const faixas = await db.select().from(musicaFaixas).where(and2(eq3(musicaFaixas.albumId, id), eq3(musicaFaixas.ativo, 1))).orderBy(sql3`${musicaFaixas.numeroFaixa} is null`, musicaFaixas.numeroFaixa, musicaFaixas.ordem);
  return { album: album[0], artista, faixas };
}
async function getFaixaById(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const faixa = await db.select().from(musicaFaixas).where(and2(eq3(musicaFaixas.id, id), eq3(musicaFaixas.ativo, 1))).limit(1);
  if (!faixa[0]) return null;
  const artista = faixa[0].artistaId ? (await db.select().from(musicaArtistas).where(eq3(musicaArtistas.id, faixa[0].artistaId)).limit(1))[0] ?? null : null;
  const album = faixa[0].albumId ? (await db.select().from(musicaAlbuns).where(eq3(musicaAlbuns.id, faixa[0].albumId)).limit(1))[0] ?? null : null;
  const where = faixa[0].albumId ? and2(
    eq3(musicaFaixas.albumId, faixa[0].albumId),
    eq3(musicaFaixas.ativo, 1),
    sql3`${musicaFaixas.id} != ${id}`
  ) : faixa[0].artistaId ? and2(
    eq3(musicaFaixas.artistaId, faixa[0].artistaId),
    eq3(musicaFaixas.ativo, 1),
    sql3`${musicaFaixas.id} != ${id}`
  ) : and2(eq3(musicaFaixas.ativo, 1), sql3`${musicaFaixas.id} != ${id}`);
  const relacionadas = await db.select().from(musicaFaixas).where(where).limit(6);
  return { faixa: faixa[0], artista, album, relacionadas };
}
async function getFaixaOgData(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const rows = await db.select({
    titulo: musicaFaixas.titulo,
    capaFaixa: musicaFaixas.capa,
    capaAlbum: musicaAlbuns.capa,
    artistaNome: musicaArtistas.nome
  }).from(musicaFaixas).leftJoin(musicaAlbuns, eq3(musicaFaixas.albumId, musicaAlbuns.id)).leftJoin(musicaArtistas, eq3(musicaFaixas.artistaId, musicaArtistas.id)).where(and2(eq3(musicaFaixas.id, id), eq3(musicaFaixas.ativo, 1))).limit(1);
  const row = rows[0];
  if (!row) return null;
  return {
    titulo: row.titulo,
    capa: row.capaFaixa ?? row.capaAlbum ?? null,
    artistaNome: row.artistaNome ?? null
  };
}
async function getTopFaixas(limit = 20) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  return await db.select().from(musicaFaixas).where(eq3(musicaFaixas.ativo, 1)).orderBy(desc2(musicaFaixas.reproducoes), desc2(musicaFaixas.id)).limit(limit);
}
async function incrementarReproducoesFaixa(faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.update(musicaFaixas).set({ reproducoes: sql3`${musicaFaixas.reproducoes} + 1` }).where(eq3(musicaFaixas.id, faixaId));
  const faixa = await db.select().from(musicaFaixas).where(eq3(musicaFaixas.id, faixaId)).limit(1);
  if (faixa[0]?.artistaId) {
    await db.update(musicaArtistas).set({ reproducoes: sql3`${musicaArtistas.reproducoes} + 1` }).where(eq3(musicaArtistas.id, faixa[0].artistaId));
  }
}
function slugify2(value) {
  return value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 250);
}
async function criarArtista(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify2(input.nome);
  const result = await db.insert(musicaArtistas).values({
    nome: input.nome,
    slug,
    bio: input.bio ?? null,
    foto: input.foto ?? null,
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarArtista(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(musicaArtistas).set(values).where(eq3(musicaArtistas.id, id));
}
async function deletarArtista(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(musicaArtistas).where(eq3(musicaArtistas.id, id));
}
async function getEstatisticas() {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const [totalFaixas] = await db.select({ count: sql3`count(*)` }).from(musicaFaixas);
  const [totalArtistas] = await db.select({ count: sql3`count(*)` }).from(musicaArtistas);
  const [totalAlbuns] = await db.select({ count: sql3`count(*)` }).from(musicaAlbuns);
  const [reproducoes] = await db.select({ sum: sql3`coalesce(sum(${musicaFaixas.reproducoes}), 0)` }).from(musicaFaixas);
  return {
    totalFaixas: totalFaixas?.count || 0,
    totalArtistas: totalArtistas?.count || 0,
    totalAlbuns: totalAlbuns?.count || 0,
    totalReproducoes: reproducoes?.sum || 0
  };
}
async function adicionarFavorito2(userId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.insert(userFavoritosMusica).values({ userId, faixaId });
}
async function removerFavorito2(userId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(userFavoritosMusica).where(and2(eq3(userFavoritosMusica.userId, userId), eq3(userFavoritosMusica.faixaId, faixaId)));
}
async function isFavorito2(userId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const result = await db.select().from(userFavoritosMusica).where(and2(eq3(userFavoritosMusica.userId, userId), eq3(userFavoritosMusica.faixaId, faixaId))).limit(1);
  return result.length > 0;
}
async function getFavoritosComFaixas(userId, page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const favoritos = await db.select({ faixa: musicaFaixas, favoritadoEm: userFavoritosMusica.criadoEm }).from(userFavoritosMusica).innerJoin(musicaFaixas, eq3(userFavoritosMusica.faixaId, musicaFaixas.id)).where(eq3(userFavoritosMusica.userId, userId)).orderBy(desc2(userFavoritosMusica.criadoEm)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(userFavoritosMusica).where(eq3(userFavoritosMusica.userId, userId));
  return {
    faixas: favoritos.map((f) => f.faixa),
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function adicionarAoHistorico2(userId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.insert(userHistoricoMusica).values({ userId, faixaId });
}
async function getHistoricoComFaixas(userId, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const historico = await db.select({ faixa: musicaFaixas, tocadoEm: userHistoricoMusica.tocadoEm }).from(userHistoricoMusica).innerJoin(musicaFaixas, eq3(userHistoricoMusica.faixaId, musicaFaixas.id)).where(eq3(userHistoricoMusica.userId, userId)).orderBy(desc2(userHistoricoMusica.tocadoEm)).limit(limit);
  return historico.map((h) => h.faixa);
}
async function getPlaylistItemsComFaixas(playlistId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const itens = await db.select({ faixa: musicaFaixas, artistaNome: musicaArtistas.nome }).from(userPlaylistItemsMusica).innerJoin(musicaFaixas, eq3(userPlaylistItemsMusica.faixaId, musicaFaixas.id)).leftJoin(musicaArtistas, eq3(musicaFaixas.artistaId, musicaArtistas.id)).where(eq3(userPlaylistItemsMusica.playlistId, playlistId)).orderBy(userPlaylistItemsMusica.adicionadoEm);
  return itens.map((item) => ({ ...item.faixa, artistaNome: item.artistaNome ?? null }));
}
async function adicionarFaixaAPlaylist(playlistId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.insert(userPlaylistItemsMusica).values({ playlistId, faixaId });
}
async function removerFaixaDaPlaylist(playlistId, faixaId) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(userPlaylistItemsMusica).where(
    and2(eq3(userPlaylistItemsMusica.playlistId, playlistId), eq3(userPlaylistItemsMusica.faixaId, faixaId))
  );
}
async function getArtistasAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const artistas = await db.select().from(musicaArtistas).orderBy(desc2(musicaArtistas.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaArtistas);
  return {
    artistas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function criarAlbum(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify2(input.titulo);
  const result = await db.insert(musicaAlbuns).values({
    titulo: input.titulo,
    slug,
    descricao: input.descricao ?? null,
    capa: input.capa ?? null,
    anoLancamento: input.anoLancamento ?? null,
    genero: input.genero ?? null,
    artistaId: input.artistaId ?? null,
    status: input.status ?? "publicado",
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarAlbum(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(musicaAlbuns).set(values).where(eq3(musicaAlbuns.id, id));
}
async function deletarAlbum(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(musicaAlbuns).where(eq3(musicaAlbuns.id, id));
}
async function getAlbunsAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const albuns = await db.select().from(musicaAlbuns).orderBy(desc2(musicaAlbuns.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaAlbuns);
  return {
    albuns,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}
async function criarFaixa(input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const slug = input.slug?.trim() || slugify2(input.titulo);
  const result = await db.insert(musicaFaixas).values({
    titulo: input.titulo,
    slug,
    descricao: input.descricao ?? null,
    arquivo: input.arquivo ?? null,
    tipoMidia: input.tipoMidia ?? "audio",
    videoUrl: input.videoUrl ?? null,
    capa: input.capa ?? null,
    duracao: input.duracao ?? null,
    numeroFaixa: input.numeroFaixa ?? null,
    ordem: input.ordem ?? 0,
    letra: input.letra ?? null,
    tags: input.tags ?? null,
    artistaId: input.artistaId ?? null,
    albumId: input.albumId ?? null,
    status: input.status ?? "publicado",
    ativo: input.ativo ?? 1
  });
  return { id: Number(result.insertId ?? 0) };
}
async function atualizarFaixa(id, input) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const values = {};
  for (const [key, value] of Object.entries(input)) {
    if (value !== void 0) values[key] = value;
  }
  if (Object.keys(values).length === 0) return;
  await db.update(musicaFaixas).set(values).where(eq3(musicaFaixas.id, id));
}
async function deletarFaixa(id) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  await db.delete(musicaFaixas).where(eq3(musicaFaixas.id, id));
}
async function getFaixasAdmin(page = 1, limit = 30) {
  const db = await getDb();
  if (!db) throw new Error("Database not available");
  const offset = (page - 1) * limit;
  const faixas = await db.select().from(musicaFaixas).orderBy(desc2(musicaFaixas.id)).limit(limit).offset(offset);
  const total = await db.select({ count: sql3`count(*)` }).from(musicaFaixas);
  return {
    faixas,
    total: total[0]?.count || 0,
    page,
    pages: Math.ceil((total[0]?.count || 0) / limit)
  };
}

// server/routers.ts
var emailSchema = z2.string().trim().toLowerCase().email("E-mail inv\xE1lido");
var passwordSchema = z2.string().min(8, "A senha deve ter pelo menos 8 caracteres");
var nameSchema = z2.string().trim().min(2, "Informe seu nome").max(255);
var episodioStatusSchema = z2.enum(["rascunho", "publicado", "agendado"]);
var programaStatusSchema = z2.enum(["rascunho", "publicado", "arquivado"]);
var vinhetaCategoriaSchema = z2.enum(["abertura", "transicao", "encerramento"]);
var musicaAlbumStatusSchema = z2.enum(["rascunho", "publicado", "arquivado"]);
var musicaFaixaStatusSchema = z2.enum(["rascunho", "publicado", "agendado"]);
function publicUser(user) {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role
  };
}
async function startSession(ctx, userId) {
  const token = await createSessionToken(userId);
  const cookieOptions = getSessionCookieOptions(ctx.req);
  ctx.res.cookie(COOKIE_NAME, token, { ...cookieOptions, maxAge: ONE_YEAR_MS });
}
var paginationInput = z2.object({
  page: z2.number().int().min(1).default(1),
  limit: z2.number().int().min(1).max(60).default(24)
});
var appRouter = router({
  system: systemRouter,
  auth: router({
    me: publicProcedure.query((opts) => opts.ctx.user),
    register: publicProcedure.input(
      z2.object({
        name: nameSchema,
        email: emailSchema,
        password: passwordSchema
      })
    ).mutation(async ({ input, ctx }) => {
      const existing = await getUserByEmail(input.email);
      if (existing) {
        throw new TRPCError2({
          code: "CONFLICT",
          message: "J\xE1 existe uma conta com este e-mail."
        });
      }
      const isFirstUser = await countUsers() === 0;
      const passwordHash = await hashPassword(input.password);
      const user = await createLocalUser({
        name: input.name,
        email: input.email,
        passwordHash,
        role: isFirstUser ? "admin" : "user"
      });
      await startSession(ctx, user.id);
      return publicUser(user);
    }),
    login: publicProcedure.input(
      z2.object({
        email: emailSchema,
        password: z2.string().min(1, "Informe sua senha")
      })
    ).mutation(async ({ input, ctx }) => {
      const user = await getUserByEmail(input.email);
      const valid = await verifyPassword(input.password, user?.password);
      if (!user || !valid) {
        throw new TRPCError2({
          code: "UNAUTHORIZED",
          message: "E-mail ou senha inv\xE1lidos."
        });
      }
      await atualizarUltimoLogin(user.id);
      await startSession(ctx, user.id);
      return publicUser(user);
    }),
    logout: publicProcedure.mutation(({ ctx }) => {
      const cookieOptions = getSessionCookieOptions(ctx.req);
      ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
      return {
        success: true
      };
    })
  }),
  podcast: router({
    getHomeData: publicProcedure.query(async () => {
      const [topEpisodios, apresentadores, programas] = await Promise.all([
        getTopEpisodios(8),
        getApresentadoresDestaque(8),
        getProgramasRecentes(8)
      ]);
      return { topEpisodios, apresentadores, programas };
    }),
    getEpisodios: publicProcedure.input(
      paginationInput.extend({
        apresentadorId: z2.number().int().optional(),
        programaId: z2.number().int().optional()
      })
    ).query(async ({ input }) => {
      return await getEpisodios(input.page, input.limit, {
        apresentadorId: input.apresentadorId,
        programaId: input.programaId
      });
    }),
    getEpisodio: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      const episodio = await getEpisodioById(input.id);
      if (!episodio) return null;
      const [programa, apresentador, relacionados] = await Promise.all([
        episodio.programaId ? getProgramaById(episodio.programaId) : null,
        episodio.apresentadorId ? getApresentadorById(episodio.apresentadorId) : null,
        getEpisodiosRelacionados(episodio, 6)
      ]);
      return { episodio, programa, apresentador, relacionados };
    }),
    getApresentadores: publicProcedure.input(paginationInput).query(async ({ input }) => {
      return await getApresentadores(input.page, input.limit);
    }),
    getApresentador: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      const apresentador = await getApresentadorById(input.id);
      const episodios = await getEpisodios(1, 50, { apresentadorId: input.id });
      const programas = await getProgramas(1, 50);
      return {
        apresentador,
        episodios: episodios.episodios,
        programas: programas.programas.filter((p) => p.apresentadorId === input.id)
      };
    }),
    getProgramas: publicProcedure.input(paginationInput).query(async ({ input }) => {
      return await getProgramas(input.page, input.limit);
    }),
    getPrograma: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      const programa = await getProgramaById(input.id);
      const episodios = await getEpisodios(1, 100, { programaId: input.id });
      return { programa, episodios: episodios.episodios };
    }),
    getVinhetas: publicProcedure.input(z2.object({ categoria: vinhetaCategoriaSchema.optional() }).optional()).query(async ({ input }) => {
      return await getVinhetas(input?.categoria);
    }),
    // Rodízio de vinhetas — usado tanto pelo AutoDJ de Podcast (via
    // getProximoConteudoAutoDJ) quanto pelo intervalo automático de Música
    // (a cada 3 faixas). Vinhetas são um recurso da plataforma como um
    // todo, não exclusivas de um módulo.
    getProximasVinhetas: publicProcedure.input(z2.object({ quantidade: z2.number().int().min(1).max(5).default(2) })).query(async ({ input }) => {
      return await getProximasVinhetas(input.quantidade);
    }),
    getProximoConteudoAutoDJ: publicProcedure.input(z2.object({ programaId: z2.number(), episodioAtualId: z2.number() })).query(async ({ input }) => {
      return await getProximoConteudoAutoDJ(input.programaId, input.episodioAtualId);
    }),
    buscar: publicProcedure.input(z2.object({ query: z2.string() })).query(async ({ input }) => {
      if (!input.query.trim()) {
        return { episodios: [], apresentadores: [], programas: [] };
      }
      return await buscar(input.query.trim());
    }),
    // Registra uma reprodução: incrementa contadores e, se houver usuário
    // autenticado, registra no histórico.
    reproduzir: publicProcedure.input(z2.object({ episodioId: z2.number() })).mutation(async ({ input, ctx }) => {
      await incrementarReproducoes(input.episodioId);
      if (ctx.user) {
        await adicionarAoHistorico(ctx.user.id, input.episodioId);
      }
      return { success: true };
    }),
    // Favoritos
    toggleFavorito: protectedProcedure.input(z2.object({ episodioId: z2.number() })).mutation(async ({ input, ctx }) => {
      const userId = ctx.user.id;
      const favorito = await isFavorito(userId, input.episodioId);
      if (favorito) {
        await removerFavorito(userId, input.episodioId);
        return { favorited: false };
      } else {
        await adicionarFavorito(userId, input.episodioId);
        return { favorited: true };
      }
    }),
    getFavoritos: protectedProcedure.input(paginationInput.partial().optional()).query(async ({ input, ctx }) => {
      return await getFavoritosComEpisodios(ctx.user.id, input?.page ?? 1, input?.limit ?? 30);
    }),
    isFavorito: protectedProcedure.input(z2.object({ episodioId: z2.number() })).query(async ({ input, ctx }) => {
      return await isFavorito(ctx.user.id, input.episodioId);
    }),
    // Histórico
    getHistorico: protectedProcedure.input(z2.object({ limit: z2.number().int().min(1).max(100).default(30) }).optional()).query(async ({ input, ctx }) => {
      return await getHistoricoComEpisodios(ctx.user.id, input?.limit ?? 30);
    }),
    // Playlists
    playlists: router({
      listar: protectedProcedure.query(async ({ ctx }) => {
        return await getPlaylistsComContagem(ctx.user.id);
      }),
      obter: protectedProcedure.input(z2.object({ id: z2.number() })).query(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.id);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        const episodios = await getPlaylistItemsComEpisodios(input.id);
        return { playlist, episodios };
      }),
      criar: protectedProcedure.input(z2.object({ titulo: z2.string().trim().min(1).max(255), descricao: z2.string().trim().max(2e3).optional() })).mutation(async ({ input, ctx }) => {
        return await criarPlaylist(ctx.user.id, input.titulo, input.descricao);
      }),
      atualizar: protectedProcedure.input(
        z2.object({
          id: z2.number(),
          titulo: z2.string().trim().min(1).max(255).optional(),
          descricao: z2.string().trim().max(2e3).nullable().optional(),
          capa: z2.string().trim().max(500).nullable().optional()
        })
      ).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.id);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await atualizarPlaylist(input.id, {
          titulo: input.titulo,
          descricao: input.descricao,
          capa: input.capa
        });
        return { success: true };
      }),
      deletar: protectedProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.id);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await deletarPlaylist(input.id);
        return { success: true };
      }),
      adicionarEpisodio: protectedProcedure.input(z2.object({ playlistId: z2.number(), episodioId: z2.number() })).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.playlistId);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await adicionarAPlaylist(input.playlistId, input.episodioId);
        return { success: true };
      }),
      removerEpisodio: protectedProcedure.input(z2.object({ playlistId: z2.number(), episodioId: z2.number() })).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.playlistId);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await removerDaPlaylist(input.playlistId, input.episodioId);
        return { success: true };
      })
    }),
    // Admin routes
    admin: router({
      stats: adminProcedure.query(async () => {
        const [episodios, apresentadores, programas] = await Promise.all([
          getEpisodiosAdmin(1, 1),
          getApresentadoresAdmin(1, 1),
          getProgramasAdmin(1, 1)
        ]);
        const topEpisodios = await getTopEpisodios(1e3);
        const totalReproducoes = topEpisodios.reduce((acc, ep) => acc + (ep.reproducoes || 0), 0);
        return {
          totalEpisodios: episodios.total,
          totalApresentadores: apresentadores.total,
          totalProgramas: programas.total,
          totalReproducoes
        };
      }),
      episodios: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getEpisodiosAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            titulo: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            arquivo: z2.string().trim().max(500).optional().nullable(),
            tipoMidia: z2.enum(["audio", "video", "audio_video"]).optional(),
            videoUrl: z2.string().trim().max(500).url().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            duracao: z2.number().int().min(0).optional().nullable(),
            numeroEpisodio: z2.number().int().optional().nullable(),
            ordem: z2.number().int().optional().nullable(),
            tags: z2.string().optional().nullable(),
            transcricao: z2.string().optional().nullable(),
            apresentadorId: z2.number().int().optional().nullable(),
            programaId: z2.number().int().optional().nullable(),
            status: episodioStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarEpisodio(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            titulo: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            arquivo: z2.string().trim().max(500).optional().nullable(),
            tipoMidia: z2.enum(["audio", "video", "audio_video"]).optional(),
            videoUrl: z2.string().trim().max(500).url().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            duracao: z2.number().int().min(0).optional().nullable(),
            numeroEpisodio: z2.number().int().optional().nullable(),
            ordem: z2.number().int().optional().nullable(),
            tags: z2.string().optional().nullable(),
            transcricao: z2.string().optional().nullable(),
            apresentadorId: z2.number().int().optional().nullable(),
            programaId: z2.number().int().optional().nullable(),
            status: episodioStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarEpisodio(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarEpisodio(input.id);
          return { success: true };
        })
      }),
      apresentadores: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getApresentadoresAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            nome: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            bio: z2.string().optional().nullable(),
            foto: z2.string().trim().max(500).optional().nullable(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarApresentador(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            nome: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            bio: z2.string().optional().nullable(),
            foto: z2.string().trim().max(500).optional().nullable(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarApresentador(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarApresentador(input.id);
          return { success: true };
        })
      }),
      programas: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getProgramasAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            titulo: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            ano: z2.number().int().optional().nullable(),
            genero: z2.string().trim().max(100).optional().nullable(),
            categoria: z2.string().trim().max(100).optional().nullable(),
            apresentadorId: z2.number().int().optional().nullable(),
            status: programaStatusSchema.optional(),
            configAutodj: z2.number().int().min(0).max(1).optional(),
            configSequencial: z2.number().int().min(0).max(1).optional(),
            configVinheta: z2.number().int().min(0).max(1).optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarPrograma(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            titulo: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            ano: z2.number().int().optional().nullable(),
            genero: z2.string().trim().max(100).optional().nullable(),
            categoria: z2.string().trim().max(100).optional().nullable(),
            apresentadorId: z2.number().int().optional().nullable(),
            status: programaStatusSchema.optional(),
            configAutodj: z2.number().int().min(0).max(1).optional(),
            configSequencial: z2.number().int().min(0).max(1).optional(),
            configVinheta: z2.number().int().min(0).max(1).optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarPrograma(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarPrograma(input.id);
          return { success: true };
        })
      }),
      vinhetas: router({
        listar: adminProcedure.query(async () => {
          return await getVinhetasAdmin();
        }),
        criar: adminProcedure.input(
          z2.object({
            titulo: z2.string().trim().min(1).max(255),
            arquivo: z2.string().trim().min(1).max(500),
            categoria: vinhetaCategoriaSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarVinheta(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            titulo: z2.string().trim().min(1).max(255).optional(),
            arquivo: z2.string().trim().min(1).max(500).optional(),
            categoria: vinhetaCategoriaSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarVinheta(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarVinheta(input.id);
          return { success: true };
        })
      })
    })
  }),
  musica: router({
    admin: router({
      stats: adminProcedure.query(async () => {
        return await getEstatisticas();
      }),
      artistas: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getArtistasAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            nome: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            bio: z2.string().optional().nullable(),
            foto: z2.string().trim().max(500).optional().nullable(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarArtista(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            nome: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            bio: z2.string().optional().nullable(),
            foto: z2.string().trim().max(500).optional().nullable(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarArtista(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarArtista(input.id);
          return { success: true };
        })
      }),
      albuns: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getAlbunsAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            titulo: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            anoLancamento: z2.number().int().optional().nullable(),
            genero: z2.string().trim().max(100).optional().nullable(),
            artistaId: z2.number().int().optional().nullable(),
            status: musicaAlbumStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarAlbum(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            titulo: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            anoLancamento: z2.number().int().optional().nullable(),
            genero: z2.string().trim().max(100).optional().nullable(),
            artistaId: z2.number().int().optional().nullable(),
            status: musicaAlbumStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarAlbum(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarAlbum(input.id);
          return { success: true };
        })
      }),
      faixas: router({
        listar: adminProcedure.input(paginationInput).query(async ({ input }) => {
          return await getFaixasAdmin(input.page, input.limit);
        }),
        criar: adminProcedure.input(
          z2.object({
            titulo: z2.string().trim().min(1).max(255),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            arquivo: z2.string().trim().max(500).optional().nullable(),
            tipoMidia: z2.enum(["audio", "video", "audio_video"]).optional(),
            videoUrl: z2.string().trim().max(500).url().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            duracao: z2.number().int().min(0).optional().nullable(),
            numeroFaixa: z2.number().int().optional().nullable(),
            ordem: z2.number().int().optional().nullable(),
            letra: z2.string().optional().nullable(),
            tags: z2.string().optional().nullable(),
            artistaId: z2.number().int().optional().nullable(),
            albumId: z2.number().int().optional().nullable(),
            status: musicaFaixaStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          return await criarFaixa(input);
        }),
        atualizar: adminProcedure.input(
          z2.object({
            id: z2.number(),
            titulo: z2.string().trim().min(1).max(255).optional(),
            slug: z2.string().trim().max(255).optional(),
            descricao: z2.string().optional().nullable(),
            arquivo: z2.string().trim().max(500).optional().nullable(),
            tipoMidia: z2.enum(["audio", "video", "audio_video"]).optional(),
            videoUrl: z2.string().trim().max(500).url().optional().nullable(),
            capa: z2.string().trim().max(500).optional().nullable(),
            duracao: z2.number().int().min(0).optional().nullable(),
            numeroFaixa: z2.number().int().optional().nullable(),
            ordem: z2.number().int().optional().nullable(),
            letra: z2.string().optional().nullable(),
            tags: z2.string().optional().nullable(),
            artistaId: z2.number().int().optional().nullable(),
            albumId: z2.number().int().optional().nullable(),
            status: musicaFaixaStatusSchema.optional(),
            ativo: z2.number().int().min(0).max(1).optional()
          })
        ).mutation(async ({ input }) => {
          const { id, ...rest } = input;
          await atualizarFaixa(id, rest);
          return { success: true };
        }),
        deletar: adminProcedure.input(z2.object({ id: z2.number() })).mutation(async ({ input }) => {
          await deletarFaixa(input.id);
          return { success: true };
        })
      })
    }),
    getArtistas: publicProcedure.input(paginationInput).query(async ({ input }) => {
      return await getArtistas(input.page, input.limit);
    }),
    getArtista: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      return await getArtistaById(input.id);
    }),
    getAlbuns: publicProcedure.input(paginationInput).query(async ({ input }) => {
      return await getAlbuns(input.page, input.limit);
    }),
    getFaixas: publicProcedure.input(paginationInput).query(async ({ input }) => {
      return await getFaixas(input.page, input.limit);
    }),
    getAlbum: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      return await getAlbumById(input.id);
    }),
    getFaixa: publicProcedure.input(z2.object({ id: z2.number() })).query(async ({ input }) => {
      return await getFaixaById(input.id);
    }),
    getTopFaixas: publicProcedure.query(async () => {
      return await getTopFaixas(20);
    }),
    reproduzir: publicProcedure.input(z2.object({ faixaId: z2.number() })).mutation(async ({ input, ctx }) => {
      await incrementarReproducoesFaixa(input.faixaId);
      if (ctx.user) {
        await adicionarAoHistorico2(ctx.user.id, input.faixaId);
      }
      return { success: true };
    }),
    toggleFavorito: protectedProcedure.input(z2.object({ faixaId: z2.number() })).mutation(async ({ input, ctx }) => {
      const userId = ctx.user.id;
      const favorito = await isFavorito2(userId, input.faixaId);
      if (favorito) {
        await removerFavorito2(userId, input.faixaId);
        return { favorited: false };
      } else {
        await adicionarFavorito2(userId, input.faixaId);
        return { favorited: true };
      }
    }),
    getFavoritos: protectedProcedure.input(paginationInput.partial().optional()).query(async ({ input, ctx }) => {
      return await getFavoritosComFaixas(ctx.user.id, input?.page ?? 1, input?.limit ?? 30);
    }),
    isFavorito: protectedProcedure.input(z2.object({ faixaId: z2.number() })).query(async ({ input, ctx }) => {
      return await isFavorito2(ctx.user.id, input.faixaId);
    }),
    getHistorico: protectedProcedure.input(z2.object({ limit: z2.number().int().min(1).max(100).default(30) }).optional()).query(async ({ input, ctx }) => {
      return await getHistoricoComFaixas(ctx.user.id, input?.limit ?? 30);
    }),
    playlists: router({
      obterFaixas: protectedProcedure.input(z2.object({ playlistId: z2.number() })).query(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.playlistId);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        return await getPlaylistItemsComFaixas(input.playlistId);
      }),
      adicionarFaixa: protectedProcedure.input(z2.object({ playlistId: z2.number(), faixaId: z2.number() })).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.playlistId);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await adicionarFaixaAPlaylist(input.playlistId, input.faixaId);
        return { success: true };
      }),
      removerFaixa: protectedProcedure.input(z2.object({ playlistId: z2.number(), faixaId: z2.number() })).mutation(async ({ input, ctx }) => {
        const playlist = await getPlaylistById(input.playlistId);
        if (!playlist || playlist.userId !== ctx.user.id) {
          throw new TRPCError2({ code: "NOT_FOUND", message: "Playlist n\xE3o encontrada." });
        }
        await removerFaixaDaPlaylist(input.playlistId, input.faixaId);
        return { success: true };
      })
    })
  })
});

// server/_core/context.ts
import { parse as parseCookieHeader2 } from "cookie";
async function createContext(opts) {
  let user = null;
  try {
    const cookies = parseCookieHeader2(opts.req.headers.cookie ?? "");
    const sessionToken = cookies[COOKIE_NAME];
    const session = await verifySessionToken(sessionToken);
    if (session) {
      const found = await getUserById(session.uid);
      user = found ?? null;
    }
  } catch (error) {
    user = null;
  }
  return {
    req: opts.req,
    res: opts.res,
    user
  };
}

// server/_core/createApp.ts
function createApp() {
  const app = express2();
  app.set("trust proxy", 1);
  const server = createServer(app);
  app.use(express2.json({ limit: "50mb" }));
  app.use(express2.urlencoded({ limit: "50mb", extended: true }));
  const coreRouter = express2.Router();
  registerUploadRoutes(coreRouter);
  coreRouter.use(
    "/api/trpc",
    createExpressMiddleware({
      router: appRouter,
      createContext
    })
  );
  return { app, server, coreRouter };
}

// server/_core/static.ts
import express3 from "express";
import fs2 from "fs";
import path2 from "path";
var root = process.cwd();
function serveStatic(app) {
  const distPath = path2.resolve(root, "dist", "public");
  if (!fs2.existsSync(distPath)) {
    console.error("[STATIC] Build n\xE3o encontrado:", distPath);
  }
  const indexFile = path2.resolve(distPath, "index.html");
  const indexHtml = fs2.existsSync(indexFile) ? fs2.readFileSync(indexFile, "utf-8") : null;
  app.use(express3.static(distPath));
  app.use("*", (_req, res) => {
    if (!indexHtml) {
      return res.status(500).send("index.html n\xE3o encontrado no build");
    }
    res.set("Content-Type", "text/html");
    res.set("Cache-Control", "no-cache, no-store, must-revalidate");
    res.send(indexHtml);
  });
}

// server/_core/share.ts
function escapeHtml(text2) {
  return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function absolutizarImagem(origin, capa) {
  const fallback = `${origin}${ENV.basePath}/logo_hz.jpg`;
  if (!capa) return fallback;
  if (/^https?:\/\//i.test(capa)) return capa;
  return `${origin}${ENV.basePath}${capa}`;
}
function paginaShare(params) {
  const { titulo, descricao, imagem, ogType, url, destino } = params;
  const tituloSeguro = escapeHtml(titulo);
  const destinoSeguro = escapeHtml(destino);
  return `<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${tituloSeguro} \xB7 SpotOnFide</title>
<meta property="og:site_name" content="SpotOnFide" />
<meta property="og:type" content="${ogType}" />
<meta property="og:title" content="${tituloSeguro}" />
<meta property="og:description" content="${escapeHtml(descricao)}" />
<meta property="og:image" content="${escapeHtml(imagem)}" />
<meta property="og:url" content="${escapeHtml(url)}" />
<meta name="twitter:card" content="summary_large_image" />
<meta http-equiv="refresh" content="0; url=${destinoSeguro}" />
</head>
<body>
<script>location.replace(${JSON.stringify(destino)});</script>
<noscript>
  <p>Abrindo <a href="${destinoSeguro}">${tituloSeguro}</a> no SpotOnFide\u2026</p>
</noscript>
</body>
</html>`;
}
function registerShareRoutes(router2) {
  router2.get("/share/episodio/:id", async (req, res) => {
    const id = Number(req.params.id);
    const destino = `${ENV.basePath}/episodio/${id}`;
    if (!Number.isInteger(id)) return res.redirect(destino);
    try {
      const episodio = await getEpisodioById(id);
      if (!episodio) return res.redirect(destino);
      let capa = episodio.capa;
      if (!capa && episodio.programaId) {
        const programa = await getProgramaById(episodio.programaId);
        capa = programa?.capa ?? null;
      }
      const origin = `${req.protocol}://${req.get("host")}`;
      const html = paginaShare({
        titulo: episodio.titulo,
        descricao: episodio.descricao ? episodio.descricao.slice(0, 200) : "SpotOnFide Podcast",
        imagem: absolutizarImagem(origin, capa),
        ogType: "video.episode",
        url: `${origin}${req.originalUrl}`,
        destino
      });
      res.set("Content-Type", "text/html; charset=utf-8");
      res.set("Cache-Control", "no-store");
      res.send(html);
    } catch (err) {
      console.error("[SHARE] Erro ao gerar p\xE1gina de epis\xF3dio:", err);
      res.redirect(destino);
    }
  });
  router2.get("/share/faixa/:id", async (req, res) => {
    const id = Number(req.params.id);
    const destino = `${ENV.basePath}/musica/faixa/${id}`;
    if (!Number.isInteger(id)) return res.redirect(destino);
    try {
      const dados = await getFaixaOgData(id);
      if (!dados) return res.redirect(destino);
      const origin = `${req.protocol}://${req.get("host")}`;
      const html = paginaShare({
        titulo: dados.titulo,
        descricao: dados.artistaNome ? `Por ${dados.artistaNome} \xB7 SpotOnFide M\xFAsica` : "SpotOnFide M\xFAsica",
        imagem: absolutizarImagem(origin, dados.capa),
        ogType: "music.song",
        url: `${origin}${req.originalUrl}`,
        destino
      });
      res.set("Content-Type", "text/html; charset=utf-8");
      res.set("Cache-Control", "no-store");
      res.send(html);
    } catch (err) {
      console.error("[SHARE] Erro ao gerar p\xE1gina de faixa:", err);
      res.redirect(destino);
    }
  });
}

// server/_core/port.ts
import net from "net";
function isPortAvailable(port) {
  return new Promise((resolve) => {
    const server = net.createServer();
    server.listen(port, () => {
      server.close(() => resolve(true));
    });
    server.on("error", () => resolve(false));
  });
}
async function findAvailablePort(startPort = 3e3) {
  for (let port = startPort; port < startPort + 20; port++) {
    if (await isPortAvailable(port)) {
      return port;
    }
  }
  throw new Error(`No available port found starting from ${startPort}`);
}

// server/_core/index.ts
async function startServer() {
  const { app, server, coreRouter } = createApp();
  registerShareRoutes(coreRouter);
  serveStatic(coreRouter);
  if (ENV.basePath) {
    app.use(ENV.basePath, coreRouter);
  }
  app.use("/", coreRouter);
  const preferredPort = parseInt(process.env.PORT || "3000");
  const port = await findAvailablePort(preferredPort);
  if (port !== preferredPort) {
    console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
  }
  server.listen(port, () => {
    console.log(`Server running on http://localhost:${port}/`);
  });
}
startServer().catch(console.error);
