diff --git a/config/development.toml b/config/development.toml index 6a0b36e..00a97df 100644 --- a/config/development.toml +++ b/config/development.toml @@ -2,9 +2,11 @@ host = "localhost" port = 3000 -[spotify] -client_id = "your_client_id" -client_secret = "your_client_secret" +[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" [tap] api = "https://tap.zeus.gent" diff --git a/db/migrations/20241127162048_add_song_history_table.sql b/db/migrations/20241127162048_add_song_history_table.sql new file mode 100644 index 0000000..bd71340 --- /dev/null +++ b/db/migrations/20241127162048_add_song_history_table.sql @@ -0,0 +1,24 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE spotify +DROP COLUMN created_at; + +ALTER TABLE spotify RENAME TO song; + +CREATE TABLE song_history ( + id INTEGER PRIMARY KEY, + song_id INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(song_id) REFERENCES song(id) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS song_history; + +ALTER TABLE song RENAME TO spotify; + +ALTER TABLE spotify +ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +-- +goose StatementEnd diff --git a/db/queries/song.sql b/db/queries/song.sql new file mode 100644 index 0000000..42829ac --- /dev/null +++ b/db/queries/song.sql @@ -0,0 +1,33 @@ +-- CRUD + +-- name: GetAllSongs :many +SELECT * +FROM song; + +-- name: GetSongByID :one +SELECT * +FROM song +WHERE id = ?; + +-- name: CreateSong :one +INSERT INTO song (title, artists, spotify_id, duration_ms) +VALUES (?, ?, ?, ?) +RETURNING *; + +-- name: UpdateSong :one +UPDATE song +SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? +WHERE id = ? +RETURNING *; + +-- name: DeleteSong :execrows +DELETE FROM song +WHERE id = ?; + + +-- Other + +-- name: GetSongBySpotifyID :one +SELECT * +FROM song +WHERE spotify_id = ?; diff --git a/db/queries/song_history.sql b/db/queries/song_history.sql new file mode 100644 index 0000000..cc9fcf8 --- /dev/null +++ b/db/queries/song_history.sql @@ -0,0 +1,16 @@ +-- 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/db/queries/spotify.sql b/db/queries/spotify.sql deleted file mode 100644 index 2074db9..0000000 --- a/db/queries/spotify.sql +++ /dev/null @@ -1,33 +0,0 @@ --- CRUD - --- name: GetAllSpotify :many -SELECT * -FROM spotify; - --- name: GetSpotifyByID :one -SELECT * -FROM spotify -WHERE id = ?; - --- name: CreateSpotify :one -INSERT INTO spotify (title, artists, spotify_id, duration_ms) -VALUES (?, ?, ?, ?) -RETURNING *; - --- name: UpdateSpotify :one -UPDATE spotify -SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? -WHERE id = ? -RETURNING *; - --- name: DeleteSpotify :execrows -DELETE FROM spotify -WHERE id = ?; - - --- Other - --- name: GetSpotifyBySpotifyID :one -SELECT * -FROM spotify -WHERE spotify_id = ?; diff --git a/internal/api/api.go b/internal/api/api.go index e78b6cd..2097678 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -4,13 +4,13 @@ package api import ( "github.com/gofiber/fiber/v2" "github.com/zeusWPI/scc/internal/api/message" - apiSpotify "github.com/zeusWPI/scc/internal/api/spotify" + apiSong "github.com/zeusWPI/scc/internal/api/song" "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/internal/pkg/spotify" + "github.com/zeusWPI/scc/internal/pkg/song" ) // New creates a new API instance -func New(router fiber.Router, db *db.DB, spotify *spotify.Spotify) { +func New(router fiber.Router, db *db.DB, song *song.Song) { message.New(router, db) - apiSpotify.New(router, db, spotify) + apiSong.New(router, db, song) } diff --git a/internal/api/song/song.go b/internal/api/song/song.go new file mode 100644 index 0000000..220663f --- /dev/null +++ b/internal/api/song/song.go @@ -0,0 +1,56 @@ +// Package song provides the API regarding songs integration +package song + +import ( + "github.com/gofiber/fiber/v2" + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/db/dto" + "github.com/zeusWPI/scc/internal/pkg/song" + "go.uber.org/zap" +) + +// Router is the song API router +type Router struct { + router fiber.Router + db *db.DB + song *song.Song +} + +// New creates a new song API instance +func New(router fiber.Router, db *db.DB, song *song.Song) *Router { + api := &Router{ + router: router.Group("/song"), + db: db, + song: song, + } + api.createRoutes() + + return api +} + +func (r *Router) createRoutes() { + r.router.Post("/", r.new) +} + +func (r *Router) new(c *fiber.Ctx) error { + song := new(dto.Song) + + if err := c.BodyParser(song); err != nil { + zap.S().Error("API: Song body parser\n", err) + return c.SendStatus(fiber.StatusBadRequest) + } + + if err := dto.Validate.Struct(song); err != nil { + zap.S().Error("API: Song validation\n", err) + return c.SendStatus(fiber.StatusBadRequest) + } + + go func() { + err := r.song.Track(song) + if err != nil { + zap.S().Error("Song: Get Track\n", err) + } + }() + + return c.SendStatus(fiber.StatusOK) +} diff --git a/internal/api/spotify/spotify.go b/internal/api/spotify/spotify.go deleted file mode 100644 index c9a0bd4..0000000 --- a/internal/api/spotify/spotify.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package spotify provides the API regarding spotify integration -package spotify - -import ( - "github.com/gofiber/fiber/v2" - "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/internal/pkg/db/dto" - "github.com/zeusWPI/scc/internal/pkg/spotify" - "go.uber.org/zap" -) - -// Router is the spotify API router -type Router struct { - router fiber.Router - db *db.DB - spotify *spotify.Spotify -} - -// New creates a new spotify API instance -func New(router fiber.Router, db *db.DB, spotify *spotify.Spotify) *Router { - api := &Router{ - router: router.Group("/spotify"), - db: db, - spotify: spotify, - } - api.createRoutes() - - return api -} - -func (r *Router) createRoutes() { - r.router.Post("/", r.new) -} - -func (r *Router) new(c *fiber.Ctx) error { - spotify := new(dto.Spotify) - - if err := c.BodyParser(spotify); err != nil { - zap.S().Error("API: Spotify body parser\n", err) - return c.SendStatus(fiber.StatusBadRequest) - } - - if err := dto.Validate.Struct(spotify); err != nil { - zap.S().Error("API: Spotify validation\n", err) - return c.SendStatus(fiber.StatusBadRequest) - } - - go func() { - err := r.spotify.Track(spotify) - if err != nil { - zap.S().Error("Spotify: Get Track\n", err) - } - }() - - return c.SendStatus(fiber.StatusOK) -} diff --git a/internal/cmd/api.go b/internal/cmd/api.go index 9a9e508..c021fd7 100644 --- a/internal/cmd/api.go +++ b/internal/cmd/api.go @@ -8,13 +8,13 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" "github.com/zeusWPI/scc/internal/api" "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/internal/pkg/spotify" + "github.com/zeusWPI/scc/internal/pkg/song" "github.com/zeusWPI/scc/pkg/config" "go.uber.org/zap" ) // API starts the API server -func API(db *db.DB, spotify *spotify.Spotify) { +func API(db *db.DB, song *song.Song) { app := fiber.New(fiber.Config{ BodyLimit: 1024 * 1024 * 1024, }) @@ -29,7 +29,7 @@ func API(db *db.DB, spotify *spotify.Spotify) { ) apiGroup := app.Group("/api") - api.New(apiGroup, db, spotify) + api.New(apiGroup, db, song) host := config.GetDefaultString("server.host", "127.0.0.1") port := config.GetDefaultInt("server.port", 3000) diff --git a/internal/cmd/song.go b/internal/cmd/song.go new file mode 100644 index 0000000..198d84c --- /dev/null +++ b/internal/cmd/song.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/song" +) + +// Song starts the Song integration +func Song(db *db.DB) (*song.Song, error) { + song, err := song.New(db) + + return song, err +} diff --git a/internal/cmd/spotify.go b/internal/cmd/spotify.go deleted file mode 100644 index 64e4072..0000000 --- a/internal/cmd/spotify.go +++ /dev/null @@ -1,13 +0,0 @@ -package cmd - -import ( - "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/internal/pkg/spotify" -) - -// Spotify starts the Spotify integration -func Spotify(db *db.DB) (*spotify.Spotify, error) { - spotify, err := spotify.New(db) - - return spotify, err -} diff --git a/internal/pkg/db/dto/song.go b/internal/pkg/db/dto/song.go new file mode 100644 index 0000000..268b8d4 --- /dev/null +++ b/internal/pkg/db/dto/song.go @@ -0,0 +1,35 @@ +package dto + +import ( + "github.com/zeusWPI/scc/internal/pkg/db/sqlc" +) + +// Song is the DTO for the 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"` +} + +// SongDTO converts a sqlc.Song to a Song +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{ + Title: s.Title, + Artists: s.Artists, + SpotifyID: s.SpotifyID, + DurationMs: s.DurationMS, + } +} diff --git a/internal/pkg/db/dto/spotify.go b/internal/pkg/db/dto/spotify.go deleted file mode 100644 index be4cbb5..0000000 --- a/internal/pkg/db/dto/spotify.go +++ /dev/null @@ -1,39 +0,0 @@ -package dto - -import ( - "time" - - "github.com/zeusWPI/scc/internal/pkg/db/sqlc" -) - -// Spotify is the DTO for the spotify -type Spotify 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"` - CreatedAt time.Time `json:"created_at"` -} - -// SpotifyDTO converts a sqlc.Spotify to a Spotify -func SpotifyDTO(spotify sqlc.Spotify) *Spotify { - return &Spotify{ - ID: spotify.ID, - Title: spotify.Title, - Artists: spotify.Artists, - SpotifyID: spotify.SpotifyID, - DurationMS: spotify.DurationMs, - CreatedAt: spotify.CreatedAt, - } -} - -// CreateParams converts a Spotify to sqlc.CreateSpotifyParams -func (s *Spotify) CreateParams() sqlc.CreateSpotifyParams { - return sqlc.CreateSpotifyParams{ - Title: s.Title, - Artists: s.Artists, - SpotifyID: s.SpotifyID, - DurationMs: s.DurationMS, - } -} diff --git a/internal/pkg/db/sqlc/models.go b/internal/pkg/db/sqlc/models.go index ed03606..84b77f7 100644 --- a/internal/pkg/db/sqlc/models.go +++ b/internal/pkg/db/sqlc/models.go @@ -44,13 +44,18 @@ type Season struct { Current bool } -type Spotify struct { +type Song struct { ID int64 Title string Artists string SpotifyID string DurationMs int64 - CreatedAt time.Time +} + +type SongHistory struct { + ID int64 + SongID int64 + CreatedAt time.Time } type Tap struct { diff --git a/internal/pkg/db/sqlc/song.sql.go b/internal/pkg/db/sqlc/song.sql.go new file mode 100644 index 0000000..e72a626 --- /dev/null +++ b/internal/pkg/db/sqlc/song.sql.go @@ -0,0 +1,164 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: song.sql + +package sqlc + +import ( + "context" +) + +const createSong = `-- name: CreateSong :one +INSERT INTO song (title, artists, spotify_id, duration_ms) +VALUES (?, ?, ?, ?) +RETURNING id, title, artists, 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, + ) + var i Song + err := row.Scan( + &i.ID, + &i.Title, + &i.Artists, + &i.SpotifyID, + &i.DurationMs, + ) + return i, err +} + +const deleteSong = `-- name: DeleteSong :execrows +DELETE FROM song +WHERE id = ? +` + +func (q *Queries) DeleteSong(ctx context.Context, id int64) (int64, error) { + result, err := q.db.ExecContext(ctx, deleteSong, id) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const getAllSongs = `-- name: GetAllSongs :many + +SELECT id, title, artists, spotify_id, duration_ms +FROM song +` + +// CRUD +func (q *Queries) GetAllSongs(ctx context.Context) ([]Song, error) { + rows, err := q.db.QueryContext(ctx, getAllSongs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Song + for rows.Next() { + var i Song + if err := rows.Scan( + &i.ID, + &i.Title, + &i.Artists, + &i.SpotifyID, + &i.DurationMs, + ); 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 getSongByID = `-- name: GetSongByID :one +SELECT id, title, artists, spotify_id, duration_ms +FROM song +WHERE id = ? +` + +func (q *Queries) GetSongByID(ctx context.Context, id int64) (Song, error) { + row := q.db.QueryRowContext(ctx, getSongByID, id) + var i Song + err := row.Scan( + &i.ID, + &i.Title, + &i.Artists, + &i.SpotifyID, + &i.DurationMs, + ) + return i, err +} + +const getSongBySpotifyID = `-- name: GetSongBySpotifyID :one + +SELECT id, title, artists, spotify_id, duration_ms +FROM song +WHERE spotify_id = ? +` + +// Other +func (q *Queries) GetSongBySpotifyID(ctx context.Context, spotifyID string) (Song, error) { + row := q.db.QueryRowContext(ctx, getSongBySpotifyID, spotifyID) + var i Song + err := row.Scan( + &i.ID, + &i.Title, + &i.Artists, + &i.SpotifyID, + &i.DurationMs, + ) + return i, err +} + +const updateSong = `-- name: UpdateSong :one +UPDATE song +SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? +WHERE id = ? +RETURNING id, title, artists, spotify_id, duration_ms +` + +type UpdateSongParams struct { + Title string + Artists string + SpotifyID string + DurationMs int64 + ID int64 +} + +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, + ) + var i Song + err := row.Scan( + &i.ID, + &i.Title, + &i.Artists, + &i.SpotifyID, + &i.DurationMs, + ) + return i, err +} diff --git a/internal/pkg/db/sqlc/song_history.sql.go b/internal/pkg/db/sqlc/song_history.sql.go new file mode 100644 index 0000000..d1e8f95 --- /dev/null +++ b/internal/pkg/db/sqlc/song_history.sql.go @@ -0,0 +1,42 @@ +// 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/db/sqlc/spotify.sql.go b/internal/pkg/db/sqlc/spotify.sql.go deleted file mode 100644 index c79eeb6..0000000 --- a/internal/pkg/db/sqlc/spotify.sql.go +++ /dev/null @@ -1,169 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: spotify.sql - -package sqlc - -import ( - "context" -) - -const createSpotify = `-- name: CreateSpotify :one -INSERT INTO spotify (title, artists, spotify_id, duration_ms) -VALUES (?, ?, ?, ?) -RETURNING id, title, artists, spotify_id, duration_ms, created_at -` - -type CreateSpotifyParams struct { - Title string - Artists string - SpotifyID string - DurationMs int64 -} - -func (q *Queries) CreateSpotify(ctx context.Context, arg CreateSpotifyParams) (Spotify, error) { - row := q.db.QueryRowContext(ctx, createSpotify, - arg.Title, - arg.Artists, - arg.SpotifyID, - arg.DurationMs, - ) - var i Spotify - err := row.Scan( - &i.ID, - &i.Title, - &i.Artists, - &i.SpotifyID, - &i.DurationMs, - &i.CreatedAt, - ) - return i, err -} - -const deleteSpotify = `-- name: DeleteSpotify :execrows -DELETE FROM spotify -WHERE id = ? -` - -func (q *Queries) DeleteSpotify(ctx context.Context, id int64) (int64, error) { - result, err := q.db.ExecContext(ctx, deleteSpotify, id) - if err != nil { - return 0, err - } - return result.RowsAffected() -} - -const getAllSpotify = `-- name: GetAllSpotify :many - -SELECT id, title, artists, spotify_id, duration_ms, created_at -FROM spotify -` - -// CRUD -func (q *Queries) GetAllSpotify(ctx context.Context) ([]Spotify, error) { - rows, err := q.db.QueryContext(ctx, getAllSpotify) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Spotify - for rows.Next() { - var i Spotify - if err := rows.Scan( - &i.ID, - &i.Title, - &i.Artists, - &i.SpotifyID, - &i.DurationMs, - &i.CreatedAt, - ); 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 getSpotifyByID = `-- name: GetSpotifyByID :one -SELECT id, title, artists, spotify_id, duration_ms, created_at -FROM spotify -WHERE id = ? -` - -func (q *Queries) GetSpotifyByID(ctx context.Context, id int64) (Spotify, error) { - row := q.db.QueryRowContext(ctx, getSpotifyByID, id) - var i Spotify - err := row.Scan( - &i.ID, - &i.Title, - &i.Artists, - &i.SpotifyID, - &i.DurationMs, - &i.CreatedAt, - ) - return i, err -} - -const getSpotifyBySpotifyID = `-- name: GetSpotifyBySpotifyID :one - -SELECT id, title, artists, spotify_id, duration_ms, created_at -FROM spotify -WHERE spotify_id = ? -` - -// Other -func (q *Queries) GetSpotifyBySpotifyID(ctx context.Context, spotifyID string) (Spotify, error) { - row := q.db.QueryRowContext(ctx, getSpotifyBySpotifyID, spotifyID) - var i Spotify - err := row.Scan( - &i.ID, - &i.Title, - &i.Artists, - &i.SpotifyID, - &i.DurationMs, - &i.CreatedAt, - ) - return i, err -} - -const updateSpotify = `-- name: UpdateSpotify :one -UPDATE spotify -SET title = ?, artists = ?, spotify_id = ?, duration_ms = ? -WHERE id = ? -RETURNING id, title, artists, spotify_id, duration_ms, created_at -` - -type UpdateSpotifyParams struct { - Title string - Artists string - SpotifyID string - DurationMs int64 - ID int64 -} - -func (q *Queries) UpdateSpotify(ctx context.Context, arg UpdateSpotifyParams) (Spotify, error) { - row := q.db.QueryRowContext(ctx, updateSpotify, - arg.Title, - arg.Artists, - arg.SpotifyID, - arg.DurationMs, - arg.ID, - ) - var i Spotify - err := row.Scan( - &i.ID, - &i.Title, - &i.Artists, - &i.SpotifyID, - &i.DurationMs, - &i.CreatedAt, - ) - return i, err -} diff --git a/internal/pkg/spotify/account.go b/internal/pkg/song/account.go similarity index 58% rename from internal/pkg/spotify/account.go rename to internal/pkg/song/account.go index 7b95575..7a445b0 100644 --- a/internal/pkg/spotify/account.go +++ b/internal/pkg/song/account.go @@ -1,4 +1,4 @@ -package spotify +package song import ( "encoding/json" @@ -6,19 +6,18 @@ import ( "time" "github.com/gofiber/fiber/v2" + "github.com/zeusWPI/scc/pkg/config" "go.uber.org/zap" ) -const accountURL = "https://accounts.spotify.com/api/token" - type accountResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` } -func (s *Spotify) refreshToken() error { - zap.S().Info("Spotify: Refreshing access token") +func (s *Song) refreshToken() error { + zap.S().Info("Song: Refreshing spotify access token") body, err := json.Marshal(fiber.Map{ "grant_type": "client_credentials", @@ -29,15 +28,16 @@ func (s *Spotify) refreshToken() error { return err } - req := fiber.Post(accountURL).Body(body).ContentType("application/json") + api := config.GetDefaultString("song.spotify_account", "https://accounts.spotify.com/api/token") + req := fiber.Post(api).Body(body).ContentType("application/json") res := new(accountResponse) status, _, errs := req.Struct(res) if len(errs) > 0 { - return errors.Join(append([]error{errors.New("Spotify: Token refresh request failed")}, errs...)...) + return errors.Join(append([]error{errors.New("Song: Spotify token refresh request failed")}, errs...)...) } if status != fiber.StatusOK { - return errors.New("error getting access token") + return errors.New("Song: Error getting access token") } s.AccessToken = res.AccessToken diff --git a/internal/pkg/spotify/api.go b/internal/pkg/song/api.go similarity index 61% rename from internal/pkg/spotify/api.go rename to internal/pkg/song/api.go index d57a969..9ae37bc 100644 --- a/internal/pkg/spotify/api.go +++ b/internal/pkg/song/api.go @@ -1,4 +1,4 @@ -package spotify +package song import ( "errors" @@ -6,12 +6,11 @@ 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" ) -const apiURL = "https://api.spotify.com/v1" - type trackArtist struct { Name string `json:"name"` } @@ -22,19 +21,20 @@ type trackResponse struct { DurationMS int64 `json:"duration_ms"` } -func (s *Spotify) setTrack(track *dto.Spotify) error { - zap.S().Info("Spotify: Getting track info for id: ", track.SpotifyID) +func (s *Song) getTrack(track *dto.Song) error { + zap.S().Info("Song: Getting track info for id: ", track.SpotifyID) - req := fiber.Get(fmt.Sprintf("%s/%s/%s", apiURL, "tracks", 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)) res := new(trackResponse) status, _, errs := req.Struct(res) if len(errs) > 0 { - return errors.Join(append([]error{errors.New("Spotify: Track request failed")}, errs...)...) + return errors.Join(append([]error{errors.New("Song: Track request failed")}, errs...)...) } if status != fiber.StatusOK { - return errors.New("error getting track") + return errors.New("Song: Error getting track") } track.Title = res.Name diff --git a/internal/pkg/song/song.go b/internal/pkg/song/song.go new file mode 100644 index 0000000..3d4cd5f --- /dev/null +++ b/internal/pkg/song/song.go @@ -0,0 +1,93 @@ +// Package song provides all song related logic +package song + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/db/dto" + "github.com/zeusWPI/scc/internal/pkg/db/sqlc" + "github.com/zeusWPI/scc/pkg/config" +) + +// Song represents a song instance +type Song struct { + db *db.DB + ClientID string + ClientSecret string + AccessToken string + ExpiresTime int64 +} + +// New creates a new song instance +func New(db *db.DB) (*Song, error) { + clientID := config.GetDefaultString("song.spotify_client_id", "") + clientSecret := config.GetDefaultString("song.spotify_client_secret", "") + + if clientID == "" || clientSecret == "" { + return &Song{}, errors.New("Song: Spotify client id or secret not set") + } + + return &Song{db: db, ClientID: clientID, ClientSecret: clientSecret, ExpiresTime: 0}, nil +} + +// Track gets information about the current track and stores it in the database +func (s *Song) Track(track *dto.Song) error { + if s.ClientID == "" || s.ClientSecret == "" { + return errors.New("Song: Spotify client id or secret not set") + } + + // Check if song is already in DB + trackDB, err := s.db.Queries.GetSongBySpotifyID(context.Background(), track.SpotifyID) + if err != nil && err != sql.ErrNoRows { + return err + } + + if (trackDB != sqlc.Song{}) { + // Already in DB + // Add to song history if it's not the latest song + songHistory, err := s.db.Queries.GetLastSongHistory(context.Background()) + if err != nil && err != sql.ErrNoRows { + return err + } + + if (songHistory != sqlc.SongHistory{}) && songHistory.SongID == trackDB.ID { + // Song is already the latest, don't add it again + return nil + } + + _, err = s.db.Queries.CreateSongHistory(context.Background(), trackDB.ID) + return err + } + + // Not in database yet, add it + + // Refresh token if needed + if s.ExpiresTime <= time.Now().Unix() { + err := s.refreshToken() + if err != nil { + return err + } + } + + // Set track info + err = s.getTrack(track) + if err != nil { + return err + } + + // Store track in DB + trackDB, err = s.db.Queries.CreateSong(context.Background(), track.CreateParams()) + if err != nil { + return err + } + + // Add to song history + _, err = s.db.Queries.CreateSongHistory(context.Background(), trackDB.ID) + + return err + +} diff --git a/internal/pkg/spotify/spotify.go b/internal/pkg/spotify/spotify.go deleted file mode 100644 index 4cc4b33..0000000 --- a/internal/pkg/spotify/spotify.go +++ /dev/null @@ -1,84 +0,0 @@ -// Package spotify provides all spotify related logic -package spotify - -import ( - "context" - "database/sql" - "errors" - "time" - - "github.com/zeusWPI/scc/internal/pkg/db" - "github.com/zeusWPI/scc/internal/pkg/db/dto" - "github.com/zeusWPI/scc/internal/pkg/db/sqlc" - "github.com/zeusWPI/scc/pkg/config" -) - -// Spotify represents a spotify instance -type Spotify struct { - db *db.DB - ClientID string - ClientSecret string - AccessToken string - ExpiresTime int64 -} - -// New creates a new spotify instance -func New(db *db.DB) (*Spotify, error) { - clientID := config.GetDefaultString("spotify.client_id", "") - clientSecret := config.GetDefaultString("spotify.client_secret", "") - - if clientID == "" || clientSecret == "" { - return &Spotify{}, errors.New("Spotify client id or secret not set") - } - - return &Spotify{db: db, ClientID: clientID, ClientSecret: clientSecret, ExpiresTime: 0}, nil -} - -// Track gets information about the current track and stores it in the database -func (s *Spotify) Track(track *dto.Spotify) error { - if s.ClientID == "" || s.ClientSecret == "" { - return errors.New("spotify client id or secret not set") - } - - // Check if song is already in DB - trackDB, err := s.db.Queries.GetSpotifyBySpotifyID(context.Background(), track.SpotifyID) - if err != nil { - if err != sql.ErrNoRows { - return err - } - } - - if (trackDB != sqlc.Spotify{}) { - // Already in DB - // No need to refetch data - track.Title = trackDB.Title - track.Artists = trackDB.Artists - track.DurationMS = trackDB.DurationMs - _, err := s.db.Queries.CreateSpotify(context.Background(), track.CreateParams()) - - return err - } - - // Refresh token if needed - if s.ExpiresTime <= time.Now().Unix() { - err := s.refreshToken() - if err != nil { - return err - } - } - - // Set track info - err = s.setTrack(track) - if err != nil { - return err - } - - // Store track in DB - _, err = s.db.Queries.CreateSpotify(context.Background(), track.CreateParams()) - if err != nil { - return err - } - - return nil - -}