first sync version
This commit is contained in:
parent
6ae23f7c07
commit
ba27e07749
5 changed files with 657 additions and 10 deletions
|
|
@ -3,10 +3,16 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|||
import { db } from "./db";
|
||||
import Elysia, { status, type Context } from "elysia";
|
||||
import * as schema from "./db/auth-schema";
|
||||
import { SpotifyApi } from "@spotify/web-api-ts-sdk";
|
||||
|
||||
export const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID!;
|
||||
export const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!;
|
||||
|
||||
export const defaultSdk = SpotifyApi.withClientCredentials(
|
||||
SPOTIFY_CLIENT_ID,
|
||||
SPOTIFY_CLIENT_SECRET,
|
||||
);
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import "./schema";
|
||||
import { relations } from "./schema";
|
||||
|
||||
export const db = drizzle(process.env.DATABASE_URL!);
|
||||
export const db = drizzle(process.env.DATABASE_URL!, { relations });
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { defineRelations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
|
|
@ -7,6 +8,7 @@ import {
|
|||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
|
@ -26,6 +28,7 @@ export const artist = pgTable("artist", {
|
|||
|
||||
export const genre = pgTable("genre", {
|
||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||
name: text().notNull().unique(),
|
||||
});
|
||||
|
||||
export const artistGenre = pgTable(
|
||||
|
|
@ -45,13 +48,20 @@ export const artistGenre = pgTable(
|
|||
],
|
||||
);
|
||||
|
||||
export const platformImage = pgTable("platform_image", {
|
||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||
platform: platform().notNull(),
|
||||
url: text(),
|
||||
height: integer(),
|
||||
width: integer(),
|
||||
});
|
||||
export const platformImage = pgTable(
|
||||
"platform_image",
|
||||
{
|
||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||
platform: platform().notNull(),
|
||||
url: text(),
|
||||
height: integer(),
|
||||
width: integer(),
|
||||
},
|
||||
(platformImage) => [
|
||||
uniqueIndex().on(platformImage.platform, platformImage.url),
|
||||
index().on(platformImage.url),
|
||||
],
|
||||
);
|
||||
|
||||
export const artistImage = pgTable(
|
||||
"artist_image",
|
||||
|
|
@ -98,6 +108,40 @@ export const albumImage = pgTable(
|
|||
],
|
||||
);
|
||||
|
||||
export const albumArtist = pgTable(
|
||||
"album_artist",
|
||||
{
|
||||
album: uuid()
|
||||
.references(() => album.id)
|
||||
.notNull(),
|
||||
artist: uuid()
|
||||
.references(() => artist.id)
|
||||
.notNull(),
|
||||
},
|
||||
(albumArtist) => [
|
||||
primaryKey({
|
||||
columns: [albumArtist.album, albumArtist.artist],
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
export const albumGenre = pgTable(
|
||||
"album_genre",
|
||||
{
|
||||
album: uuid()
|
||||
.references(() => album.id)
|
||||
.notNull(),
|
||||
genre: uuid()
|
||||
.references(() => genre.id)
|
||||
.notNull(),
|
||||
},
|
||||
(albumGenre) => [
|
||||
primaryKey({
|
||||
columns: [albumGenre.album, albumGenre.genre],
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
export const track = pgTable(
|
||||
"track",
|
||||
{
|
||||
|
|
@ -252,3 +296,230 @@ export const playbackHistory = pgTable(
|
|||
},
|
||||
(playbackHistory) => [index().on([playbackHistory.user])],
|
||||
);
|
||||
|
||||
export const relations = defineRelations(
|
||||
{
|
||||
album,
|
||||
albumImage,
|
||||
albumArtist,
|
||||
albumGenre,
|
||||
artist,
|
||||
artistGenre,
|
||||
artistImage,
|
||||
followedArtist,
|
||||
genre,
|
||||
platformImage,
|
||||
playbackHistory,
|
||||
savedAlbum,
|
||||
savedTrack,
|
||||
topArtist,
|
||||
topTrack,
|
||||
track,
|
||||
trackArtist,
|
||||
user,
|
||||
},
|
||||
(r) => ({
|
||||
artist: {
|
||||
artistGenres: r.many.artistGenre(),
|
||||
artistImages: r.many.artistImage(),
|
||||
trackArtists: r.many.trackArtist(),
|
||||
albumArtists: r.many.albumArtist(),
|
||||
topArtists: r.many.topArtist(),
|
||||
followedArtists: r.many.followedArtist(),
|
||||
albumGenres: r.many.albumGenre(),
|
||||
genres: r.many.genre({
|
||||
from: r.artist.id.through(r.artistGenre.artist),
|
||||
to: r.genre.id.through(r.artistGenre.genre),
|
||||
}),
|
||||
images: r.many.platformImage({
|
||||
from: r.artist.id.through(r.artistImage.artist),
|
||||
to: r.platformImage.id.through(r.artistImage.image),
|
||||
}),
|
||||
albums: r.many.album({
|
||||
from: r.artist.id.through(r.albumArtist.artist),
|
||||
to: r.album.id.through(r.albumArtist.album),
|
||||
}),
|
||||
tracks: r.many.track({
|
||||
from: r.artist.id.through(r.trackArtist.artist),
|
||||
to: r.track.id.through(r.trackArtist.track),
|
||||
}),
|
||||
},
|
||||
genre: {
|
||||
artistGenres: r.many.artistGenre(),
|
||||
albumGenres: r.many.albumGenre(),
|
||||
artists: r.many.artist({
|
||||
from: r.genre.id.through(r.artistGenre.genre),
|
||||
to: r.artist.id.through(r.artistGenre.artist),
|
||||
}),
|
||||
albums: r.many.album({
|
||||
from: r.genre.id.through(r.albumGenre.genre),
|
||||
to: r.album.id.through(r.albumGenre.album),
|
||||
}),
|
||||
},
|
||||
artistGenre: {
|
||||
artist: r.one.artist({
|
||||
from: r.artistGenre.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
genre: r.one.genre({
|
||||
from: r.artistGenre.genre,
|
||||
to: r.genre.id,
|
||||
}),
|
||||
},
|
||||
platformImage: {
|
||||
artistImages: r.many.artistImage(),
|
||||
albumImages: r.many.albumImage(),
|
||||
artists: r.many.artist({
|
||||
from: r.platformImage.id.through(r.artistImage.image),
|
||||
to: r.artist.id.through(r.artistImage.artist),
|
||||
}),
|
||||
albums: r.many.album({
|
||||
from: r.platformImage.id.through(r.albumImage.image),
|
||||
to: r.album.id.through(r.albumImage.album),
|
||||
}),
|
||||
},
|
||||
artistImage: {
|
||||
artist: r.one.artist({
|
||||
from: r.artistImage.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
image: r.one.platformImage({
|
||||
from: r.artistImage.image,
|
||||
to: r.platformImage.id,
|
||||
}),
|
||||
},
|
||||
album: {
|
||||
tracks: r.many.track(),
|
||||
albumImages: r.many.albumImage(),
|
||||
albumArtists: r.many.albumArtist(),
|
||||
albumGenres: r.many.albumGenre(),
|
||||
savedAlbums: r.many.savedAlbum(),
|
||||
images: r.many.platformImage({
|
||||
from: r.album.id.through(r.albumImage.album),
|
||||
to: r.platformImage.id.through(r.albumImage.image),
|
||||
}),
|
||||
artists: r.many.artist({
|
||||
from: r.album.id.through(r.albumArtist.album),
|
||||
to: r.artist.id.through(r.albumArtist.artist),
|
||||
}),
|
||||
genres: r.many.genre({
|
||||
from: r.album.id.through(r.albumGenre.album),
|
||||
to: r.genre.id.through(r.albumGenre.genre),
|
||||
}),
|
||||
},
|
||||
albumImage: {
|
||||
album: r.one.album({
|
||||
from: r.albumImage.album,
|
||||
to: r.album.id,
|
||||
}),
|
||||
image: r.one.platformImage({
|
||||
from: r.albumImage.image,
|
||||
to: r.platformImage.id,
|
||||
}),
|
||||
},
|
||||
albumArtist: {
|
||||
album: r.one.album({
|
||||
from: r.albumArtist.album,
|
||||
to: r.album.id,
|
||||
}),
|
||||
artist: r.one.artist({
|
||||
from: r.albumArtist.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
},
|
||||
albumGenre: {
|
||||
album: r.one.album({
|
||||
from: r.albumGenre.album,
|
||||
to: r.album.id,
|
||||
}),
|
||||
genre: r.one.genre({
|
||||
from: r.albumGenre.genre,
|
||||
to: r.genre.id,
|
||||
}),
|
||||
},
|
||||
track: {
|
||||
album: r.one.album({
|
||||
from: r.track.album,
|
||||
to: r.album.id,
|
||||
}),
|
||||
trackArtists: r.many.trackArtist(),
|
||||
topTracks: r.many.topTrack(),
|
||||
savedTracks: r.many.savedTrack(),
|
||||
playbackHistory: r.many.playbackHistory(),
|
||||
artists: r.many.artist({
|
||||
from: r.track.id.through(r.trackArtist.track),
|
||||
to: r.artist.id.through(r.trackArtist.artist),
|
||||
}),
|
||||
},
|
||||
trackArtist: {
|
||||
track: r.one.track({
|
||||
from: r.trackArtist.track,
|
||||
to: r.track.id,
|
||||
}),
|
||||
artist: r.one.artist({
|
||||
from: r.trackArtist.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
},
|
||||
topTrack: {
|
||||
track: r.one.track({
|
||||
from: r.topTrack.track,
|
||||
to: r.track.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.topTrack.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
topArtist: {
|
||||
artist: r.one.artist({
|
||||
from: r.topArtist.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.topArtist.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
savedAlbum: {
|
||||
album: r.one.album({
|
||||
from: r.savedAlbum.album,
|
||||
to: r.album.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.savedAlbum.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
savedTrack: {
|
||||
track: r.one.track({
|
||||
from: r.savedTrack.track,
|
||||
to: r.track.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.savedTrack.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
followedArtist: {
|
||||
artist: r.one.artist({
|
||||
from: r.followedArtist.artist,
|
||||
to: r.artist.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.followedArtist.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
playbackHistory: {
|
||||
track: r.one.track({
|
||||
from: r.playbackHistory.track,
|
||||
to: r.track.id,
|
||||
}),
|
||||
user: r.one.user({
|
||||
from: r.playbackHistory.user,
|
||||
to: r.user.id,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
345
api/src/db/spotify.ts
Normal file
345
api/src/db/spotify.ts
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
import type {
|
||||
Album,
|
||||
Artist,
|
||||
FollowedArtists,
|
||||
Image,
|
||||
PlayHistory,
|
||||
SavedAlbum,
|
||||
SavedTrack,
|
||||
SimplifiedAlbum,
|
||||
SimplifiedArtist,
|
||||
Track,
|
||||
} from "@spotify/web-api-ts-sdk";
|
||||
import { db } from ".";
|
||||
import {
|
||||
album,
|
||||
albumArtist,
|
||||
albumGenre,
|
||||
albumImage,
|
||||
artist,
|
||||
artistGenre,
|
||||
artistImage,
|
||||
followedArtist,
|
||||
genre,
|
||||
playbackHistory,
|
||||
platformImage,
|
||||
savedAlbum,
|
||||
savedTrack,
|
||||
topArtist,
|
||||
topTrack,
|
||||
track,
|
||||
trackArtist,
|
||||
} from "./schema";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { defaultSdk } from "../auth";
|
||||
|
||||
export const PLATFORM_SPOTIFY = "spotify" as const;
|
||||
|
||||
export async function upsertImages(images: Image[]) {
|
||||
await db.insert(platformImage).values(
|
||||
images.map(({ url, height, width }) => ({
|
||||
platform: PLATFORM_SPOTIFY,
|
||||
url,
|
||||
height,
|
||||
width,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertGenres(genres: string[]) {
|
||||
await db.insert(genre).values(genres.map((name) => ({ name })));
|
||||
}
|
||||
|
||||
export async function upsertArtists(artists: Artist[]) {
|
||||
await db.insert(artist).values(
|
||||
artists.map(({ id, name, images, genres, popularity, type }) => ({
|
||||
platform: PLATFORM_SPOTIFY,
|
||||
platform_id: id,
|
||||
name,
|
||||
popularity,
|
||||
type,
|
||||
})),
|
||||
);
|
||||
await upsertImages(artists.flatMap((a) => a.images));
|
||||
await upsertGenres(artists.flatMap((a) => a.genres));
|
||||
for (const spotifyArtist of artists) {
|
||||
await db.insert(artistImage).select(
|
||||
db
|
||||
.select({
|
||||
artist: artist.id,
|
||||
image: platformImage.id,
|
||||
})
|
||||
.from(platformImage)
|
||||
.where(
|
||||
and(
|
||||
eq(platformImage.platform, PLATFORM_SPOTIFY),
|
||||
inArray(
|
||||
platformImage.url,
|
||||
spotifyArtist.images.map((t) => t.url),
|
||||
),
|
||||
),
|
||||
)
|
||||
.innerJoin(artist, eq(artist.platform_id, spotifyArtist.id)),
|
||||
);
|
||||
await db.insert(artistGenre).select(
|
||||
db
|
||||
.select({
|
||||
artist: artist.id,
|
||||
genre: genre.id,
|
||||
})
|
||||
.from(genre)
|
||||
.where(inArray(genre.name, spotifyArtist.genres))
|
||||
.innerJoin(artist, eq(artist.platform_id, spotifyArtist.id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMissingArtists(artistIds: string[]) {
|
||||
const existingArtists = await db
|
||||
.select({ id: artist.id })
|
||||
.from(artist)
|
||||
.where(inArray(artist.platform_id, artistIds));
|
||||
return artistIds.filter((id) => !existingArtists.some((a) => a.id === id));
|
||||
}
|
||||
|
||||
async function lookupMissingArtists(artistIds: string[]) {
|
||||
const missingArtistIds = await getMissingArtists(artistIds);
|
||||
if (missingArtistIds.length === 0) return [];
|
||||
const missingArtists = await defaultSdk.artists.get(missingArtistIds);
|
||||
await upsertArtists(missingArtists);
|
||||
return missingArtists;
|
||||
}
|
||||
|
||||
function isFullArtistArray(
|
||||
artists: Artist[] | SimplifiedArtist[],
|
||||
): artists is Artist[] {
|
||||
return "images" in artists[0]!;
|
||||
}
|
||||
|
||||
async function upsertMissingArtists(artists: SimplifiedArtist[] | Artist[]) {
|
||||
if (artists.length === 0) return;
|
||||
let missingArtists: Artist[];
|
||||
if (isFullArtistArray(artists)) {
|
||||
const missingArtistIds = await getMissingArtists(artists.map((t) => t.id));
|
||||
if (missingArtistIds.length === 0) return;
|
||||
missingArtists = artists.filter((a) => missingArtistIds.includes(a.id));
|
||||
} else {
|
||||
missingArtists = await lookupMissingArtists(artists.map((t) => t.id));
|
||||
}
|
||||
if (missingArtists.length === 0) return;
|
||||
await upsertArtists(missingArtists);
|
||||
return missingArtists;
|
||||
}
|
||||
|
||||
async function getArtistIdMap(artistIds: string[]) {
|
||||
if (artistIds.length === 0) return new Map<string, string>();
|
||||
const rows = await db
|
||||
.select({ id: artist.id, platform_id: artist.platform_id })
|
||||
.from(artist)
|
||||
.where(inArray(artist.platform_id, artistIds));
|
||||
return new Map(rows.map((row) => [row.platform_id, row.id]));
|
||||
}
|
||||
|
||||
async function getAlbumIdMap(albumIds: string[]) {
|
||||
if (albumIds.length === 0) return new Map<string, string>();
|
||||
const rows = await db
|
||||
.select({ id: album.id, platform_id: album.platform_id })
|
||||
.from(album)
|
||||
.where(inArray(album.platform_id, albumIds));
|
||||
return new Map(rows.map((row) => [row.platform_id ?? "", row.id]));
|
||||
}
|
||||
|
||||
async function getTrackIdMap(trackIds: string[]) {
|
||||
if (trackIds.length === 0) return new Map<string, string>();
|
||||
const rows = await db
|
||||
.select({ id: track.id, platform_id: track.platform_id })
|
||||
.from(track)
|
||||
.where(inArray(track.platform_id, trackIds));
|
||||
return new Map(rows.map((row) => [row.platform_id ?? "", row.id]));
|
||||
}
|
||||
|
||||
export async function upsertAlbums(albums: Album[] | SimplifiedAlbum[]) {
|
||||
await upsertMissingArtists(albums.flatMap((a) => a.artists));
|
||||
await db.insert(album).values(
|
||||
albums.map(({ id, name, type, popularity, release_date, label }) => ({
|
||||
platform: PLATFORM_SPOTIFY,
|
||||
platform_id: id,
|
||||
name,
|
||||
type,
|
||||
popularity,
|
||||
release_date: new Date(release_date),
|
||||
label,
|
||||
})),
|
||||
);
|
||||
await upsertImages(albums.flatMap((a) => a.images));
|
||||
await upsertGenres(albums.flatMap((a) => a.genres));
|
||||
for (const spotifyAlbum of albums) {
|
||||
await db.insert(albumImage).select(
|
||||
db
|
||||
.select({
|
||||
album: album.id,
|
||||
image: platformImage.id,
|
||||
})
|
||||
.from(platformImage)
|
||||
.where(
|
||||
and(
|
||||
eq(platformImage.platform, PLATFORM_SPOTIFY),
|
||||
inArray(
|
||||
platformImage.url,
|
||||
spotifyAlbum.images.map((t) => t.url),
|
||||
),
|
||||
),
|
||||
)
|
||||
.innerJoin(album, eq(album.platform_id, spotifyAlbum.id)),
|
||||
);
|
||||
await db.insert(albumArtist).select(
|
||||
db
|
||||
.select({
|
||||
album: album.id,
|
||||
artist: artist.id,
|
||||
})
|
||||
.from(artist)
|
||||
.where(inArray(artist.platform_id, spotifyAlbum.artists.map((t) => t.id)))
|
||||
.innerJoin(album, eq(album.platform_id, spotifyAlbum.id)),
|
||||
);
|
||||
await db.insert(albumGenre).select(
|
||||
db
|
||||
.select({
|
||||
album: album.id,
|
||||
genre: genre.id,
|
||||
})
|
||||
.from(genre)
|
||||
.where(inArray(genre.name, spotifyAlbum.genres))
|
||||
.innerJoin(album, eq(album.platform_id, spotifyAlbum.id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertTracks(tracks: Track[]) {
|
||||
if (tracks.length === 0) return;
|
||||
await upsertAlbums(tracks.map((t) => t.album));
|
||||
await upsertMissingArtists(tracks.flatMap((t) => t.artists));
|
||||
const albumIdMap = await getAlbumIdMap(tracks.map((t) => t.album.id));
|
||||
await db.insert(track).values(
|
||||
tracks.map((spotifyTrack) => ({
|
||||
album: albumIdMap.get(spotifyTrack.album.id)!,
|
||||
name: spotifyTrack.name,
|
||||
platform: PLATFORM_SPOTIFY,
|
||||
platform_id: spotifyTrack.id,
|
||||
popularity: spotifyTrack.popularity,
|
||||
duration: spotifyTrack.duration_ms,
|
||||
explicit: spotifyTrack.explicit,
|
||||
disc_number: spotifyTrack.disc_number,
|
||||
track_number: spotifyTrack.track_number,
|
||||
})),
|
||||
);
|
||||
for (const spotifyTrack of tracks) {
|
||||
await db.insert(trackArtist).select(
|
||||
db
|
||||
.select({
|
||||
track: track.id,
|
||||
artist: artist.id,
|
||||
})
|
||||
.from(artist)
|
||||
.where(inArray(artist.platform_id, spotifyTrack.artists.map((t) => t.id)))
|
||||
.innerJoin(track, eq(track.platform_id, spotifyTrack.id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertTopArtists(
|
||||
userId: string,
|
||||
timeline: "short_term" | "medium_term" | "long_term",
|
||||
artists: Artist[],
|
||||
) {
|
||||
if (artists.length === 0) return;
|
||||
await upsertArtists(artists);
|
||||
const artistIdMap = await getArtistIdMap(artists.map((t) => t.id));
|
||||
await db.insert(topArtist).values(
|
||||
artists.map((spotifyArtist, index) => ({
|
||||
artist: artistIdMap.get(spotifyArtist.id)!,
|
||||
position: index + 1,
|
||||
user: userId,
|
||||
timeline,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertTopTracks(
|
||||
userId: string,
|
||||
timeline: "short_term" | "medium_term" | "long_term",
|
||||
tracks: Track[],
|
||||
) {
|
||||
if (tracks.length === 0) return;
|
||||
await upsertTracks(tracks);
|
||||
const trackIdMap = await getTrackIdMap(tracks.map((t) => t.id));
|
||||
await db.insert(topTrack).values(
|
||||
tracks.map((spotifyTrack, index) => ({
|
||||
track: trackIdMap.get(spotifyTrack.id)!,
|
||||
position: index + 1,
|
||||
user: userId,
|
||||
timeline,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertSavedAlbums(userId: string, saved: SavedAlbum[]) {
|
||||
if (saved.length === 0) return;
|
||||
const albums = saved.map((item) => item.album);
|
||||
await upsertAlbums(albums);
|
||||
const albumIdMap = await getAlbumIdMap(albums.map((t) => t.id));
|
||||
await db.insert(savedAlbum).values(
|
||||
saved.map((item) => ({
|
||||
album: albumIdMap.get(item.album.id)!,
|
||||
user: userId,
|
||||
saved_at: new Date(item.added_at),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertSavedTracks(userId: string, saved: SavedTrack[]) {
|
||||
if (saved.length === 0) return;
|
||||
const tracks = saved.map((item) => item.track);
|
||||
await upsertTracks(tracks);
|
||||
const trackIdMap = await getTrackIdMap(tracks.map((t) => t.id));
|
||||
await db.insert(savedTrack).values(
|
||||
saved.map((item) => ({
|
||||
track: trackIdMap.get(item.track.id)!,
|
||||
user: userId,
|
||||
saved_at: new Date(item.added_at),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertFollowedArtists(
|
||||
userId: string,
|
||||
followed: FollowedArtists,
|
||||
) {
|
||||
const artists = followed.artists.items;
|
||||
if (artists.length === 0) return;
|
||||
await upsertArtists(artists);
|
||||
const artistIdMap = await getArtistIdMap(artists.map((t) => t.id));
|
||||
await db.insert(followedArtist).values(
|
||||
artists.map((spotifyArtist) => ({
|
||||
artist: artistIdMap.get(spotifyArtist.id)!,
|
||||
user: userId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export async function upsertPlaybackHistory(
|
||||
userId: string,
|
||||
items: PlayHistory[],
|
||||
) {
|
||||
if (items.length === 0) return;
|
||||
const tracks = items.map((item) => item.track);
|
||||
await upsertTracks(tracks);
|
||||
const trackIdMap = await getTrackIdMap(tracks.map((t) => t.id));
|
||||
await db.insert(playbackHistory).values(
|
||||
items.map((item) => ({
|
||||
track: trackIdMap.get(item.track.id)!,
|
||||
user: userId,
|
||||
played_at: new Date(item.played_at),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
import Elysia from "elysia";
|
||||
import { auth, betterAuthElysia, SPOTIFY_CLIENT_ID } from "../auth";
|
||||
import { SpotifyApi } from "@spotify/web-api-ts-sdk";
|
||||
import {
|
||||
upsertFollowedArtists,
|
||||
upsertPlaybackHistory,
|
||||
upsertSavedAlbums,
|
||||
upsertSavedTracks,
|
||||
upsertTopArtists,
|
||||
upsertTopTracks,
|
||||
} from "../db/spotify";
|
||||
|
||||
export const syncApp = new Elysia().use(betterAuthElysia).post(
|
||||
"/sync",
|
||||
|
|
@ -28,8 +36,25 @@ export const syncApp = new Elysia().use(betterAuthElysia).post(
|
|||
timeline,
|
||||
50,
|
||||
);
|
||||
const topTracks = await sdk.currentUser.topItems("tracks", timeline, 50);
|
||||
await upsertTopArtists(user.id, timeline, topArtists.items);
|
||||
}
|
||||
for (const timeline of [
|
||||
"short_term",
|
||||
"medium_term",
|
||||
"long_term",
|
||||
] as const) {
|
||||
const topTracks = await sdk.currentUser.topItems("tracks", timeline, 50);
|
||||
await upsertTopTracks(user.id, timeline, topTracks.items);
|
||||
}
|
||||
const followedArtists = await sdk.currentUser.followedArtists();
|
||||
await upsertFollowedArtists(user.id, followedArtists);
|
||||
const savedAlbums = await sdk.currentUser.albums.savedAlbums(50);
|
||||
await upsertSavedAlbums(user.id, savedAlbums.items);
|
||||
const savedTracks = await sdk.currentUser.tracks.savedTracks(50);
|
||||
await upsertSavedTracks(user.id, savedTracks.items);
|
||||
const recentlyPlayed = await sdk.player.getRecentlyPlayedTracks(50);
|
||||
await upsertPlaybackHistory(user.id, recentlyPlayed.items);
|
||||
return { ok: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue