A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add genres support to artist and track records

- Updated the createScrobble function to log the creation of scrobbles.
- Enhanced the artist and track structures to include genres as an optional field.
- Modified the save_artist function to accept genres and store them in the database.
- Adjusted various functions to handle genres when creating or updating artist and track records.
- Updated Spotify integration to fetch and include genres for artists.

+1911 -189
+41
apps/api/.xata/migrations/.ledger
··· 276 276 mig_d0md0k2r9e9qaqkp2jvg 277 277 mig_d0md0vido80pli9qp7t0 278 278 mig_d0md192r9e9qaqkp2k0g 279 + mig_d1m1n9qoqhr2s1o6vnj0 280 + mig_d1m1nhrj0aqdfvuvbfa0 281 + mig_d1m1o0qoqhr2s1o6vnk0 282 + mig_d1m1ob2oqhr2s1o6vnl0 283 + mig_d1m1of2oqhr2s1o6vnm0 284 + mig_d1m1pihr27un0qigpah0 285 + mig_d1m1q2qoqhr2s1o6vnn0 286 + mig_d1m1qi2oqhr2s1o6vno0 287 + mig_d1m1rghr27un0qigpaj0 288 + mig_d1m1ssrj0aqdfvuvbfi0 289 + mig_d1m1t8aoqhr2s1o6vnp0 290 + mig_d1m1tk9r27un0qigpak0 291 + mig_d1m1u4bj0aqdfvuvbfl0 292 + mig_d1m1ucrj0aqdfvuvbfm0 293 + mig_d1m1ut1r27un0qigpam0 294 + mig_d1m1vmioqhr2s1o6vnrg 295 + mig_d1m20hpr27un0qigpan0 296 + mig_d1m210ioqhr2s1o6vnsg 297 + mig_d1m21aioqhr2s1o6vntg 298 + mig_d1m21q9r27un0qigpao0 299 + mig_d1m223rj0aqdfvuvbfo0 300 + mig_d1m22fbj0aqdfvuvbfp0 301 + mig_d1m22r1r27un0qigpap0 302 + mig_d1m23a9r27un0qigpaq0 303 + mig_d1m2c02oqhr2s1o6vnv0 304 + mig_d1m2cthr27un0qigpavg 305 + mig_d1m2dupr27un0qigpb0g 306 + mig_d1m2edioqhr2s1o6vo00 307 + sql_70e285f209c400 308 + sql_027411d41b3b86 309 + sql_2d6fff5c5e6c78 310 + sql_76f1105aa6ca0f 311 + mig_d1mb1urj0aqdfvuvbhk0 312 + mig_d1o1f1qoqhr2s1o705dg 313 + sql_18cc4eb60ac890 314 + sql_18f83793ce7a31 315 + sql_45a899d3597b60 316 + sql_d58ea73a9fcdc5 317 + mig_d20ot3jj0aqdfvuvdh6g 318 + mig_d2mlh1gbs5avv4bj7f0g 319 + mig_d3ea7abdeq0sm5b3ib8g
+57
apps/api/.xata/migrations/mig_d1m1n9qoqhr2s1o6vnj0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1n9qoqhr2s1o6vnj0", 5 + "operations": [ 6 + { 7 + "create_table": { 8 + "name": "google_drive_directories", 9 + "columns": [ 10 + { 11 + "name": "xata_createdat", 12 + "type": "timestamptz", 13 + "default": "now()" 14 + }, 15 + { 16 + "name": "xata_updatedat", 17 + "type": "timestamptz", 18 + "default": "now()" 19 + }, 20 + { 21 + "name": "xata_id", 22 + "type": "text", 23 + "check": { 24 + "name": "google_drive_directories_xata_id_length_xata_id", 25 + "constraint": "length(\"xata_id\") < 256" 26 + }, 27 + "unique": true, 28 + "default": "'rec_' || xata_private.xid()" 29 + }, 30 + { 31 + "name": "xata_version", 32 + "type": "integer", 33 + "default": "0" 34 + } 35 + ] 36 + } 37 + }, 38 + { 39 + "sql": { 40 + "up": "ALTER TABLE \"google_drive_directories\" REPLICA IDENTITY FULL", 41 + "onComplete": true 42 + } 43 + }, 44 + { 45 + "sql": { 46 + "up": "CREATE TRIGGER xata_maintain_metadata_trigger_pgroll\n BEFORE INSERT OR UPDATE\n ON \"google_drive_directories\"\n FOR EACH ROW\n EXECUTE FUNCTION xata_private.maintain_metadata_trigger_pgroll()", 47 + "onComplete": true 48 + } 49 + } 50 + ] 51 + }, 52 + "migrationType": "pgroll", 53 + "name": "mig_d1m1n9qoqhr2s1o6vnj0", 54 + "parent": "mig_d0md192r9e9qaqkp2k0g", 55 + "schema": "public", 56 + "startedAt": "2025-07-07T19:10:31.893881Z" 57 + }
+57
apps/api/.xata/migrations/mig_d1m1nhrj0aqdfvuvbfa0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1nhrj0aqdfvuvbfa0", 5 + "operations": [ 6 + { 7 + "create_table": { 8 + "name": "dropbox_directories", 9 + "columns": [ 10 + { 11 + "name": "xata_id", 12 + "type": "text", 13 + "check": { 14 + "name": "dropbox_directories_xata_id_length_xata_id", 15 + "constraint": "length(\"xata_id\") < 256" 16 + }, 17 + "unique": true, 18 + "default": "'rec_' || xata_private.xid()" 19 + }, 20 + { 21 + "name": "xata_version", 22 + "type": "integer", 23 + "default": "0" 24 + }, 25 + { 26 + "name": "xata_createdat", 27 + "type": "timestamptz", 28 + "default": "now()" 29 + }, 30 + { 31 + "name": "xata_updatedat", 32 + "type": "timestamptz", 33 + "default": "now()" 34 + } 35 + ] 36 + } 37 + }, 38 + { 39 + "sql": { 40 + "up": "ALTER TABLE \"dropbox_directories\" REPLICA IDENTITY FULL", 41 + "onComplete": true 42 + } 43 + }, 44 + { 45 + "sql": { 46 + "up": "CREATE TRIGGER xata_maintain_metadata_trigger_pgroll\n BEFORE INSERT OR UPDATE\n ON \"dropbox_directories\"\n FOR EACH ROW\n EXECUTE FUNCTION xata_private.maintain_metadata_trigger_pgroll()", 47 + "onComplete": true 48 + } 49 + } 50 + ] 51 + }, 52 + "migrationType": "pgroll", 53 + "name": "mig_d1m1nhrj0aqdfvuvbfa0", 54 + "parent": "mig_d1m1n9qoqhr2s1o6vnj0", 55 + "schema": "public", 56 + "startedAt": "2025-07-07T19:11:05.36521Z" 57 + }
+57
apps/api/.xata/migrations/mig_d1m1o0qoqhr2s1o6vnk0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1o0qoqhr2s1o6vnk0", 5 + "operations": [ 6 + { 7 + "create_table": { 8 + "name": "s3_directories", 9 + "columns": [ 10 + { 11 + "name": "xata_version", 12 + "type": "integer", 13 + "default": "0" 14 + }, 15 + { 16 + "name": "xata_createdat", 17 + "type": "timestamptz", 18 + "default": "now()" 19 + }, 20 + { 21 + "name": "xata_updatedat", 22 + "type": "timestamptz", 23 + "default": "now()" 24 + }, 25 + { 26 + "name": "xata_id", 27 + "type": "text", 28 + "check": { 29 + "name": "s3_directories_xata_id_length_xata_id", 30 + "constraint": "length(\"xata_id\") < 256" 31 + }, 32 + "unique": true, 33 + "default": "'rec_' || xata_private.xid()" 34 + } 35 + ] 36 + } 37 + }, 38 + { 39 + "sql": { 40 + "up": "ALTER TABLE \"s3_directories\" REPLICA IDENTITY FULL", 41 + "onComplete": true 42 + } 43 + }, 44 + { 45 + "sql": { 46 + "up": "CREATE TRIGGER xata_maintain_metadata_trigger_pgroll\n BEFORE INSERT OR UPDATE\n ON \"s3_directories\"\n FOR EACH ROW\n EXECUTE FUNCTION xata_private.maintain_metadata_trigger_pgroll()", 47 + "onComplete": true 48 + } 49 + } 50 + ] 51 + }, 52 + "migrationType": "pgroll", 53 + "name": "mig_d1m1o0qoqhr2s1o6vnk0", 54 + "parent": "mig_d1m1nhrj0aqdfvuvbfa0", 55 + "schema": "public", 56 + "startedAt": "2025-07-07T19:12:03.832402Z" 57 + }
+57
apps/api/.xata/migrations/mig_d1m1ob2oqhr2s1o6vnl0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1ob2oqhr2s1o6vnl0", 5 + "operations": [ 6 + { 7 + "create_table": { 8 + "name": "ftp_directories", 9 + "columns": [ 10 + { 11 + "name": "xata_id", 12 + "type": "text", 13 + "check": { 14 + "name": "ftp_directories_xata_id_length_xata_id", 15 + "constraint": "length(\"xata_id\") < 256" 16 + }, 17 + "unique": true, 18 + "default": "'rec_' || xata_private.xid()" 19 + }, 20 + { 21 + "name": "xata_version", 22 + "type": "integer", 23 + "default": "0" 24 + }, 25 + { 26 + "name": "xata_createdat", 27 + "type": "timestamptz", 28 + "default": "now()" 29 + }, 30 + { 31 + "name": "xata_updatedat", 32 + "type": "timestamptz", 33 + "default": "now()" 34 + } 35 + ] 36 + } 37 + }, 38 + { 39 + "sql": { 40 + "up": "ALTER TABLE \"ftp_directories\" REPLICA IDENTITY FULL", 41 + "onComplete": true 42 + } 43 + }, 44 + { 45 + "sql": { 46 + "up": "CREATE TRIGGER xata_maintain_metadata_trigger_pgroll\n BEFORE INSERT OR UPDATE\n ON \"ftp_directories\"\n FOR EACH ROW\n EXECUTE FUNCTION xata_private.maintain_metadata_trigger_pgroll()", 47 + "onComplete": true 48 + } 49 + } 50 + ] 51 + }, 52 + "migrationType": "pgroll", 53 + "name": "mig_d1m1ob2oqhr2s1o6vnl0", 54 + "parent": "mig_d1m1o0qoqhr2s1o6vnk0", 55 + "schema": "public", 56 + "startedAt": "2025-07-07T19:12:45.561136Z" 57 + }
+19
apps/api/.xata/migrations/mig_d1m1of2oqhr2s1o6vnm0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1of2oqhr2s1o6vnm0", 5 + "operations": [ 6 + { 7 + "rename_table": { 8 + "to": "sftp_directories", 9 + "from": "ftp_directories" 10 + } 11 + } 12 + ] 13 + }, 14 + "migrationType": "pgroll", 15 + "name": "mig_d1m1of2oqhr2s1o6vnm0", 16 + "parent": "mig_d1m1ob2oqhr2s1o6vnl0", 17 + "schema": "public", 18 + "startedAt": "2025-07-07T19:13:01.468679Z" 19 + }
+25
apps/api/.xata/migrations/mig_d1m1pihr27un0qigpah0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1pihr27un0qigpah0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "sftp_directories", 10 + "column": { 11 + "name": "path", 12 + "type": "text", 13 + "unique": true, 14 + "comment": "" 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_d1m1pihr27un0qigpah0", 22 + "parent": "mig_d1m1of2oqhr2s1o6vnm0", 23 + "schema": "public", 24 + "startedAt": "2025-07-07T19:15:23.144073Z" 25 + }
+24
apps/api/.xata/migrations/mig_d1m1q2qoqhr2s1o6vnn0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1q2qoqhr2s1o6vnn0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "sftp_directories", 10 + "column": { 11 + "name": "name", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d1m1q2qoqhr2s1o6vnn0", 21 + "parent": "mig_d1m1pihr27un0qigpah0", 22 + "schema": "public", 23 + "startedAt": "2025-07-07T19:16:28.582367Z" 24 + }
+30
apps/api/.xata/migrations/mig_d1m1qi2oqhr2s1o6vno0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1qi2oqhr2s1o6vno0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "sftp_directories", 9 + "column": { 10 + "name": "parent_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"sftp_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "parent_id_link", 16 + "table": "sftp_directories", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m1qi2oqhr2s1o6vno0", 27 + "parent": "mig_d1m1q2qoqhr2s1o6vnn0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:17:29.004821Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m1rghr27un0qigpaj0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1rghr27un0qigpaj0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "sftp_path", 9 + "column": { 10 + "name": "directory_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"sftp_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "directory_id_link", 16 + "table": "sftp_directories", 17 + "column": "xata_id", 18 + "on_delete": "SET NULL" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m1rghr27un0qigpaj0", 27 + "parent": "mig_d1m1qi2oqhr2s1o6vno0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:19:31.155379Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m1ssrj0aqdfvuvbfi0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1ssrj0aqdfvuvbfi0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "s3_paths", 9 + "column": { 10 + "name": "directory_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"s3_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "directory_id_link", 16 + "table": "s3_directories", 17 + "column": "xata_id", 18 + "on_delete": "SET NULL" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m1ssrj0aqdfvuvbfi0", 27 + "parent": "mig_d1m1rghr27un0qigpaj0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:22:28.691198Z" 30 + }
+24
apps/api/.xata/migrations/mig_d1m1t8aoqhr2s1o6vnp0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1t8aoqhr2s1o6vnp0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "s3_directories", 10 + "column": { 11 + "name": "name", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d1m1t8aoqhr2s1o6vnp0", 21 + "parent": "mig_d1m1ssrj0aqdfvuvbfi0", 22 + "schema": "public", 23 + "startedAt": "2025-07-07T19:23:14.283412Z" 24 + }
+25
apps/api/.xata/migrations/mig_d1m1tk9r27un0qigpak0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1tk9r27un0qigpak0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "s3_directories", 10 + "column": { 11 + "name": "path", 12 + "type": "text", 13 + "unique": true, 14 + "comment": "" 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_d1m1tk9r27un0qigpak0", 22 + "parent": "mig_d1m1t8aoqhr2s1o6vnp0", 23 + "schema": "public", 24 + "startedAt": "2025-07-07T19:24:02.056561Z" 25 + }
+30
apps/api/.xata/migrations/mig_d1m1u4bj0aqdfvuvbfl0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1u4bj0aqdfvuvbfl0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "s3_directories", 9 + "column": { 10 + "name": "parent_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"s3_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "parent_id_link", 16 + "table": "s3_directories", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m1u4bj0aqdfvuvbfl0", 27 + "parent": "mig_d1m1tk9r27un0qigpak0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:25:06.362932Z" 30 + }
+24
apps/api/.xata/migrations/mig_d1m1ucrj0aqdfvuvbfm0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1ucrj0aqdfvuvbfm0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "google_drive_directories", 10 + "column": { 11 + "name": "name", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d1m1ucrj0aqdfvuvbfm0", 21 + "parent": "mig_d1m1u4bj0aqdfvuvbfl0", 22 + "schema": "public", 23 + "startedAt": "2025-07-07T19:25:39.600856Z" 24 + }
+25
apps/api/.xata/migrations/mig_d1m1ut1r27un0qigpam0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1ut1r27un0qigpam0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "google_drive_directories", 10 + "column": { 11 + "name": "path", 12 + "type": "text", 13 + "unique": true, 14 + "comment": "" 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_d1m1ut1r27un0qigpam0", 22 + "parent": "mig_d1m1ucrj0aqdfvuvbfm0", 23 + "schema": "public", 24 + "startedAt": "2025-07-07T19:26:45.356723Z" 25 + }
+30
apps/api/.xata/migrations/mig_d1m1vmioqhr2s1o6vnrg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m1vmioqhr2s1o6vnrg", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "google_drive_directories", 9 + "column": { 10 + "name": "parent_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"google_drive_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "parent_id_link", 16 + "table": "google_drive_directories", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m1vmioqhr2s1o6vnrg", 27 + "parent": "mig_d1m1ut1r27un0qigpam0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:28:27.271223Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m20hpr27un0qigpan0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m20hpr27un0qigpan0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "google_drive_paths", 9 + "column": { 10 + "name": "directory_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"google_drive_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "directory_id_link", 16 + "table": "google_drive_directories", 17 + "column": "xata_id", 18 + "on_delete": "SET NULL" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m20hpr27un0qigpan0", 27 + "parent": "mig_d1m1vmioqhr2s1o6vnrg", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:30:15.706553Z" 30 + }
+21
apps/api/.xata/migrations/mig_d1m210ioqhr2s1o6vnsg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m210ioqhr2s1o6vnsg", 5 + "operations": [ 6 + { 7 + "drop_constraint": { 8 + "up": "\"path\"", 9 + "down": "\"path\"", 10 + "name": "google_drive_directories__pgroll_new_path_key", 11 + "table": "google_drive_directories" 12 + } 13 + } 14 + ] 15 + }, 16 + "migrationType": "pgroll", 17 + "name": "mig_d1m210ioqhr2s1o6vnsg", 18 + "parent": "mig_d1m20hpr27un0qigpan0", 19 + "schema": "public", 20 + "startedAt": "2025-07-07T19:31:15.285501Z" 21 + }
+21
apps/api/.xata/migrations/mig_d1m21aioqhr2s1o6vntg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m21aioqhr2s1o6vntg", 5 + "operations": [ 6 + { 7 + "drop_constraint": { 8 + "up": "\"path\"", 9 + "down": "\"path\"", 10 + "name": "s3_directories__pgroll_new_path_key", 11 + "table": "s3_directories" 12 + } 13 + } 14 + ] 15 + }, 16 + "migrationType": "pgroll", 17 + "name": "mig_d1m21aioqhr2s1o6vntg", 18 + "parent": "mig_d1m210ioqhr2s1o6vnsg", 19 + "schema": "public", 20 + "startedAt": "2025-07-07T19:31:55.444336Z" 21 + }
+21
apps/api/.xata/migrations/mig_d1m21q9r27un0qigpao0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m21q9r27un0qigpao0", 5 + "operations": [ 6 + { 7 + "drop_constraint": { 8 + "up": "\"path\"", 9 + "down": "\"path\"", 10 + "name": "sftp_directories__pgroll_new_path_key", 11 + "table": "sftp_directories" 12 + } 13 + } 14 + ] 15 + }, 16 + "migrationType": "pgroll", 17 + "name": "mig_d1m21q9r27un0qigpao0", 18 + "parent": "mig_d1m21aioqhr2s1o6vntg", 19 + "schema": "public", 20 + "startedAt": "2025-07-07T19:32:57.960317Z" 21 + }
+24
apps/api/.xata/migrations/mig_d1m223rj0aqdfvuvbfo0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m223rj0aqdfvuvbfo0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "dropbox_directories", 10 + "column": { 11 + "name": "name", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d1m223rj0aqdfvuvbfo0", 21 + "parent": "mig_d1m21q9r27un0qigpao0", 22 + "schema": "public", 23 + "startedAt": "2025-07-07T19:33:36.261724Z" 24 + }
+24
apps/api/.xata/migrations/mig_d1m22fbj0aqdfvuvbfp0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m22fbj0aqdfvuvbfp0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "dropbox_directories", 10 + "column": { 11 + "name": "path", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d1m22fbj0aqdfvuvbfp0", 21 + "parent": "mig_d1m223rj0aqdfvuvbfo0", 22 + "schema": "public", 23 + "startedAt": "2025-07-07T19:34:23.005599Z" 24 + }
+30
apps/api/.xata/migrations/mig_d1m22r1r27un0qigpap0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m22r1r27un0qigpap0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "dropbox_directories", 9 + "column": { 10 + "name": "parent_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"dropbox_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "parent_id_link", 16 + "table": "dropbox_directories", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m22r1r27un0qigpap0", 27 + "parent": "mig_d1m22fbj0aqdfvuvbfp0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:35:09.120135Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m23a9r27un0qigpaq0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m23a9r27un0qigpaq0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "dropbox_paths", 9 + "column": { 10 + "name": "directory_id", 11 + "type": "text", 12 + "comment": "{\"xata.link\":\"dropbox_directories\"}", 13 + "nullable": true, 14 + "references": { 15 + "name": "directory_id_link", 16 + "table": "dropbox_directories", 17 + "column": "xata_id", 18 + "on_delete": "SET NULL" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m23a9r27un0qigpaq0", 27 + "parent": "mig_d1m22r1r27un0qigpap0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:36:10.079937Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m2c02oqhr2s1o6vnv0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m2c02oqhr2s1o6vnv0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "google_drive_directories", 10 + "column": { 11 + "name": "google_drive_id", 12 + "type": "text", 13 + "comment": "{\"xata.link\":\"google_drive\"}", 14 + "references": { 15 + "name": "google_drive_id_link", 16 + "table": "google_drive", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m2c02oqhr2s1o6vnv0", 27 + "parent": "mig_d1m23a9r27un0qigpaq0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:54:41.546052Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m2cthr27un0qigpavg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m2cthr27un0qigpavg", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "dropbox_directories", 10 + "column": { 11 + "name": "dropbox_id", 12 + "type": "text", 13 + "comment": "{\"xata.link\":\"dropbox\"}", 14 + "references": { 15 + "name": "dropbox_id_link", 16 + "table": "dropbox", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m2cthr27un0qigpavg", 27 + "parent": "mig_d1m2c02oqhr2s1o6vnv0", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:56:39.777931Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m2dupr27un0qigpb0g.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m2dupr27un0qigpb0g", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "s3_directories", 10 + "column": { 11 + "name": "s3_bucket_id", 12 + "type": "text", 13 + "comment": "{\"xata.link\":\"s3_bucket\"}", 14 + "references": { 15 + "name": "s3_bucket_id_link", 16 + "table": "s3_bucket", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m2dupr27un0qigpb0g", 27 + "parent": "mig_d1m2cthr27un0qigpavg", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:58:51.774928Z" 30 + }
+30
apps/api/.xata/migrations/mig_d1m2edioqhr2s1o6vo00.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1m2edioqhr2s1o6vo00", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "sftp_directories", 10 + "column": { 11 + "name": "sftp_id", 12 + "type": "text", 13 + "comment": "{\"xata.link\":\"sftp\"}", 14 + "references": { 15 + "name": "sftp_id_link", 16 + "table": "sftp", 17 + "column": "xata_id", 18 + "on_delete": "CASCADE" 19 + } 20 + } 21 + } 22 + } 23 + ] 24 + }, 25 + "migrationType": "pgroll", 26 + "name": "mig_d1m2edioqhr2s1o6vo00", 27 + "parent": "mig_d1m2dupr27un0qigpb0g", 28 + "schema": "public", 29 + "startedAt": "2025-07-07T19:59:51.387907Z" 30 + }
+25
apps/api/.xata/migrations/mig_d1mb1urj0aqdfvuvbhk0.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1mb1urj0aqdfvuvbhk0", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "google_drive_directories", 10 + "column": { 11 + "name": "file_id", 12 + "type": "text", 13 + "unique": true, 14 + "comment": "" 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_d1mb1urj0aqdfvuvbhk0", 22 + "parent": "sql_76f1105aa6ca0f", 23 + "schema": "public", 24 + "startedAt": "2025-07-08T05:47:40.211674Z" 25 + }
+25
apps/api/.xata/migrations/mig_d1o1f1qoqhr2s1o705dg.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d1o1f1qoqhr2s1o705dg", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "dropbox_directories", 10 + "column": { 11 + "name": "file_id", 12 + "type": "text", 13 + "unique": true, 14 + "comment": "" 15 + } 16 + } 17 + } 18 + ] 19 + }, 20 + "migrationType": "pgroll", 21 + "name": "mig_d1o1f1qoqhr2s1o705dg", 22 + "parent": "mig_d1mb1urj0aqdfvuvbhk0", 23 + "schema": "public", 24 + "startedAt": "2025-07-10T19:42:00.932588Z" 25 + }
+24
apps/api/.xata/migrations/mig_d20ot3jj0aqdfvuvdh6g.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d20ot3jj0aqdfvuvdh6g", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "up": "''", 9 + "table": "queue_tracks", 10 + "column": { 11 + "name": "file_uri", 12 + "type": "text", 13 + "comment": "" 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d20ot3jj0aqdfvuvdh6g", 21 + "parent": "sql_d58ea73a9fcdc5", 22 + "schema": "public", 23 + "startedAt": "2025-07-24T01:38:23.467364Z" 24 + }
+21
apps/api/.xata/migrations/mig_d2mlh1gbs5avv4bj7f0g.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d2mlh1gbs5avv4bj7f0g", 5 + "operations": [ 6 + { 7 + "drop_constraint": { 8 + "up": "\"name\"", 9 + "down": "\"name\"", 10 + "name": "api_keys_name_unique", 11 + "table": "api_keys" 12 + } 13 + } 14 + ] 15 + }, 16 + "migrationType": "pgroll", 17 + "name": "mig_d2mlh1gbs5avv4bj7f0g", 18 + "parent": "mig_d20ot3jj0aqdfvuvdh6g", 19 + "schema": "public", 20 + "startedAt": "2025-08-26T06:47:35.046302Z" 21 + }
+24
apps/api/.xata/migrations/mig_d3ea7abdeq0sm5b3ib8g.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "mig_d3ea7abdeq0sm5b3ib8g", 5 + "operations": [ 6 + { 7 + "add_column": { 8 + "table": "artists", 9 + "column": { 10 + "name": "genres", 11 + "type": "text[]", 12 + "comment": "", 13 + "nullable": true 14 + } 15 + } 16 + } 17 + ] 18 + }, 19 + "migrationType": "pgroll", 20 + "name": "mig_d3ea7abdeq0sm5b3ib8g", 21 + "parent": "mig_d2mlh1gbs5avv4bj7f0g", 22 + "schema": "public", 23 + "startedAt": "2025-10-01T03:44:42.977368Z" 24 + }
+18
apps/api/.xata/migrations/sql_027411d41b3b86.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_027411d41b3b86", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE UNIQUE INDEX unique_dropbox_path ON dropbox_directories USING btree (dropbox_id, path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_027411d41b3b86", 15 + "parent": "sql_70e285f209c400", 16 + "schema": "public", 17 + "startedAt": "2025-07-07T20:30:25.496512Z" 18 + }
+18
apps/api/.xata/migrations/sql_18cc4eb60ac890.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_18cc4eb60ac890", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE INDEX idx_dropbox_directories_path ON dropbox_directories USING btree (path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_18cc4eb60ac890", 15 + "parent": "mig_d1o1f1qoqhr2s1o705dg", 16 + "schema": "public", 17 + "startedAt": "2025-07-13T03:39:39.136028Z" 18 + }
+18
apps/api/.xata/migrations/sql_18f83793ce7a31.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_18f83793ce7a31", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE INDEX idx_google_drive_directories_path ON google_drive_directories USING btree (path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_18f83793ce7a31", 15 + "parent": "sql_18cc4eb60ac890", 16 + "schema": "public", 17 + "startedAt": "2025-07-13T03:40:29.714531Z" 18 + }
+18
apps/api/.xata/migrations/sql_2d6fff5c5e6c78.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_2d6fff5c5e6c78", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE UNIQUE INDEX unique_s3_path ON s3_directories USING btree (s3_bucket_id, path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_2d6fff5c5e6c78", 15 + "parent": "sql_027411d41b3b86", 16 + "schema": "public", 17 + "startedAt": "2025-07-07T20:31:21.125842Z" 18 + }
+18
apps/api/.xata/migrations/sql_45a899d3597b60.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_45a899d3597b60", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE INDEX idx_google_drive_directories_file_id ON google_drive_directories USING btree (file_id)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_45a899d3597b60", 15 + "parent": "sql_18f83793ce7a31", 16 + "schema": "public", 17 + "startedAt": "2025-07-14T07:26:40.176048Z" 18 + }
+18
apps/api/.xata/migrations/sql_70e285f209c400.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_70e285f209c400", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE UNIQUE INDEX unique_drive_path ON google_drive_directories USING btree (google_drive_id, path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_70e285f209c400", 15 + "parent": "mig_d1m2edioqhr2s1o6vo00", 16 + "schema": "public", 17 + "startedAt": "2025-07-07T20:29:49.116223Z" 18 + }
+18
apps/api/.xata/migrations/sql_76f1105aa6ca0f.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_76f1105aa6ca0f", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE UNIQUE INDEX unique_sftp_path ON sftp_directories USING btree (sftp_id, path)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_76f1105aa6ca0f", 15 + "parent": "sql_2d6fff5c5e6c78", 16 + "schema": "public", 17 + "startedAt": "2025-07-07T20:31:54.518491Z" 18 + }
+18
apps/api/.xata/migrations/sql_d58ea73a9fcdc5.json
··· 1 + { 2 + "done": true, 3 + "migration": { 4 + "name": "sql_d58ea73a9fcdc5", 5 + "operations": [ 6 + { 7 + "sql": { 8 + "up": "CREATE INDEX idx_dropbox_directories_file_id ON dropbox_directories USING btree (file_id)" 9 + } 10 + } 11 + ] 12 + }, 13 + "migrationType": "inferred", 14 + "name": "sql_d58ea73a9fcdc5", 15 + "parent": "sql_45a899d3597b60", 16 + "schema": "public", 17 + "startedAt": "2025-07-14T07:27:17.581342Z" 18 + }
+3
apps/api/src/nowplaying/nowplaying.service.ts
··· 22 22 name: string; 23 23 createdAt: string; 24 24 picture?: BlobRef; 25 + tags?: string[]; 25 26 } = { 26 27 $type: "app.rocksky.artist", 27 28 name: track.albumArtist, 28 29 createdAt: new Date().toISOString(), 30 + tags: track.genres, 29 31 }; 30 32 31 33 if (track.artistPicture) { ··· 162 164 : undefined, 163 165 createdAt: new Date().toISOString(), 164 166 spotifyLink: track.spotifyLink ? track.spotifyLink : undefined, 167 + tags: track.genres, 165 168 }; 166 169 167 170 if (!Song.validateRecord(record).success) {
+1
apps/api/src/schema/artists.ts
··· 15 15 spotifyLink: text("spotify_link"), 16 16 tidalLink: text("tidal_link"), 17 17 youtubeLink: text("youtube_link"), 18 + genres: text("genres").array(), 18 19 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 19 20 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 20 21 xataVersion: integer("xata_version"),
+1
apps/api/src/types/track.ts
··· 30 30 appleMusicLink: z.string().optional().nullable(), 31 31 deezerLink: z.string().optional().nullable(), 32 32 timestamp: z.number().optional().nullable(), 33 + genres: z.array(z.string()).optional().nullable(), 33 34 }); 34 35 35 36 export type Track = z.infer<typeof trackSchema>;
+526 -3
apps/api/src/xata.ts
··· 381 381 name: "api_keys__pgroll_new_shared_secret_key", 382 382 columns: ["shared_secret"], 383 383 }, 384 - api_keys_name_unique: { name: "api_keys_name_unique", columns: ["name"] }, 385 384 }, 386 385 columns: [ 387 386 { ··· 412 411 name: "name", 413 412 type: "text", 414 413 notNull: true, 415 - unique: true, 414 + unique: false, 416 415 defaultValue: null, 417 416 comment: "", 418 417 }, ··· 805 804 comment: "", 806 805 }, 807 806 { 807 + name: "genres", 808 + type: "multiple", 809 + notNull: false, 810 + unique: false, 811 + defaultValue: null, 812 + comment: "", 813 + }, 814 + { 808 815 name: "lastfm_link", 809 816 type: "text", 810 817 notNull: false, ··· 1174 1181 ], 1175 1182 }, 1176 1183 { 1184 + name: "dropbox_directories", 1185 + checkConstraints: { 1186 + dropbox_directories_xata_id_length_xata_id: { 1187 + name: "dropbox_directories_xata_id_length_xata_id", 1188 + columns: ["xata_id"], 1189 + definition: "CHECK ((length(xata_id) < 256))", 1190 + }, 1191 + }, 1192 + foreignKeys: { 1193 + dropbox_id_link: { 1194 + name: "dropbox_id_link", 1195 + columns: ["dropbox_id"], 1196 + referencedTable: "dropbox", 1197 + referencedColumns: ["xata_id"], 1198 + onDelete: "CASCADE", 1199 + }, 1200 + parent_id_link: { 1201 + name: "parent_id_link", 1202 + columns: ["parent_id"], 1203 + referencedTable: "dropbox_directories", 1204 + referencedColumns: ["xata_id"], 1205 + onDelete: "CASCADE", 1206 + }, 1207 + }, 1208 + primaryKey: [], 1209 + uniqueConstraints: { 1210 + _pgroll_new_dropbox_directories_xata_id_key: { 1211 + name: "_pgroll_new_dropbox_directories_xata_id_key", 1212 + columns: ["xata_id"], 1213 + }, 1214 + dropbox_directories__pgroll_new_file_id_key: { 1215 + name: "dropbox_directories__pgroll_new_file_id_key", 1216 + columns: ["file_id"], 1217 + }, 1218 + }, 1219 + columns: [ 1220 + { 1221 + name: "dropbox_id", 1222 + type: "link", 1223 + link: { table: "dropbox" }, 1224 + notNull: true, 1225 + unique: false, 1226 + defaultValue: null, 1227 + comment: '{"xata.link":"dropbox"}', 1228 + }, 1229 + { 1230 + name: "file_id", 1231 + type: "text", 1232 + notNull: true, 1233 + unique: true, 1234 + defaultValue: null, 1235 + comment: "", 1236 + }, 1237 + { 1238 + name: "name", 1239 + type: "text", 1240 + notNull: true, 1241 + unique: false, 1242 + defaultValue: null, 1243 + comment: "", 1244 + }, 1245 + { 1246 + name: "parent_id", 1247 + type: "link", 1248 + link: { table: "dropbox_directories" }, 1249 + notNull: false, 1250 + unique: false, 1251 + defaultValue: null, 1252 + comment: '{"xata.link":"dropbox_directories"}', 1253 + }, 1254 + { 1255 + name: "path", 1256 + type: "text", 1257 + notNull: true, 1258 + unique: false, 1259 + defaultValue: null, 1260 + comment: "", 1261 + }, 1262 + { 1263 + name: "xata_createdat", 1264 + type: "datetime", 1265 + notNull: true, 1266 + unique: false, 1267 + defaultValue: "now()", 1268 + comment: "", 1269 + }, 1270 + { 1271 + name: "xata_id", 1272 + type: "text", 1273 + notNull: true, 1274 + unique: true, 1275 + defaultValue: "('rec_'::text || (xata_private.xid())::text)", 1276 + comment: "", 1277 + }, 1278 + { 1279 + name: "xata_updatedat", 1280 + type: "datetime", 1281 + notNull: true, 1282 + unique: false, 1283 + defaultValue: "now()", 1284 + comment: "", 1285 + }, 1286 + { 1287 + name: "xata_version", 1288 + type: "int", 1289 + notNull: true, 1290 + unique: false, 1291 + defaultValue: "0", 1292 + comment: "", 1293 + }, 1294 + ], 1295 + }, 1296 + { 1177 1297 name: "dropbox_paths", 1178 1298 checkConstraints: { 1179 1299 dropbox_paths_xata_id_length_xata_id: { ··· 1183 1303 }, 1184 1304 }, 1185 1305 foreignKeys: { 1306 + directory_id_link: { 1307 + name: "directory_id_link", 1308 + columns: ["directory_id"], 1309 + referencedTable: "dropbox_directories", 1310 + referencedColumns: ["xata_id"], 1311 + onDelete: "SET NULL", 1312 + }, 1186 1313 dropbox_id_link: { 1187 1314 name: "dropbox_id_link", 1188 1315 columns: ["dropbox_id"], ··· 1211 1338 }, 1212 1339 columns: [ 1213 1340 { 1341 + name: "directory_id", 1342 + type: "link", 1343 + link: { table: "dropbox_directories" }, 1344 + notNull: false, 1345 + unique: false, 1346 + defaultValue: null, 1347 + comment: '{"xata.link":"dropbox_directories"}', 1348 + }, 1349 + { 1214 1350 name: "dropbox_id", 1215 1351 type: "link", 1216 1352 link: { table: "dropbox" }, ··· 1521 1657 ], 1522 1658 }, 1523 1659 { 1660 + name: "google_drive_directories", 1661 + checkConstraints: { 1662 + google_drive_directories_xata_id_length_xata_id: { 1663 + name: "google_drive_directories_xata_id_length_xata_id", 1664 + columns: ["xata_id"], 1665 + definition: "CHECK ((length(xata_id) < 256))", 1666 + }, 1667 + }, 1668 + foreignKeys: { 1669 + google_drive_id_link: { 1670 + name: "google_drive_id_link", 1671 + columns: ["google_drive_id"], 1672 + referencedTable: "google_drive", 1673 + referencedColumns: ["xata_id"], 1674 + onDelete: "CASCADE", 1675 + }, 1676 + parent_id_link: { 1677 + name: "parent_id_link", 1678 + columns: ["parent_id"], 1679 + referencedTable: "google_drive_directories", 1680 + referencedColumns: ["xata_id"], 1681 + onDelete: "CASCADE", 1682 + }, 1683 + }, 1684 + primaryKey: [], 1685 + uniqueConstraints: { 1686 + _pgroll_new_google_drive_directories_xata_id_key: { 1687 + name: "_pgroll_new_google_drive_directories_xata_id_key", 1688 + columns: ["xata_id"], 1689 + }, 1690 + google_drive_directories__pgroll_new_file_id_key: { 1691 + name: "google_drive_directories__pgroll_new_file_id_key", 1692 + columns: ["file_id"], 1693 + }, 1694 + }, 1695 + columns: [ 1696 + { 1697 + name: "file_id", 1698 + type: "text", 1699 + notNull: true, 1700 + unique: true, 1701 + defaultValue: null, 1702 + comment: "", 1703 + }, 1704 + { 1705 + name: "google_drive_id", 1706 + type: "link", 1707 + link: { table: "google_drive" }, 1708 + notNull: true, 1709 + unique: false, 1710 + defaultValue: null, 1711 + comment: '{"xata.link":"google_drive"}', 1712 + }, 1713 + { 1714 + name: "name", 1715 + type: "text", 1716 + notNull: true, 1717 + unique: false, 1718 + defaultValue: null, 1719 + comment: "", 1720 + }, 1721 + { 1722 + name: "parent_id", 1723 + type: "link", 1724 + link: { table: "google_drive_directories" }, 1725 + notNull: false, 1726 + unique: false, 1727 + defaultValue: null, 1728 + comment: '{"xata.link":"google_drive_directories"}', 1729 + }, 1730 + { 1731 + name: "path", 1732 + type: "text", 1733 + notNull: true, 1734 + unique: false, 1735 + defaultValue: null, 1736 + comment: "", 1737 + }, 1738 + { 1739 + name: "xata_createdat", 1740 + type: "datetime", 1741 + notNull: true, 1742 + unique: false, 1743 + defaultValue: "now()", 1744 + comment: "", 1745 + }, 1746 + { 1747 + name: "xata_id", 1748 + type: "text", 1749 + notNull: true, 1750 + unique: true, 1751 + defaultValue: "('rec_'::text || (xata_private.xid())::text)", 1752 + comment: "", 1753 + }, 1754 + { 1755 + name: "xata_updatedat", 1756 + type: "datetime", 1757 + notNull: true, 1758 + unique: false, 1759 + defaultValue: "now()", 1760 + comment: "", 1761 + }, 1762 + { 1763 + name: "xata_version", 1764 + type: "int", 1765 + notNull: true, 1766 + unique: false, 1767 + defaultValue: "0", 1768 + comment: "", 1769 + }, 1770 + ], 1771 + }, 1772 + { 1524 1773 name: "google_drive_paths", 1525 1774 checkConstraints: { 1526 1775 google_drive_paths_xata_id_length_xata_id: { ··· 1530 1779 }, 1531 1780 }, 1532 1781 foreignKeys: { 1782 + directory_id_link: { 1783 + name: "directory_id_link", 1784 + columns: ["directory_id"], 1785 + referencedTable: "google_drive_directories", 1786 + referencedColumns: ["xata_id"], 1787 + onDelete: "SET NULL", 1788 + }, 1533 1789 google_drive_id_link: { 1534 1790 name: "google_drive_id_link", 1535 1791 columns: ["google_drive_id"], ··· 1558 1814 }, 1559 1815 columns: [ 1560 1816 { 1817 + name: "directory_id", 1818 + type: "link", 1819 + link: { table: "google_drive_directories" }, 1820 + notNull: false, 1821 + unique: false, 1822 + defaultValue: null, 1823 + comment: '{"xata.link":"google_drive_directories"}', 1824 + }, 1825 + { 1561 1826 name: "file_id", 1562 1827 type: "text", 1563 1828 notNull: true, ··· 2228 2493 }, 2229 2494 columns: [ 2230 2495 { 2496 + name: "file_uri", 2497 + type: "text", 2498 + notNull: true, 2499 + unique: false, 2500 + defaultValue: null, 2501 + comment: "", 2502 + }, 2503 + { 2231 2504 name: "position", 2232 2505 type: "int", 2233 2506 notNull: true, ··· 2497 2770 ], 2498 2771 }, 2499 2772 { 2773 + name: "s3_directories", 2774 + checkConstraints: { 2775 + s3_directories_xata_id_length_xata_id: { 2776 + name: "s3_directories_xata_id_length_xata_id", 2777 + columns: ["xata_id"], 2778 + definition: "CHECK ((length(xata_id) < 256))", 2779 + }, 2780 + }, 2781 + foreignKeys: { 2782 + parent_id_link: { 2783 + name: "parent_id_link", 2784 + columns: ["parent_id"], 2785 + referencedTable: "s3_directories", 2786 + referencedColumns: ["xata_id"], 2787 + onDelete: "CASCADE", 2788 + }, 2789 + s3_bucket_id_link: { 2790 + name: "s3_bucket_id_link", 2791 + columns: ["s3_bucket_id"], 2792 + referencedTable: "s3_bucket", 2793 + referencedColumns: ["xata_id"], 2794 + onDelete: "CASCADE", 2795 + }, 2796 + }, 2797 + primaryKey: [], 2798 + uniqueConstraints: { 2799 + _pgroll_new_s3_directories_xata_id_key: { 2800 + name: "_pgroll_new_s3_directories_xata_id_key", 2801 + columns: ["xata_id"], 2802 + }, 2803 + }, 2804 + columns: [ 2805 + { 2806 + name: "name", 2807 + type: "text", 2808 + notNull: true, 2809 + unique: false, 2810 + defaultValue: null, 2811 + comment: "", 2812 + }, 2813 + { 2814 + name: "parent_id", 2815 + type: "link", 2816 + link: { table: "s3_directories" }, 2817 + notNull: false, 2818 + unique: false, 2819 + defaultValue: null, 2820 + comment: '{"xata.link":"s3_directories"}', 2821 + }, 2822 + { 2823 + name: "path", 2824 + type: "text", 2825 + notNull: true, 2826 + unique: false, 2827 + defaultValue: null, 2828 + comment: "", 2829 + }, 2830 + { 2831 + name: "s3_bucket_id", 2832 + type: "link", 2833 + link: { table: "s3_bucket" }, 2834 + notNull: true, 2835 + unique: false, 2836 + defaultValue: null, 2837 + comment: '{"xata.link":"s3_bucket"}', 2838 + }, 2839 + { 2840 + name: "xata_createdat", 2841 + type: "datetime", 2842 + notNull: true, 2843 + unique: false, 2844 + defaultValue: "now()", 2845 + comment: "", 2846 + }, 2847 + { 2848 + name: "xata_id", 2849 + type: "text", 2850 + notNull: true, 2851 + unique: true, 2852 + defaultValue: "('rec_'::text || (xata_private.xid())::text)", 2853 + comment: "", 2854 + }, 2855 + { 2856 + name: "xata_updatedat", 2857 + type: "datetime", 2858 + notNull: true, 2859 + unique: false, 2860 + defaultValue: "now()", 2861 + comment: "", 2862 + }, 2863 + { 2864 + name: "xata_version", 2865 + type: "int", 2866 + notNull: true, 2867 + unique: false, 2868 + defaultValue: "0", 2869 + comment: "", 2870 + }, 2871 + ], 2872 + }, 2873 + { 2500 2874 name: "s3_paths", 2501 2875 checkConstraints: { 2502 2876 s3_paths_xata_id_length_xata_id: { ··· 2506 2880 }, 2507 2881 }, 2508 2882 foreignKeys: { 2883 + directory_id_link: { 2884 + name: "directory_id_link", 2885 + columns: ["directory_id"], 2886 + referencedTable: "s3_directories", 2887 + referencedColumns: ["xata_id"], 2888 + onDelete: "SET NULL", 2889 + }, 2509 2890 s3_bucket_id_link: { 2510 2891 name: "s3_bucket_id_link", 2511 2892 columns: ["s3_bucket_id"], ··· 2529 2910 }, 2530 2911 }, 2531 2912 columns: [ 2913 + { 2914 + name: "directory_id", 2915 + type: "link", 2916 + link: { table: "s3_directories" }, 2917 + notNull: false, 2918 + unique: false, 2919 + defaultValue: null, 2920 + comment: '{"xata.link":"s3_directories"}', 2921 + }, 2532 2922 { 2533 2923 name: "name", 2534 2924 type: "text", ··· 2904 3294 ], 2905 3295 }, 2906 3296 { 3297 + name: "sftp_directories", 3298 + checkConstraints: { 3299 + ftp_directories_xata_id_length_xata_id: { 3300 + name: "ftp_directories_xata_id_length_xata_id", 3301 + columns: ["xata_id"], 3302 + definition: "CHECK ((length(xata_id) < 256))", 3303 + }, 3304 + }, 3305 + foreignKeys: { 3306 + parent_id_link: { 3307 + name: "parent_id_link", 3308 + columns: ["parent_id"], 3309 + referencedTable: "sftp_directories", 3310 + referencedColumns: ["xata_id"], 3311 + onDelete: "CASCADE", 3312 + }, 3313 + sftp_id_link: { 3314 + name: "sftp_id_link", 3315 + columns: ["sftp_id"], 3316 + referencedTable: "sftp", 3317 + referencedColumns: ["xata_id"], 3318 + onDelete: "CASCADE", 3319 + }, 3320 + }, 3321 + primaryKey: [], 3322 + uniqueConstraints: { 3323 + _pgroll_new_ftp_directories_xata_id_key: { 3324 + name: "_pgroll_new_ftp_directories_xata_id_key", 3325 + columns: ["xata_id"], 3326 + }, 3327 + }, 3328 + columns: [ 3329 + { 3330 + name: "name", 3331 + type: "text", 3332 + notNull: true, 3333 + unique: false, 3334 + defaultValue: null, 3335 + comment: "", 3336 + }, 3337 + { 3338 + name: "parent_id", 3339 + type: "link", 3340 + link: { table: "sftp_directories" }, 3341 + notNull: false, 3342 + unique: false, 3343 + defaultValue: null, 3344 + comment: '{"xata.link":"sftp_directories"}', 3345 + }, 3346 + { 3347 + name: "path", 3348 + type: "text", 3349 + notNull: true, 3350 + unique: false, 3351 + defaultValue: null, 3352 + comment: "", 3353 + }, 3354 + { 3355 + name: "sftp_id", 3356 + type: "link", 3357 + link: { table: "sftp" }, 3358 + notNull: true, 3359 + unique: false, 3360 + defaultValue: null, 3361 + comment: '{"xata.link":"sftp"}', 3362 + }, 3363 + { 3364 + name: "xata_createdat", 3365 + type: "datetime", 3366 + notNull: true, 3367 + unique: false, 3368 + defaultValue: "now()", 3369 + comment: "", 3370 + }, 3371 + { 3372 + name: "xata_id", 3373 + type: "text", 3374 + notNull: true, 3375 + unique: true, 3376 + defaultValue: "('rec_'::text || (xata_private.xid())::text)", 3377 + comment: "", 3378 + }, 3379 + { 3380 + name: "xata_updatedat", 3381 + type: "datetime", 3382 + notNull: true, 3383 + unique: false, 3384 + defaultValue: "now()", 3385 + comment: "", 3386 + }, 3387 + { 3388 + name: "xata_version", 3389 + type: "int", 3390 + notNull: true, 3391 + unique: false, 3392 + defaultValue: "0", 3393 + comment: "", 3394 + }, 3395 + ], 3396 + }, 3397 + { 2907 3398 name: "sftp_path", 2908 3399 checkConstraints: { 2909 3400 sftp_path_xata_id_length_xata_id: { ··· 2913 3404 }, 2914 3405 }, 2915 3406 foreignKeys: { 3407 + directory_id_link: { 3408 + name: "directory_id_link", 3409 + columns: ["directory_id"], 3410 + referencedTable: "sftp_directories", 3411 + referencedColumns: ["xata_id"], 3412 + onDelete: "SET NULL", 3413 + }, 2916 3414 sftp_id_link: { 2917 3415 name: "sftp_id_link", 2918 3416 columns: ["sftp_id"], ··· 2936 3434 }, 2937 3435 }, 2938 3436 columns: [ 3437 + { 3438 + name: "directory_id", 3439 + type: "link", 3440 + link: { table: "sftp_directories" }, 3441 + notNull: false, 3442 + unique: false, 3443 + defaultValue: null, 3444 + comment: '{"xata.link":"sftp_directories"}', 3445 + }, 2939 3446 { 2940 3447 name: "name", 2941 3448 type: "text", ··· 4569 5076 export type DropboxAccounts = InferredTypes["dropbox_accounts"]; 4570 5077 export type DropboxAccountsRecord = DropboxAccounts & XataRecord; 4571 5078 5079 + export type DropboxDirectories = InferredTypes["dropbox_directories"]; 5080 + export type DropboxDirectoriesRecord = DropboxDirectories & XataRecord; 5081 + 4572 5082 export type DropboxPaths = InferredTypes["dropbox_paths"]; 4573 5083 export type DropboxPathsRecord = DropboxPaths & XataRecord; 4574 5084 ··· 4580 5090 4581 5091 export type GoogleDriveAccounts = InferredTypes["google_drive_accounts"]; 4582 5092 export type GoogleDriveAccountsRecord = GoogleDriveAccounts & XataRecord; 5093 + 5094 + export type GoogleDriveDirectories = InferredTypes["google_drive_directories"]; 5095 + export type GoogleDriveDirectoriesRecord = GoogleDriveDirectories & XataRecord; 4583 5096 4584 5097 export type GoogleDrivePaths = InferredTypes["google_drive_paths"]; 4585 5098 export type GoogleDrivePathsRecord = GoogleDrivePaths & XataRecord; ··· 4610 5123 4611 5124 export type S3Bucket = InferredTypes["s3_bucket"]; 4612 5125 export type S3BucketRecord = S3Bucket & XataRecord; 5126 + 5127 + export type S3Directories = InferredTypes["s3_directories"]; 5128 + export type S3DirectoriesRecord = S3Directories & XataRecord; 4613 5129 4614 5130 export type S3Paths = InferredTypes["s3_paths"]; 4615 5131 export type S3PathsRecord = S3Paths & XataRecord; ··· 4626 5142 export type SftpAccess = InferredTypes["sftp_access"]; 4627 5143 export type SftpAccessRecord = SftpAccess & XataRecord; 4628 5144 5145 + export type SftpDirectories = InferredTypes["sftp_directories"]; 5146 + export type SftpDirectoriesRecord = SftpDirectories & XataRecord; 5147 + 4629 5148 export type SftpPath = InferredTypes["sftp_path"]; 4630 5149 export type SftpPathRecord = SftpPath & XataRecord; 4631 5150 ··· 4683 5202 builtin_storage_paths: BuiltinStoragePathsRecord; 4684 5203 dropbox: DropboxRecord; 4685 5204 dropbox_accounts: DropboxAccountsRecord; 5205 + dropbox_directories: DropboxDirectoriesRecord; 4686 5206 dropbox_paths: DropboxPathsRecord; 4687 5207 dropbox_tokens: DropboxTokensRecord; 4688 5208 google_drive: GoogleDriveRecord; 4689 5209 google_drive_accounts: GoogleDriveAccountsRecord; 5210 + google_drive_directories: GoogleDriveDirectoriesRecord; 4690 5211 google_drive_paths: GoogleDrivePathsRecord; 4691 5212 google_drive_tokens: GoogleDriveTokensRecord; 4692 5213 loved_tracks: LovedTracksRecord; ··· 4697 5218 queue_tracks: QueueTracksRecord; 4698 5219 radios: RadiosRecord; 4699 5220 s3_bucket: S3BucketRecord; 5221 + s3_directories: S3DirectoriesRecord; 4700 5222 s3_paths: S3PathsRecord; 4701 5223 s3_tokens: S3TokensRecord; 4702 5224 scrobbles: ScrobblesRecord; 4703 5225 sftp: SftpRecord; 4704 5226 sftp_access: SftpAccessRecord; 5227 + sftp_directories: SftpDirectoriesRecord; 4705 5228 sftp_path: SftpPathRecord; 4706 5229 shout_likes: ShoutLikesRecord; 4707 5230 shout_reports: ShoutReportsRecord; ··· 4732 5255 } 4733 5256 } 4734 5257 4735 - let instance: XataClient | undefined; 5258 + let instance: XataClient | undefined = undefined; 4736 5259 4737 5260 export const getXataClient = () => { 4738 5261 if (instance) return instance;
+183 -182
apps/api/src/xrpc/app/rocksky/scrobble/createScrobble.ts
··· 38 38 pipe( 39 39 scrobbleTrack(ctx, track, agent, did), 40 40 Effect.tap(() => 41 - Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`), 42 - ), 43 - ), 41 + Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`) 42 + ) 43 + ) 44 44 ), 45 45 Effect.flatMap(presentation), 46 46 Effect.retry({ times: 3 }), ··· 48 48 Effect.catchAll((err) => { 49 49 console.error(err); 50 50 return Effect.succeed({}); 51 - }), 51 + }) 52 52 ); 53 53 server.app.rocksky.scrobble.createScrobble({ 54 54 auth: ctx.authVerifier, ··· 82 82 ctx, 83 83 did, 84 84 input, 85 - })), 85 + })) 86 86 ), 87 87 Match.orElse(() => { 88 88 throw new Error("Authentication required to create a scrobble"); 89 - }), 89 + }) 90 90 ), 91 91 catch: (error) => new Error(`Failed to create agent: ${error}`), 92 92 }); ··· 138 138 Effect.flatMap(([imageBuffer, options]) => 139 139 pipe( 140 140 Effect.tryPromise(() => agent.uploadBlob(imageBuffer, options)), 141 - Effect.map((uploadResponse) => uploadResponse.data.blob), 142 - ), 141 + Effect.map((uploadResponse) => uploadResponse.data.blob) 142 + ) 143 143 ), 144 - Effect.catchAll(() => Effect.succeed(undefined as BlobRef | undefined)), 144 + Effect.catchAll(() => Effect.succeed(undefined as BlobRef | undefined)) 145 145 ); 146 146 147 147 const putRecord = <T>( 148 148 agent: Agent, 149 149 collection: string, 150 150 record: T, 151 - validate: (record: T) => { success: boolean }, 151 + validate: (record: T) => { success: boolean } 152 152 ) => 153 153 pipe( 154 154 Effect.succeed(record), 155 155 Effect.filterOrFail( 156 156 (rec) => validate(rec).success, 157 - () => new Error("Invalid record"), 157 + () => new Error("Invalid record") 158 158 ), 159 159 Effect.flatMap(() => 160 160 pipe( ··· 167 167 rkey, 168 168 record, 169 169 validate: false, 170 - }), 171 - ), 170 + }) 171 + ) 172 172 ), 173 173 Effect.tap((res) => 174 - Effect.logInfo(`Record created at ${res.data.uri}`), 174 + Effect.logInfo(`Record created at ${res.data.uri}`) 175 175 ), 176 - Effect.map((res) => res.data.uri), 177 - ), 176 + Effect.map((res) => res.data.uri) 177 + ) 178 178 ), 179 179 Effect.catchAll((error) => { 180 180 console.error(`Error creating ${collection} record`, error); 181 181 return Effect.succeed(null); 182 - }), 182 + }) 183 183 ); 184 184 185 185 const putArtistRecord = (track: Track, agent: Agent) => ··· 192 192 name: track.albumArtist, 193 193 createdAt: new Date().toISOString(), 194 194 picture, 195 + tags: track.genres, 195 196 })), 196 197 Effect.flatMap((record) => 197 - putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord), 198 - ), 198 + putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord) 199 + ) 199 200 ); 200 201 201 202 const putAlbumRecord = (track: Track, agent: Agent) => ··· 203 204 Match.value(track.albumArt).pipe( 204 205 Match.when( 205 206 (url) => !!url, 206 - (url) => uploadImage(url, agent), 207 + (url) => uploadImage(url, agent) 207 208 ), 208 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 209 + Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)) 209 210 ), 210 211 Effect.map((albumArt) => ({ 211 212 $type: "app.rocksky.album", ··· 219 220 albumArt, 220 221 })), 221 222 Effect.flatMap((record) => 222 - putRecord(agent, "app.rocksky.album", record, Album.validateRecord), 223 - ), 223 + putRecord(agent, "app.rocksky.album", record, Album.validateRecord) 224 + ) 224 225 ); 225 226 226 227 const putSongRecord = (track: Track, agent: Agent) => ··· 228 229 Match.value(track.albumArt).pipe( 229 230 Match.when( 230 231 (url) => !!url, 231 - (url) => uploadImage(url, agent), 232 + (url) => uploadImage(url, agent) 232 233 ), 233 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 234 + Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)) 234 235 ), 235 236 Effect.map((albumArt) => ({ 236 237 $type: "app.rocksky.song", ··· 253 254 spotifyLink: track.spotifyLink ?? undefined, 254 255 })), 255 256 Effect.flatMap((record) => 256 - putRecord(agent, "app.rocksky.song", record, Song.validateRecord), 257 - ), 257 + putRecord(agent, "app.rocksky.song", record, Song.validateRecord) 258 + ) 258 259 ); 259 260 260 261 const putScrobbleRecord = (track: Track, agent: Agent) => ··· 262 263 Match.value(track.albumArt).pipe( 263 264 Match.when( 264 265 (url) => !!url, 265 - (url) => uploadImage(url, agent), 266 + (url) => uploadImage(url, agent) 266 267 ), 267 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 268 + Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)) 268 269 ), 269 270 Effect.map((albumArt) => ({ 270 271 $type: "app.rocksky.scrobble", ··· 289 290 spotifyLink: track.spotifyLink ?? undefined, 290 291 })), 291 292 Effect.flatMap((record) => 292 - putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord), 293 - ), 293 + putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord) 294 + ) 294 295 ); 295 296 296 297 const getScrobble = ({ ctx, id }: { ctx: Context; id: string }) => ··· 302 303 .leftJoin(tables.albums, eq(tables.albums.id, tables.scrobbles.albumId)) 303 304 .leftJoin( 304 305 tables.artists, 305 - eq(tables.artists.id, tables.scrobbles.artistId), 306 + eq(tables.artists.id, tables.scrobbles.artistId) 306 307 ) 307 308 .leftJoin(tables.users, eq(tables.users.id, tables.scrobbles.userId)) 308 309 .where(eq(tables.scrobbles.id, id)) 309 310 .execute() 310 - .then(([row]) => row), 311 + .then(([row]) => row) 311 312 ); 312 313 313 314 const getUserAlbum = ( ··· 317 318 artists: SelectArtist; 318 319 users: SelectUser; 319 320 tracks: SelectTrack; 320 - }, 321 + } 321 322 ) => 322 323 Effect.tryPromise(() => 323 324 ctx.db ··· 325 326 .from(tables.userAlbums) 326 327 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 327 328 .execute() 328 - .then(([row]) => row), 329 + .then(([row]) => row) 329 330 ); 330 331 331 332 const getUserArtist = ( ··· 335 336 artists: SelectArtist; 336 337 users: SelectUser; 337 338 tracks: SelectTrack; 338 - }, 339 + } 339 340 ) => 340 341 Effect.tryPromise(() => 341 342 ctx.db ··· 343 344 .from(tables.userArtists) 344 345 .where(eq(tables.userArtists.id, scrobble.artists.id)) 345 346 .execute() 346 - .then(([row]) => row), 347 + .then(([row]) => row) 347 348 ); 348 349 349 350 const getUserTrack = ( ··· 353 354 artists: SelectArtist; 354 355 users: SelectUser; 355 356 tracks: SelectTrack; 356 - }, 357 + } 357 358 ) => 358 359 Effect.tryPromise(() => 359 360 ctx.db ··· 361 362 .from(tables.userTracks) 362 363 .where(eq(tables.userTracks.id, scrobble.tracks.id)) 363 364 .execute() 364 - .then(([row]) => row), 365 + .then(([row]) => row) 365 366 ); 366 367 367 368 const getAlbumTrack = ( ··· 371 372 artists: SelectArtist; 372 373 users: SelectUser; 373 374 tracks: SelectTrack; 374 - }, 375 + } 375 376 ) => 376 377 Effect.tryPromise(() => 377 378 ctx.db ··· 379 380 .from(tables.albumTracks) 380 381 .where(eq(tables.albumTracks.trackId, scrobble.tracks.id)) 381 382 .execute() 382 - .then(([row]) => row), 383 + .then(([row]) => row) 383 384 ); 384 385 385 386 const getArtistTrack = ( ··· 389 390 artists: SelectArtist; 390 391 users: SelectUser; 391 392 tracks: SelectTrack; 392 - }, 393 + } 393 394 ) => 394 395 Effect.tryPromise(() => 395 396 ctx.db ··· 397 398 .from(tables.artistTracks) 398 399 .where(eq(tables.artistTracks.trackId, scrobble.tracks.id)) 399 400 .execute() 400 - .then(([row]) => row), 401 + .then(([row]) => row) 401 402 ); 402 403 403 404 const getArtistAlbum = ( ··· 407 408 artists: SelectArtist; 408 409 users: SelectUser; 409 410 tracks: SelectTrack; 410 - }, 411 + } 411 412 ) => 412 413 Effect.tryPromise(() => 413 414 ctx.db ··· 416 417 .where( 417 418 and( 418 419 eq(tables.artistAlbums.albumId, scrobble.albums.id), 419 - eq(tables.artistAlbums.artistId, scrobble.artists.id), 420 - ), 420 + eq(tables.artistAlbums.artistId, scrobble.artists.id) 421 + ) 421 422 ) 422 - .then(([row]) => row), 423 + .then(([row]) => row) 423 424 ); 424 425 425 426 const createUserArtist = ( ··· 429 430 artists: SelectArtist; 430 431 users: SelectUser; 431 432 tracks: SelectTrack; 432 - }, 433 + } 433 434 ) => 434 435 pipe( 435 436 Effect.tryPromise(() => ··· 441 442 uri: scrobble.artists.uri, 442 443 scrobbles: 1, 443 444 } as InsertUserArtist) 444 - .execute(), 445 + .execute() 445 446 ), 446 447 Effect.flatMap(() => 447 448 Effect.tryPromise(() => ··· 450 451 .from(tables.userArtists) 451 452 .where(eq(tables.userArtists.artistId, scrobble.artists.id)) 452 453 .execute() 453 - .then(([row]) => row), 454 - ), 455 - ), 454 + .then(([row]) => row) 455 + ) 456 + ) 456 457 ); 457 458 458 459 const createUserAlbum = ( ··· 462 463 artists: SelectArtist; 463 464 users: SelectUser; 464 465 tracks: SelectTrack; 465 - }, 466 + } 466 467 ) => 467 468 pipe( 468 469 Effect.tryPromise(() => ··· 474 475 uri: scrobble.albums.uri, 475 476 scrobbles: 1, 476 477 } as InsertUserAlbum) 477 - .execute(), 478 + .execute() 478 479 ), 479 480 Effect.flatMap(() => 480 481 Effect.tryPromise(() => ··· 483 484 .from(tables.userAlbums) 484 485 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 485 486 .execute() 486 - .then(([row]) => row), 487 - ), 488 - ), 487 + .then(([row]) => row) 488 + ) 489 + ) 489 490 ); 490 491 491 492 const createUserTrack = ( ··· 495 496 artists: SelectArtist; 496 497 users: SelectUser; 497 498 tracks: SelectTrack; 498 - }, 499 + } 499 500 ) => 500 501 pipe( 501 502 Effect.tryPromise(() => ··· 507 508 uri: scrobble.tracks.uri, 508 509 scrobbles: 1, 509 510 } as InsertUserTrack) 510 - .execute(), 511 + .execute() 511 512 ), 512 513 Effect.flatMap(() => 513 514 Effect.tryPromise(() => ··· 515 516 .select() 516 517 .from(tables.userTracks) 517 518 .where(eq(tables.userTracks.trackId, scrobble.tracks.id)) 518 - .then(([row]) => row), 519 - ), 520 - ), 519 + .then(([row]) => row) 520 + ) 521 + ) 521 522 ); 522 523 523 524 const publishScrobble = (ctx: Context, id: string) => ··· 688 689 xata_updatedat: artistAlbum.updatedAt.toISOString(), 689 690 xata_version: artistAlbum.xataVersion, 690 691 }, 691 - }), 692 - ), 693 - ), 694 - ), 695 - ), 692 + }) 693 + ) 694 + ) 695 + ) 696 + ) 696 697 ), 697 698 Effect.flatMap((data) => 698 699 Effect.try(() => 699 700 ctx.nc.publish( 700 701 "rocksky.scrobble", 701 702 Buffer.from( 702 - JSON.stringify(data).replaceAll("sha_256", "sha256"), 703 - ), 704 - ), 705 - ), 706 - ), 707 - ), 708 - ), 709 - ), 710 - ), 703 + JSON.stringify(data).replaceAll("sha_256", "sha256") 704 + ) 705 + ) 706 + ) 707 + ) 708 + ) 709 + ) 710 + ) 711 + ) 711 712 ); 712 713 713 714 const computeTrackHash = (track: Track): Effect.Effect<string, never> => 714 715 Effect.succeed( 715 716 createHash("sha256") 716 717 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) 717 - .digest("hex"), 718 + .digest("hex") 718 719 ); 719 720 720 721 const computeAlbumHash = (track: Track): Effect.Effect<string, never> => 721 722 Effect.succeed( 722 723 createHash("sha256") 723 724 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 724 - .digest("hex"), 725 + .digest("hex") 725 726 ); 726 727 727 728 const computeArtistHash = (track: Track): Effect.Effect<string, never> => 728 729 Effect.succeed( 729 - createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex"), 730 + createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 730 731 ); 731 732 732 733 const fetchExistingTrack = ( 733 734 ctx: Context, 734 - trackHash: string, 735 + trackHash: string 735 736 ): Effect.Effect<SelectTrack | undefined, Error> => 736 737 Effect.tryPromise(() => 737 738 ctx.db ··· 739 740 .from(tables.tracks) 740 741 .where(eq(tables.tracks.sha256, trackHash)) 741 742 .execute() 742 - .then(([row]) => row), 743 + .then(([row]) => row) 743 744 ); 744 745 745 746 // Update track metadata (album_uri and artist_uri) 746 747 const updateTrackMetadata = ( 747 748 ctx: Context, 748 749 track: Track, 749 - trackRecord: SelectTrack, 750 + trackRecord: SelectTrack 750 751 ) => 751 752 pipe( 752 753 Effect.succeed(trackRecord), ··· 761 762 .from(tables.albums) 762 763 .where(eq(tables.albums.sha256, albumHash)) 763 764 .execute() 764 - .then(([row]) => row), 765 - ), 765 + .then(([row]) => row) 766 + ) 766 767 ), 767 768 Effect.flatMap((album) => 768 769 album ··· 773 774 albumUri: album.uri, 774 775 }) 775 776 .where(eq(tables.tracks.id, trackRecord.id)) 776 - .execute(), 777 + .execute() 777 778 ) 778 - : Effect.succeed(undefined), 779 - ), 779 + : Effect.succeed(undefined) 780 + ) 780 781 ) 781 - : Effect.succeed(undefined), 782 + : Effect.succeed(undefined) 782 783 ), 783 784 Effect.tap((trackRecord) => 784 785 !trackRecord.artistUri ··· 791 792 .from(tables.artists) 792 793 .where(eq(tables.artists.sha256, artistHash)) 793 794 .execute() 794 - .then(([row]) => row), 795 - ), 795 + .then(([row]) => row) 796 + ) 796 797 ), 797 798 Effect.flatMap((artist) => 798 799 artist ··· 803 804 artistUri: artist.uri, 804 805 }) 805 806 .where(eq(tables.tracks.id, trackRecord.id)) 806 - .execute(), 807 + .execute() 807 808 ) 808 - : Effect.succeed(undefined), 809 - ), 809 + : Effect.succeed(undefined) 810 + ) 810 811 ) 811 - : Effect.succeed(undefined), 812 - ), 812 + : Effect.succeed(undefined) 813 + ) 813 814 ); 814 815 815 816 // Ensure track exists or create it ··· 818 819 track: Track, 819 820 agent: Agent, 820 821 userDid: string, 821 - existingTrack: SelectTrack | undefined, 822 + existingTrack: SelectTrack | undefined 822 823 ) => 823 824 pipe( 824 825 Effect.succeed(existingTrack), ··· 826 827 Match.value(trackOpt).pipe( 827 828 Match.when( 828 829 (value) => !!value, 829 - () => updateTrackMetadata(ctx, track, trackOpt), 830 + () => updateTrackMetadata(ctx, track, trackOpt) 830 831 ), 831 - Match.orElse(() => Effect.succeed(undefined)), 832 - ), 832 + Match.orElse(() => Effect.succeed(undefined)) 833 + ) 833 834 ), 834 835 Effect.flatMap((trackOpt) => 835 836 pipe( ··· 839 840 .from(tables.userTracks) 840 841 .leftJoin( 841 842 tables.tracks, 842 - eq(tables.userTracks.trackId, tables.tracks.id), 843 + eq(tables.userTracks.trackId, tables.tracks.id) 843 844 ) 844 845 .leftJoin( 845 846 tables.users, 846 - eq(tables.userTracks.userId, tables.users.id), 847 + eq(tables.userTracks.userId, tables.users.id) 847 848 ) 848 849 .where( 849 850 and( 850 851 eq(tables.tracks.id, trackOpt?.id), 851 - eq(tables.users.did, userDid), 852 - ), 852 + eq(tables.users.did, userDid) 853 + ) 853 854 ) 854 855 .execute() 855 - .then(([row]) => row.user_tracks), 856 + .then(([row]) => row.user_tracks) 856 857 ), 857 858 Effect.flatMap((userTrack) => 858 859 Option.isNone(Option.fromNullable(userTrack)) || 859 860 !userTrack?.uri?.includes(userDid) 860 861 ? putSongRecord(track, agent) 861 - : Effect.succeed(null), 862 - ), 863 - ), 864 - ), 862 + : Effect.succeed(null) 863 + ) 864 + ) 865 + ) 865 866 ); 866 867 867 868 // Ensure album exists or create it ··· 869 870 ctx: Context, 870 871 track: Track, 871 872 agent: Agent, 872 - userDid: string, 873 + userDid: string 873 874 ) => 874 875 pipe( 875 876 computeAlbumHash(track), ··· 880 881 .from(tables.albums) 881 882 .where(eq(tables.albums.sha256, albumHash)) 882 883 .execute() 883 - .then(([row]) => row), 884 - ), 884 + .then(([row]) => row) 885 + ) 885 886 ), 886 887 Effect.flatMap((existingAlbum) => 887 888 pipe( ··· 893 894 .from(tables.userAlbums) 894 895 .leftJoin( 895 896 tables.albums, 896 - eq(tables.userAlbums.albumId, tables.albums.id), 897 + eq(tables.userAlbums.albumId, tables.albums.id) 897 898 ) 898 899 .leftJoin( 899 900 tables.users, 900 - eq(tables.userAlbums.userId, tables.users.id), 901 + eq(tables.userAlbums.userId, tables.users.id) 901 902 ) 902 903 .where( 903 904 and( 904 905 eq(tables.albums.id, album.id), 905 - eq(tables.users.did, userDid), 906 - ), 906 + eq(tables.users.did, userDid) 907 + ) 907 908 ) 908 909 .execute() 909 - .then(([row]) => row.user_albums), 910 - ), 910 + .then(([row]) => row.user_albums) 911 + ) 911 912 ), 912 913 Effect.flatMap((userAlbum) => 913 914 Option.isNone(Option.fromNullable(existingAlbum)) || 914 915 Option.isNone(Option.fromNullable(userAlbum)) || 915 916 !userAlbum?.uri?.includes(userDid) 916 917 ? putAlbumRecord(track, agent) 917 - : Effect.succeed(null), 918 - ), 919 - ), 920 - ), 918 + : Effect.succeed(null) 919 + ) 920 + ) 921 + ) 921 922 ); 922 923 923 924 // Ensure artist exists or create it ··· 925 926 ctx: Context, 926 927 track: Track, 927 928 agent: Agent, 928 - userDid: string, 929 + userDid: string 929 930 ) => 930 931 pipe( 931 932 computeArtistHash(track), ··· 936 937 .from(tables.artists) 937 938 .where(eq(tables.artists.sha256, artistHash)) 938 939 .execute() 939 - .then(([row]) => row), 940 - ), 940 + .then(([row]) => row) 941 + ) 941 942 ), 942 943 Effect.flatMap((existingArtist) => 943 944 pipe( ··· 949 950 .from(tables.userArtists) 950 951 .leftJoin( 951 952 tables.artists, 952 - eq(tables.userArtists.artistId, tables.artists.id), 953 + eq(tables.userArtists.artistId, tables.artists.id) 953 954 ) 954 955 .leftJoin( 955 956 tables.users, 956 - eq(tables.userArtists.userId, tables.users.id), 957 + eq(tables.userArtists.userId, tables.users.id) 957 958 ) 958 959 .where( 959 960 and( 960 961 eq(tables.artists.id, artist.id), 961 - eq(tables.users.did, userDid), 962 - ), 962 + eq(tables.users.did, userDid) 963 + ) 963 964 ) 964 965 .execute() 965 - .then(([row]) => row.user_artists), 966 - ), 966 + .then(([row]) => row.user_artists) 967 + ) 967 968 ), 968 969 Effect.flatMap((userArtist) => 969 970 Effect.if( ··· 973 974 { 974 975 onTrue: () => putArtistRecord(track, agent), 975 976 onFalse: () => Effect.succeed(null), 976 - }, 977 - ), 978 - ), 979 - ), 980 - ), 977 + } 978 + ) 979 + ) 980 + ) 981 + ) 981 982 ); 982 983 983 984 // Retry fetching track until metadata is ready 984 985 const retryFetchTrack = ( 985 986 ctx: Context, 986 987 trackHash: string, 987 - initialTrack: SelectTrack | undefined, 988 + initialTrack: SelectTrack | undefined 988 989 ) => 989 990 pipe( 990 991 Effect.iterate( ··· 1000 1001 .from(tables.tracks) 1001 1002 .where(eq(tables.tracks.sha256, trackHash)) 1002 1003 .execute() 1003 - .then(([row]) => row), 1004 + .then(([row]) => row) 1004 1005 ), 1005 1006 Effect.flatMap((trackRecord) => 1006 1007 Option.fromNullable(trackRecord).pipe( 1007 1008 Effect.flatMap((track) => 1008 - updateTrackMetadata(ctx, track, trackRecord), 1009 - ), 1010 - ), 1009 + updateTrackMetadata(ctx, track, trackRecord) 1010 + ) 1011 + ) 1011 1012 ), 1012 1013 Effect.tap((trackRecord) => 1013 1014 Effect.logInfo( 1014 1015 trackRecord 1015 1016 ? `Track metadata ready: ${chalk.cyan(trackRecord.id)} - ${track.title}, after ${chalk.magenta(tries + 1)} tries` 1016 - : `Retrying track fetch: ${chalk.magenta(tries + 1)}`, 1017 - ), 1017 + : `Retrying track fetch: ${chalk.magenta(tries + 1)}` 1018 + ) 1018 1019 ), 1019 1020 Effect.map((trackRecord) => ({ 1020 1021 tries: tries + 1, 1021 1022 track: trackRecord, 1022 1023 })), 1023 - Effect.delay("1 second"), 1024 + Effect.delay("1 second") 1024 1025 ), 1025 - }, 1026 + } 1026 1027 ), 1027 1028 Effect.tap(({ tries, track }) => 1028 1029 tries >= 30 && !(track?.artistUri && track?.albumUri) 1029 1030 ? Effect.logError( 1030 - `Track metadata not ready after ${chalk.magenta("30 tries")}`, 1031 + `Track metadata not ready after ${chalk.magenta("30 tries")}` 1031 1032 ) 1032 - : Effect.succeed(undefined), 1033 + : Effect.succeed(undefined) 1033 1034 ), 1034 - Effect.map(({ track }) => track), 1035 + Effect.map(({ track }) => track) 1035 1036 ); 1036 1037 1037 1038 // Retry fetching scrobble until complete ··· 1069 1070 .from(tables.scrobbles) 1070 1071 .leftJoin( 1071 1072 tables.tracks, 1072 - eq(tables.scrobbles.trackId, tables.tracks.id), 1073 + eq(tables.scrobbles.trackId, tables.tracks.id) 1073 1074 ) 1074 1075 .leftJoin( 1075 1076 tables.albums, 1076 - eq(tables.scrobbles.albumId, tables.albums.id), 1077 + eq(tables.scrobbles.albumId, tables.albums.id) 1077 1078 ) 1078 1079 .leftJoin( 1079 1080 tables.artists, 1080 - eq(tables.scrobbles.artistId, tables.artists.id), 1081 + eq(tables.scrobbles.artistId, tables.artists.id) 1081 1082 ) 1082 1083 .leftJoin( 1083 1084 tables.users, 1084 - eq(tables.scrobbles.userId, tables.users.id), 1085 + eq(tables.scrobbles.userId, tables.users.id) 1085 1086 ) 1086 1087 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1087 1088 .execute() 1088 - .then(([row]) => row), 1089 + .then(([row]) => row) 1089 1090 ), 1090 1091 Effect.tap((scrobble) => 1091 1092 Effect.if( ··· 1102 1103 artistUri: scrobble.artists.uri, 1103 1104 }) 1104 1105 .where(eq(tables.albums.id, scrobble.albums.id)) 1105 - .execute(), 1106 + .execute() 1106 1107 ), 1107 1108 onFalse: () => Effect.succeed(undefined), 1108 - }, 1109 - ), 1109 + } 1110 + ) 1110 1111 ), 1111 1112 Effect.flatMap(() => 1112 1113 Effect.tryPromise(() => ··· 1115 1116 .from(tables.scrobbles) 1116 1117 .leftJoin( 1117 1118 tables.tracks, 1118 - eq(tables.scrobbles.trackId, tables.tracks.id), 1119 + eq(tables.scrobbles.trackId, tables.tracks.id) 1119 1120 ) 1120 1121 .leftJoin( 1121 1122 tables.albums, 1122 - eq(tables.scrobbles.albumId, tables.albums.id), 1123 + eq(tables.scrobbles.albumId, tables.albums.id) 1123 1124 ) 1124 1125 .leftJoin( 1125 1126 tables.artists, 1126 - eq(tables.scrobbles.artistId, tables.artists.id), 1127 + eq(tables.scrobbles.artistId, tables.artists.id) 1127 1128 ) 1128 1129 .leftJoin( 1129 1130 tables.users, 1130 - eq(tables.scrobbles.userId, tables.users.id), 1131 + eq(tables.scrobbles.userId, tables.users.id) 1131 1132 ) 1132 1133 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1133 1134 .execute() 1134 - .then(([row]) => row), 1135 - ), 1135 + .then(([row]) => row) 1136 + ) 1136 1137 ), 1137 1138 Effect.map((scrobble) => ({ 1138 1139 tries: tries + 1, ··· 1149 1150 scrobble.tracks.albumUri && 1150 1151 scrobble.scrobbles 1151 1152 ? `Scrobble found after ${chalk.magenta(tries + 1)} tries` 1152 - : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}`, 1153 - ), 1153 + : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}` 1154 + ) 1154 1155 ), 1155 - Effect.delay("1 second"), 1156 + Effect.delay("1 second") 1156 1157 ), 1157 - }, 1158 + } 1158 1159 ), 1159 1160 Effect.tap(({ tries, scrobble }) => 1160 1161 tries >= 30 && ··· 1168 1169 scrobble.tracks.albumUri 1169 1170 ) 1170 1171 ? Effect.logError( 1171 - `Scrobble not found after ${chalk.magenta("30 tries")}`, 1172 + `Scrobble not found after ${chalk.magenta("30 tries")}` 1172 1173 ) 1173 - : Effect.succeed(undefined), 1174 + : Effect.succeed(undefined) 1174 1175 ), 1175 - Effect.map(({ scrobble }) => scrobble), 1176 + Effect.map(({ scrobble }) => scrobble) 1176 1177 ); 1177 1178 1178 1179 export const scrobbleTrack = ( 1179 1180 ctx: Context, 1180 1181 track: Track, 1181 1182 agent: Agent, 1182 - userDid: string, 1183 + userDid: string 1183 1184 ) => 1184 1185 pipe( 1185 1186 computeTrackHash(track), ··· 1192 1193 Effect.flatMap(() => ensureAlbum(ctx, track, agent, userDid)), 1193 1194 Effect.flatMap(() => ensureArtist(ctx, track, agent, userDid)), 1194 1195 Effect.flatMap(() => 1195 - retryFetchTrack(ctx, trackHash, existingTrack), 1196 + retryFetchTrack(ctx, trackHash, existingTrack) 1196 1197 ), 1197 1198 Effect.flatMap(() => 1198 1199 pipe( ··· 1212 1213 ? pipe( 1213 1214 publishScrobble(ctx, scrobble.scrobbles.id), 1214 1215 Effect.tap(() => 1215 - Effect.logInfo("Scrobble published"), 1216 - ), 1216 + Effect.logInfo("Scrobble published") 1217 + ) 1217 1218 ) 1218 - : Effect.succeed(undefined), 1219 - ), 1220 - ), 1221 - ), 1222 - ), 1223 - ), 1224 - ), 1225 - ), 1226 - ), 1227 - ), 1219 + : Effect.succeed(undefined) 1220 + ) 1221 + ) 1222 + ) 1223 + ) 1224 + ) 1225 + ) 1226 + ) 1227 + ) 1228 + ) 1228 1229 );
+4 -2
crates/jetstream/src/repo.rs
··· 409 409 name, 410 410 sha256, 411 411 uri, 412 - picture 412 + picture, 413 + genres 413 414 ) VALUES ( 414 - $1, $2, $3, $4 415 + $1, $2, $3, $4, $5 415 416 ) 416 417 "#, 417 418 ) ··· 419 420 .bind(&hash) 420 421 .bind(uri) 421 422 .bind(picture) 423 + .bind(scrobble_record.tags) 422 424 .execute(&mut **tx) 423 425 .await?; 424 426
+1
crates/jetstream/src/xata/artist.rs
··· 18 18 pub youtube_link: Option<String>, 19 19 pub apple_music_link: Option<String>, 20 20 pub uri: Option<String>, 21 + pub genres: Option<Vec<String>>, 21 22 #[serde(with = "chrono::serde::ts_seconds")] 22 23 pub xata_createdat: DateTime<Utc>, 23 24 }
+1
crates/scrobbler/src/spotify/types.rs
··· 76 76 pub kind: String, 77 77 pub uri: String, 78 78 pub images: Option<Vec<Image>>, 79 + pub genres: Option<Vec<String>>, 79 80 } 80 81 81 82 #[derive(Debug, Deserialize, Clone)]
+7
crates/scrobbler/src/types.rs
··· 39 39 pub label: Option<String>, 40 40 pub artist_picture: Option<String>, 41 41 pub timestamp: Option<u64>, 42 + pub genres: Option<Vec<String>>, 42 43 } 43 44 44 45 impl From<xata::track::Track> for Track { ··· 59 60 disc_number: track.disc_number as u32, 60 61 year: None, 61 62 release_date: None, 63 + genres: None, 62 64 } 63 65 } 64 66 } ··· 173 175 _ => None, 174 176 }, 175 177 label: track.album.label.clone(), 178 + genres: track 179 + .album 180 + .artists 181 + .first() 182 + .and_then(|artist| artist.genres.clone()), 176 183 ..Default::default() 177 184 } 178 185 }
+6 -2
crates/spotify/src/rocksky.rs
··· 71 71 None => None, 72 72 }, 73 73 "label": track_item.album.label, 74 - "artistPicture": match artist { 75 - Some(artist) => match artist.images { 74 + "artistPicture": match &artist { 75 + Some(artist) => match &artist.images { 76 76 Some(images) => Some(images.first().map(|image| image.url.clone())), 77 77 None => None 78 78 }, 79 + None => None 80 + }, 81 + "genres": match &artist { 82 + Some(artist) => artist.genres.clone(), 79 83 None => None 80 84 }, 81 85 }))
+1
crates/spotify/src/types/currently_playing.rs
··· 95 95 pub artist_type: String, 96 96 pub uri: String, 97 97 pub images: Option<Vec<Image>>, 98 + pub genres: Option<Vec<String>>, 98 99 } 99 100 100 101 #[derive(Debug, Serialize, Deserialize, Clone)]
+1
crates/webscrobbler/src/spotify/types.rs
··· 76 76 pub kind: String, 77 77 pub uri: String, 78 78 pub images: Option<Vec<Image>>, 79 + pub genres: Option<Vec<String>>, 79 80 } 80 81 81 82 #[derive(Debug, Deserialize, Clone)]
+7
crates/webscrobbler/src/types.rs
··· 131 131 pub label: Option<String>, 132 132 pub artist_picture: Option<String>, 133 133 pub timestamp: Option<u64>, 134 + pub genres: Option<Vec<String>>, 134 135 } 135 136 136 137 impl From<xata::track::Track> for Track { ··· 151 152 disc_number: track.disc_number as u32, 152 153 year: None, 153 154 release_date: None, 155 + genres: None, 154 156 } 155 157 } 156 158 } ··· 268 270 _ => None, 269 271 }, 270 272 label: track.album.label.clone(), 273 + genres: track 274 + .album 275 + .artists 276 + .first() 277 + .and_then(|artist| artist.genres.clone()), 271 278 ..Default::default() 272 279 } 273 280 }