From 6415ebac5dd9648c5c8e40bfbf3a45e6bffc0215 Mon Sep 17 00:00:00 2001 From: Topvennie Date: Wed, 27 Nov 2024 22:11:05 +0100 Subject: [PATCH] feat(song): genre --- .env.example | 4 +- README.md | 11 +- config/development.toml | 2 - .../20241127162048_add_song_history_table.sql | 2 +- .../20241127165609_add_song_genre.sql | 45 ++++ db/queries/song.sql | 61 ++++- db/queries/song_history.sql | 16 -- internal/pkg/db/dto/song.go | 67 ++++- internal/pkg/db/sqlc/models.go | 26 +- internal/pkg/db/sqlc/song.sql.go | 239 ++++++++++++++++-- internal/pkg/db/sqlc/song_history.sql.go | 42 --- internal/pkg/song/account.go | 13 +- internal/pkg/song/api.go | 50 +++- internal/pkg/song/song.go | 80 +++++- 14 files changed, 536 insertions(+), 122 deletions(-) create mode 100644 db/migrations/20241127165609_add_song_genre.sql delete mode 100644 db/queries/song_history.sql delete mode 100644 internal/pkg/db/sqlc/song_history.sql.go diff --git a/.env.example b/.env.example index 8b9e46e..580ea18 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -app.env = development +APP_ENV = development +SONG_SPOTIFY_CLIENT_ID = your_client_id +SONG_SPOTIFY_CLIENT_SECRET = your_client_secret diff --git a/README.md b/README.md index 99c2169..2dacb28 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,12 @@ Displays the cammie chat along with some other statistics. ### Configuration -1. Create a `.env` file specifying the environment. Available options are: - - `development` - - `production` +1. Create a `.env` file specifying + - `APP_ENV`. Available options are: + - `development` + - `production` + - `SONG_SPOTIFY_CLIENT_ID` + - `SONG_SPOTIFY_CLIENT_SECRET` 2. Configure the appropriate settings in the corresponding configuration file located in the [config directory](./config) ## DB @@ -26,7 +29,7 @@ SQLC is used to generate statically typed queries and goose is responsible for t ### Usefull commands -- `make migrate`: Run database migrations to update your database schema. +- `make migrate`: Run database migrations to update your database schema (watch out, migrations might result in minor data loss). - `make create-migration`: Create a new migration in the [db/migrations](./db/migrations/) directory. - `make sqlc`: Generate statically typed queries based on the .sql files in the [db/queries](./db/queries/) directory. Add new queries to this directory as needed. diff --git a/config/development.toml b/config/development.toml index 00a97df..055ad23 100644 --- a/config/development.toml +++ b/config/development.toml @@ -3,8 +3,6 @@ host = "localhost" port = 3000 [song] -spotify_client_id = "your_client_id" -spotify_client_secret = "your_client_secret" spotify_api = "https://api.spotify.com/v1" spotify_account = "https://accounts.spotify.com/api/token" diff --git a/db/migrations/20241127162048_add_song_history_table.sql b/db/migrations/20241127162048_add_song_history_table.sql index bd71340..03cc0a6 100644 --- a/db/migrations/20241127162048_add_song_history_table.sql +++ b/db/migrations/20241127162048_add_song_history_table.sql @@ -5,7 +5,7 @@ DROP COLUMN created_at; ALTER TABLE spotify RENAME TO song; -CREATE TABLE song_history ( +CREATE TABLE IF NOT EXISTS song_history ( id INTEGER PRIMARY KEY, song_id INTEGER NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/db/migrations/20241127165609_add_song_genre.sql b/db/migrations/20241127165609_add_song_genre.sql new file mode 100644 index 0000000..75b1b12 --- /dev/null +++ b/db/migrations/20241127165609_add_song_genre.sql @@ -0,0 +1,45 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE song +DROP COLUMN artists; + +CREATE TABLE IF NOT EXISTS song_genre ( + id INTEGER PRIMARY KEY, + genre TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS song_artist ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + spotify_id TEXT NOT NULL, + followers INTEGER NOT NULL, + popularity INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS song_artist_song ( + id INTEGER PRIMARY KEY, + artist_id INTEGER NOT NULL, + song_id INTEGER NOT NULL, + FOREIGN KEY(artist_id) REFERENCES artist(id), + FOREIGN KEY(song_id) REFERENCES song(id) +); + +CREATE TABLE IF NOT EXISTS song_artist_genre ( + id INTEGER PRIMARY KEY, + artist_id INTEGER NOT NULL, + genre_id INTEGER NOT NULL, + FOREIGN KEY(artist_id) REFERENCES artist(id), + FOREIGN KEY(genre_id) REFERENCES genre(id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS song_artist_genre; +DROP TABLE IF EXISTS song_artist_song; +DROP TABLE IF EXISTS song_artist; +DROP TABLE IF EXISTS song_genre; + +ALTER TABLE song +ADD COLUMN artists TEXT NOT NULL DEFAULT 'Unknown'; +-- +goose StatementEnd diff --git a/db/queries/song.sql b/db/queries/song.sql index 42829ac..595d7fd 100644 --- a/db/queries/song.sql +++ b/db/queries/song.sql @@ -10,13 +10,38 @@ FROM song WHERE id = ?; -- name: CreateSong :one -INSERT INTO song (title, artists, spotify_id, duration_ms) +INSERT INTO song (title, spotify_id, duration_ms) +VALUES (?, ?, ?) +RETURNING *; + +-- name: CreateSongHistory :one +INSERT INTO song_history (song_id) +VALUES (?) +RETURNING *; + +-- name: CreateSongGenre :one +INSERT INTO song_genre (genre) +VALUES (?) +RETURNING *; + +-- name: CreateSongArtist :one +INSERT INTO song_artist (name, spotify_id, followers, popularity) VALUES (?, ?, ?, ?) RETURNING *; +-- name: CreateSongArtistSong :one +INSERT INTO song_artist_song (artist_id, song_id) +VALUES (?, ?) +RETURNING *; + +-- name: CreateSongArtistGenre :one +INSERT INTO song_artist_genre (artist_id, genre_id) +VALUES (?, ?) +RETURNING *; + -- name: UpdateSong :one UPDATE song -SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? +SET title = ?, spotify_id = ?, duration_ms = ? WHERE id = ? RETURNING *; @@ -31,3 +56,35 @@ WHERE id = ?; SELECT * FROM song WHERE spotify_id = ?; + +-- name: GetSongArtistBySpotifyID :one +SELECT * +FROM song_artist +WHERE spotify_id = ?; + +-- name: GetLastSongHistory :one +SELECT * +FROM song_history +ORDER BY created_at DESC +LIMIT 1; + +-- name: GetSongGenreByName :one +SELECT * +FROM song_genre +WHERE genre = ?; + +-- name: GetSongArtistByName :one +SELECT * +FROM song_artist +WHERE name = ?; + +-- name: GetLastSongFull :many +SELECT s.title AS song_title, s.spotify_id, s.duration_ms, a.name AS artist_name, g.genre AS genre +FROM song_history sh +JOIN song s ON sh.song_id = s.id +LEFT JOIN song_artist_song sa ON s.id = sa.song_id +LEFT JOIN song_artist a ON sa.artist_id = a.id +LEFT JOIN song_artist_genre ag ON ag.artist_id = a.id +LEFT JOIN song_genre g ON ag.genre_id = g.id +WHERE sh.created_at = (SELECT MAX(created_at) FROM song_history) +ORDER BY a.name, g.genre; diff --git a/db/queries/song_history.sql b/db/queries/song_history.sql deleted file mode 100644 index cc9fcf8..0000000 --- a/db/queries/song_history.sql +++ /dev/null @@ -1,16 +0,0 @@ --- CRUD - --- name: CreateSongHistory :one -INSERT INTO song_history (song_id) -VALUES (?) -RETURNING *; - - --- Other - - --- name: GetLastSongHistory :one -SELECT * -FROM song_history -ORDER BY created_at DESC -LIMIT 1; diff --git a/internal/pkg/db/dto/song.go b/internal/pkg/db/dto/song.go index 268b8d4..4476d3c 100644 --- a/internal/pkg/db/dto/song.go +++ b/internal/pkg/db/dto/song.go @@ -4,13 +4,29 @@ import ( "github.com/zeusWPI/scc/internal/pkg/db/sqlc" ) -// Song is the DTO for the song +// Song is the DTO for a song type Song struct { - ID int64 `json:"id"` - Title string `json:"title"` - Artists string `json:"artists"` - SpotifyID string `json:"spotify_id" validate:"required"` - DurationMS int64 `json:"duration_ms"` + ID int64 `json:"id"` + Title string `json:"title"` + SpotifyID string `json:"spotify_id" validate:"required"` + DurationMS int64 `json:"duration_ms"` + Artists []SongArtist `json:"artists"` +} + +// SongArtist is the DTO for a song artist +type SongArtist struct { + ID int64 `json:"id"` + Name string `json:"name"` + SpotifyID string `json:"spotify_id"` + Followers int64 `json:"followers"` + Popularity int64 `json:"popularity"` + Genres []SongGenre `json:"genres"` +} + +// SongGenre is the DTO for a song genre +type SongGenre struct { + ID int64 `json:"id"` + Genre string `json:"genre"` } // SongDTO converts a sqlc.Song to a Song @@ -18,18 +34,47 @@ func SongDTO(song sqlc.Song) *Song { return &Song{ ID: song.ID, Title: song.Title, - Artists: song.Artists, SpotifyID: song.SpotifyID, DurationMS: song.DurationMs, } } -// CreateParams converts a Song to sqlc.CreateSongParams -func (s *Song) CreateParams() sqlc.CreateSongParams { - return sqlc.CreateSongParams{ +// CreateSongParams converts a Song DTO to a sqlc CreateSongParams object +func (s *Song) CreateSongParams() *sqlc.CreateSongParams { + return &sqlc.CreateSongParams{ Title: s.Title, - Artists: s.Artists, SpotifyID: s.SpotifyID, DurationMs: s.DurationMS, } } + +// CreateSongGenreParams converts a Song DTO to a string to create a new genre +func (s *Song) CreateSongGenreParams(idxArtist, idxGenre int) string { + return s.Artists[idxArtist].Genres[idxGenre].Genre +} + +// CreateSongArtistParams converts a Song DTO to a sqlc CreateSongArtistParams object +func (s *Song) CreateSongArtistParams(idxArtist int) *sqlc.CreateSongArtistParams { + return &sqlc.CreateSongArtistParams{ + Name: s.Artists[idxArtist].Name, + SpotifyID: s.Artists[idxArtist].SpotifyID, + Followers: s.Artists[idxArtist].Followers, + Popularity: s.Artists[idxArtist].Popularity, + } +} + +// CreateSongArtistSongParams converts a Song DTO to a sqlc CreateSongArtistSongParams object +func (s *Song) CreateSongArtistSongParams(idxArtist int) *sqlc.CreateSongArtistSongParams { + return &sqlc.CreateSongArtistSongParams{ + ArtistID: s.Artists[idxArtist].ID, + SongID: s.ID, + } +} + +// CreateSongArtistGenreParamas converts a Song DTO to a sqlc CreateSongArtistGenreParams object +func (s *Song) CreateSongArtistGenreParamas(idxArtist, idxGenre int) *sqlc.CreateSongArtistGenreParams { + return &sqlc.CreateSongArtistGenreParams{ + ArtistID: s.Artists[idxArtist].ID, + GenreID: s.Artists[idxArtist].Genres[idxGenre].ID, + } +} diff --git a/internal/pkg/db/sqlc/models.go b/internal/pkg/db/sqlc/models.go index 84b77f7..5bff769 100644 --- a/internal/pkg/db/sqlc/models.go +++ b/internal/pkg/db/sqlc/models.go @@ -47,11 +47,35 @@ type Season struct { type Song struct { ID int64 Title string - Artists string SpotifyID string DurationMs int64 } +type SongArtist struct { + ID int64 + Name string + SpotifyID string + Followers int64 + Popularity int64 +} + +type SongArtistGenre struct { + ID int64 + ArtistID int64 + GenreID int64 +} + +type SongArtistSong struct { + ID int64 + ArtistID int64 + SongID int64 +} + +type SongGenre struct { + ID int64 + Genre string +} + type SongHistory struct { ID int64 SongID int64 diff --git a/internal/pkg/db/sqlc/song.sql.go b/internal/pkg/db/sqlc/song.sql.go index e72a626..8ba42fe 100644 --- a/internal/pkg/db/sqlc/song.sql.go +++ b/internal/pkg/db/sqlc/song.sql.go @@ -7,39 +7,126 @@ package sqlc import ( "context" + "database/sql" ) const createSong = `-- name: CreateSong :one -INSERT INTO song (title, artists, spotify_id, duration_ms) -VALUES (?, ?, ?, ?) -RETURNING id, title, artists, spotify_id, duration_ms +INSERT INTO song (title, spotify_id, duration_ms) +VALUES (?, ?, ?) +RETURNING id, title, spotify_id, duration_ms ` type CreateSongParams struct { Title string - Artists string SpotifyID string DurationMs int64 } func (q *Queries) CreateSong(ctx context.Context, arg CreateSongParams) (Song, error) { - row := q.db.QueryRowContext(ctx, createSong, - arg.Title, - arg.Artists, - arg.SpotifyID, - arg.DurationMs, - ) + row := q.db.QueryRowContext(ctx, createSong, arg.Title, arg.SpotifyID, arg.DurationMs) var i Song err := row.Scan( &i.ID, &i.Title, - &i.Artists, &i.SpotifyID, &i.DurationMs, ) return i, err } +const createSongArtist = `-- name: CreateSongArtist :one +INSERT INTO song_artist (name, spotify_id, followers, popularity) +VALUES (?, ?, ?, ?) +RETURNING id, name, spotify_id, followers, popularity +` + +type CreateSongArtistParams struct { + Name string + SpotifyID string + Followers int64 + Popularity int64 +} + +func (q *Queries) CreateSongArtist(ctx context.Context, arg CreateSongArtistParams) (SongArtist, error) { + row := q.db.QueryRowContext(ctx, createSongArtist, + arg.Name, + arg.SpotifyID, + arg.Followers, + arg.Popularity, + ) + var i SongArtist + err := row.Scan( + &i.ID, + &i.Name, + &i.SpotifyID, + &i.Followers, + &i.Popularity, + ) + return i, err +} + +const createSongArtistGenre = `-- name: CreateSongArtistGenre :one +INSERT INTO song_artist_genre (artist_id, genre_id) +VALUES (?, ?) +RETURNING id, artist_id, genre_id +` + +type CreateSongArtistGenreParams struct { + ArtistID int64 + GenreID int64 +} + +func (q *Queries) CreateSongArtistGenre(ctx context.Context, arg CreateSongArtistGenreParams) (SongArtistGenre, error) { + row := q.db.QueryRowContext(ctx, createSongArtistGenre, arg.ArtistID, arg.GenreID) + var i SongArtistGenre + err := row.Scan(&i.ID, &i.ArtistID, &i.GenreID) + return i, err +} + +const createSongArtistSong = `-- name: CreateSongArtistSong :one +INSERT INTO song_artist_song (artist_id, song_id) +VALUES (?, ?) +RETURNING id, artist_id, song_id +` + +type CreateSongArtistSongParams struct { + ArtistID int64 + SongID int64 +} + +func (q *Queries) CreateSongArtistSong(ctx context.Context, arg CreateSongArtistSongParams) (SongArtistSong, error) { + row := q.db.QueryRowContext(ctx, createSongArtistSong, arg.ArtistID, arg.SongID) + var i SongArtistSong + err := row.Scan(&i.ID, &i.ArtistID, &i.SongID) + return i, err +} + +const createSongGenre = `-- name: CreateSongGenre :one +INSERT INTO song_genre (genre) +VALUES (?) +RETURNING id, genre +` + +func (q *Queries) CreateSongGenre(ctx context.Context, genre string) (SongGenre, error) { + row := q.db.QueryRowContext(ctx, createSongGenre, genre) + var i SongGenre + err := row.Scan(&i.ID, &i.Genre) + return i, err +} + +const createSongHistory = `-- name: CreateSongHistory :one +INSERT INTO song_history (song_id) +VALUES (?) +RETURNING id, song_id, created_at +` + +func (q *Queries) CreateSongHistory(ctx context.Context, songID int64) (SongHistory, error) { + row := q.db.QueryRowContext(ctx, createSongHistory, songID) + var i SongHistory + err := row.Scan(&i.ID, &i.SongID, &i.CreatedAt) + return i, err +} + const deleteSong = `-- name: DeleteSong :execrows DELETE FROM song WHERE id = ? @@ -55,7 +142,7 @@ func (q *Queries) DeleteSong(ctx context.Context, id int64) (int64, error) { const getAllSongs = `-- name: GetAllSongs :many -SELECT id, title, artists, spotify_id, duration_ms +SELECT id, title, spotify_id, duration_ms FROM song ` @@ -72,7 +159,6 @@ func (q *Queries) GetAllSongs(ctx context.Context) ([]Song, error) { if err := rows.Scan( &i.ID, &i.Title, - &i.Artists, &i.SpotifyID, &i.DurationMs, ); err != nil { @@ -89,8 +175,109 @@ func (q *Queries) GetAllSongs(ctx context.Context) ([]Song, error) { return items, nil } +const getLastSongFull = `-- name: GetLastSongFull :many +SELECT s.title AS song_title, s.spotify_id, s.duration_ms, a.name AS artist_name, g.genre AS genre +FROM song_history sh +JOIN song s ON sh.song_id = s.id +LEFT JOIN song_artist_song sa ON s.id = sa.song_id +LEFT JOIN song_artist a ON sa.artist_id = a.id +LEFT JOIN song_artist_genre ag ON ag.artist_id = a.id +LEFT JOIN song_genre g ON ag.genre_id = g.id +WHERE sh.created_at = (SELECT MAX(created_at) FROM song_history) +ORDER BY a.name, g.genre +` + +type GetLastSongFullRow struct { + SongTitle string + SpotifyID string + DurationMs int64 + ArtistName sql.NullString + Genre sql.NullString +} + +func (q *Queries) GetLastSongFull(ctx context.Context) ([]GetLastSongFullRow, error) { + rows, err := q.db.QueryContext(ctx, getLastSongFull) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLastSongFullRow + for rows.Next() { + var i GetLastSongFullRow + if err := rows.Scan( + &i.SongTitle, + &i.SpotifyID, + &i.DurationMs, + &i.ArtistName, + &i.Genre, + ); 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 +} + +const getLastSongHistory = `-- name: GetLastSongHistory :one +SELECT id, song_id, created_at +FROM song_history +ORDER BY created_at DESC +LIMIT 1 +` + +func (q *Queries) GetLastSongHistory(ctx context.Context) (SongHistory, error) { + row := q.db.QueryRowContext(ctx, getLastSongHistory) + var i SongHistory + err := row.Scan(&i.ID, &i.SongID, &i.CreatedAt) + return i, err +} + +const getSongArtistByName = `-- name: GetSongArtistByName :one +SELECT id, name, spotify_id, followers, popularity +FROM song_artist +WHERE name = ? +` + +func (q *Queries) GetSongArtistByName(ctx context.Context, name string) (SongArtist, error) { + row := q.db.QueryRowContext(ctx, getSongArtistByName, name) + var i SongArtist + err := row.Scan( + &i.ID, + &i.Name, + &i.SpotifyID, + &i.Followers, + &i.Popularity, + ) + return i, err +} + +const getSongArtistBySpotifyID = `-- name: GetSongArtistBySpotifyID :one +SELECT id, name, spotify_id, followers, popularity +FROM song_artist +WHERE spotify_id = ? +` + +func (q *Queries) GetSongArtistBySpotifyID(ctx context.Context, spotifyID string) (SongArtist, error) { + row := q.db.QueryRowContext(ctx, getSongArtistBySpotifyID, spotifyID) + var i SongArtist + err := row.Scan( + &i.ID, + &i.Name, + &i.SpotifyID, + &i.Followers, + &i.Popularity, + ) + return i, err +} + const getSongByID = `-- name: GetSongByID :one -SELECT id, title, artists, spotify_id, duration_ms +SELECT id, title, spotify_id, duration_ms FROM song WHERE id = ? ` @@ -101,7 +288,6 @@ func (q *Queries) GetSongByID(ctx context.Context, id int64) (Song, error) { err := row.Scan( &i.ID, &i.Title, - &i.Artists, &i.SpotifyID, &i.DurationMs, ) @@ -110,7 +296,7 @@ func (q *Queries) GetSongByID(ctx context.Context, id int64) (Song, error) { const getSongBySpotifyID = `-- name: GetSongBySpotifyID :one -SELECT id, title, artists, spotify_id, duration_ms +SELECT id, title, spotify_id, duration_ms FROM song WHERE spotify_id = ? ` @@ -122,23 +308,34 @@ func (q *Queries) GetSongBySpotifyID(ctx context.Context, spotifyID string) (Son err := row.Scan( &i.ID, &i.Title, - &i.Artists, &i.SpotifyID, &i.DurationMs, ) return i, err } +const getSongGenreByName = `-- name: GetSongGenreByName :one +SELECT id, genre +FROM song_genre +WHERE genre = ? +` + +func (q *Queries) GetSongGenreByName(ctx context.Context, genre string) (SongGenre, error) { + row := q.db.QueryRowContext(ctx, getSongGenreByName, genre) + var i SongGenre + err := row.Scan(&i.ID, &i.Genre) + return i, err +} + const updateSong = `-- name: UpdateSong :one UPDATE song -SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? +SET title = ?, spotify_id = ?, duration_ms = ? WHERE id = ? -RETURNING id, title, artists, spotify_id, duration_ms +RETURNING id, title, spotify_id, duration_ms ` type UpdateSongParams struct { Title string - Artists string SpotifyID string DurationMs int64 ID int64 @@ -147,7 +344,6 @@ type UpdateSongParams struct { func (q *Queries) UpdateSong(ctx context.Context, arg UpdateSongParams) (Song, error) { row := q.db.QueryRowContext(ctx, updateSong, arg.Title, - arg.Artists, arg.SpotifyID, arg.DurationMs, arg.ID, @@ -156,7 +352,6 @@ func (q *Queries) UpdateSong(ctx context.Context, arg UpdateSongParams) (Song, e err := row.Scan( &i.ID, &i.Title, - &i.Artists, &i.SpotifyID, &i.DurationMs, ) diff --git a/internal/pkg/db/sqlc/song_history.sql.go b/internal/pkg/db/sqlc/song_history.sql.go deleted file mode 100644 index d1e8f95..0000000 --- a/internal/pkg/db/sqlc/song_history.sql.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: song_history.sql - -package sqlc - -import ( - "context" -) - -const createSongHistory = `-- name: CreateSongHistory :one - -INSERT INTO song_history (song_id) -VALUES (?) -RETURNING id, song_id, created_at -` - -// CRUD -func (q *Queries) CreateSongHistory(ctx context.Context, songID int64) (SongHistory, error) { - row := q.db.QueryRowContext(ctx, createSongHistory, songID) - var i SongHistory - err := row.Scan(&i.ID, &i.SongID, &i.CreatedAt) - return i, err -} - -const getLastSongHistory = `-- name: GetLastSongHistory :one - - -SELECT id, song_id, created_at -FROM song_history -ORDER BY created_at DESC -LIMIT 1 -` - -// Other -func (q *Queries) GetLastSongHistory(ctx context.Context) (SongHistory, error) { - row := q.db.QueryRowContext(ctx, getLastSongHistory) - var i SongHistory - err := row.Scan(&i.ID, &i.SongID, &i.CreatedAt) - return i, err -} diff --git a/internal/pkg/song/account.go b/internal/pkg/song/account.go index 7a445b0..be29ffb 100644 --- a/internal/pkg/song/account.go +++ b/internal/pkg/song/account.go @@ -1,7 +1,6 @@ package song import ( - "encoding/json" "errors" "time" @@ -19,17 +18,11 @@ type accountResponse struct { func (s *Song) refreshToken() error { zap.S().Info("Song: Refreshing spotify access token") - body, err := json.Marshal(fiber.Map{ - "grant_type": "client_credentials", - "client_id": s.ClientID, - "client_secret": s.ClientSecret, - }) - if err != nil { - return err - } + form := &fiber.Args{} + form.Add("grant_type", "client_credentials") api := config.GetDefaultString("song.spotify_account", "https://accounts.spotify.com/api/token") - req := fiber.Post(api).Body(body).ContentType("application/json") + req := fiber.Post(api).Form(form).BasicAuth(s.ClientID, s.ClientSecret) res := new(accountResponse) status, _, errs := req.Struct(res) diff --git a/internal/pkg/song/api.go b/internal/pkg/song/api.go index 9ae37bc..b58d31c 100644 --- a/internal/pkg/song/api.go +++ b/internal/pkg/song/api.go @@ -7,11 +7,13 @@ import ( "github.com/gofiber/fiber/v2" "github.com/zeusWPI/scc/internal/pkg/db/dto" "github.com/zeusWPI/scc/pkg/config" - "github.com/zeusWPI/scc/pkg/util" "go.uber.org/zap" ) +var api = config.GetDefaultString("song.spotify_api", "https://api.spotify.com/v1") + type trackArtist struct { + ID string `json:"id"` Name string `json:"name"` } @@ -24,7 +26,6 @@ type trackResponse struct { func (s *Song) getTrack(track *dto.Song) error { zap.S().Info("Song: Getting track info for id: ", track.SpotifyID) - api := config.GetDefaultString("song.spotify_api", "https://api.spotify.com/v1") req := fiber.Get(fmt.Sprintf("%s/%s/%s", api, "tracks", track.SpotifyID)). Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken)) @@ -34,12 +35,53 @@ func (s *Song) getTrack(track *dto.Song) error { return errors.Join(append([]error{errors.New("Song: Track request failed")}, errs...)...) } if status != fiber.StatusOK { - return errors.New("Song: Error getting track") + return fmt.Errorf("Song: Track request wrong status code %d", status) } track.Title = res.Name - track.Artists = util.SliceStringJoin(res.Artists, ", ", func(a trackArtist) string { return a.Name }) track.DurationMS = res.DurationMS + for _, a := range res.Artists { + track.Artists = append(track.Artists, dto.SongArtist{ + Name: a.Name, + SpotifyID: a.ID, + }) + } + + return nil +} + +type artistFollowers struct { + Total int `json:"total"` +} + +type artistResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Genres []string `json:"genres"` + Popularity int `json:"popularity"` + Followers artistFollowers `json:"followers"` +} + +func (s *Song) getArtist(artist *dto.SongArtist) error { + req := fiber.Get(fmt.Sprintf("%s/%s/%s", api, "artists", artist.SpotifyID)). + Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken)) + + res := new(artistResponse) + status, _, errs := req.Struct(res) + if len(errs) > 0 { + return errors.Join(append([]error{errors.New("Song: Artist request failed")}, errs...)...) + } + if status != fiber.StatusOK { + return fmt.Errorf("Song: Artist request wrong status code %d", status) + } + + artist.Popularity = int64(res.Popularity) + artist.Followers = int64(res.Followers.Total) + + for _, genre := range res.Genres { + artist.Genres = append(artist.Genres, dto.SongGenre{Genre: genre}) + } + return nil } diff --git a/internal/pkg/song/song.go b/internal/pkg/song/song.go index 3d4cd5f..34eec0d 100644 --- a/internal/pkg/song/song.go +++ b/internal/pkg/song/song.go @@ -73,21 +73,89 @@ func (s *Song) Track(track *dto.Song) error { } } - // Set track info - err = s.getTrack(track) - if err != nil { + // Get track info + if err = s.getTrack(track); err != nil { return err } // Store track in DB - trackDB, err = s.db.Queries.CreateSong(context.Background(), track.CreateParams()) + trackDB, err = s.db.Queries.CreateSong(context.Background(), *track.CreateSongParams()) if err != nil { return err } + track.ID = trackDB.ID + + // Handle artists + var errs []error + for i, artist := range track.Artists { + a, err := s.db.Queries.GetSongArtistBySpotifyID(context.Background(), artist.SpotifyID) + if err != nil && err != sql.ErrNoRows { + errs = append(errs, err) + continue + } + + if (a != sqlc.SongArtist{}) { + // Artist already exists + // Add it as an artist for this track + if _, err := s.db.Queries.CreateSongArtistSong(context.Background(), *track.CreateSongArtistSongParams(i)); err != nil { + errs = append(errs, err) + } + continue + } + + // Get artist data + if err := s.getArtist(&track.Artists[i]); err != nil { + errs = append(errs, err) + continue + } + + // Insert artist in DB + a, err = s.db.Queries.CreateSongArtist(context.Background(), *track.CreateSongArtistParams(i)) + if err != nil { + errs = append(errs, err) + continue + } + track.Artists[i].ID = a.ID + + // Add artist as an artist for this song + if _, err := s.db.Queries.CreateSongArtistSong(context.Background(), *track.CreateSongArtistSongParams(i)); err != nil { + errs = append(errs, err) + continue + } + + // Check if the artists genres are in db + for j, genre := range track.Artists[i].Genres { + g, err := s.db.Queries.GetSongGenreByName(context.Background(), genre.Genre) + if err != nil && err != sql.ErrNoRows { + errs = append(errs, err) + continue + } + + if (g != sqlc.SongGenre{}) { + // Genre already exists + continue + } + + // Insert genre in DB + g, err = s.db.Queries.CreateSongGenre(context.Background(), track.CreateSongGenreParams(i, j)) + if err != nil { + errs = append(errs, err) + continue + } + track.Artists[i].Genres[j].ID = g.ID + + // Add genre as a genre for this artist + if _, err := s.db.Queries.CreateSongArtistGenre(context.Background(), *track.CreateSongArtistGenreParamas(i, j)); err != nil { + errs = append(errs, err) + } + } + } // Add to song history - _, err = s.db.Queries.CreateSongHistory(context.Background(), trackDB.ID) + if _, err = s.db.Queries.CreateSongHistory(context.Background(), trackDB.ID); err != nil { + errs = append(errs, err) + } - return err + return errors.Join(errs...) }