Skip to content

Commit

Permalink
save missing tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
Zibbp committed Sep 8, 2024
1 parent 80f5735 commit bed952f
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 19 deletions.
7 changes: 7 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM mcr.microsoft.com/devcontainers/go:1

RUN apt update

RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

RUN apt install sqlite3 -y
40 changes: 40 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "Existing Dockerfile",
"build": {
// Sets the run context to one level up instead of the .devcontainer folder.
"context": ".",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerfile": "./Dockerfile"
},
"features": {
"ghcr.io/guiyomh/features/golangci-lint:0": {},
"ghcr.io/eitsupi/devcontainer-features/go-task:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"mtxr.sqltools",
"eamodio.gitlens",
"golang.go",
"mtxr.sqltools-driver-sqlite"
]
}
}

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Uncomment the next line to run commands after the container is created.
// "postCreateCommand": "cat /etc/os-release",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "devcontainer"
}
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot

version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
config.json
.env.dev
.env
vendor
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"envFile": "${workspaceFolder}/.env.dev"
"envFile": "${workspaceFolder}/.env",
}
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Convert Spotify playlists to a different service. Currently only supports Tidal.

## Development

`.env.dev`
`.env`

```
SPOTIFY_CLIENT_ID=123
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: '3'

tasks:
dev:
cmds:
- export $(grep -v '^#' .env | xargs) && go run main.go {{.CLI_ARGS}}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
)

type Config struct {
Debug bool `env:"DEBUG, default=false"`
SpotifyClientId string `env:"SPOTIFY_CLIENT_ID, required"`
SpotifyClientSecret string `env:"SPOTIFY_CLIENT_SECRET, required"`
SpotifyRedirectUri string `env:"SPOTIFY_CLIENT_REDIRECT_URI, default=http://localhost:28542/callback"`
Expand Down
23 changes: 15 additions & 8 deletions convert/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ func cleanName(trackName string) string {
return trackName
}

// durationMatch returns a boolean if the provided duration is within 2 seconds.
func durationMatch(spotifyDuration int, tidalDuration int) bool {
// allow for a 2 second difference
log.Debug().Msgf("Spotify Duration: %d, Tidal Duration: %d", spotifyDuration, tidalDuration)
return spotifyDuration >= tidalDuration-2 && spotifyDuration <= tidalDuration+2
return spotifyDuration >= tidalDuration-5 && spotifyDuration <= tidalDuration+5
}

func nameMatch(spotifyName string, tidalName string) bool {
Expand All @@ -46,14 +47,16 @@ func artistMatch(spotifyArtists []string, tidalArtists []string) bool {
return true
}

// spotifyToTidalTrack attempts to find the provided spotify track on Tidal.
// Tracks are checed by ISRC first, falling back to a more crude title/album/artist search
func (s *Service) spotifyToTidalTrack(spotifyTrack *spotifyPkg.FullTrack) (*tidal.OpenAPITrackResource, error) {
spotifyIsrc := spotifyTrack.ExternalIDs["isrc"]
if spotifyIsrc != "" {
// attempt to find the track using the ISRC
tidalTrack, err := s.TidalService.GetTrackByISRC(spotifyIsrc)
if err != nil {
if err.Error() == "track not found" {
log.Warn().Msgf("Track not found via ISRC: %s - %s (%s)", spotifyTrack.ID, spotifyTrack.Name, spotifyIsrc)
log.Warn().Str("platform", "tidal").Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Str("spotify_track_isrc", spotifyIsrc).Msgf("track not found via")
// continue
} else {
return nil, err
Expand All @@ -70,15 +73,16 @@ func (s *Service) spotifyToTidalTrack(spotifyTrack *spotifyPkg.FullTrack) (*tida
spotifyArtists = append(spotifyArtists, artist.Name)
}

// create a clean track name
spotifyName := cleanName(spotifyTrack.Name)
// spotifyDuration := spotifyTrack.Duration

spotifyAlbum := spotifyTrack.Album.Name

// search #1
// search #1 using the track and and album
query := fmt.Sprintf("%s %s", spotifyName, spotifyAlbum)

log.Debug().Msgf("Searching for track: %s", query)
log.Debug().Str("platform", "tidal").Str("query", query).Msg("searching for track")

tidalSearch, err := s.TidalService.SearchTrack(
query,
Expand All @@ -91,23 +95,24 @@ func (s *Service) spotifyToTidalTrack(spotifyTrack *spotifyPkg.FullTrack) (*tida
return nil, err
}

// iterate over list of tidal results to check if we have a match
for _, tidalTrack := range tidalSearch.Tracks {
// check if we have a match

tidalArtists := make([]string, 0)
for _, artist := range tidalTrack.Resource.Artists {
tidalArtists = append(tidalArtists, artist.Name)
}

// check if track meets basic checks
if nameMatch(spotifyName, tidalTrack.Resource.Title) && artistMatch(spotifyArtists, tidalArtists) && durationMatch(int((spotifyTrack.Duration/1000)), tidalTrack.Resource.Duration) {
return &tidalTrack.Resource, nil
}
}

// search #2
// search #2 using the track name and first artist
query = fmt.Sprintf("%s %s", spotifyName, spotifyArtists[0])

log.Debug().Msgf("Searching for track: %s", query)
log.Debug().Str("platform", "tidal").Str("query", query).Msg("searching for track")

tidalSearch, err = s.TidalService.SearchTrack(
query,
Expand All @@ -120,14 +125,16 @@ func (s *Service) spotifyToTidalTrack(spotifyTrack *spotifyPkg.FullTrack) (*tida
return nil, err
}

// iterate over list of tidal results to check if we have a match
for _, tidalTrack := range tidalSearch.Tracks {
// check if we have a match

// check if we have a match
tidalArtists := make([]string, 0)
for _, artist := range tidalTrack.Resource.Artists {
tidalArtists = append(tidalArtists, artist.Name)
}

// check if track meets basic checks
if nameMatch(spotifyName, tidalTrack.Resource.Title) && artistMatch(spotifyArtists, tidalArtists) && durationMatch(int((spotifyTrack.Duration/1000)), tidalTrack.Resource.Duration) {
return &tidalTrack.Resource, nil
}
Expand Down
48 changes: 40 additions & 8 deletions convert/spotify_tidal.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/zibbp/spotify-playlist-convert/db"
"github.com/zibbp/spotify-playlist-convert/spotify"
"github.com/zibbp/spotify-playlist-convert/tidal"
"github.com/zibbp/spotify-playlist-convert/utils"
libSpotify "github.com/zmb3/spotify/v2"

"github.com/rs/zerolog/log"
)
Expand Down Expand Up @@ -41,15 +43,22 @@ func (s *Service) SpotifyToTidal() error {
return err
}

log.Info().Msgf("fetched %d Spotify playlists", len(spotifyPlaylists))

tidalPlaylists, err := s.TidalService.GetUserPlaylists()
if err != nil {
return err
}

log.Info().Msgf("fetched %d Tidal playlists", len(tidalPlaylists.Items))

// compare playlists
for _, spotifyPlaylist := range spotifyPlaylists {
if spotifyPlaylist.Name != "Cool" {
continue
}

// database stuff
// check if spotify playlist is in local database
ctx := context.Background()
dbPlaylist, err := s.Queries.GetPlaylistById(ctx, string(spotifyPlaylist.ID))
if err == sql.ErrNoRows {
Expand All @@ -66,6 +75,7 @@ func (s *Service) SpotifyToTidal() error {
return fmt.Errorf("playlist is empty: %s", dbPlaylist)
}

// get all local database tracks
dbPlaylistTracks, err := s.Queries.GetPlaylistTracks(ctx, sql.NullString{String: dbPlaylist, Valid: true})
if err != nil {
return err
Expand Down Expand Up @@ -115,50 +125,62 @@ func (s *Service) SpotifyToTidal() error {
}
}

//
// begin sync
//

// get all tracks from Spotify playlist
spotifyTracks, err := s.SpotifyService.GetPlaylistTracks(spotifyPlaylist.ID)
if err != nil {
return err
}

log.Info().Str("platform", "spotify").Msgf("fetched %d tracks from playlist %s", len(spotifyTracks), spotifyPlaylist.Name)

// get all tracks from Tidal playlist
tidalTracks, err := s.TidalService.GetPlaylistTracks(tidalPlaylist.UUID)
if err != nil {
return err
}

tidalTrackISRCMap := make(map[string]bool)
log.Info().Str("platform", "tidal").Msgf("fetched %d tracks from playlist %s", len(tidalTracks.Items), tidalPlaylist.Title)

// get the isrc of all tidal tracks
tidalTrackISRCMap := make(map[string]bool)
for _, tidalTrack := range tidalTracks.Items {
tidalTrackISRCMap[tidalTrack.Isrc] = true
}

// hold missing tracks
var missingTracks []*libSpotify.FullTrack

// loop over each spotify track to convert
for _, spotifyTrack := range spotifyTracks {
// check if track is already in playlist using db
if _, ok := dbPlaylistTrackMap[spotifyTrack.ID.String()]; ok {
// log.Info().Msgf("Track already in playlist: %s - %s", spotifyTrack.ID, spotifyTrack.Name)
log.Debug().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Msgf("track is already in playlist according to database")
continue
}

// attempt to find track
tidalTrack, err := s.spotifyToTidalTrack(spotifyTrack)
if err != nil {
log.Error().Err(err).Msgf("Failed to find track: %s - %s", spotifyTrack.ID, spotifyTrack.Name)
log.Error().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Msgf("failed to find track on Tidal")
missingTracks = append(missingTracks, spotifyTrack)
continue
}

if tidalTrack == nil {
log.Warn().Msgf("Track not found: %s - %s", spotifyTrack.ID, spotifyTrack.Name)
missingTracks = append(missingTracks, spotifyTrack)
log.Warn().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Msgf("track not found")
continue
}

// add track to playlist
log.Info().Msgf("Adding track to playlist: %s - %s from: %s", spotifyTrack.ID, spotifyTrack.Name, spotifyPlaylist.Name)
log.Info().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Str("tidal_playlist_id", tidalPlaylist.UUID).Str("tidal_track_id", tidalTrack.ID).Msgf("adding track to tidal playlist")
err = s.TidalService.AddTrackToPlaylist(tidalPlaylist.UUID, tidalTrack.ID)
if err != nil {
log.Error().Err(err).Msgf("Failed to add track to playlist: %s - %s", spotifyTrack.ID, spotifyTrack.Name)
log.Error().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Str("tidal_playlist_id", tidalPlaylist.UUID).Str("tidal_track_id", tidalTrack.ID).Msgf("error adding track to playlist")
continue
}

Expand All @@ -168,13 +190,23 @@ func (s *Service) SpotifyToTidal() error {
TrackID: sql.NullString{String: spotifyTrack.ID.String(), Valid: true},
})
if err != nil {
log.Error().Err(err).Msgf("Failed to add track to database: %s - %s", spotifyTrack.ID, spotifyTrack.Name)
log.Error().Str("spotify_track_id", spotifyTrack.ID.String()).Str("spotify_track_name", spotifyTrack.Name).Msgf("error adding track to database")
continue
}

// sleep to prevent rate limiting
time.Sleep(1 * time.Second)
}

// write missing tracks to file
if len(missingTracks) > 0 {
log.Info().Str("spotify_playlist", spotifyPlaylist.Name).Msgf("processing complete - found %d missing tracks", len(missingTracks))
err := utils.WriteMissingTracks(fmt.Sprintf("%s", spotifyPlaylist.ID), spotifyPlaylist, missingTracks)
if err != nil {
return err
}
}

}

return nil
Expand Down
24 changes: 24 additions & 0 deletions utils/missing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package utils

import (
"encoding/json"
"fmt"
"os"
)

type MissingTracks struct {
Playlist interface{} `json:"playlist"`
Tracks interface{} `json:"tracks"`
}

func WriteMissingTracks(filename string, playlist interface{}, tracks interface{}) error {
if err := os.MkdirAll("/data/missing", 0755); err != nil {
return err
}
data := MissingTracks{Playlist: playlist, Tracks: tracks}
json, err := json.Marshal(data)
if err != nil {
return err
}
return os.WriteFile(fmt.Sprintf("/data/missing/%s.json", filename), json, 0644)
}

0 comments on commit bed952f

Please sign in to comment.