diff --git a/api/src/auth.ts b/api/src/auth.ts index 5764eb2..19af106 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -4,6 +4,9 @@ import { db } from "./db"; import Elysia, { status, type Context } from "elysia"; import * as schema from "./db/auth-schema"; +export const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID!; +export const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!; + export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", @@ -11,8 +14,8 @@ export const auth = betterAuth({ }), socialProviders: { spotify: { - clientId: process.env.SPOTIFY_CLIENT_ID!, - clientSecret: process.env.SPOTIFY_CLIENT_SECRET!, + clientId: SPOTIFY_CLIENT_ID, + clientSecret: SPOTIFY_CLIENT_SECRET, scope: [ "user-read-playback-state", "user-read-currently-playing", diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index 10e7a63..ec59a44 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -1,2 +1,254 @@ -import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; +import { + boolean, + index, + integer, + pgEnum, + pgTable, + primaryKey, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { user } from "./auth-schema"; export * from "./auth-schema"; + +const platform = pgEnum("platform", ["spotify"]); + +export const artist = pgTable("artist", { + id: uuid().defaultRandom().primaryKey().notNull(), + platform_id: text().notNull(), + platform: platform().notNull(), + name: text().notNull(), + type: text(), + popularity: integer(), +}); + +export const genre = pgTable("genre", { + id: uuid().defaultRandom().primaryKey().notNull(), +}); + +export const artistGenre = pgTable( + "artist_genre", + { + artist: uuid() + .references(() => artist.id) + .notNull(), + genre: uuid() + .references(() => genre.id) + .notNull(), + }, + (artistGenres) => [ + primaryKey({ + columns: [artistGenres.artist, artistGenres.genre], + }), + ], +); + +export const platformImage = pgTable("platform_image", { + id: uuid().defaultRandom().primaryKey().notNull(), + platform: platform().notNull(), + url: text(), + height: integer(), + width: integer(), +}); + +export const artistImage = pgTable( + "artist_image", + { + artist: uuid() + .references(() => artist.id) + .notNull(), + image: uuid() + .references(() => platformImage.id) + .notNull(), + }, + (artistImages) => [ + primaryKey({ + columns: [artistImages.artist, artistImages.image], + }), + ], +); + +export const album = pgTable("album", { + id: uuid().defaultRandom().primaryKey().notNull(), + type: text(), + name: text(), + platform: platform(), + platform_id: text(), + popularity: integer(), + release_date: timestamp(), + label: text(), +}); + +export const albumImage = pgTable( + "album_image", + { + album: uuid() + .references(() => album.id) + .notNull(), + image: uuid() + .references(() => platformImage.id) + .notNull(), + }, + (albumImage) => [ + primaryKey({ + columns: [albumImage.album, albumImage.image], + }), + ], +); + +export const track = pgTable( + "track", + { + id: uuid().defaultRandom().primaryKey().notNull(), + album: uuid() + .references(() => album.id) + .notNull(), + name: text(), + platform: platform(), + platform_id: text(), + popularity: integer(), + duration: integer(), + explicit: boolean(), + disc_number: integer(), + track_number: integer(), + }, + (track) => [index().on([track.album])], +); + +export const trackArtist = pgTable( + "track_artist", + { + track: uuid() + .references(() => track.id) + .notNull(), + artist: uuid() + .references(() => artist.id) + .notNull(), + }, + (trackArtist) => [ + primaryKey({ + columns: [trackArtist.track, trackArtist.artist], + }), + index().on([trackArtist.track]), + index().on([trackArtist.artist]), + ], +); + +const topTimeline = pgEnum("top_timeline", [ + "short_term", + "medium_term", + "long_term", +]); + +export const topTrack = pgTable( + "top_track", + { + track: uuid() + .references(() => track.id) + .notNull(), + position: integer().notNull(), + user: text() + .references(() => user.id) + .notNull(), + timeline: topTimeline().notNull(), + }, + (topTrack) => [ + primaryKey({ + columns: [topTrack.track, topTrack.user, topTrack.timeline], + }), + index().on([topTrack.user]), + ], +); + +export const topArtist = pgTable( + "top_artist", + { + artist: uuid() + .references(() => artist.id) + .notNull(), + position: integer().notNull(), + user: text() + .references(() => user.id) + .notNull(), + timeline: topTimeline().notNull(), + }, + (topArtist) => [ + primaryKey({ + columns: [topArtist.artist, topArtist.user, topArtist.timeline], + }), + index().on([topArtist.user]), + ], +); + +export const savedAlbum = pgTable( + "saved_album", + { + album: uuid() + .references(() => album.id) + .notNull(), + user: text() + .references(() => user.id) + .notNull(), + saved_at: timestamp("saved_at").defaultNow().notNull(), + }, + (savedAlbum) => [ + primaryKey({ + columns: [savedAlbum.album, savedAlbum.user], + }), + index().on([savedAlbum.user]), + ], +); + +export const savedTrack = pgTable( + "saved_track", + { + track: uuid() + .references(() => track.id) + .notNull(), + user: text() + .references(() => user.id) + .notNull(), + saved_at: timestamp("saved_at").defaultNow().notNull(), + }, + (savedTrack) => [ + primaryKey({ + columns: [savedTrack.track, savedTrack.user], + }), + index().on([savedTrack.user]), + ], +); + +export const followedArtist = pgTable( + "followed_artist", + { + artist: uuid() + .references(() => artist.id) + .notNull(), + user: text() + .references(() => user.id) + .notNull(), + }, + (followedArtist) => [ + primaryKey({ + columns: [followedArtist.artist, followedArtist.user], + }), + index().on([followedArtist.user]), + ], +); + +export const playbackHistory = pgTable( + "playback_history", + { + id: uuid().defaultRandom().primaryKey().notNull(), + track: uuid() + .references(() => track.id) + .notNull(), + user: text() + .references(() => user.id) + .notNull(), + played_at: timestamp("played_at").defaultNow().notNull(), + }, + (playbackHistory) => [index().on([playbackHistory.user])], +); diff --git a/api/src/index.ts b/api/src/index.ts index f291404..6ffa0f9 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,6 +1,7 @@ import { Elysia, t } from "elysia"; import { betterAuthElysia } from "./auth"; +import { syncApp } from "./routes/sync"; -const app = new Elysia().use(betterAuthElysia).listen(4000); +const app = new Elysia().use(betterAuthElysia).use(syncApp).listen(4000); export type App = typeof app; diff --git a/api/src/routes/sync.ts b/api/src/routes/sync.ts new file mode 100644 index 0000000..0a8fa2d --- /dev/null +++ b/api/src/routes/sync.ts @@ -0,0 +1,37 @@ +import Elysia from "elysia"; +import { auth, betterAuthElysia, SPOTIFY_CLIENT_ID } from "../auth"; +import { SpotifyApi } from "@spotify/web-api-ts-sdk"; + +export const syncApp = new Elysia().use(betterAuthElysia).post( + "/sync", + async ({ user }) => { + const accessToken = await auth.api.getAccessToken({ + body: { + userId: user.id, + providerId: "spotify", + }, + }); + const sdk = SpotifyApi.withAccessToken(SPOTIFY_CLIENT_ID, { + access_token: accessToken.accessToken, + expires_in: Date.now() - Number(accessToken.accessTokenExpiresAt!), + expires: Number(accessToken.accessTokenExpiresAt), + refresh_token: "", + token_type: "", + }); + for (const timeline of [ + "short_term", + "medium_term", + "long_term", + ] as const) { + const topArtists = await sdk.currentUser.topItems( + "artists", + timeline, + 50, + ); + const topTracks = await sdk.currentUser.topItems("tracks", timeline, 50); + } + }, + { + auth: true, + }, +);