package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// checkLikeAlbumUseCase is a struct that contains the use case of checking for an album.
type checkLikeAlbumUseCase struct {
albumRepo albumDomain.AlbumRepository
}
// NewCheckLikeAlbumUseCase returns a new instance of the checkLikeAlbumUseCase struct.
func NewCheckLikeAlbumUseCase(albumRepo albumDomain.AlbumRepository) *checkLikeAlbumUseCase {
return &checkLikeAlbumUseCase{
albumRepo: albumRepo,
}
}
// Run returns the check result of the album.
func (uc *checkLikeAlbumUseCase) Run(ctx context.Context, id string) (bool, error) {
return uc.albumRepo.IsLiked(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
)
// checkLikeArtistUseCase is a struct that contains the use case of checking for an artist.
type checkLikeArtistUseCase struct {
artistRepo artistDomain.ArtistRepository
}
// NewCheckLikeArtistUseCase returns a new instance of the checkLikeArtistUseCase struct.
func NewCheckLikeArtistUseCase(artistRepo artistDomain.ArtistRepository) *checkLikeArtistUseCase {
return &checkLikeArtistUseCase{
artistRepo: artistRepo,
}
}
// Run returns the check result of the artist.
func (uc *checkLikeArtistUseCase) Run(ctx context.Context, id string) (bool, error) {
return uc.artistRepo.IsLiked(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// checkLikeTrackUseCase is a struct that contains the use case of checking for an track.
type checkLikeTrackUseCase struct {
trackRepo trackDomain.TrackRepository
}
// NewCheckLikeTrackUseCase returns a new instance of the checkLikeTrackUseCase struct.
func NewCheckLikeTrackUseCase(trackRepo trackDomain.TrackRepository) *checkLikeTrackUseCase {
return &checkLikeTrackUseCase{
trackRepo: trackRepo,
}
}
// Run returns the check result of the track.
func (uc *checkLikeTrackUseCase) Run(ctx context.Context, id string) (bool, error) {
return uc.trackRepo.IsLiked(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"strings"
"time"
"github.com/zmb3/spotify/v2"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// getAlbumUseCase is a struct that contains the use case of getting for an album.
type getAlbumUseCase struct {
albumRepo albumDomain.AlbumRepository
}
// NewGetAlbumUseCase returns a new instance of the GetAlbumUseCase struct.
func NewGetAlbumUseCase(albumRepo albumDomain.AlbumRepository) *getAlbumUseCase {
return &getAlbumUseCase{
albumRepo: albumRepo,
}
}
// GetAlbumUseCaseOutputDto is a DTO struct that contains the output data of the getAlbumUseCase.
type GetAlbumUseCaseOutputDto struct {
ID string
Name string
Artists string
ReleaseDate time.Time
}
// Run returns the get result of the album.
func (uc *getAlbumUseCase) Run(ctx context.Context, id string) (*GetAlbumUseCaseOutputDto, error) {
album, err := uc.albumRepo.FindById(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
artistNames := make([]string, len(album.Artists))
for i, artist := range album.Artists {
artistNames[i] = artist.Name
}
return &GetAlbumUseCaseOutputDto{
ID: album.ID.String(),
Name: album.Name,
Artists: strings.Join(artistNames, ", "),
ReleaseDate: album.ReleaseDate,
}, nil
}
package spotlike
import (
"context"
"sort"
"strings"
"time"
"github.com/zmb3/spotify/v2"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// getAllAlbumsByArtistIdUseCase is a struct that contains the use case of getting for an artist.
type getAllAlbumsByArtistIdUseCase struct {
albumRepo albumDomain.AlbumRepository
}
// NewGetAllAlbumsByArtistIdUseCase returns a new instance of the GetAllAlbumsByArtistIdUseCase struct.
func NewGetAllAlbumsByArtistIdUseCase(albumRepo albumDomain.AlbumRepository) *getAllAlbumsByArtistIdUseCase {
return &getAllAlbumsByArtistIdUseCase{
albumRepo: albumRepo,
}
}
// GetAllAlbumsByArtistIdUseCaseOutputDto is a DTO struct that contains the output data of the getAllAlbumsUseCase.
type GetAllAlbumsByArtistIdUseCaseOutputDto struct {
ID string
Artists string
Name string
ReleaseDate time.Time
}
// Run returns the get result of the albums.
func (uc *getAllAlbumsByArtistIdUseCase) Run(ctx context.Context, id string) ([]*GetAllAlbumsByArtistIdUseCaseOutputDto, error) {
albums, err := uc.albumRepo.FindByArtistId(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
sort.Slice(albums, func(i, j int) bool {
return albums[i].ReleaseDate.Before(albums[j].ReleaseDate)
})
var getAllAlbumsByArtistIdUseCaseOutputDtos []*GetAllAlbumsByArtistIdUseCaseOutputDto
for _, album := range albums {
artistNames := make([]string, len(album.Artists))
for i, artist := range album.Artists {
artistNames[i] = artist.Name
}
getAllAlbumsUseCaseOutputDto := &GetAllAlbumsByArtistIdUseCaseOutputDto{
ID: album.ID.String(),
Artists: strings.Join(artistNames, ", "),
Name: album.Name,
ReleaseDate: album.ReleaseDate,
}
getAllAlbumsByArtistIdUseCaseOutputDtos = append(getAllAlbumsByArtistIdUseCaseOutputDtos, getAllAlbumsUseCaseOutputDto)
}
return getAllAlbumsByArtistIdUseCaseOutputDtos, nil
}
package spotlike
import (
"context"
"strings"
"time"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// getAllTracksByAlbumIdUseCase is a struct that contains the use case of getting for an track.
type getAllTracksByAlbumIdUseCase struct {
trackRepo trackDomain.TrackRepository
}
// NewGetAllTracksByAlbumIdUseCase returns a new instance of the GetAllTracksByAlbumIdUseCase struct.
func NewGetAllTracksByAlbumIdUseCase(trackRepo trackDomain.TrackRepository) *getAllTracksByAlbumIdUseCase {
return &getAllTracksByAlbumIdUseCase{
trackRepo: trackRepo,
}
}
// GetAllTracksByAlbumIdUseCaseOutputDto is a DTO struct that contains the output data of the getAllTracksUseCase.
type GetAllTracksByAlbumIdUseCaseOutputDto struct {
ID string
Artists string
Album string
Name string
TrackNumber spotify.Numeric
ReleaseDate time.Time
}
// Run returns the get result of the tracks.
func (uc *getAllTracksByAlbumIdUseCase) Run(ctx context.Context, id string) ([]*GetAllTracksByAlbumIdUseCaseOutputDto, error) {
tracks, err := uc.trackRepo.FindByAlbumId(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
var getAllTracksByAlbumIdUseCaseOutputDtos []*GetAllTracksByAlbumIdUseCaseOutputDto
for _, track := range tracks {
artistNames := make([]string, len(track.Artists))
for i, artist := range track.Artists {
artistNames[i] = artist.Name
}
getAllTracksByArtistIdUseCaseOutputDto := &GetAllTracksByAlbumIdUseCaseOutputDto{
ID: track.ID.String(),
Artists: strings.Join(artistNames, ", "),
Album: track.Album.Name,
Name: track.Name,
TrackNumber: track.TrackNumber,
ReleaseDate: track.ReleaseDate,
}
getAllTracksByAlbumIdUseCaseOutputDtos = append(getAllTracksByAlbumIdUseCaseOutputDtos, getAllTracksByArtistIdUseCaseOutputDto)
}
return getAllTracksByAlbumIdUseCaseOutputDtos, nil
}
package spotlike
import (
"context"
"sort"
"strings"
"time"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// getAllTracksByArtistIdUseCase is a struct that contains the use case of getting for an artist.
type getAllTracksByArtistIdUseCase struct {
trackRepo trackDomain.TrackRepository
}
// NewGetAllTracksByArtistIdUseCase returns a new instance of the GetAllTracksByArtistIdUseCase struct.
func NewGetAllTracksByArtistIdUseCase(tracksRepo trackDomain.TrackRepository) *getAllTracksByArtistIdUseCase {
return &getAllTracksByArtistIdUseCase{
trackRepo: tracksRepo,
}
}
// GetAllTracksByArtistIdUseCaseOutputDto is a DTO struct that contains the output data of the getAllTracksUseCase.
type GetAllTracksByArtistIdUseCaseOutputDto struct {
ID string
Artists string
Album string
Name string
TrackNumber spotify.Numeric
ReleaseDate time.Time
}
// Run returns the get result of the tracks.
func (uc *getAllTracksByArtistIdUseCase) Run(ctx context.Context, id string) ([]*GetAllTracksByArtistIdUseCaseOutputDto, error) {
tracks, err := uc.trackRepo.FindByArtistId(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
// group tracks by album
albumMap := make(map[string][]*trackDomain.Track)
for _, track := range tracks {
albumID := track.Album.ID.String()
albumMap[albumID] = append(albumMap[albumID], track)
}
// sort albums by release date
var albumIDs []string
for albumID := range albumMap {
albumIDs = append(albumIDs, albumID)
}
sort.Slice(albumIDs, func(i, j int) bool {
return albumMap[albumIDs[i]][0].ReleaseDate.Before(albumMap[albumIDs[j]][0].ReleaseDate)
})
var getAllTracksByArtistIdUseCaseOutputDtos []*GetAllTracksByArtistIdUseCaseOutputDto
for _, albumID := range albumIDs {
albumTracks := albumMap[albumID]
// sort tracks by track number
sort.Slice(albumTracks, func(i, j int) bool {
return albumTracks[i].TrackNumber < albumTracks[j].TrackNumber
})
for _, track := range albumTracks {
artistNames := make([]string, len(track.Artists))
for i, artist := range track.Artists {
artistNames[i] = artist.Name
}
getAllTracksByArtistIdUseCaseOutputDto := &GetAllTracksByArtistIdUseCaseOutputDto{
ID: track.ID.String(),
Artists: strings.Join(artistNames, ", "),
Album: track.Album.Name,
Name: track.Name,
TrackNumber: track.TrackNumber,
ReleaseDate: track.ReleaseDate,
}
getAllTracksByArtistIdUseCaseOutputDtos = append(getAllTracksByArtistIdUseCaseOutputDtos, getAllTracksByArtistIdUseCaseOutputDto)
}
}
return getAllTracksByArtistIdUseCaseOutputDtos, nil
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
)
// GetArtistUseCase is an interface that defines the use case of getting for an artist.
type GetArtistUseCase interface {
Run(ctx context.Context, id string) (*GetArtistUseCaseOutputDto, error)
}
// GetArtistUseCaseStruct is a struct that implements the GetArtistUseCase interface.
type GetArtistUseCaseStruct struct {
artistRepo artistDomain.ArtistRepository
}
var (
// NewGetArtistUseCase is a function that returns a new instance of the GetArtistUseCaseStruct struct.
NewGetArtistUseCase = newGetArtistUseCase
)
// NewGetArtistUseCase returns a new instance of the GetArtistUseCase struct.
func newGetArtistUseCase(artistRepo artistDomain.ArtistRepository) *GetArtistUseCaseStruct {
return &GetArtistUseCaseStruct{
artistRepo: artistRepo,
}
}
// GetArtistUseCaseOutputDto is a DTO struct that contains the output data of the getArtistUseCase.
type GetArtistUseCaseOutputDto struct {
ID string
Name string
}
// Run returns the get result of the artist.
func (uc *GetArtistUseCaseStruct) Run(ctx context.Context, id string) (*GetArtistUseCaseOutputDto, error) {
artist, err := uc.artistRepo.FindById(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
return &GetArtistUseCaseOutputDto{
ID: artist.ID.String(),
Name: artist.Name,
}, nil
}
package spotlike
import (
"context"
"strings"
"time"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// getTrackUseCase is a struct that contains the use case of getting for an track.
type getTrackUseCase struct {
trackRepo trackDomain.TrackRepository
}
// NewGetTrackUseCase returns a new instance of the GetTrackUseCase struct.
func NewGetTrackUseCase(trackRepo trackDomain.TrackRepository) *getTrackUseCase {
return &getTrackUseCase{
trackRepo: trackRepo,
}
}
// GetTrackUseCaseOutputDto is a DTO struct that contains the output data of the getTrackUseCase.
type GetTrackUseCaseOutputDto struct {
ID string
Name string
Artists string
Album string
TrackNumber spotify.Numeric
ReleaseDate time.Time
}
// Run returns the get result of the track.
func (uc *getTrackUseCase) Run(ctx context.Context, id string) (*GetTrackUseCaseOutputDto, error) {
track, err := uc.trackRepo.FindById(ctx, spotify.ID(id))
if err != nil {
return nil, err
}
artistNames := make([]string, len(track.Artists))
for i, artist := range track.Artists {
artistNames[i] = artist.Name
}
return &GetTrackUseCaseOutputDto{
ID: track.ID.String(),
Name: track.Name,
Artists: strings.Join(artistNames, ", "),
Album: track.Album.Name,
TrackNumber: track.TrackNumber,
ReleaseDate: track.ReleaseDate,
}, nil
}
package spotlike
import ()
// getVersionUseCase is a struct that contains the use case of getting the version.
type getVersionUseCase struct{}
// NewGetVersionUseCase returns a new instance of the GetVersionUseCase struct.
func NewGetVersionUseCase() *getVersionUseCase {
return &getVersionUseCase{}
}
// GetVersionUseCaseOutputDto is a DTO struct that contains the output data of the GetVersionUseCase.
type GetVersionUseCaseOutputDto struct {
Version string
}
// Run returns the output of the GetVersionUseCase.
func (uc *getVersionUseCase) Run(version string) *GetVersionUseCaseOutputDto {
return &GetVersionUseCaseOutputDto{
Version: version,
}
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// likeAlbumUseCase is a struct that contains the use case of likeing for an album.
type likeAlbumUseCase struct {
albumDomain albumDomain.AlbumRepository
}
// NewLikeAlbumUseCase returns a new instance of the LikeAlbumUseCase struct.
func NewLikeAlbumUseCase(albumDomain albumDomain.AlbumRepository) *likeAlbumUseCase {
return &likeAlbumUseCase{
albumDomain: albumDomain,
}
}
// Run returns the like result of the album.
func (uc *likeAlbumUseCase) Run(ctx context.Context, id string) error {
return uc.albumDomain.Like(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
)
// likeArtistUseCase is a struct that contains the use case of likeing for an artist.
type likeArtistUseCase struct {
artistRepo artistDomain.ArtistRepository
}
// NewLikeArtistUseCase returns a new instance of the LikeArtistUseCase struct.
func NewLikeArtistUseCase(artistRepo artistDomain.ArtistRepository) *likeArtistUseCase {
return &likeArtistUseCase{
artistRepo: artistRepo,
}
}
// Run returns the like result of the artist.
func (uc *likeArtistUseCase) Run(ctx context.Context, id string) error {
return uc.artistRepo.Like(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// likeTrackUseCase is a struct that contains the use case of likeing for an track.
type likeTrackUseCase struct {
trackDomain trackDomain.TrackRepository
}
// NewLikeTrackUseCase returns a new instance of the LikeTrackUseCase struct.
func NewLikeTrackUseCase(trackDomain trackDomain.TrackRepository) *likeTrackUseCase {
return &likeTrackUseCase{
trackDomain: trackDomain,
}
}
// Run returns the like result of the track.
func (uc *likeTrackUseCase) Run(ctx context.Context, id string) error {
return uc.trackDomain.Like(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"strings"
"time"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// SearchAlbumUseCase is a struct that contains the use case of searching for an album.
type SearchAlbumUseCase interface {
Run(ctx context.Context, keywords []string, max int) ([]*SearchAlbumUseCaseOutputDto, error)
}
// SearchAlbumUseCaseStruct is a struct that contains the use case of searching for an album.
type SearchAlbumUseCaseStruct struct {
albumRepo albumDomain.AlbumRepository
}
var (
// NewSearchAlbumUseCase is a function that returns a new instance of the SearchAlbumUseCase struct.
NewSearchAlbumUseCase = newSearchAlbumUseCase
)
// newSearchAlbumUseCase returns a new instance of the SearchAlbumUseCase struct.
func newSearchAlbumUseCase(albumRepo albumDomain.AlbumRepository) *SearchAlbumUseCaseStruct {
return &SearchAlbumUseCaseStruct{
albumRepo: albumRepo,
}
}
// SearchAlbumUseCaseOutputDto is a DTO struct that contains the output data of the SearchAlbumUseCase.
type SearchAlbumUseCaseOutputDto struct {
ID string
Artists string
Name string
ReleaseDate time.Time
}
// Run returns the search result of the album.
func (uc *SearchAlbumUseCaseStruct) Run(ctx context.Context, keywords []string, max int) ([]*SearchAlbumUseCaseOutputDto, error) {
query := strings.Join(keywords, " ")
albums, err := uc.albumRepo.FindByNameLimit(ctx, query, max)
if err != nil {
return nil, err
}
var searchAlbumResultDtos []*SearchAlbumUseCaseOutputDto
for _, album := range albums {
artistNames := make([]string, len(album.Artists))
for i, artist := range album.Artists {
artistNames[i] = artist.Name
}
searchAlbumResultDtos = append(searchAlbumResultDtos, &SearchAlbumUseCaseOutputDto{
ID: album.ID.String(),
Artists: strings.Join(artistNames, ", "),
Name: album.Name,
ReleaseDate: album.ReleaseDate,
})
}
return searchAlbumResultDtos, nil
}
package spotlike
import (
"context"
"strings"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
)
// SearchArtistUseCase is a struct that contains the use case of searching for an artist.
type SearchArtistUseCase interface {
Run(ctx context.Context, keywords []string, max int) ([]*SearchArtistUseCaseOutputDto, error)
}
// SearchArtistUseCaseStruct is a struct that contains the use case of searching for an artist.
type SearchArtistUseCaseStruct struct {
artistRepo artistDomain.ArtistRepository
}
var (
// NewSearchArtistUseCase is a function that returns a new instance of the SearchArtistUseCaseStruct struct.
NewSearchArtistUseCase = newSearchArtistUseCase
)
// newSearchArtistUseCase returns a new instance of the SearchArtistUseCase struct.
func newSearchArtistUseCase(artistRepo artistDomain.ArtistRepository) *SearchArtistUseCaseStruct {
return &SearchArtistUseCaseStruct{
artistRepo: artistRepo,
}
}
// SearchArtistUseCaseOutputDto is a DTO struct that contains the output data of the SearchArtistUseCase.
type SearchArtistUseCaseOutputDto struct {
ID string
Name string
}
// Run returns the search result of the artist.
func (uc *SearchArtistUseCaseStruct) Run(ctx context.Context, keywords []string, max int) ([]*SearchArtistUseCaseOutputDto, error) {
query := strings.Join(keywords, " ")
artists, err := uc.artistRepo.FindByNameLimit(ctx, query, max)
if err != nil {
return nil, err
}
var searchArtistResultDtos []*SearchArtistUseCaseOutputDto
for _, artist := range artists {
searchArtistResultDtos = append(searchArtistResultDtos, &SearchArtistUseCaseOutputDto{
ID: artist.ID.String(),
Name: artist.Name,
})
}
return searchArtistResultDtos, nil
}
package spotlike
import (
"context"
"strings"
"time"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// searchTrackUseCase is a struct that contains the use case of searching for an track.
type SearchTrackUseCase interface {
Run(ctx context.Context, keywords []string, max int) ([]*SearchTrackUseCaseOutputDto, error)
}
// SearchTrackUseCaseStruct is a struct that contains the use case of searching for an track.
type SearchTrackUseCaseStruct struct {
trackRepo trackDomain.TrackRepository
}
var (
// NewSearchTrackUseCase is a function that returns a new instance of the SearchTrackUseCase struct.
NewSearchTrackUseCase = newSearchTrackUseCase
)
// NewSearchTrackUseCaseStruct returns a new instance of the SearchTrackUseCase struct.
func newSearchTrackUseCase(trackRepo trackDomain.TrackRepository) *SearchTrackUseCaseStruct {
return &SearchTrackUseCaseStruct{
trackRepo: trackRepo,
}
}
// SearchTrackUseCaseOutputDto is a DTO struct that contains the output data of the SearchTrackUseCase.
type SearchTrackUseCaseOutputDto struct {
ID string
Artists string
Album string
Name string
TrackNumber spotify.Numeric
ReleaseDate time.Time
}
// Run returns the search result of the track.
func (uc *SearchTrackUseCaseStruct) Run(ctx context.Context, keywords []string, max int) ([]*SearchTrackUseCaseOutputDto, error) {
query := strings.Join(keywords, " ")
tracks, err := uc.trackRepo.FindByNameLimit(ctx, query, max)
if err != nil {
return nil, err
}
var searchTrackResultDtos []*SearchTrackUseCaseOutputDto
for _, track := range tracks {
artistNames := make([]string, len(track.Artists))
for i, artist := range track.Artists {
artistNames[i] = artist.Name
}
searchTrackResultDtos = append(searchTrackResultDtos, &SearchTrackUseCaseOutputDto{
ID: track.ID.String(),
Artists: strings.Join(artistNames, ", "),
Album: track.Album.Name,
Name: track.Name,
TrackNumber: track.TrackNumber,
ReleaseDate: track.Album.ReleaseDateTime(),
})
}
return searchTrackResultDtos, nil
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
)
// unlikeAlbumUseCase is a struct that contains the use case of unlikeing for an album.
type unlikeAlbumUseCase struct {
albumDomain albumDomain.AlbumRepository
}
// NewUnlikeAlbumUseCase returns a new instance of the unLikeAlbumUseCase struct.
func NewUnlikeAlbumUseCase(albumDomain albumDomain.AlbumRepository) *unlikeAlbumUseCase {
return &unlikeAlbumUseCase{
albumDomain: albumDomain,
}
}
// Run returns the unlike result of the album.
func (uc *unlikeAlbumUseCase) Run(ctx context.Context, id string) error {
return uc.albumDomain.Unlike(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
)
// unlikeArtistUseCase is a struct that contains the use case of unlikeing for an artist.
type unlikeArtistUseCase struct {
artistRepo artistDomain.ArtistRepository
}
// NewUnlikeArtistUseCase returns a new instance of the LikeArtistUseCase struct.
func NewUnlikeArtistUseCase(artistRepo artistDomain.ArtistRepository) *unlikeArtistUseCase {
return &unlikeArtistUseCase{
artistRepo: artistRepo,
}
}
// Run returns the unlike result of the artist.
func (uc *unlikeArtistUseCase) Run(ctx context.Context, id string) error {
return uc.artistRepo.Unlike(ctx, spotify.ID(id))
}
package spotlike
import (
"context"
"github.com/zmb3/spotify/v2"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
)
// unlikeTrackUseCase is a struct that contains the use case of unlikeing for an track.
type unlikeTrackUseCase struct {
trackDomain trackDomain.TrackRepository
}
// NewUnlikeTrackUseCase returns a new instance of the UnlikeTrackUseCase struct.
func NewUnlikeTrackUseCase(trackDomain trackDomain.TrackRepository) *unlikeTrackUseCase {
return &unlikeTrackUseCase{
trackDomain: trackDomain,
}
}
// Run returns the unlike result of the artist.
func (uc *unlikeTrackUseCase) Run(ctx context.Context, id string) error {
return uc.trackDomain.Unlike(ctx, spotify.ID(id))
}
package config
import (
"github.com/yanosea/spotlike/pkg/proxy"
)
// Configurator is an interface that gets the configuration.
type Configurator interface {
GetConfig() (*SpotlikeConfig, error)
}
// BaseConfigurator is a struct that implements the Configurator interface.
type BaseConfigurator struct {
Envconfig proxy.Envconfig
}
// SpotlikeConfig is a struct that contains the configuration of the spotlike application.
type SpotlikeConfig struct {
// SpotifyID is the Spotify client ID.
SpotifyID string
// SpotifySecret is the Spotify client secret.
SpotifySecret string
// SpotifyRedirectUri is the Spotify redirect URI.
SpotifyRedirectUri string
// SpotifyRefreshToken is the Spotify refresh token.
SpotifyRefreshToken string
}
// NewConfigurator creates a new Configurator.
func NewConfigurator(
envconfigProxy proxy.Envconfig,
) *BaseConfigurator {
return &BaseConfigurator{
Envconfig: envconfigProxy,
}
}
package album
import (
"time"
"github.com/zmb3/spotify/v2"
)
// Album is a struct that represents a Spotify album.
type Album struct {
// ID is the Spotify ID of the album.
ID spotify.ID
// Name is the name of the album.
Name string
// Artists is a list of the artists that contributed to the album.
Artists []spotify.SimpleArtist
// ReleaseDate is the release date of the album.
ReleaseDate time.Time
}
// NewAlbum returns a new instance of Album struct.
func NewAlbum(
id spotify.ID,
name string,
artists []spotify.SimpleArtist,
releaseDate time.Time,
) *Album {
return &Album{
ID: id,
Name: name,
Artists: artists,
ReleaseDate: releaseDate,
}
}
package artist
import (
"github.com/zmb3/spotify/v2"
)
// Artist is a struct that represents a Spotify artist.
type Artist struct {
// ID is the Spotify ID of the artist.
ID spotify.ID
// Name is the name of the artist.
Name string
}
// NewArtist returns a new instance of Artist struct.
func NewArtist(
id spotify.ID,
name string,
) *Artist {
return &Artist{
ID: id,
Name: name,
}
}
package track
import (
"time"
"github.com/zmb3/spotify/v2"
)
// Track is a struct that represents a Spotify track.
type Track struct {
// ID is the Spotify ID of the track.
ID spotify.ID
// Name is the name of the track.
Name string
// Artists is a list of the artists that contributed to the track.
Artists []spotify.SimpleArtist
// Album is the album that the track belongs to.
Album spotify.SimpleAlbum
// TrackNumber is the track number of the track in the album.
TrackNumber spotify.Numeric
// ReleaseDate is the release date of the track.
ReleaseDate time.Time
}
// NewTrack returns a new instance of Track struct.
func NewTrack(
id spotify.ID,
name string,
artists []spotify.SimpleArtist,
album spotify.SimpleAlbum,
trackNumber spotify.Numeric,
releaseDate time.Time,
) *Track {
return &Track{
ID: id,
Name: name,
Artists: artists,
Album: album,
TrackNumber: trackNumber,
ReleaseDate: releaseDate,
}
}
package api
import (
"context"
"errors"
"net/http"
"sync"
"time"
"golang.org/x/oauth2"
"github.com/zmb3/spotify/v2/auth"
"github.com/yanosea/spotlike/pkg/proxy"
)
// Client is an interface that contains the client.
type Client interface {
Auth(chan<- string, string) (proxy.Client, string, error)
Close() error
Open() proxy.Client
UpdateConfig(*ClientConfig)
}
// client is a struct that contains the client.
type client struct {
spotify proxy.Spotify
client proxy.Client
http proxy.Http
randstr proxy.Randstr
url proxy.Url
config *ClientConfig
context context.Context
mutex *sync.RWMutex
}
var (
// dc is a channel that is used to notify the server shutdown.
dc chan struct{}
)
// Close closes the client.
func (c *client) Close() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.client != nil {
c.client = nil
}
return nil
}
// Open opens the client.
func (c *client) Open() proxy.Client {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.client != nil {
return c.client
}
authenticator := c.spotify.NewAuthenticator(
spotifyauth.WithScopes(
spotifyauth.ScopeUserFollowRead,
spotifyauth.ScopeUserFollowModify,
spotifyauth.ScopeUserLibraryRead,
spotifyauth.ScopeUserLibraryModify,
),
spotifyauth.WithClientID(c.config.SpotifyID),
spotifyauth.WithClientSecret(c.config.SpotifySecret),
spotifyauth.WithRedirectURL(c.config.SpotifyRedirectUri),
)
tok := &oauth2.Token{
TokenType: "bearer",
RefreshToken: c.config.SpotifyRefreshToken,
}
c.client = c.spotify.NewClient(authenticator.Client(c.context, tok))
return c.client
}
// UpdateConfig updates the client configuration.
func (c *client) UpdateConfig(config *ClientConfig) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.config = config
}
// Auth authenticates the client.
func (c *client) Auth(authUrlChan chan<- string, version string) (proxy.Client, string, error) {
c.mutex.Lock()
if c.client != nil {
c.mutex.Unlock()
return c.client, "", nil
}
c.mutex.Unlock()
ctx, cancel := context.WithTimeout(c.context, 5*time.Minute)
defer cancel()
authenticator := c.spotify.NewAuthenticator(
spotifyauth.WithScopes(
spotifyauth.ScopeUserFollowRead,
spotifyauth.ScopeUserFollowModify,
spotifyauth.ScopeUserLibraryRead,
spotifyauth.ScopeUserLibraryModify,
),
spotifyauth.WithClientID(c.config.SpotifyID),
spotifyauth.WithClientSecret(c.config.SpotifySecret),
spotifyauth.WithRedirectURL(c.config.SpotifyRedirectUri),
)
state := c.randstr.Hex(11)
authUrlChan <- authenticator.AuthURL(state)
uri, err := c.url.Parse(c.config.SpotifyRedirectUri)
if err != nil {
return nil, "", err
}
var port string
if port = uri.Port(); port == "" {
return nil, "", errors.New("failed to get port")
}
server := c.http.NewServer(":" + port)
var (
client proxy.Client
refreshToken string
clientChan = make(chan proxy.Client, 1)
errChan = make(chan error, 1)
doneChan = make(chan struct{}, 1)
)
dc = doneChan
defer func() {
close(clientChan)
close(errChan)
close(doneChan)
}()
c.http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
defer func() {
select {
case doneChan <- struct{}{}:
default:
}
}()
tok, err := authenticator.Token(r.Context(), state, r)
if err != nil {
errChan <- err
http.Error(w, "authentication failed: "+err.Error(), http.StatusForbidden)
return
}
if tok == nil || tok.RefreshToken == "" {
errChan <- errors.New("refresh token is empty")
http.Error(w, "authentication failed: refresh token is empty", http.StatusForbidden)
return
}
client = c.spotify.NewClient(authenticator.Client(r.Context(), tok))
refreshToken = tok.RefreshToken
clientChan <- client
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
successPageHtml := `
<!DOCTYPE html>
<html>
<head>
<title>🎉 spotlike authentication succeeded!</title>
</head>
<body>
<p>🎉 Authentication succeeded! You may close this page and return to spotlike!</p>
<p style="font-style: italic">- <a href="https://github.com/yanosea/spotlike">spotlike</a> v` + version + `</p>
</body>
</html>`
if _, err := w.Write([]byte(successPageHtml)); err != nil {
select {
case errChan <- err:
default:
}
}
})
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errChan <- err
}
}()
cleanup := func() error {
ctx, cancel := context.WithTimeout(c.context, 1*time.Second)
defer cancel()
return server.Shutdown(ctx)
}
select {
case <-ctx.Done():
if err := cleanup(); err != nil {
return nil, "", err
}
return nil, "", errors.New("authentication timed out")
case err := <-errChan:
if cerr := cleanup(); cerr != nil {
return nil, "", err
}
return nil, "", err
case <-doneChan:
if err := cleanup(); err != nil {
return nil, "", err
}
select {
case client = <-clientChan:
if client == nil {
return nil, "", errors.New("failed to get client")
}
default:
return nil, "", errors.New("client not received")
}
}
c.mutex.Lock()
c.client = client
c.mutex.Unlock()
return client, refreshToken, nil
}
package api
import (
"context"
"errors"
"sync"
"github.com/yanosea/spotlike/pkg/proxy"
)
var (
// gcm is a global client manager.
gcm ClientManager
// gmutex is a global mutex.
gmutex = &sync.Mutex{}
// GetClientManagerFunc is a function to get the client manager.
GetClientManagerFunc = getClientManager
)
// ClientManager is an interface that manages api clients.
type ClientManager interface {
CloseClient() error
GetClient() (Client, error)
InitializeClient(ctx context.Context, config *ClientConfig) error
IsClientInitialized() bool
}
// connectionManager is a struct that implements the ConnectionManager interface.
type clientManager struct {
spotify proxy.Spotify
client Client
http proxy.Http
randstr proxy.Randstr
url proxy.Url
mutex *sync.RWMutex
}
// NewClientManager initializes the client manager.
func NewClientManager(spotify proxy.Spotify, http proxy.Http, randstr proxy.Randstr, url proxy.Url) ClientManager {
gmutex.Lock()
defer gmutex.Unlock()
if gcm == nil {
gcm = &clientManager{
spotify: spotify,
client: nil,
http: http,
randstr: randstr,
url: url,
mutex: &sync.RWMutex{},
}
}
return gcm
}
// GetClientManager gets the client manager.
func GetClientManager() ClientManager {
return GetClientManagerFunc()
}
// getClientManager gets the client manager.
func getClientManager() ClientManager {
gmutex.Lock()
defer gmutex.Unlock()
if gcm == nil {
return nil
}
return gcm
}
// ResetClientManager resets the client manager.
func ResetClientManager() error {
gmutex.Lock()
defer gmutex.Unlock()
if gcm == nil {
return nil
}
if err := gcm.CloseClient(); err != nil {
return err
} else {
gcm = nil
}
return nil
}
// IsClientInitialized checks if the client is initialized.
func (cm *clientManager) IsClientInitialized() bool {
cm.mutex.Lock()
defer cm.mutex.Unlock()
return cm.client != nil
}
// CloseClient closes the client.
func (cm *clientManager) CloseClient() error {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.client != nil {
if err := cm.client.Close(); err != nil {
return err
}
cm.client = nil
}
return nil
}
// GetClient gets the api client.
func (cm *clientManager) GetClient() (Client, error) {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
if cm.client == nil {
return nil, errors.New("client not initialized")
}
return cm.client, nil
}
// InitializeClient initializes the api client.
func (cm *clientManager) InitializeClient(ctx context.Context, config *ClientConfig) error {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.client != nil {
return errors.New("client already initialized")
}
cm.client = &client{
spotify: cm.spotify,
client: nil,
http: cm.http,
randstr: cm.randstr,
url: cm.url,
config: config,
context: ctx,
mutex: &sync.RWMutex{},
}
return nil
}
package repository
import (
"context"
albumDomain "github.com/yanosea/spotlike/app/domain/spotify/album"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/zmb3/spotify/v2"
)
// albumRepository is a struct that implements the AlbumRepository interface.
type albumRepository struct {
clientManager api.ClientManager
}
// NewAlbumRepository returns a new instance of the albumRepository struct.
func NewAlbumRepository() albumDomain.AlbumRepository {
return &albumRepository{
clientManager: api.GetClientManager(),
}
}
// FindByArtistId returns the albums by the artist ID.
func (r *albumRepository) FindByArtistId(ctx context.Context, id spotify.ID) ([]*albumDomain.Album, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
result, err := client.GetArtistAlbums(ctx, id, nil)
if err != nil {
return nil, err
}
var albums []*albumDomain.Album
for _, album := range result.Albums {
albums = append(
albums,
albumDomain.NewAlbum(
album.ID,
album.Name,
album.Artists,
album.ReleaseDateTime(),
),
)
}
return albums, nil
}
// FindById returns the album by the ID.
func (r *albumRepository) FindById(ctx context.Context, id spotify.ID) (*albumDomain.Album, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
album, err := client.GetAlbum(ctx, id)
if err != nil {
return nil, err
}
return albumDomain.NewAlbum(
album.ID,
album.Name,
album.Artists,
album.ReleaseDateTime(),
), nil
}
// FindByNameLimit returns the album by the name with the limit.
func (r *albumRepository) FindByNameLimit(ctx context.Context, name string, limit int) ([]*albumDomain.Album, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
result, err := client.Search(ctx, name, spotify.SearchTypeAlbum, spotify.Limit(limit))
if err != nil {
return nil, err
}
var albums []*albumDomain.Album
for _, album := range result.Albums.Albums {
albums = append(
albums,
albumDomain.NewAlbum(
album.ID,
album.Name,
album.Artists,
album.ReleaseDateTime(),
),
)
}
return albums, nil
}
// IsLiked returns whether the album is liked.
func (r *albumRepository) IsLiked(ctx context.Context, id spotify.ID) (bool, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return false, err
}
client := c.Open()
result, err := client.UserHasAlbums(ctx, id)
if err != nil {
return false, err
}
return result[0], nil
}
// Like likes the album.
func (r *albumRepository) Like(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.AddAlbumsToLibrary(ctx, id)
}
// Unlike unlikes the album.
func (r *albumRepository) Unlike(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.RemoveAlbumsFromLibrary(ctx, id)
}
package repository
import (
"context"
artistDomain "github.com/yanosea/spotlike/app/domain/spotify/artist"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/zmb3/spotify/v2"
)
// artistRepository is a struct that implements the ArtistRepository interface.
type artistRepository struct {
clientManager api.ClientManager
}
// NewArtistRepository returns a new instance of the artistRepository struct.
func NewArtistRepository() artistDomain.ArtistRepository {
return &artistRepository{
clientManager: api.GetClientManager(),
}
}
// FindById returns the artist by the ID.
func (r *artistRepository) FindById(ctx context.Context, id spotify.ID) (*artistDomain.Artist, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
artist, err := client.GetArtist(ctx, id)
if err != nil {
return nil, err
}
return artistDomain.NewArtist(
artist.ID,
artist.Name,
), nil
}
// FindByNameLimit returns the artist by the name with the limit.
func (r *artistRepository) FindByNameLimit(ctx context.Context, name string, limit int) ([]*artistDomain.Artist, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
result, err := client.Search(ctx, name, spotify.SearchTypeArtist, spotify.Limit(limit))
if err != nil {
return nil, err
}
var artists []*artistDomain.Artist
for _, artist := range result.Artists.Artists {
artists = append(
artists,
artistDomain.NewArtist(
artist.ID,
artist.Name,
),
)
}
return artists, nil
}
// IsLiked returns whether the artist is liked.
func (r *artistRepository) IsLiked(ctx context.Context, id spotify.ID) (bool, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return false, err
}
client := c.Open()
result, err := client.CurrentUserFollows(ctx, "artist", id)
if err != nil {
return false, err
}
return result[0], nil
}
// Like likes the artist.
func (r *artistRepository) Like(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.FollowArtist(ctx, id)
}
// Unlike unlikes the artist.
func (r *artistRepository) Unlike(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.UnfollowArtist(ctx, id)
}
package repository
import (
"context"
trackDomain "github.com/yanosea/spotlike/app/domain/spotify/track"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/zmb3/spotify/v2"
)
// trackRepository is a struct that implements the TrackRepository interface.
type trackRepository struct {
clientManager api.ClientManager
}
// NewTrackRepository returns a new instance of the trackRepository struct.
func NewTrackRepository() trackDomain.TrackRepository {
return &trackRepository{
clientManager: api.GetClientManager(),
}
}
// FindByArtistId returns the tracks by the artist ID.
func (r *trackRepository) FindByArtistId(ctx context.Context, id spotify.ID) ([]*trackDomain.Track, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
albumsResult, err := client.GetArtistAlbums(ctx, id, nil)
if err != nil {
return nil, err
}
var tracks []*trackDomain.Track
for _, album := range albumsResult.Albums {
tracksResult, err := client.GetAlbumTracks(ctx, album.ID)
if err != nil {
return nil, err
}
for _, track := range tracksResult.Tracks {
tracks = append(
tracks,
trackDomain.NewTrack(
track.ID,
track.Name,
track.Artists,
album,
track.TrackNumber,
album.ReleaseDateTime(),
),
)
}
}
return tracks, nil
}
// FindByAlbumId returns the tracks by the album ID.
func (r *trackRepository) FindByAlbumId(ctx context.Context, id spotify.ID) ([]*trackDomain.Track, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
album, err := client.GetAlbum(ctx, id)
if err != nil {
return nil, err
}
var tracks []*trackDomain.Track
tracksResult, err := client.GetAlbumTracks(ctx, id)
if err != nil {
return nil, err
}
for _, track := range tracksResult.Tracks {
tracks = append(
tracks,
trackDomain.NewTrack(
track.ID,
track.Name,
track.Artists,
album.SimpleAlbum,
track.TrackNumber,
album.ReleaseDateTime(),
),
)
}
return tracks, nil
}
// FindById returns the track by the ID.
func (r *trackRepository) FindById(ctx context.Context, id spotify.ID) (*trackDomain.Track, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
track, err := client.GetTrack(ctx, id)
if err != nil {
return nil, err
}
return trackDomain.NewTrack(
track.ID,
track.Name,
track.Artists,
track.Album,
track.TrackNumber,
track.Album.ReleaseDateTime(),
), nil
}
// FindByNameLimit returns the track by the name with the limit.
func (r *trackRepository) FindByNameLimit(ctx context.Context, name string, limit int) ([]*trackDomain.Track, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return nil, err
}
client := c.Open()
result, err := client.Search(ctx, name, spotify.SearchTypeTrack, spotify.Limit(limit))
if err != nil {
return nil, err
}
var tracks []*trackDomain.Track
for _, track := range result.Tracks.Tracks {
tracks = append(
tracks,
trackDomain.NewTrack(
track.ID,
track.Name,
track.Artists,
track.Album,
track.TrackNumber,
track.Album.ReleaseDateTime(),
),
)
}
return tracks, nil
}
// IsLiked returns whether the track is liked.
func (r *trackRepository) IsLiked(ctx context.Context, id spotify.ID) (bool, error) {
c, err := r.clientManager.GetClient()
if err != nil {
return false, err
}
client := c.Open()
result, err := client.UserHasTracks(ctx, id)
if err != nil {
return false, err
}
return result[0], nil
}
// Like likes the track.
func (r *trackRepository) Like(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.AddTracksToLibrary(ctx, id)
}
// Unlike unlikes the track.
func (r *trackRepository) Unlike(ctx context.Context, id spotify.ID) error {
c, err := r.clientManager.GetClient()
if err != nil {
return err
}
client := c.Open()
return client.RemoveTracksFromLibrary(ctx, id)
}
package command
import (
"context"
"os"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/config"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
"github.com/yanosea/spotlike/pkg/utility"
)
var (
// output is the output string.
output = ""
// NewCli is a variable holding the current Cli creation function.
NewCli CreateCliFunc = newCli
)
// Cli is an interface that represents the command line interface of spotlike cli.
type Cli interface {
Init(envconfig proxy.Envconfig, spotify proxy.Spotify, http proxy.Http, randstr proxy.Randstr, url proxy.Url, version string, versionUtil utility.VersionUtil) int
Run() int
}
// cli is a struct that represents the command line interface of spotlike cli.
type cli struct {
Exit func(int)
Cobra proxy.Cobra
RootCommand proxy.Command
Context context.Context
ClientManager api.ClientManager
}
// CreateCliFunc is a function type for creating new Cli instances.
type CreateCliFunc func(exit func(int), cobra proxy.Cobra, ctx context.Context) Cli
// newCli is the default implementation of CreateCliFunc.
func newCli(exit func(int), cobra proxy.Cobra, ctx context.Context) Cli {
return &cli{
Exit: exit,
Cobra: cobra,
RootCommand: nil,
Context: ctx,
ClientManager: nil,
}
}
// Init initializes the command line interface of spotlike.
func (c *cli) Init(
envconfig proxy.Envconfig,
spotify proxy.Spotify,
http proxy.Http,
randstr proxy.Randstr,
url proxy.Url,
version string,
versionUtil utility.VersionUtil,
) int {
configurator := config.NewSpotlikeCliConfigurator(envconfig)
conf, err := configurator.GetConfig()
if err != nil {
output = formatter.AppendErrorToOutput(err, output)
if err := presenter.Print(os.Stderr, output); err != nil {
return 1
}
return 1
}
if c.ClientManager == nil {
c.ClientManager = api.NewClientManager(spotify, http, randstr, url)
}
if conf.SpotifyID != "" &&
conf.SpotifySecret != "" &&
conf.SpotifyRedirectUri != "" &&
conf.SpotifyRefreshToken != "" {
if err := c.ClientManager.InitializeClient(
c.Context,
&api.ClientConfig{
SpotifyID: conf.SpotifyID,
SpotifySecret: conf.SpotifySecret,
SpotifyRedirectUri: conf.SpotifyRedirectUri,
SpotifyRefreshToken: conf.SpotifyRefreshToken,
},
); err != nil {
output = formatter.AppendErrorToOutput(err, output)
if err := presenter.Print(os.Stderr, output); err != nil {
return 1
}
return 1
}
}
c.RootCommand = NewRootCommand(
c.Exit,
c.Cobra,
versionUtil.GetVersion(version),
conf,
&output,
)
return 0
}
// Run runs the command line interface of spotlike cli.
func (c *cli) Run() (exitCode int) {
exitCode = 0
defer func() {
if c.ClientManager != nil && c.ClientManager.IsClientInitialized() {
if err := c.ClientManager.CloseClient(); err != nil {
output = formatter.AppendErrorToOutput(err, output)
if err := presenter.Print(os.Stderr, output); err != nil {
exitCode = 1
}
exitCode = 1
}
}
}()
out := os.Stdout
if err := c.RootCommand.ExecuteContext(c.Context); err != nil {
output = formatter.AppendErrorToOutput(err, output)
out = os.Stderr
exitCode = 1
}
if output != "" {
if err := presenter.Print(out, output); err != nil {
exitCode = 1
}
}
return exitCode
}
package command
import (
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command/spotlike"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command/spotlike/completion"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command/spotlike/get"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command/spotlike/like"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command/spotlike/unlike"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/config"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// RootOptions provides the options for the root command.
type RootOptions struct {
// Version is a flag to show the version of spotlike.
Version bool
}
var (
// rootOps is a variable to store the root options with the default values for injecting the dependencies in testing.
rootOps = RootOptions{
Version: false,
}
)
// NewRootCommand returns a new instance of the root command.
func NewRootCommand(
exit func(int),
cobra proxy.Cobra,
version string,
conf *config.SpotlikeCliConfig,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("spotlike")
cmd.SetUsageTemplate(rootUsageTemplate)
cmd.SetHelpTemplate(rootHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.PersistentFlags().BoolVarP(
&rootOps.Version,
"version",
"v",
false,
"🔖 show the version of spotlike",
)
versionCmd := spotlike.NewVersionCommand(
cobra,
version,
output,
)
authCmd := spotlike.NewAuthCommand(
exit,
cobra,
version,
conf,
output,
)
cmd.AddCommand(
authCmd,
completion.NewCompletionCommand(
cobra,
output,
),
get.NewGetCommand(
cobra,
authCmd,
output,
),
like.NewLikeCommand(
exit,
cobra,
authCmd,
output,
),
unlike.NewUnlikeCommand(
exit,
cobra,
authCmd,
output,
),
spotlike.NewSearchCommand(
cobra,
authCmd,
output,
),
versionCmd,
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runRoot(cmd, args, output, versionCmd)
},
)
return cmd
}
// runRoot runs the root command.
func runRoot(
cmd *c.Command,
args []string,
output *string,
versionCmd proxy.Command,
) error {
if rootOps.Version {
return versionCmd.RunE(cmd, args)
}
o := formatter.Yellow("⚡ Use sub command below...")
o += `
- 🔑 auth, au, a - Authenticate your Spotify client.
- 📚 get, ge, g - Get the information of the content on Spotify by ID.
- 🤍 like, li, l - Like content on Spotify by ID.
- 💔 unlike, un, u - Unlike content on Spotify by ID.
- 🔍 search, se, s - Search for the ID of content in Spotify.
- 🔧 completion, comp, c - Generate the autocompletion script for the specified shell.
- 🔖 version, ver, v - Show the version of spotlike.
- 🤝 help - Help for spotlike.
Use "spotlike --help" for more information about spotlike.
Use "spotlike [command] --help" for more information about a command.
`
*output = o
return nil
}
const (
// rootHelpTemplate is the help template of the root command.
rootHelpTemplate = `⚪ spotlike is the CLI tool to LIKE contents in Spotify.
You can like tracks, albums, and artists in Spotify by ID.
Also, you can search for the ID of tracks, albums, and artists in Spotify.
` + rootUsageTemplate
// rootUsageTemplate is the usage template of the root command.
rootUsageTemplate = `Usage:
spotlike [flags]
spotlike [command]
Available Commands:
auth, au, a 🔑 Authenticate your Spotify client.
get, ge, g 📚 Get the information of the content on Spotify by ID.
like, li, l 🤍 Like content on Spotify by ID.
unlike, un, u 💔 Unlike content on Spotify by ID.
search, se, s 🔍 Search for the ID of content in Spotify.
completion, comp, c 🔧 Generate the autocompletion script for the specified shell.
version, ver, v 🔖 Show the version of spotlike.
help 🤝 Help for spotlike.
Flags:
-h, --help 🤝 help for spotlike
-v, --version 🔖 version for spotlike
Use "spotlike [command] --help" for more information about a command.
`
)
package spotlike
import (
"os"
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/config"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// AuthOptions provides the options for the auth command.
type AuthOptions struct {
// ID is the client ID of the Spotify API.
ID string
// Secret is the client secret of the Spotify API.
Secret string
// RedirectUri is the redirect URI of the Spotify API.
RedirectUri string
}
// NewAuthCommand returns a new instance of the auth command.
func NewAuthCommand(
exit func(int),
cobra proxy.Cobra,
version string,
conf *config.SpotlikeCliConfig,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("auth")
cmd.SetAliases([]string{"au", "a"})
cmd.SetUsageTemplate(authUsageTemplate)
cmd.SetHelpTemplate(authHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(cmd *c.Command, _ []string) error {
return runAuth(exit, cmd, version, conf, output)
},
)
return cmd
}
// runAuth runs the auth command.
func runAuth(exit func(int), cmd *c.Command, version string, conf *config.SpotlikeCliConfig, output *string) error {
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
if client, err := clientManager.GetClient(); err != nil && err.Error() != "client not initialized" {
return err
} else if client != nil {
o := formatter.Yellow("⚡You've already setup your Spotify client...")
*output = o
return nil
}
if conf.SpotifyID == "" {
for {
if id, err := presenter.RunPrompt(
"🆔 Input your Spotify Client ID",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled authentication...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if id == "" {
continue
} else {
conf.SpotifyID = id
break
}
}
}
if conf.SpotifySecret == "" {
for {
if secret, err := presenter.RunPromptWithMask(
"🔑 Input your Spotify Client Secret",
'*',
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled authentication...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if secret == "" {
continue
} else {
conf.SpotifySecret = secret
break
}
}
}
if conf.SpotifyRedirectUri == "" {
for {
if redirectUri, err := presenter.RunPrompt(
"🔗 Input your Spotify Redirect URI",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled authentication...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if redirectUri == "" {
continue
} else {
conf.SpotifyRedirectUri = redirectUri
break
}
}
}
var (
client api.Client
refreshToken string
err error
authUrlChan = make(chan string, 1)
doneChan = make(chan bool)
)
go func() {
defer close(doneChan)
err = clientManager.InitializeClient(
cmd.Context(),
&api.ClientConfig{
SpotifyID: conf.SpotifyID,
SpotifySecret: conf.SpotifySecret,
SpotifyRedirectUri: conf.SpotifyRedirectUri,
SpotifyRefreshToken: "",
},
)
if err != nil {
authUrlChan <- ""
return
}
client, err = clientManager.GetClient()
if err != nil {
authUrlChan <- ""
return
}
_, refreshToken, err = client.Auth(authUrlChan, version)
if err != nil {
return
}
conf.SpotifyRefreshToken = refreshToken
if client != nil {
client.UpdateConfig(&api.ClientConfig{
SpotifyID: conf.SpotifyID,
SpotifySecret: conf.SpotifySecret,
SpotifyRedirectUri: conf.SpotifyRedirectUri,
SpotifyRefreshToken: refreshToken,
})
}
}()
authUrl := <-authUrlChan
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, "🌐 Login to Spotify by visiting the page below in your browser."); err != nil {
return err
}
if err := presenter.Print(os.Stdout, " "+authUrl); err != nil {
return err
}
<-doneChan
if err != nil {
o := formatter.Red("❌ Failed to authenticate... Please try again...")
*output = o
return err
}
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Green("🎉 Authentication succeeded!")); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("⚡ If you don't want spotlike to ask above again, execute commands below to set envs or set your profile to set those.")); err != nil {
return err
}
if err := presenter.Print(os.Stdout, " export SPOTIFY_ID="+conf.SpotifyID); err != nil {
return err
}
if err := presenter.Print(os.Stdout, " export SPOTIFY_SECRET="+conf.SpotifySecret); err != nil {
return err
}
if err := presenter.Print(os.Stdout, " export SPOTIFY_REDIRECT_URI="+conf.SpotifyRedirectUri); err != nil {
return err
}
if err := presenter.Print(os.Stdout, " export SPOTIFY_REFRESH_TOKEN="+refreshToken); err != nil {
return err
}
return nil
}
const (
// authHelpTemplate is the help template of the auth command.
authHelpTemplate = `🔑 Authenticate your Spotify client
You have to authenticate your Spotify client to use spotlike at first.
spotlike will ask you to input your Client ID, Client Secret, and Redirect URI.
Also, you can set those by environment variables below:
SPOTIFY_ID
SPOTIFY_SECRET
SPOTIFY_REDIRECT_URI
Finally, spotlike will guide you to set those all environment variables.
` + authUsageTemplate
// authUsageTemplate is the usage template of the auth command.
authUsageTemplate = `Usage:
spotlike auth [flags]
spotlike au [flags]
spotlike a [flags]
Flags:
-h, --help 🤝 help for auth
`
)
package completion
import (
"bytes"
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewCompletionBashCommand returns a new instance of the completion bash command.
func NewCompletionBashCommand(
cobra proxy.Cobra,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("bash")
cmd.SetAliases([]string{"ba", "b"})
cmd.SetUsageTemplate(completionBashUsageTemplate)
cmd.SetHelpTemplate(completionBashHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runCompletionBash(cmd, output)
},
)
return cmd
}
// runCompletionBash generates the autocompletion script for the bash shell.
func runCompletionBash(cmd *c.Command, output *string) error {
buf := new(bytes.Buffer)
err := cmd.Root().GenBashCompletion(buf)
*output = buf.String()
return err
}
const (
// completionBashHelpTemplate is the help template of the completion bash command.
completionBashHelpTemplate = `🔧🐚 Generate the autocompletion script for the bash shell.
This script depends on the "bash-completion" package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(spotlike completion bash)
To load completions for every new session, execute once:
- 🐧 Linux:
spotlike completion bash > /etc/bash_completion.d/spotlike
- 🍎 macOS:
spotlike completion bash > $(brew --prefix)/etc/bash_completion.d/spotlike
You will need to start a new shell for this setup to take effect.
` + completionBashUsageTemplate
// compleitonUsageTemplate is the usage template of the completion bash command.
completionBashUsageTemplate = `Usage:
spotlike completion bash [flags]
Flags:
-h, --help 🤝 help for bash
`
)
package completion
import (
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewCompletionCommand returns a new instance of the completion command.
func NewCompletionCommand(
cobra proxy.Cobra,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("completion")
cmd.SetAliases([]string{"comp", "c"})
cmd.SetUsageTemplate(completionUsageTemplate)
cmd.SetHelpTemplate(completionHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.AddCommand(
NewCompletionBashCommand(cobra, output),
NewCompletionFishCommand(cobra, output),
NewCompletionPowerShellCommand(cobra, output),
NewCompletionZshCommand(cobra, output),
)
return cmd
}
const (
// completionHelpTemplate is the help template of the completion command.
completionHelpTemplate = `🔧 Generate the autocompletion script for the specified shell.
` + completionUsageTemplate
// compleitonUsageTemplate is the usage template of the completion command.
completionUsageTemplate = `Usage:
spotlike completion [flags]
spotlike completion [command]
Available Subommands:
bash 🔧🐚 Generate the autocompletion script for the bash shell.
fish 🔧🐟 Generate the autocompletion script for the fish shell.
powershell 🔧🪟 Generate the autocompletion script for the powershell shell.
zsh 🔧🧙 Generate the autocompletion script for the zsh shell.
Flags:
-h, --help 🤝 help for completion
Use "spotlike completion [command] --help" for more information about a command.
`
)
package completion
import (
"bytes"
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewCompletionFishCommand returns a new instance of the completion fish command.
func NewCompletionFishCommand(
cobra proxy.Cobra,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("fish")
cmd.SetAliases([]string{"fi", "f"})
cmd.SetUsageTemplate(completionFishUsageTemplate)
cmd.SetHelpTemplate(completionFishHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runCompletionFish(cmd, output)
},
)
return cmd
}
// runCompletionFish generates the autocompletion script for the fish shell.
func runCompletionFish(cmd *c.Command, output *string) error {
buf := new(bytes.Buffer)
err := cmd.Root().GenFishCompletion(buf, true)
*output = buf.String()
return err
}
const (
// completionFishHelpTemplate is the help template of the completion fish command.
completionFishHelpTemplate = `🔧🐟 Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
spotlike completion fish | source
To load completions for every new session, execute once:
spotlike completion fish > ~/.config/fish/completions/spotlike.fish
You will need to start a new shell for this setup to take effect.
` + completionFishUsageTemplate
// compleitonUsageTemplate is the usage template of the completion fish command.
completionFishUsageTemplate = `Usage:
spotlike completion fish [flags]
Flags:
-h, --help 🤝 help for fish
`
)
package completion
import (
"bytes"
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewCompletionPowerShellCommand returns a new instance of the completion powershell command.
func NewCompletionPowerShellCommand(
cobra proxy.Cobra,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("powershell")
cmd.SetAliases([]string{"pwsh", "p"})
cmd.SetUsageTemplate(completionPowerShellUsageTemplate)
cmd.SetHelpTemplate(completionPowerShellHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runCompletionPowerShell(cmd, output)
},
)
return cmd
}
// runCompletionPowerShell generates the autocompletion script for the powershell shell.
func runCompletionPowerShell(cmd *c.Command, output *string) error {
buf := new(bytes.Buffer)
err := cmd.Root().GenPowerShellCompletion(buf)
*output = buf.String()
return err
}
const (
// completionPowerShellHelpTemplate is the help template of the completion powershell command.
completionPowerShellHelpTemplate = `🔧🪟 Generate the autocompletion script for the powershell shell.
To load completions in your current shell session:
spotlike completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command to your powershell profile.
` + completionPowerShellUsageTemplate
// compleitonUsageTemplate is the usage template of the completion powershell command.
completionPowerShellUsageTemplate = `Usage:
spotlike completion powershell [flags]
Flags:
-h, --help 🤝 help for powershell
`
)
package completion
import (
"bytes"
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewCompletionZshCommand returns a new instance of the completion zsh command.
func NewCompletionZshCommand(
cobra proxy.Cobra,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("zsh")
cmd.SetAliases([]string{"zs", "z"})
cmd.SetUsageTemplate(completionZshUsageTemplate)
cmd.SetHelpTemplate(completionZshHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runCompletionZsh(cmd, output)
},
)
return cmd
}
// runCompletionZsh generates the autocompletion script for the zsh shell.
func runCompletionZsh(cmd *c.Command, output *string) error {
buf := new(bytes.Buffer)
err := cmd.Root().GenZshCompletion(buf)
*output = buf.String()
return err
}
const (
// completionZshHelpTemplate is the help template of the completion zsh command.
completionZshHelpTemplate = `🔧🧙 Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need to enable it.
You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(spotlike completion zsh)
To load completions for every new session, execute once:
- 🐧 Linux:
spotlike completion zsh > "${fpath[1]}/_spotlike"
- 🍎 macOS:
spotlike completion zsh > $(brew --prefix)/share/zsh/site-functions/_spotlike
You will need to start a new shell for this setup to take effect.
` + completionZshUsageTemplate
// compleitonUsageTemplate is the usage template of the completion zsh command.
completionZshUsageTemplate = `Usage:
spotlike completion zsh [flags]
Flags:
-h, --help 🤝 help for zsh
`
)
package get
import (
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// GetAlbumsOptions represents the options for the get albums command.
type GetAlbumsOptions struct {
Format string
}
var (
// getAlbumsOps is a variable to store the get albums options with the default values for injecting the dependencies in testing.
getAlbumsOps = GetAlbumsOptions{
Format: "table",
}
)
// NewGetAlbumsCommand returns a new instance of the get albums command.
func NewGetAlbumsCommand(
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("albums")
cmd.SetAliases([]string{"als", "a"})
cmd.SetUsageTemplate(getAlbumsUsageTemplate)
cmd.SetHelpTemplate(getAlbumsHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(1))
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&getAlbumsOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runGetAlbums(cmd, authCmd, output, args)
},
)
return cmd
}
// runGetAlbums is a function to get the albums on Spotify by artist ID.
func runGetAlbums(cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
}
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), args[0])
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + args[0] + " is not found... or it is not an artist...")
*output = o
return nil
}
albumRepo := repository.NewAlbumRepository()
gaauc := spotlikeApp.NewGetAllAlbumsByArtistIdUseCase(albumRepo)
gaaucoDtos, err := gaauc.Run(cmd.Context(), args[0])
if err != nil {
return err
}
var albums []*spotlikeApp.GetAlbumUseCaseOutputDto
for _, gaaucoDto := range gaaucoDtos {
album := &spotlikeApp.GetAlbumUseCaseOutputDto{
ID: gaaucoDto.ID,
Name: gaaucoDto.Name,
Artists: gaaucoDto.Artists,
ReleaseDate: gaaucoDto.ReleaseDate,
}
albums = append(albums, album)
}
f, err := formatter.NewFormatter(getAlbumsOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(albums)
if err != nil {
return err
}
*output = "\n" + o
return nil
}
const (
// getAlbumsHelpTemplate is a help template for the get albums command.
getAlbumsHelpTemplate = `ℹ️💿 Get the information of the albums on Spotify by artist ID.
You can get the information of the albums on Spotify by artist ID.
Before using this command,
you need to get the ID of the artist you want to get by using the search command.
` + getAlbumsUsageTemplate
// getAlbumsUsageTemplate is a usage template for the get albums command.
getAlbumsUsageTemplate = `Usage:
spotlike get albums [flags] [arguments]
spotlike get als [flags] [arguments]
spotlike get a [flags] [arguments]
Flags:
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for albums
Argument:
ID 🆔 ID of the artist (e.g. : "00DuPiLri3mNomvvM3nZvU")
`
)
package get
import (
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewGetCommand creates a new get command.
func NewGetCommand(
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("get")
cmd.SetAliases([]string{"ge", "g"})
cmd.SetUsageTemplate(getUsageTemplate)
cmd.SetHelpTemplate(getHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.AddCommand(
NewGetAlbumsCommand(
cobra,
authCmd,
output,
),
NewGetTracksCommand(
cobra,
authCmd,
output,
),
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runGet(output)
},
)
return cmd
}
// runGet runs the get command.
func runGet(output *string) error {
o := formatter.Yellow("⚡ Use sub command below...")
o += `
- 💿 album
- 🎵 track
Use "spotlike get --help" for more information about spotlike get.
Use "spotlike get [command] --help" for more information about a command.
`
*output = o
return nil
}
const (
// getHelpTemplate is the help template of the get command.
getHelpTemplate = `ℹ️ Get the information of the content on Spotify by ID.
You can get the information of the content on Spotify by ID.
Before using this command,
you need to get the ID of the content you want to get by using the search command.
` + getUsageTemplate
// getUsageTemplate is the usage template of the get command.
getUsageTemplate = `Usage:
spotlike get [flags]
spotlike ge [flags]
spotlike g [flags]
spotlike get [command]
spotlike ge [command]
spotlike g [command]
Available Commands:
albums, als, a 💿 Get the information of the albums on Spotify by ID.
tracks, trs, t 🎵 Get the information of the tracks on Spotify by ID.
Flags:
-h, --help 🤝 help for get
Use "spotlike get [command] --help" for more information about a command.
`
)
package get
import (
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// GetTracksOptions represents the options for the get tracks command.
type GetTracksOptions struct {
Format string
}
var (
// getTracksOps is a variable to store the get tracks options with the default values for injecting the dependencies in testing.
getTracksOps = GetTracksOptions{
Format: "table",
}
)
// NewGetTracksCommand returns a new instance of the get tracks command.
func NewGetTracksCommand(
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("tracks")
cmd.SetAliases([]string{"trs", "t"})
cmd.SetUsageTemplate(getTracksUsageTemplate)
cmd.SetHelpTemplate(getTracksHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(1))
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&getTracksOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runGetTracks(cmd, authCmd, output, args)
},
)
return cmd
}
// runGetTracks is a function to get the tracks on Spotify by ID.
func runGetTracks(cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
}
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), args[0])
if err != nil && err.Error() != "Resource not found" {
return err
}
albumRepo := repository.NewAlbumRepository()
gauc := spotlikeApp.NewGetAlbumUseCase(albumRepo)
gaucoDto, err := gauc.Run(cmd.Context(), args[0])
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil && gaucoDto == nil {
o := formatter.Yellow("⚡ The id " + args[0] + " is not found... or it is not an artist or album...")
*output = o
return nil
}
var tracks []*spotlikeApp.GetTrackUseCaseOutputDto
trackRepo := repository.NewTrackRepository()
if gAucoDto != nil {
gatAuc := spotlikeApp.NewGetAllTracksByArtistIdUseCase(trackRepo)
gatAucoDtos, err := gatAuc.Run(cmd.Context(), args[0])
if err != nil {
return err
}
for _, gatAucoDto := range gatAucoDtos {
track := &spotlikeApp.GetTrackUseCaseOutputDto{
ID: gatAucoDto.ID,
Name: gatAucoDto.Name,
Artists: gatAucoDto.Artists,
Album: gatAucoDto.Album,
TrackNumber: gatAucoDto.TrackNumber,
ReleaseDate: gatAucoDto.ReleaseDate,
}
tracks = append(tracks, track)
}
}
if gaucoDto != nil {
gatAuc := spotlikeApp.NewGetAllTracksByAlbumIdUseCase(trackRepo)
gatAucoDtos, err := gatAuc.Run(cmd.Context(), args[0])
if err != nil {
return err
}
for _, gatAucoDto := range gatAucoDtos {
track := &spotlikeApp.GetTrackUseCaseOutputDto{
ID: gatAucoDto.ID,
Name: gatAucoDto.Name,
Artists: gatAucoDto.Artists,
Album: gatAucoDto.Album,
TrackNumber: gatAucoDto.TrackNumber,
ReleaseDate: gatAucoDto.ReleaseDate,
}
tracks = append(tracks, track)
}
}
f, err := formatter.NewFormatter(getTracksOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(tracks)
if err != nil {
return err
}
*output = "\n" + o
return nil
}
const (
// getTracksHelpTemplate is a help template for the get tracks command.
getTracksHelpTemplate = `ℹ️🎵 Get the information of the tracks on Spotify by ID.
You can get the information of the tracks on Spotify by artist ID or album ID.
Before using this command,
you need to get the ID of the artist or album you want to get by using the search command.
` + getTracksUsageTemplate
// getTracksUsageTemplate is a usage template for the get tracks command.
getTracksUsageTemplate = `Usage:
spotlike get tracks [flags] [arguments]
spotlike get trs [flags] [arguments]
spotlike get t [flags] [arguments]
Flags:
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for tracks
Argument:
ID 🆔 ID of the artist or album (e.g: "00DuPiLri3mNomvvM3nZvU")
`
)
package like
import (
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// LikeAlbumOptions represents the options for the like album command.
type LikeAlbumOptions struct {
Artist string
NoConfirm bool
Format string
}
var (
// likeAlbumOps is a variable to store the like album options with the default values for injecting the dependencies in testing.
likeAlbumOps = LikeAlbumOptions{
Artist: "",
NoConfirm: false,
Format: "table",
}
)
// NewLikeAlbumCommand creates a new like album command.
func NewLikeAlbumCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("album")
cmd.SetAliases([]string{"al", "a"})
cmd.SetUsageTemplate(likeAlbumUsageTemplate)
cmd.SetHelpTemplate(likeAlbumHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&likeAlbumOps.Artist,
"artist",
"A",
"",
"🆔 an ID of the artist to like all albums released by the artist",
)
cmd.Flags().BoolVarP(
&likeAlbumOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before liking the album",
)
cmd.Flags().StringVarP(
&likeAlbumOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runLikeAlbum(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runLikeAlbum runs the like album command.
func runLikeAlbum(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if likeAlbumOps.Artist == "" && len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gaucoDtos []*spotlikeApp.GetAlbumUseCaseOutputDto
albumRepo := repository.NewAlbumRepository()
if likeAlbumOps.Artist != "" {
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), likeAlbumOps.Artist)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + likeAlbumOps.Artist + " is not found or it is not an artist...")
*output = o
return nil
}
gaaAuc := spotlikeApp.NewGetAllAlbumsByArtistIdUseCase(albumRepo)
gaaAucoDtos, err := gaaAuc.Run(cmd.Context(), likeAlbumOps.Artist)
if err != nil {
return err
}
for _, gaaAucoDto := range gaaAucoDtos {
gaucoDtos = append(
gaucoDtos,
&spotlikeApp.GetAlbumUseCaseOutputDto{
ID: gaaAucoDto.ID,
Name: gaaAucoDto.Name,
Artists: gaaAucoDto.Artists,
ReleaseDate: gaaAucoDto.ReleaseDate,
},
)
}
} else {
for _, id := range args {
gauc := spotlikeApp.NewGetAlbumUseCase(albumRepo)
gaucoDto, err := gauc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gaucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not an album...")
*output = o
continue
}
gaucoDtos = append(
gaucoDtos,
&spotlikeApp.GetAlbumUseCaseOutputDto{
ID: gaucoDto.ID,
Name: gaucoDto.Name,
Artists: gaucoDto.Artists,
ReleaseDate: gaucoDto.ReleaseDate,
},
)
}
}
clauc := spotlikeApp.NewCheckLikeAlbumUseCase(albumRepo)
lauc := spotlikeApp.NewLikeAlbumUseCase(albumRepo)
var likeExecutedAlbums []*spotlikeApp.GetAlbumUseCaseOutputDto
for _, gaucoDto := range gaucoDtos {
alreadyLiked, err := clauc.Run(cmd.Context(), gaucoDto.ID)
if err != nil {
return err
}
if alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Album "+gaucoDto.Name+" ("+gaucoDto.ID+")"+" released by "+gaucoDto.Artists+" is already liked. skipping...")); err != nil {
return err
}
continue
}
if !likeAlbumOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with liking " + gaucoDto.Name + " (" + gaucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled liking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled liking album " + gaucoDto.Name + " (" + gaucoDto.ID + ") ...")
*output = o
continue
}
}
if err := lauc.Run(cmd.Context(), gaucoDto.ID); err != nil {
return err
}
likeExecutedAlbums = append(likeExecutedAlbums, gaucoDto)
}
if len(likeExecutedAlbums) != 0 {
f, err := formatter.NewFormatter(likeAlbumOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(likeExecutedAlbums)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅🤍💿 Successfully liked albums below!")); err != nil {
return err
}
}
return nil
}
const (
// likeAlbumHelpTemplate is a help template for the like album command.
likeAlbumHelpTemplate = `🤍💿 Like albums on Spotify by ID.
You can like tracks on Spotify by ID.
Before using this command,
you need to get the ID of the track you want to like by using the search command.
Also, you can like all albums released by the artist with specifying the ID of the artist with artist flag.
If you specify artist flag, the arguments would be ignored.
` + likeAlbumUsageTemplate
// likeAlbumUsageTemplate is the usage template of the like album command.
likeAlbumUsageTemplate = `Usage:
spotlike like album [flags] [arguments]
spotlike like al [flags] [arguments]
spotlike like a [flags] [arguments]
Flags:
-A, --artist 🆔 an ID of the artist to like all albums released by the artist
--no-confirm 🚫 do not confirm before liking the album
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for album
Arguments:
ID 🆔 ID of the albums (e.g: "1dGzXXa8MeTCdi0oBbvB1J 6Xy481vVb9vPK4qbCuT9u1")
`
)
package like
import (
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// LikeArtistOptions represents the options for the like artist command.
type LikeArtistOptions struct {
NoConfirm bool
Format string
}
var (
// likeArtistOps is a variable to store the like artist options with the default values for injecting the dependencies in testing.
likeArtistOps = LikeArtistOptions{
NoConfirm: false,
Format: "table",
}
)
// NewLikeArtistCommand creates a new like artist command.
func NewLikeArtistCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("artist")
cmd.SetAliases([]string{"ar", "A"})
cmd.SetUsageTemplate(likeArtistUsageTemplate)
cmd.SetHelpTemplate(likeArtistHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().BoolVarP(
&likeArtistOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before liking the artist",
)
cmd.Flags().StringVarP(
&likeArtistOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runLikeArtist(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runLikeArtist runs the like artist command.
func runLikeArtist(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gAucoDtos []*spotlikeApp.GetArtistUseCaseOutputDto
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
for _, id := range args {
gAucoDto, err := gAuc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not an artist...")
*output = o
continue
}
gAucoDtos = append(gAucoDtos, gAucoDto)
}
clAuc := spotlikeApp.NewCheckLikeArtistUseCase(artistRepo)
lAuc := spotlikeApp.NewLikeArtistUseCase(artistRepo)
var likeExecutedArtists []*spotlikeApp.GetArtistUseCaseOutputDto
for _, gAucoDto := range gAucoDtos {
alreadyLiked, err := clAuc.Run(cmd.Context(), gAucoDto.ID)
if err != nil {
return err
}
if alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Artist "+gAucoDto.Name+" ("+gAucoDto.ID+") "+"is already liked. skipping...")); err != nil {
return err
}
continue
}
if !likeArtistOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with liking " + gAucoDto.Name + " (" + gAucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled liking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled liking artist " + gAucoDto.Name + " (" + gAucoDto.ID + ") ...")
*output = o
continue
}
}
if err := lAuc.Run(cmd.Context(), gAucoDto.ID); err != nil {
return err
}
likeExecutedArtists = append(likeExecutedArtists, gAucoDto)
}
if len(likeExecutedArtists) != 0 {
f, err := formatter.NewFormatter(likeArtistOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(likeExecutedArtists)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅🤍🎤 Successfully liked artists below!")); err != nil {
return err
}
}
return nil
}
const (
// likeArtistHelpTemplate is a help template for the like artist command.
likeArtistHelpTemplate = `🤍🎤 Like artists on Spotify by ID.
You can like artists on Spotify by ID.
Before using this command,
you need to get the ID of the artist you want to like by using the search command.
` + likeArtistUsageTemplate
// likeArtistUsageTemplate is the usage template of the like artist command.
likeArtistUsageTemplate = `Usage:
spotlike like artist [flags] [arguments]
spotlike like ar [flags] [arguments]
spotlike like A [flags] [arguments]
Flags:
--no-confirm 🚫 do not confirm before liking the artist
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for artist
Arguments:
ID 🆔 ID of the artists (e.g. : "00DuPiLri3mNomvvM3nZvU 3B9O5mYYw89fFXkwKh7jCS")
`
)
package like
import (
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewLikeCommand creates a new like command.
func NewLikeCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("like")
cmd.SetAliases([]string{"li", "l"})
cmd.SetUsageTemplate(likeUsageTemplate)
cmd.SetHelpTemplate(likeHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.AddCommand(
NewLikeArtistCommand(
exit,
cobra,
authCmd,
output,
),
NewLikeAlbumCommand(
exit,
cobra,
authCmd,
output,
),
NewLikeTrackCommand(
exit,
cobra,
authCmd,
output,
),
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runLike(output)
},
)
return cmd
}
// runLike runs the like command.
func runLike(output *string) error {
o := formatter.Yellow("⚡ Use sub command below...")
o += `
- 🎤 artist
- 💿 album
- 🎵 track
Use "spotlike like --help" for more information about spotlike like.
Use "spotlike like [command] --help" for more information about a command.
`
*output = o
return nil
}
const (
// likeHelpTemplate is the help template of the like command.
likeHelpTemplate = `🤍 Like content on Spotify by ID.
You can like content on Spotify by ID.
Before using this command,
you need to get the ID of the content you want to like by using the search command.
` + likeUsageTemplate
// likeUsageTemplate is the usage template of the like command.
likeUsageTemplate = `Usage:
spotlike like [flags]
spotlike li [flags]
spotlike l [flags]
spotlike like [command]
spotlike li [command]
spotlike l [command]
Available Commands:
artist, ar, A 🎤 Like an artist on Spotify by ID.
album, al, a 💿 Like albums on Spotify by ID.
track, tr, t 🎵 Like tracks on Spotify by ID.
Flags:
-h, --help 🤝 help for like
Use "spotlike like [command] --help" for more information about a command.
`
)
package like
import (
"fmt"
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// LikeTrackOptions represents the options for the like track command.
type LikeTrackOptions struct {
Artist string
Album string
NoConfirm bool
Format string
}
var (
// likeTrackOps is a variable to store the like track options with the default values for injecting the dependencies in testing.
likeTrackOps = LikeTrackOptions{
Artist: "",
Album: "",
NoConfirm: false,
Format: "table",
}
)
// NewLikeTrackCommand creates a new like track command.
func NewLikeTrackCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("track")
cmd.SetAliases([]string{"tr", "t"})
cmd.SetUsageTemplate(likeTrackUsageTemplate)
cmd.SetHelpTemplate(likeTrackHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&likeTrackOps.Artist,
"artist",
"A",
"",
"🆔 an ID of the artist to like all albums released by the artist",
)
cmd.Flags().StringVarP(
&likeTrackOps.Album,
"album",
"a",
"",
"🆔 an ID of the album to like all tracks in the album",
)
cmd.Flags().BoolVarP(
&likeTrackOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before liking the track",
)
cmd.Flags().StringVarP(
&likeTrackOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runLikeTrack(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runLikeTrack executes the like track command.
func runLikeTrack(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if likeTrackOps.Artist != "" && likeTrackOps.Album != "" {
o := formatter.Yellow("⚡ Both artist and album flags can not be specified at the same time...")
*output = o
return nil
}
if likeTrackOps.Artist == "" && likeTrackOps.Album == "" && len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gtucoDtos []*spotlikeApp.GetTrackUseCaseOutputDto
trackRepo := repository.NewTrackRepository()
if likeTrackOps.Artist != "" {
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), likeTrackOps.Artist)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + likeTrackOps.Artist + " is not found or it is not an artist...")
*output = o
return nil
}
gatAuc := spotlikeApp.NewGetAllTracksByArtistIdUseCase(trackRepo)
gatAucoDtos, err := gatAuc.Run(cmd.Context(), likeTrackOps.Artist)
if err != nil {
return err
}
for _, gatAucoDto := range gatAucoDtos {
gtucoDtos = append(
gtucoDtos,
&spotlikeApp.GetTrackUseCaseOutputDto{
ID: gatAucoDto.ID,
Name: gatAucoDto.Name,
Artists: gatAucoDto.Artists,
Album: gatAucoDto.Album,
TrackNumber: gatAucoDto.TrackNumber,
ReleaseDate: gatAucoDto.ReleaseDate,
},
)
}
} else if likeTrackOps.Album != "" {
albumRepo := repository.NewAlbumRepository()
gauc := spotlikeApp.NewGetAlbumUseCase(albumRepo)
gaucoDto, err := gauc.Run(cmd.Context(), likeTrackOps.Album)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gaucoDto == nil {
o := formatter.Yellow("⚡ The id " + likeTrackOps.Album + " is not found or it is not an album...")
*output = o
return nil
}
gatauc := spotlikeApp.NewGetAllTracksByAlbumIdUseCase(trackRepo)
gataucoDtos, err := gatauc.Run(cmd.Context(), likeTrackOps.Album)
if err != nil {
return err
}
for _, gataucoDto := range gataucoDtos {
gtucoDtos = append(
gtucoDtos,
&spotlikeApp.GetTrackUseCaseOutputDto{
ID: gataucoDto.ID,
Name: gataucoDto.Name,
Artists: gataucoDto.Artists,
Album: gataucoDto.Album,
TrackNumber: gataucoDto.TrackNumber,
ReleaseDate: gataucoDto.ReleaseDate,
},
)
}
} else {
for _, id := range args {
gtuc := spotlikeApp.NewGetTrackUseCase(trackRepo)
gtucoDto, err := gtuc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gtucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not a track...")
*output = o
continue
}
gtucoDtos = append(gtucoDtos, gtucoDto)
}
}
cltuc := spotlikeApp.NewCheckLikeTrackUseCase(trackRepo)
ltuc := spotlikeApp.NewLikeTrackUseCase(trackRepo)
var likeExecutedTracks []*spotlikeApp.GetTrackUseCaseOutputDto
for _, gtucoDto := range gtucoDtos {
alreadyLiked, err := cltuc.Run(cmd.Context(), gtucoDto.ID)
if err != nil {
return err
}
if alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Track #"+fmt.Sprint(gtucoDto.TrackNumber)+" "+gtucoDto.Name+" ("+gtucoDto.ID+")"+" on "+gtucoDto.Album+" rereased by "+gtucoDto.Artists+" is already liked. skipping...")); err != nil {
return err
}
continue
}
if !likeTrackOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with liking " + gtucoDto.Name + " (" + gtucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled liking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled liking track " + gtucoDto.Name + " (" + gtucoDto.ID + ") ...")
*output = o
continue
}
}
if err := ltuc.Run(cmd.Context(), gtucoDto.ID); err != nil {
return err
}
likeExecutedTracks = append(likeExecutedTracks, gtucoDto)
}
if len(likeExecutedTracks) != 0 {
f, err := formatter.NewFormatter(likeTrackOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(likeExecutedTracks)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅🤍🎵 Successfully liked tracks below!")); err != nil {
return err
}
}
return nil
}
const (
// likeTrackHelpTemplate is a template for the help message of the like track command.
likeTrackHelpTemplate = `🤍🎵 Like tracks on Spotify by ID.
You can like tracks on Spotify by ID.
Before using this command,
you need to get the ID of the album you want to like by using the search command.
Also, you can like all tracks released by the artist with specifying the ID of the artist with artist flag.
If you specify artist flag, the arguments would be ignored.
Also, you can like all tracks in the album with specifying the ID of the album with album flag.
If you specify album flag, the arguments would be ignored.
Both artist and album flags can not be specified at the same time.
` + likeTrackUsageTemplate
// likeTrackUsageTemplate is a template for the usage message of the like track command.
likeTrackUsageTemplate = `Usage:
spotlike like track [flags] [arguments]
spotlike like tr [flags] [arguments]
spotlike like t [flags] [arguments]
Flags:
-A, --artist 🆔 an ID of the artist to like all albums released by the artist
-a, --album 🆔 an ID of the album to like all tracks in the album
--no-confirm 🚫 do not confirm before liking the track
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for track
Arguments:
ID 🆔 ID of the tracks (e.g: "20q73dOrP7ceLGAJQVtuTq 5A7nqzXUt5IZIOA7oNBv6M")
`
)
package spotlike
import (
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// SearchOptions provides the options for the search command.
type SearchOptions struct {
Artist bool
Album bool
Track bool
Max int
Format string
}
var (
// searchOps is a variable to store the search options with the default values for injecting the dependencies in testing.
searchOps = SearchOptions{
Artist: false,
Album: false,
Track: false,
Max: 10,
Format: "table",
}
)
// NewSearchCommand returns a new instance of the search command.
func NewSearchCommand(
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("search")
cmd.SetAliases([]string{"se", "s"})
cmd.SetUsageTemplate(searchUsageTemplate)
cmd.SetHelpTemplate(searchHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().BoolVarP(
&searchOps.Artist,
"artist",
"A",
false,
"🎤 search for artists",
)
cmd.Flags().BoolVarP(
&searchOps.Album,
"album",
"a",
false,
"💿 search for albums",
)
cmd.Flags().BoolVarP(
&searchOps.Track,
"track",
"t",
false,
"🎵 search for tracks",
)
cmd.Flags().IntVarP(
&searchOps.Max,
"max",
"m",
10,
"🔢 maximum number of search results",
)
cmd.Flags().StringVarP(
&searchOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runSearch(cmd, authCmd, output, args)
},
)
return cmd
}
// runSearch runs the search command.
func runSearch(cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if len(args) == 0 {
o := formatter.Yellow("⚡ No query arguments specified...")
*output = o
return nil
}
if !searchOps.Artist && !searchOps.Album && !searchOps.Track {
o := formatter.Yellow("⚡ No search type specified...")
*output = o
return nil
}
if searchOps.Artist && searchOps.Album ||
searchOps.Artist && searchOps.Track ||
searchOps.Album && searchOps.Track {
o := formatter.Yellow("⚡ Multiple search types is not supported...")
*output = o
return nil
}
if searchOps.Max < 1 {
o := formatter.Yellow("⚡ Invalid search result number...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
}
var dtos any
if searchOps.Artist {
artistRepo := repository.NewArtistRepository()
sAuc := spotlikeApp.NewSearchArtistUseCase(artistRepo)
sAoDtos, err := sAuc.Run(cmd.Context(), args, searchOps.Max)
if err != nil {
return err
}
if len(sAoDtos) == 0 {
o := formatter.Yellow("⚡ No artists found...")
*output = o
return nil
}
dtos = sAoDtos
}
if searchOps.Album {
albumRepo := repository.NewAlbumRepository()
sauc := spotlikeApp.NewSearchAlbumUseCase(albumRepo)
saoDtos, err := sauc.Run(cmd.Context(), args, searchOps.Max)
if err != nil {
return err
}
if len(saoDtos) == 0 {
o := formatter.Yellow("⚡ No albums found...")
*output = o
return nil
}
dtos = saoDtos
}
if searchOps.Track {
trackRepo := repository.NewTrackRepository()
stuc := spotlikeApp.NewSearchTrackUseCase(trackRepo)
stoDtos, err := stuc.Run(cmd.Context(), args, searchOps.Max)
if err != nil {
return err
}
if len(stoDtos) == 0 {
o := formatter.Yellow("⚡ No tracks found...")
*output = o
return nil
}
dtos = stoDtos
}
f, err := formatter.NewFormatter(searchOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(dtos)
if err != nil {
return err
}
*output = "\n" + o
return nil
}
const (
// searchHelpTemplate is the help template of the search command.
searchHelpTemplate = `🔍 Search for the ID of content in Spotify.
You can search for artists, albums, and tracks by specifying the type of content to search for.
If you want to search artists, specify the "-A" or "--artist" option.
If you want to search albums, specify the "-a" or "--album" option.
If you want to search tracks, specify the "-t" or "--track" option.
Also, you can specify the maximum number of search results by specifying the "-m" or "--max" option.
` + searchUsageTemplate
// searchUsageTemplate is the usage template of the search command.
searchUsageTemplate = `Usage:
spotlike search [flags] [arguments]
spotlike se [flags] [arguments]
spotlike s [flags] [arguments]
Flags:
-A, --artist 🎤 search for artists
-a, --album 💿 search for albums
-t, --track 🎵 search for tracks
-m, --max 🔢 maximum number of search results (default 10)
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for search
Arguments:
keywords 🔡 search content by keywords (multiple keywords are separated by a space)
`
)
package unlike
import (
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// UnlikeAlbumOptions represents the options for the unlike album command.
type UnlikeAlbumOptions struct {
Artist string
NoConfirm bool
Format string
}
var (
// unlikeAlbumOps is a variable to store the unlike album options with the default values for injecting the dependencies in testing.
unlikeAlbumOps = UnlikeAlbumOptions{
Artist: "",
NoConfirm: false,
Format: "table",
}
)
// NewUnlikeAlbumCommand creates a new unlike album command.
func NewUnlikeAlbumCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("album")
cmd.SetAliases([]string{"al", "a"})
cmd.SetUsageTemplate(unlikeAlbumUsageTemplate)
cmd.SetHelpTemplate(unlikeAlbumHelpTemplatep)
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&unlikeAlbumOps.Artist,
"artist",
"A",
"",
"🆔 an ID of the artist to unlike all albums released by the artist",
)
cmd.Flags().BoolVarP(
&unlikeAlbumOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before unliking the album",
)
cmd.Flags().StringVarP(
&unlikeAlbumOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runUnlikeAlbum(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runUnlikeAlbum executes the unlike album command.
func runUnlikeAlbum(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if unlikeAlbumOps.Artist == "" && len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gaucoDtos []*spotlikeApp.GetAlbumUseCaseOutputDto
albumRepo := repository.NewAlbumRepository()
if unlikeAlbumOps.Artist != "" {
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), unlikeAlbumOps.Artist)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + unlikeAlbumOps.Artist + " is not found or it is not an artist...")
*output = o
return nil
}
gaaAuc := spotlikeApp.NewGetAllAlbumsByArtistIdUseCase(albumRepo)
gaaAucoDtos, err := gaaAuc.Run(cmd.Context(), unlikeAlbumOps.Artist)
if err != nil {
return err
}
for _, gaaAucoDto := range gaaAucoDtos {
gaucoDtos = append(
gaucoDtos,
&spotlikeApp.GetAlbumUseCaseOutputDto{
ID: gaaAucoDto.ID,
Name: gaaAucoDto.Name,
Artists: gaaAucoDto.Artists,
ReleaseDate: gaaAucoDto.ReleaseDate,
},
)
}
} else {
for _, id := range args {
gauc := spotlikeApp.NewGetAlbumUseCase(albumRepo)
gaucoDto, err := gauc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gaucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not an album...")
*output = o
continue
}
gaucoDtos = append(
gaucoDtos,
&spotlikeApp.GetAlbumUseCaseOutputDto{
ID: gaucoDto.ID,
Name: gaucoDto.Name,
Artists: gaucoDto.Artists,
ReleaseDate: gaucoDto.ReleaseDate,
},
)
}
}
clauc := spotlikeApp.NewCheckLikeAlbumUseCase(albumRepo)
uauc := spotlikeApp.NewUnlikeAlbumUseCase(albumRepo)
var unlikeExecutedAlbums []*spotlikeApp.GetAlbumUseCaseOutputDto
for _, gaucoDto := range gaucoDtos {
alreadyLiked, err := clauc.Run(cmd.Context(), gaucoDto.ID)
if err != nil {
return err
}
if !alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Album "+gaucoDto.Name+" ("+gaucoDto.ID+")"+" released by "+gaucoDto.Artists+" is not liked. skipping...")); err != nil {
return err
}
continue
}
if !unlikeAlbumOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with unliking " + gaucoDto.Name + " (" + gaucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled unliking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled unliking album " + gaucoDto.Name + " (" + gaucoDto.ID + ") ...")
*output = o
continue
}
}
if err := uauc.Run(cmd.Context(), gaucoDto.ID); err != nil {
return err
}
unlikeExecutedAlbums = append(unlikeExecutedAlbums, gaucoDto)
}
if len(unlikeExecutedAlbums) != 0 {
f, err := formatter.NewFormatter(unlikeAlbumOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(unlikeExecutedAlbums)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅💔💿 Successfully unliked albums below!")); err != nil {
return err
}
}
return nil
}
const (
// unlikeAlbumHelpTemplatep is a help message template for the unlike album command.
unlikeAlbumHelpTemplatep = `💔💿 Unlike albums on Spotify by ID.
You can unlike tracks on Spotify by ID.
Before using this command,
you need to get the ID of the track you want to unlike by using the search command.
Also, you can unlike all albums released by the artist with specifying the ID of the artist with artist flag.
If you specify artist flag, the arguments would be ignored.
` + unlikeAlbumUsageTemplate
// unlikeAlbumUsageTemplate is a usage message template for the unlike album command.
unlikeAlbumUsageTemplate = `Usage:
spotlike unlike album [flags] [arguments]
spotlike unlike al [flags] [arguments]
spotlike unlike a [flags] [arguments]
Flags:
-A, --artist 🆔 an ID of the artist to unlike all albums released by the artist
--no-confirm 🚫 do not confirm before unliking the album
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for album
Arguments:
ID 🆔 ID of the albums (e.g: "1dGzXXa8MeTCdi0oBbvB1J 6Xy481vVb9vPK4qbCuT9u1")
`
)
package unlike
import (
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// UnlikeArtistOptions represents the options for the unlike artist command.
type UnlikeArtistOptions struct {
NoConfirm bool
Format string
}
var (
// unlikeArtistOps is a variable to store the unlike artist options with the default values for injecting the dependencies in testing.
unlikeArtistOps = UnlikeArtistOptions{
NoConfirm: false,
Format: "table",
}
)
// NewUnlikeArtistCommand creates a new unlike artist command.
func NewUnlikeArtistCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("artist")
cmd.SetAliases([]string{"ar", "A"})
cmd.SetUsageTemplate(unlikeArtistUsageTemplate)
cmd.SetHelpTemplate(unlikeArtistHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().BoolVarP(
&unlikeArtistOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before unliking the artist",
)
cmd.Flags().StringVarP(
&unlikeArtistOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runUnlikeArtist(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runUnlikeArtist executes the unlike artist command.
func runUnlikeArtist(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gAucoDtos []*spotlikeApp.GetArtistUseCaseOutputDto
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
for _, id := range args {
gAucoDto, err := gAuc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not an artist...")
*output = o
continue
}
gAucoDtos = append(gAucoDtos, gAucoDto)
}
clAuc := spotlikeApp.NewCheckLikeArtistUseCase(artistRepo)
uAuc := spotlikeApp.NewUnlikeArtistUseCase(artistRepo)
var unlikeExecutedArtists []*spotlikeApp.GetArtistUseCaseOutputDto
for _, gAucoDto := range gAucoDtos {
alreadyLiked, err := clAuc.Run(cmd.Context(), gAucoDto.ID)
if err != nil {
return err
}
if !alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Artist "+gAucoDto.Name+" ("+gAucoDto.ID+") "+"is not liked. skipping...")); err != nil {
return err
}
continue
}
if !unlikeArtistOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with unliking " + gAucoDto.Name + " (" + gAucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled unliking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled unliking artist " + gAucoDto.Name + " (" + gAucoDto.ID + ") ...")
*output = o
continue
}
}
if err := uAuc.Run(cmd.Context(), gAucoDto.ID); err != nil {
return err
}
unlikeExecutedArtists = append(unlikeExecutedArtists, gAucoDto)
}
if len(unlikeExecutedArtists) != 0 {
f, err := formatter.NewFormatter(unlikeArtistOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(unlikeExecutedArtists)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅💔🎤 Successfully unliked artists below!")); err != nil {
return err
}
}
return nil
}
const (
// unlikeArtistHelpTemplate is the help template of the unlike artist command.
unlikeArtistHelpTemplate = `💔🎤 Unlike artists on Spotify by ID.
You can unlike artists on Spotify by ID.
Before using this command,
you need to get the ID of the artist you want to like by using the search command.
` + unlikeArtistUsageTemplate
// unlikeArtistUsageTemplate is the usage template of the unlike artist command.
unlikeArtistUsageTemplate = `Usage:
spotlike unlike artist [flags] [arguments]
spotlike unlike ar [flags] [arguments]
spotlike unlike A [flags] [arguments]
Flags:
--no-confirm 🚫 do not confirm before unliking the artist
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for artist
Arguments:
ID 🆔 ID of the artists (e.g. : "00DuPiLri3mNomvvM3nZvU 3B9O5mYYw89fFXkwKh7jCS")
`
)
package unlike
import (
"fmt"
"os"
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/infrastructure/spotify/api"
"github.com/yanosea/spotlike/app/infrastructure/spotify/repository"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/presenter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// UnlikeTrackOptions represents the options for the unlike track command.
type UnlikeTrackOptions struct {
Artist string
Album string
NoConfirm bool
Format string
}
var (
// unlikeTrackOps is a variable to store the unlike track options with the default values for injecting the dependencies in testing.
unlikeTrackOps = UnlikeTrackOptions{
Artist: "",
Album: "",
NoConfirm: false,
Format: "table",
}
)
// NewUnlikeTrackCommand creates a new unlike track command.
func NewUnlikeTrackCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("track")
cmd.SetAliases([]string{"tr", "t"})
cmd.SetUsageTemplate(unlikeTrackUsageTemplate)
cmd.SetHelpTemplate(unlikeTrackHelpTemplate)
cmd.SetSilenceErrors(true)
cmd.Flags().StringVarP(
&unlikeTrackOps.Artist,
"artist",
"A",
"",
"🆔 an ID of the artist to unlike all albums released by the artist",
)
cmd.Flags().StringVarP(
&unlikeTrackOps.Album,
"album",
"a",
"",
"🆔 an ID of the album to unlike all tracks in the album",
)
cmd.Flags().BoolVarP(
&unlikeTrackOps.NoConfirm,
"no-confirm",
"",
false,
"🚫 do not confirm before unliking the track",
)
cmd.Flags().StringVarP(
&unlikeTrackOps.Format,
"format",
"f",
"table",
"📝 format of the output (default \"table\", e.g: \"plain\")",
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runUnlikeTrack(exit, cmd, authCmd, output, args)
},
)
return cmd
}
// runUnlikeTrack executes the unlike track command.
func runUnlikeTrack(exit func(int), cmd *c.Command, authCmd proxy.Command, output *string, args []string) error {
if unlikeTrackOps.Artist != "" && unlikeTrackOps.Album != "" {
o := formatter.Yellow("⚡ Both artist and album flags can not be specified at the same time...")
*output = o
return nil
}
if unlikeTrackOps.Artist == "" && unlikeTrackOps.Album == "" && len(args) == 0 {
o := formatter.Yellow("⚡ No ID arguments specified...")
*output = o
return nil
}
clientManager := api.GetClientManager()
if clientManager == nil {
o := formatter.Red("❌ Client manager is not initialized...")
*output = o
return nil
}
_, err := clientManager.GetClient()
if err != nil && err.Error() == "client not initialized" {
if err := authCmd.RunE(cmd, args); err != nil {
return err
}
} else if err != nil {
o := formatter.Red("❌ Failed to get client...")
*output = o
return err
}
var gtucoDtos []*spotlikeApp.GetTrackUseCaseOutputDto
trackRepo := repository.NewTrackRepository()
if unlikeTrackOps.Artist != "" {
artistRepo := repository.NewArtistRepository()
gAuc := spotlikeApp.NewGetArtistUseCase(artistRepo)
gAucoDto, err := gAuc.Run(cmd.Context(), unlikeTrackOps.Artist)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gAucoDto == nil {
o := formatter.Yellow("⚡ The id " + unlikeTrackOps.Artist + " is not found or it is not an artist...")
*output = o
return nil
}
gatAuc := spotlikeApp.NewGetAllTracksByArtistIdUseCase(trackRepo)
gatAucoDtos, err := gatAuc.Run(cmd.Context(), unlikeTrackOps.Artist)
if err != nil {
return err
}
for _, gatAucoDto := range gatAucoDtos {
gtucoDtos = append(
gtucoDtos,
&spotlikeApp.GetTrackUseCaseOutputDto{
ID: gatAucoDto.ID,
Name: gatAucoDto.Name,
Artists: gatAucoDto.Artists,
Album: gatAucoDto.Album,
TrackNumber: gatAucoDto.TrackNumber,
ReleaseDate: gatAucoDto.ReleaseDate,
},
)
}
} else if unlikeTrackOps.Album != "" {
albumRepo := repository.NewAlbumRepository()
gauc := spotlikeApp.NewGetAlbumUseCase(albumRepo)
gaucoDto, err := gauc.Run(cmd.Context(), unlikeTrackOps.Album)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gaucoDto == nil {
o := formatter.Yellow("⚡ The id " + unlikeTrackOps.Album + " is not found or it is not an album...")
*output = o
return nil
}
gatauc := spotlikeApp.NewGetAllTracksByAlbumIdUseCase(trackRepo)
gataucoDtos, err := gatauc.Run(cmd.Context(), unlikeTrackOps.Album)
if err != nil {
return err
}
for _, gataucoDto := range gataucoDtos {
gtucoDtos = append(
gtucoDtos,
&spotlikeApp.GetTrackUseCaseOutputDto{
ID: gataucoDto.ID,
Name: gataucoDto.Name,
Artists: gataucoDto.Artists,
Album: gataucoDto.Album,
TrackNumber: gataucoDto.TrackNumber,
ReleaseDate: gataucoDto.ReleaseDate,
},
)
}
} else {
for _, id := range args {
gtuc := spotlikeApp.NewGetTrackUseCase(trackRepo)
gtucoDto, err := gtuc.Run(cmd.Context(), id)
if err != nil && err.Error() != "Resource not found" {
return err
}
if gtucoDto == nil {
o := formatter.Yellow("⚡ The id " + id + " is not found or it is not a track...")
*output = o
continue
}
gtucoDtos = append(gtucoDtos, gtucoDto)
}
}
cltuc := spotlikeApp.NewCheckLikeTrackUseCase(trackRepo)
utuc := spotlikeApp.NewUnlikeTrackUseCase(trackRepo)
var likeExecutedTracks []*spotlikeApp.GetTrackUseCaseOutputDto
for _, gtucoDto := range gtucoDtos {
alreadyLiked, err := cltuc.Run(cmd.Context(), gtucoDto.ID)
if err != nil {
return err
}
if !alreadyLiked {
if err := presenter.Print(os.Stdout, formatter.Blue("⏩ Track #"+fmt.Sprint(gtucoDto.TrackNumber)+" "+gtucoDto.Name+" ("+gtucoDto.ID+")"+" on "+gtucoDto.Album+" rereased by "+gtucoDto.Artists+" is not liked. skipping...")); err != nil {
return err
}
continue
}
if !unlikeTrackOps.NoConfirm {
if answer, err := presenter.RunPrompt(
"Proceed with unliking " + gtucoDto.Name + " (" + gtucoDto.ID + ") ? [y/N]",
); err != nil && err.Error() == "^C" {
if err := presenter.Print(os.Stdout, "\n"); err != nil {
return err
}
if err := presenter.Print(os.Stdout, formatter.Yellow("🚫 Cancelled unliking...")); err != nil {
return err
}
exit(130)
return nil
} else if err != nil {
return err
} else if answer != "y" && answer != "Y" {
o := formatter.Yellow("🚫 Cancelled unliking track" + gtucoDto.Name + " (" + gtucoDto.ID + ") ...")
*output = o
continue
}
}
if err := utuc.Run(cmd.Context(), gtucoDto.ID); err != nil {
return err
}
likeExecutedTracks = append(likeExecutedTracks, gtucoDto)
}
if len(likeExecutedTracks) != 0 {
f, err := formatter.NewFormatter(unlikeTrackOps.Format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(likeExecutedTracks)
if err != nil {
return err
}
*output = "\n" + o
if err := presenter.Print(os.Stdout, formatter.Green("✅💔🎵 Successfully unliked tracks below!")); err != nil {
return err
}
}
return nil
}
const (
// unlikeTrackHelpTemplate is the help template of the unlike track command.
unlikeTrackHelpTemplate = `🤍🎵 Unlike tracks on Spotify by ID.
You can unlike tracks on Spotify by ID.
Before using this command,
you need to get the ID of the album you want to unlike by using the search command.
Also, you can unlike all tracks released by the artist with specifying the ID of the artist with artist flag.
If you specify artist flag, the arguments would be ignored.
Also, you can unlike all tracks in the album with specifying the ID of the album with album flag.
If you specify album flag, the arguments would be ignored.
Both artist and album flags can not be specified at the same time.
` + unlikeTrackUsageTemplate
// unlikeTrackUsageTemplate is the usage template of the unlike track command.
unlikeTrackUsageTemplate = `Usage:
spotlike unlike track [flags] [arguments]
spotlike unlike tr [flags] [arguments]
spotlike unlike t [flags] [arguments]
Flags:
-A, --artist 🆔 an ID of the artist to unlike all albums released by the artist
-a, --album 🆔 an ID of the album to unlike all tracks in the album
--no-confirm 🚫 do not confirm before unliking the track
-f, --format 📝 format of the output (default "table", e.g: "plain")
-h, --help 🤝 help for track
Arguments:
ID 🆔 ID of the tracks (e.g: " ")
`
)
package unlike
import (
c "github.com/spf13/cobra"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
// NewUnlikeCommand creates a new unlike command.
func NewUnlikeCommand(
exit func(int),
cobra proxy.Cobra,
authCmd proxy.Command,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("unlike")
cmd.SetAliases([]string{"un", "u"})
cmd.SetUsageTemplate(unlikeUsageTemplate)
cmd.SetHelpTemplate(unlikeHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.AddCommand(
NewUnlikeArtistCommand(
exit,
cobra,
authCmd,
output,
),
NewUnlikeAlbumCommand(
exit,
cobra,
authCmd,
output,
),
NewUnlikeTrackCommand(
exit,
cobra,
authCmd,
output,
),
)
cmd.SetRunE(
func(cmd *c.Command, args []string) error {
return runUnlike(output)
},
)
return cmd
}
// runUnlike runs the unlike command.
func runUnlike(output *string) error {
o := formatter.Yellow("⚡ Use sub command below...")
o += `
- 🎤 artist
- 💿 album
- 🎵 track
Use "spotlike unlike --help" for more information about spotlike unlike.
Use "spotlike unlike [command] --help" for more information about a command.
`
*output = o
return nil
}
const (
// unlikeHelpTemplate is the help template of the unlike command.
unlikeHelpTemplate = `💔 Unlike content on Spotify by ID.
You can unlike content on Spotify by ID.
Before using this command,
you need to get the ID of the content you want to like by using the search command.
` + unlikeUsageTemplate
// unlikeUsageTemplate is the usage template of the unlike command.
unlikeUsageTemplate = `Usage:
spotlike unlike [flags]
spotlike un [flags]
spotlike u [flags]
spotlike unlike [command]
spotlike un [command]
spotlike u [command]
Available Commands:
artist, ar, A 🎤 Unlike an artist on Spotify by ID.
album, al, a 💿 Unlike albums on Spotify by ID.
track, tr, t 🎵 Unlike tracks on Spotify by ID.
Flags:
-h, --help 🤝 help for unlike
Use "spotlike unlike [command] --help" for more information about a command.
`
)
package spotlike
import (
c "github.com/spf13/cobra"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/formatter"
"github.com/yanosea/spotlike/pkg/proxy"
)
var (
// format is the format of the output.
format = "plain"
)
// NewVersionCommand returns a new instance of the version command.
func NewVersionCommand(
cobra proxy.Cobra,
version string,
output *string,
) proxy.Command {
cmd := cobra.NewCommand()
cmd.SetUse("version")
cmd.SetAliases([]string{"ver", "v"})
cmd.SetUsageTemplate(versionUsageTemplate)
cmd.SetHelpTemplate(versionHelpTemplate)
cmd.SetArgs(cobra.ExactArgs(0))
cmd.SetSilenceErrors(true)
cmd.SetRunE(
func(_ *c.Command, _ []string) error {
return runVersion(version, output)
},
)
return cmd
}
// runVersion runs the version command.
func runVersion(version string, output *string) error {
uc := spotlikeApp.NewGetVersionUseCase()
dto := uc.Run(version)
f, err := formatter.NewFormatter(format)
if err != nil {
o := formatter.Red("❌ Failed to create a formatter...")
*output = o
return err
}
o, err := f.Format(dto)
if err != nil {
return err
}
*output = o
return nil
}
const (
// versionHelpTemplate is the help template of the version command.
versionHelpTemplate = `🔖 Show the version of spotlike
` + versionUsageTemplate
// versionUsageTemplate is the usage template of the version command.
versionUsageTemplate = `Usage:
spotlike version [flags]
spotlike ver [flags]
spotlike v [flags]
Flags:
-h, --help 🤝 help for version
`
)
package config
import (
baseConfig "github.com/yanosea/spotlike/app/config"
"github.com/yanosea/spotlike/pkg/proxy"
)
// SpotlikeCliConfigurator is an interface that gets the configuration of the Spotlike cli application.
type SpotlikeCliConfigurator interface {
GetConfig() (*SpotlikeCliConfig, error)
}
// cliConfigurator is a struct that implements the SpotlikeCliConfigurator interface.
type cliConfigurator struct {
*baseConfig.BaseConfigurator
}
// NewSpotlikeCliConfigurator creates a new SpotlikeCliConfigurator.
func NewSpotlikeCliConfigurator(
envconfigProxy proxy.Envconfig,
) SpotlikeCliConfigurator {
return &cliConfigurator{
BaseConfigurator: baseConfig.NewConfigurator(
envconfigProxy,
),
}
}
// SpotlikeCliConfig is a struct that contains the configuration of the Spotlike cli application.
type SpotlikeCliConfig struct {
baseConfig.SpotlikeConfig
}
// envConfig is a struct that contains the environment variables.
type envConfig struct {
// SpotifyID is the Spotify client ID.
SpotifyID string `envconfig:"SPOTIFY_ID"`
// SpotifySecret is the Spotify client secret.
SpotifySecret string `envconfig:"SPOTIFY_SECRET"`
// SpotifyRedirectUri is the Spotify redirect URI.
SpotifyRedirectUri string `envconfig:"SPOTIFY_REDIRECT_URI"`
// SpotifyRefreshToken is the Spotify refresh token.
SpotifyRefreshToken string `envconfig:"SPOTIFY_REFRESH_TOKEN"`
}
// GetConfig gets the configuration of the spotlike cli application.
func (c *cliConfigurator) GetConfig() (*SpotlikeCliConfig, error) {
var env envConfig
if err := c.Envconfig.Process("", &env); err != nil {
return nil, err
}
config := &SpotlikeCliConfig{
SpotlikeConfig: baseConfig.SpotlikeConfig{
SpotifyID: env.SpotifyID,
SpotifySecret: env.SpotifySecret,
SpotifyRedirectUri: env.SpotifyRedirectUri,
SpotifyRefreshToken: env.SpotifyRefreshToken,
},
}
return config, nil
}
package formatter
import (
"errors"
"fmt"
)
// Formatter is an interface that formats the output of spotlike cli.
type Formatter interface {
Format(result any) (string, error)
}
// NewFormatterFunc is a function type that defines the signature for creating a new Formatter.
type NewFormatterFunc func(format string) (Formatter, error)
// NewFormatter is a function that returns a new instance of the Formatter interface.
var NewFormatter NewFormatterFunc = func(format string) (Formatter, error) {
var f Formatter
switch format {
case "plain":
f = NewPlainFormatter()
case "table":
f = NewTableFormatter()
default:
return nil, errors.New("invalid format")
}
return f, nil
}
// AppendErrorToOutput appends an error to the output.
func AppendErrorToOutput(err error, output string) string {
if err == nil && output == "" {
return ""
}
var result string
if err != nil {
if output == "" {
result = fmt.Sprintf("Error : %s", err)
} else {
result = fmt.Sprintf(output+"\nError : %s", err)
}
} else {
result = output
}
if result != "" {
result = Red(result)
}
return result
}
package formatter
import (
"fmt"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
)
// PlainFormatter is a struct that formats the output of spotlike cli.
type PlainFormatter struct{}
// NewPlainFormatter returns a new instance of the PlainFormatter struct.
func NewPlainFormatter() *PlainFormatter {
return &PlainFormatter{}
}
// Format formats the output of spotlike cli.
func (f *PlainFormatter) Format(result any) (string, error) {
var formatted string
switch v := result.(type) {
case *spotlikeApp.GetVersionUseCaseOutputDto:
formatted = fmt.Sprintf("spotlike version %s", v.Version)
case []*spotlikeApp.SearchArtistUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Artist : " + item.Name
if i < len(v)-1 {
formatted += "\n"
}
}
case []*spotlikeApp.GetArtistUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Artist : " + item.Name
if i < len(v)-1 {
formatted += "\n"
}
}
case []*spotlikeApp.SearchAlbumUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Album : " + item.Name + " released at " + item.ReleaseDate.Format("2006-01-02") + " by " + item.Artists
if i < len(v)-1 {
formatted += "\n"
}
}
case []*spotlikeApp.GetAlbumUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Album : " + item.Name + " released at " + item.ReleaseDate.Format("2006-01-02") + " by " + item.Artists
if i < len(v)-1 {
formatted += "\n"
}
}
case []*spotlikeApp.SearchTrackUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Track : #" + fmt.Sprint(item.TrackNumber) + " " + item.Name + " on " + item.Album + " released at " + item.ReleaseDate.Format("2006-01-02") + " by " + item.Artists
if i < len(v)-1 {
formatted += "\n"
}
}
case []*spotlikeApp.GetTrackUseCaseOutputDto:
for i, item := range v {
formatted += "[" + item.ID + "]" + " Track : #" + fmt.Sprint(item.TrackNumber) + " " + item.Name + " on " + item.Album + " released at " + item.ReleaseDate.Format("2006-01-02") + " by " + item.Artists
if i < len(v)-1 {
formatted += "\n"
}
}
default:
formatted = ""
}
return formatted, nil
}
package formatter
import (
"fmt"
"strings"
spotlikeApp "github.com/yanosea/spotlike/app/application/spotlike"
"github.com/yanosea/spotlike/pkg/proxy"
"github.com/yanosea/spotlike/pkg/utility"
)
// TableFormatter is a struct that formats the output of spotlike cli.
type TableFormatter struct{}
// NewTableFormatter returns a new instance of the TableFormatter struct.
func NewTableFormatter() *TableFormatter {
return &TableFormatter{}
}
var (
// Tu is a variable to store the table writer with the default values for injecting the dependencies in testing.
Tu = utility.NewTableWriterUtil(proxy.NewTableWriter())
)
// tableData is a struct that holds the data of a table.
type tableData struct {
header []string
rows [][]string
}
// Format formats the output of spotlike cli.
func (f *TableFormatter) Format(result any) (string, error) {
var data tableData
switch v := result.(type) {
case []*spotlikeApp.SearchArtistUseCaseOutputDto:
data = f.formatSearchArtists(v)
case []*spotlikeApp.GetArtistUseCaseOutputDto:
data = f.formatGetArtists(v)
case []*spotlikeApp.SearchAlbumUseCaseOutputDto:
data = f.formatSearchAlbums(v)
case []*spotlikeApp.GetAlbumUseCaseOutputDto:
data = f.formatGetAlbums(v)
case []*spotlikeApp.SearchTrackUseCaseOutputDto:
data = f.formatSearchTracks(v)
case []*spotlikeApp.GetTrackUseCaseOutputDto:
data = f.formatGetTracks(v)
default:
return "", nil
}
return f.getTableString(data)
}
// formatSearchArtists formats the output of the search artists use case.
func (f *TableFormatter) formatSearchArtists(items []*spotlikeApp.SearchArtistUseCaseOutputDto) tableData {
header := []string{"🆔 ID", "🎤 Artist"}
var rows [][]string
for _, artist := range items {
rows = append(rows, []string{
artist.ID,
artist.Name,
})
}
rows = f.addTotalRow(rows, "artists")
return tableData{header: header, rows: rows}
}
// formatGetArtists formats the output of the get artists use case.
func (f *TableFormatter) formatGetArtists(items []*spotlikeApp.GetArtistUseCaseOutputDto) tableData {
var header []string
var rows [][]string
header = []string{"🆔 ID", "🎤 Artist"}
for _, item := range items {
rows = append(rows, []string{
item.ID,
item.Name,
})
}
rows = f.addTotalRow(rows, "artists")
return tableData{header: header, rows: rows}
}
// formatSearchAlbums formats the output of the search albums use case.
func (f *TableFormatter) formatSearchAlbums(items []*spotlikeApp.SearchAlbumUseCaseOutputDto) tableData {
header := []string{"🆔 ID", "💿 Album", "🎤 Artists", "📅 Release Date"}
var rows [][]string
for _, album := range items {
rows = append(rows, []string{
album.ID,
album.Name,
album.Artists,
album.ReleaseDate.Format("2006-01-02"),
})
}
rows = f.addTotalRow(rows, "albums")
return tableData{header: header, rows: rows}
}
// formatGetAlbums formats the output of the get albums use case.
func (f *TableFormatter) formatGetAlbums(items []*spotlikeApp.GetAlbumUseCaseOutputDto) tableData {
var header []string
var rows [][]string
header = []string{"🆔 ID", "💿 Album", "🎤 Artists", "📅 Release Date"}
for _, item := range items {
rows = append(rows, []string{
item.ID,
item.Name,
item.Artists,
item.ReleaseDate.Format("2006-01-02"),
})
}
rows = f.addTotalRow(rows, "albums")
return tableData{header: header, rows: rows}
}
// formatSearchTracks formats the output of the search tracks use case.
func (f *TableFormatter) formatSearchTracks(items []*spotlikeApp.SearchTrackUseCaseOutputDto) tableData {
header := []string{"🆔 ID", "🔢 Number", "🎵 Track", "💿 Album", "🎤 Artists", "📅 Release Date"}
var rows [][]string
for _, track := range items {
rows = append(rows, []string{
track.ID,
fmt.Sprint(track.TrackNumber),
track.Name,
track.Album,
track.Artists,
track.ReleaseDate.Format("2006-01-02"),
})
}
rows = f.addTotalRow(rows, "tracks")
return tableData{header: header, rows: rows}
}
// formatGetTracks formats the output of the get tracks use case.
func (f *TableFormatter) formatGetTracks(items []*spotlikeApp.GetTrackUseCaseOutputDto) tableData {
var header []string
var rows [][]string
header = []string{"🆔 ID", "🔢 Number", "🎵 Track", "💿 Album", "🎤 Artists", "📅 Release Date"}
for _, item := range items {
rows = append(rows, []string{
item.ID,
fmt.Sprint(item.TrackNumber),
item.Name,
item.Album,
item.Artists,
item.ReleaseDate.Format("2006-01-02"),
})
}
rows = f.addTotalRow(rows, "tracks")
return tableData{header: header, rows: rows}
}
// addTotalRow adds a total row to the table.
func (f *TableFormatter) addTotalRow(rows [][]string, contentType string) [][]string {
if len(rows) == 0 {
return [][]string{}
}
emptyRow := make([]string, len(rows[0]))
for i := range emptyRow {
emptyRow[i] = ""
}
totalRow := make([]string, len(rows[0]))
totalRow[0] = fmt.Sprintf("TOTAL : %d %s!", len(rows), contentType)
rows = append(rows, emptyRow)
rows = append(rows, totalRow)
return rows
}
// getTableString returns a string representation of a table.
func (f *TableFormatter) getTableString(data tableData) (string, error) {
if len(data.header) == 0 || len(data.rows) == 0 {
return "", nil
}
tableString := &strings.Builder{}
table := Tu.GetNewDefaultTable(tableString)
table.Header(data.header)
if err := table.Bulk(data.rows); err != nil {
return "", err
}
if err := table.Render(); err != nil {
return "", err
}
return strings.TrimSuffix(tableString.String(), "\n"), nil
}
package main
import (
"context"
"os"
"github.com/yanosea/spotlike/app/presentation/cli/spotlike/command"
"github.com/yanosea/spotlike/pkg/proxy"
"github.com/yanosea/spotlike/pkg/utility"
)
// SpotlikeCliParams is a struct that represents the options of spotlike cli.
type SpotlikeCliParams struct {
// Version is the version of spotlike cli.
Version string
// Context is the context of spotlike cli.
Context context.Context
// Cobra is a proxy of spf13/cobra.
Cobra proxy.Cobra
// Envconfig is a proxy of kelseyhightower/envconfig.
Envconfig proxy.Envconfig
// Spotify is a proxy of zmb3/spotify.
Spotify proxy.Spotify
// Http is a proxy of net/http.
Http proxy.Http
// Randstr is a proxy of thanhpk/randstr
Randstr proxy.Randstr
// Url is a proxy of net/url.
Url proxy.Url
// VersionUtil provides the version of the application.
VersionUtil utility.VersionUtil
}
var (
// version is the version of spotlike cli and is embedded by goreleaser.
version = ""
// exit is a variable that contains the os.Exit function for injecting dependencies in testing.
exit = os.Exit
// spotlikeCliParams is a variable that contains the SpotlikeCliParams struct.
spotlikeCliParams = SpotlikeCliParams{
Version: version,
Context: context.Background(),
Cobra: proxy.NewCobra(),
Envconfig: proxy.NewEnvconfig(),
Spotify: proxy.NewSpotify(),
Http: proxy.NewHttp(),
Randstr: proxy.NewRandstr(),
Url: proxy.NewUrl(),
VersionUtil: utility.NewVersionUtil(proxy.NewDebug()),
}
)
// main is the entry point of spotlike cli.
func main() {
cli := command.NewCli(
exit,
spotlikeCliParams.Cobra,
spotlikeCliParams.Context,
)
if exitCode := cli.Init(
spotlikeCliParams.Envconfig,
spotlikeCliParams.Spotify,
spotlikeCliParams.Http,
spotlikeCliParams.Randstr,
spotlikeCliParams.Url,
spotlikeCliParams.Version,
spotlikeCliParams.VersionUtil,
); exitCode != 0 {
exit(exitCode)
}
defer spotlikeCliParams.Context.Done()
exit(cli.Run())
}
package presenter
import (
"fmt"
"io"
)
// PrintFunc is a function type that defines the signature for printing output.
type PrintFunc func(writer io.Writer, output string) error
// Print is a function that writes the output to the writer.
var Print PrintFunc = func(writer io.Writer, output string) error {
if output != "" && output != "\n" {
_, err := fmt.Fprintf(writer, "%s\n", output)
return err
} else {
_, err := fmt.Fprintln(writer)
return err
}
}
package presenter
import (
"github.com/yanosea/spotlike/pkg/proxy"
"github.com/yanosea/spotlike/pkg/utility"
)
var (
// Pu is a variable that contains the PromptUtil struct for injecting dependencies in testing.
Pu = utility.NewPromptUtil(proxy.NewPromptui())
)
// RunPrompt runs the prompt.
func RunPrompt(label string) (string, error) {
prompt := Pu.GetPrompt(label)
return prompt.Run()
}
// RunPromptWithMask runs the prompt with mask.
func RunPromptWithMask(label string, mask rune) (string, error) {
prompt := Pu.GetPrompt(label)
prompt.SetMask(mask)
return prompt.Run()
}
package utility
import (
"os"
"github.com/yanosea/spotlike/pkg/proxy"
)
// Capturer is an interface that captures the output of a function.
type Capturer interface {
CaptureOutput(fnc func()) (string, string, error)
}
// capturer is a struct that implements the Captures interface.
type capturer struct {
// os is an interface for operating system functions.
os proxy.Os
// stdBuffer is a buffer for standard output.
stdBuffer proxy.Buffer
// errBuffer is a buffer for error output.
errBuffer proxy.Buffer
}
// NewCapturer returns a new instance of the capturer struct.
func NewCapturer(
os proxy.Os,
stdBuffer proxy.Buffer,
errBuffer proxy.Buffer,
) *capturer {
return &capturer{
os: os,
stdBuffer: stdBuffer,
errBuffer: errBuffer,
}
}
// CaptureOutput captures the output of a function.
func (c *capturer) CaptureOutput(fnc func()) (string, string, error) {
origStdout := os.Stdout
origStderr := os.Stderr
defer func() {
os.Stdout = origStdout
os.Stderr = origStderr
}()
rOut, wOut, err := c.os.Pipe()
if err != nil {
return "", "", err
}
rErr, wErr, err := c.os.Pipe()
if err != nil {
return "", "", err
}
os.Stdout = wOut.(interface{ AsOsFile() *os.File }).AsOsFile()
os.Stderr = wErr.(interface{ AsOsFile() *os.File }).AsOsFile()
fnc()
if err := wOut.Close(); err != nil {
return "", "", err
}
if err := wErr.Close(); err != nil {
return "", "", err
}
if _, err := c.stdBuffer.ReadFrom(rOut); err != nil {
return "", "", err
}
if _, err := c.errBuffer.ReadFrom(rErr); err != nil {
return "", "", err
}
stdout := c.stdBuffer.String()
errout := c.errBuffer.String()
c.stdBuffer.Reset()
c.errBuffer.Reset()
return stdout, errout, nil
}
package utility
import (
"github.com/yanosea/spotlike/pkg/proxy"
)
type PromptUtil interface {
GetPrompt(label string) proxy.Prompt
}
// promptUtil is a struct that implements the PromptUtil interface.
type promptUtil struct {
promptui proxy.Promptui
}
// NewPromptUtil returns a new instance of the promptUtil struct.
func NewPromptUtil(
promptui proxy.Promptui,
) PromptUtil {
return &promptUtil{
promptui: promptui,
}
}
// GetPrompt returns a new instance of the promptui.Prompt.
func (p *promptUtil) GetPrompt(label string) proxy.Prompt {
prompt := p.promptui.NewPrompt()
prompt.SetLabel(label)
return prompt
}
package utility
import (
"strings"
)
// StringsUtil is an interface that contains the utility functions for manipulating strings.
type StringsUtil interface {
RemoveNewLines(s string) string
RemoveSpaces(s string) string
RemoveTabs(s string) string
}
// stringsUtil is a struct that contains the utility functions for manipulating strings.
type stringsUtil struct{}
// NewStringsUtil returns a new instance of the StringsUtil struct.
func NewStringsUtil() StringsUtil {
return &stringsUtil{}
}
// RemoveNewLines removes all new lines from the given strings.
func (s *stringsUtil) RemoveNewLines(str string) string {
return strings.ReplaceAll(str, "\n", "")
}
// RemoveSpaces removes all spaces from the given strings.
func (s *stringsUtil) RemoveSpaces(str string) string {
return strings.ReplaceAll(str, " ", "")
}
// RemoveTabs removes all tabs from the given strings.
func (s *stringsUtil) RemoveTabs(str string) string {
return strings.ReplaceAll(str, "\t", "")
}
package utility
import (
"io"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/yanosea/spotlike/pkg/proxy"
)
// TableWriterUtil is an interface that contains the utility functions for writing tables.
type TableWriterUtil interface {
GetNewDefaultTable(writer io.Writer) proxy.Table
}
// tableWriterUtil is a struct that contains the utility functions for writing tables.
type tableWriterUtil struct {
tableWriter proxy.TableWriter
}
// NewTableWriterUtil returns a new instance of the TableWriterUtil interface.
func NewTableWriterUtil(tableWriter proxy.TableWriter) TableWriterUtil {
return &tableWriterUtil{
tableWriter: tableWriter,
}
}
// GetNewDefaultTable returns a new instance of the default table.
func (t *tableWriterUtil) GetNewDefaultTable(writer io.Writer) proxy.Table {
return t.tableWriter.NewTable(writer,
tablewriter.WithRowAutoWrap(tw.WrapNone),
tablewriter.WithHeaderAutoFormat(tw.On),
tablewriter.WithHeaderAlignment(tw.AlignLeft),
tablewriter.WithRowAlignment(tw.AlignLeft),
tablewriter.WithRendition(tw.Rendition{
Borders: tw.BorderNone,
Settings: tw.Settings{
Lines: tw.LinesNone,
Separators: tw.SeparatorsNone,
},
}),
)
}
package utility
import (
"github.com/yanosea/spotlike/pkg/proxy"
)
// VersionUtil is an interface that provides the version of the application.
type VersionUtil interface {
GetVersion(version string) string
}
// versionUtil is a struct that implements the VersionUtil interface.
type versionUtil struct {
debug proxy.Debug
}
// NewVersionUtil returns a new instance of the versionUtil struct.
func NewVersionUtil(
debug proxy.Debug,
) VersionUtil {
return &versionUtil{
debug: debug,
}
}
// GetVersion returns the version of the application.
func (v *versionUtil) GetVersion(version string) string {
// if version is embedded, return it.
if version != "" {
return version
}
if i, ok := v.debug.ReadBuildInfo(); !ok {
return "unknown"
} else if i.Main.Version != "" {
return i.Main.Version
} else {
return "dev"
}
}