diff --git a/apps/api/drizzle/0004_long_zzzax.sql b/apps/api/drizzle/0004_long_zzzax.sql new file mode 100644 index 00000000..7015ff14 --- /dev/null +++ b/apps/api/drizzle/0004_long_zzzax.sql @@ -0,0 +1,10 @@ +CREATE TABLE "spotify_apps" ( + "xata_id" text PRIMARY KEY DEFAULT xata_id() NOT NULL, + "xata_version" integer, + "spotify_app_id" text NOT NULL, + "xata_createdat" timestamp DEFAULT now() NOT NULL, + "xata_updatedat" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "spotify_accounts" ADD COLUMN "spotify_app_id" text NOT NULL;--> statement-breakpoint +ALTER TABLE "spotify_tokens" ADD COLUMN "spotify_app_id" text NOT NULL; \ No newline at end of file diff --git a/apps/api/drizzle/0005_same_hydra.sql b/apps/api/drizzle/0005_same_hydra.sql new file mode 100644 index 00000000..08727106 --- /dev/null +++ b/apps/api/drizzle/0005_same_hydra.sql @@ -0,0 +1,2 @@ +ALTER TABLE "spotify_accounts" ALTER COLUMN "spotify_app_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "spotify_apps" ADD COLUMN "spotify_secret" text NOT NULL; \ No newline at end of file diff --git a/apps/api/drizzle/meta/0004_snapshot.json b/apps/api/drizzle/meta/0004_snapshot.json new file mode 100644 index 00000000..6365f42d --- /dev/null +++ b/apps/api/drizzle/meta/0004_snapshot.json @@ -0,0 +1,3224 @@ +{ + "id": "3294c136-37d7-4c61-8633-26d17c05a059", + "prevId": "015093fe-a66e-4ec3-b5b6-5466c6266a39", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.album_tracks": { + "name": "album_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "album_tracks_album_id_albums_xata_id_fk": { + "name": "album_tracks_album_id_albums_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "album_tracks_track_id_tracks_xata_id_fk": { + "name": "album_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.albums": { + "name": "albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "albums_uri_unique": { + "name": "albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + }, + "albums_apple_music_link_unique": { + "name": "albums_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "albums_spotify_link_unique": { + "name": "albums_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "albums_tidal_link_unique": { + "name": "albums_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "albums_youtube_link_unique": { + "name": "albums_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "albums_sha256_unique": { + "name": "albums_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_secret": { + "name": "shared_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_xata_id_fk": { + "name": "api_keys_user_id_users_xata_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_albums": { + "name": "artist_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_albums_artist_id_artists_xata_id_fk": { + "name": "artist_albums_artist_id_artists_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_albums_album_id_albums_xata_id_fk": { + "name": "artist_albums_album_id_albums_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_tracks": { + "name": "artist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_tracks_artist_id_artists_xata_id_fk": { + "name": "artist_tracks_artist_id_artists_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_tracks_track_id_tracks_xata_id_fk": { + "name": "artist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artists": { + "name": "artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biography": { + "name": "biography", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "born": { + "name": "born", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "born_in": { + "name": "born_in", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "died": { + "name": "died", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genres": { + "name": "genres", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "artists_sha256_unique": { + "name": "artists_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "artists_uri_unique": { + "name": "artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_accounts": { + "name": "dropbox_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_accounts_user_id_users_xata_id_fk": { + "name": "dropbox_accounts_user_id_users_xata_id_fk", + "tableFrom": "dropbox_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_accounts_email_unique": { + "name": "dropbox_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_directories": { + "name": "dropbox_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_directories_parent_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_directories_parent_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_directories", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_directories_file_id_unique": { + "name": "dropbox_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_paths": { + "name": "dropbox_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_paths_directory_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_paths_directory_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_paths", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_paths_file_id_unique": { + "name": "dropbox_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_tokens": { + "name": "dropbox_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox": { + "name": "dropbox", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_token_id": { + "name": "dropbox_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_user_id_users_xata_id_fk": { + "name": "dropbox_user_id_users_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk": { + "name": "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "dropbox_tokens", + "columnsFrom": [ + "dropbox_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_accounts": { + "name": "google_drive_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_accounts_user_id_users_xata_id_fk": { + "name": "google_drive_accounts_user_id_users_xata_id_fk", + "tableFrom": "google_drive_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_accounts_email_unique": { + "name": "google_drive_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_directories": { + "name": "google_drive_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_directories_parent_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_directories_parent_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_directories", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_directories_file_id_unique": { + "name": "google_drive_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_paths": { + "name": "google_drive_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_paths_directory_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_paths_directory_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_paths", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_paths_file_id_unique": { + "name": "google_drive_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_tokens": { + "name": "google_drive_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive": { + "name": "google_drive", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_token_id": { + "name": "google_drive_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk": { + "name": "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "google_drive_tokens", + "columnsFrom": [ + "google_drive_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "google_drive_user_id_users_xata_id_fk": { + "name": "google_drive_user_id_users_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loved_tracks": { + "name": "loved_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "loved_tracks_user_id_users_xata_id_fk": { + "name": "loved_tracks_user_id_users_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "loved_tracks_track_id_tracks_xata_id_fk": { + "name": "loved_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "loved_tracks_uri_unique": { + "name": "loved_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_tracks": { + "name": "playlist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlist_tracks_playlist_id_playlists_xata_id_fk": { + "name": "playlist_tracks_playlist_id_playlists_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "playlist_tracks_track_id_tracks_xata_id_fk": { + "name": "playlist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlists": { + "name": "playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlists_created_by_users_xata_id_fk": { + "name": "playlists_created_by_users_xata_id_fk", + "tableFrom": "playlists", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "playlists_uri_unique": { + "name": "playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_shouts": { + "name": "profile_shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "profile_shouts_user_id_users_xata_id_fk": { + "name": "profile_shouts_user_id_users_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "profile_shouts_shout_id_shouts_xata_id_fk": { + "name": "profile_shouts_shout_id_shouts_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queue_tracks": { + "name": "queue_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_uri": { + "name": "file_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "queue_tracks_user_id_users_xata_id_fk": { + "name": "queue_tracks_user_id_users_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "queue_tracks_track_id_tracks_xata_id_fk": { + "name": "queue_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrobbles": { + "name": "scrobbles", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scrobbles_user_id_users_xata_id_fk": { + "name": "scrobbles_user_id_users_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_track_id_tracks_xata_id_fk": { + "name": "scrobbles_track_id_tracks_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_album_id_albums_xata_id_fk": { + "name": "scrobbles_album_id_albums_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_artist_id_artists_xata_id_fk": { + "name": "scrobbles_artist_id_artists_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scrobbles_uri_unique": { + "name": "scrobbles_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_likes": { + "name": "shout_likes", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "shout_likes_user_id_users_xata_id_fk": { + "name": "shout_likes_user_id_users_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_likes_shout_id_shouts_xata_id_fk": { + "name": "shout_likes_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shout_likes_uri_unique": { + "name": "shout_likes_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_reports": { + "name": "shout_reports", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shout_reports_user_id_users_xata_id_fk": { + "name": "shout_reports_user_id_users_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_reports_shout_id_shouts_xata_id_fk": { + "name": "shout_reports_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shouts": { + "name": "shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scrobble_id": { + "name": "scrobble_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shouts_track_id_tracks_xata_id_fk": { + "name": "shouts_track_id_tracks_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_artist_id_users_xata_id_fk": { + "name": "shouts_artist_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_album_id_albums_xata_id_fk": { + "name": "shouts_album_id_albums_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_scrobble_id_scrobbles_xata_id_fk": { + "name": "shouts_scrobble_id_scrobbles_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "scrobbles", + "columnsFrom": [ + "scrobble_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_author_id_users_xata_id_fk": { + "name": "shouts_author_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_parent_id_shouts_xata_id_fk": { + "name": "shouts_parent_id_shouts_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "shouts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shouts_uri_unique": { + "name": "shouts_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_accounts": { + "name": "spotify_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_accounts_user_id_users_xata_id_fk": { + "name": "spotify_accounts_user_id_users_xata_id_fk", + "tableFrom": "spotify_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_apps": { + "name": "spotify_apps", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_tokens": { + "name": "spotify_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_tokens_user_id_users_xata_id_fk": { + "name": "spotify_tokens_user_id_users_xata_id_fk", + "tableFrom": "spotify_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracks": { + "name": "tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_artist": { + "name": "album_artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album": { + "name": "album", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_number": { + "name": "track_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mb_id": { + "name": "mb_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disc_number": { + "name": "disc_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lyrics": { + "name": "lyrics", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composer": { + "name": "composer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genre": { + "name": "genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copyright_message": { + "name": "copyright_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_uri": { + "name": "album_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tracks_mb_id_unique": { + "name": "tracks_mb_id_unique", + "nullsNotDistinct": false, + "columns": [ + "mb_id" + ] + }, + "tracks_youtube_link_unique": { + "name": "tracks_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "tracks_spotify_link_unique": { + "name": "tracks_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "tracks_apple_music_link_unique": { + "name": "tracks_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "tracks_tidal_link_unique": { + "name": "tracks_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "tracks_sha256_unique": { + "name": "tracks_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "tracks_uri_unique": { + "name": "tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_albums": { + "name": "user_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_albums_user_id_users_xata_id_fk": { + "name": "user_albums_user_id_users_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_albums_album_id_albums_xata_id_fk": { + "name": "user_albums_album_id_albums_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_albums_uri_unique": { + "name": "user_albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_artists": { + "name": "user_artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_artists_user_id_users_xata_id_fk": { + "name": "user_artists_user_id_users_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_artists_artist_id_artists_xata_id_fk": { + "name": "user_artists_artist_id_artists_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_artists_uri_unique": { + "name": "user_artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_playlists": { + "name": "user_playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_playlists_user_id_users_xata_id_fk": { + "name": "user_playlists_user_id_users_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_playlists_playlist_id_playlists_xata_id_fk": { + "name": "user_playlists_playlist_id_playlists_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_playlists_uri_unique": { + "name": "user_playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tracks": { + "name": "user_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_tracks_user_id_users_xata_id_fk": { + "name": "user_tracks_user_id_users_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_tracks_track_id_tracks_xata_id_fk": { + "name": "user_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_tracks_uri_unique": { + "name": "user_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webscrobblers": { + "name": "webscrobblers", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webscrobblers_user_id_users_xata_id_fk": { + "name": "webscrobblers_user_id_users_xata_id_fk", + "tableFrom": "webscrobblers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/0005_snapshot.json b/apps/api/drizzle/meta/0005_snapshot.json new file mode 100644 index 00000000..faf08083 --- /dev/null +++ b/apps/api/drizzle/meta/0005_snapshot.json @@ -0,0 +1,3230 @@ +{ + "id": "b4f141d3-3e14-4aaf-b3f1-b3165d009289", + "prevId": "3294c136-37d7-4c61-8633-26d17c05a059", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.album_tracks": { + "name": "album_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "album_tracks_album_id_albums_xata_id_fk": { + "name": "album_tracks_album_id_albums_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "album_tracks_track_id_tracks_xata_id_fk": { + "name": "album_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "album_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.albums": { + "name": "albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "release_date": { + "name": "release_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "albums_uri_unique": { + "name": "albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + }, + "albums_apple_music_link_unique": { + "name": "albums_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "albums_spotify_link_unique": { + "name": "albums_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "albums_tidal_link_unique": { + "name": "albums_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "albums_youtube_link_unique": { + "name": "albums_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "albums_sha256_unique": { + "name": "albums_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_secret": { + "name": "shared_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_xata_id_fk": { + "name": "api_keys_user_id_users_xata_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_albums": { + "name": "artist_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_albums_artist_id_artists_xata_id_fk": { + "name": "artist_albums_artist_id_artists_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_albums_album_id_albums_xata_id_fk": { + "name": "artist_albums_album_id_albums_xata_id_fk", + "tableFrom": "artist_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artist_tracks": { + "name": "artist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "artist_tracks_artist_id_artists_xata_id_fk": { + "name": "artist_tracks_artist_id_artists_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "artist_tracks_track_id_tracks_xata_id_fk": { + "name": "artist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "artist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artists": { + "name": "artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biography": { + "name": "biography", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "born": { + "name": "born", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "born_in": { + "name": "born_in", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "died": { + "name": "died", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genres": { + "name": "genres", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "artists_sha256_unique": { + "name": "artists_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "artists_uri_unique": { + "name": "artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_accounts": { + "name": "dropbox_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_accounts_user_id_users_xata_id_fk": { + "name": "dropbox_accounts_user_id_users_xata_id_fk", + "tableFrom": "dropbox_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_accounts_email_unique": { + "name": "dropbox_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_directories": { + "name": "dropbox_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_directories_parent_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_directories_parent_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_directories", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_directories_file_id_unique": { + "name": "dropbox_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_paths": { + "name": "dropbox_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_id": { + "name": "dropbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_paths_directory_id_dropbox_directories_xata_id_fk": { + "name": "dropbox_paths_directory_id_dropbox_directories_xata_id_fk", + "tableFrom": "dropbox_paths", + "tableTo": "dropbox_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dropbox_paths_file_id_unique": { + "name": "dropbox_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_tokens": { + "name": "dropbox_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox": { + "name": "dropbox", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dropbox_token_id": { + "name": "dropbox_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dropbox_user_id_users_xata_id_fk": { + "name": "dropbox_user_id_users_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk": { + "name": "dropbox_dropbox_token_id_dropbox_tokens_xata_id_fk", + "tableFrom": "dropbox", + "tableTo": "dropbox_tokens", + "columnsFrom": [ + "dropbox_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_accounts": { + "name": "google_drive_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_accounts_user_id_users_xata_id_fk": { + "name": "google_drive_accounts_user_id_users_xata_id_fk", + "tableFrom": "google_drive_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_accounts_email_unique": { + "name": "google_drive_accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_directories": { + "name": "google_drive_directories", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_directories_parent_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_directories_parent_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_directories", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_directories_file_id_unique": { + "name": "google_drive_directories_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_paths": { + "name": "google_drive_paths", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_id": { + "name": "google_drive_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory_id": { + "name": "directory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_paths_directory_id_google_drive_directories_xata_id_fk": { + "name": "google_drive_paths_directory_id_google_drive_directories_xata_id_fk", + "tableFrom": "google_drive_paths", + "tableTo": "google_drive_directories", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_drive_paths_file_id_unique": { + "name": "google_drive_paths_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive_tokens": { + "name": "google_drive_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_drive": { + "name": "google_drive", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "google_drive_token_id": { + "name": "google_drive_token_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk": { + "name": "google_drive_google_drive_token_id_google_drive_tokens_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "google_drive_tokens", + "columnsFrom": [ + "google_drive_token_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "google_drive_user_id_users_xata_id_fk": { + "name": "google_drive_user_id_users_xata_id_fk", + "tableFrom": "google_drive", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loved_tracks": { + "name": "loved_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "loved_tracks_user_id_users_xata_id_fk": { + "name": "loved_tracks_user_id_users_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "loved_tracks_track_id_tracks_xata_id_fk": { + "name": "loved_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "loved_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "loved_tracks_uri_unique": { + "name": "loved_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_tracks": { + "name": "playlist_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlist_tracks_playlist_id_playlists_xata_id_fk": { + "name": "playlist_tracks_playlist_id_playlists_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "playlist_tracks_track_id_tracks_xata_id_fk": { + "name": "playlist_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "playlist_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlists": { + "name": "playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playlists_created_by_users_xata_id_fk": { + "name": "playlists_created_by_users_xata_id_fk", + "tableFrom": "playlists", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "playlists_uri_unique": { + "name": "playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_shouts": { + "name": "profile_shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "profile_shouts_user_id_users_xata_id_fk": { + "name": "profile_shouts_user_id_users_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "profile_shouts_shout_id_shouts_xata_id_fk": { + "name": "profile_shouts_shout_id_shouts_xata_id_fk", + "tableFrom": "profile_shouts", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queue_tracks": { + "name": "queue_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_uri": { + "name": "file_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "queue_tracks_user_id_users_xata_id_fk": { + "name": "queue_tracks_user_id_users_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "queue_tracks_track_id_tracks_xata_id_fk": { + "name": "queue_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "queue_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrobbles": { + "name": "scrobbles", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scrobbles_user_id_users_xata_id_fk": { + "name": "scrobbles_user_id_users_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_track_id_tracks_xata_id_fk": { + "name": "scrobbles_track_id_tracks_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_album_id_albums_xata_id_fk": { + "name": "scrobbles_album_id_albums_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scrobbles_artist_id_artists_xata_id_fk": { + "name": "scrobbles_artist_id_artists_xata_id_fk", + "tableFrom": "scrobbles", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scrobbles_uri_unique": { + "name": "scrobbles_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_likes": { + "name": "shout_likes", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "shout_likes_user_id_users_xata_id_fk": { + "name": "shout_likes_user_id_users_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_likes_shout_id_shouts_xata_id_fk": { + "name": "shout_likes_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_likes", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shout_likes_uri_unique": { + "name": "shout_likes_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shout_reports": { + "name": "shout_reports", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shout_id": { + "name": "shout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shout_reports_user_id_users_xata_id_fk": { + "name": "shout_reports_user_id_users_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shout_reports_shout_id_shouts_xata_id_fk": { + "name": "shout_reports_shout_id_shouts_xata_id_fk", + "tableFrom": "shout_reports", + "tableTo": "shouts", + "columnsFrom": [ + "shout_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shouts": { + "name": "shouts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scrobble_id": { + "name": "scrobble_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shouts_track_id_tracks_xata_id_fk": { + "name": "shouts_track_id_tracks_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_artist_id_users_xata_id_fk": { + "name": "shouts_artist_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_album_id_albums_xata_id_fk": { + "name": "shouts_album_id_albums_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_scrobble_id_scrobbles_xata_id_fk": { + "name": "shouts_scrobble_id_scrobbles_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "scrobbles", + "columnsFrom": [ + "scrobble_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_author_id_users_xata_id_fk": { + "name": "shouts_author_id_users_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shouts_parent_id_shouts_xata_id_fk": { + "name": "shouts_parent_id_shouts_xata_id_fk", + "tableFrom": "shouts", + "tableTo": "shouts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shouts_uri_unique": { + "name": "shouts_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_accounts": { + "name": "spotify_accounts", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_beta_user": { + "name": "is_beta_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_accounts_user_id_users_xata_id_fk": { + "name": "spotify_accounts_user_id_users_xata_id_fk", + "tableFrom": "spotify_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_apps": { + "name": "spotify_apps", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotify_secret": { + "name": "spotify_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spotify_tokens": { + "name": "spotify_tokens", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotify_app_id": { + "name": "spotify_app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spotify_tokens_user_id_users_xata_id_fk": { + "name": "spotify_tokens_user_id_users_xata_id_fk", + "tableFrom": "spotify_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracks": { + "name": "tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist": { + "name": "artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_artist": { + "name": "album_artist", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_art": { + "name": "album_art", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album": { + "name": "album", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_number": { + "name": "track_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mb_id": { + "name": "mb_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_link": { + "name": "youtube_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_link": { + "name": "spotify_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_link": { + "name": "apple_music_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tidal_link": { + "name": "tidal_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disc_number": { + "name": "disc_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lyrics": { + "name": "lyrics", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composer": { + "name": "composer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genre": { + "name": "genre", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copyright_message": { + "name": "copyright_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_uri": { + "name": "album_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_uri": { + "name": "artist_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tracks_mb_id_unique": { + "name": "tracks_mb_id_unique", + "nullsNotDistinct": false, + "columns": [ + "mb_id" + ] + }, + "tracks_youtube_link_unique": { + "name": "tracks_youtube_link_unique", + "nullsNotDistinct": false, + "columns": [ + "youtube_link" + ] + }, + "tracks_spotify_link_unique": { + "name": "tracks_spotify_link_unique", + "nullsNotDistinct": false, + "columns": [ + "spotify_link" + ] + }, + "tracks_apple_music_link_unique": { + "name": "tracks_apple_music_link_unique", + "nullsNotDistinct": false, + "columns": [ + "apple_music_link" + ] + }, + "tracks_tidal_link_unique": { + "name": "tracks_tidal_link_unique", + "nullsNotDistinct": false, + "columns": [ + "tidal_link" + ] + }, + "tracks_sha256_unique": { + "name": "tracks_sha256_unique", + "nullsNotDistinct": false, + "columns": [ + "sha256" + ] + }, + "tracks_uri_unique": { + "name": "tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_albums": { + "name": "user_albums", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_albums_user_id_users_xata_id_fk": { + "name": "user_albums_user_id_users_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_albums_album_id_albums_xata_id_fk": { + "name": "user_albums_album_id_albums_xata_id_fk", + "tableFrom": "user_albums", + "tableTo": "albums", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_albums_uri_unique": { + "name": "user_albums_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_artists": { + "name": "user_artists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artist_id": { + "name": "artist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_artists_user_id_users_xata_id_fk": { + "name": "user_artists_user_id_users_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_artists_artist_id_artists_xata_id_fk": { + "name": "user_artists_artist_id_artists_xata_id_fk", + "tableFrom": "user_artists", + "tableTo": "artists", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_artists_uri_unique": { + "name": "user_artists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_playlists": { + "name": "user_playlists", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "playlist_id": { + "name": "playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_playlists_user_id_users_xata_id_fk": { + "name": "user_playlists_user_id_users_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_playlists_playlist_id_playlists_xata_id_fk": { + "name": "user_playlists_playlist_id_playlists_xata_id_fk", + "tableFrom": "user_playlists", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_playlists_uri_unique": { + "name": "user_playlists_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tracks": { + "name": "user_tracks", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "track_id": { + "name": "track_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scrobbles": { + "name": "scrobbles", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_tracks_user_id_users_xata_id_fk": { + "name": "user_tracks_user_id_users_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_tracks_track_id_tracks_xata_id_fk": { + "name": "user_tracks_track_id_tracks_xata_id_fk", + "tableFrom": "user_tracks", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_tracks_uri_unique": { + "name": "user_tracks_uri_unique", + "nullsNotDistinct": false, + "columns": [ + "uri" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_version": { + "name": "xata_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webscrobblers": { + "name": "webscrobblers", + "schema": "", + "columns": { + "xata_id": { + "name": "xata_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "xata_id()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "xata_createdat": { + "name": "xata_createdat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "xata_updatedat": { + "name": "xata_updatedat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webscrobblers_user_id_users_xata_id_fk": { + "name": "webscrobblers_user_id_users_xata_id_fk", + "tableFrom": "webscrobblers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "xata_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index eb29d46d..915fe233 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -29,6 +29,20 @@ "when": 1759819936859, "tag": "0003_same_rocket_racer", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1761572359505, + "tag": "0004_long_zzzax", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1761625200550, + "tag": "0005_same_hydra", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/schema/index.ts b/apps/api/src/schema/index.ts index 880233fc..5c27c377 100644 --- a/apps/api/src/schema/index.ts +++ b/apps/api/src/schema/index.ts @@ -23,6 +23,7 @@ import shoutLikes from "./shout-likes"; import shoutReports from "./shout-reports"; import shouts from "./shouts"; import spotifyAccounts from "./spotify-accounts"; +import spotifyApps from "./spotify-apps"; import spotifyTokens from "./spotify-tokens"; import tracks from "./tracks"; import userAlbums from "./user-albums"; @@ -54,6 +55,7 @@ export default { lovedTracks, spotifyAccounts, spotifyTokens, + spotifyApps, artistTracks, artistAlbums, dropboxAccounts, diff --git a/apps/api/src/schema/spotify-accounts.ts b/apps/api/src/schema/spotify-accounts.ts index 79d27407..e885a6f8 100644 --- a/apps/api/src/schema/spotify-accounts.ts +++ b/apps/api/src/schema/spotify-accounts.ts @@ -9,13 +9,16 @@ import { import users from "./users"; const spotifyAccounts = pgTable("spotify_accounts", { - id: text("xata_id").primaryKey().default(sql`xata_id()`), + id: text("xata_id") + .primaryKey() + .default(sql`xata_id()`), xataVersion: integer("xata_version"), email: text("email").notNull(), userId: text("user_id") .notNull() .references(() => users.id), isBetaUser: boolean("is_beta_user").default(false).notNull(), + spotifyAppId: text("spotify_app_id"), createdAt: timestamp("xata_createdat").defaultNow().notNull(), updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), }); diff --git a/apps/api/src/schema/spotify-apps.ts b/apps/api/src/schema/spotify-apps.ts new file mode 100644 index 00000000..b0f96354 --- /dev/null +++ b/apps/api/src/schema/spotify-apps.ts @@ -0,0 +1,18 @@ +import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; +import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +const spotifyApps = pgTable("spotify_apps", { + id: text("xata_id") + .primaryKey() + .default(sql`xata_id()`), + xataVersion: integer("xata_version"), + spotifyAppId: text("spotify_app_id").notNull(), + spotifySecret: text("spotify_secret").notNull(), + createdAt: timestamp("xata_createdat").defaultNow().notNull(), + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), +}); + +export type SelectSpotifyApp = InferSelectModel; +export type InsertSpotifyApp = InferInsertModel; + +export default spotifyApps; diff --git a/apps/api/src/schema/spotify-tokens.ts b/apps/api/src/schema/spotify-tokens.ts index b3b0f9d4..732cb2e1 100644 --- a/apps/api/src/schema/spotify-tokens.ts +++ b/apps/api/src/schema/spotify-tokens.ts @@ -3,13 +3,16 @@ import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; import users from "./users"; const spotifyTokens = pgTable("spotify_tokens", { - id: text("xata_id").primaryKey().default(sql`xata_id()`), + id: text("xata_id") + .primaryKey() + .default(sql`xata_id()`), xataVersion: integer("xata_version"), accessToken: text("access_token").notNull(), refreshToken: text("refresh_token").notNull(), userId: text("user_id") .notNull() .references(() => users.id), + spotifyAppId: text("spotify_app_id").notNull(), createdAt: timestamp("xata_createdat").defaultNow().notNull(), updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), }); diff --git a/apps/api/src/scripts/genres.ts b/apps/api/src/scripts/genres.ts index ab0ff5a8..fc5aa979 100644 --- a/apps/api/src/scripts/genres.ts +++ b/apps/api/src/scripts/genres.ts @@ -11,15 +11,21 @@ async function getSpotifyToken(): Promise { .from(tables.spotifyTokens) .leftJoin( tables.spotifyAccounts, - eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId), + eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId) + ) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId) ) .where(eq(tables.spotifyAccounts.isBetaUser, true)) - .execute() - .then((res) => res.map(({ spotify_tokens }) => spotify_tokens)); + .execute(); const record = spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; - const refreshToken = decrypt(record.refreshToken, env.SPOTIFY_ENCRYPTION_KEY); + const refreshToken = decrypt( + record.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ); const accessToken = await fetch("https://accounts.spotify.com/api/token", { method: "POST", @@ -29,8 +35,11 @@ async function getSpotifyToken(): Promise { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: record.spotify_apps.spotifyAppId, + client_secret: decrypt( + record.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }) .then((res) => res.json() as Promise<{ access_token: string }>) @@ -51,7 +60,7 @@ async function getGenresAndPicture(artists) { headers: { Authorization: `Bearer ${token}`, }, - }, + } ) .then( (res) => @@ -64,7 +73,7 @@ async function getGenresAndPicture(artists) { images: Array<{ url: string }>; }>; }; - }>, + }> ) .then(async (data) => _.get(data, "artists.items.0")); diff --git a/apps/api/src/spotify/app.ts b/apps/api/src/spotify/app.ts index f334af54..ea36cc01 100644 --- a/apps/api/src/spotify/app.ts +++ b/apps/api/src/spotify/app.ts @@ -1,14 +1,16 @@ import { ctx } from "context"; -import { and, eq, or } from "drizzle-orm"; +import { and, eq, or, sql } from "drizzle-orm"; import { Hono } from "hono"; import jwt from "jsonwebtoken"; import { decrypt, encrypt } from "lib/crypto"; import { env } from "lib/env"; +import _ from "lodash"; import { requestCounter } from "metrics"; import crypto, { createHash } from "node:crypto"; import { rateLimiter } from "ratelimiter"; import lovedTracks from "schema/loved-tracks"; import spotifyAccounts from "schema/spotify-accounts"; +import spotifyApps from "schema/spotify-apps"; import spotifyTokens from "schema/spotify-tokens"; import tracks from "schema/tracks"; import users from "schema/users"; @@ -50,9 +52,26 @@ app.get("/login", async (c) => { return c.text("Unauthorized"); } + const spotifyAccount = await ctx.db + .select() + .from(spotifyAccounts) + .leftJoin(users, eq(spotifyAccounts.userId, users.id)) + .leftJoin( + spotifyApps, + eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId) + ) + .where( + and( + eq(spotifyAccounts.userId, user.id), + eq(spotifyAccounts.isBetaUser, true) + ) + ) + .limit(1) + .then((rows) => rows[0]); + const state = crypto.randomBytes(16).toString("hex"); ctx.kv.set(state, did); - const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${env.SPOTIFY_CLIENT_ID}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; + const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${spotifyAccount?.spotify_apps?.spotifyAppId}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; c.header( "Set-Cookie", `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` @@ -65,27 +84,6 @@ app.get("/callback", async (c) => { const params = new URLSearchParams(c.req.url.split("?")[1]); const { code, state } = Object.fromEntries(params.entries()); - const response = await fetch("https://accounts.spotify.com/api/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code, - redirect_uri: env.SPOTIFY_REDIRECT_URI, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, - }), - }); - const { - access_token, - refresh_token, - }: { - access_token: string; - refresh_token: string; - } = await response.json(); - if (!state) { return c.redirect(env.FRONTEND_URL); } @@ -107,6 +105,59 @@ app.get("/callback", async (c) => { return c.redirect(env.FRONTEND_URL); } + const spotifyAccount = await ctx.db + .select() + .from(spotifyAccounts) + .where( + and( + eq(spotifyAccounts.userId, user.id), + eq(spotifyAccounts.isBetaUser, true) + ) + ) + .limit(1) + .then((rows) => rows[0]); + + const spotifyAppId = spotifyAccount.spotifyAppId + ? spotifyAccount.spotifyAppId + : env.SPOTIFY_CLIENT_ID; + + const spotifyAppToken = await ctx.db + .select() + .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) + .where(eq(spotifyTokens.spotifyAppId, spotifyAppId)) + .limit(1) + .then((rows) => rows[0]); + + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: env.SPOTIFY_REDIRECT_URI, + client_id: spotifyAppId, + client_secret: spotifyAppToken?.spotify_apps + ? decrypt( + spotifyAppToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ) + : env.SPOTIFY_CLIENT_SECRET, + }), + }); + const { + access_token, + refresh_token, + }: { + access_token: string; + refresh_token: string; + } = await response.json(); + const existingSpotifyToken = await ctx.db .select() .from(spotifyTokens) @@ -127,6 +178,7 @@ app.get("/callback", async (c) => { userId: user.id, accessToken: encrypt(access_token, env.SPOTIFY_ENCRYPTION_KEY), refreshToken: encrypt(refresh_token, env.SPOTIFY_ENCRYPTION_KEY), + spotifyAppId, }); } @@ -179,9 +231,22 @@ app.post("/join", async (c) => { if (parsed.error) { c.status(400); - return c.text("Invalid email: " + parsed.error.message); + return c.text(`Invalid email: ${parsed.error.message}`); } + const apps = await ctx.db + .select({ + appId: spotifyApps.id, + spotifyAppId: spotifyApps.spotifyAppId, + accountCount: sql`COUNT(${spotifyAccounts.id})`.as( + "account_count" + ), + }) + .from(spotifyApps) + .leftJoin(spotifyAccounts, eq(spotifyApps.id, spotifyAccounts.spotifyAppId)) + .groupBy(spotifyApps.id) + .having(sql`COUNT(${spotifyAccounts.id}) < 25`); + const { email } = parsed.data; try { @@ -189,6 +254,7 @@ app.post("/join", async (c) => { userId: user.id, email, isBetaUser: false, + spotifyAppId: _.get(apps, "[0].spotifyAppId"), }); } catch (e) { if (!e.message.includes("duplicate key value violates unique constraint")) { @@ -326,6 +392,10 @@ app.put("/pause", async (c) => { const spotifyToken = await ctx.db .select() .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) .then((rows) => rows[0]); @@ -336,7 +406,7 @@ app.put("/pause", async (c) => { } const refreshToken = decrypt( - spotifyToken.refreshToken, + spotifyToken.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY ); @@ -349,8 +419,11 @@ app.put("/pause", async (c) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: spotifyToken.spotify_apps.spotifyAppId, + client_secret: decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }); @@ -402,6 +475,10 @@ app.put("/play", async (c) => { const spotifyToken = await ctx.db .select() .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) .then((rows) => rows[0]); @@ -412,7 +489,7 @@ app.put("/play", async (c) => { } const refreshToken = decrypt( - spotifyToken.refreshToken, + spotifyToken.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY ); @@ -425,8 +502,11 @@ app.put("/play", async (c) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: spotifyToken.spotify_apps.spotifyAppId, + client_secret: decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }); @@ -478,6 +558,10 @@ app.post("/next", async (c) => { const spotifyToken = await ctx.db .select() .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) .then((rows) => rows[0]); @@ -488,7 +572,7 @@ app.post("/next", async (c) => { } const refreshToken = decrypt( - spotifyToken.refreshToken, + spotifyToken.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY ); @@ -501,8 +585,11 @@ app.post("/next", async (c) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: spotifyToken.spotify_apps.spotifyAppId, + client_secret: decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }); @@ -554,6 +641,10 @@ app.post("/previous", async (c) => { const spotifyToken = await ctx.db .select() .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) .then((rows) => rows[0]); @@ -564,7 +655,7 @@ app.post("/previous", async (c) => { } const refreshToken = decrypt( - spotifyToken.refreshToken, + spotifyToken.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY ); @@ -577,8 +668,11 @@ app.post("/previous", async (c) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: spotifyToken.spotify_apps.spotifyAppId, + client_secret: decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }); @@ -633,6 +727,10 @@ app.put("/seek", async (c) => { const spotifyToken = await ctx.db .select() .from(spotifyTokens) + .leftJoin( + spotifyApps, + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) .then((rows) => rows[0]); @@ -643,7 +741,7 @@ app.put("/seek", async (c) => { } const refreshToken = decrypt( - spotifyToken.refreshToken, + spotifyToken.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY ); @@ -656,8 +754,11 @@ app.put("/seek", async (c) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: spotifyToken.spotify_apps.spotifyAppId, + client_secret: decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), }), }); diff --git a/apps/api/src/xrpc/app/rocksky/spotify/next.ts b/apps/api/src/xrpc/app/rocksky/spotify/next.ts index 0877786b..6ab4158d 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/next.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/next.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }), + }) ); server.app.rocksky.spotify.next({ auth: ctx.authVerifier, @@ -69,15 +69,27 @@ const withSpotifyRefreshToken = ({ ctx.db .select() .from(tables.spotifyTokens) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() - .then(([spotifyToken]) => - decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY), - ) - .then((refreshToken) => ({ - user, - ctx, + .then(([spotifyToken]) => [ + decrypt( + spotifyToken.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ), + decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), + spotifyToken.spotify_apps.spotifyAppId, + ]) + .then(([refreshToken, clientSecret, clientId]) => ({ refreshToken, + clientId, + clientSecret, })), catch: (error) => new Error(`Failed to retrieve Spotify Refresh token: ${error}`), @@ -86,10 +98,12 @@ const withSpotifyRefreshToken = ({ const withSpotifyToken = ({ refreshToken, - ctx, + clientId, + clientSecret, }: { refreshToken: string; - ctx: Context; + clientId: string; + clientSecret; }) => { return Effect.tryPromise({ try: () => @@ -101,8 +115,8 @@ const withSpotifyToken = ({ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }), }) .then((res) => res.json() as Promise<{ access_token: string }>) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/pause.ts b/apps/api/src/xrpc/app/rocksky/spotify/pause.ts index 807691ae..c63f3ced 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/pause.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/pause.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }), + }) ); server.app.rocksky.spotify.pause({ auth: ctx.authVerifier, @@ -69,15 +69,27 @@ const withSpotifyRefreshToken = ({ ctx.db .select() .from(tables.spotifyTokens) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() - .then(([spotifyToken]) => - decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY), - ) - .then((refreshToken) => ({ - user, - ctx, + .then(([spotifyToken]) => [ + decrypt( + spotifyToken.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ), + decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), + spotifyToken.spotify_apps.spotifyAppId, + ]) + .then(([refreshToken, clientSecret, clientId]) => ({ refreshToken, + clientId, + clientSecret, })), catch: (error) => new Error(`Failed to retrieve Spotify Refresh token: ${error}`), @@ -86,9 +98,12 @@ const withSpotifyRefreshToken = ({ const withSpotifyToken = ({ refreshToken, + clientId, + clientSecret, }: { refreshToken: string; - ctx: Context; + clientId: string; + clientSecret; }) => { return Effect.tryPromise({ try: () => @@ -100,8 +115,8 @@ const withSpotifyToken = ({ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }), }) .then((res) => res.json() as Promise<{ access_token: string }>) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/play.ts b/apps/api/src/xrpc/app/rocksky/spotify/play.ts index 82093768..23c3f15f 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/play.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/play.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }), + }) ); server.app.rocksky.spotify.play({ auth: ctx.authVerifier, @@ -69,15 +69,27 @@ const withSpotifyRefreshToken = ({ ctx.db .select() .from(tables.spotifyTokens) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() - .then(([spotifyToken]) => - decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY), - ) - .then((refreshToken) => ({ - user, - ctx, + .then(([spotifyToken]) => [ + decrypt( + spotifyToken.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ), + decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), + spotifyToken.spotify_apps.spotifyAppId, + ]) + .then(([refreshToken, clientSecret, clientId]) => ({ refreshToken, + clientId, + clientSecret, })), catch: (error) => new Error(`Failed to retrieve Spotify Refresh token: ${error}`), @@ -86,10 +98,12 @@ const withSpotifyRefreshToken = ({ const withSpotifyToken = ({ refreshToken, - ctx, + clientId, + clientSecret, }: { refreshToken: string; - ctx: Context; + clientId: string; + clientSecret; }) => { return Effect.tryPromise({ try: () => @@ -101,8 +115,8 @@ const withSpotifyToken = ({ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }), }) .then((res) => res.json() as Promise<{ access_token: string }>) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/previous.ts b/apps/api/src/xrpc/app/rocksky/spotify/previous.ts index 0619c09d..2acc9499 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/previous.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/previous.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }), + }) ); server.app.rocksky.spotify.previous({ auth: ctx.authVerifier, @@ -69,20 +69,42 @@ const withSpotifyRefreshToken = ({ ctx.db .select() .from(tables.spotifyTokens) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() - .then(([spotifyToken]) => - decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY), - ) - .then((refreshToken) => ({ + .then(([spotifyToken]) => [ + decrypt( + spotifyToken.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ), + decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), + spotifyToken.spotify_apps.spotifyAppId, + ]) + .then(([refreshToken, clientSecret, clientId]) => ({ refreshToken, + clientId, + clientSecret, })), catch: (error) => new Error(`Failed to retrieve Spotify Refresh token: ${error}`), }); }; -const withSpotifyToken = ({ refreshToken }: { refreshToken: string }) => { +const withSpotifyToken = ({ + refreshToken, + clientId, + clientSecret, +}: { + refreshToken: string; + clientId: string; + clientSecret; +}) => { return Effect.tryPromise({ try: () => fetch("https://accounts.spotify.com/api/token", { @@ -93,8 +115,8 @@ const withSpotifyToken = ({ refreshToken }: { refreshToken: string }) => { body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }), }) .then((res) => res.json() as Promise<{ access_token: string }>) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/seek.ts b/apps/api/src/xrpc/app/rocksky/spotify/seek.ts index 5c12f259..6e8c4311 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/seek.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/seek.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }), + }) ); server.app.rocksky.spotify.seek({ auth: ctx.authVerifier, @@ -72,13 +72,27 @@ const withSpotifyRefreshToken = ({ ctx.db .select() .from(tables.spotifyTokens) + .leftJoin( + tables.spotifyApps, + eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() - .then(([spotifyToken]) => - decrypt(spotifyToken.refreshToken, env.SPOTIFY_ENCRYPTION_KEY), - ) - .then((refreshToken) => ({ + .then(([spotifyToken]) => [ + decrypt( + spotifyToken.spotify_tokens.refreshToken, + env.SPOTIFY_ENCRYPTION_KEY + ), + decrypt( + spotifyToken.spotify_apps.spotifySecret, + env.SPOTIFY_ENCRYPTION_KEY + ), + spotifyToken.spotify_apps.spotifyAppId, + ]) + .then(([refreshToken, clientSecret, clientId]) => ({ refreshToken, + clientId, + clientSecret, params, })), catch: (error) => @@ -88,9 +102,13 @@ const withSpotifyRefreshToken = ({ const withSpotifyToken = ({ refreshToken, + clientSecret, + clientId, params, }: { refreshToken: string; + clientSecret: string; + clientId: string; params: QueryParams; }) => { return Effect.tryPromise({ @@ -103,8 +121,8 @@ const withSpotifyToken = ({ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, }), }) .then((res) => res.json() as Promise<{ access_token: string }>) @@ -132,7 +150,7 @@ const handleSeek = ({ headers: { Authorization: `Bearer ${accessToken}`, }, - }, + } ).then((res) => res.status), catch: (error) => new Error(`Failed to handle next action: ${error}`), }); diff --git a/crates/playlists/src/core.rs b/crates/playlists/src/core.rs index db6ce8ed..c08cad93 100644 --- a/crates/playlists/src/core.rs +++ b/crates/playlists/src/core.rs @@ -136,12 +136,14 @@ pub async fn find_spotify_users( pool: &Pool, offset: usize, limit: usize, -) -> Result, Error> { +) -> Result, Error> { let results: Vec = sqlx::query_as( r#" SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id + WHERE spotify_accounts.is_beta_user = true LIMIT $1 OFFSET $2 "#, ) @@ -157,11 +159,17 @@ pub async fn find_spotify_users( &result.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; + let spotify_secret = decrypt_aes_256_ctr( + &result.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; user_tokens.push(( result.email.clone(), token, result.did.clone(), result.user_id.clone(), + result.spotify_app_id.clone(), + spotify_secret.clone(), )); } diff --git a/crates/playlists/src/lib.rs b/crates/playlists/src/lib.rs index 4bdc65e2..5db1e64e 100644 --- a/crates/playlists/src/lib.rs +++ b/crates/playlists/src/lib.rs @@ -53,7 +53,9 @@ pub async fn start() -> Result<(), Error> { let token = user.1.clone(); let did = user.2.clone(); let user_id = user.3.clone(); - let playlists = get_user_playlists(token).await?; + let client_id = user.4.clone(); + let client_secret = user.5.clone(); + let playlists = get_user_playlists(token, client_id, client_secret).await?; save_playlists(&pool, conn.clone(), nc.clone(), playlists, &user_id, &did).await?; } diff --git a/crates/playlists/src/spotify.rs b/crates/playlists/src/spotify.rs index eb9928ed..8b0d8028 100644 --- a/crates/playlists/src/spotify.rs +++ b/crates/playlists/src/spotify.rs @@ -5,14 +5,11 @@ use reqwest::Client; use crate::types::{self, token::AccessToken}; -pub async fn refresh_token(token: &str) -> Result { - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); - } - - let client_id = env::var("SPOTIFY_CLIENT_ID")?; - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; - +pub async fn refresh_token( + token: &str, + client_id: &str, + client_secret: &str, +) -> Result { let client = Client::new(); let response = client @@ -29,8 +26,12 @@ pub async fn refresh_token(token: &str) -> Result { Ok(token) } -pub async fn get_user_playlists(token: String) -> Result, Error> { - let token = refresh_token(&token).await?; +pub async fn get_user_playlists( + token: String, + client_id: String, + client_secret: String, +) -> Result, Error> { + let token = refresh_token(&token, &client_id, &client_secret).await?; let client = Client::new(); let response = client .get("https://api.spotify.com/v1/me/playlists") diff --git a/crates/playlists/src/types/spotify_token.rs b/crates/playlists/src/types/spotify_token.rs index 5f137a09..11d15f8b 100644 --- a/crates/playlists/src/types/spotify_token.rs +++ b/crates/playlists/src/types/spotify_token.rs @@ -12,6 +12,8 @@ pub struct SpotifyToken { pub user_id: String, pub access_token: String, pub refresh_token: String, + pub spotify_app_id: String, + pub spotify_secret: String, } #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] @@ -27,4 +29,6 @@ pub struct SpotifyTokenWithEmail { pub refresh_token: String, pub email: String, pub did: String, + pub spotify_app_id: String, + pub spotify_secret: String, } diff --git a/crates/scrobbler/src/repo/spotify_account.rs b/crates/scrobbler/src/repo/spotify_account.rs index a81a09f7..1b4c737f 100644 --- a/crates/scrobbler/src/repo/spotify_account.rs +++ b/crates/scrobbler/src/repo/spotify_account.rs @@ -11,6 +11,7 @@ pub async fn get_spotify_account( r#" SELECT * FROM spotify_accounts LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_accounts.spotify_app_id = spotify_apps.spotify_app_id WHERE users.did = $1 "#, ) diff --git a/crates/scrobbler/src/repo/spotify_token.rs b/crates/scrobbler/src/repo/spotify_token.rs index f3c66190..ac3ed82a 100644 --- a/crates/scrobbler/src/repo/spotify_token.rs +++ b/crates/scrobbler/src/repo/spotify_token.rs @@ -12,6 +12,7 @@ pub async fn get_spotify_token( SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id WHERE users.did = $1 "#, ) @@ -35,6 +36,7 @@ pub async fn get_spotify_tokens( SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id LIMIT $1 "#, ) diff --git a/crates/scrobbler/src/scrobbler.rs b/crates/scrobbler/src/scrobbler.rs index 48f887df..38258887 100644 --- a/crates/scrobbler/src/scrobbler.rs +++ b/crates/scrobbler/src/scrobbler.rs @@ -188,13 +188,19 @@ pub async fn scrobble( let mut rng = rand::rng(); let random_index = rng.random_range(0..spofity_tokens.len()); let spotify_token = &spofity_tokens[random_index]; + let client_id = spotify_token.spotify_app_id.clone(); + + let client_secret = decrypt_aes_256_ctr( + &spotify_token.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; let spotify_token = decrypt_aes_256_ctr( &spotify_token.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; - let spotify_token = refresh_token(&spotify_token).await?; + let spotify_token = refresh_token(&spotify_token, &client_id, &client_secret).await?; let spotify_client = SpotifyClient::new(&spotify_token.access_token); let result = spotify_client @@ -362,13 +368,19 @@ pub async fn scrobble_v1( let mut rng = rand::rng(); let random_index = rng.random_range(0..spofity_tokens.len()); let spotify_token = &spofity_tokens[random_index]; + let client_id = spotify_token.spotify_app_id.clone(); + + let client_secret = decrypt_aes_256_ctr( + &spotify_token.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; let spotify_token = decrypt_aes_256_ctr( &spotify_token.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; - let spotify_token = refresh_token(&spotify_token).await?; + let spotify_token = refresh_token(&spotify_token, &client_id, &client_secret).await?; let spotify_client = SpotifyClient::new(&spotify_token.access_token); let result = spotify_client @@ -615,12 +627,18 @@ pub async fn scrobble_listenbrainz( let random_index = rng.random_range(0..spofity_tokens.len()); let spotify_token = &spofity_tokens[random_index]; + let client_id = spotify_token.spotify_app_id.clone(); + let client_secret = decrypt_aes_256_ctr( + &spotify_token.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; + let spotify_token = decrypt_aes_256_ctr( &spotify_token.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; - let spotify_token = refresh_token(&spotify_token).await?; + let spotify_token = refresh_token(&spotify_token, &client_id, &client_secret).await?; let spotify_client = SpotifyClient::new(&spotify_token.access_token); let result = spotify_client diff --git a/crates/scrobbler/src/spotify/mod.rs b/crates/scrobbler/src/spotify/mod.rs index 5fb95c13..c763b521 100644 --- a/crates/scrobbler/src/spotify/mod.rs +++ b/crates/scrobbler/src/spotify/mod.rs @@ -1,5 +1,3 @@ -use std::env; - use anyhow::Error; use reqwest::Client; use types::AccessToken; @@ -7,14 +5,11 @@ use types::AccessToken; pub mod client; pub mod types; -pub async fn refresh_token(token: &str) -> Result { - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); - } - - let client_id = env::var("SPOTIFY_CLIENT_ID")?; - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; - +pub async fn refresh_token( + token: &str, + client_id: &str, + client_secret: &str, +) -> Result { let client = Client::new(); let response = client diff --git a/crates/scrobbler/src/xata/mod.rs b/crates/scrobbler/src/xata/mod.rs index 1d023de1..3c1c412f 100644 --- a/crates/scrobbler/src/xata/mod.rs +++ b/crates/scrobbler/src/xata/mod.rs @@ -2,6 +2,7 @@ pub mod album; pub mod api_key; pub mod artist; pub mod spotify_account; +pub mod spotify_apps; pub mod spotify_token; pub mod track; pub mod user; diff --git a/crates/scrobbler/src/xata/spotify_account.rs b/crates/scrobbler/src/xata/spotify_account.rs index 74a42847..1edc29e7 100644 --- a/crates/scrobbler/src/xata/spotify_account.rs +++ b/crates/scrobbler/src/xata/spotify_account.rs @@ -12,4 +12,6 @@ pub struct SpotifyAccount { pub email: String, pub user_id: String, pub is_beta_user: bool, + pub spotify_app_id: Option, + pub spotify_secret: Option, } diff --git a/crates/scrobbler/src/xata/spotify_apps.rs b/crates/scrobbler/src/xata/spotify_apps.rs new file mode 100644 index 00000000..0dec478e --- /dev/null +++ b/crates/scrobbler/src/xata/spotify_apps.rs @@ -0,0 +1,14 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] +pub struct SpotifyApp { + pub xata_id: String, + pub xata_version: i32, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_updatedat: DateTime, + pub spotify_app_id: String, + pub spotify_secret: String, +} diff --git a/crates/scrobbler/src/xata/spotify_token.rs b/crates/scrobbler/src/xata/spotify_token.rs index 5f137a09..11d15f8b 100644 --- a/crates/scrobbler/src/xata/spotify_token.rs +++ b/crates/scrobbler/src/xata/spotify_token.rs @@ -12,6 +12,8 @@ pub struct SpotifyToken { pub user_id: String, pub access_token: String, pub refresh_token: String, + pub spotify_app_id: String, + pub spotify_secret: String, } #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] @@ -27,4 +29,6 @@ pub struct SpotifyTokenWithEmail { pub refresh_token: String, pub email: String, pub did: String, + pub spotify_app_id: String, + pub spotify_secret: String, } diff --git a/crates/spotify/src/lib.rs b/crates/spotify/src/lib.rs index 590cdc8c..0a69815f 100644 --- a/crates/spotify/src/lib.rs +++ b/crates/spotify/src/lib.rs @@ -58,6 +58,8 @@ pub async fn run() -> Result<(), Error> { let email = user.0.clone(); let token = user.1.clone(); let did = user.2.clone(); + let client_id = user.3.clone(); + let client_secret = user.4.clone(); let stop_flag = Arc::new(AtomicBool::new(false)); let cache = cache.clone(); let nc = nc.clone(); @@ -71,8 +73,16 @@ pub async fn run() -> Result<(), Error> { thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); match rt.block_on(async { - watch_currently_playing(email.clone(), token, did, stop_flag, cache.clone()) - .await?; + watch_currently_playing( + email.clone(), + token, + did, + stop_flag, + cache.clone(), + client_id, + client_secret, + ) + .await?; Ok::<(), Error>(()) }) { Ok(_) => {} @@ -140,6 +150,8 @@ pub async fn run() -> Result<(), Error> { let email = user.0.clone(); let token = user.1.clone(); let did = user.2.clone(); + let client_id = user.3.clone(); + let client_secret = user.4.clone(); let cache = cache.clone(); thread::spawn(move || { @@ -151,6 +163,8 @@ pub async fn run() -> Result<(), Error> { did, new_stop_flag, cache.clone(), + client_id, + client_secret, ) .await?; Ok::<(), Error>(()) @@ -178,6 +192,8 @@ pub async fn run() -> Result<(), Error> { let email = user.0.clone(); let token = user.1.clone(); let did = user.2.clone(); + let client_id = user.3.clone(); + let client_secret = user.4.clone(); let stop_flag = Arc::new(AtomicBool::new(false)); let cache = cache.clone(); let nc = nc.clone(); @@ -193,6 +209,8 @@ pub async fn run() -> Result<(), Error> { did, stop_flag, cache.clone(), + client_id, + client_secret, ) .await?; Ok::<(), Error>(()) @@ -227,14 +245,11 @@ pub async fn run() -> Result<(), Error> { Ok(()) } -pub async fn refresh_token(token: &str) -> Result { - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); - } - - let client_id = env::var("SPOTIFY_CLIENT_ID")?; - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; - +pub async fn refresh_token( + token: &str, + client_id: &str, + client_secret: &str, +) -> Result { let client = Client::new(); let response = client @@ -255,6 +270,8 @@ pub async fn get_currently_playing( cache: Cache, user_id: &str, token: &str, + client_id: &str, + client_secret: &str, ) -> Result, Error> { if let Ok(Some(data)) = cache.get(user_id) { println!( @@ -329,7 +346,7 @@ pub async fn get_currently_playing( return Ok(Some((data, changed))); } - let token = refresh_token(token).await?; + let token = refresh_token(token, client_id, client_secret).await?; let client = Client::new(); let response = client .get(format!("{}/me/player/currently-playing", BASE_URL)) @@ -529,12 +546,14 @@ pub async fn get_artist( cache: Cache, artist_id: &str, token: &str, + client_id: &str, + client_secret: &str, ) -> Result, Error> { if let Ok(Some(data)) = cache.get(artist_id) { return Ok(Some(serde_json::from_str(&data)?)); } - let token = refresh_token(token).await?; + let token = refresh_token(token, client_id, client_secret).await?; let client = Client::new(); let response = client .get(&format!("{}/artists/{}", BASE_URL, artist_id)) @@ -569,12 +588,18 @@ pub async fn get_artist( Ok(Some(serde_json::from_str(&data)?)) } -pub async fn get_album(cache: Cache, album_id: &str, token: &str) -> Result, Error> { +pub async fn get_album( + cache: Cache, + album_id: &str, + token: &str, + client_id: &str, + client_secret: &str, +) -> Result, Error> { if let Ok(Some(data)) = cache.get(album_id) { return Ok(Some(serde_json::from_str(&data)?)); } - let token = refresh_token(token).await?; + let token = refresh_token(token, client_id, client_secret).await?; let client = Client::new(); let response = client .get(&format!("{}/albums/{}", BASE_URL, album_id)) @@ -613,12 +638,14 @@ pub async fn get_album_tracks( cache: Cache, album_id: &str, token: &str, + client_id: &str, + client_secret: &str, ) -> Result { if let Ok(Some(data)) = cache.get(&format!("{}:tracks", album_id)) { return Ok(serde_json::from_str(&data)?); } - let token = refresh_token(token).await?; + let token = refresh_token(token, client_id, client_secret).await?; let client = Client::new(); let mut all_tracks = Vec::new(); let mut offset = 0; @@ -678,12 +705,13 @@ pub async fn find_spotify_users( pool: &Pool, offset: usize, limit: usize, -) -> Result, Error> { +) -> Result, Error> { let results: Vec = sqlx::query_as( r#" SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id LIMIT $1 OFFSET $2 "#, ) @@ -699,11 +727,17 @@ pub async fn find_spotify_users( &result.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; + let spotify_secret = decrypt_aes_256_ctr( + &result.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; user_tokens.push(( result.email.clone(), token, result.did.clone(), result.user_id.clone(), + result.spotify_app_id.clone(), + spotify_secret, )); } @@ -713,12 +747,13 @@ pub async fn find_spotify_users( pub async fn find_spotify_user( pool: &Pool, email: &str, -) -> Result, Error> { +) -> Result, Error> { let result: Vec = sqlx::query_as( r#" SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id WHERE spotify_accounts.email = $1 "#, ) @@ -732,7 +767,17 @@ pub async fn find_spotify_user( &result.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; - Ok(Some((result.email.clone(), token, result.did.clone()))) + let spotify_secret = decrypt_aes_256_ctr( + &result.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; + Ok(Some(( + result.email.clone(), + token, + result.did.clone(), + result.spotify_app_id.clone(), + spotify_secret, + ))) } None => Ok(None), } @@ -744,6 +789,8 @@ pub async fn watch_currently_playing( did: String, stop_flag: Arc, cache: Cache, + client_id: String, + client_secret: String, ) -> Result<(), Error> { println!( "{} {}", @@ -832,8 +879,17 @@ pub async fn watch_currently_playing( let token = token.clone(); let did = did.clone(); let cache = cache.clone(); - - let currently_playing = get_currently_playing(cache.clone(), &spotify_email, &token).await; + let client_id = client_id.clone(); + let client_secret = client_secret.clone(); + + let currently_playing = get_currently_playing( + cache.clone(), + &spotify_email, + &token, + &client_id, + &client_secret, + ) + .await; let currently_playing = match currently_playing { Ok(currently_playing) => currently_playing, Err(e) => { @@ -867,14 +923,44 @@ pub async fn watch_currently_playing( ); if changed { - scrobble(cache.clone(), &spotify_email, &did, &token).await?; + scrobble( + cache.clone(), + &spotify_email, + &did, + &token, + &client_id, + &client_secret, + ) + .await?; thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); match rt.block_on(async { - get_album_tracks(cache.clone(), &data_item.album.id, &token).await?; - get_album(cache.clone(), &data_item.album.id, &token).await?; - update_library(cache.clone(), &spotify_email, &did, &token).await?; + get_album_tracks( + cache.clone(), + &data_item.album.id, + &token, + &client_id, + &client_secret, + ) + .await?; + get_album( + cache.clone(), + &data_item.album.id, + &token, + &client_id, + &client_secret, + ) + .await?; + update_library( + cache.clone(), + &spotify_email, + &did, + &token, + &client_id, + &client_secret, + ) + .await?; Ok::<(), Error>(()) }) { Ok(_) => {} diff --git a/crates/spotify/src/main.rs b/crates/spotify/src/main.rs deleted file mode 100644 index 4cc7ed85..00000000 --- a/crates/spotify/src/main.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::{ - collections::HashMap, - env, - sync::{atomic::AtomicBool, Arc, Mutex}, - thread, -}; - -use anyhow::Error; -use async_nats::connect; -use dotenv::dotenv; -use owo_colors::OwoColorize; -use rocksky_spotify::cache::Cache; -use rocksky_spotify::{find_spotify_user, find_spotify_users, watch_currently_playing}; -use sqlx::postgres::PgPoolOptions; -use tokio_stream::StreamExt; - -#[tokio::main] -async fn main() -> Result<(), Box> { - dotenv().ok(); - let cache = Cache::new()?; - let pool = PgPoolOptions::new() - .max_connections(5) - .connect(&env::var("XATA_POSTGRES_URL")?) - .await?; - - let addr = env::var("NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".to_string()); - let nc = connect(&addr).await?; - println!("Connected to NATS server at {}", addr.bright_green()); - - let mut sub = nc.subscribe("rocksky.spotify.user".to_string()).await?; - println!("Subscribed to {}", "rocksky.spotify.user".bright_green()); - - let users = find_spotify_users(&pool, 0, 100).await?; - println!("Found {} users", users.len().bright_green()); - - // Shared HashMap to manage threads and their stop flags - let thread_map: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); - - // Start threads for all users - for user in users { - let email = user.0.clone(); - let token = user.1.clone(); - let did = user.2.clone(); - let stop_flag = Arc::new(AtomicBool::new(false)); - let cache = cache.clone(); - let nc = nc.clone(); - let thread_map = Arc::clone(&thread_map); - - thread_map - .lock() - .unwrap() - .insert(email.clone(), Arc::clone(&stop_flag)); - - thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - match rt.block_on(async { - watch_currently_playing(email.clone(), token, did, stop_flag, cache.clone()) - .await?; - Ok::<(), Error>(()) - }) { - Ok(_) => {} - Err(e) => { - println!( - "{} Error starting thread for user: {} - {}", - format!("[{}]", email).bright_green(), - email.bright_green(), - e.to_string().bright_red() - ); - - // If there's an error, publish a message to restart the thread - match rt.block_on(nc.publish("rocksky.spotify.user", email.clone().into())) { - Ok(_) => { - println!( - "{} Published message to restart thread for user: {}", - format!("[{}]", email).bright_green(), - email.bright_green() - ); - } - Err(e) => { - println!( - "{} Error publishing message to restart thread: {}", - format!("[{}]", email).bright_green(), - e.to_string().bright_red() - ); - } - } - } - } - }); - } - - // Handle subscription messages - while let Some(message) = sub.next().await { - let user_id = String::from_utf8(message.payload.to_vec()).unwrap(); - println!( - "Received message to restart thread for user: {}", - user_id.bright_green() - ); - - let mut thread_map = thread_map.lock().unwrap(); - - // Check if the user exists in the thread map - if let Some(stop_flag) = thread_map.get(&user_id) { - // Stop the existing thread - stop_flag.store(true, std::sync::atomic::Ordering::Relaxed); - - // Create a new stop flag and restart the thread - let new_stop_flag = Arc::new(AtomicBool::new(false)); - thread_map.insert(user_id.clone(), Arc::clone(&new_stop_flag)); - - let user = find_spotify_user(&pool, &user_id).await?; - - if user.is_none() { - println!( - "Spotify user not found: {}, skipping", - user_id.bright_green() - ); - continue; - } - - let user = user.unwrap(); - - let email = user.0.clone(); - let token = user.1.clone(); - let did = user.2.clone(); - let cache = cache.clone(); - - thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - match rt.block_on(async { - watch_currently_playing( - email.clone(), - token, - did, - new_stop_flag, - cache.clone(), - ) - .await?; - Ok::<(), Error>(()) - }) { - Ok(_) => {} - Err(e) => { - println!( - "{} Error restarting thread for user: {} - {}", - format!("[{}]", email).bright_green(), - email.bright_green(), - e.to_string().bright_red() - ); - } - } - }); - - println!("Restarted thread for user: {}", user_id.bright_green()); - } else { - println!( - "No thread found for user: {}, starting new thread", - user_id.bright_green() - ); - let user = find_spotify_user(&pool, &user_id).await?; - if let Some(user) = user { - let email = user.0.clone(); - let token = user.1.clone(); - let did = user.2.clone(); - let stop_flag = Arc::new(AtomicBool::new(false)); - let cache = cache.clone(); - let nc = nc.clone(); - - thread_map.insert(email.clone(), Arc::clone(&stop_flag)); - - thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - match rt.block_on(async { - watch_currently_playing( - email.clone(), - token, - did, - stop_flag, - cache.clone(), - ) - .await?; - Ok::<(), Error>(()) - }) { - Ok(_) => {} - Err(e) => { - println!( - "{} Error starting thread for user: {} - {}", - format!("[{}]", email).bright_green(), - email.bright_green(), - e.to_string().bright_red() - ); - match rt - .block_on(nc.publish("rocksky.spotify.user", email.clone().into())) - { - Ok(_) => {} - Err(e) => { - println!( - "{} Error publishing message to restart thread: {}", - format!("[{}]", email).bright_green(), - e.to_string().bright_red() - ); - } - } - } - } - }); - } - } - } - - Ok(()) -} diff --git a/crates/spotify/src/rocksky.rs b/crates/spotify/src/rocksky.rs index dd9a4121..b764729b 100644 --- a/crates/spotify/src/rocksky.rs +++ b/crates/spotify/src/rocksky.rs @@ -18,6 +18,8 @@ pub async fn scrobble( spotify_email: &str, did: &str, refresh_token: &str, + client_id: &str, + client_secret: &str, ) -> Result<(), Error> { let cached = cache.get(spotify_email)?; if cached.is_none() { @@ -40,6 +42,8 @@ pub async fn scrobble( cache.clone(), &track_item.artists.first().unwrap().id, &refresh_token, + &client_id, + &client_secret, ) .await?; @@ -98,6 +102,8 @@ pub async fn update_library( spotify_email: &str, did: &str, refresh_token: &str, + client_id: &str, + client_secret: &str, ) -> Result<(), Error> { let cached = cache.get(spotify_email)?; if cached.is_none() { @@ -105,7 +111,14 @@ pub async fn update_library( "No currently playing song is cached for {}, refreshing", spotify_email ); - get_currently_playing(cache.clone(), &spotify_email, &refresh_token).await?; + get_currently_playing( + cache.clone(), + &spotify_email, + &refresh_token, + client_id, + client_secret, + ) + .await?; } let cached = cache.get(spotify_email)?; diff --git a/crates/spotify/src/types/spotify_account.rs b/crates/spotify/src/types/spotify_account.rs index 74a42847..1edc29e7 100644 --- a/crates/spotify/src/types/spotify_account.rs +++ b/crates/spotify/src/types/spotify_account.rs @@ -12,4 +12,6 @@ pub struct SpotifyAccount { pub email: String, pub user_id: String, pub is_beta_user: bool, + pub spotify_app_id: Option, + pub spotify_secret: Option, } diff --git a/crates/spotify/src/types/spotify_token.rs b/crates/spotify/src/types/spotify_token.rs index 5f137a09..11d15f8b 100644 --- a/crates/spotify/src/types/spotify_token.rs +++ b/crates/spotify/src/types/spotify_token.rs @@ -12,6 +12,8 @@ pub struct SpotifyToken { pub user_id: String, pub access_token: String, pub refresh_token: String, + pub spotify_app_id: String, + pub spotify_secret: String, } #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] @@ -27,4 +29,6 @@ pub struct SpotifyTokenWithEmail { pub refresh_token: String, pub email: String, pub did: String, + pub spotify_app_id: String, + pub spotify_secret: String, } diff --git a/crates/webscrobbler/src/repo/spotify_account.rs b/crates/webscrobbler/src/repo/spotify_account.rs index c5eaf9b1..cf20e590 100644 --- a/crates/webscrobbler/src/repo/spotify_account.rs +++ b/crates/webscrobbler/src/repo/spotify_account.rs @@ -9,6 +9,7 @@ pub async fn get_spotify_account( let results: Vec = sqlx::query_as( r#" SELECT * FROM spotify_accounts + LEFT JOIN spotify_apps ON spotify_accounts.spotify_app_id = spotify_apps.spotify_app_id WHERE user_id = $1 "#, ) diff --git a/crates/webscrobbler/src/repo/spotify_token.rs b/crates/webscrobbler/src/repo/spotify_token.rs index f3c66190..ac3ed82a 100644 --- a/crates/webscrobbler/src/repo/spotify_token.rs +++ b/crates/webscrobbler/src/repo/spotify_token.rs @@ -12,6 +12,7 @@ pub async fn get_spotify_token( SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id WHERE users.did = $1 "#, ) @@ -35,6 +36,7 @@ pub async fn get_spotify_tokens( SELECT * FROM spotify_tokens LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id LEFT JOIN users ON spotify_accounts.user_id = users.xata_id + LEFT JOIN spotify_apps ON spotify_tokens.spotify_app_id = spotify_apps.spotify_app_id LIMIT $1 "#, ) diff --git a/crates/webscrobbler/src/scrobbler.rs b/crates/webscrobbler/src/scrobbler.rs index b8beb9db..d6c98f0b 100644 --- a/crates/webscrobbler/src/scrobbler.rs +++ b/crates/webscrobbler/src/scrobbler.rs @@ -82,13 +82,19 @@ pub async fn scrobble( let mut rng = rand::rng(); let random_index = rng.random_range(0..spofity_tokens.len()); let spotify_token = &spofity_tokens[random_index]; + let client_id = spotify_token.spotify_app_id.clone(); + + let client_secret = decrypt_aes_256_ctr( + &spotify_token.spotify_secret, + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, + )?; let spotify_token = decrypt_aes_256_ctr( &spotify_token.refresh_token, &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, )?; - let spotify_token = refresh_token(&spotify_token).await?; + let spotify_token = refresh_token(&spotify_token, &client_id, &client_secret).await?; let spotify_client = SpotifyClient::new(&spotify_token.access_token); let query = match scrobble.data.song.parsed.artist.contains(" x ") { diff --git a/crates/webscrobbler/src/spotify/mod.rs b/crates/webscrobbler/src/spotify/mod.rs index 5fb95c13..c763b521 100644 --- a/crates/webscrobbler/src/spotify/mod.rs +++ b/crates/webscrobbler/src/spotify/mod.rs @@ -1,5 +1,3 @@ -use std::env; - use anyhow::Error; use reqwest::Client; use types::AccessToken; @@ -7,14 +5,11 @@ use types::AccessToken; pub mod client; pub mod types; -pub async fn refresh_token(token: &str) -> Result { - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); - } - - let client_id = env::var("SPOTIFY_CLIENT_ID")?; - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; - +pub async fn refresh_token( + token: &str, + client_id: &str, + client_secret: &str, +) -> Result { let client = Client::new(); let response = client diff --git a/crates/webscrobbler/src/xata/mod.rs b/crates/webscrobbler/src/xata/mod.rs index 42b71632..457b6c68 100644 --- a/crates/webscrobbler/src/xata/mod.rs +++ b/crates/webscrobbler/src/xata/mod.rs @@ -1,6 +1,7 @@ pub mod album; pub mod artist; pub mod spotify_account; +pub mod spotify_apps; pub mod spotify_token; pub mod track; pub mod user; diff --git a/crates/webscrobbler/src/xata/spotify_account.rs b/crates/webscrobbler/src/xata/spotify_account.rs index 74a42847..1edc29e7 100644 --- a/crates/webscrobbler/src/xata/spotify_account.rs +++ b/crates/webscrobbler/src/xata/spotify_account.rs @@ -12,4 +12,6 @@ pub struct SpotifyAccount { pub email: String, pub user_id: String, pub is_beta_user: bool, + pub spotify_app_id: Option, + pub spotify_secret: Option, } diff --git a/crates/webscrobbler/src/xata/spotify_apps.rs b/crates/webscrobbler/src/xata/spotify_apps.rs new file mode 100644 index 00000000..0dec478e --- /dev/null +++ b/crates/webscrobbler/src/xata/spotify_apps.rs @@ -0,0 +1,14 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] +pub struct SpotifyApp { + pub xata_id: String, + pub xata_version: i32, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_updatedat: DateTime, + pub spotify_app_id: String, + pub spotify_secret: String, +} diff --git a/crates/webscrobbler/src/xata/spotify_token.rs b/crates/webscrobbler/src/xata/spotify_token.rs index 5f137a09..11d15f8b 100644 --- a/crates/webscrobbler/src/xata/spotify_token.rs +++ b/crates/webscrobbler/src/xata/spotify_token.rs @@ -12,6 +12,8 @@ pub struct SpotifyToken { pub user_id: String, pub access_token: String, pub refresh_token: String, + pub spotify_app_id: String, + pub spotify_secret: String, } #[derive(Debug, Deserialize, sqlx::FromRow, Default, Clone)] @@ -27,4 +29,6 @@ pub struct SpotifyTokenWithEmail { pub refresh_token: String, pub email: String, pub did: String, + pub spotify_app_id: String, + pub spotify_secret: String, }