generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique name String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") favorites UserFavorite[] history PlayHistory[] settings UserSettings? trackLikes TrackLike[] @@map("users") } model MagicLinkToken { id String @id @default(uuid()) email String token String @unique expiresAt DateTime @map("expires_at") usedAt DateTime? @map("used_at") createdAt DateTime @default(now()) @map("created_at") @@index([token]) @@index([email]) @@map("magic_link_tokens") } model Station { id String @id @default(cuid()) stationId Int @unique @map("station_id") name String prefix String streamUrl String @map("stream_url") coverUrl String? @map("cover_url") genre String? tags String[] sortOrder Int @map("sort_order") source String @default("local") // "record" | "local" isOnline Boolean @default(true) @map("is_online") lastCheckAt DateTime? @map("last_check_at") recordStationId Int? @unique @map("record_station_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") favorites UserFavorite[] history PlayHistory[] nowPlaying NowPlaying? trackPlays TrackPlay[] @@index([isOnline]) @@index([source]) @@index([recordStationId]) @@map("stations") } model NowPlaying { id String @id @default(cuid()) stationId String @unique @map("station_id") station Station @relation(fields: [stationId], references: [id], onDelete: Cascade) song String artist String coverUrl String? @map("cover_url") updatedAt DateTime @updatedAt @map("updated_at") @@map("now_playing") } model UserFavorite { id String @id @default(cuid()) userId String @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) stationId String @map("station_id") station Station @relation(fields: [stationId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@unique([userId, stationId]) @@index([userId]) @@map("user_favorites") } model PlayHistory { id String @id @default(cuid()) userId String @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) stationId String @map("station_id") station Station @relation(fields: [stationId], references: [id], onDelete: Cascade) playedAt DateTime @default(now()) @map("played_at") @@index([userId, playedAt]) @@map("play_history") } model UserSettings { id String @id @default(cuid()) userId String @unique @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) theme String @default("system") language String @default("ru") autoPlay Boolean @default(false) @map("auto_play") showOffline Boolean @default(true) @map("show_offline") sleepTimerMinutes Int? @map("sleep_timer_minutes") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("user_settings") } // Уникальный трек (нормализованный ключ artist+song) model Track { id String @id @default(cuid()) normKey String @unique @map("norm_key") artist String song String coverUrl String? @map("cover_url") album String? releaseDate DateTime? @map("release_date") // Обогащение через Discogs (своя БД — в рантайме к Discogs не ходим) genre String? styles String[] @default([]) label String? year Int? discogsId Int? @map("discogs_id") // Состояние обогащения: pending | done | failed enrichStatus String @default("pending") @map("enrich_status") firstSeenAt DateTime @default(now()) @map("first_seen_at") enrichedAt DateTime? @map("enriched_at") plays TrackPlay[] likes TrackLike[] @@index([genre]) // Частичный индекс под бэкфилл обогащения создаётся миграцией (Prisma не умеет // partial WHERE): tracks_enrich_pending_idx (first_seen_at DESC) WHERE enrich_status='pending'. @@map("tracks") } // Факт проигрывания трека на станции model TrackPlay { id String @id @default(cuid()) trackId String @map("track_id") track Track @relation(fields: [trackId], references: [id], onDelete: Cascade) stationId String @map("station_id") station Station @relation(fields: [stationId], references: [id], onDelete: Cascade) playedAt DateTime @default(now()) @map("played_at") @@index([trackId, playedAt]) @@index([playedAt]) @@index([stationId]) // Покрывающий индекс под агрегацию чартов (WHERE played_at>=X → GROUP BY track_id, COUNT DISTINCT station_id) @@index([playedAt, trackId, stationId], map: "track_plays_window_idx") @@map("track_plays") } // Лайк трека пользователем model TrackLike { id String @id @default(cuid()) trackId String @map("track_id") track Track @relation(fields: [trackId], references: [id], onDelete: Cascade) userId String @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) @map("created_at") @@unique([trackId, userId]) @@index([trackId]) @@map("track_likes") }