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 { db } from "./db";
|
||||||
import Elysia, { status, type Context } from "elysia";
|
import Elysia, { status, type Context } from "elysia";
|
||||||
import * as schema from "./db/auth-schema";
|
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_ID = process.env.SPOTIFY_CLIENT_ID!;
|
||||||
export const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!;
|
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({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "pg",
|
provider: "pg",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
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 {
|
import {
|
||||||
boolean,
|
boolean,
|
||||||
index,
|
index,
|
||||||
|
|
@ -7,6 +8,7 @@ import {
|
||||||
primaryKey,
|
primaryKey,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
uuid,
|
uuid,
|
||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
@ -26,6 +28,7 @@ export const artist = pgTable("artist", {
|
||||||
|
|
||||||
export const genre = pgTable("genre", {
|
export const genre = pgTable("genre", {
|
||||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||||
|
name: text().notNull().unique(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const artistGenre = pgTable(
|
export const artistGenre = pgTable(
|
||||||
|
|
@ -45,13 +48,20 @@ export const artistGenre = pgTable(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const platformImage = pgTable("platform_image", {
|
export const platformImage = pgTable(
|
||||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
"platform_image",
|
||||||
platform: platform().notNull(),
|
{
|
||||||
url: text(),
|
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||||
height: integer(),
|
platform: platform().notNull(),
|
||||||
width: integer(),
|
url: text(),
|
||||||
});
|
height: integer(),
|
||||||
|
width: integer(),
|
||||||
|
},
|
||||||
|
(platformImage) => [
|
||||||
|
uniqueIndex().on(platformImage.platform, platformImage.url),
|
||||||
|
index().on(platformImage.url),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
export const artistImage = pgTable(
|
export const artistImage = pgTable(
|
||||||
"artist_image",
|
"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(
|
export const track = pgTable(
|
||||||
"track",
|
"track",
|
||||||
{
|
{
|
||||||
|
|
@ -252,3 +296,230 @@ export const playbackHistory = pgTable(
|
||||||
},
|
},
|
||||||
(playbackHistory) => [index().on([playbackHistory.user])],
|
(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 Elysia from "elysia";
|
||||||
import { auth, betterAuthElysia, SPOTIFY_CLIENT_ID } from "../auth";
|
import { auth, betterAuthElysia, SPOTIFY_CLIENT_ID } from "../auth";
|
||||||
import { SpotifyApi } from "@spotify/web-api-ts-sdk";
|
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(
|
export const syncApp = new Elysia().use(betterAuthElysia).post(
|
||||||
"/sync",
|
"/sync",
|
||||||
|
|
@ -28,8 +36,25 @@ export const syncApp = new Elysia().use(betterAuthElysia).post(
|
||||||
timeline,
|
timeline,
|
||||||
50,
|
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,
|
auth: true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue