diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..40f6304 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +config.json +.env +vendor +dist/ diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ccfca3c..ae6b41f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -7,9 +7,9 @@ name: Docker on: push: - branches: [ "main" ] + branches: ["main"] # Publish semver tags as releases. - tags: [ 'v*.*.*' ] + tags: ["v*.*.*"] env: # Use docker.io for Docker Hub if empty @@ -17,7 +17,6 @@ env: # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build: runs-on: ubuntu-latest @@ -31,11 +30,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - # Set up BuildKit Docker container builder to be able to build - # multi-platform images and export cache - # https://github.com/docker/setup-buildx-action + # Set up QEMU for Arm64 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up Docker Buildx - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 # Login against a Docker registry except on PR # https://github.com/docker/login-action @@ -54,16 +55,18 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: | + latest=auto + tags: | + type=semver,pattern={{version}} + type=raw,value=dev - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5 + - name: Build and push + uses: docker/build-push-action@v6 with: - context: . + platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + context: . cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 1952932..40f6304 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ config.json .env -vendor \ No newline at end of file +vendor +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..13de124 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,47 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - env: + - CGO_ENABLED=1 + goos: + - linux + goarch: + - amd64 + # - arm64 + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/Dockerfile b/Dockerfile index b2eb881..90da2f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ -FROM golang:1 AS build-stage-01 +FROM golang:1.22 AS build-stage-01 RUN mkdir /app ADD . /app WORKDIR /app -RUN CGO_ENABLED=1 GOOS=linux go build -o spotify-convert main.go +RUN CGO_ENABLED=1 GOOS=linux go build -o spotify-playlist-convert main.go FROM debian:12-slim -COPY --from=build-stage-01 /app/spotify-convert . +COPY --from=build-stage-01 /app/spotify-playlist-convert . RUN apt update && apt install -y ca-certificates RUN update-ca-certificates -ENTRYPOINT ["./spotify-convert"] +ENTRYPOINT ["./spotify-playlist-convert"] diff --git a/README.md b/README.md index 9ef651d..539f91d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,56 @@ -# Spotify Playlist Convert +# Spotify Playlist Sync + +Sync Spotify playlists to different services. Currently only supports Tidal. + +A local database is used to speed up subsequent runs, skipping any tracks that have already been synced. + +## Tidal + +The Tidal conversion uses a mix of Tidal's [work-in-progress public APIs](https://developer.tidal.com/apiref?spec=catalogue-v2&ref=get-albums-v2) and some undocumented ones. + +### Sync + +The track sync first checks if the Spotify track exists on Tidal by searching for the ISRC. If the track is not found by ISRC then a more crude method is used, searching for track name, arists, album, and duration. + +Tidal has aggressive rate limits so a one-second sleep runs after every conversion. Subsequent runs should be much faster as the sync checks the local database first. + +## Usage + +### Requirements + +- A Spotify [developer application](https://developer.spotify.com/) is required for the client ID and client secret. +- A Tidal [developer application](https://developer.tidal.com) is required for the client ID and client secret if you are converting to Tidal. + +### Commands + +Run the application with `-h` to see a list of commands. Currently only a `tidal` command exists which converts your Spotifyp playlists to Tidal playlists. + +#### Tidal + +Options + +```bash + --save-missing-tracks Save missing tracks during the conversion (default: false) + --save-tidal-playlist Save the tidal playlist (default: false) + --save-navidrome-playlist Save a version of the tidal playlist for importing in Navidrome (default: false) +``` + +- Save missing tracks writes all missing Spotify tracks to `/data/missing/.json`. +- Save Tidal playlist writes the Tidal playlist to `/data/tidal/.json`. +- Save Navidrome playlist writes the Tidal playlist in a special format for [importing into Navidrome](https://github.com/Zibbp/navidrome-utils). + +### Docker + +Docker is the recommended way to run the application. See [compose.yml](compose.yml) to get started. + +- Modify the `command` to run whichever command and arguments. +- Update the various `*_CLIENT_ID` and `*_CLIENT_SECRET` variables with your values. +- Update the `SPOTIFY_CLIENT_REDIRECT_URI` with the IP/hostname of your server. -Convert Spotify playlists to a different service. Currently only supports Tidal. ## Development -`.env` +Create a `.env` file with the below variables. Then use [task](https://taskfile.dev/) to run with `task dev`. ``` SPOTIFY_CLIENT_ID=123 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..c1775f4 --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +services: + spotify-playlist-convert: + image: ghcr.io/zibbp/spotify-playlist-convert:latest + volumes: + - ./data:/data + ports: + - 28542:28542 # required for oauth callback + environment: + - TZ=America/Chicago + - DEBUG=true + - SPOTIFY_CLIENT_ID= + - SPOTIFY_CLIENT_SECRET= + - SPOTIFY_CLIENT_REDIRECT_URI=http://SERVERIP:28542/callback + - TIDAL_CLIENT_ID= + - TIDAL_CLIENT_SECRET== + command: tidal --save-missing-tracks --save-tidal-playlist diff --git a/convert/helpers.go b/convert/helpers.go index d3bfe1b..feb5011 100644 --- a/convert/helpers.go +++ b/convert/helpers.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/zibbp/spotify-playlist-convert/tidal" + "github.com/zibbp/spotify-playlist-sync/tidal" "github.com/rs/zerolog/log" spotifyPkg "github.com/zmb3/spotify/v2" diff --git a/convert/spotify_tidal.go b/convert/spotify_tidal.go index dc15100..4b2b2dd 100644 --- a/convert/spotify_tidal.go +++ b/convert/spotify_tidal.go @@ -8,12 +8,11 @@ import ( "strings" "time" - "github.com/zibbp/spotify-playlist-convert/config" - "github.com/zibbp/spotify-playlist-convert/db" - "github.com/zibbp/spotify-playlist-convert/navidrome" - "github.com/zibbp/spotify-playlist-convert/spotify" - "github.com/zibbp/spotify-playlist-convert/tidal" - "github.com/zibbp/spotify-playlist-convert/utils" + "github.com/zibbp/spotify-playlist-sync/config" + "github.com/zibbp/spotify-playlist-sync/db" + "github.com/zibbp/spotify-playlist-sync/navidrome" + "github.com/zibbp/spotify-playlist-sync/spotify" + "github.com/zibbp/spotify-playlist-sync/tidal" libSpotify "github.com/zmb3/spotify/v2" "github.com/rs/zerolog/log" @@ -36,7 +35,8 @@ func Initialize(spotifyService *spotify.Service, tidalService *tidal.Service, co return &s, nil } -func (s *Service) SpotifyToTidal() error { +// SpotifyToTidal converts a user's Spotify playlists to Tidal playlists. +func (s *Service) SpotifyToTidal(saveMissingTracks bool, saveTidalPlaylist bool, saveNavidromePlaylist bool) error { log.Info().Msg("Starting Spotify to Tidal conversion") // get all playlists from Spotify @@ -197,50 +197,70 @@ func (s *Service) SpotifyToTidal() error { } // write missing tracks to file - if len(missingTracks) > 0 { + if saveMissingTracks && (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) + err := spotify.WriteMissingTracks(fmt.Sprintf("%s", spotifyPlaylist.ID), spotify.MissingTracks{ + Playlist: spotifyPlaylist, + Tracks: missingTracks, + }) if err != nil { return err } } - // fetch fresh tidal playlist to save to disk - tPlaylist, err := s.TidalService.GetPlaylist(tidalPlaylist.UUID) - if err != nil { - return err - } - tPlaylistTracks, err := s.TidalService.GetPlaylistTracks(tidalPlaylist.UUID) - if err != nil { - return err - } - for _, tidalTrack := range tPlaylistTracks.Items { - tPlaylist.Tracks = append(tPlaylist.Tracks, tidalTrack) - } - err = utils.WriteTidalPlaylist(fmt.Sprintf("%s", tPlaylist.UUID), tPlaylist) - if err != nil { - return err + if saveTidalPlaylist { + // fetch fresh tidal playlist to save to disk + tPlaylist, err := s.TidalService.GetPlaylist(tidalPlaylist.UUID) + if err != nil { + return err + } + tPlaylistTracks, err := s.TidalService.GetPlaylistTracks(tidalPlaylist.UUID) + if err != nil { + return err + } + for _, tidalTrack := range tPlaylistTracks.Items { + tPlaylist.Tracks = append(tPlaylist.Tracks, tidalTrack) + } + err = tidal.WriteTidalPlaylist(fmt.Sprintf("%s", tPlaylist.UUID), tPlaylist) + if err != nil { + return err + } } - navidromePlaylist := navidrome.Playlist{ - Name: spotifyPlaylist.Name, - Description: spotifyPlaylist.Description, - } + if saveNavidromePlaylist { + tPlaylist, err := s.TidalService.GetPlaylist(tidalPlaylist.UUID) + if err != nil { + return err + } - for _, track := range tPlaylist.Tracks { - navidromePlaylist.Tracks = append(navidromePlaylist.Tracks, navidrome.Track{ - ID: strconv.FormatInt(track.ID, 10), - Title: track.Title, - Album: track.Album.Title, - Artist: track.Artist.Name, - Duration: track.Duration, - ISRC: track.Isrc, - }) - } + tPlaylistTracks, err := s.TidalService.GetPlaylistTracks(tidalPlaylist.UUID) + if err != nil { + return err + } + for _, tidalTrack := range tPlaylistTracks.Items { + tPlaylist.Tracks = append(tPlaylist.Tracks, tidalTrack) + } - err = navidrome.WriteNavidromePlaylist(fmt.Sprintf("%s", tPlaylist.UUID), navidromePlaylist) - if err != nil { - return err + navidromePlaylist := navidrome.Playlist{ + Name: spotifyPlaylist.Name, + Description: spotifyPlaylist.Description, + } + + for _, track := range tPlaylist.Tracks { + navidromePlaylist.Tracks = append(navidromePlaylist.Tracks, navidrome.Track{ + ID: strconv.FormatInt(track.ID, 10), + Title: track.Title, + Album: track.Album.Title, + Artist: track.Artist.Name, + Duration: track.Duration, + ISRC: track.Isrc, + }) + } + + err = navidrome.WriteNavidromePlaylist(fmt.Sprintf("%s", tPlaylist.UUID), navidromePlaylist) + if err != nil { + return err + } } } diff --git a/deezer/arl.go b/deezer/arl.go deleted file mode 100644 index 1f9e5d9..0000000 --- a/deezer/arl.go +++ /dev/null @@ -1,123 +0,0 @@ -package deezer - -import ( - "bytes" - "fmt" - "io" - "log" - "net/http" - "strings" - - "github.com/tidwall/gjson" -) - -type DeezerARL struct { - Plan string - Date string - ARL string -} - -type DeezerUserData struct { - Lossless bool - Plan string - Country string -} - -func GetARLs() ([]DeezerARL, error) { - - // download string from url - url := "https://rentry.org/firehawk52/raw" - - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("error while downloading ARLs: %v", err) - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error while reading ARLs: %v", err) - } - - // Split the large string into lines - lines := strings.Split(string(body), "\n") - - var arls []DeezerARL - - // Iterate over each line - for _, line := range lines { - // Check if the line starts with "->![United States of America]" - if strings.HasPrefix(line, "->![United States of America]") { - if !strings.Contains(line, "Deezer") { - continue - } - - // 0: Country - // 1: Plan - // 2: Date - // 3: ARL - parts := strings.Split(line, "|") - if len(parts) < 2 { - continue - } - - arl := DeezerARL{ - Plan: strings.TrimSpace(strings.ReplaceAll(parts[1], "`", "")), - Date: strings.TrimSpace(strings.ReplaceAll(parts[2], "`", "")), - ARL: strings.TrimSpace(strings.ReplaceAll(parts[3], "`", "")), - } - - arls = append(arls, arl) - - } - } - - log.Print("Parsed ARLs:") - for _, arl := range arls { - log.Printf("Plan: %s, Date: %s, ARL: %s", arl.Plan, arl.Date, arl.ARL) - } - - return arls, nil -} - -func TestARL(arl string) (*DeezerUserData, error) { - - sessionData := `{"api_token": "null", "api_version": "1.0", "input": "3", "method": "deezer.getUserData"}` - - arlCookie := http.Cookie{Name: "arl", Value: arl} - - req, err := http.NewRequest("POST", "http://www.deezer.com/ajax/gw-light.php", bytes.NewBufferString(sessionData)) - if err != nil { - return nil, fmt.Errorf("error while creating request: %v", err) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.AddCookie(&arlCookie) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error while sending request: %v", err) - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error while reading response: %v", err) - } - - var userData DeezerUserData - - country := gjson.Get(string(bodyBytes), "results.COUNTRY") - lossless := gjson.Get(string(bodyBytes), "results.USER.OPTIONS.web_sound_quality.lossless") - plan := gjson.Get(string(bodyBytes), "results.OFFER_NAME") - if !country.Exists() || !lossless.Exists() || !plan.Exists() { - return nil, fmt.Errorf("error while parsing response: %v", err) - } - - userData.Country = country.String() - userData.Lossless = lossless.Bool() - userData.Plan = plan.String() - - return &userData, nil -} diff --git a/go.mod b/go.mod index dd34215..ecd9cfb 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/zibbp/spotify-playlist-convert +module github.com/zibbp/spotify-playlist-sync go 1.22 @@ -8,7 +8,6 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 github.com/rs/zerolog v1.32.0 github.com/sethvargo/go-envconfig v1.0.1 - github.com/tidwall/gjson v1.17.1 github.com/urfave/cli/v2 v2.27.1 github.com/zmb3/spotify/v2 v2.4.1 golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 @@ -20,8 +19,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index 89b99ae..b7fb504 100644 --- a/go.sum +++ b/go.sum @@ -135,12 +135,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= diff --git a/main.go b/main.go index 343a3ec..5ee655a 100644 --- a/main.go +++ b/main.go @@ -11,11 +11,11 @@ import ( _ "github.com/mattn/go-sqlite3" - "github.com/zibbp/spotify-playlist-convert/config" - "github.com/zibbp/spotify-playlist-convert/convert" - "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-sync/config" + "github.com/zibbp/spotify-playlist-sync/convert" + "github.com/zibbp/spotify-playlist-sync/db" + "github.com/zibbp/spotify-playlist-sync/spotify" + "github.com/zibbp/spotify-playlist-sync/tidal" "github.com/urfave/cli/v2" ) @@ -24,6 +24,9 @@ import ( var ddl string func initialize() (*config.Config, *config.JsonConfigService, *spotify.Service, *db.Queries) { + ctx := context.Background() + + // initialize config c, err := config.Init() if err != nil { log.Fatal().Err(err).Msg("Failed to load config") @@ -35,26 +38,26 @@ func initialize() (*config.Config, *config.JsonConfigService, *spotify.Service, log.Fatal().Err(err).Msg("Failed to open database") } - ctx := context.Background() - // create tables if _, err := dbConn.ExecContext(ctx, ddl); err != nil { log.Fatal().Err(err).Msg("Failed to create tables") } - queries := db.New(dbConn) + // load json config which has credentials jsonConfig := config.NewJsonConfigService("/data/config.json") err = jsonConfig.Init() if err != nil { log.Fatal().Err(err).Msg("Failed to load Spotify config") } + // initialize the spotify connection spotifyService, err := spotify.Initialize(c.SpotifyClientId, c.SpotifyClientSecret, c.SpotifyRedirectUri, jsonConfig) if err != nil { log.Fatal().Err(err).Msg("Failed to initialize Spotify service") } + // authenticate with spotify err = spotifyService.Authenticate() if err != nil { log.Fatal().Err(err).Msg("Failed to authenticate with Spotify") @@ -64,13 +67,34 @@ func initialize() (*config.Config, *config.JsonConfigService, *spotify.Service, } func main() { + var saveMissingTracks bool + var saveTidalPlaylist bool + var saveNavidromePlaylist bool + app := &cli.App{ - Name: "spotify-convert", - Usage: "convert spotify playlists to other services", + Name: "spotify-playlist-sync", + Usage: "sync spotify playlists to other services", Commands: []*cli.Command{ { Name: "tidal", - Usage: "convert playlists to tidal", + Usage: "sync playlists to tidal", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "save-missing-tracks", + Usage: "Save missing tracks during the conversion", + Destination: &saveMissingTracks, + }, + &cli.BoolFlag{ + Name: "save-tidal-playlist", + Usage: "Save the tidal playlist", + Destination: &saveTidalPlaylist, + }, + &cli.BoolFlag{ + Name: "save-navidrome-playlist", + Usage: "Save a version of the tidal playlist for importing in Navidrome", + Destination: &saveNavidromePlaylist, + }, + }, Action: func(cCtx *cli.Context) error { c, jsonConfigService, spotifyService, queries := initialize() @@ -86,13 +110,12 @@ func main() { } // convert - convertService, err := convert.Initialize(spotifyService, tidalService, jsonConfigService, queries) if err != nil { log.Fatal().Err(err).Msg("Failed to initialize convert service") } - err = convertService.SpotifyToTidal() + err = convertService.SpotifyToTidal(saveMissingTracks, saveTidalPlaylist, saveMissingTracks) if err != nil { log.Fatal().Err(err).Msg("Failed to convert Spotify to Tidal") } @@ -110,6 +133,11 @@ func main() { zerolog.SetGlobalLevel(zerolog.InfoLevel) } + container := os.Getenv("CONTAINER") + if container != "true" { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + } + if err := app.Run(os.Args); err != nil { log.Fatal().Err(err).Msg("Failed to run app") } diff --git a/spotify/spotify.go b/spotify/spotify.go index ba39657..00ddcfa 100644 --- a/spotify/spotify.go +++ b/spotify/spotify.go @@ -3,7 +3,7 @@ package spotify import ( "context" - "github.com/zibbp/spotify-playlist-convert/config" + "github.com/zibbp/spotify-playlist-sync/config" spotifyPkg "github.com/zmb3/spotify/v2" ) diff --git a/spotify/utils.go b/spotify/utils.go new file mode 100644 index 0000000..8b7ff59 --- /dev/null +++ b/spotify/utils.go @@ -0,0 +1,26 @@ +package spotify + +import ( + "encoding/json" + "fmt" + "os" + + libSpotify "github.com/zmb3/spotify/v2" +) + +type MissingTracks struct { + Playlist libSpotify.SimplePlaylist `json:"playlist"` + Tracks []*libSpotify.FullTrack `json:"tracks"` +} + +// WriteMissingTracks writes missing tracks Spotify playlist tracks to disk +func WriteMissingTracks(filename string, missingTracks MissingTracks) error { + if err := os.MkdirAll("/data/missing", 0755); err != nil { + return err + } + json, err := json.Marshal(missingTracks) + if err != nil { + return err + } + return os.WriteFile(fmt.Sprintf("/data/missing/%s.json", filename), json, 0644) +} diff --git a/tidal/tidal.go b/tidal/tidal.go index 9deb314..96c4edc 100644 --- a/tidal/tidal.go +++ b/tidal/tidal.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/zibbp/spotify-playlist-convert/config" + "github.com/zibbp/spotify-playlist-sync/config" "github.com/rs/zerolog/log" ) diff --git a/tidal/utils.go b/tidal/utils.go new file mode 100644 index 0000000..2832689 --- /dev/null +++ b/tidal/utils.go @@ -0,0 +1,19 @@ +package tidal + +import ( + "encoding/json" + "fmt" + "os" +) + +// WriteTidalPlaylist writes Tidal playlist to disk +func WriteTidalPlaylist(filename string, playlist *Playlist) error { + if err := os.MkdirAll("/data/tidal", 0755); err != nil { + return err + } + json, err := json.Marshal(playlist) + if err != nil { + return err + } + return os.WriteFile(fmt.Sprintf("/data/tidal/%s.json", filename), json, 0644) +} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index dee84df..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,35 +0,0 @@ -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) -} - -func WriteTidalPlaylist(filename string, playlist interface{}) error { - if err := os.MkdirAll("/data/tidal", 0755); err != nil { - return err - } - json, err := json.Marshal(playlist) - if err != nil { - return err - } - return os.WriteFile(fmt.Sprintf("/data/tidal/%s.json", filename), json, 0644) -}