From a8807d94cd4cb458358be71a3e7d9d67e3b2df88 Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 18:44:16 -0400 Subject: [PATCH 01/38] r moving dtos file to corresponding file and deleting scripts --- back/scripts/addData/Typesong.ts | 25 ---- back/scripts/addData/addAlbums.ts | 24 ---- back/scripts/addData/addArtists.ts | 23 ---- back/scripts/addData/addGenres.ts | 27 ---- back/scripts/addData/addSongArtists.ts | 39 ------ back/scripts/addData/addSongDetails.ts | 41 ------ back/scripts/addData/addSongGenres.ts | 38 ------ back/scripts/addData/addSongs.ts | 46 ------- back/scripts/addData/albums.ts | 26 ---- back/scripts/addData/artists.ts | 22 ---- back/scripts/addData/dbConnection.ts | 24 ---- back/scripts/addData/genres.ts | 22 ---- back/scripts/addData/getCSVData.ts | 24 ---- back/scripts/addData/index.ts | 19 --- back/scripts/addData/song.ts | 53 -------- back/scripts/addData/songArtists.ts | 28 ----- back/scripts/addData/songDetails.ts | 118 ------------------ back/scripts/addData/songGenres.ts | 27 ---- back/scripts/addData/utils/formatTag.ts | 6 - back/scripts/bonsai/bonsaiClient.ts | 13 -- back/scripts/dto/multipleSearchDto.ts | 60 ++++++++- back/scripts/migration/migrations.ts | 24 ---- back/scripts/migration/rollback.js | 3 - back/src/albums/albums.resolver.ts | 2 +- back/src/artists/artists.resolver.ts | 4 +- .../artists}/dto/artistsResults.dto.ts | 0 .../search}/dto/albumsSearchDto.ts | 0 .../search}/dto/artistsSearchDto.ts | 0 back/src/search/dto/multipleSearchDto.ts | 25 ++++ .../search}/dto/songsSearchDto.ts | 0 back/src/search/search.module.ts | 14 ++- back/src/search/search.resolver.ts | 2 +- back/src/song_genres/song_genres.resolver.ts | 2 +- .../songs}/dto/FullSongResponse.dto.ts | 0 .../songs}/dto/SongResponse.dto.ts | 0 back/src/songs/songs.resolver.ts | 4 +- 36 files changed, 101 insertions(+), 684 deletions(-) delete mode 100644 back/scripts/addData/Typesong.ts delete mode 100644 back/scripts/addData/addAlbums.ts delete mode 100644 back/scripts/addData/addArtists.ts delete mode 100644 back/scripts/addData/addGenres.ts delete mode 100644 back/scripts/addData/addSongArtists.ts delete mode 100644 back/scripts/addData/addSongDetails.ts delete mode 100644 back/scripts/addData/addSongGenres.ts delete mode 100644 back/scripts/addData/addSongs.ts delete mode 100644 back/scripts/addData/albums.ts delete mode 100644 back/scripts/addData/artists.ts delete mode 100644 back/scripts/addData/dbConnection.ts delete mode 100644 back/scripts/addData/genres.ts delete mode 100644 back/scripts/addData/getCSVData.ts delete mode 100644 back/scripts/addData/index.ts delete mode 100644 back/scripts/addData/song.ts delete mode 100644 back/scripts/addData/songArtists.ts delete mode 100644 back/scripts/addData/songDetails.ts delete mode 100644 back/scripts/addData/songGenres.ts delete mode 100644 back/scripts/addData/utils/formatTag.ts delete mode 100644 back/scripts/bonsai/bonsaiClient.ts delete mode 100644 back/scripts/migration/migrations.ts delete mode 100644 back/scripts/migration/rollback.js rename back/{scripts => src/artists}/dto/artistsResults.dto.ts (100%) rename back/{scripts => src/search}/dto/albumsSearchDto.ts (100%) rename back/{scripts => src/search}/dto/artistsSearchDto.ts (100%) create mode 100644 back/src/search/dto/multipleSearchDto.ts rename back/{scripts => src/search}/dto/songsSearchDto.ts (100%) rename back/{scripts => src/songs}/dto/FullSongResponse.dto.ts (100%) rename back/{scripts => src/songs}/dto/SongResponse.dto.ts (100%) diff --git a/back/scripts/addData/Typesong.ts b/back/scripts/addData/Typesong.ts deleted file mode 100644 index b27637e..0000000 --- a/back/scripts/addData/Typesong.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface TypeSong { - track_id:number; - name:string; - artist:string; - album_image_url:string; - album_name:string; - spotify_preview_url:string; - spotify_id:string; - tags:string[]; - genre:string[]; - year:number; - duration_ms:number; - danceability:number; - energy:number; - track_key:number; - loudness:number; - mode:number; - speechiness:number; - acousticness:number; - instrumentalness:number; - liveness:number; - valence:number; - tempo:number; - time_signature:number -}; \ No newline at end of file diff --git a/back/scripts/addData/addAlbums.ts b/back/scripts/addData/addAlbums.ts deleted file mode 100644 index 83ece20..0000000 --- a/back/scripts/addData/addAlbums.ts +++ /dev/null @@ -1,24 +0,0 @@ -import getCSVData from "./getCSVData"; -import { Albums } from "./albums"; - -const addAlbums = async () => { - const csvData = await getCSVData(1, 50230); - const AlbumMap = new Map(); - - for await (const song of csvData) { - if (!AlbumMap.has(song.album_name)) { - const albumObj = { name: song.album_name, url_image: song.album_image_url } - AlbumMap.set(song.album_name, albumObj); - } - }; - - const uniqueAlbums = Array.from(AlbumMap.values()); - try { - await Albums.bulkCreate(uniqueAlbums); - console.log("Albums added without issue!"); - } catch (err) { - console.error("There was an error trying to add the album to the DB,", err.message); - }; -}; - -addAlbums(); \ No newline at end of file diff --git a/back/scripts/addData/addArtists.ts b/back/scripts/addData/addArtists.ts deleted file mode 100644 index 51ca664..0000000 --- a/back/scripts/addData/addArtists.ts +++ /dev/null @@ -1,23 +0,0 @@ -import getCSVData from "./getCSVData"; -import { Artists } from "./artists"; - -const addArtists = async () => { - const csvData = await getCSVData(1, 50230); - const artistSet = new Set(); - - for (const row of csvData) { - row.artist.split(",").forEach(art => artistSet.add(art.trim())); - }; - - const uniqueArtists: string[] = [...artistSet]; - const UArtistsObj = uniqueArtists.map(name => ({ name })); - - try { - await Artists.bulkCreate(UArtistsObj); - console.log("All Artists added without issue!"); - } catch (err) { - console.error("There was an error trying to add the Artists ", err); - }; -}; - -addArtists(); \ No newline at end of file diff --git a/back/scripts/addData/addGenres.ts b/back/scripts/addData/addGenres.ts deleted file mode 100644 index 3ab82df..0000000 --- a/back/scripts/addData/addGenres.ts +++ /dev/null @@ -1,27 +0,0 @@ -import getCSVData from "./getCSVData"; -import { Genres } from "./genres"; -import { formatTag } from "./utils/formatTag"; - -const addGenres = async () => { - const csvData = await getCSVData(1, 50230); - const genreSet = new Set(); - - for (const row of csvData) { - row.tags.forEach(tag => { - const formatted = formatTag(tag); - if (formatted !== "") genreSet.add(formatted); - }); - } - - const uniqueGenres = [...genreSet]; - const UGenresObj = uniqueGenres.map(genre => ({ genre })); - - try { - await Genres.bulkCreate(UGenresObj); - console.log("All Genres added successfully!") - } catch (err) { - console.error("There was an error trying to add the genres", err) - }; -}; - -addGenres() \ No newline at end of file diff --git a/back/scripts/addData/addSongArtists.ts b/back/scripts/addData/addSongArtists.ts deleted file mode 100644 index c6b8c7c..0000000 --- a/back/scripts/addData/addSongArtists.ts +++ /dev/null @@ -1,39 +0,0 @@ -import getCSVData from "./getCSVData" -import { Songs } from "./song"; -import { Artists } from "./artists"; -import { SongArtists } from "./songArtists"; -import { SongArtistsCreationAttributes } from "../../src/types/songArtistsAttributes"; - -const addSongArtists = async () => { - const csvData = await getCSVData(1,50230); - const USongArtistsData: SongArtistsCreationAttributes[] = [] - - const songData = (await Songs.findAll()).map(songIns => songIns.get({ plain: true })); - const songMap = new Map(songData.map(song => [song.name, song.id])); - - const artistData = (await Artists.findAll()).map(songIns => songIns.get({ plain: true })); - const artistMap = new Map(artistData.map(artist => [artist.name, artist.id])); - - for (const song of csvData) { - const songID = songMap.get(song.name); - if (!songID) throw new Error(`There was an error with the ID "${songID}" of the song: "${song.name}"`) - - const songArtists: string[] = song.artist.split(",").map(artist => artist.trim()); - const artistIDs = songArtists.map(artist => { - const ID = artistMap.get(artist); - if (!ID) throw new Error(`There was an error with the artist ID "${ID}" of the song: "${song.name}"`) - return ID; - }); - - artistIDs.forEach(artist_id => USongArtistsData.push({ song_id: songID, artist_id })); - }; - - try { - await SongArtists.bulkCreate(USongArtistsData); - console.log("The RP between songs and artists was sucessfully accomplished!"); - } catch (err) { - console.error("There was an error trying to establish the RP:", err.message); - }; -}; - -addSongArtists() \ No newline at end of file diff --git a/back/scripts/addData/addSongDetails.ts b/back/scripts/addData/addSongDetails.ts deleted file mode 100644 index 15931b2..0000000 --- a/back/scripts/addData/addSongDetails.ts +++ /dev/null @@ -1,41 +0,0 @@ -import getCSVData from "./getCSVData"; -import { Songs } from "./song"; -import { songDetails } from "./songDetails"; -import { SongDetailsCreationAttributes } from "../../src/types/songDetailsAttributes"; - -const addSongDetails = async () => { - const csvData = await getCSVData(1, 50230); - const USongDetailData: SongDetailsCreationAttributes[] = []; - const songs = (await Songs.findAll()).map(songIns => songIns.get({ plain: true })); - const songsMap = new Map(songs.map(song => [song.name, song.id])); - - for (const song of csvData) { - const SongID = songsMap.get(song.name); - if (!SongID) throw new Error (`Couldnt find the song ID: "${SongID}" for ${song.name}`); - - USongDetailData.push({ - song_id: SongID, - danceability: song.danceability, - energy: song.energy, - track_key: song.track_key, - loudness: song.loudness, - mode: song.mode, - speechiness: song.speechiness, - acousticness: song.acousticness, - instrumentalness: song.instrumentalness, - liveness: song.liveness, - valence: song.valence, - tempo: song.tempo, - time_signature: song.time_signature, - }); - }; - - try { - await songDetails.bulkCreate(USongDetailData); - console.log("All the song details were added sucessfully!"); - } catch (err) { - console.error("There was an error trying to add the details", err.message); - }; -}; - -addSongDetails(); \ No newline at end of file diff --git a/back/scripts/addData/addSongGenres.ts b/back/scripts/addData/addSongGenres.ts deleted file mode 100644 index 37046a9..0000000 --- a/back/scripts/addData/addSongGenres.ts +++ /dev/null @@ -1,38 +0,0 @@ -import getCSVData from "./getCSVData"; -import { Songs } from "./song"; -import { Genres } from "./genres"; -import { songGenres } from "./songGenres"; -import { SongGenresCreationAttributes } from "../../src/types/songGenresAttributes"; - -const addSongGenres = async () => { - const csvData = await getCSVData(1, 50230); - const USongGenresData: SongGenresCreationAttributes[] = []; - - const songs = (await Songs.findAll()).map(songIns => songIns.get({ plain: true })); - const songMap = new Map(songs.map(song => [song.name, song.id ])); - - const genres = (await Genres.findAll()).map(genreIns => genreIns.get({ plain: true })); - const genreMap = new Map(genres.map(genre => [genre.genre, genre.id])); - - for (const song of csvData) { - const songID = songMap.get(song.name); - if (!songID) throw new Error (`Couldnt get the songID: "${songID}" of the song ${song.name}`); - - const genresId = song.tags.map(genre => { - const ID = genreMap.get(genre); - if (!ID) throw new Error (`Couldnt get the genreID: "${songID}" of the song ${song.name}`); - return ID; - }); - - genresId.forEach(genre_id => USongGenresData.push({ song_id: songID, genre_id })); - }; - - try { - await songGenres.bulkCreate(USongGenresData); - console.log("The RP between the songs and the genres was successful!"); - } catch (err) { - console.error("There was an error trying to get the RP", err.message); - }; -}; - -addSongGenres(); \ No newline at end of file diff --git a/back/scripts/addData/addSongs.ts b/back/scripts/addData/addSongs.ts deleted file mode 100644 index 4907bde..0000000 --- a/back/scripts/addData/addSongs.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { SongCreationAttributes } from "../../src/types/songAttributes"; -import getCSVData from "./getCSVData" -import { Albums } from "./albums"; -import { Songs } from "./song"; -import { AlbumAttributes } from "../../src/types/albumAttributes"; - -const addSongs = async () => { - const csvData = await getCSVData(1, 50230); - const uniqueSongs: SongCreationAttributes[] = []; - const albums: AlbumAttributes[] = (await Albums.findAll()).map(album => album.get({ plain: true }) ); - const albumMap = new Map(albums.map(album => [album.name, album.id])); - - for (const song of csvData) { - const albumID = albumMap.get(song.album_name); - if (!albumID) throw new Error(`Album not found: ${song.album_name}`); - - const duration = Math.round((song.duration_ms / 60000) * 100) / 100; - - if (duration < 0 || duration > 500) { - throw new Error(`Invalid duration (${duration}) for song: ${song.name}`); - } - - if (!song.name || !song.spotify_id || !song.year) { - throw new Error(`Missing essential data for song: ${song.name}`); - } - - uniqueSongs.push({ - name: song.name, - spotify_id: song.spotify_id, - url_preview: song.spotify_preview_url, - year: song.year, - duration, - album_id: albumID - }); - }; - - try { - await Songs.bulkCreate(uniqueSongs); - console.log("All songs added sucessfully!"); - } catch (err) { - console.error("There was an error trying to add the Songs", err.message); - console.error(err.stack); - }; -}; - -addSongs(); \ No newline at end of file diff --git a/back/scripts/addData/albums.ts b/back/scripts/addData/albums.ts deleted file mode 100644 index 71630f7..0000000 --- a/back/scripts/addData/albums.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { AlbumAttributes, AlbumCreationAttributes } from "../../src/types/albumAttributes"; - -export class Albums extends Model {}; -Albums.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - url_image: { - type: DataTypes.STRING, - allowNull: false, - } -}, { - sequelize, - underscored: true, - timestamps: false, - modelName: "albums" -}); \ No newline at end of file diff --git a/back/scripts/addData/artists.ts b/back/scripts/addData/artists.ts deleted file mode 100644 index 2f5cf74..0000000 --- a/back/scripts/addData/artists.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { ArtistsAttributtes, ArtistsCreationAttributtes } from "../../src/types/artistAttributes"; - -export class Artists extends Model {} -Artists.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - } -}, { - sequelize, - timestamps: false, - underscored: true, - modelName: "artists" -}); \ No newline at end of file diff --git a/back/scripts/addData/dbConnection.ts b/back/scripts/addData/dbConnection.ts deleted file mode 100644 index f5a7e1d..0000000 --- a/back/scripts/addData/dbConnection.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Sequelize } from "sequelize"; -import dotenv from "dotenv"; dotenv.config(); - -const DB_URL = process.env.LOCAL_DB_URL; - -if (!DB_URL) { - console.log(DB_URL) - throw new Error("The url is not defined!"); -}; - -export const sequelize = new Sequelize(DB_URL); - -export const connectToDatabase = async () => { - try { - await sequelize.authenticate(); - console.log("Connected to the DB!"); - } catch (err) { - console.error("❌ Failed to connect to the DB"); - console.error(err); - return process.exit(1); - }; - - return null; -}; \ No newline at end of file diff --git a/back/scripts/addData/genres.ts b/back/scripts/addData/genres.ts deleted file mode 100644 index 471def9..0000000 --- a/back/scripts/addData/genres.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { GenresAttributes, GenresCreationAttributtes } from "../../src/types/genreAttributes"; - -export class Genres extends Model {} -Genres.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - genre: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, -}, { - sequelize, - underscored: true, - timestamps: false, - modelName: "genres" -}) \ No newline at end of file diff --git a/back/scripts/addData/getCSVData.ts b/back/scripts/addData/getCSVData.ts deleted file mode 100644 index ef5d161..0000000 --- a/back/scripts/addData/getCSVData.ts +++ /dev/null @@ -1,24 +0,0 @@ -import fs from "fs"; -import csv from "csv-parser" -import { Readable } from "stream"; -import { formatTag } from "./utils/formatTag"; -import { TypeSong } from "./Typesong"; - -const getCSVData = async (startRow: number, endRow: number): Promise => { - const csvResults: TypeSong[] = []; - const stream = fs.createReadStream("full_songs_DB.csv").pipe(csv()); - let currentRow = 0; - - for await (const row of Readable.from(stream)) { - currentRow++ - - if (currentRow < startRow) continue; - if (currentRow > endRow) break; - - csvResults.push({ ...row, tags: row.tags.split(",").map((tag: string) => formatTag(tag.trim())) }); - }; - - return csvResults; -} - -export default getCSVData; diff --git a/back/scripts/addData/index.ts b/back/scripts/addData/index.ts deleted file mode 100644 index 36d1bd2..0000000 --- a/back/scripts/addData/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Albums } from "./albums"; -import { Artists } from "./artists"; -import { Genres } from "./genres"; -import { Songs } from "./song"; -import { SongArtists } from "./songArtists"; -import { songDetails } from "./songDetails"; -import { songGenres } from "./songGenres"; - -Songs.hasOne(songDetails, { foreignKey: "song_id", as: "details" }); -songDetails.belongsTo(Songs, { foreignKey: "song_id" }); - -Albums.hasMany(Songs, { foreignKey: "album_id" }); -Songs.belongsTo(Albums, { foreignKey: "album_id" }); - -Songs.belongsToMany(Genres, { through: songGenres, as: "song_genres" }); -Genres.belongsToMany(Songs, { through: songGenres, as: "track_genres" }); - -Songs.belongsToMany(Artists, { through: SongArtists, as: "song_artists" }); -Artists.belongsToMany(Songs, { through: SongArtists, as:"artists_tracks" }); \ No newline at end of file diff --git a/back/scripts/addData/song.ts b/back/scripts/addData/song.ts deleted file mode 100644 index 075fcd7..0000000 --- a/back/scripts/addData/song.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { SongAttributes, SongCreationAttributes } from "../../src/types/songAttributes"; - -export class Songs extends Model {} -Songs.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - spotify_id: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - url_preview: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - duration: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 6 - } - }, - year: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 1980, - max: 2025 - } - }, - album_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "albums", key: "id" } - }, -}, { - sequelize, - underscored: true, - timestamps: false, - modelName: "songs" -}) \ No newline at end of file diff --git a/back/scripts/addData/songArtists.ts b/back/scripts/addData/songArtists.ts deleted file mode 100644 index 880f431..0000000 --- a/back/scripts/addData/songArtists.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { SongArtistsAttributes, SongArtistsCreationAttributes } from "../../src/types/songArtistsAttributes"; - -export class SongArtists extends Model {} -SongArtists.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - artist_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "artists", key: "id" } - } -}, { - sequelize, - underscored: true, - timestamps: false, - modelName: "songArtists", - tableName: "song_artists_rp" -}) \ No newline at end of file diff --git a/back/scripts/addData/songDetails.ts b/back/scripts/addData/songDetails.ts deleted file mode 100644 index 244b5e9..0000000 --- a/back/scripts/addData/songDetails.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; -import { SongDetailsAttributes, SongDetailsCreationAttributes } from "../../src/types/songDetailsAttributes"; - -export class songDetails extends Model {} -songDetails.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - danceability: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - energy: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - track_key: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 0, - max: 11 - } - }, - loudness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: -60, - max: 0 - } - }, - mode: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - speechiness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - acousticness: { - type: DataTypes.DECIMAL(10, 6), - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - instrumentalness: { - type: DataTypes.DECIMAL(10, 6), - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - liveness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - valence: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - tempo: { - type: DataTypes.DECIMAL(6, 3), - allowNull: false, - validate: { - min: 30, - max: 300 - } - }, - time_signature: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 1, - max: 12 - } - }, -}, { - sequelize, - underscored: true, - timestamps: false, - modelName: "song_details" -}) \ No newline at end of file diff --git a/back/scripts/addData/songGenres.ts b/back/scripts/addData/songGenres.ts deleted file mode 100644 index 2050a74..0000000 --- a/back/scripts/addData/songGenres.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DataTypes, Model } from "sequelize"; -import { sequelize } from "./dbConnection"; - -export class songGenres extends Model{} -songGenres.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - genre_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "genres", key: "id" } - }, -}, { - sequelize, - timestamps: false, - underscored: true, - modelName: "songGenres", - tableName: "song_genres_rp" -}) \ No newline at end of file diff --git a/back/scripts/addData/utils/formatTag.ts b/back/scripts/addData/utils/formatTag.ts deleted file mode 100644 index 3afb958..0000000 --- a/back/scripts/addData/utils/formatTag.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const formatTag = (tag: string) => { - const formatted = tag.trim().replace(/_/g," ").split(" ").map((word: string) => word.charAt(0).toUpperCase() + - word.slice(1).toLocaleLowerCase()).join(" "); - - return formatted.trim(); -}; \ No newline at end of file diff --git a/back/scripts/bonsai/bonsaiClient.ts b/back/scripts/bonsai/bonsaiClient.ts deleted file mode 100644 index e792be1..0000000 --- a/back/scripts/bonsai/bonsaiClient.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Client } from "elasticsearch"; -import dotenv from "dotenv"; dotenv.config(); - -export const bonsaiClientProvider = { - provide: "BonsaiClient", - useFactory: async () => { - return new Client({ - host: process.env.ELASTICSEARCH_NODE, - log: "error", - ssl: { rejectUnauthorized: false } - }) - } -}; \ No newline at end of file diff --git a/back/scripts/dto/multipleSearchDto.ts b/back/scripts/dto/multipleSearchDto.ts index 1f602b2..29c1044 100644 --- a/back/scripts/dto/multipleSearchDto.ts +++ b/back/scripts/dto/multipleSearchDto.ts @@ -1,7 +1,4 @@ import { Field, ObjectType } from "@nestjs/graphql"; -import { artistsSearchDto } from "./artistsSearchDto"; -import { albumsSearchDto } from "./albumsSearchDto"; -import { searchSongsDto } from "./songsSearchDto"; @ObjectType() export class multipleSearchResultsDto { @@ -22,4 +19,61 @@ export class multipleSearchResultsDto { @Field(() => [searchSongsDto]) songResults: searchSongsDto[]; +}; + +@ObjectType() +export class albumsSearchDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field() + album_cover: string; + + @Field(() => String) + type: "album"; +}; + +@ObjectType() +export class artistsSearchDto { + @Field() + id: number; + + @Field() + name: string; + + @Field() + album_cover: string; + + @Field(() => String) + type: "artist"; +}; + +@ObjectType() +export class searchSongsDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field() + album: string; + + @Field() + album_cover: string; + + @Field() + url_preview: string; + + @Field(() => String) + type: "song"; }; \ No newline at end of file diff --git a/back/scripts/migration/migrations.ts b/back/scripts/migration/migrations.ts deleted file mode 100644 index 7b6845e..0000000 --- a/back/scripts/migration/migrations.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { sequelize } from "../addData/dbConnection"; -import { Umzug, SequelizeStorage } from "umzug"; - -const migrationConf = { - migrations: { - glob: "migrations/*.ts" - }, - storage: new SequelizeStorage({ sequelize, tableName: "migrations" }), - context: sequelize.getQueryInterface(), - logger: console -}; - -export const runMigrations = async () => { - const migrator = new Umzug(migrationConf); - const migrations = await migrator.up(); - - console.log("Migrations up to date", { files: migrations.map((mig)=> mig.name) }); -}; - -export const rollbackMigration = async () => { - await sequelize.authenticate(); - const migrator = new Umzug(migrationConf); - await migrator.down(); -}; \ No newline at end of file diff --git a/back/scripts/migration/rollback.js b/back/scripts/migration/rollback.js deleted file mode 100644 index 06b5d79..0000000 --- a/back/scripts/migration/rollback.js +++ /dev/null @@ -1,3 +0,0 @@ -import { rollbackMigration } from "./migrations"; - -rollbackMigration(); \ No newline at end of file diff --git a/back/src/albums/albums.resolver.ts b/back/src/albums/albums.resolver.ts index bac9c8f..a0c15ff 100644 --- a/back/src/albums/albums.resolver.ts +++ b/back/src/albums/albums.resolver.ts @@ -1,7 +1,7 @@ import { Args, Query, Resolver } from '@nestjs/graphql'; import { AlbumsService } from './albums.service'; import { GraphQLString } from 'graphql'; -import { SongResponseDto } from '../../scripts/dto/SongResponse.dto'; +import { SongResponseDto } from 'src/songs/dto/SongResponse.dto'; @Resolver() export class AlbumsResolver { diff --git a/back/src/artists/artists.resolver.ts b/back/src/artists/artists.resolver.ts index fe929d3..1d76d8b 100644 --- a/back/src/artists/artists.resolver.ts +++ b/back/src/artists/artists.resolver.ts @@ -1,7 +1,7 @@ import { Args, Int, Query, Resolver } from '@nestjs/graphql'; import { ArtistsService } from './artists.service'; -import { ArtistsResultsDto } from '../../scripts/dto/artistsResults.dto'; -import { SongResponseDto } from '../../scripts/dto/SongResponse.dto'; +import { SongResponseDto } from 'src/songs/dto/SongResponse.dto'; +import { ArtistsResultsDto } from './dto/artistsResults.dto'; @Resolver() export class ArtistsResolver { diff --git a/back/scripts/dto/artistsResults.dto.ts b/back/src/artists/dto/artistsResults.dto.ts similarity index 100% rename from back/scripts/dto/artistsResults.dto.ts rename to back/src/artists/dto/artistsResults.dto.ts diff --git a/back/scripts/dto/albumsSearchDto.ts b/back/src/search/dto/albumsSearchDto.ts similarity index 100% rename from back/scripts/dto/albumsSearchDto.ts rename to back/src/search/dto/albumsSearchDto.ts diff --git a/back/scripts/dto/artistsSearchDto.ts b/back/src/search/dto/artistsSearchDto.ts similarity index 100% rename from back/scripts/dto/artistsSearchDto.ts rename to back/src/search/dto/artistsSearchDto.ts diff --git a/back/src/search/dto/multipleSearchDto.ts b/back/src/search/dto/multipleSearchDto.ts new file mode 100644 index 0000000..1f602b2 --- /dev/null +++ b/back/src/search/dto/multipleSearchDto.ts @@ -0,0 +1,25 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import { artistsSearchDto } from "./artistsSearchDto"; +import { albumsSearchDto } from "./albumsSearchDto"; +import { searchSongsDto } from "./songsSearchDto"; + +@ObjectType() +export class multipleSearchResultsDto { + @Field(() => artistsSearchDto) + exactArtist: artistsSearchDto; + + @Field(() => albumsSearchDto) + exactAlbum: albumsSearchDto; + + @Field(() => searchSongsDto) + exactSong: searchSongsDto; + + @Field(() => [artistsSearchDto]) + artistResults: artistsSearchDto[]; + + @Field(() => [albumsSearchDto]) + albumResults: albumsSearchDto[]; + + @Field(() => [searchSongsDto]) + songResults: searchSongsDto[]; +}; \ No newline at end of file diff --git a/back/scripts/dto/songsSearchDto.ts b/back/src/search/dto/songsSearchDto.ts similarity index 100% rename from back/scripts/dto/songsSearchDto.ts rename to back/src/search/dto/songsSearchDto.ts diff --git a/back/src/search/search.module.ts b/back/src/search/search.module.ts index 5633587..e6f37c3 100644 --- a/back/src/search/search.module.ts +++ b/back/src/search/search.module.ts @@ -10,7 +10,8 @@ import { SongsModel } from '../../models/songs/song.model'; import { ArtistsModel } from '../../models/artists/artists.model'; import { AlbumsModel } from '../../models/albums/albums.model'; import { GenresModel } from '../../models/genres/genres.model'; -import { bonsaiClientProvider } from '../../scripts/bonsai/bonsaiClient'; +import { Client } from "elasticsearch"; +import dotenv from "dotenv"; dotenv.config(); @Module({ imports: [ @@ -25,7 +26,16 @@ import { bonsaiClientProvider } from '../../scripts/bonsai/bonsaiClient'; GenresModel, ]), ], - providers: [bonsaiClientProvider, SearchService, SearchResolver], + providers: [{ + provide: "BonsaiClient", + useFactory: async () => { + return new Client({ + host: process.env.ELASTICSEARCH_NODE, + log: "error", + ssl: { rejectUnauthorized: false } + }) + } +}, SearchService, SearchResolver], exports: [SearchService], }) export class SearchModule {} diff --git a/back/src/search/search.resolver.ts b/back/src/search/search.resolver.ts index 0120013..ea39b74 100644 --- a/back/src/search/search.resolver.ts +++ b/back/src/search/search.resolver.ts @@ -1,6 +1,6 @@ import { Args, Query, Resolver } from '@nestjs/graphql'; import { SearchService } from './search.service'; -import { multipleSearchResultsDto } from '../../scripts/dto/multipleSearchDto'; +import { multipleSearchResultsDto } from './dto/multipleSearchDto'; @Resolver() export class SearchResolver { diff --git a/back/src/song_genres/song_genres.resolver.ts b/back/src/song_genres/song_genres.resolver.ts index 9be1194..ff269c1 100644 --- a/back/src/song_genres/song_genres.resolver.ts +++ b/back/src/song_genres/song_genres.resolver.ts @@ -1,6 +1,6 @@ import { Args, Resolver, Query, Int } from '@nestjs/graphql'; import { SongGenresService } from './song_genres.service'; -import { SongResponseDto } from '../../scripts/dto/SongResponse.dto'; +import { SongResponseDto } from 'src/songs/dto/SongResponse.dto'; @Resolver() export class SongGenresResolver { diff --git a/back/scripts/dto/FullSongResponse.dto.ts b/back/src/songs/dto/FullSongResponse.dto.ts similarity index 100% rename from back/scripts/dto/FullSongResponse.dto.ts rename to back/src/songs/dto/FullSongResponse.dto.ts diff --git a/back/scripts/dto/SongResponse.dto.ts b/back/src/songs/dto/SongResponse.dto.ts similarity index 100% rename from back/scripts/dto/SongResponse.dto.ts rename to back/src/songs/dto/SongResponse.dto.ts diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index 558fcb8..19bef92 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -1,8 +1,8 @@ import { Args, Float, Int, Query, Resolver } from '@nestjs/graphql'; import { SongsService } from './songs.service'; -import { SongResponseDto } from '../../scripts/dto/SongResponse.dto'; -import { FullSongResponseDto } from '../../scripts/dto/FullSongResponse.dto'; import { USER_VECTOR } from '../../test/constants/constants'; +import { FullSongResponseDto } from './dto/FullSongResponse.dto'; +import { SongResponseDto } from './dto/SongResponse.dto'; @Resolver() export class SongsResolver { From ba850108148eee18b6fc8a3a2d1d16e00aa7d23f Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 18:49:34 -0400 Subject: [PATCH 02/38] r deleting scripts folder --- .../create-es-indexes/addIdxAlbumsData.ts | 24 ------ .../create-es-indexes/addIdxArtistData.ts | 24 ------ .../create-es-indexes/addIdxSongData.ts | 24 ------ back/scripts/create-es-indexes/cleanIdxs.ts | 13 --- .../create-es-indexes/createAlbumIdx.ts | 79 ------------------ .../create-es-indexes/createArtistIdx.ts | 78 ------------------ .../create-es-indexes/createSongIdx.ts | 81 ------------------- .../scripts/create-es-indexes/getAllAlbums.ts | 30 ------- .../create-es-indexes/getAllArtists.ts | 24 ------ back/scripts/create-es-indexes/getAllSongs.ts | 26 ------ back/scripts/dto/multipleSearchDto.ts | 79 ------------------ 11 files changed, 482 deletions(-) delete mode 100644 back/scripts/create-es-indexes/addIdxAlbumsData.ts delete mode 100644 back/scripts/create-es-indexes/addIdxArtistData.ts delete mode 100644 back/scripts/create-es-indexes/addIdxSongData.ts delete mode 100644 back/scripts/create-es-indexes/cleanIdxs.ts delete mode 100644 back/scripts/create-es-indexes/createAlbumIdx.ts delete mode 100644 back/scripts/create-es-indexes/createArtistIdx.ts delete mode 100644 back/scripts/create-es-indexes/createSongIdx.ts delete mode 100644 back/scripts/create-es-indexes/getAllAlbums.ts delete mode 100644 back/scripts/create-es-indexes/getAllArtists.ts delete mode 100644 back/scripts/create-es-indexes/getAllSongs.ts delete mode 100644 back/scripts/dto/multipleSearchDto.ts diff --git a/back/scripts/create-es-indexes/addIdxAlbumsData.ts b/back/scripts/create-es-indexes/addIdxAlbumsData.ts deleted file mode 100644 index c9af18d..0000000 --- a/back/scripts/create-es-indexes/addIdxAlbumsData.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import { albumSearchResults } from "../../src/types/searchTypes"; -import { getAllAlbums } from "./getAllAlbums"; -import dotenv from "dotenv"; dotenv.config(); - -export const elasticSearchService = new Client({ node: process.env.LOCAL_ES_NODE }); - -const addAlbumsData = async (album: albumSearchResults) => { - return elasticSearchService.index({ - index: "albums", - id: album.id.toString(), - document: album - }); -}; - -const addIndexAlbumsData = async (): Promise => { - const allAlbums = await getAllAlbums(); - for (const album of allAlbums) { - await addAlbumsData(album); - }; - return "Albums indexed successfully!"; -}; - -addIndexAlbumsData(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/addIdxArtistData.ts b/back/scripts/create-es-indexes/addIdxArtistData.ts deleted file mode 100644 index d49a88b..0000000 --- a/back/scripts/create-es-indexes/addIdxArtistData.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import { artistSearchResults } from "../../src/types/searchTypes"; -import { getArtists } from "./getAllArtists"; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({ node: process.env.LOCAL_ES_NODE }); - -const addArtistsData = async (artist: artistSearchResults) => { - return elasticSearchService.index({ - index: "artists", - id: artist.id.toString(), - document: artist - }); -}; - -const addIndexArtistsData = async () : Promise => { - const allArtists = await getArtists(); - for (const artist of allArtists) { - await addArtistsData(artist); - }; - return "Artists indexed successfully!"; -}; - -addIndexArtistsData(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/addIdxSongData.ts b/back/scripts/create-es-indexes/addIdxSongData.ts deleted file mode 100644 index 6fcaaea..0000000 --- a/back/scripts/create-es-indexes/addIdxSongData.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import { songSearchResults } from "../../src/types/searchTypes"; -import { getAllSongs } from "./getAllSongs"; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({ node: process.env.LOCAL_ES_NODE }); - -const addSongsData = async (song: songSearchResults) => { - return elasticSearchService.index({ - index: "songs", - id: song.id.toString(), - document: song - }) -}; - -const addIndexSongsData = async () : Promise => { - const allSongs = await getAllSongs(); - for (const song of allSongs) { - await addSongsData(song); - }; - return "Songs indexed successfully!"; -}; - -addIndexSongsData(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/cleanIdxs.ts b/back/scripts/create-es-indexes/cleanIdxs.ts deleted file mode 100644 index 2a68a7e..0000000 --- a/back/scripts/create-es-indexes/cleanIdxs.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({ node: process.env.LOCAL_ES_NODE }); - -const cleanIndexes = async () : Promise => { - await elasticSearchService.indices.delete({ index: 'songs' }); - await elasticSearchService.indices.delete({ index: 'albums' }); - await elasticSearchService.indices.delete({ index: 'artists' }); - return "Indexes cleaned!"; -}; - -cleanIndexes(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/createAlbumIdx.ts b/back/scripts/create-es-indexes/createAlbumIdx.ts deleted file mode 100644 index 8f13a3a..0000000 --- a/back/scripts/create-es-indexes/createAlbumIdx.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({ node: process.env.LOCAL_ES_NODE }); - -const createAlbumIndex = async () : Promise => { - await elasticSearchService.indices.create({ - index: "albums", - settings: { - analysis: { - analyzer: { - autocomplete: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"] - }, - edge_ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"] - }, - ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "ngram"] - } - }, - filter: { - edge_ngram: { - type: "edge_ngram", - min_gram: 1, - max_gram: 20 - }, - ngram: { - type: "ngram", - min_gram: 2, - max_gram: 3 - } - }, - normalizer: { - lowercase_ascii: { - type: "custom", - filter: ["lowercase", "asciifolding"] - } - } - } - }, - mappings: { - properties: { - id: { type: "integer" }, - name: { - type: "text", - analyzer: "autocomplete", - search_analyzer: "standard", - fields: { - edge: { - type: "text", - analyzer: "edge_ngram_analyzer" - }, - ngram: { - type: "text", - analyzer: "ngram_analyzer" - }, - keyword: { - type: "keyword", - normalizer: "lowercase_ascii" - } - } - }, - artists: { type: "text" }, - album_cover: { type: "keyword" }, - type: { type: "keyword" } - } - } - }); - return "ok" -}; - -createAlbumIndex(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/createArtistIdx.ts b/back/scripts/create-es-indexes/createArtistIdx.ts deleted file mode 100644 index db6d140..0000000 --- a/back/scripts/create-es-indexes/createArtistIdx.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Client } from "@elastic/elasticsearch"; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({node: process.env.LOCAL_ES_NODE}); - -const createArtistIndex = async () : Promise => { - await elasticSearchService.indices.create({ - index: "artists", - settings: { - analysis: { - analyzer: { - autocomplete: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"] - }, - edge_ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"], - }, - ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "ngram"] - } - }, - filter: { - edge_ngram: { - type: "edge_ngram", - min_gram: 1, - max_gram: 20 - }, - ngram: { - type: "ngram", - min_gram: 2, - max_gram: 3 - } - }, - normalizer: { - lowercase_ascii: { - type: "custom", - filter: ["lowercase", "asciifolding"] - } - } - } - }, - mappings: { - properties: { - id: { type: "integer" }, - name: { - type: "text", - analyzer: "autocomplete", - search_analyzer: "standard", - fields: { - edge: { - type: "text", - analyzer: "edge_ngram_analyzer" - }, - ngram: { - type: "text", - analyzer: "ngram_analyzer", - }, - keyword: { - type: "keyword", - normalizer: "lowercase_ascii" - } - }, - }, - album_cover: { type: "keyword" }, - type: { type: "keyword" }, - } - } - }) - return "ok"; -}; - -createArtistIndex(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/createSongIdx.ts b/back/scripts/create-es-indexes/createSongIdx.ts deleted file mode 100644 index fb1cf6a..0000000 --- a/back/scripts/create-es-indexes/createSongIdx.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Client } from '@elastic/elasticsearch'; -import dotenv from "dotenv"; dotenv.config(); - -const elasticSearchService = new Client({node: process.env.LOCAL_ES_NODE}); - -const createSongIndex = async () : Promise => { - await elasticSearchService.indices.create({ - index: "songs", - settings: { - analysis: { - analyzer: { - autocomplete: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"], - }, - edge_ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "edge_ngram"], - }, - ngram_analyzer: { - type: "custom", - tokenizer: "standard", - filter: ["lowercase", "asciifolding", "ngram"] - } - }, - filter: { - edge_ngram: { - type: "edge_ngram", - min_gram: 1, - max_gram: 20, - }, - ngram: { - type: "ngram", - min_gram: 2, - max_gram: 3, - } - }, - normalizer: { - lowercase_ascii: { - type: "custom", - filter: ["lowercase", "asciifolding"], - }, - }, - }, - }, - mappings: { - properties: { - id: { type: "integer" }, - name: { - type: "text", - analyzer: "autocomplete", - search_analyzer: "standard", - fields: { - edge: { - type: "text", - analyzer: "edge_ngram_analyzer" - }, - ngram: { - type: "text", - analyzer: "ngram_analyzer" - }, - keyword: { - type: "keyword", - normalizer: "lowercase_ascii" - }, - }, - }, - artists: { type: "text" }, - album: { type: "text" }, - album_cover: { type: "keyword" }, - url_preview: { type: "keyword" }, - type: { type: "keyword" } - } - } - }); - return "ok"; -}; - -createSongIndex(); \ No newline at end of file diff --git a/back/scripts/create-es-indexes/getAllAlbums.ts b/back/scripts/create-es-indexes/getAllAlbums.ts deleted file mode 100644 index ff6a63b..0000000 --- a/back/scripts/create-es-indexes/getAllAlbums.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { AlbumsModel } from "../../models/albums/albums.model"; -import { ArtistsModel } from "../../models/artists/artists.model"; -import { SongsModel } from "../../models/songs/song.model"; -import { albumSearchResults } from "../../src/types/searchTypes"; -import { parseAlbumSong } from "../../src/types/parses"; - -// Function to index the data in ES -export const getAllAlbums = async () : Promise => { - const rawData = await AlbumsModel.findAll({ - include: [ - { model: SongsModel, attributes: ["name"], include: [ - { model: ArtistsModel, attributes: ["name"], through: { attributes: [] } }, - ] } - ] - }); - - const data = rawData.map(album => parseAlbumSong(album.get({ plain: true }))); - - return data.map(album => { - const artistSet = new Set(); - album.songs.forEach(a => a.artists.forEach(artist => artistSet.add(artist.name) )); - return { - id: album.id, - name: album.name, - artists: [...artistSet], - album_cover: album.url_image, - type: "album", - }; - }); -}; \ No newline at end of file diff --git a/back/scripts/create-es-indexes/getAllArtists.ts b/back/scripts/create-es-indexes/getAllArtists.ts deleted file mode 100644 index 83c0327..0000000 --- a/back/scripts/create-es-indexes/getAllArtists.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { artistSearchResults } from "../../src/types/searchTypes"; -import { parseArtistSongs } from "../../src/types/parses"; -import { ArtistsModel } from "../../models/artists/artists.model"; -import { SongsModel } from "../../models/songs/song.model"; -import { AlbumsModel } from "../../models/albums/albums.model"; - -// Add data to the indexes in ES -export const getArtists = async () : Promise => { - const rawData = await ArtistsModel.findAll({ - include: [ - { model: SongsModel, attributes: ["name"], include: [ - { model: AlbumsModel, attributes: ["url_image"] } - ] } - ] - }); - - const data = rawData.map(song => parseArtistSongs(song.get({ plain: true }))); - return data.map(artist => ({ - id: artist.id, - name: artist.name, - album_cover: artist.songs[0].album.url_image, - type: "artist" - })); -}; \ No newline at end of file diff --git a/back/scripts/create-es-indexes/getAllSongs.ts b/back/scripts/create-es-indexes/getAllSongs.ts deleted file mode 100644 index 4765e2b..0000000 --- a/back/scripts/create-es-indexes/getAllSongs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { songSearchResults } from "../../src/types/searchTypes"; -import { parseSongResponse, parseStringArray, parseString } from "../../src/types/parses"; -import { SongsModel } from "../../models/songs/song.model"; -import { ArtistsModel } from "../../models/artists/artists.model"; -import { AlbumsModel } from "../../models/albums/albums.model"; - -// Function used to create the indexes in ES -export const getAllSongs = async () : Promise => { - const rawData = await SongsModel.findAll({ - include: [ - { model: ArtistsModel, attributes: ["name"], through: { attributes: [] } }, - { model: AlbumsModel, attributes: ["name", "url_image"] }, - ] - }); - - const data = rawData.map(song => parseSongResponse(song.get({ plain: true }))); - return data.map(song => ({ - id: song.id, - name: song.name, - artists: parseStringArray(song.artists.map(artist => artist.name)), - album: parseString(song.album.name), - album_cover: parseString(song.album.url_image), - url_preview: song.url_preview, - type: "song", - })); -}; \ No newline at end of file diff --git a/back/scripts/dto/multipleSearchDto.ts b/back/scripts/dto/multipleSearchDto.ts deleted file mode 100644 index 29c1044..0000000 --- a/back/scripts/dto/multipleSearchDto.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class multipleSearchResultsDto { - @Field(() => artistsSearchDto) - exactArtist: artistsSearchDto; - - @Field(() => albumsSearchDto) - exactAlbum: albumsSearchDto; - - @Field(() => searchSongsDto) - exactSong: searchSongsDto; - - @Field(() => [artistsSearchDto]) - artistResults: artistsSearchDto[]; - - @Field(() => [albumsSearchDto]) - albumResults: albumsSearchDto[]; - - @Field(() => [searchSongsDto]) - songResults: searchSongsDto[]; -}; - -@ObjectType() -export class albumsSearchDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field() - album_cover: string; - - @Field(() => String) - type: "album"; -}; - -@ObjectType() -export class artistsSearchDto { - @Field() - id: number; - - @Field() - name: string; - - @Field() - album_cover: string; - - @Field(() => String) - type: "artist"; -}; - -@ObjectType() -export class searchSongsDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field() - album: string; - - @Field() - album_cover: string; - - @Field() - url_preview: string; - - @Field(() => String) - type: "song"; -}; \ No newline at end of file From c302ee7707ac2c5ecb699a533507a82666566f83 Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:03:58 -0400 Subject: [PATCH 03/38] r creating database using Go --- back/database/go.mod | 5 + back/database/go.sum | 2 + back/database/main.go | 38 ++++ back/database/sql/schema/001_artists.sql | 8 + .../20250523_00_tables_creations.ts | 208 ------------------ .../20250523_01_create_relations.ts | 44 ---- back/migrations/20250524_02_alter_albums.ts | 17 -- back/package.json | 1 - 8 files changed, 53 insertions(+), 270 deletions(-) create mode 100644 back/database/go.mod create mode 100644 back/database/go.sum create mode 100644 back/database/main.go create mode 100644 back/database/sql/schema/001_artists.sql delete mode 100644 back/migrations/20250523_00_tables_creations.ts delete mode 100644 back/migrations/20250523_01_create_relations.ts delete mode 100644 back/migrations/20250524_02_alter_albums.ts diff --git a/back/database/go.mod b/back/database/go.mod new file mode 100644 index 0000000..68236a3 --- /dev/null +++ b/back/database/go.mod @@ -0,0 +1,5 @@ +module scripts + +go 1.25.3 + +require github.com/lib/pq v1.11.1 // indirect diff --git a/back/database/go.sum b/back/database/go.sum new file mode 100644 index 0000000..3ad08b1 --- /dev/null +++ b/back/database/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= +github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= diff --git a/back/database/main.go b/back/database/main.go new file mode 100644 index 0000000..2c8456a --- /dev/null +++ b/back/database/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/url" + + _ "github.com/lib/pq" +) + +func main() { + serviceURI := "postgresql://postgres:postgres@localhost:5432/music_db?sslmode=disable" + + conn, _ := url.Parse(serviceURI) + conn.RawQuery = "sslmode=verify-ca;sslrootcert=ca.pem" + + db, err := sql.Open("postgres", conn.String()) + + if err != nil { + log.Fatal(err) + } + defer db.Close() + + rows, err := db.Query("SELECT version()") + if err != nil { + panic(err) + } + + for rows.Next() { + var result string + err = rows.Scan(&result) + if err != nil { + panic(err) + } + fmt.Printf("Version: %s\n", result) + } +} diff --git a/back/database/sql/schema/001_artists.sql b/back/database/sql/schema/001_artists.sql new file mode 100644 index 0000000..8949051 --- /dev/null +++ b/back/database/sql/schema/001_artists.sql @@ -0,0 +1,8 @@ +-- +goose Up +CREATE TABLE artists( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +-- +goose Down +DROP TABLE artists; \ No newline at end of file diff --git a/back/migrations/20250523_00_tables_creations.ts b/back/migrations/20250523_00_tables_creations.ts deleted file mode 100644 index aa24b47..0000000 --- a/back/migrations/20250523_00_tables_creations.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { DataTypes } from "sequelize" - -export const up = async ({ context: queryInterface }) => { - await queryInterface.createTable("albums", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - url_image: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - }); - - await queryInterface.createTable("songs", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - spotify_id: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - url_preview: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - duration: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 6 - } - }, - year: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 1980, - max: 2025 - } - }, - album_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "albums", key: "id" } - }, - }) - - await queryInterface.createTable("artists", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - }) - - await queryInterface.createTable("genres", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - genre: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - }) - - await queryInterface.createTable("song_details", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - danceability: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - energy: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - track_key: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 0, - max: 11 - } - }, - loudness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: -60, - max: 0 - } - }, - mode: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - speechiness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - acousticness: { - type: DataTypes.DECIMAL(10, 6), - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - instrumentalness: { - type: DataTypes.DECIMAL(10, 6), - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - liveness: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - valence: { - type: DataTypes.FLOAT, - allowNull: false, - validate: { - min: 0, - max: 1 - } - }, - tempo: { - type: DataTypes.DECIMAL(6, 3), - allowNull: false, - validate: { - min: 30, - max: 300 - } - }, - time_signature: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - min: 1, - max: 12 - } - }, - }) -} - -export const down = async ({ context: queryInterface }) => { - await queryInterface.dropTable("songs"); - await queryInterface.dropTable("albums"); - await queryInterface.dropTable("artists"); - await queryInterface.dropTable("genres"); - await queryInterface.dropTable("song_details"); -} \ No newline at end of file diff --git a/back/migrations/20250523_01_create_relations.ts b/back/migrations/20250523_01_create_relations.ts deleted file mode 100644 index 18642d7..0000000 --- a/back/migrations/20250523_01_create_relations.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DataTypes } from "sequelize" - -export const up = async ({ context: queryInterface }) => { - await queryInterface.createTable("song_artists_rp", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - artist_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "artists", key: "id" } - } - }); - - await queryInterface.createTable("song_genres_rp", { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - song_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "songs", key: "id" } - }, - genre_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "genres", key: "id" } - }, - }); -} - -export const down = async ({ context: queryInterface }) => { - await queryInterface.dropTable("song_artists_rp"); - await queryInterface.dropTable("song_genres_rp"); -}; \ No newline at end of file diff --git a/back/migrations/20250524_02_alter_albums.ts b/back/migrations/20250524_02_alter_albums.ts deleted file mode 100644 index 28451f2..0000000 --- a/back/migrations/20250524_02_alter_albums.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DataTypes } from "sequelize" - -export const up = async ({ context: queryInterface }) => { - await queryInterface.changeColumn("albums", "url_image", { - type: DataTypes.STRING, - allowNull: false, - unique: false - }) -} - -export const down = async ({ context: queryInterface }) => { - await queryInterface.changeColumn("albums", "url_image", { - type: DataTypes.STRING, - allowNull: false, - unique: true - }) -} \ No newline at end of file diff --git a/back/package.json b/back/package.json index 87a0c71..bed009f 100644 --- a/back/package.json +++ b/back/package.json @@ -10,7 +10,6 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "dev": "nest start --watch", - "migration:down": "node src/songs/utils/rollback.js", "start:debug": "nest start --debug --watch", "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", From f2ab2ff3acfd52a0702e825637c1dbf47f79dd5e Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:10:45 -0400 Subject: [PATCH 04/38] r creating albums schema --- back/database/go.mod | 2 +- back/database/sql/schema/002_albums.sql | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 back/database/sql/schema/002_albums.sql diff --git a/back/database/go.mod b/back/database/go.mod index 68236a3..8722e75 100644 --- a/back/database/go.mod +++ b/back/database/go.mod @@ -2,4 +2,4 @@ module scripts go 1.25.3 -require github.com/lib/pq v1.11.1 // indirect +require github.com/lib/pq v1.11.1 diff --git a/back/database/sql/schema/002_albums.sql b/back/database/sql/schema/002_albums.sql new file mode 100644 index 0000000..e7f7eaa --- /dev/null +++ b/back/database/sql/schema/002_albums.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE albums( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + url_image TEXT NOT NULL UNIQUE +); + +-- +goose Down +DROP TABLE albums; From b1d3010382c54e26d66c5dab299bfcfa78785795 Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:14:32 -0400 Subject: [PATCH 05/38] r creating genres schema --- back/database/sql/schema/003_genres.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 back/database/sql/schema/003_genres.sql diff --git a/back/database/sql/schema/003_genres.sql b/back/database/sql/schema/003_genres.sql new file mode 100644 index 0000000..24fcb2a --- /dev/null +++ b/back/database/sql/schema/003_genres.sql @@ -0,0 +1,8 @@ +-- +goose Up +CREATE TABLE genres( + id INTEGER PRIMARY KEY, + genre TEXT UNIQUE NOT NULL +); + +-- +goose Down +DROP TABLE genres; \ No newline at end of file From 217b7f7eb95cf49a673323e1e836ed3759747603 Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:21:13 -0400 Subject: [PATCH 06/38] r creating songs schema --- back/database/sql/schema/004_songs.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 back/database/sql/schema/004_songs.sql diff --git a/back/database/sql/schema/004_songs.sql b/back/database/sql/schema/004_songs.sql new file mode 100644 index 0000000..b3c0fc8 --- /dev/null +++ b/back/database/sql/schema/004_songs.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE songs( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + spotify_id TEXT NOT NULL UNIQUE, + url_preview TEXT NOT NULL UNIQUE, + duration INTEGER NOT NULL, + year INTEGER NOT NULL +); + +-- +goose Down +DROP TABLE songs; \ No newline at end of file From 128542e8213d975c3c4214c349519fabb4c7320a Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:30:41 -0400 Subject: [PATCH 07/38] r creating song_genres schema --- back/database/sql/schema/005_song_genres.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 back/database/sql/schema/005_song_genres.sql diff --git a/back/database/sql/schema/005_song_genres.sql b/back/database/sql/schema/005_song_genres.sql new file mode 100644 index 0000000..405d70d --- /dev/null +++ b/back/database/sql/schema/005_song_genres.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE song_genres( + id INTEGER PRIMARY KEY, + song_id INTEGER NOT NULL REFERENCES songs(id) ON DELETE CASCADE, + genre_id INTEGER NOT NULL REFERENCES genres(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE song_genres; \ No newline at end of file From 462b25e5ac27d68f5e6c44cd55d55f22d4136a42 Mon Sep 17 00:00:00 2001 From: Raghart Date: Fri, 6 Feb 2026 21:36:01 -0400 Subject: [PATCH 08/38] r creating song_artists schema --- back/database/sql/schema/006_song_artists.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 back/database/sql/schema/006_song_artists.sql diff --git a/back/database/sql/schema/006_song_artists.sql b/back/database/sql/schema/006_song_artists.sql new file mode 100644 index 0000000..cdc6a63 --- /dev/null +++ b/back/database/sql/schema/006_song_artists.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE song_artists( + id INTEGER PRIMARY KEY, + song_id INTEGER NOT NULL REFERENCES songs(id) ON DELETE CASCADE, + artist_id INTEGER NOT NULL REFERENCES artists(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE song_artists; \ No newline at end of file From 861a12be63ae073e375970458a67dbbea2df5d41 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 10:29:01 -0400 Subject: [PATCH 09/38] r adding song_details schema --- back/database/sql/schema/007_song_details.sql | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 back/database/sql/schema/007_song_details.sql diff --git a/back/database/sql/schema/007_song_details.sql b/back/database/sql/schema/007_song_details.sql new file mode 100644 index 0000000..647f0f2 --- /dev/null +++ b/back/database/sql/schema/007_song_details.sql @@ -0,0 +1,20 @@ +-- +goose Up +CREATE TABLE song_details( + id INTEGER PRIMARY KEY, + song_id INTEGER NOT NULL REFERENCES songs(id) ON DELETE CASCADE, + danceability REAL NOT NULL, + energy REAL NOT NULL, + track_key REAL NOT NULL, + loudness REAL NOT NULL, + mode REAL NOT NULL, + speechiness REAL NOT NULL, + acousticness DOUBLE PRECISION NOT NULL, + instrumentalness DOUBLE PRECISION NOT NULL, + liveness REAL NOT NULL, + valence REAL NOT NULL, + tempo REAL NOT NULL, + time_signature INTEGER NOT NULL +); + +-- +goose Down +DROP TABLE song_details; \ No newline at end of file From 62815b8d740d12f3c51a8043e47acc882cec0fdf Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 12:17:03 -0400 Subject: [PATCH 10/38] r adding artists and starting to configure cli architecture --- .gitignore | 2 + .../database/internal/database/artists.sql.go | 39 ++++++++++ back/database/internal/database/db.go | 31 ++++++++ back/database/internal/database/models.go | 59 ++++++++++++++ back/database/main.go | 77 ++++++++++++++++--- back/database/sql/queries/artists.sql | 7 ++ back/database/sqlc.yaml | 8 ++ 7 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 back/database/internal/database/artists.sql.go create mode 100644 back/database/internal/database/db.go create mode 100644 back/database/internal/database/models.go create mode 100644 back/database/sql/queries/artists.sql create mode 100644 back/database/sqlc.yaml diff --git a/.gitignore b/.gitignore index 504c46d..0e474d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # compiled output node_modules/ +back/database/csv_data/ + # Logs logs *.log diff --git a/back/database/internal/database/artists.sql.go b/back/database/internal/database/artists.sql.go new file mode 100644 index 0000000..97d222e --- /dev/null +++ b/back/database/internal/database/artists.sql.go @@ -0,0 +1,39 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: artists.sql + +package database + +import ( + "context" +) + +const addArtist = `-- name: AddArtist :one +INSERT INTO artists (id, name) +VALUES ($1, $2) +RETURNING id, name +` + +type AddArtistParams struct { + ID int32 + Name string +} + +func (q *Queries) AddArtist(ctx context.Context, arg AddArtistParams) (Artist, error) { + row := q.db.QueryRowContext(ctx, addArtist, arg.ID, arg.Name) + var i Artist + err := row.Scan(&i.ID, &i.Name) + return i, err +} + +const getArtist = `-- name: GetArtist :one +SELECT id, name FROM artists WHERE id = $1 +` + +func (q *Queries) GetArtist(ctx context.Context, id int32) (Artist, error) { + row := q.db.QueryRowContext(ctx, getArtist, id) + var i Artist + err := row.Scan(&i.ID, &i.Name) + return i, err +} diff --git a/back/database/internal/database/db.go b/back/database/internal/database/db.go new file mode 100644 index 0000000..85d4b8c --- /dev/null +++ b/back/database/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/back/database/internal/database/models.go b/back/database/internal/database/models.go new file mode 100644 index 0000000..e25c863 --- /dev/null +++ b/back/database/internal/database/models.go @@ -0,0 +1,59 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package database + +type Album struct { + ID int32 + Name string + UrlImage string +} + +type Artist struct { + ID int32 + Name string +} + +type Genre struct { + ID int32 + Genre string +} + +type Song struct { + ID int32 + Name string + SpotifyID string + UrlPreview string + Duration int32 + Year int32 +} + +type SongArtist struct { + ID int32 + SongID int32 + ArtistID int32 +} + +type SongDetail struct { + ID int32 + SongID int32 + Danceability float32 + Energy float32 + TrackKey float32 + Loudness float32 + Mode float32 + Speechiness float32 + Acousticness float64 + Instrumentalness float64 + Liveness float32 + Valence float32 + Tempo float32 + TimeSignature int32 +} + +type SongGenre struct { + ID int32 + SongID int32 + GenreID int32 +} diff --git a/back/database/main.go b/back/database/main.go index 2c8456a..633e10a 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -1,16 +1,28 @@ package main import ( + "context" "database/sql" + "encoding/csv" "fmt" "log" "net/url" + "os" + "path/filepath" + "scripts/internal/database" + "strconv" _ "github.com/lib/pq" ) func main() { - serviceURI := "postgresql://postgres:postgres@localhost:5432/music_db?sslmode=disable" + serviceURI := os.Getenv("LOCAL_DB_URI") + var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") + + if len(os.Args) != 2 { + printHelp() + return + } conn, _ := url.Parse(serviceURI) conn.RawQuery = "sslmode=verify-ca;sslrootcert=ca.pem" @@ -22,17 +34,60 @@ func main() { } defer db.Close() - rows, err := db.Query("SELECT version()") - if err != nil { - panic(err) - } + queries := database.New(db) + + switch os.Args[1] { + case "artists": + { + artistFile, err := os.Open(ARTISTPATH) + if err != nil { + log.Fatal(err) + } + + defer artistFile.Close() + reader := csv.NewReader(artistFile) + records, err := reader.ReadAll() + + if err != nil { + log.Fatalf("error while trying to read the records: %v", err) + } + fmt.Println("starting to print the records!") + for idx, record := range records { + if idx == 0 { + continue + } - for rows.Next() { - var result string - err = rows.Scan(&result) - if err != nil { - panic(err) + id, err := strconv.Atoi(record[0]) + if err != nil { + log.Fatalf("id is not a valin number") + } + + name := record[1] + artist, err := queries.AddArtist(context.Background(), database.AddArtistParams{ + ID: int32(id), + Name: name, + }) + + if err != nil { + log.Fatalf("error while trying to add the artist: %v", err) + } + + fmt.Println(artist) + } + fmt.Println("the records has been added!") + } + default: + { + fmt.Printf("'%v' is not a valid command\n", os.Args[1]) + printHelp() + return } - fmt.Printf("Version: %s\n", result) } } + +func printHelp() { + fmt.Println("Help:") + fmt.Println("Usage: go run main.go [arg]") + fmt.Println("Avaible Commands:") + fmt.Println("'artists': to add the artists of a specific route.") +} diff --git a/back/database/sql/queries/artists.sql b/back/database/sql/queries/artists.sql new file mode 100644 index 0000000..f790ce1 --- /dev/null +++ b/back/database/sql/queries/artists.sql @@ -0,0 +1,7 @@ +-- name: AddArtist :one +INSERT INTO artists (id, name) +VALUES ($1, $2) +RETURNING *; + +-- name: GetArtist :one +SELECT * FROM artists WHERE id = $1; \ No newline at end of file diff --git a/back/database/sqlc.yaml b/back/database/sqlc.yaml new file mode 100644 index 0000000..5762c5f --- /dev/null +++ b/back/database/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" \ No newline at end of file From 710836c42562b7f4f158ed64c27ad106d2d8f191 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 16:42:16 -0400 Subject: [PATCH 11/38] r refactoring cli code to be more flexible --- back/database/dbConfig/databaseConfig.go | 67 +++++++++++++++++++ .../database/internal/database/artists.sql.go | 9 +++ back/database/main.go | 50 ++------------ back/database/pathConstants/paths.go | 5 ++ back/database/sql/queries/artists.sql | 5 +- 5 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 back/database/dbConfig/databaseConfig.go create mode 100644 back/database/pathConstants/paths.go diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go new file mode 100644 index 0000000..6852ce4 --- /dev/null +++ b/back/database/dbConfig/databaseConfig.go @@ -0,0 +1,67 @@ +package databaseConfig + +import ( + "context" + "encoding/csv" + "fmt" + "log" + "os" + "scripts/internal/database" + "strconv" +) + +type DbConfig struct { + Queries *database.Queries +} + +func AddToDatabase(path string, databaseMethod func([][]string)) { + csvData, err := parseCSV(path) + if err != nil { + log.Fatalf("path not found: %v", err) + } + + databaseMethod(csvData) +} + +func (cfg *DbConfig) AddArtistsDatabase(records [][]string) { + fmt.Println("Adding data to the artists table...") + for idx, record := range records { + if idx == 0 { + continue + } + + id, err := strconv.Atoi(record[0]) + if err != nil { + log.Fatalf("id is not a valin number") + } + + name := record[1] + artist, err := cfg.Queries.AddArtist(context.Background(), database.AddArtistParams{ + ID: int32(id), + Name: name, + }) + + if err != nil { + log.Fatalf("error while trying to add the artist: %v", err) + } + + fmt.Println(artist) + } + fmt.Println("the records has been added!") +} + +func parseCSV(path string) ([][]string, error) { + file, err := os.Open(path) + if err != nil { + log.Fatal(err) + } + + reader := csv.NewReader(file) + return reader.ReadAll() +} + +type TableType string + +const ( + ArtistsType TableType = "artists" +) diff --git a/back/database/internal/database/artists.sql.go b/back/database/internal/database/artists.sql.go index 97d222e..32d173d 100644 --- a/back/database/internal/database/artists.sql.go +++ b/back/database/internal/database/artists.sql.go @@ -27,6 +27,15 @@ func (q *Queries) AddArtist(ctx context.Context, arg AddArtistParams) (Artist, e return i, err } +const cleanArtists = `-- name: CleanArtists :exec +DELETE FROM artists +` + +func (q *Queries) CleanArtists(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanArtists) + return err +} + const getArtist = `-- name: GetArtist :one SELECT id, name FROM artists WHERE id = $1 ` diff --git a/back/database/main.go b/back/database/main.go index 633e10a..0405bfc 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -1,23 +1,20 @@ package main import ( - "context" "database/sql" - "encoding/csv" "fmt" "log" "net/url" "os" - "path/filepath" + databaseConfig "scripts/dbConfig" "scripts/internal/database" - "strconv" + paths "scripts/pathConstants" _ "github.com/lib/pq" ) func main() { - serviceURI := os.Getenv("LOCAL_DB_URI") - var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") + serviceURI := "postgresql://postgres:postgres@localhost:5432/music_db?sslmode=disable" if len(os.Args) != 2 { printHelp() @@ -34,47 +31,14 @@ func main() { } defer db.Close() - queries := database.New(db) + dbCfg := databaseConfig.DbConfig{ + Queries: database.New(db), + } switch os.Args[1] { case "artists": { - artistFile, err := os.Open(ARTISTPATH) - if err != nil { - log.Fatal(err) - } - - defer artistFile.Close() - reader := csv.NewReader(artistFile) - records, err := reader.ReadAll() - - if err != nil { - log.Fatalf("error while trying to read the records: %v", err) - } - fmt.Println("starting to print the records!") - for idx, record := range records { - if idx == 0 { - continue - } - - id, err := strconv.Atoi(record[0]) - if err != nil { - log.Fatalf("id is not a valin number") - } - - name := record[1] - artist, err := queries.AddArtist(context.Background(), database.AddArtistParams{ - ID: int32(id), - Name: name, - }) - - if err != nil { - log.Fatalf("error while trying to add the artist: %v", err) - } - - fmt.Println(artist) - } - fmt.Println("the records has been added!") + databaseConfig.AddToDatabase(paths.ARTISTPATH, dbCfg.AddArtistsDatabase) } default: { diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go new file mode 100644 index 0000000..55565bf --- /dev/null +++ b/back/database/pathConstants/paths.go @@ -0,0 +1,5 @@ +package paths + +import "path/filepath" + +var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") diff --git a/back/database/sql/queries/artists.sql b/back/database/sql/queries/artists.sql index f790ce1..38594c8 100644 --- a/back/database/sql/queries/artists.sql +++ b/back/database/sql/queries/artists.sql @@ -4,4 +4,7 @@ VALUES ($1, $2) RETURNING *; -- name: GetArtist :one -SELECT * FROM artists WHERE id = $1; \ No newline at end of file +SELECT * FROM artists WHERE id = $1; + +-- name: CleanArtists :exec +DELETE FROM artists; \ No newline at end of file From 8c5167533ce8f1105909e3090b299d7cc1572a10 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 17:05:18 -0400 Subject: [PATCH 12/38] r cmd to add genres to the database --- back/database/dbConfig/databaseConfig.go | 29 +++++++++++++++++++ back/database/internal/database/genres.sql.go | 28 ++++++++++++++++++ back/database/main.go | 4 +++ back/database/pathConstants/paths.go | 1 + back/database/sql/queries/genres.sql | 4 +++ 5 files changed, 66 insertions(+) create mode 100644 back/database/internal/database/genres.sql.go create mode 100644 back/database/sql/queries/genres.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 6852ce4..2741260 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -50,6 +50,35 @@ func (cfg *DbConfig) AddArtistsDatabase(records [][]string) { fmt.Println("the records has been added!") } +func (cfg *DbConfig) AddGenresDatabase(records [][]string) { + fmt.Println("Adding the genres to the database...") + for idx, genre := range records { + if idx == 0 { + continue + } + + genreStrID := genre[0] + genreName := genre[1] + + genreID, err := strconv.Atoi(genreStrID) + if err != nil { + log.Fatal(err) + } + + genreAdded, err := cfg.Queries.AddGenre(context.Background(), database.AddGenreParams{ + ID: int32(genreID), + Genre: genreName, + }) + + if err != nil { + log.Fatalf("there was a problem while trying to add the genre") + } + + fmt.Println(genreAdded) + } + fmt.Println("Finish adding genres!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/genres.sql.go b/back/database/internal/database/genres.sql.go new file mode 100644 index 0000000..58af22b --- /dev/null +++ b/back/database/internal/database/genres.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: genres.sql + +package database + +import ( + "context" +) + +const addGenre = `-- name: AddGenre :one +INSERT INTO genres(id, genre) +VALUES($1, $2) +RETURNING id, genre +` + +type AddGenreParams struct { + ID int32 + Genre string +} + +func (q *Queries) AddGenre(ctx context.Context, arg AddGenreParams) (Genre, error) { + row := q.db.QueryRowContext(ctx, addGenre, arg.ID, arg.Genre) + var i Genre + err := row.Scan(&i.ID, &i.Genre) + return i, err +} diff --git a/back/database/main.go b/back/database/main.go index 0405bfc..fda45fb 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -40,6 +40,10 @@ func main() { { databaseConfig.AddToDatabase(paths.ARTISTPATH, dbCfg.AddArtistsDatabase) } + case "genres": + { + databaseConfig.AddToDatabase(paths.GenresPath, dbCfg.AddGenresDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index 55565bf..3c19481 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -3,3 +3,4 @@ package paths import "path/filepath" var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") +var GenresPath = filepath.Join("csv_data", "genres_rows.csv") diff --git a/back/database/sql/queries/genres.sql b/back/database/sql/queries/genres.sql new file mode 100644 index 0000000..edb80b1 --- /dev/null +++ b/back/database/sql/queries/genres.sql @@ -0,0 +1,4 @@ +-- name: AddGenre :one +INSERT INTO genres(id, genre) +VALUES($1, $2) +RETURNING *; \ No newline at end of file From 7826f2924da38246141838e43872465f94ea3634 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 17:28:28 -0400 Subject: [PATCH 13/38] r adding cmd to add albums to the local database --- back/database/dbConfig/databaseConfig.go | 35 ++++++++++++++++- back/database/internal/database/albums.sql.go | 38 +++++++++++++++++++ .../database/internal/database/artists.sql.go | 26 ++++++------- back/database/internal/database/genres.sql.go | 8 ++-- back/database/main.go | 4 ++ back/database/pathConstants/paths.go | 1 + back/database/sql/queries/albums.sql | 7 ++++ back/database/sql/queries/artists.sql | 2 +- back/database/sql/queries/genres.sql | 2 +- back/database/sql/schema/002_albums.sql | 2 +- 10 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 back/database/internal/database/albums.sql.go create mode 100644 back/database/sql/queries/albums.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 2741260..f721e63 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -36,7 +36,7 @@ func (cfg *DbConfig) AddArtistsDatabase(records [][]string) { } name := record[1] - artist, err := cfg.Queries.AddArtist(context.Background(), database.AddArtistParams{ + artist, err := cfg.Queries.CreateArtist(context.Background(), database.CreateArtistParams{ ID: int32(id), Name: name, }) @@ -65,7 +65,7 @@ func (cfg *DbConfig) AddGenresDatabase(records [][]string) { log.Fatal(err) } - genreAdded, err := cfg.Queries.AddGenre(context.Background(), database.AddGenreParams{ + genreAdded, err := cfg.Queries.CreateGenre(context.Background(), database.CreateGenreParams{ ID: int32(genreID), Genre: genreName, }) @@ -79,6 +79,37 @@ func (cfg *DbConfig) AddGenresDatabase(records [][]string) { fmt.Println("Finish adding genres!") } +func (cfg *DbConfig) AddAlbumsDatabase(records [][]string) { + fmt.Println("Adding albums to the database...") + for idx, albumRecord := range records { + if idx == 0 { + continue + } + + albumStrId := albumRecord[0] + albumName := albumRecord[1] + albumUrlImg := albumRecord[2] + + albumID, err := strconv.Atoi(albumStrId) + if err != nil { + log.Fatalf("%v is not a valid string: %v", albumStrId, err) + } + + addedAlbum, err := cfg.Queries.CreateAlbum(context.Background(), database.CreateAlbumParams{ + ID: int32(albumID), + Name: albumName, + UrlImage: albumUrlImg, + }) + + if err != nil { + log.Fatal(err) + } + + fmt.Println(addedAlbum) + } + fmt.Println("Finish adding albums!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/albums.sql.go b/back/database/internal/database/albums.sql.go new file mode 100644 index 0000000..269cd44 --- /dev/null +++ b/back/database/internal/database/albums.sql.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: albums.sql + +package database + +import ( + "context" +) + +const cleanAlbums = `-- name: CleanAlbums :exec +DELETE FROM albums +` + +func (q *Queries) CleanAlbums(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanAlbums) + return err +} + +const createAlbum = `-- name: CreateAlbum :one +INSERT INTO albums(id, name, url_image) +VALUES($1, $2, $3) +RETURNING id, name, url_image +` + +type CreateAlbumParams struct { + ID int32 + Name string + UrlImage string +} + +func (q *Queries) CreateAlbum(ctx context.Context, arg CreateAlbumParams) (Album, error) { + row := q.db.QueryRowContext(ctx, createAlbum, arg.ID, arg.Name, arg.UrlImage) + var i Album + err := row.Scan(&i.ID, &i.Name, &i.UrlImage) + return i, err +} diff --git a/back/database/internal/database/artists.sql.go b/back/database/internal/database/artists.sql.go index 32d173d..1c2e4dd 100644 --- a/back/database/internal/database/artists.sql.go +++ b/back/database/internal/database/artists.sql.go @@ -9,33 +9,33 @@ import ( "context" ) -const addArtist = `-- name: AddArtist :one +const cleanArtists = `-- name: CleanArtists :exec +DELETE FROM artists +` + +func (q *Queries) CleanArtists(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanArtists) + return err +} + +const createArtist = `-- name: CreateArtist :one INSERT INTO artists (id, name) VALUES ($1, $2) RETURNING id, name ` -type AddArtistParams struct { +type CreateArtistParams struct { ID int32 Name string } -func (q *Queries) AddArtist(ctx context.Context, arg AddArtistParams) (Artist, error) { - row := q.db.QueryRowContext(ctx, addArtist, arg.ID, arg.Name) +func (q *Queries) CreateArtist(ctx context.Context, arg CreateArtistParams) (Artist, error) { + row := q.db.QueryRowContext(ctx, createArtist, arg.ID, arg.Name) var i Artist err := row.Scan(&i.ID, &i.Name) return i, err } -const cleanArtists = `-- name: CleanArtists :exec -DELETE FROM artists -` - -func (q *Queries) CleanArtists(ctx context.Context) error { - _, err := q.db.ExecContext(ctx, cleanArtists) - return err -} - const getArtist = `-- name: GetArtist :one SELECT id, name FROM artists WHERE id = $1 ` diff --git a/back/database/internal/database/genres.sql.go b/back/database/internal/database/genres.sql.go index 58af22b..39d8bd1 100644 --- a/back/database/internal/database/genres.sql.go +++ b/back/database/internal/database/genres.sql.go @@ -9,19 +9,19 @@ import ( "context" ) -const addGenre = `-- name: AddGenre :one +const createGenre = `-- name: CreateGenre :one INSERT INTO genres(id, genre) VALUES($1, $2) RETURNING id, genre ` -type AddGenreParams struct { +type CreateGenreParams struct { ID int32 Genre string } -func (q *Queries) AddGenre(ctx context.Context, arg AddGenreParams) (Genre, error) { - row := q.db.QueryRowContext(ctx, addGenre, arg.ID, arg.Genre) +func (q *Queries) CreateGenre(ctx context.Context, arg CreateGenreParams) (Genre, error) { + row := q.db.QueryRowContext(ctx, createGenre, arg.ID, arg.Genre) var i Genre err := row.Scan(&i.ID, &i.Genre) return i, err diff --git a/back/database/main.go b/back/database/main.go index fda45fb..669b9fa 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -44,6 +44,10 @@ func main() { { databaseConfig.AddToDatabase(paths.GenresPath, dbCfg.AddGenresDatabase) } + case "albums": + { + databaseConfig.AddToDatabase(paths.AlbumsPath, dbCfg.AddAlbumsDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index 3c19481..d67a397 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -4,3 +4,4 @@ import "path/filepath" var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") var GenresPath = filepath.Join("csv_data", "genres_rows.csv") +var AlbumsPath = filepath.Join("csv_data", "albums_rows.csv") diff --git a/back/database/sql/queries/albums.sql b/back/database/sql/queries/albums.sql new file mode 100644 index 0000000..29e6d3a --- /dev/null +++ b/back/database/sql/queries/albums.sql @@ -0,0 +1,7 @@ +-- name: CreateAlbum :one +INSERT INTO albums(id, name, url_image) +VALUES($1, $2, $3) +RETURNING *; + +-- name: CleanAlbums :exec +DELETE FROM albums; \ No newline at end of file diff --git a/back/database/sql/queries/artists.sql b/back/database/sql/queries/artists.sql index 38594c8..4a61974 100644 --- a/back/database/sql/queries/artists.sql +++ b/back/database/sql/queries/artists.sql @@ -1,4 +1,4 @@ --- name: AddArtist :one +-- name: CreateArtist :one INSERT INTO artists (id, name) VALUES ($1, $2) RETURNING *; diff --git a/back/database/sql/queries/genres.sql b/back/database/sql/queries/genres.sql index edb80b1..209db64 100644 --- a/back/database/sql/queries/genres.sql +++ b/back/database/sql/queries/genres.sql @@ -1,4 +1,4 @@ --- name: AddGenre :one +-- name: CreateGenre :one INSERT INTO genres(id, genre) VALUES($1, $2) RETURNING *; \ No newline at end of file diff --git a/back/database/sql/schema/002_albums.sql b/back/database/sql/schema/002_albums.sql index e7f7eaa..8f92deb 100644 --- a/back/database/sql/schema/002_albums.sql +++ b/back/database/sql/schema/002_albums.sql @@ -2,7 +2,7 @@ CREATE TABLE albums( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, - url_image TEXT NOT NULL UNIQUE + url_image TEXT NOT NULL ); -- +goose Down From 34056c2fad752ba4ce38386000065a84b4e40654 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 17:35:47 -0400 Subject: [PATCH 14/38] r adding missing column to songs table --- back/database/sql/schema/008_songs_column.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 back/database/sql/schema/008_songs_column.sql diff --git a/back/database/sql/schema/008_songs_column.sql b/back/database/sql/schema/008_songs_column.sql new file mode 100644 index 0000000..66ebcad --- /dev/null +++ b/back/database/sql/schema/008_songs_column.sql @@ -0,0 +1,6 @@ +-- +goose Up +ALTER TABLE songs +ADD album_id INTEGER NOT NULL REFERENCES albums(id) ON DELETE CASCADE; + +-- +goose Down +ALTER TABLE songs DROP COLUMN album_id; \ No newline at end of file From 1287166a24419f50b035cd0a539885c0e48e74cc Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 18:47:24 -0400 Subject: [PATCH 15/38] r adding songs to the local database --- back/database/dbConfig/databaseConfig.go | 60 +++++++++++++++++++ back/database/internal/database/albums.sql.go | 11 ++++ back/database/internal/database/models.go | 3 +- back/database/internal/database/songs.sql.go | 58 ++++++++++++++++++ back/database/main.go | 4 ++ back/database/pathConstants/paths.go | 1 + back/database/sql/queries/albums.sql | 3 + back/database/sql/queries/songs.sql | 7 +++ .../sql/schema/009_songs_duration_fix.sql | 7 +++ .../sql/schema/010_id_constraint_fix.sql | 7 +++ .../sql/schema/011_id_preview_fix.sql | 7 +++ 11 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 back/database/internal/database/songs.sql.go create mode 100644 back/database/sql/queries/songs.sql create mode 100644 back/database/sql/schema/009_songs_duration_fix.sql create mode 100644 back/database/sql/schema/010_id_constraint_fix.sql create mode 100644 back/database/sql/schema/011_id_preview_fix.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index f721e63..500415c 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -110,6 +110,66 @@ func (cfg *DbConfig) AddAlbumsDatabase(records [][]string) { fmt.Println("Finish adding albums!") } +func (cfg *DbConfig) AddSongsDatabase(records [][]string) { + fmt.Println("Addings songs to the database...") + for idx, songRecord := range records { + if idx == 0 { + continue + } + + songStrID := songRecord[0] + songName := songRecord[1] + songSpotify := songRecord[2] + songUrlPreview := songRecord[3] + songDurationStr := songRecord[4] + songYearStr := songRecord[5] + songAlbumIDStr := songRecord[6] + + songAlbumID, err := strconv.Atoi(songAlbumIDStr) + if err != nil { + log.Fatal(err) + } + + albumData, err := cfg.Queries.GetAlbumByID(context.Background(), int32(songAlbumID)) + if err != nil { + log.Fatalf("album with the ID: %v not in database: %v", songAlbumID, err) + } + + songDuration, err := strconv.ParseFloat(songDurationStr, 32) + if err != nil { + log.Fatal(err) + } + + songID, err := strconv.Atoi(songStrID) + if err != nil { + log.Fatal(err) + } + + songYear, err := strconv.Atoi(songYearStr) + if err != nil { + log.Fatal(err) + } + + addedSong, err := cfg.Queries.CreateSong(context.Background(), database.CreateSongParams{ + ID: int32(songID), + Name: songName, + SpotifyID: songSpotify, + UrlPreview: songUrlPreview, + Duration: float32(songDuration), + Year: int32(songYear), + AlbumID: albumData.ID, + }) + + if err != nil { + log.Fatalf("error while trying to add the song: %v", err) + } + + fmt.Println(addedSong) + } + + fmt.Println("Finish adding songs!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/albums.sql.go b/back/database/internal/database/albums.sql.go index 269cd44..c729254 100644 --- a/back/database/internal/database/albums.sql.go +++ b/back/database/internal/database/albums.sql.go @@ -36,3 +36,14 @@ func (q *Queries) CreateAlbum(ctx context.Context, arg CreateAlbumParams) (Album err := row.Scan(&i.ID, &i.Name, &i.UrlImage) return i, err } + +const getAlbumByID = `-- name: GetAlbumByID :one +SELECT id, name, url_image FROM albums WHERE id = $1 +` + +func (q *Queries) GetAlbumByID(ctx context.Context, id int32) (Album, error) { + row := q.db.QueryRowContext(ctx, getAlbumByID, id) + var i Album + err := row.Scan(&i.ID, &i.Name, &i.UrlImage) + return i, err +} diff --git a/back/database/internal/database/models.go b/back/database/internal/database/models.go index e25c863..39bcae8 100644 --- a/back/database/internal/database/models.go +++ b/back/database/internal/database/models.go @@ -25,8 +25,9 @@ type Song struct { Name string SpotifyID string UrlPreview string - Duration int32 + Duration float32 Year int32 + AlbumID int32 } type SongArtist struct { diff --git a/back/database/internal/database/songs.sql.go b/back/database/internal/database/songs.sql.go new file mode 100644 index 0000000..ebe0650 --- /dev/null +++ b/back/database/internal/database/songs.sql.go @@ -0,0 +1,58 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: songs.sql + +package database + +import ( + "context" +) + +const cleanSongs = `-- name: CleanSongs :exec +DELETE FROM songs +` + +func (q *Queries) CleanSongs(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanSongs) + return err +} + +const createSong = `-- name: CreateSong :one +INSERT INTO songs(id, name, spotify_id, url_preview, duration, year, album_id) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id, name, spotify_id, url_preview, duration, year, album_id +` + +type CreateSongParams struct { + ID int32 + Name string + SpotifyID string + UrlPreview string + Duration float32 + Year int32 + AlbumID int32 +} + +func (q *Queries) CreateSong(ctx context.Context, arg CreateSongParams) (Song, error) { + row := q.db.QueryRowContext(ctx, createSong, + arg.ID, + arg.Name, + arg.SpotifyID, + arg.UrlPreview, + arg.Duration, + arg.Year, + arg.AlbumID, + ) + var i Song + err := row.Scan( + &i.ID, + &i.Name, + &i.SpotifyID, + &i.UrlPreview, + &i.Duration, + &i.Year, + &i.AlbumID, + ) + return i, err +} diff --git a/back/database/main.go b/back/database/main.go index 669b9fa..951dfcd 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -48,6 +48,10 @@ func main() { { databaseConfig.AddToDatabase(paths.AlbumsPath, dbCfg.AddAlbumsDatabase) } + case "songs": + { + databaseConfig.AddToDatabase(paths.SongsPath, dbCfg.AddSongsDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index d67a397..635d640 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -5,3 +5,4 @@ import "path/filepath" var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") var GenresPath = filepath.Join("csv_data", "genres_rows.csv") var AlbumsPath = filepath.Join("csv_data", "albums_rows.csv") +var SongsPath = filepath.Join("csv_data", "songs_rows.csv") diff --git a/back/database/sql/queries/albums.sql b/back/database/sql/queries/albums.sql index 29e6d3a..960b124 100644 --- a/back/database/sql/queries/albums.sql +++ b/back/database/sql/queries/albums.sql @@ -3,5 +3,8 @@ INSERT INTO albums(id, name, url_image) VALUES($1, $2, $3) RETURNING *; +-- name: GetAlbumByID :one +SELECT * FROM albums WHERE id = $1; + -- name: CleanAlbums :exec DELETE FROM albums; \ No newline at end of file diff --git a/back/database/sql/queries/songs.sql b/back/database/sql/queries/songs.sql new file mode 100644 index 0000000..133b906 --- /dev/null +++ b/back/database/sql/queries/songs.sql @@ -0,0 +1,7 @@ +-- name: CreateSong :one +INSERT INTO songs(id, name, spotify_id, url_preview, duration, year, album_id) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING *; + +-- name: CleanSongs :exec +DELETE FROM songs; \ No newline at end of file diff --git a/back/database/sql/schema/009_songs_duration_fix.sql b/back/database/sql/schema/009_songs_duration_fix.sql new file mode 100644 index 0000000..0c4062f --- /dev/null +++ b/back/database/sql/schema/009_songs_duration_fix.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE songs +ALTER COLUMN duration TYPE REAL; + +-- +goose Down +ALTER TABLE songs +ALTER COLUMN duration TYPE INTEGER; \ No newline at end of file diff --git a/back/database/sql/schema/010_id_constraint_fix.sql b/back/database/sql/schema/010_id_constraint_fix.sql new file mode 100644 index 0000000..d7209af --- /dev/null +++ b/back/database/sql/schema/010_id_constraint_fix.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE songs +DROP CONSTRAINT songs_spotify_id_key; + +-- +goose Down +ALTER TABLE songs +ADD CONSTRAINT UNIQUE; diff --git a/back/database/sql/schema/011_id_preview_fix.sql b/back/database/sql/schema/011_id_preview_fix.sql new file mode 100644 index 0000000..51c5e88 --- /dev/null +++ b/back/database/sql/schema/011_id_preview_fix.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE songs +DROP CONSTRAINT songs_url_preview_key; + +-- +goose Down +ALTER TABLE songs +ADD CONSTRAINT UNIQUE; \ No newline at end of file From 65d43e0d3a0a7fafb1a54d0d5ee50845719e7c9a Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 20:18:38 -0400 Subject: [PATCH 16/38] r adding cmd to add the song_artists relationship --- back/database/dbConfig/databaseConfig.go | 51 +++++++++++++++++++ .../database/internal/database/artists.sql.go | 6 +-- .../internal/database/song_artists.sql.go | 29 +++++++++++ back/database/internal/database/songs.sql.go | 19 +++++++ back/database/main.go | 4 ++ back/database/pathConstants/paths.go | 1 + back/database/sql/queries/artists.sql | 2 +- back/database/sql/queries/song_artists.sql | 4 ++ back/database/sql/queries/songs.sql | 3 ++ 9 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 back/database/internal/database/song_artists.sql.go create mode 100644 back/database/sql/queries/song_artists.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 500415c..a495857 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -170,6 +170,57 @@ func (cfg *DbConfig) AddSongsDatabase(records [][]string) { fmt.Println("Finish adding songs!") } +func (cfg *DbConfig) AddSongsArtistsDatabase(records [][]string) { + fmt.Println("Processing the relationship between song and artists...") + for idx, record := range records { + if idx == 0 { + continue + } + + songArtistStrID := record[0] + songStrID := record[1] + artistStrID := record[2] + + songArtistID, err := strconv.Atoi(songArtistStrID) + if err != nil { + log.Fatal(err) + } + + songID, err := strconv.Atoi(songStrID) + if err != nil { + log.Fatal(err) + } + + artistID, err := strconv.Atoi(artistStrID) + if err != nil { + log.Fatal(err) + } + + dbSong, err := cfg.Queries.GetSongByID(context.Background(), int32(songID)) + if err != nil { + log.Fatalf("error while trying to get the song: %v", err) + } + + dbArtist, err := cfg.Queries.GetArtistByID(context.Background(), int32(artistID)) + if err != nil { + log.Fatalf("error while trying to get the artist: %v", err) + } + + addedArtistSong, err := cfg.Queries.CreateSongArtist(context.Background(), database.CreateSongArtistParams{ + ID: int32(songArtistID), + SongID: dbSong.ID, + ArtistID: dbArtist.ID, + }) + + if err != nil { + log.Fatalf("error while trying to create the artist song rp: %v", err) + } + + fmt.Println(addedArtistSong) + } + fmt.Println("Finish processing the relationship!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/artists.sql.go b/back/database/internal/database/artists.sql.go index 1c2e4dd..b5d1b04 100644 --- a/back/database/internal/database/artists.sql.go +++ b/back/database/internal/database/artists.sql.go @@ -36,12 +36,12 @@ func (q *Queries) CreateArtist(ctx context.Context, arg CreateArtistParams) (Art return i, err } -const getArtist = `-- name: GetArtist :one +const getArtistByID = `-- name: GetArtistByID :one SELECT id, name FROM artists WHERE id = $1 ` -func (q *Queries) GetArtist(ctx context.Context, id int32) (Artist, error) { - row := q.db.QueryRowContext(ctx, getArtist, id) +func (q *Queries) GetArtistByID(ctx context.Context, id int32) (Artist, error) { + row := q.db.QueryRowContext(ctx, getArtistByID, id) var i Artist err := row.Scan(&i.ID, &i.Name) return i, err diff --git a/back/database/internal/database/song_artists.sql.go b/back/database/internal/database/song_artists.sql.go new file mode 100644 index 0000000..8b5eeb3 --- /dev/null +++ b/back/database/internal/database/song_artists.sql.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: song_artists.sql + +package database + +import ( + "context" +) + +const createSongArtist = `-- name: CreateSongArtist :one +INSERT INTO song_artists(id, song_id, artist_id) +VALUES($1, $2, $3) +RETURNING id, song_id, artist_id +` + +type CreateSongArtistParams struct { + ID int32 + SongID int32 + ArtistID int32 +} + +func (q *Queries) CreateSongArtist(ctx context.Context, arg CreateSongArtistParams) (SongArtist, error) { + row := q.db.QueryRowContext(ctx, createSongArtist, arg.ID, arg.SongID, arg.ArtistID) + var i SongArtist + err := row.Scan(&i.ID, &i.SongID, &i.ArtistID) + return i, err +} diff --git a/back/database/internal/database/songs.sql.go b/back/database/internal/database/songs.sql.go index ebe0650..87c8ef8 100644 --- a/back/database/internal/database/songs.sql.go +++ b/back/database/internal/database/songs.sql.go @@ -56,3 +56,22 @@ func (q *Queries) CreateSong(ctx context.Context, arg CreateSongParams) (Song, e ) return i, err } + +const getSongByID = `-- name: GetSongByID :one +SELECT id, name, spotify_id, url_preview, duration, year, album_id FROM songs WHERE id = $1 +` + +func (q *Queries) GetSongByID(ctx context.Context, id int32) (Song, error) { + row := q.db.QueryRowContext(ctx, getSongByID, id) + var i Song + err := row.Scan( + &i.ID, + &i.Name, + &i.SpotifyID, + &i.UrlPreview, + &i.Duration, + &i.Year, + &i.AlbumID, + ) + return i, err +} diff --git a/back/database/main.go b/back/database/main.go index 951dfcd..16e070e 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -52,6 +52,10 @@ func main() { { databaseConfig.AddToDatabase(paths.SongsPath, dbCfg.AddSongsDatabase) } + case "song_artists": + { + databaseConfig.AddToDatabase(paths.SongArtistsPath, dbCfg.AddSongsArtistsDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index 635d640..70bf147 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -6,3 +6,4 @@ var ARTISTPATH = filepath.Join("csv_data", "artists_rows.csv") var GenresPath = filepath.Join("csv_data", "genres_rows.csv") var AlbumsPath = filepath.Join("csv_data", "albums_rows.csv") var SongsPath = filepath.Join("csv_data", "songs_rows.csv") +var SongArtistsPath = filepath.Join("csv_data", "song_artists_rp_rows.csv") diff --git a/back/database/sql/queries/artists.sql b/back/database/sql/queries/artists.sql index 4a61974..fd4aa8b 100644 --- a/back/database/sql/queries/artists.sql +++ b/back/database/sql/queries/artists.sql @@ -3,7 +3,7 @@ INSERT INTO artists (id, name) VALUES ($1, $2) RETURNING *; --- name: GetArtist :one +-- name: GetArtistByID :one SELECT * FROM artists WHERE id = $1; -- name: CleanArtists :exec diff --git a/back/database/sql/queries/song_artists.sql b/back/database/sql/queries/song_artists.sql new file mode 100644 index 0000000..6454b23 --- /dev/null +++ b/back/database/sql/queries/song_artists.sql @@ -0,0 +1,4 @@ +-- name: CreateSongArtist :one +INSERT INTO song_artists(id, song_id, artist_id) +VALUES($1, $2, $3) +RETURNING *; \ No newline at end of file diff --git a/back/database/sql/queries/songs.sql b/back/database/sql/queries/songs.sql index 133b906..72dc0a2 100644 --- a/back/database/sql/queries/songs.sql +++ b/back/database/sql/queries/songs.sql @@ -3,5 +3,8 @@ INSERT INTO songs(id, name, spotify_id, url_preview, duration, year, album_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; +-- name: GetSongByID :one +SELECT * FROM songs WHERE id = $1; + -- name: CleanSongs :exec DELETE FROM songs; \ No newline at end of file From a6e1829c27f3e48365fab08f0d482fe7cb0c75d1 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 7 Feb 2026 20:41:14 -0400 Subject: [PATCH 17/38] r adding cmd to add the song genres rp to the local DB --- back/database/dbConfig/databaseConfig.go | 50 +++++++++++++++++++ back/database/internal/database/genres.sql.go | 11 ++++ .../internal/database/song_genres.sql.go | 29 +++++++++++ back/database/main.go | 4 ++ back/database/pathConstants/paths.go | 1 + back/database/sql/queries/genres.sql | 5 +- back/database/sql/queries/song_genres.sql | 4 ++ 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 back/database/internal/database/song_genres.sql.go create mode 100644 back/database/sql/queries/song_genres.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index a495857..123cac3 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -221,6 +221,56 @@ func (cfg *DbConfig) AddSongsArtistsDatabase(records [][]string) { fmt.Println("Finish processing the relationship!") } +func (cfg *DbConfig) AddSongGenresDatabase(records [][]string) { + fmt.Println("Processing the song genres relationship...") + for idx, record := range records { + if idx == 0 { + continue + } + + songGenreStrID := record[0] + songStrID := record[1] + genreStrID := record[2] + + songGenreID, err := strconv.Atoi(songGenreStrID) + if err != nil { + log.Fatal(err) + } + + songID, err := strconv.Atoi(songStrID) + if err != nil { + log.Fatal(err) + } + + genreID, err := strconv.Atoi(genreStrID) + if err != nil { + log.Fatal(err) + } + + genreDB, err := cfg.Queries.GetGenreByID(context.Background(), int32(genreID)) + if err != nil { + log.Fatal(err) + } + + songDB, err := cfg.Queries.GetSongByID(context.Background(), int32(songID)) + if err != nil { + log.Fatal(err) + } + + addedSongGenre, err := cfg.Queries.CreateSongGenre(context.Background(), database.CreateSongGenreParams{ + ID: int32(songGenreID), + SongID: songDB.ID, + GenreID: genreDB.ID, + }) + + if err != nil { + log.Fatalf("error while trying to add a song genre rp: %v", err) + } + fmt.Println(addedSongGenre) + } + fmt.Println("Finish processing the relationship!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/genres.sql.go b/back/database/internal/database/genres.sql.go index 39d8bd1..1c12c06 100644 --- a/back/database/internal/database/genres.sql.go +++ b/back/database/internal/database/genres.sql.go @@ -26,3 +26,14 @@ func (q *Queries) CreateGenre(ctx context.Context, arg CreateGenreParams) (Genre err := row.Scan(&i.ID, &i.Genre) return i, err } + +const getGenreByID = `-- name: GetGenreByID :one +SELECT id, genre FROM genres WHERE id = $1 +` + +func (q *Queries) GetGenreByID(ctx context.Context, id int32) (Genre, error) { + row := q.db.QueryRowContext(ctx, getGenreByID, id) + var i Genre + err := row.Scan(&i.ID, &i.Genre) + return i, err +} diff --git a/back/database/internal/database/song_genres.sql.go b/back/database/internal/database/song_genres.sql.go new file mode 100644 index 0000000..2ac08a8 --- /dev/null +++ b/back/database/internal/database/song_genres.sql.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: song_genres.sql + +package database + +import ( + "context" +) + +const createSongGenre = `-- name: CreateSongGenre :one +INSERT INTO song_genres(id, song_id, genre_id) +VALUES ($1, $2, $3) +RETURNING id, song_id, genre_id +` + +type CreateSongGenreParams struct { + ID int32 + SongID int32 + GenreID int32 +} + +func (q *Queries) CreateSongGenre(ctx context.Context, arg CreateSongGenreParams) (SongGenre, error) { + row := q.db.QueryRowContext(ctx, createSongGenre, arg.ID, arg.SongID, arg.GenreID) + var i SongGenre + err := row.Scan(&i.ID, &i.SongID, &i.GenreID) + return i, err +} diff --git a/back/database/main.go b/back/database/main.go index 16e070e..b921756 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -56,6 +56,10 @@ func main() { { databaseConfig.AddToDatabase(paths.SongArtistsPath, dbCfg.AddSongsArtistsDatabase) } + case "song_genres": + { + databaseConfig.AddToDatabase(paths.SongGenresPath, dbCfg.AddSongGenresDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index 70bf147..1b06b92 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -7,3 +7,4 @@ var GenresPath = filepath.Join("csv_data", "genres_rows.csv") var AlbumsPath = filepath.Join("csv_data", "albums_rows.csv") var SongsPath = filepath.Join("csv_data", "songs_rows.csv") var SongArtistsPath = filepath.Join("csv_data", "song_artists_rp_rows.csv") +var SongGenresPath = filepath.Join("csv_data", "song_genres_rp_rows.csv") diff --git a/back/database/sql/queries/genres.sql b/back/database/sql/queries/genres.sql index 209db64..bd55573 100644 --- a/back/database/sql/queries/genres.sql +++ b/back/database/sql/queries/genres.sql @@ -1,4 +1,7 @@ -- name: CreateGenre :one INSERT INTO genres(id, genre) VALUES($1, $2) -RETURNING *; \ No newline at end of file +RETURNING *; + +-- name: GetGenreByID :one +SELECT * FROM genres WHERE id = $1; \ No newline at end of file diff --git a/back/database/sql/queries/song_genres.sql b/back/database/sql/queries/song_genres.sql new file mode 100644 index 0000000..949236f --- /dev/null +++ b/back/database/sql/queries/song_genres.sql @@ -0,0 +1,4 @@ +-- name: CreateSongGenre :one +INSERT INTO song_genres(id, song_id, genre_id) +VALUES ($1, $2, $3) +RETURNING *; \ No newline at end of file From 84ea83673f54b4459659010a595b26a3e91190b7 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 9 Feb 2026 10:16:06 -0400 Subject: [PATCH 18/38] r adding song details to the database --- back/database/dbConfig/databaseConfig.go | 124 ++++++++++++++++++ .../internal/database/song_details.sql.go | 80 +++++++++++ back/database/main.go | 6 +- back/database/pathConstants/paths.go | 1 + back/database/sql/queries/song_details.sql | 8 ++ 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 back/database/internal/database/song_details.sql.go create mode 100644 back/database/sql/queries/song_details.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 123cac3..f7ec132 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -266,11 +266,135 @@ func (cfg *DbConfig) AddSongGenresDatabase(records [][]string) { if err != nil { log.Fatalf("error while trying to add a song genre rp: %v", err) } + fmt.Println(addedSongGenre) } fmt.Println("Finish processing the relationship!") } +func (cfg *DbConfig) AddSongDetailsDatabase(records [][]string) { + fmt.Println("Processing the song details...") + for idx, record := range records { + if idx == 0 { + continue + } + + songDetailsStrID := record[0] + songStrID := record[1] + danceabilityStr := record[2] + energyStr := record[3] + track_keyStr := record[4] + loudnessStr := record[5] + modeStr := record[6] + speechinessStr := record[7] + acousticnessStr := record[8] + instrumentalnessStr := record[9] + livenessStr := record[10] + valenceStr := record[11] + tempoStr := record[12] + time_signatureStr := record[13] + + songDetailsID, err := strconv.Atoi(songDetailsStrID) + if err != nil { + log.Fatal(err) + } + + songID, err := strconv.Atoi(songStrID) + if err != nil { + log.Fatal(err) + } + + dbSong, err := cfg.Queries.GetSongByID(context.Background(), int32(songID)) + if err != nil { + log.Fatal(err) + } + + danceability, err := strconv.ParseFloat(danceabilityStr, 32) + if err != nil { + log.Fatal(err) + } + + energy, err := strconv.ParseFloat(energyStr, 32) + if err != nil { + log.Fatal(err) + } + + trackKey, err := strconv.ParseFloat(track_keyStr, 32) + if err != nil { + log.Fatal(err) + } + + loudness, err := strconv.ParseFloat(loudnessStr, 32) + if err != nil { + log.Fatal(err) + } + + mode, err := strconv.ParseFloat(modeStr, 32) + if err != nil { + log.Fatal(err) + } + + speechiness, err := strconv.ParseFloat(speechinessStr, 32) + if err != nil { + log.Fatal(err) + } + + acousticness, err := strconv.ParseFloat(acousticnessStr, 32) + if err != nil { + log.Fatal(err) + } + + instrumentalness, err := strconv.ParseFloat(instrumentalnessStr, 32) + if err != nil { + log.Fatal(err) + } + + liveness, err := strconv.ParseFloat(livenessStr, 32) + if err != nil { + log.Fatal(err) + } + + valence, err := strconv.ParseFloat(valenceStr, 32) + if err != nil { + log.Fatal(err) + } + + tempo, err := strconv.ParseFloat(tempoStr, 32) + if err != nil { + log.Fatal(err) + } + + timeSignature, err := strconv.Atoi(time_signatureStr) + if err != nil { + log.Fatal(err) + } + + added_details, err := cfg.Queries.CreateSongDetails(context.Background(), database.CreateSongDetailsParams{ + ID: int32(songDetailsID), + SongID: dbSong.ID, + Danceability: float32(danceability), + Energy: float32(energy), + TrackKey: float32(trackKey), + Loudness: float32(loudness), + Mode: float32(mode), + Speechiness: float32(speechiness), + Acousticness: acousticness, + Instrumentalness: instrumentalness, + Liveness: float32(liveness), + Valence: float32(valence), + Tempo: float32(tempo), + TimeSignature: int32(timeSignature), + }) + + if err != nil { + log.Fatalf("error while trying to add the song details: %v", err) + } + + fmt.Println(added_details) + } + fmt.Println("Finish processing the song details!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/internal/database/song_details.sql.go b/back/database/internal/database/song_details.sql.go new file mode 100644 index 0000000..b8bf356 --- /dev/null +++ b/back/database/internal/database/song_details.sql.go @@ -0,0 +1,80 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: song_details.sql + +package database + +import ( + "context" +) + +const cleanSongDetails = `-- name: CleanSongDetails :exec +DELETE FROM song_details +` + +func (q *Queries) CleanSongDetails(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, cleanSongDetails) + return err +} + +const createSongDetails = `-- name: CreateSongDetails :one +INSERT INTO song_details (id, song_id, danceability, energy, track_key, loudness, +mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) +RETURNING id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature +` + +type CreateSongDetailsParams struct { + ID int32 + SongID int32 + Danceability float32 + Energy float32 + TrackKey float32 + Loudness float32 + Mode float32 + Speechiness float32 + Acousticness float64 + Instrumentalness float64 + Liveness float32 + Valence float32 + Tempo float32 + TimeSignature int32 +} + +func (q *Queries) CreateSongDetails(ctx context.Context, arg CreateSongDetailsParams) (SongDetail, error) { + row := q.db.QueryRowContext(ctx, createSongDetails, + arg.ID, + arg.SongID, + arg.Danceability, + arg.Energy, + arg.TrackKey, + arg.Loudness, + arg.Mode, + arg.Speechiness, + arg.Acousticness, + arg.Instrumentalness, + arg.Liveness, + arg.Valence, + arg.Tempo, + arg.TimeSignature, + ) + var i SongDetail + err := row.Scan( + &i.ID, + &i.SongID, + &i.Danceability, + &i.Energy, + &i.TrackKey, + &i.Loudness, + &i.Mode, + &i.Speechiness, + &i.Acousticness, + &i.Instrumentalness, + &i.Liveness, + &i.Valence, + &i.Tempo, + &i.TimeSignature, + ) + return i, err +} diff --git a/back/database/main.go b/back/database/main.go index b921756..93f26a5 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -31,7 +31,7 @@ func main() { } defer db.Close() - dbCfg := databaseConfig.DbConfig{ + dbCfg := &databaseConfig.DbConfig{ Queries: database.New(db), } @@ -60,6 +60,10 @@ func main() { { databaseConfig.AddToDatabase(paths.SongGenresPath, dbCfg.AddSongGenresDatabase) } + case "song_details": + { + databaseConfig.AddToDatabase(paths.SongDetailsPath, dbCfg.AddSongDetailsDatabase) + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/pathConstants/paths.go b/back/database/pathConstants/paths.go index 1b06b92..242810f 100644 --- a/back/database/pathConstants/paths.go +++ b/back/database/pathConstants/paths.go @@ -8,3 +8,4 @@ var AlbumsPath = filepath.Join("csv_data", "albums_rows.csv") var SongsPath = filepath.Join("csv_data", "songs_rows.csv") var SongArtistsPath = filepath.Join("csv_data", "song_artists_rp_rows.csv") var SongGenresPath = filepath.Join("csv_data", "song_genres_rp_rows.csv") +var SongDetailsPath = filepath.Join("csv_data", "song_details_rows.csv") diff --git a/back/database/sql/queries/song_details.sql b/back/database/sql/queries/song_details.sql new file mode 100644 index 0000000..14ba66d --- /dev/null +++ b/back/database/sql/queries/song_details.sql @@ -0,0 +1,8 @@ +-- name: CreateSongDetails :one +INSERT INTO song_details (id, song_id, danceability, energy, track_key, loudness, +mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) +RETURNING *; + +-- name: CleanSongDetails :exec +DELETE FROM song_details; \ No newline at end of file From 87211f7c76a42a731dda895c07d83e2b0a47172e Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 9 Feb 2026 16:53:49 -0400 Subject: [PATCH 19/38] r adding vectors to the song_details table --- back/database/dbConfig/databaseConfig.go | 47 +++++++++++ back/database/go.mod | 2 + back/database/go.sum | 56 +++++++++++++ back/database/internal/database/models.go | 1 + .../internal/database/song_details.sql.go | 81 ++++++++++++++++++- back/database/main.go | 4 + back/database/sql/queries/song_details.sql | 9 +++ back/database/sql/schema/012_add_vectors.sql | 7 ++ 8 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 back/database/sql/schema/012_add_vectors.sql diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index f7ec132..358d819 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -8,6 +8,8 @@ import ( "os" "scripts/internal/database" "strconv" + + "github.com/pgvector/pgvector-go" ) type DbConfig struct { @@ -395,6 +397,51 @@ func (cfg *DbConfig) AddSongDetailsDatabase(records [][]string) { fmt.Println("Finish processing the song details!") } +func (cfg *DbConfig) AddVectorsDatabase() { + fmt.Println("Processing vector to the database...") + songDetails, err := cfg.Queries.GetSongDetails(context.Background()) + if err != nil { + log.Fatalf("there was a problem while trying to get the songs details: %v", err) + } + + for _, song := range songDetails { + dbSong, err := cfg.Queries.GetSongByID(context.Background(), song.SongID) + if err != nil { + log.Fatalf("there was a problem while trying to get the song: %v", err) + } + + songParams := []float32{ + dbSong.Duration, + song.Danceability, + song.Energy, + song.TrackKey, + song.Loudness, + song.Mode, + song.Speechiness, + float32(song.Acousticness), + float32(song.Instrumentalness), + song.Liveness, + song.Valence, + song.Tempo, + float32(song.TimeSignature), + } + + embedding := pgvector.NewVector(songParams) + + vectorAdded, err := cfg.Queries.CreateVector(context.Background(), database.CreateVectorParams{ + Vectors: embedding, + SongID: song.SongID, + }) + + if err != nil { + log.Fatal(err) + } + + fmt.Println(vectorAdded) + } + fmt.Println("Finish processing the vectors!") +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/database/go.mod b/back/database/go.mod index 8722e75..23747ef 100644 --- a/back/database/go.mod +++ b/back/database/go.mod @@ -3,3 +3,5 @@ module scripts go 1.25.3 require github.com/lib/pq v1.11.1 + +require github.com/pgvector/pgvector-go v0.3.0 diff --git a/back/database/go.sum b/back/database/go.sum index 3ad08b1..7f6661d 100644 --- a/back/database/go.sum +++ b/back/database/go.sum @@ -1,2 +1,58 @@ +entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= +entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= +github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc= +github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= +github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= +github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= +github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= +github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= +github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= +mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= diff --git a/back/database/internal/database/models.go b/back/database/internal/database/models.go index 39bcae8..919730c 100644 --- a/back/database/internal/database/models.go +++ b/back/database/internal/database/models.go @@ -51,6 +51,7 @@ type SongDetail struct { Valence float32 Tempo float32 TimeSignature int32 + Vectors interface{} } type SongGenre struct { diff --git a/back/database/internal/database/song_details.sql.go b/back/database/internal/database/song_details.sql.go index b8bf356..71943dc 100644 --- a/back/database/internal/database/song_details.sql.go +++ b/back/database/internal/database/song_details.sql.go @@ -22,7 +22,7 @@ const createSongDetails = `-- name: CreateSongDetails :one INSERT INTO song_details (id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) -RETURNING id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature +RETURNING id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature, vectors ` type CreateSongDetailsParams struct { @@ -75,6 +75,85 @@ func (q *Queries) CreateSongDetails(ctx context.Context, arg CreateSongDetailsPa &i.Valence, &i.Tempo, &i.TimeSignature, + &i.Vectors, ) return i, err } + +const createVector = `-- name: CreateVector :one +UPDATE song_details +SET vectors = $1 +WHERE song_id = $2 +RETURNING id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature, vectors +` + +type CreateVectorParams struct { + Vectors interface{} + SongID int32 +} + +func (q *Queries) CreateVector(ctx context.Context, arg CreateVectorParams) (SongDetail, error) { + row := q.db.QueryRowContext(ctx, createVector, arg.Vectors, arg.SongID) + var i SongDetail + err := row.Scan( + &i.ID, + &i.SongID, + &i.Danceability, + &i.Energy, + &i.TrackKey, + &i.Loudness, + &i.Mode, + &i.Speechiness, + &i.Acousticness, + &i.Instrumentalness, + &i.Liveness, + &i.Valence, + &i.Tempo, + &i.TimeSignature, + &i.Vectors, + ) + return i, err +} + +const getSongDetails = `-- name: GetSongDetails :many +SELECT id, song_id, danceability, energy, track_key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, time_signature, vectors FROM song_details +` + +func (q *Queries) GetSongDetails(ctx context.Context) ([]SongDetail, error) { + rows, err := q.db.QueryContext(ctx, getSongDetails) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SongDetail + for rows.Next() { + var i SongDetail + if err := rows.Scan( + &i.ID, + &i.SongID, + &i.Danceability, + &i.Energy, + &i.TrackKey, + &i.Loudness, + &i.Mode, + &i.Speechiness, + &i.Acousticness, + &i.Instrumentalness, + &i.Liveness, + &i.Valence, + &i.Tempo, + &i.TimeSignature, + &i.Vectors, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/back/database/main.go b/back/database/main.go index 93f26a5..319b467 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -64,6 +64,10 @@ func main() { { databaseConfig.AddToDatabase(paths.SongDetailsPath, dbCfg.AddSongDetailsDatabase) } + case "vectors": + { + dbCfg.AddVectorsDatabase() + } default: { fmt.Printf("'%v' is not a valid command\n", os.Args[1]) diff --git a/back/database/sql/queries/song_details.sql b/back/database/sql/queries/song_details.sql index 14ba66d..5cd3490 100644 --- a/back/database/sql/queries/song_details.sql +++ b/back/database/sql/queries/song_details.sql @@ -4,5 +4,14 @@ mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, tim VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *; +-- name: CreateVector :one +UPDATE song_details +SET vectors = $1 +WHERE song_id = $2 +RETURNING *; + +-- name: GetSongDetails :many +SELECT * FROM song_details; + -- name: CleanSongDetails :exec DELETE FROM song_details; \ No newline at end of file diff --git a/back/database/sql/schema/012_add_vectors.sql b/back/database/sql/schema/012_add_vectors.sql new file mode 100644 index 0000000..9b1a546 --- /dev/null +++ b/back/database/sql/schema/012_add_vectors.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE song_details +ADD COLUMN vectors vector(13); + +-- +goose Down +ALTER TABLE song_details +DROP COLUMN vectors; From ffaf397dcda8216ca398e9451fd85133a6f41dca Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 9 Feb 2026 17:02:04 -0400 Subject: [PATCH 20/38] r changing variables names to work with the local database --- back/models/song_artists/songArtists.model.ts | 2 +- back/models/song_genres/SongGenres.model.ts | 2 +- back/src/app.module.ts | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/back/models/song_artists/songArtists.model.ts b/back/models/song_artists/songArtists.model.ts index 36ade22..15f5592 100644 --- a/back/models/song_artists/songArtists.model.ts +++ b/back/models/song_artists/songArtists.model.ts @@ -5,7 +5,7 @@ import { AllowNull, AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Mode import { SongArtistsAttributes, SongArtistsCreationAttributes } from "src/types/songArtistsAttributes"; @Table({ - tableName: "song_artists_rp", + tableName: "song_artists", underscored: true, timestamps: false }) export class SongArtistsModel extends Model { diff --git a/back/models/song_genres/SongGenres.model.ts b/back/models/song_genres/SongGenres.model.ts index d1a9dfe..aee882c 100644 --- a/back/models/song_genres/SongGenres.model.ts +++ b/back/models/song_genres/SongGenres.model.ts @@ -5,7 +5,7 @@ import { AllowNull, AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Mode import { SongGenresAttributes, SongGenresCreationAttributes } from "src/types/songGenresAttributes"; @Table({ - tableName: "song_genres_rp", + tableName: "song_genres", underscored: true, timestamps: false }) export class SongGenresModel extends Model { diff --git a/back/src/app.module.ts b/back/src/app.module.ts index c88c472..f5264c5 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -27,11 +27,8 @@ dotenv.config(); rootPath: join(__dirname, "..", "..", "public") }), SequelizeModule.forRoot({ - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, dialect: 'postgres', - host: process.env.DB_HOST, + uri: "postgresql://postgres:postgres@localhost:5432/music_db", port: Number(process.env.DB_PORT), dialectOptions: { ssl: { From 684ef18b9e151f68543a92d983dd6cbc44f28ed1 Mon Sep 17 00:00:00 2001 From: Raghart Date: Tue, 10 Feb 2026 16:38:45 -0400 Subject: [PATCH 21/38] r testing the Query Cosine recommendation of PostgreSQL --- back/models/song_details/SongDetails.model.ts | 5 ++++ back/src/app.module.ts | 9 ++++-- back/src/songs/songs.resolver.ts | 2 ++ back/src/songs/songs.service.ts | 28 ++++++++++++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/back/models/song_details/SongDetails.model.ts b/back/models/song_details/SongDetails.model.ts index aaf847b..2479603 100644 --- a/back/models/song_details/SongDetails.model.ts +++ b/back/models/song_details/SongDetails.model.ts @@ -140,4 +140,9 @@ import { SongDetailsAttributes, SongDetailsCreationAttributes } from "src/types/ } }) declare time_signature: number; + + @Column({ + type: DataType.TSVECTOR, + }) + declare vectors: number[]; } \ No newline at end of file diff --git a/back/src/app.module.ts b/back/src/app.module.ts index f5264c5..c03079c 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -23,9 +23,6 @@ dotenv.config(); ConfigModule.forRoot({ isGlobal: true, }), - ServeStaticModule.forRoot({ - rootPath: join(__dirname, "..", "..", "public") - }), SequelizeModule.forRoot({ dialect: 'postgres', uri: "postgresql://postgres:postgres@localhost:5432/music_db", @@ -58,3 +55,9 @@ dotenv.config(); providers: [AppService], }) export class AppModule {} + +/* +ServeStaticModule.forRoot({ + rootPath: join(__dirname, "..", "..", "public") + }) +*/ diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index 19bef92..b4caa04 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -34,6 +34,8 @@ export class SongsResolver { @Args('userVector', { type: () => [Float], defaultValue: USER_VECTOR }) userVector: number[], ): Promise { + this.songsService.fetchIACosRecommendations(genres) + return this.songsService.getIARecommendations(genres, userVector); } diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index af951b4..e17d0c9 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -5,7 +5,7 @@ import { ServiceUnavailableException, } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; -import { Op, Sequelize } from 'sequelize'; +import { Op, QueryTypes, Sequelize } from 'sequelize'; import { buildSongVector } from './utils/buildSongVector'; import { parseFullSongResponse, @@ -135,6 +135,32 @@ export class SongsService { return this.parseFullSong(songData); } + async fetchIACosRecommendations(genres: string[]) { + const rawSongData = await this.songModel.sequelize?.query(`SELECT songs.id, + songs.name, + string_agg(artists.name, ', ') AS artists, + songs.url_preview, + albums.url_image AS album_cover, + 1 - (song_details.vectors <=> '[3.39,0.373,0.86,11,-4.869,0,0.0533,0.009039999917149544,2.8000000384054147e-05,0.228,0.48,175.103,4]') AS cos_sim + FROM songs + JOIN song_details ON songs.id = song_details.song_id + JOIN albums ON albums.id = songs.album_id + JOIN song_artists ON songs.id = song_artists.song_id + JOIN artists ON song_artists.artist_id = artists.id + JOIN song_genres ON songs.id = song_genres.song_id + JOIN genres ON song_genres.genre_id = genres.id + WHERE genres.genre IN(:genres) + GROUP BY songs.id, songs.name, songs.url_preview, albums.url_image, song_details.vectors + ORDER BY cos_sim DESC + LIMIT 10;`, { + type: QueryTypes.SELECT, + logging: console.log, + replacements:{ genres } + }) + + console.log(rawSongData) + } + async fetchIARecommendations(genres: string[]): Promise { const rawSongData = await safeQuery(() => this.songModel.findAll({ From 548cc9dbe2570831e5baf30b73f0868716715a95 Mon Sep 17 00:00:00 2001 From: Raghart Date: Tue, 10 Feb 2026 20:58:56 -0400 Subject: [PATCH 22/38] r refactoring to make back use new cosine similarity --- back/src/songs/songs.resolver.ts | 8 +++----- back/src/songs/songs.service.ts | 23 ++++++++++++++++++----- back/src/types/parses.ts | 7 +++++++ back/src/types/songAttributes.ts | 9 +++++++++ back/src/types/verify.ts | 14 +++++++++++++- back/test/constants/constants.ts | 17 +++++++++++++++++ 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index b4caa04..dc9dee9 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -1,6 +1,6 @@ import { Args, Float, Int, Query, Resolver } from '@nestjs/graphql'; import { SongsService } from './songs.service'; -import { USER_VECTOR } from '../../test/constants/constants'; +import { TRUE_USER_VECTOR, USER_VECTOR } from '../../test/constants/constants'; import { FullSongResponseDto } from './dto/FullSongResponse.dto'; import { SongResponseDto } from './dto/SongResponse.dto'; @@ -31,12 +31,10 @@ export class SongsResolver { async getIARecommendations( @Args('genres', { type: () => [String], defaultValue: ['Rock'] }) genres: string[], - @Args('userVector', { type: () => [Float], defaultValue: USER_VECTOR }) + @Args('userVector', { type: () => [Float], defaultValue: TRUE_USER_VECTOR }) userVector: number[], ): Promise { - this.songsService.fetchIACosRecommendations(genres) - - return this.songsService.getIARecommendations(genres, userVector); + return this.songsService.fetchIACosRecommendations(genres, userVector) } @Query(() => SongResponseDto, { name: 'getRandomSong' }) diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index e17d0c9..4e73c32 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -8,6 +8,7 @@ import { InjectModel } from '@nestjs/sequelize'; import { Op, QueryTypes, Sequelize } from 'sequelize'; import { buildSongVector } from './utils/buildSongVector'; import { + parseCosSongData, parseFullSongResponse, parseIASongData, parseSongResponse, @@ -18,6 +19,7 @@ import { FullSongResponse, FullSongResponseAttributes, IASongResponse, + SongCosResponse, SongResponse, SongResponseAttributes, } from 'src/types/songAttributes'; @@ -135,13 +137,14 @@ export class SongsService { return this.parseFullSong(songData); } - async fetchIACosRecommendations(genres: string[]) { + async fetchIACosRecommendations(genres: string[], userVector: number[]) : Promise { + const parsedVector = `[${userVector.join(", ")}]` const rawSongData = await this.songModel.sequelize?.query(`SELECT songs.id, songs.name, - string_agg(artists.name, ', ') AS artists, + string_agg(DISTINCT artists.name, ', ') AS artists, songs.url_preview, albums.url_image AS album_cover, - 1 - (song_details.vectors <=> '[3.39,0.373,0.86,11,-4.869,0,0.0533,0.009039999917149544,2.8000000384054147e-05,0.228,0.48,175.103,4]') AS cos_sim + 1 - (song_details.vectors <=> :userVector) AS cos_sim FROM songs JOIN song_details ON songs.id = song_details.song_id JOIN albums ON albums.id = songs.album_id @@ -155,10 +158,20 @@ export class SongsService { LIMIT 10;`, { type: QueryTypes.SELECT, logging: console.log, - replacements:{ genres } + replacements:{ genres, userVector: [parsedVector] } }) - console.log(rawSongData) + + const parsedData = parseCosSongData(rawSongData) + + return parsedData.map(songData => ({ + id: songData.id, + name: songData.name, + artists: songData.artists.split(","), + url_preview: songData.url_preview, + album_cover: songData.album_cover, + + })) } async fetchIARecommendations(genres: string[]): Promise { diff --git a/back/src/types/parses.ts b/back/src/types/parses.ts index 53b56d3..e071f28 100644 --- a/back/src/types/parses.ts +++ b/back/src/types/parses.ts @@ -9,6 +9,7 @@ import { import { FullSongResponseAttributes, IASongResponse, + SongCosResponse, SongResponseAttributes, } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; @@ -26,6 +27,7 @@ import { isStringArray, isString, isNumber, + isSongCosData, } from './verify'; export const parseString = (text: unknown): string => { @@ -102,6 +104,11 @@ export const parseIASongData = (data: unknown): IASongResponse => { ); }; +export const parseCosSongData = (data: unknown): SongCosResponse[] => { + if (isSongCosData(data)) return data; + throw new BadRequestException("The data recieved didn't match with the expected song recommendation format") +} + export const parseArtistSearchData = (data: unknown): artistSearchResults => { if (isArtistSearchData(data)) return data; throw new BadRequestException( diff --git a/back/src/types/songAttributes.ts b/back/src/types/songAttributes.ts index b9fd24c..fa6627c 100644 --- a/back/src/types/songAttributes.ts +++ b/back/src/types/songAttributes.ts @@ -31,6 +31,15 @@ export interface SongResponse { url_preview: string; } +export interface SongCosResponse { + id: number; + name: string; + artists: string; + album_cover: string; + url_preview: string; + cos_sim: number; +} + export interface FullSongResponse { id: number; name: string; diff --git a/back/src/types/verify.ts b/back/src/types/verify.ts index c77dc80..d5798c6 100644 --- a/back/src/types/verify.ts +++ b/back/src/types/verify.ts @@ -5,7 +5,7 @@ import { artistSearchResults, songSearchResults, } from './searchTypes'; -import { FullSongResponseAttributes, IASongResponse } from './songAttributes'; +import { FullSongResponseAttributes, IASongResponse, SongCosResponse } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; export const isString = (text: unknown): text is string => { @@ -132,6 +132,18 @@ export const isFullSongResponse = ( ); }; +export const isSongCosData = (data: unknown): data is SongCosResponse[] => { + if (data === null || data === undefined) return false; + return (Array.isArray(data) && + "id" in data[0] && + "name" in data[0] && + "artists" in data[0] && + "url_preview" in data[0] && + "album_cover" in data[0] && + "cos_sim" in data[0] + ); +}; + export const isIASongData = (data: unknown): data is IASongResponse => { if (data === null || data === undefined) return false; diff --git a/back/test/constants/constants.ts b/back/test/constants/constants.ts index b86b90a..f7f781d 100644 --- a/back/test/constants/constants.ts +++ b/back/test/constants/constants.ts @@ -18,3 +18,20 @@ export const USER_VECTOR: [ number, number, ] = [0.5, 0.165, 0.5, 2.5, 0.5, 0.05, 1, 0.15]; + +export const TRUE_USER_VECTOR: [ +number,number,number,number,number,number,number,number,number,number,number,number,number, +] = [ + 3.39, + 0.373, + 0.86,11, + -4.869, + 0, + 0.0533, + 0.009039999917149544, + 2.8000000384054147e-05, + 0.228, + 0.48, + 175.103, + 4 +] From b9b0a14876505eec9791b3bb138a88720b7c824b Mon Sep 17 00:00:00 2001 From: Raghart Date: Tue, 10 Feb 2026 21:12:06 -0400 Subject: [PATCH 23/38] r added limit to control how many songs to recieve --- back/src/songs/songs.resolver.ts | 4 +++- back/src/songs/songs.service.ts | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index dc9dee9..c3b769b 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -33,8 +33,10 @@ export class SongsResolver { genres: string[], @Args('userVector', { type: () => [Float], defaultValue: TRUE_USER_VECTOR }) userVector: number[], + @Args('limit', {type: () => Int, defaultValue: 40}) + limit: number, ): Promise { - return this.songsService.fetchIACosRecommendations(genres, userVector) + return this.songsService.fetchIACosRecommendations(genres, userVector, limit) } @Query(() => SongResponseDto, { name: 'getRandomSong' }) diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index 4e73c32..cd10eaa 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -137,7 +137,8 @@ export class SongsService { return this.parseFullSong(songData); } - async fetchIACosRecommendations(genres: string[], userVector: number[]) : Promise { + async fetchIACosRecommendations(genres: string[], userVector: number[], + limit: number) : Promise { const parsedVector = `[${userVector.join(", ")}]` const rawSongData = await this.songModel.sequelize?.query(`SELECT songs.id, songs.name, @@ -155,10 +156,10 @@ export class SongsService { WHERE genres.genre IN(:genres) GROUP BY songs.id, songs.name, songs.url_preview, albums.url_image, song_details.vectors ORDER BY cos_sim DESC - LIMIT 10;`, { + LIMIT :limit;`, { type: QueryTypes.SELECT, logging: console.log, - replacements:{ genres, userVector: [parsedVector] } + replacements:{ genres, userVector: [parsedVector], limit } }) console.log(rawSongData) From 32594a0610d0620fd60423be78c1046c6cfc1e30 Mon Sep 17 00:00:00 2001 From: Raghart Date: Wed, 11 Feb 2026 10:57:03 -0400 Subject: [PATCH 24/38] r loading function in local DB to search for songs --- back/database/sql/queries/vectorFunction.sql | 33 ++++++++++++++++++++ back/src/songs/songs.service.ts | 21 ++----------- 2 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 back/database/sql/queries/vectorFunction.sql diff --git a/back/database/sql/queries/vectorFunction.sql b/back/database/sql/queries/vectorFunction.sql new file mode 100644 index 0000000..dc612e0 --- /dev/null +++ b/back/database/sql/queries/vectorFunction.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION search_songs_cosine_similarity( + genres_filter text[], + query_vector vector(13), + limit_num int DEFAULT 40 +) +RETURNS TABLE ( + id int, + name text, + artists text, + url_preview text, + album_cover text, + cos_sim float +) +LANGUAGE sql stable +AS $$ + SELECT songs.id, + songs.name, + string_agg(DISTINCT artists.name, ', ') AS artists, + songs.url_preview, + albums.url_image AS album_cover, + 1 - (song_details.vectors <=> query_vector) AS cos_sim + FROM songs + JOIN song_details ON songs.id = song_details.song_id + JOIN albums ON albums.id = songs.album_id + JOIN song_artists ON songs.id = song_artists.song_id + JOIN artists ON song_artists.artist_id = artists.id + JOIN song_genres ON songs.id = song_genres.song_id + JOIN genres ON song_genres.genre_id = genres.id + WHERE genres.genre = ANY(genres_filter) + GROUP BY songs.id, songs.name, songs.url_preview, albums.url_image, song_details.vectors + ORDER BY cos_sim DESC + LIMIT limit_num; +$$; \ No newline at end of file diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index cd10eaa..9153d34 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -140,28 +140,12 @@ export class SongsService { async fetchIACosRecommendations(genres: string[], userVector: number[], limit: number) : Promise { const parsedVector = `[${userVector.join(", ")}]` - const rawSongData = await this.songModel.sequelize?.query(`SELECT songs.id, - songs.name, - string_agg(DISTINCT artists.name, ', ') AS artists, - songs.url_preview, - albums.url_image AS album_cover, - 1 - (song_details.vectors <=> :userVector) AS cos_sim - FROM songs - JOIN song_details ON songs.id = song_details.song_id - JOIN albums ON albums.id = songs.album_id - JOIN song_artists ON songs.id = song_artists.song_id - JOIN artists ON song_artists.artist_id = artists.id - JOIN song_genres ON songs.id = song_genres.song_id - JOIN genres ON song_genres.genre_id = genres.id - WHERE genres.genre IN(:genres) - GROUP BY songs.id, songs.name, songs.url_preview, albums.url_image, song_details.vectors - ORDER BY cos_sim DESC - LIMIT :limit;`, { + const rawSongData = await this.songModel.sequelize?.query(`SELECT * + FROM search_songs_cosine_similarity(ARRAY[:genres]::text[], :userVector::vector, :limit::int);`, { type: QueryTypes.SELECT, logging: console.log, replacements:{ genres, userVector: [parsedVector], limit } }) - console.log(rawSongData) const parsedData = parseCosSongData(rawSongData) @@ -171,7 +155,6 @@ export class SongsService { artists: songData.artists.split(","), url_preview: songData.url_preview, album_cover: songData.album_cover, - })) } From 748f27a585f37eb635b89be2f01cdae8ec372ccf Mon Sep 17 00:00:00 2001 From: Raghart Date: Wed, 11 Feb 2026 16:59:41 -0400 Subject: [PATCH 25/38] r refactoring DB local vectors to be normalized --- back/database/dbConfig/databaseConfig.go | 36 ++++++++++++++++--- back/test/constants/constants.ts | 23 ++++++------ .../src/components/Utils/hooks/useLoadRec.ts | 19 +++++++++- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 358d819..2d9db41 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -410,20 +410,42 @@ func (cfg *DbConfig) AddVectorsDatabase() { log.Fatalf("there was a problem while trying to get the song: %v", err) } + minLoudness := -60.00 + maxLoudness := 3.642 + + minTempo := 0.0 + maxTempo := 238.895 + + minTimeSig := 0 + maxTimeSig := 5 + + minTrackKey := 0 + maxTrackKey := 11 + + minDuration := 0.2 + maxDuration := 63.31 + + loudnessNor := minMaxScaling(song.Loudness, float32(minLoudness), float32(maxLoudness)) + tempoNor := minMaxScaling(song.Tempo, float32(minTempo), float32(maxTempo)) + timeSigNor := minMaxScaling(float32(song.TimeSignature), float32(minTimeSig), + float32(maxTimeSig)) + trackKeyNor := minMaxScaling(song.TrackKey, float32(minTrackKey), float32(maxTrackKey)) + durationNor := minMaxScaling(dbSong.Duration, float32(minDuration), float32(maxDuration)) + songParams := []float32{ - dbSong.Duration, + durationNor, song.Danceability, song.Energy, - song.TrackKey, - song.Loudness, + trackKeyNor, + loudnessNor, song.Mode, song.Speechiness, float32(song.Acousticness), float32(song.Instrumentalness), song.Liveness, song.Valence, - song.Tempo, - float32(song.TimeSignature), + tempoNor, + timeSigNor, } embedding := pgvector.NewVector(songParams) @@ -442,6 +464,10 @@ func (cfg *DbConfig) AddVectorsDatabase() { fmt.Println("Finish processing the vectors!") } +func minMaxScaling(value, dbMin, dbMax float32) float32 { + return (value - dbMin) / (dbMax - dbMin) +} + func parseCSV(path string) ([][]string, error) { file, err := os.Open(path) if err != nil { diff --git a/back/test/constants/constants.ts b/back/test/constants/constants.ts index f7f781d..b2482e3 100644 --- a/back/test/constants/constants.ts +++ b/back/test/constants/constants.ts @@ -22,16 +22,17 @@ export const USER_VECTOR: [ export const TRUE_USER_VECTOR: [ number,number,number,number,number,number,number,number,number,number,number,number,number, ] = [ - 3.39, - 0.373, - 0.86,11, - -4.869, + 0.049279034, + 0.508, + 0.979, + 0.90909094, + 0.87538105, 0, - 0.0533, - 0.009039999917149544, - 2.8000000384054147e-05, - 0.228, - 0.48, - 175.103, - 4 + 0.0847, + 8.7e-05, + 0.000643, + 0.0641, + 0.704, + 0.5777852, + 0.8 ] diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index 2ca5587..b882c09 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -14,7 +14,24 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const dispatch = useAppDispatch(); const [getIASongs] = useLazyQuery(getIARecommendations); - const userVector = [energy, speechLevel, danceability, duration, sentiment, voiceType, mood, acousticness]; + const trackKey = 9; + const loudness = mood == 1 ? -4.356: -10.356; + const tempo = sentiment > 0.5 ? 130.576 : 85.365; + const timeSignature = 4; + + const liveness = mood == 1 ? 0.735 : 0.135; + + const userVector = [ + energy, + speechLevel, + danceability, + duration, + sentiment, + voiceType, + mood, + acousticness, + liveness + ]; const loadRecommendations = () => { setLoading(true); From f52785aaafa45f7b323581cd1814103055f9e915 Mon Sep 17 00:00:00 2001 From: Raghart Date: Wed, 11 Feb 2026 21:21:30 -0400 Subject: [PATCH 26/38] r refactoring front userVector to adapt to the new vectors --- back/database/dbConfig/constants.go | 12 +++++++ back/database/dbConfig/databaseConfig.go | 15 --------- .../GenreSelector/GenreSelector.tsx | 7 ++--- .../components/Utils/handleDelLastGenre.ts | 9 +----- .../components/Utils/handleValueChanges.ts | 4 +-- .../src/components/Utils/hooks/useLoadRec.ts | 31 ++++++++++++------- front/src/components/Utils/minMaxScale.ts | 5 +++ front/src/components/constants/ModalC.ts | 8 +++++ 8 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 back/database/dbConfig/constants.go create mode 100644 front/src/components/Utils/minMaxScale.ts create mode 100644 front/src/components/constants/ModalC.ts diff --git a/back/database/dbConfig/constants.go b/back/database/dbConfig/constants.go new file mode 100644 index 0000000..6863b24 --- /dev/null +++ b/back/database/dbConfig/constants.go @@ -0,0 +1,12 @@ +package databaseConfig + +const minLoudness = -60.00 +const maxLoudness = 3.642 +const minTempo = 0.0 +const maxTempo = 238.895 +const minTimeSig = 0 +const maxTimeSig = 5 +const minTrackKey = 0 +const maxTrackKey = 11 +const minDuration = 0.2 +const maxDuration = 63.31 diff --git a/back/database/dbConfig/databaseConfig.go b/back/database/dbConfig/databaseConfig.go index 2d9db41..93fea0e 100644 --- a/back/database/dbConfig/databaseConfig.go +++ b/back/database/dbConfig/databaseConfig.go @@ -410,21 +410,6 @@ func (cfg *DbConfig) AddVectorsDatabase() { log.Fatalf("there was a problem while trying to get the song: %v", err) } - minLoudness := -60.00 - maxLoudness := 3.642 - - minTempo := 0.0 - maxTempo := 238.895 - - minTimeSig := 0 - maxTimeSig := 5 - - minTrackKey := 0 - maxTrackKey := 11 - - minDuration := 0.2 - maxDuration := 63.31 - loudnessNor := minMaxScaling(song.Loudness, float32(minLoudness), float32(maxLoudness)) tempoNor := minMaxScaling(song.Tempo, float32(minTempo), float32(maxTempo)) timeSigNor := minMaxScaling(float32(song.TimeSignature), float32(minTimeSig), diff --git a/front/src/components/MainLayout/TopBar/LaraRecommendModal/ModalComponents/GenreSelector/GenreSelector.tsx b/front/src/components/MainLayout/TopBar/LaraRecommendModal/ModalComponents/GenreSelector/GenreSelector.tsx index 530bcf1..72850f5 100644 --- a/front/src/components/MainLayout/TopBar/LaraRecommendModal/ModalComponents/GenreSelector/GenreSelector.tsx +++ b/front/src/components/MainLayout/TopBar/LaraRecommendModal/ModalComponents/GenreSelector/GenreSelector.tsx @@ -13,7 +13,6 @@ import { GenreListFormat } from "@/types/genreTypes"; const GenreSelector = () => { const [searchValue, setSearchValue] = useState(""); const [selectedGenres, setSelectedGenres] = useState([]); - const [isNavigating, setIsNavigating] = useState(false); const collection = useGenreCollection(searchValue, selectedGenres); const dispatch = useAppDispatch(); return( @@ -33,13 +32,13 @@ const GenreSelector = () => { setSearchValue(details.inputValue)} aria-label="Select a Music Genre" - onValueChange={(details) => dispatch(handleValueChange(details, setSelectedGenres, setIsNavigating))} + onValueChange={(details) => dispatch(handleValueChange(details, setSelectedGenres))} placeholder={selectedGenres.length > 0 ? "Delete by pressing 'Del'" : "Select a Genre"} - onHighlightChange={(details) => setIsNavigating(details.highlightedValue != null)} variant="outline"> + variant="outline"> dispatch(handleDelLastGenre(e, selectedGenres, - setSelectedGenres, collection, isNavigating))} /> + setSelectedGenres))} /> diff --git a/front/src/components/Utils/handleDelLastGenre.ts b/front/src/components/Utils/handleDelLastGenre.ts index 2f57c61..257f4de 100644 --- a/front/src/components/Utils/handleDelLastGenre.ts +++ b/front/src/components/Utils/handleDelLastGenre.ts @@ -1,21 +1,14 @@ import { deleteLastGenre } from "@/reducers/recommendReducer"; import { AppDispatch } from "@/store"; -import { GenreListFormat } from "@/types/genreTypes"; -import { ListCollection } from "@chakra-ui/react"; const handleDelLastGenre = (e: React.KeyboardEvent, selectedGenres: string[], - setSelectedGenres: React.Dispatch>, collection: ListCollection, - isNavigating: boolean) => { + setSelectedGenres: React.Dispatch>) => { return (dispatch: AppDispatch) => { if ((e.key === "Backspace" || e.key === "delete") && selectedGenres.length > 0 && !e.currentTarget.value) { setSelectedGenres((prev) => prev.slice(0, -1)); dispatch(deleteLastGenre()); e.preventDefault(); }; - - if (e.key === "Enter" && collection.items.length > 0 && !isNavigating) { - setSelectedGenres((prev) => [...prev, collection.items[0].name]); - }; }; }; diff --git a/front/src/components/Utils/handleValueChanges.ts b/front/src/components/Utils/handleValueChanges.ts index 25cdadf..8388430 100644 --- a/front/src/components/Utils/handleValueChanges.ts +++ b/front/src/components/Utils/handleValueChanges.ts @@ -3,10 +3,8 @@ import { AppDispatch } from "@/store"; import { Combobox } from "@chakra-ui/react"; const handleValueChange = (details: Combobox.ValueChangeDetails, - setSelectedGenres: React.Dispatch>, - setIsNavigating: React.Dispatch>,) => { + setSelectedGenres: React.Dispatch>,) => { return (dispatch: AppDispatch) => { - setIsNavigating(false); setSelectedGenres(details.value); dispatch(setRecommendedGenres(details.value)); }; diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index b882c09..0ed2215 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -4,6 +4,8 @@ import { useAppDispatch, useAppSelector } from "../redux-hooks"; import { useLazyQuery } from "@apollo/client"; import { setLaraRecommendations } from "@/reducers/recommendReducer"; import { getIARecommendations } from "@/queries/LaraRecQuerie"; +import minMaxScale from "../minMaxScale"; +import { MAXDURATION, maxLoudness, MINDURATION, minLoudness, TIMESIGNATURENOR, TRACKKEYNOR } from "@/components/constants/ModalC"; const useLoadRec = (setOpen: React.Dispatch>) => { const [loading, setLoading] = useState(false); @@ -14,23 +16,30 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const dispatch = useAppDispatch(); const [getIASongs] = useLazyQuery(getIARecommendations); - const trackKey = 9; const loudness = mood == 1 ? -4.356: -10.356; const tempo = sentiment > 0.5 ? 130.576 : 85.365; - const timeSignature = 4; - + const liveness = mood == 1 ? 0.735 : 0.135; + const durationNor = minMaxScale(duration, MINDURATION, MAXDURATION) + const loudnessNor = minMaxScale(loudness, minLoudness, maxLoudness) + const tempoNor = minMaxScale(tempo, minLoudness, maxLoudness) + console.log(tempoNor) + console.log(genres) const userVector = [ - energy, - speechLevel, - danceability, - duration, - sentiment, - voiceType, - mood, + durationNor, + danceability, + energy, + TRACKKEYNOR, + loudnessNor, + mood, + speechLevel, acousticness, - liveness + voiceType, + liveness, + sentiment, + tempoNor, + TIMESIGNATURENOR, ]; const loadRecommendations = () => { diff --git a/front/src/components/Utils/minMaxScale.ts b/front/src/components/Utils/minMaxScale.ts new file mode 100644 index 0000000..3194227 --- /dev/null +++ b/front/src/components/Utils/minMaxScale.ts @@ -0,0 +1,5 @@ +const minMaxScale = (value: number, min: number, max: number) : number => { + return (value - min) / (max - min) +}; + +export default minMaxScale; \ No newline at end of file diff --git a/front/src/components/constants/ModalC.ts b/front/src/components/constants/ModalC.ts new file mode 100644 index 0000000..10d713a --- /dev/null +++ b/front/src/components/constants/ModalC.ts @@ -0,0 +1,8 @@ +export const minLoudness = -60.00; +export const maxLoudness = 3.642; +export const minTempo = 0; +export const maxTempo = 238.895; +export const TIMESIGNATURENOR = 0.8; +export const TRACKKEYNOR = 0.81818181818; +export const MINDURATION = 0.2; +export const MAXDURATION = 63.31; \ No newline at end of file From a1ea382e4fd1c97bf3e2582749b199f33a02ea1d Mon Sep 17 00:00:00 2001 From: Raghart Date: Wed, 11 Feb 2026 21:26:56 -0400 Subject: [PATCH 27/38] r cleaning fronts debugging console.logs --- front/src/components/Utils/hooks/useLoadRec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index 0ed2215..2d5edd6 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -23,8 +23,6 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const durationNor = minMaxScale(duration, MINDURATION, MAXDURATION) const loudnessNor = minMaxScale(loudness, minLoudness, maxLoudness) const tempoNor = minMaxScale(tempo, minLoudness, maxLoudness) - console.log(tempoNor) - console.log(genres) const userVector = [ durationNor, From 9de0111106fce535ccd0fa0b7a82af687db6f320 Mon Sep 17 00:00:00 2001 From: Raghart Date: Thu, 12 Feb 2026 10:42:02 -0400 Subject: [PATCH 28/38] r refactoring vector Function to work without a genre --- back/database/sql/queries/vectorFunction.sql | 2 +- front/src/components/Utils/hooks/useLoadRec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/back/database/sql/queries/vectorFunction.sql b/back/database/sql/queries/vectorFunction.sql index dc612e0..017552b 100644 --- a/back/database/sql/queries/vectorFunction.sql +++ b/back/database/sql/queries/vectorFunction.sql @@ -26,7 +26,7 @@ AS $$ JOIN artists ON song_artists.artist_id = artists.id JOIN song_genres ON songs.id = song_genres.song_id JOIN genres ON song_genres.genre_id = genres.id - WHERE genres.genre = ANY(genres_filter) + WHERE (cardinality(genres_filter) = 0 OR genres.genre = ANY(genres_filter)) GROUP BY songs.id, songs.name, songs.url_preview, albums.url_image, song_details.vectors ORDER BY cos_sim DESC LIMIT limit_num; diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index 2d5edd6..3e54037 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -18,8 +18,8 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const loudness = mood == 1 ? -4.356: -10.356; const tempo = sentiment > 0.5 ? 130.576 : 85.365; - const liveness = mood == 1 ? 0.735 : 0.135; + const durationNor = minMaxScale(duration, MINDURATION, MAXDURATION) const loudnessNor = minMaxScale(loudness, minLoudness, maxLoudness) const tempoNor = minMaxScale(tempo, minLoudness, maxLoudness) From 8b0bfa516ad169083128ba3702ec607ff1208596 Mon Sep 17 00:00:00 2001 From: Raghart Date: Thu, 12 Feb 2026 15:57:34 -0400 Subject: [PATCH 29/38] r tweaking front userVector values to improve user experience & fixing git --- front/src/components/Utils/hooks/useLoadRec.ts | 12 +++++++----- front/src/components/Utils/randInRange.ts | 5 +++++ 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 front/src/components/Utils/randInRange.ts diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index 3e54037..d3f7fb4 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -5,7 +5,9 @@ import { useLazyQuery } from "@apollo/client"; import { setLaraRecommendations } from "@/reducers/recommendReducer"; import { getIARecommendations } from "@/queries/LaraRecQuerie"; import minMaxScale from "../minMaxScale"; -import { MAXDURATION, maxLoudness, MINDURATION, minLoudness, TIMESIGNATURENOR, TRACKKEYNOR } from "@/components/constants/ModalC"; +import { MAXDURATION, maxLoudness, maxTempo, MINDURATION, minLoudness, minTempo, + TIMESIGNATURENOR, TRACKKEYNOR } from "@/components/constants/ModalC"; +import randInRange from "../randInRange"; const useLoadRec = (setOpen: React.Dispatch>) => { const [loading, setLoading] = useState(false); @@ -16,13 +18,13 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const dispatch = useAppDispatch(); const [getIASongs] = useLazyQuery(getIARecommendations); - const loudness = mood == 1 ? -4.356: -10.356; - const tempo = sentiment > 0.5 ? 130.576 : 85.365; - const liveness = mood == 1 ? 0.735 : 0.135; + const liveness = mood === 1 ? randInRange(0.6, 0.9) : randInRange(0.05, 0.3); + const tempo = sentiment > 0.5 ? randInRange(120, 150) : randInRange(70, 100); + const loudness = mood === 1 ? randInRange(-6, -3) : randInRange(-14, -8); const durationNor = minMaxScale(duration, MINDURATION, MAXDURATION) const loudnessNor = minMaxScale(loudness, minLoudness, maxLoudness) - const tempoNor = minMaxScale(tempo, minLoudness, maxLoudness) + const tempoNor = minMaxScale(tempo, minTempo, maxTempo) const userVector = [ durationNor, diff --git a/front/src/components/Utils/randInRange.ts b/front/src/components/Utils/randInRange.ts new file mode 100644 index 0000000..473134b --- /dev/null +++ b/front/src/components/Utils/randInRange.ts @@ -0,0 +1,5 @@ +const randInRange = (min: number, max: number) : number => { + return min + Math.random() * (max - min) +}; + +export default randInRange; \ No newline at end of file From 2bc2d6b6dfe62045d56860245a2da8414283d942 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 15:36:32 -0400 Subject: [PATCH 30/38] r hiding secret value --- back/database/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/database/main.go b/back/database/main.go index 319b467..66e85ae 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -14,7 +14,7 @@ import ( ) func main() { - serviceURI := "postgresql://postgres:postgres@localhost:5432/music_db?sslmode=disable" + serviceURI := os.Getenv("dbURI") if len(os.Args) != 2 { printHelp() From 1c942e1ec7c8be5a554fcbac389e6d3798096972 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 15:57:59 -0400 Subject: [PATCH 31/38] r fixing another conflict in git --- back/database/main.go | 1 - back/src/app.module.ts | 9 ++-- back/src/songs/songs.resolver.spec.ts | 13 ++--- back/src/songs/songs.service.spec.ts | 41 ---------------- back/src/songs/songs.service.ts | 70 --------------------------- 5 files changed, 7 insertions(+), 127 deletions(-) diff --git a/back/database/main.go b/back/database/main.go index 66e85ae..683234c 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -15,7 +15,6 @@ import ( func main() { serviceURI := os.Getenv("dbURI") - if len(os.Args) != 2 { printHelp() return diff --git a/back/src/app.module.ts b/back/src/app.module.ts index c03079c..fb85112 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -37,6 +37,9 @@ dotenv.config(); synchronize: true, logging: false, }), + ServeStaticModule.forRoot({ + rootPath: join(__dirname, "..", "..", "public") + }), GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: true, @@ -55,9 +58,3 @@ dotenv.config(); providers: [AppService], }) export class AppModule {} - -/* -ServeStaticModule.forRoot({ - rootPath: join(__dirname, "..", "..", "public") - }) -*/ diff --git a/back/src/songs/songs.resolver.spec.ts b/back/src/songs/songs.resolver.spec.ts index b54e313..e9217c5 100644 --- a/back/src/songs/songs.resolver.spec.ts +++ b/back/src/songs/songs.resolver.spec.ts @@ -63,12 +63,7 @@ describe('SongsResolver receives the expected songs array from the service', () it('getIARecommendations retrieves a songs recommendations array ready to be delivered', async () => { const results = await resolver.getIARecommendations( ['Rock', 'Alternative', 'Alternative Rock', 'Grunge'], - USER_VECTOR, - ); - - expect(service.getIARecommendations).toHaveBeenCalledWith( - ['Rock', 'Alternative', 'Alternative Rock', 'Grunge'], - USER_VECTOR, + USER_VECTOR, 40 ); expect(results).toHaveLength(5); @@ -174,16 +169,16 @@ describe('SongsResolver is able to communicate the error from the service to hel it("getIARecommendations throws an error when the service couldn't connect with the database", async () => { await expect( - resolver.getIARecommendations([], USER_VECTOR), + resolver.getIARecommendations([], USER_VECTOR, 40), ).rejects.toThrow(InternalServerErrorException); await expect( - resolver.getIARecommendations([], USER_VECTOR), + resolver.getIARecommendations([], USER_VECTOR, 40), ).rejects.toThrow( 'Database Error: SequelizeTimeoutError: Connection refused', ); - expect(service.getIARecommendations).toHaveBeenCalledWith([], USER_VECTOR); + expect(service.fetchIACosRecommendations).toHaveBeenCalledWith([], USER_VECTOR); }); it("getNextSong throws an error when the service couldn't connect with the database", async () => { diff --git a/back/src/songs/songs.service.spec.ts b/back/src/songs/songs.service.spec.ts index e8948ad..2c8887a 100644 --- a/back/src/songs/songs.service.spec.ts +++ b/back/src/songs/songs.service.spec.ts @@ -271,37 +271,6 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab beforeEach(() => { songModel.findAll.mockResolvedValue(rawIASongs); }); - - it('fetchIARecommendations retrieves a song array matching the given genres', async () => { - const songData = await service.fetchIARecommendations(['Rock']); - expect(songData).toStrictEqual(songIARawResponse); - }); - - it('getCosineSimilarity returns a score representing the similiraty of two number arrays', () => { - const score = service.getCosineSimilarity( - [1, 2, 3, 4, 5], - [1, 2, 3, 4, 5], - ); - expect(score).toBe(1); - }); - - it('calculateRecommendations returns a song array, sorted by their score (highest to lowest)', () => { - const songRecommendations = service.calculateRecommendations( - songIARawResponse, - USER_VECTOR, - ); - expect(songRecommendations).toHaveLength(5); - expect(songRecommendations).toStrictEqual(songIATestScores); - }); - - it('getIARecommendations returns a song recommendations array ready to be delivered', async () => { - const songRecommendations = await service.getIARecommendations( - ['Rock'], - USER_VECTOR, - ); - expect(songRecommendations).toHaveLength(5); - expect(songRecommendations).toStrictEqual(songIATestResponses); - }); }); }); @@ -380,14 +349,4 @@ describe("SongsService throws an error if it couldn't retrieve the data from the 'Database Error: SequelizeTimeoutError: Connection refused', ); }); - - it("getIARecommendations throws an error if it couldn't retrieve the song data from the database", async () => { - await expect(service.fetchIARecommendations([])).rejects.toThrow( - InternalServerErrorException, - ); - - await expect(service.fetchIARecommendations([])).rejects.toThrow( - 'Database Error: SequelizeTimeoutError: Query timed out', - ); - }); }); diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index 9153d34..e7f86e8 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -143,7 +143,6 @@ export class SongsService { const rawSongData = await this.songModel.sequelize?.query(`SELECT * FROM search_songs_cosine_similarity(ARRAY[:genres]::text[], :userVector::vector, :limit::int);`, { type: QueryTypes.SELECT, - logging: console.log, replacements:{ genres, userVector: [parsedVector], limit } }) @@ -158,75 +157,6 @@ export class SongsService { })) } - async fetchIARecommendations(genres: string[]): Promise { - const rawSongData = await safeQuery(() => - this.songModel.findAll({ - attributes: ['id', 'name', 'url_preview', 'duration'], - limit: 100, - order: Sequelize.literal('RANDOM()'), - include: [ - { model: AlbumsModel, attributes: ['url_image'] }, - { - model: ArtistsModel, - attributes: ['name'], - through: { attributes: [] }, - }, - { model: SongDetailsModel }, - { - model: GenresModel, - through: { attributes: [] }, - duplicating: false, - ...(genres.length > 0 - ? { where: { genre: { [Op.in]: genres } } } - : {}), - }, - ], - }), - ); - return rawSongData.map((entry) => - parseIASongData(entry.get({ plain: true })), - ); - } - - getCosineSimilarity(songVector: number[], userVector: number[]) { - const dotProduct = songVector.reduce( - (sum, songVal, idx) => sum + songVal * userVector[idx], - 0, - ); - const songMagnitude = Math.sqrt( - songVector.reduce((sum, songVal) => sum + Math.pow(songVal, 2), 0), - ); - const userMagnitude = Math.sqrt( - userVector.reduce((sum, userVal) => sum + Math.pow(userVal, 2), 0), - ); - return dotProduct / (songMagnitude * userMagnitude); - } - - calculateRecommendations( - songData: IASongResponse[], - userVector: number[], - ): SongResponseAttributes[] { - const songScores = songData - .map((song) => ({ - id: song.id, - song, - score: this.getCosineSimilarity(buildSongVector(song), userVector), - })) - .sort((a, b) => b.score - a.score) - .slice(0, 40); - - return songScores.map((entry) => entry.song); - } - - async getIARecommendations( - genres: string[], - userVector: number[], - ): Promise { - const songData = await this.fetchIARecommendations(genres); - const songList = this.calculateRecommendations(songData, userVector); - return this.parseSongList(songList); - } - async fetchRandomSong(): Promise { const result = await safeQuery(() => this.songModel.findOne({ From 623e8cdf6184d91dd5d9ef560f2e75bdf398b868 Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 14 Feb 2026 20:19:25 -0400 Subject: [PATCH 32/38] r fixing some lint problems and refactoring tests --- back/src/app.module.ts | 2 +- back/src/artists/dto/artistsResults.dto.ts | 26 ++-- back/src/search/dto/albumsSearchDto.ts | 38 ++--- back/src/search/dto/artistsSearchDto.ts | 32 ++--- back/src/search/dto/multipleSearchDto.ts | 50 +++---- back/src/search/dto/songsSearchDto.ts | 50 +++---- back/src/search/search.module.ts | 27 ++-- back/src/songs/dto/FullSongResponse.dto.ts | 68 ++++----- back/src/songs/dto/SongResponse.dto.ts | 38 ++--- back/src/songs/songs.resolver.spec.ts | 13 +- back/src/songs/songs.resolver.ts | 12 +- back/src/songs/songs.service.spec.ts | 20 ++- back/src/songs/songs.service.ts | 33 +++-- back/src/types/parses.ts | 8 +- back/src/types/verify.ts | 21 +-- back/test/constants/constants.ts | 27 ++-- back/test/data/songsModule/resSongData.ts | 155 ++------------------- back/test/data/songsModule/serSongData.ts | 125 ++--------------- 18 files changed, 273 insertions(+), 472 deletions(-) diff --git a/back/src/app.module.ts b/back/src/app.module.ts index fb85112..388007b 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -38,7 +38,7 @@ dotenv.config(); logging: false, }), ServeStaticModule.forRoot({ - rootPath: join(__dirname, "..", "..", "public") + rootPath: join(__dirname, '..', '..', 'public'), }), GraphQLModule.forRoot({ driver: ApolloDriver, diff --git a/back/src/artists/dto/artistsResults.dto.ts b/back/src/artists/dto/artistsResults.dto.ts index 5681ddd..93e1de4 100644 --- a/back/src/artists/dto/artistsResults.dto.ts +++ b/back/src/artists/dto/artistsResults.dto.ts @@ -1,13 +1,13 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class ArtistsResultsDto { - @Field() - id: number; - - @Field() - name: string; - - @Field() - album_cover: string; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class ArtistsResultsDto { + @Field() + id: number; + + @Field() + name: string; + + @Field() + album_cover: string; +} diff --git a/back/src/search/dto/albumsSearchDto.ts b/back/src/search/dto/albumsSearchDto.ts index 3419627..b71a578 100644 --- a/back/src/search/dto/albumsSearchDto.ts +++ b/back/src/search/dto/albumsSearchDto.ts @@ -1,19 +1,19 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class albumsSearchDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field() - album_cover: string; - - @Field(() => String) - type: "album"; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class albumsSearchDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field() + album_cover: string; + + @Field(() => String) + type: 'album'; +} diff --git a/back/src/search/dto/artistsSearchDto.ts b/back/src/search/dto/artistsSearchDto.ts index 6781af0..ed043da 100644 --- a/back/src/search/dto/artistsSearchDto.ts +++ b/back/src/search/dto/artistsSearchDto.ts @@ -1,16 +1,16 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class artistsSearchDto { - @Field() - id: number; - - @Field() - name: string; - - @Field() - album_cover: string; - - @Field(() => String) - type: "artist"; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class artistsSearchDto { + @Field() + id: number; + + @Field() + name: string; + + @Field() + album_cover: string; + + @Field(() => String) + type: 'artist'; +} diff --git a/back/src/search/dto/multipleSearchDto.ts b/back/src/search/dto/multipleSearchDto.ts index 1f602b2..a466915 100644 --- a/back/src/search/dto/multipleSearchDto.ts +++ b/back/src/search/dto/multipleSearchDto.ts @@ -1,25 +1,25 @@ -import { Field, ObjectType } from "@nestjs/graphql"; -import { artistsSearchDto } from "./artistsSearchDto"; -import { albumsSearchDto } from "./albumsSearchDto"; -import { searchSongsDto } from "./songsSearchDto"; - -@ObjectType() -export class multipleSearchResultsDto { - @Field(() => artistsSearchDto) - exactArtist: artistsSearchDto; - - @Field(() => albumsSearchDto) - exactAlbum: albumsSearchDto; - - @Field(() => searchSongsDto) - exactSong: searchSongsDto; - - @Field(() => [artistsSearchDto]) - artistResults: artistsSearchDto[]; - - @Field(() => [albumsSearchDto]) - albumResults: albumsSearchDto[]; - - @Field(() => [searchSongsDto]) - songResults: searchSongsDto[]; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; +import { artistsSearchDto } from './artistsSearchDto'; +import { albumsSearchDto } from './albumsSearchDto'; +import { searchSongsDto } from './songsSearchDto'; + +@ObjectType() +export class multipleSearchResultsDto { + @Field(() => artistsSearchDto) + exactArtist: artistsSearchDto; + + @Field(() => albumsSearchDto) + exactAlbum: albumsSearchDto; + + @Field(() => searchSongsDto) + exactSong: searchSongsDto; + + @Field(() => [artistsSearchDto]) + artistResults: artistsSearchDto[]; + + @Field(() => [albumsSearchDto]) + albumResults: albumsSearchDto[]; + + @Field(() => [searchSongsDto]) + songResults: searchSongsDto[]; +} diff --git a/back/src/search/dto/songsSearchDto.ts b/back/src/search/dto/songsSearchDto.ts index b1f237d..8f78af9 100644 --- a/back/src/search/dto/songsSearchDto.ts +++ b/back/src/search/dto/songsSearchDto.ts @@ -1,25 +1,25 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class searchSongsDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field() - album: string; - - @Field() - album_cover: string; - - @Field() - url_preview: string; - - @Field(() => String) - type: "song"; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class searchSongsDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field() + album: string; + + @Field() + album_cover: string; + + @Field() + url_preview: string; + + @Field(() => String) + type: 'song'; +} diff --git a/back/src/search/search.module.ts b/back/src/search/search.module.ts index e6f37c3..1fe5be5 100644 --- a/back/src/search/search.module.ts +++ b/back/src/search/search.module.ts @@ -10,8 +10,9 @@ import { SongsModel } from '../../models/songs/song.model'; import { ArtistsModel } from '../../models/artists/artists.model'; import { AlbumsModel } from '../../models/albums/albums.model'; import { GenresModel } from '../../models/genres/genres.model'; -import { Client } from "elasticsearch"; -import dotenv from "dotenv"; dotenv.config(); +import { Client } from 'elasticsearch'; +import dotenv from 'dotenv'; +dotenv.config(); @Module({ imports: [ @@ -26,16 +27,20 @@ import dotenv from "dotenv"; dotenv.config(); GenresModel, ]), ], - providers: [{ - provide: "BonsaiClient", - useFactory: async () => { + providers: [ + { + provide: 'BonsaiClient', + useFactory: () => { return new Client({ - host: process.env.ELASTICSEARCH_NODE, - log: "error", - ssl: { rejectUnauthorized: false } - }) - } -}, SearchService, SearchResolver], + host: process.env.ELASTICSEARCH_NODE, + log: 'error', + ssl: { rejectUnauthorized: false }, + }); + }, + }, + SearchService, + SearchResolver, + ], exports: [SearchService], }) export class SearchModule {} diff --git a/back/src/songs/dto/FullSongResponse.dto.ts b/back/src/songs/dto/FullSongResponse.dto.ts index 99f4bd0..72855de 100644 --- a/back/src/songs/dto/FullSongResponse.dto.ts +++ b/back/src/songs/dto/FullSongResponse.dto.ts @@ -1,34 +1,34 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class FullSongResponseDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field(() => [String]) - genres: string[]; - - @Field() - album: string; - - @Field() - album_cover: string; - - @Field() - year: number; - - @Field() - duration: number; - - @Field() - spotify_id: string; - - @Field() - url_preview: string; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class FullSongResponseDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field(() => [String]) + genres: string[]; + + @Field() + album: string; + + @Field() + album_cover: string; + + @Field() + year: number; + + @Field() + duration: number; + + @Field() + spotify_id: string; + + @Field() + url_preview: string; +} diff --git a/back/src/songs/dto/SongResponse.dto.ts b/back/src/songs/dto/SongResponse.dto.ts index 92a780b..d5edb5d 100644 --- a/back/src/songs/dto/SongResponse.dto.ts +++ b/back/src/songs/dto/SongResponse.dto.ts @@ -1,19 +1,19 @@ -import { Field, ObjectType } from "@nestjs/graphql"; - -@ObjectType() -export class SongResponseDto { - @Field() - id: number; - - @Field() - name: string; - - @Field(() => [String]) - artists: string[]; - - @Field() - url_preview: string; - - @Field() - album_cover: string; -}; \ No newline at end of file +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class SongResponseDto { + @Field() + id: number; + + @Field() + name: string; + + @Field(() => [String]) + artists: string[]; + + @Field() + url_preview: string; + + @Field() + album_cover: string; +} diff --git a/back/src/songs/songs.resolver.spec.ts b/back/src/songs/songs.resolver.spec.ts index e9217c5..3ebfdcc 100644 --- a/back/src/songs/songs.resolver.spec.ts +++ b/back/src/songs/songs.resolver.spec.ts @@ -5,7 +5,7 @@ import { SongsService } from './songs.service'; import { singleSongData, songFullTestResponse, - songIATestResponses, + songRecommendResponses, songTestResponses, } from '../../test/data/songsModule/resSongData'; import { InternalServerErrorException } from '@nestjs/common'; @@ -29,7 +29,7 @@ describe('SongsResolver receives the expected songs array from the service', () getSongData: jest.fn().mockResolvedValue(songFullTestResponse), getIARecommendations: jest .fn() - .mockReturnValue(songIATestResponses), + .mockReturnValue(songRecommendResponses), getRandomSong: jest.fn().mockResolvedValue(singleSongData), getNextSong: jest.fn().mockResolvedValue(singleSongData), getPreviousSong: jest.fn().mockResolvedValue(singleSongData), @@ -63,7 +63,8 @@ describe('SongsResolver receives the expected songs array from the service', () it('getIARecommendations retrieves a songs recommendations array ready to be delivered', async () => { const results = await resolver.getIARecommendations( ['Rock', 'Alternative', 'Alternative Rock', 'Grunge'], - USER_VECTOR, 40 + USER_VECTOR, + 40, ); expect(results).toHaveLength(5); @@ -178,7 +179,11 @@ describe('SongsResolver is able to communicate the error from the service to hel 'Database Error: SequelizeTimeoutError: Connection refused', ); - expect(service.fetchIACosRecommendations).toHaveBeenCalledWith([], USER_VECTOR); + expect(service.getSongRecommendations).toHaveBeenCalledWith( + [], + USER_VECTOR, + 40, + ); }); it("getNextSong throws an error when the service couldn't connect with the database", async () => { diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index c3b769b..ac0c545 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -1,6 +1,6 @@ import { Args, Float, Int, Query, Resolver } from '@nestjs/graphql'; import { SongsService } from './songs.service'; -import { TRUE_USER_VECTOR, USER_VECTOR } from '../../test/constants/constants'; +import { USER_VECTOR } from '../../test/constants/constants'; import { FullSongResponseDto } from './dto/FullSongResponse.dto'; import { SongResponseDto } from './dto/SongResponse.dto'; @@ -31,12 +31,16 @@ export class SongsResolver { async getIARecommendations( @Args('genres', { type: () => [String], defaultValue: ['Rock'] }) genres: string[], - @Args('userVector', { type: () => [Float], defaultValue: TRUE_USER_VECTOR }) + @Args('userVector', { type: () => [Float], defaultValue: USER_VECTOR }) userVector: number[], - @Args('limit', {type: () => Int, defaultValue: 40}) + @Args('limit', { type: () => Int, defaultValue: 40 }) limit: number, ): Promise { - return this.songsService.fetchIACosRecommendations(genres, userVector, limit) + return this.songsService.getSongRecommendations( + genres, + userVector, + limit, + ); } @Query(() => SongResponseDto, { name: 'getRandomSong' }) diff --git a/back/src/songs/songs.service.spec.ts b/back/src/songs/songs.service.spec.ts index 2c8887a..fbeca81 100644 --- a/back/src/songs/songs.service.spec.ts +++ b/back/src/songs/songs.service.spec.ts @@ -23,8 +23,7 @@ import { SongsModel } from '../../models/songs/song.model'; import { singleSongData, songFullTestResponse, - songIATestResponses, - songIATestScores, + songRecommendResponses, songTestResponses, } from '../../test/data/songsModule/resSongData'; import { USER_VECTOR } from '../../test/constants/constants'; @@ -36,6 +35,9 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab findAll: jest.Mock; findByPk: jest.Mock; findOne: jest.Mock; + sequelize: { + query: jest.Mock + }; }; beforeEach(async () => { @@ -49,6 +51,9 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab findAll: jest.fn(), findByPk: jest.fn(), findOne: jest.fn(), + sequelize: { + query: jest.fn(), + } }, }, ], @@ -266,11 +271,16 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab }); }); - describe('getIARecommendations returns a song recommendations array based by user input', () => { - const rawIASongs = songIARawResponse.map((entry) => ({ get: () => entry })); + describe('getSongRecommendations returns a song recommendations array based by user input', () => { beforeEach(() => { - songModel.findAll.mockResolvedValue(rawIASongs); + songModel.sequelize?.query.mockResolvedValue(songIARawResponse); }); + + it("getSongRecommendations returns an array with the expected songs", async () => { + const songRecommendations = await service.getSongRecommendations(["Rock"], USER_VECTOR, 5) + expect(songRecommendations).toHaveLength(5); + expect(songRecommendations).toStrictEqual(songRecommendResponses); + }) }); }); diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index e7f86e8..43b0ff2 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -8,7 +8,7 @@ import { InjectModel } from '@nestjs/sequelize'; import { Op, QueryTypes, Sequelize } from 'sequelize'; import { buildSongVector } from './utils/buildSongVector'; import { - parseCosSongData, + parseSongRecommendations, parseFullSongResponse, parseIASongData, parseSongResponse, @@ -137,24 +137,31 @@ export class SongsService { return this.parseFullSong(songData); } - async fetchIACosRecommendations(genres: string[], userVector: number[], - limit: number) : Promise { - const parsedVector = `[${userVector.join(", ")}]` - const rawSongData = await this.songModel.sequelize?.query(`SELECT * - FROM search_songs_cosine_similarity(ARRAY[:genres]::text[], :userVector::vector, :limit::int);`, { - type: QueryTypes.SELECT, - replacements:{ genres, userVector: [parsedVector], limit } - }) + async getSongRecommendations( + genres: string[], + userVector: number[], + limit: number, + ): Promise { + const parsedVector = `[${userVector.join(', ')}]`; + const rawSongData = await this.songModel.sequelize?.query( + `SELECT * + FROM search_songs_cosine_similarity(ARRAY[:genres]::text[], :userVector::vector, :limit::int);`, + { + type: QueryTypes.SELECT, + replacements: { genres, userVector: [parsedVector], limit }, + }, + ); + console.log(rawSongData) - const parsedData = parseCosSongData(rawSongData) + const parsedData = parseSongRecommendations(rawSongData); - return parsedData.map(songData => ({ + return parsedData.map((songData) => ({ id: songData.id, name: songData.name, - artists: songData.artists.split(","), + artists: songData.artists.split(','), url_preview: songData.url_preview, album_cover: songData.album_cover, - })) + })); } async fetchRandomSong(): Promise { diff --git a/back/src/types/parses.ts b/back/src/types/parses.ts index e071f28..1173917 100644 --- a/back/src/types/parses.ts +++ b/back/src/types/parses.ts @@ -104,10 +104,12 @@ export const parseIASongData = (data: unknown): IASongResponse => { ); }; -export const parseCosSongData = (data: unknown): SongCosResponse[] => { +export const parseSongRecommendations = (data: unknown): SongCosResponse[] => { if (isSongCosData(data)) return data; - throw new BadRequestException("The data recieved didn't match with the expected song recommendation format") -} + throw new BadRequestException( + "The data recieved didn't match with the expected song recommendation format", + ); +}; export const parseArtistSearchData = (data: unknown): artistSearchResults => { if (isArtistSearchData(data)) return data; diff --git a/back/src/types/verify.ts b/back/src/types/verify.ts index d5798c6..445c4cd 100644 --- a/back/src/types/verify.ts +++ b/back/src/types/verify.ts @@ -5,7 +5,11 @@ import { artistSearchResults, songSearchResults, } from './searchTypes'; -import { FullSongResponseAttributes, IASongResponse, SongCosResponse } from './songAttributes'; +import { + FullSongResponseAttributes, + IASongResponse, + SongCosResponse, +} from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; export const isString = (text: unknown): text is string => { @@ -134,13 +138,14 @@ export const isFullSongResponse = ( export const isSongCosData = (data: unknown): data is SongCosResponse[] => { if (data === null || data === undefined) return false; - return (Array.isArray(data) && - "id" in data[0] && - "name" in data[0] && - "artists" in data[0] && - "url_preview" in data[0] && - "album_cover" in data[0] && - "cos_sim" in data[0] + return ( + Array.isArray(data) && + 'id' in data[0] && + 'name' in data[0] && + 'artists' in data[0] && + 'url_preview' in data[0] && + 'album_cover' in data[0] && + 'cos_sim' in data[0] ); }; diff --git a/back/test/constants/constants.ts b/back/test/constants/constants.ts index b2482e3..832c825 100644 --- a/back/test/constants/constants.ts +++ b/back/test/constants/constants.ts @@ -8,6 +8,7 @@ export const searchError = "ElasticSearch is not responding: Timeout Error: It's taking too long"; export const NOGENRE_ERROR = "The genre: 'noGenre' doesn't exist in the DB!"; export const WRONG_OBJ = { id: 123, name: 'papelon', url_image: '', songs: [] }; + export const USER_VECTOR: [ number, number, @@ -17,22 +18,12 @@ export const USER_VECTOR: [ number, number, number, -] = [0.5, 0.165, 0.5, 2.5, 0.5, 0.05, 1, 0.15]; - -export const TRUE_USER_VECTOR: [ -number,number,number,number,number,number,number,number,number,number,number,number,number, + number, + number, + number, + number, + number, ] = [ - 0.049279034, - 0.508, - 0.979, - 0.90909094, - 0.87538105, - 0, - 0.0847, - 8.7e-05, - 0.000643, - 0.0641, - 0.704, - 0.5777852, - 0.8 -] + 0.049279034, 0.508, 0.979, 0.90909094, 0.87538105, 0, 0.0847, 8.7e-5, + 0.000643, 0.0641, 0.704, 0.5777852, 0.8, +]; diff --git a/back/test/data/songsModule/resSongData.ts b/back/test/data/songsModule/resSongData.ts index 98e15ad..3714e39 100644 --- a/back/test/data/songsModule/resSongData.ts +++ b/back/test/data/songsModule/resSongData.ts @@ -78,153 +78,20 @@ export const songFullTestResponse: FullSongResponse = { genres: ['Rock', 'Alternative', 'Alternative Rock', 'Grunge', '90s'], }; -export const songIATestScores = [ - { - id: 5, - name: 'Head To Wall', - url_preview: - 'https://p.scdn.co/mp3-preview/a1c11bb1cb231031eb20e5951a8bfb30503224e9?cid=774b29d4f13844c495f206cafdad9c86', - duration: 3.12, - album: { url_image: TESTING_IMG }, - artists: [{ name: 'Quicksand' }], - songDetails: { - id: 5, - song_id: 5, - danceability: 0.545, - energy: 0.753, - track_key: 7, - loudness: -6.828, - mode: 1, - speechiness: 0.0538, - acousticness: '0.000343', - instrumentalness: '0.249000', - liveness: 0.526, - valence: 0.299, - tempo: '137.515', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], - }, - { - id: 4, - name: 'Ring The Bells', - url_preview: - 'https://p.scdn.co/mp3-preview/a1c11bb1cb231031eb20e5951a8bfb30503224e9?cid=774b29d4f13844c495f206cafdad9c86', - duration: 4.73, - album: { url_image: TESTING_IMG }, - artists: [{ name: 'JAMES' }], - songDetails: { - id: 4, - song_id: 4, - danceability: 0.455, - energy: 0.966, - track_key: 0, - loudness: -4, - mode: 1, - speechiness: 0.0495, - acousticness: '0.010900', - instrumentalness: '0.000604', - liveness: 0.0766, - valence: 0.48, - tempo: '154.246', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], - }, - { - id: 3, - name: 'Avant Garden', - url_preview: - 'https://p.scdn.co/mp3-preview/a1c11bb1cb231031eb20e5951a8bfb30503224e9?cid=774b29d4f13844c495f206cafdad9c86', - duration: 4.87, - album: { url_image: TESTING_IMG }, - artists: [{ name: 'Aerosmith' }], - songDetails: { - id: 3, - song_id: 3, - danceability: 0.298, - energy: 0.759, - track_key: 5, - loudness: -4.399, - mode: 1, - speechiness: 0.0346, - acousticness: '0.025400', - instrumentalness: '0.000000', - liveness: 0.15, - valence: 0.465, - tempo: '167.861', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], - }, +export const songRecommendResponses: SongResponse[] = [ { id: 1, name: 'Malpractice', - url_preview: - 'https://p.scdn.co/mp3-preview/a1c11bb1cb231031eb20e5951a8bfb30503224e9?cid=774b29d4f13844c495f206cafdad9c86', - duration: 4.04, - album: { url_image: TESTING_IMG }, - artists: [{ name: 'Testament' }], - songDetails: { - id: 1, - song_id: 1, - danceability: 0.255, - energy: 0.958, - track_key: 8, - loudness: -6.538, - mode: 1, - speechiness: 0.152, - acousticness: '0.000886', - instrumentalness: '0.576000', - liveness: 0.563, - valence: 0.122, - tempo: '119.004', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], - }, - { - id: 2, - name: 'Lead Me Into The Night', - url_preview: - 'https://p.scdn.co/mp3-preview/a1c11bb1cb231031eb20e5951a8bfb30503224e9?cid=774b29d4f13844c495f206cafdad9c86', - duration: 4.53, - album: { url_image: TESTING_IMG }, - artists: [{ name: 'The Cardigans' }], - songDetails: { - id: 2, - song_id: 2, - danceability: 0.387, - energy: 0.378, - track_key: 10, - loudness: -9.046, - mode: 1, - speechiness: 0.028, - acousticness: '0.617000', - instrumentalness: '0.000001', - liveness: 0.163, - valence: 0.132, - tempo: '129.087', - time_signature: 3, - }, - genres: [{ genre: 'Rock' }], - }, -] as unknown as SongResponseAttributes[]; - -export const songIATestResponses: SongResponse[] = [ - { - id: 5, - name: 'Head To Wall', url_preview: TESTING_URL, album_cover: TESTING_IMG, - artists: ['Quicksand'], + artists: ['Testament'], }, { - id: 4, - name: 'Ring The Bells', + id: 2, + name: 'Lead Me Into The Night', url_preview: TESTING_URL, album_cover: TESTING_IMG, - artists: ['JAMES'], + artists: ['The Cardigans'], }, { id: 3, @@ -234,17 +101,17 @@ export const songIATestResponses: SongResponse[] = [ artists: ['Aerosmith'], }, { - id: 1, - name: 'Malpractice', + id: 4, + name: 'Ring The Bells', url_preview: TESTING_URL, album_cover: TESTING_IMG, - artists: ['Testament'], + artists: ['JAMES'], }, { - id: 2, - name: 'Lead Me Into The Night', + id: 5, + name: 'Head To Wall', url_preview: TESTING_URL, album_cover: TESTING_IMG, - artists: ['The Cardigans'], + artists: ['Quicksand'], }, ]; diff --git a/back/test/data/songsModule/serSongData.ts b/back/test/data/songsModule/serSongData.ts index 61b98f1..2b13f90 100644 --- a/back/test/data/songsModule/serSongData.ts +++ b/back/test/data/songsModule/serSongData.ts @@ -103,135 +103,40 @@ export const songIARawResponse = [ id: 1, name: 'Malpractice', url_preview: TESTING_URL, - duration: 4.04, - album: { - url_image: TESTING_IMG, - }, - artists: [{ name: 'Testament' }], - songDetails: { - id: 1, - song_id: 1, - danceability: 0.255, - energy: 0.958, - track_key: 8, - loudness: -6.538, - mode: 1, - speechiness: 0.152, - acousticness: '0.000886', - instrumentalness: '0.576000', - liveness: 0.563, - valence: 0.122, - tempo: '119.004', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], + album_cover: TESTING_IMG, + artists: "Testament", + cos_sim: 0.9998 }, { id: 2, name: 'Lead Me Into The Night', url_preview: TESTING_URL, - duration: 4.53, - album: { - url_image: TESTING_IMG, - }, - artists: [{ name: 'The Cardigans' }], - songDetails: { - id: 2, - song_id: 2, - danceability: 0.387, - energy: 0.378, - track_key: 10, - loudness: -9.046, - mode: 1, - speechiness: 0.028, - acousticness: '0.617000', - instrumentalness: '0.000001', - liveness: 0.163, - valence: 0.132, - tempo: '129.087', - time_signature: 3, - }, - genres: [{ genre: 'Rock' }], + album_cover: TESTING_IMG, + artists: 'The Cardigans', + cos_sim: 0.9997 }, { id: 3, name: 'Avant Garden', url_preview: TESTING_URL, - duration: 4.87, - album: { - url_image: TESTING_IMG, - }, - artists: [{ name: 'Aerosmith' }], - songDetails: { - id: 3, - song_id: 3, - danceability: 0.298, - energy: 0.759, - track_key: 5, - loudness: -4.399, - mode: 1, - speechiness: 0.0346, - acousticness: '0.025400', - instrumentalness: '0.000000', - liveness: 0.15, - valence: 0.465, - tempo: '167.861', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], + album_cover: TESTING_IMG, + artists: 'Aerosmith', + cos_sim: 0.9996 }, { id: 4, name: 'Ring The Bells', url_preview: TESTING_URL, - duration: 4.73, - album: { - url_image: TESTING_IMG, - }, - artists: [{ name: 'JAMES' }], - songDetails: { - id: 4, - song_id: 4, - danceability: 0.455, - energy: 0.966, - track_key: 0, - loudness: -4, - mode: 1, - speechiness: 0.0495, - acousticness: '0.010900', - instrumentalness: '0.000604', - liveness: 0.0766, - valence: 0.48, - tempo: '154.246', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], + album_cover: TESTING_IMG, + artists: 'JAMES', + cos_sim: 0.9995 }, { id: 5, name: 'Head To Wall', url_preview: TESTING_URL, - duration: 3.12, - album: { - url_image: TESTING_IMG, - }, - artists: [{ name: 'Quicksand' }], - songDetails: { - id: 5, - song_id: 5, - danceability: 0.545, - energy: 0.753, - track_key: 7, - loudness: -6.828, - mode: 1, - speechiness: 0.0538, - acousticness: '0.000343', - instrumentalness: '0.249000', - liveness: 0.526, - valence: 0.299, - tempo: '137.515', - time_signature: 4, - }, - genres: [{ genre: 'Rock' }], + album_cover: TESTING_IMG, + artists: 'Quicksand', + cos_sim: 0.9994 }, ] as unknown as IASongResponse[]; From a040c84bee3ef31ca3b9e41d384e7024e54427ef Mon Sep 17 00:00:00 2001 From: Raghart Date: Sat, 14 Feb 2026 21:21:14 -0400 Subject: [PATCH 33/38] r refactoring tests & front to improve logic --- back/src/songs/songs.resolver.spec.ts | 24 ++--------- back/src/songs/songs.resolver.ts | 4 +- back/src/songs/songs.service.spec.ts | 6 +-- back/src/songs/songs.service.ts | 7 +--- back/src/songs/utils/buildSongVector.ts | 15 ------- back/src/types/parses.spec.ts | 16 ++++---- back/src/types/parses.ts | 13 +----- back/src/types/songAttributes.ts | 9 +--- back/src/types/verify.spec.ts | 12 +++--- back/src/types/verify.ts | 41 ++++--------------- back/test/data/songsModule/resSongData.ts | 1 - back/test/data/songsModule/serSongData.ts | 6 +-- .../src/components/Utils/hooks/useLoadRec.ts | 12 +++--- front/src/queries/LaraRecQuerie.ts | 6 +-- 14 files changed, 47 insertions(+), 125 deletions(-) delete mode 100644 back/src/songs/utils/buildSongVector.ts diff --git a/back/src/songs/songs.resolver.spec.ts b/back/src/songs/songs.resolver.spec.ts index 3ebfdcc..c3f1698 100644 --- a/back/src/songs/songs.resolver.spec.ts +++ b/back/src/songs/songs.resolver.spec.ts @@ -27,7 +27,7 @@ describe('SongsResolver receives the expected songs array from the service', () getLandpageSongs: jest.fn().mockResolvedValue(songTestResponses), getDBLength: jest.fn().mockResolvedValue(songTestResponses.length), getSongData: jest.fn().mockResolvedValue(songFullTestResponse), - getIARecommendations: jest + getSongRecommendations: jest .fn() .mockReturnValue(songRecommendResponses), getRandomSong: jest.fn().mockResolvedValue(singleSongData), @@ -60,8 +60,8 @@ describe('SongsResolver receives the expected songs array from the service', () expect(result).toEqual(songFullTestResponse); }); - it('getIARecommendations retrieves a songs recommendations array ready to be delivered', async () => { - const results = await resolver.getIARecommendations( + it('getSongRecommendations retrieves a songs recommendations array ready to be delivered', async () => { + const results = await resolver.getSongRecommendations( ['Rock', 'Alternative', 'Alternative Rock', 'Grunge'], USER_VECTOR, 40, @@ -168,24 +168,6 @@ describe('SongsResolver is able to communicate the error from the service to hel expect(service.getSongData).toHaveBeenCalledWith(999); }); - it("getIARecommendations throws an error when the service couldn't connect with the database", async () => { - await expect( - resolver.getIARecommendations([], USER_VECTOR, 40), - ).rejects.toThrow(InternalServerErrorException); - - await expect( - resolver.getIARecommendations([], USER_VECTOR, 40), - ).rejects.toThrow( - 'Database Error: SequelizeTimeoutError: Connection refused', - ); - - expect(service.getSongRecommendations).toHaveBeenCalledWith( - [], - USER_VECTOR, - 40, - ); - }); - it("getNextSong throws an error when the service couldn't connect with the database", async () => { await expect(resolver.getNextSong(1)).rejects.toThrow( InternalServerErrorException, diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index ac0c545..8c611c7 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -27,8 +27,8 @@ export class SongsResolver { return this.songsService.getSongData(songID); } - @Query(() => [SongResponseDto], { name: 'getIARecommendations' }) - async getIARecommendations( + @Query(() => [SongResponseDto], { name: 'getSongRecommendations' }) + async getSongRecommendations( @Args('genres', { type: () => [String], defaultValue: ['Rock'] }) genres: string[], @Args('userVector', { type: () => [Float], defaultValue: USER_VECTOR }) diff --git a/back/src/songs/songs.service.spec.ts b/back/src/songs/songs.service.spec.ts index fbeca81..a61ed95 100644 --- a/back/src/songs/songs.service.spec.ts +++ b/back/src/songs/songs.service.spec.ts @@ -11,7 +11,7 @@ import { singleSongTestData, songTestData, songFullRawResponse, - songIARawResponse, + songRecRawResponse, } from '../../test/data/songsModule/serSongData'; import { expectFullSongProps, expectSongProps } from 'src/utils/expectSongs'; import { @@ -273,14 +273,14 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab describe('getSongRecommendations returns a song recommendations array based by user input', () => { beforeEach(() => { - songModel.sequelize?.query.mockResolvedValue(songIARawResponse); + songModel.sequelize?.query.mockResolvedValue(songRecRawResponse); }); it("getSongRecommendations returns an array with the expected songs", async () => { const songRecommendations = await service.getSongRecommendations(["Rock"], USER_VECTOR, 5) expect(songRecommendations).toHaveLength(5); expect(songRecommendations).toStrictEqual(songRecommendResponses); - }) + }); }); }); diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index 43b0ff2..dd09196 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -5,12 +5,10 @@ import { ServiceUnavailableException, } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; -import { Op, QueryTypes, Sequelize } from 'sequelize'; -import { buildSongVector } from './utils/buildSongVector'; +import { QueryTypes, Sequelize } from 'sequelize'; import { parseSongRecommendations, parseFullSongResponse, - parseIASongData, parseSongResponse, parseString, parseStringArray, @@ -18,8 +16,6 @@ import { import { FullSongResponse, FullSongResponseAttributes, - IASongResponse, - SongCosResponse, SongResponse, SongResponseAttributes, } from 'src/types/songAttributes'; @@ -151,7 +147,6 @@ export class SongsService { replacements: { genres, userVector: [parsedVector], limit }, }, ); - console.log(rawSongData) const parsedData = parseSongRecommendations(rawSongData); diff --git a/back/src/songs/utils/buildSongVector.ts b/back/src/songs/utils/buildSongVector.ts deleted file mode 100644 index aee4811..0000000 --- a/back/src/songs/utils/buildSongVector.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { parseFloatNum, parseNumberArray } from 'src/types/parses'; -import { IASongResponse } from 'src/types/songAttributes'; - -export const buildSongVector = (song: IASongResponse) => { - return parseNumberArray([ - song.songDetails.energy, - song.songDetails.speechiness, - song.songDetails.danceability, - song.duration, - song.songDetails.valence, - parseFloatNum(song.songDetails.instrumentalness), - song.songDetails.mode, - parseFloatNum(song.songDetails.acousticness), - ]); -}; diff --git a/back/src/types/parses.spec.ts b/back/src/types/parses.spec.ts index b4deb5f..c3be45a 100644 --- a/back/src/types/parses.spec.ts +++ b/back/src/types/parses.spec.ts @@ -5,7 +5,7 @@ import { parseArtistSongs, parseFloatNum, parseFullSongResponse, - parseIASongData, + parseSongRecommendations, parseNumberArray, parseSongGenres, parseSongResponse, @@ -17,7 +17,7 @@ import { albumSongs } from '../../test/data/albumsModule/AlbumData'; import { songGenresData } from '../../test/data/songGenresModule/songGenresData'; import { songFullRawResponse, - songIARawResponse, + songRecRawResponse, songTestData, } from '../../test/data/songsModule/serSongData'; import { @@ -204,21 +204,21 @@ describe('parses return the data with the expected type', () => { }); }); - describe('parseIASongData', () => { - it('parseIASongData returns an error when the parse is not an array of numbers', () => { + describe('parseSongRecommendations', () => { + it('parseSongRecommendations returns an error when the parse is not an array of numbers', () => { const errArray = [123456, ['testing'], null, undefined]; errArray.forEach((err) => expectParseError( - parseIASongData, + parseSongRecommendations, err, - "The data received doesn't have the required structure for the IA recommendation.", + "The data recieved didn't match with the expected song recommendation format", ), ); }); it('parseIASongData ensure that the songs match the correct object structure', () => { - songIARawResponse.forEach((song) => - expect(parseIASongData(song)).toBe(song), + songRecRawResponse.forEach((song) => + expect(parseSongRecommendations(song)).toBe(song), ); }); }); diff --git a/back/src/types/parses.ts b/back/src/types/parses.ts index 1173917..b2b4122 100644 --- a/back/src/types/parses.ts +++ b/back/src/types/parses.ts @@ -8,8 +8,7 @@ import { } from './searchTypes'; import { FullSongResponseAttributes, - IASongResponse, - SongCosResponse, + SongRecResponse, SongResponseAttributes, } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; @@ -19,7 +18,6 @@ import { isArtistSearchData, isArtistSongs, isFullSongResponse, - isIASongData, isNumberArray, isSongGenres, isSongResponse, @@ -97,14 +95,7 @@ export const parseFullSongResponse = ( ); }; -export const parseIASongData = (data: unknown): IASongResponse => { - if (isIASongData(data)) return data; - throw new BadRequestException( - "The data received doesn't have the required structure for the IA recommendation.", - ); -}; - -export const parseSongRecommendations = (data: unknown): SongCosResponse[] => { +export const parseSongRecommendations = (data: unknown): SongRecResponse[] => { if (isSongCosData(data)) return data; throw new BadRequestException( "The data recieved didn't match with the expected song recommendation format", diff --git a/back/src/types/songAttributes.ts b/back/src/types/songAttributes.ts index fa6627c..32a62bc 100644 --- a/back/src/types/songAttributes.ts +++ b/back/src/types/songAttributes.ts @@ -31,7 +31,7 @@ export interface SongResponse { url_preview: string; } -export interface SongCosResponse { +export interface SongRecResponse { id: number; name: string; artists: string; @@ -59,13 +59,6 @@ export interface FullSongResponseAttributes extends SongAttributes { genres: GenresModel[]; } -export interface IASongResponse extends SongAttributes { - artists: ArtistsModel[]; - album: AlbumsModel; - genres: GenresModel[]; - songDetails: SongDetailsModel; -} - export type SongCreationAttributes = Optional< SongAttributes, 'id' | 'artists' | 'album' | 'genres' | 'songDetails' diff --git a/back/src/types/verify.spec.ts b/back/src/types/verify.spec.ts index 0c6b4cd..c933e69 100644 --- a/back/src/types/verify.spec.ts +++ b/back/src/types/verify.spec.ts @@ -4,7 +4,7 @@ import { isArtistSearchData, isArtistSongs, isFullSongResponse, - isIASongData, + isSongCosData, isNumber, isNumberArray, isSongGenres, @@ -17,7 +17,7 @@ import { albumSongs } from '../../test/data/albumsModule/AlbumData'; import { songGenresData } from '../../test/data/songGenresModule/songGenresData'; import { songFullRawResponse, - songIARawResponse, + songRecRawResponse, songTestData, } from '../../test/data/songsModule/serSongData'; import { @@ -184,16 +184,16 @@ describe('Verify value Types', () => { describe('isIASongData', () => { it('isIASongData returns early false when value is null or undefined', () => { - expect(isIASongData(null)).toBe(false); - expect(isIASongData(undefined)).toBe(false); + expect(isSongCosData(null)).toBe(false); + expect(isSongCosData(undefined)).toBe(false); }); it("isIASongData returns false when the object doesn't have the expected props", () => { - expect(isIASongData(WRONG_OBJ)).toBe(false); + expect(isSongCosData(WRONG_OBJ)).toBe(false); }); it('isIASongData returns true when it has the correct props', () => { - expect(isIASongData(songIARawResponse[0])).toBe(true); + expect(isSongCosData(songRecRawResponse)).toBe(true); }); }); diff --git a/back/src/types/verify.ts b/back/src/types/verify.ts index 445c4cd..39ef444 100644 --- a/back/src/types/verify.ts +++ b/back/src/types/verify.ts @@ -7,8 +7,7 @@ import { } from './searchTypes'; import { FullSongResponseAttributes, - IASongResponse, - SongCosResponse, + SongRecResponse, } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; @@ -136,40 +135,16 @@ export const isFullSongResponse = ( ); }; -export const isSongCosData = (data: unknown): data is SongCosResponse[] => { +export const isSongCosData = (data: unknown): data is SongRecResponse[] => { if (data === null || data === undefined) return false; return ( Array.isArray(data) && - 'id' in data[0] && - 'name' in data[0] && - 'artists' in data[0] && - 'url_preview' in data[0] && - 'album_cover' in data[0] && - 'cos_sim' in data[0] - ); -}; - -export const isIASongData = (data: unknown): data is IASongResponse => { - if (data === null || data === undefined) return false; - - return ( - typeof data === 'object' && - 'id' in data && - 'name' in data && - 'duration' in data && - 'url_preview' in data && - 'artists' in data && - 'genres' in data && - 'album' in data && - 'songDetails' in data && - isNumber(data.id) && - isString(data.name) && - isNumber(data.duration) && - isString(data.url_preview) && - Array.isArray(data.artists) && - Array.isArray(data.genres) && - typeof data.album === 'object' && - typeof data.songDetails === 'object' + data.every(song => "id" in song) && + data.every(song => "name" in song) && + data.every(song => "artists" in song) && + data.every(song => "url_preview" in song) && + data.every(song => "album_cover" in song) && + data.every(song => "cos_sim" in song) ); }; diff --git a/back/test/data/songsModule/resSongData.ts b/back/test/data/songsModule/resSongData.ts index 3714e39..e3b7e66 100644 --- a/back/test/data/songsModule/resSongData.ts +++ b/back/test/data/songsModule/resSongData.ts @@ -1,7 +1,6 @@ import { FullSongResponse, SongResponse, - SongResponseAttributes, } from 'src/types/songAttributes'; import { TESTING_IMG, TESTING_URL } from '../../../test/constants/constants'; diff --git a/back/test/data/songsModule/serSongData.ts b/back/test/data/songsModule/serSongData.ts index 2b13f90..b6e895e 100644 --- a/back/test/data/songsModule/serSongData.ts +++ b/back/test/data/songsModule/serSongData.ts @@ -1,6 +1,6 @@ import { FullSongResponseAttributes, - IASongResponse, + SongRecResponse, SongResponseAttributes, } from 'src/types/songAttributes'; import { TESTING_IMG, TESTING_URL } from '../../../test/constants/constants'; @@ -98,7 +98,7 @@ export const songFullRawResponse = { ], } as unknown as FullSongResponseAttributes; -export const songIARawResponse = [ +export const songRecRawResponse = [ { id: 1, name: 'Malpractice', @@ -139,4 +139,4 @@ export const songIARawResponse = [ artists: 'Quicksand', cos_sim: 0.9994 }, -] as unknown as IASongResponse[]; +] as unknown as SongRecResponse[]; diff --git a/front/src/components/Utils/hooks/useLoadRec.ts b/front/src/components/Utils/hooks/useLoadRec.ts index d3f7fb4..ac40a28 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../redux-hooks"; import { useLazyQuery } from "@apollo/client"; import { setLaraRecommendations } from "@/reducers/recommendReducer"; -import { getIARecommendations } from "@/queries/LaraRecQuerie"; +import { getSongRecommendations } from "@/queries/LaraRecQuerie"; import minMaxScale from "../minMaxScale"; import { MAXDURATION, maxLoudness, maxTempo, MINDURATION, minLoudness, minTempo, TIMESIGNATURENOR, TRACKKEYNOR } from "@/components/constants/ModalC"; @@ -16,7 +16,7 @@ const useLoadRec = (setOpen: React.Dispatch>) => { const { genres, energy, speechLevel, danceability, duration, sentiment, voiceType, mood, acousticness } = useAppSelector(state => state.songData); const dispatch = useAppDispatch(); - const [getIASongs] = useLazyQuery(getIARecommendations); + const [getIASongs] = useLazyQuery(getSongRecommendations); const liveness = mood === 1 ? randInRange(0.6, 0.9) : randInRange(0.05, 0.3); const tempo = sentiment > 0.5 ? randInRange(120, 150) : randInRange(70, 100); @@ -41,13 +41,15 @@ const useLoadRec = (setOpen: React.Dispatch>) => { tempoNor, TIMESIGNATURENOR, ]; + + const limit = 40; const loadRecommendations = () => { setLoading(true); - getIASongs({ variables: { genres, userVector}, + getIASongs({ variables: { genres, userVector, limit }, onCompleted: (data) => { - if (data.getIARecommendations) { - dispatch(setLaraRecommendations(data.getIARecommendations)); + if (data.getSongRecommendations) { + dispatch(setLaraRecommendations(data.getSongRecommendations)); navigate("/recommendations"); setLoading(false); setOpen(false); diff --git a/front/src/queries/LaraRecQuerie.ts b/front/src/queries/LaraRecQuerie.ts index 3f68bb9..8272b21 100644 --- a/front/src/queries/LaraRecQuerie.ts +++ b/front/src/queries/LaraRecQuerie.ts @@ -1,8 +1,8 @@ import { gql } from "@apollo/client"; -export const getIARecommendations = gql` -query getIARecommendedSongs ($genres: [String!], $userVector: [Float!]) { - getIARecommendations (genres: $genres, userVector: $userVector) { +export const getSongRecommendations = gql` +query getRecommendedSongs ($genres: [String!], $userVector: [Float!], $limit: Int!) { + getSongRecommendations (genres: $genres, userVector: $userVector, limit: $limit) { id name artists From 3d0603aad160e41131651b8ed695dc76257c60fe Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 10:45:06 -0400 Subject: [PATCH 34/38] r fixing git problems --- back/src/songs/songs.resolver.ts | 6 +----- back/src/songs/songs.service.spec.ts | 12 ++++++++---- back/src/songs/songs.service.ts | 1 - back/src/types/parses.spec.ts | 6 +++--- back/src/types/verify.spec.ts | 8 ++++---- back/src/types/verify.ts | 18 ++++++++---------- back/test/data/songsModule/resSongData.ts | 5 +---- back/test/data/songsModule/serSongData.ts | 12 ++++++------ 8 files changed, 31 insertions(+), 37 deletions(-) diff --git a/back/src/songs/songs.resolver.ts b/back/src/songs/songs.resolver.ts index 8c611c7..70a0f9c 100644 --- a/back/src/songs/songs.resolver.ts +++ b/back/src/songs/songs.resolver.ts @@ -36,11 +36,7 @@ export class SongsResolver { @Args('limit', { type: () => Int, defaultValue: 40 }) limit: number, ): Promise { - return this.songsService.getSongRecommendations( - genres, - userVector, - limit, - ); + return this.songsService.getSongRecommendations(genres, userVector, limit); } @Query(() => SongResponseDto, { name: 'getRandomSong' }) diff --git a/back/src/songs/songs.service.spec.ts b/back/src/songs/songs.service.spec.ts index a61ed95..10bda89 100644 --- a/back/src/songs/songs.service.spec.ts +++ b/back/src/songs/songs.service.spec.ts @@ -36,7 +36,7 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab findByPk: jest.Mock; findOne: jest.Mock; sequelize: { - query: jest.Mock + query: jest.Mock; }; }; @@ -53,7 +53,7 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab findOne: jest.fn(), sequelize: { query: jest.fn(), - } + }, }, }, ], @@ -276,8 +276,12 @@ describe('SongsService retrieves, evaluates and parses songs data from the datab songModel.sequelize?.query.mockResolvedValue(songRecRawResponse); }); - it("getSongRecommendations returns an array with the expected songs", async () => { - const songRecommendations = await service.getSongRecommendations(["Rock"], USER_VECTOR, 5) + it('getSongRecommendations returns an array with the expected songs', async () => { + const songRecommendations = await service.getSongRecommendations( + ['Rock'], + USER_VECTOR, + 5, + ); expect(songRecommendations).toHaveLength(5); expect(songRecommendations).toStrictEqual(songRecommendResponses); }); diff --git a/back/src/songs/songs.service.ts b/back/src/songs/songs.service.ts index dd09196..c778383 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -25,7 +25,6 @@ import { AlbumsModel } from '../../models/albums/albums.model'; import { SongsModel } from '../../models/songs/song.model'; import { ArtistsModel } from '../../models/artists/artists.model'; import { GenresModel } from '../../models/genres/genres.model'; -import { SongDetailsModel } from '../../models/song_details/SongDetails.model'; @Injectable() export class SongsService { diff --git a/back/src/types/parses.spec.ts b/back/src/types/parses.spec.ts index c3be45a..5d966c6 100644 --- a/back/src/types/parses.spec.ts +++ b/back/src/types/parses.spec.ts @@ -217,9 +217,9 @@ describe('parses return the data with the expected type', () => { }); it('parseIASongData ensure that the songs match the correct object structure', () => { - songRecRawResponse.forEach((song) => - expect(parseSongRecommendations(song)).toBe(song), - ); + const songResults = parseSongRecommendations(songRecRawResponse); + expect(songResults).toHaveLength(songRecRawResponse.length); + expect(songResults).toStrictEqual(songRecRawResponse); }); }); diff --git a/back/src/types/verify.spec.ts b/back/src/types/verify.spec.ts index c933e69..7c0a6d1 100644 --- a/back/src/types/verify.spec.ts +++ b/back/src/types/verify.spec.ts @@ -182,17 +182,17 @@ describe('Verify value Types', () => { }); }); - describe('isIASongData', () => { - it('isIASongData returns early false when value is null or undefined', () => { + describe('isSongCosData', () => { + it('isSongCosData returns early false when value is null or undefined', () => { expect(isSongCosData(null)).toBe(false); expect(isSongCosData(undefined)).toBe(false); }); - it("isIASongData returns false when the object doesn't have the expected props", () => { + it("isSongCosData returns false when the object doesn't have the expected props", () => { expect(isSongCosData(WRONG_OBJ)).toBe(false); }); - it('isIASongData returns true when it has the correct props', () => { + it('isSongCosData returns true when it has the correct props', () => { expect(isSongCosData(songRecRawResponse)).toBe(true); }); }); diff --git a/back/src/types/verify.ts b/back/src/types/verify.ts index 39ef444..b679407 100644 --- a/back/src/types/verify.ts +++ b/back/src/types/verify.ts @@ -5,10 +5,7 @@ import { artistSearchResults, songSearchResults, } from './searchTypes'; -import { - FullSongResponseAttributes, - SongRecResponse, -} from './songAttributes'; +import { FullSongResponseAttributes, SongRecResponse } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; export const isString = (text: unknown): text is string => { @@ -139,12 +136,13 @@ export const isSongCosData = (data: unknown): data is SongRecResponse[] => { if (data === null || data === undefined) return false; return ( Array.isArray(data) && - data.every(song => "id" in song) && - data.every(song => "name" in song) && - data.every(song => "artists" in song) && - data.every(song => "url_preview" in song) && - data.every(song => "album_cover" in song) && - data.every(song => "cos_sim" in song) + data.every((song) => typeof song === 'object') && + data.every((song) => 'id' in song) && + data.every((song) => 'name' in song) && + data.every((song) => 'artists' in song) && + data.every((song) => 'url_preview' in song) && + data.every((song) => 'album_cover' in song) && + data.every((song) => 'cos_sim' in song) ); }; diff --git a/back/test/data/songsModule/resSongData.ts b/back/test/data/songsModule/resSongData.ts index e3b7e66..1c72277 100644 --- a/back/test/data/songsModule/resSongData.ts +++ b/back/test/data/songsModule/resSongData.ts @@ -1,7 +1,4 @@ -import { - FullSongResponse, - SongResponse, -} from 'src/types/songAttributes'; +import { FullSongResponse, SongResponse } from 'src/types/songAttributes'; import { TESTING_IMG, TESTING_URL } from '../../../test/constants/constants'; export const songTestResponses: SongResponse[] = [ diff --git a/back/test/data/songsModule/serSongData.ts b/back/test/data/songsModule/serSongData.ts index b6e895e..ad18d8c 100644 --- a/back/test/data/songsModule/serSongData.ts +++ b/back/test/data/songsModule/serSongData.ts @@ -104,8 +104,8 @@ export const songRecRawResponse = [ name: 'Malpractice', url_preview: TESTING_URL, album_cover: TESTING_IMG, - artists: "Testament", - cos_sim: 0.9998 + artists: 'Testament', + cos_sim: 0.9998, }, { id: 2, @@ -113,7 +113,7 @@ export const songRecRawResponse = [ url_preview: TESTING_URL, album_cover: TESTING_IMG, artists: 'The Cardigans', - cos_sim: 0.9997 + cos_sim: 0.9997, }, { id: 3, @@ -121,7 +121,7 @@ export const songRecRawResponse = [ url_preview: TESTING_URL, album_cover: TESTING_IMG, artists: 'Aerosmith', - cos_sim: 0.9996 + cos_sim: 0.9996, }, { id: 4, @@ -129,7 +129,7 @@ export const songRecRawResponse = [ url_preview: TESTING_URL, album_cover: TESTING_IMG, artists: 'JAMES', - cos_sim: 0.9995 + cos_sim: 0.9995, }, { id: 5, @@ -137,6 +137,6 @@ export const songRecRawResponse = [ url_preview: TESTING_URL, album_cover: TESTING_IMG, artists: 'Quicksand', - cos_sim: 0.9994 + cos_sim: 0.9994, }, ] as unknown as SongRecResponse[]; From 0614aaa8815ca93a6dcbb22550263ee0e901e5d7 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 15:50:17 -0400 Subject: [PATCH 35/38] r fixing conflict --- back/database/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/back/database/main.go b/back/database/main.go index 683234c..66e85ae 100644 --- a/back/database/main.go +++ b/back/database/main.go @@ -15,6 +15,7 @@ import ( func main() { serviceURI := os.Getenv("dbURI") + if len(os.Args) != 2 { printHelp() return From 76671c0ace5a46ac354968886199b91086e9c2f7 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 16:44:30 -0400 Subject: [PATCH 36/38] r updating front latest build --- back/public/index.html | 2 +- e2e/package-lock.json | 112 ----------------------------------------- 2 files changed, 1 insertion(+), 113 deletions(-) delete mode 100644 e2e/package-lock.json diff --git a/back/public/index.html b/back/public/index.html index 328bb60..6acd5b5 100644 --- a/back/public/index.html +++ b/back/public/index.html @@ -17,7 +17,7 @@ color: white; } - + diff --git a/e2e/package-lock.json b/e2e/package-lock.json deleted file mode 100644 index 2affb71..0000000 --- a/e2e/package-lock.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "name": "e2e-tests", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "e2e-tests", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "dotenv": "^17.2.1" - }, - "devDependencies": { - "@playwright/test": "^1.54.1", - "@types/node": "^24.1.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - } - } -} From b627b7d11245c52db865862138727ef94b0f2cb0 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 16:45:40 -0400 Subject: [PATCH 37/38] r fixing db URL --- back/src/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/src/app.module.ts b/back/src/app.module.ts index 388007b..d527e04 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -25,7 +25,7 @@ dotenv.config(); }), SequelizeModule.forRoot({ dialect: 'postgres', - uri: "postgresql://postgres:postgres@localhost:5432/music_db", + uri: process.env.DB_URL, port: Number(process.env.DB_PORT), dialectOptions: { ssl: { From f4b1888e3d42f14ee5b43d4f6eeac622c39e8935 Mon Sep 17 00:00:00 2001 From: Raghart Date: Mon, 16 Feb 2026 16:51:18 -0400 Subject: [PATCH 38/38] uploading package lock for e2e --- e2e/package-lock.json | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 e2e/package-lock.json diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..aea3377 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,112 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e-tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^17.2.1" + }, + "devDependencies": { + "@playwright/test": "^1.54.1", + "@types/node": "^24.1.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + } + } +}