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/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 new file mode 100644 index 0000000..93fea0e --- /dev/null +++ b/back/database/dbConfig/databaseConfig.go @@ -0,0 +1,470 @@ +package databaseConfig + +import ( + "context" + "encoding/csv" + "fmt" + "log" + "os" + "scripts/internal/database" + "strconv" + + "github.com/pgvector/pgvector-go" +) + +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.CreateArtist(context.Background(), database.CreateArtistParams{ + 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 (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.CreateGenre(context.Background(), database.CreateGenreParams{ + 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 (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 (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 (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 (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 (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 (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) + } + + 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{ + durationNor, + song.Danceability, + song.Energy, + trackKeyNor, + loudnessNor, + song.Mode, + song.Speechiness, + float32(song.Acousticness), + float32(song.Instrumentalness), + song.Liveness, + song.Valence, + tempoNor, + timeSigNor, + } + + 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 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 { + log.Fatal(err) + } + + reader := csv.NewReader(file) + return reader.ReadAll() +} + +type TableType string + +const ( + ArtistsType TableType = "artists" +) diff --git a/back/database/go.mod b/back/database/go.mod new file mode 100644 index 0000000..23747ef --- /dev/null +++ b/back/database/go.mod @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..7f6661d --- /dev/null +++ b/back/database/go.sum @@ -0,0 +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/albums.sql.go b/back/database/internal/database/albums.sql.go new file mode 100644 index 0000000..c729254 --- /dev/null +++ b/back/database/internal/database/albums.sql.go @@ -0,0 +1,49 @@ +// 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 +} + +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/artists.sql.go b/back/database/internal/database/artists.sql.go new file mode 100644 index 0000000..b5d1b04 --- /dev/null +++ b/back/database/internal/database/artists.sql.go @@ -0,0 +1,48 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: artists.sql + +package database + +import ( + "context" +) + +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 CreateArtistParams struct { + ID int32 + Name string +} + +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 getArtistByID = `-- name: GetArtistByID :one +SELECT id, name FROM artists WHERE id = $1 +` + +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/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/genres.sql.go b/back/database/internal/database/genres.sql.go new file mode 100644 index 0000000..1c12c06 --- /dev/null +++ b/back/database/internal/database/genres.sql.go @@ -0,0 +1,39 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: genres.sql + +package database + +import ( + "context" +) + +const createGenre = `-- name: CreateGenre :one +INSERT INTO genres(id, genre) +VALUES($1, $2) +RETURNING id, genre +` + +type CreateGenreParams struct { + ID int32 + Genre string +} + +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 +} + +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/models.go b/back/database/internal/database/models.go new file mode 100644 index 0000000..919730c --- /dev/null +++ b/back/database/internal/database/models.go @@ -0,0 +1,61 @@ +// 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 float32 + Year int32 + AlbumID 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 + Vectors interface{} +} + +type SongGenre struct { + ID int32 + SongID int32 + GenreID int32 +} 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/song_details.sql.go b/back/database/internal/database/song_details.sql.go new file mode 100644 index 0000000..71943dc --- /dev/null +++ b/back/database/internal/database/song_details.sql.go @@ -0,0 +1,159 @@ +// 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, vectors +` + +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, + &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/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/internal/database/songs.sql.go b/back/database/internal/database/songs.sql.go new file mode 100644 index 0000000..87c8ef8 --- /dev/null +++ b/back/database/internal/database/songs.sql.go @@ -0,0 +1,77 @@ +// 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 +} + +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 new file mode 100644 index 0000000..66e85ae --- /dev/null +++ b/back/database/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/url" + "os" + databaseConfig "scripts/dbConfig" + "scripts/internal/database" + paths "scripts/pathConstants" + + _ "github.com/lib/pq" +) + +func main() { + serviceURI := os.Getenv("dbURI") + + if len(os.Args) != 2 { + printHelp() + return + } + + 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() + + dbCfg := &databaseConfig.DbConfig{ + Queries: database.New(db), + } + + switch os.Args[1] { + case "artists": + { + databaseConfig.AddToDatabase(paths.ARTISTPATH, dbCfg.AddArtistsDatabase) + } + case "genres": + { + databaseConfig.AddToDatabase(paths.GenresPath, dbCfg.AddGenresDatabase) + } + case "albums": + { + databaseConfig.AddToDatabase(paths.AlbumsPath, dbCfg.AddAlbumsDatabase) + } + case "songs": + { + databaseConfig.AddToDatabase(paths.SongsPath, dbCfg.AddSongsDatabase) + } + case "song_artists": + { + databaseConfig.AddToDatabase(paths.SongArtistsPath, dbCfg.AddSongsArtistsDatabase) + } + case "song_genres": + { + databaseConfig.AddToDatabase(paths.SongGenresPath, dbCfg.AddSongGenresDatabase) + } + case "song_details": + { + databaseConfig.AddToDatabase(paths.SongDetailsPath, dbCfg.AddSongDetailsDatabase) + } + case "vectors": + { + dbCfg.AddVectorsDatabase() + } + default: + { + fmt.Printf("'%v' is not a valid command\n", os.Args[1]) + printHelp() + return + } + } +} + +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/pathConstants/paths.go b/back/database/pathConstants/paths.go new file mode 100644 index 0000000..242810f --- /dev/null +++ b/back/database/pathConstants/paths.go @@ -0,0 +1,11 @@ +package paths + +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") +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/albums.sql b/back/database/sql/queries/albums.sql new file mode 100644 index 0000000..960b124 --- /dev/null +++ b/back/database/sql/queries/albums.sql @@ -0,0 +1,10 @@ +-- name: CreateAlbum :one +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/artists.sql b/back/database/sql/queries/artists.sql new file mode 100644 index 0000000..fd4aa8b --- /dev/null +++ b/back/database/sql/queries/artists.sql @@ -0,0 +1,10 @@ +-- name: CreateArtist :one +INSERT INTO artists (id, name) +VALUES ($1, $2) +RETURNING *; + +-- name: GetArtistByID :one +SELECT * FROM artists WHERE id = $1; + +-- name: CleanArtists :exec +DELETE FROM artists; \ No newline at end of file diff --git a/back/database/sql/queries/genres.sql b/back/database/sql/queries/genres.sql new file mode 100644 index 0000000..bd55573 --- /dev/null +++ b/back/database/sql/queries/genres.sql @@ -0,0 +1,7 @@ +-- name: CreateGenre :one +INSERT INTO genres(id, genre) +VALUES($1, $2) +RETURNING *; + +-- name: GetGenreByID :one +SELECT * FROM genres WHERE id = $1; \ No newline at end of file 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/song_details.sql b/back/database/sql/queries/song_details.sql new file mode 100644 index 0000000..5cd3490 --- /dev/null +++ b/back/database/sql/queries/song_details.sql @@ -0,0 +1,17 @@ +-- 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: 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/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 diff --git a/back/database/sql/queries/songs.sql b/back/database/sql/queries/songs.sql new file mode 100644 index 0000000..72dc0a2 --- /dev/null +++ b/back/database/sql/queries/songs.sql @@ -0,0 +1,10 @@ +-- 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: GetSongByID :one +SELECT * FROM songs WHERE id = $1; + +-- name: CleanSongs :exec +DELETE FROM songs; \ No newline at end of file diff --git a/back/database/sql/queries/vectorFunction.sql b/back/database/sql/queries/vectorFunction.sql new file mode 100644 index 0000000..017552b --- /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 (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; +$$; \ No newline at end of file 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/database/sql/schema/002_albums.sql b/back/database/sql/schema/002_albums.sql new file mode 100644 index 0000000..8f92deb --- /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 +); + +-- +goose Down +DROP TABLE albums; 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 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 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 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 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 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 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 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; 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 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/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_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/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/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", 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/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/FullSongResponse.dto.ts b/back/scripts/dto/FullSongResponse.dto.ts deleted file mode 100644 index 99f4bd0..0000000 --- a/back/scripts/dto/FullSongResponse.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 diff --git a/back/scripts/dto/SongResponse.dto.ts b/back/scripts/dto/SongResponse.dto.ts deleted file mode 100644 index 92a780b..0000000 --- a/back/scripts/dto/SongResponse.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/back/scripts/dto/albumsSearchDto.ts b/back/scripts/dto/albumsSearchDto.ts deleted file mode 100644 index 3419627..0000000 --- a/back/scripts/dto/albumsSearchDto.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/back/scripts/dto/artistsResults.dto.ts b/back/scripts/dto/artistsResults.dto.ts deleted file mode 100644 index 5681ddd..0000000 --- a/back/scripts/dto/artistsResults.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 diff --git a/back/scripts/dto/artistsSearchDto.ts b/back/scripts/dto/artistsSearchDto.ts deleted file mode 100644 index 6781af0..0000000 --- a/back/scripts/dto/artistsSearchDto.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 diff --git a/back/scripts/dto/multipleSearchDto.ts b/back/scripts/dto/multipleSearchDto.ts deleted file mode 100644 index 1f602b2..0000000 --- a/back/scripts/dto/multipleSearchDto.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/scripts/dto/songsSearchDto.ts deleted file mode 100644 index b1f237d..0000000 --- a/back/scripts/dto/songsSearchDto.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 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/app.module.ts b/back/src/app.module.ts index c88c472..388007b 100644 --- a/back/src/app.module.ts +++ b/back/src/app.module.ts @@ -23,15 +23,9 @@ dotenv.config(); ConfigModule.forRoot({ isGlobal: true, }), - ServeStaticModule.forRoot({ - 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: { @@ -43,6 +37,9 @@ dotenv.config(); synchronize: true, logging: false, }), + ServeStaticModule.forRoot({ + rootPath: join(__dirname, '..', '..', 'public'), + }), GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: true, 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/src/artists/dto/artistsResults.dto.ts b/back/src/artists/dto/artistsResults.dto.ts new file mode 100644 index 0000000..93e1de4 --- /dev/null +++ b/back/src/artists/dto/artistsResults.dto.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..b71a578 --- /dev/null +++ b/back/src/search/dto/albumsSearchDto.ts @@ -0,0 +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'; +} diff --git a/back/src/search/dto/artistsSearchDto.ts b/back/src/search/dto/artistsSearchDto.ts new file mode 100644 index 0000000..ed043da --- /dev/null +++ b/back/src/search/dto/artistsSearchDto.ts @@ -0,0 +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'; +} diff --git a/back/src/search/dto/multipleSearchDto.ts b/back/src/search/dto/multipleSearchDto.ts new file mode 100644 index 0000000..a466915 --- /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[]; +} diff --git a/back/src/search/dto/songsSearchDto.ts b/back/src/search/dto/songsSearchDto.ts new file mode 100644 index 0000000..8f78af9 --- /dev/null +++ b/back/src/search/dto/songsSearchDto.ts @@ -0,0 +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'; +} diff --git a/back/src/search/search.module.ts b/back/src/search/search.module.ts index 5633587..1fe5be5 100644 --- a/back/src/search/search.module.ts +++ b/back/src/search/search.module.ts @@ -10,7 +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 { bonsaiClientProvider } from '../../scripts/bonsai/bonsaiClient'; +import { Client } from 'elasticsearch'; +import dotenv from 'dotenv'; +dotenv.config(); @Module({ imports: [ @@ -25,7 +27,20 @@ import { bonsaiClientProvider } from '../../scripts/bonsai/bonsaiClient'; GenresModel, ]), ], - providers: [bonsaiClientProvider, SearchService, SearchResolver], + providers: [ + { + provide: 'BonsaiClient', + useFactory: () => { + 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/src/songs/dto/FullSongResponse.dto.ts b/back/src/songs/dto/FullSongResponse.dto.ts new file mode 100644 index 0000000..72855de --- /dev/null +++ b/back/src/songs/dto/FullSongResponse.dto.ts @@ -0,0 +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; +} diff --git a/back/src/songs/dto/SongResponse.dto.ts b/back/src/songs/dto/SongResponse.dto.ts new file mode 100644 index 0000000..d5edb5d --- /dev/null +++ b/back/src/songs/dto/SongResponse.dto.ts @@ -0,0 +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; +} diff --git a/back/src/songs/songs.resolver.spec.ts b/back/src/songs/songs.resolver.spec.ts index b54e313..c3f1698 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'; @@ -27,9 +27,9 @@ 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(songIATestResponses), + .mockReturnValue(songRecommendResponses), getRandomSong: jest.fn().mockResolvedValue(singleSongData), getNextSong: jest.fn().mockResolvedValue(singleSongData), getPreviousSong: jest.fn().mockResolvedValue(singleSongData), @@ -60,15 +60,11 @@ 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( - ['Rock', 'Alternative', 'Alternative Rock', 'Grunge'], - USER_VECTOR, - ); - - expect(service.getIARecommendations).toHaveBeenCalledWith( + 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, ); expect(results).toHaveLength(5); @@ -172,20 +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), - ).rejects.toThrow(InternalServerErrorException); - - await expect( - resolver.getIARecommendations([], USER_VECTOR), - ).rejects.toThrow( - 'Database Error: SequelizeTimeoutError: Connection refused', - ); - - expect(service.getIARecommendations).toHaveBeenCalledWith([], USER_VECTOR); - }); - 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 558fcb8..70a0f9c 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 { @@ -27,14 +27,16 @@ 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 }) userVector: number[], + @Args('limit', { type: () => Int, defaultValue: 40 }) + limit: number, ): Promise { - return this.songsService.getIARecommendations(genres, userVector); + 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 e8948ad..10bda89 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 { @@ -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,41 +271,19 @@ 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(songRecRawResponse); }); - 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( + 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(songIATestResponses); + expect(songRecommendations).toStrictEqual(songRecommendResponses); }); }); }); @@ -380,14 +363,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 af951b4..c778383 100644 --- a/back/src/songs/songs.service.ts +++ b/back/src/songs/songs.service.ts @@ -5,11 +5,10 @@ import { ServiceUnavailableException, } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; -import { Op, Sequelize } from 'sequelize'; -import { buildSongVector } from './utils/buildSongVector'; +import { QueryTypes, Sequelize } from 'sequelize'; import { + parseSongRecommendations, parseFullSongResponse, - parseIASongData, parseSongResponse, parseString, parseStringArray, @@ -17,7 +16,6 @@ import { import { FullSongResponse, FullSongResponseAttributes, - IASongResponse, SongResponse, SongResponseAttributes, } from 'src/types/songAttributes'; @@ -27,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 { @@ -135,73 +132,30 @@ export class SongsService { return this.parseFullSong(songData); } - 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( + async getSongRecommendations( genres: string[], userVector: number[], + limit: number, ): Promise { - const songData = await this.fetchIARecommendations(genres); - const songList = this.calculateRecommendations(songData, userVector); - return this.parseSongList(songList); + 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 }, + }, + ); + + const parsedData = parseSongRecommendations(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 fetchRandomSong(): Promise { 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..5d966c6 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,22 +204,22 @@ 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), - ); + const songResults = parseSongRecommendations(songRecRawResponse); + expect(songResults).toHaveLength(songRecRawResponse.length); + expect(songResults).toStrictEqual(songRecRawResponse); }); }); diff --git a/back/src/types/parses.ts b/back/src/types/parses.ts index 53b56d3..b2b4122 100644 --- a/back/src/types/parses.ts +++ b/back/src/types/parses.ts @@ -8,7 +8,7 @@ import { } from './searchTypes'; import { FullSongResponseAttributes, - IASongResponse, + SongRecResponse, SongResponseAttributes, } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; @@ -18,7 +18,6 @@ import { isArtistSearchData, isArtistSongs, isFullSongResponse, - isIASongData, isNumberArray, isSongGenres, isSongResponse, @@ -26,6 +25,7 @@ import { isStringArray, isString, isNumber, + isSongCosData, } from './verify'; export const parseString = (text: unknown): string => { @@ -95,10 +95,10 @@ export const parseFullSongResponse = ( ); }; -export const parseIASongData = (data: unknown): IASongResponse => { - if (isIASongData(data)) return data; +export const parseSongRecommendations = (data: unknown): SongRecResponse[] => { + if (isSongCosData(data)) return data; throw new BadRequestException( - "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", ); }; diff --git a/back/src/types/songAttributes.ts b/back/src/types/songAttributes.ts index b9fd24c..32a62bc 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 SongRecResponse { + id: number; + name: string; + artists: string; + album_cover: string; + url_preview: string; + cos_sim: number; +} + export interface FullSongResponse { id: number; name: string; @@ -50,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..7c0a6d1 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 { @@ -182,18 +182,18 @@ 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); + 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", () => { - expect(isIASongData(WRONG_OBJ)).toBe(false); + 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', () => { - expect(isIASongData(songIARawResponse[0])).toBe(true); + 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 c77dc80..b679407 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, SongRecResponse } from './songAttributes'; import { SongGenresRPWithSongs } from './songGenresAttributes'; export const isString = (text: unknown): text is string => { @@ -132,27 +132,17 @@ export const isFullSongResponse = ( ); }; -export const isIASongData = (data: unknown): data is IASongResponse => { +export const isSongCosData = (data: unknown): data is SongRecResponse[] => { 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' + Array.isArray(data) && + 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/constants/constants.ts b/back/test/constants/constants.ts index b86b90a..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,4 +18,12 @@ export const USER_VECTOR: [ number, number, number, -] = [0.5, 0.165, 0.5, 2.5, 0.5, 0.05, 1, 0.15]; + number, + number, + number, + number, + number, +] = [ + 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..1c72277 100644 --- a/back/test/data/songsModule/resSongData.ts +++ b/back/test/data/songsModule/resSongData.ts @@ -1,8 +1,4 @@ -import { - FullSongResponse, - SongResponse, - SongResponseAttributes, -} from 'src/types/songAttributes'; +import { FullSongResponse, SongResponse } from 'src/types/songAttributes'; import { TESTING_IMG, TESTING_URL } from '../../../test/constants/constants'; export const songTestResponses: SongResponse[] = [ @@ -78,153 +74,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 +97,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..ad18d8c 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,140 +98,45 @@ export const songFullRawResponse = { ], } as unknown as FullSongResponseAttributes; -export const songIARawResponse = [ +export const songRecRawResponse = [ { 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[]; +] as unknown as SongRecResponse[]; 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 2ca5587..ac40a28 100644 --- a/front/src/components/Utils/hooks/useLoadRec.ts +++ b/front/src/components/Utils/hooks/useLoadRec.ts @@ -3,7 +3,11 @@ 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"; +import randInRange from "../randInRange"; const useLoadRec = (setOpen: React.Dispatch>) => { const [loading, setLoading] = useState(false); @@ -12,16 +16,40 @@ 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 userVector = [energy, speechLevel, danceability, duration, sentiment, voiceType, mood, acousticness]; + 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, minTempo, maxTempo) + + const userVector = [ + durationNor, + danceability, + energy, + TRACKKEYNOR, + loudnessNor, + mood, + speechLevel, + acousticness, + voiceType, + liveness, + sentiment, + 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/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/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 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 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