feat(charts): сбор статистики проигрываний и API чартов

- модели Track / TrackPlay / TrackLike (+ миграция add_charts)
- сбор проигрываний в now-playing-поллере: при смене трека на станции
  пишется TrackPlay (нормализация artist+song -> Track), fire-and-forget
  обогащение через MusicBrainz (album/releaseDate)
- ChartsModule: GET /charts/tracks (период day/week/month/all, ранг, тренд,
  проигрывания, станции, лайки), GET /charts/tracks/:id (метрики, таймлайны
  популярности и лайков по дням, топ станций, isLiked), POST/DELETE like
- OptionalAuthGuard для публичной детальной страницы с опц. userId
This commit is contained in:
nk
2026-06-02 23:40:13 +03:00
parent bbfec76a7b
commit 38fe92d695
12 changed files with 716 additions and 16 deletions

View File

@@ -0,0 +1,68 @@
-- CreateTable: tracks
CREATE TABLE "tracks" (
"id" TEXT NOT NULL,
"norm_key" TEXT NOT NULL,
"artist" TEXT NOT NULL,
"song" TEXT NOT NULL,
"cover_url" TEXT,
"album" TEXT,
"release_date" TIMESTAMPTZ,
"first_seen_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
"enriched_at" TIMESTAMPTZ,
CONSTRAINT "tracks_pkey" PRIMARY KEY ("id")
);
-- CreateTable: track_plays
CREATE TABLE "track_plays" (
"id" TEXT NOT NULL,
"track_id" TEXT NOT NULL,
"station_id" TEXT NOT NULL,
"played_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "track_plays_pkey" PRIMARY KEY ("id")
);
-- CreateTable: track_likes
CREATE TABLE "track_likes" (
"id" TEXT NOT NULL,
"track_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "track_likes_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tracks_norm_key_key" ON "tracks"("norm_key");
-- CreateIndex
CREATE INDEX "track_plays_track_id_played_at_idx" ON "track_plays"("track_id", "played_at");
-- CreateIndex
CREATE INDEX "track_plays_played_at_idx" ON "track_plays"("played_at");
-- CreateIndex
CREATE INDEX "track_plays_station_id_idx" ON "track_plays"("station_id");
-- CreateIndex
CREATE UNIQUE INDEX "track_likes_track_id_user_id_key" ON "track_likes"("track_id", "user_id");
-- CreateIndex
CREATE INDEX "track_likes_track_id_idx" ON "track_likes"("track_id");
-- AddForeignKey
ALTER TABLE "track_plays" ADD CONSTRAINT "track_plays_track_id_fkey"
FOREIGN KEY ("track_id") REFERENCES "tracks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "track_plays" ADD CONSTRAINT "track_plays_station_id_fkey"
FOREIGN KEY ("station_id") REFERENCES "stations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "track_likes" ADD CONSTRAINT "track_likes_track_id_fkey"
FOREIGN KEY ("track_id") REFERENCES "tracks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "track_likes" ADD CONSTRAINT "track_likes_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -14,9 +14,10 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
favorites UserFavorite[]
history PlayHistory[]
settings UserSettings?
favorites UserFavorite[]
history PlayHistory[]
settings UserSettings?
trackLikes TrackLike[]
@@map("users")
}
@@ -51,9 +52,10 @@ model Station {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
favorites UserFavorite[]
history PlayHistory[]
nowPlaying NowPlaying?
favorites UserFavorite[]
history PlayHistory[]
nowPlaying NowPlaying?
trackPlays TrackPlay[]
@@index([isOnline])
@@index([source])
@@ -112,3 +114,50 @@ model UserSettings {
@@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")
firstSeenAt DateTime @default(now()) @map("first_seen_at")
enrichedAt DateTime? @map("enriched_at")
plays TrackPlay[]
likes TrackLike[]
@@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])
@@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")
}