Skip to content

Commit

Permalink
clean up for release
Browse files Browse the repository at this point in the history
  • Loading branch information
Zibbp committed Sep 16, 2024
1 parent e7da4a8 commit 07242f7
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 247 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
config.json
.env
vendor
dist/
31 changes: 17 additions & 14 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ 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
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}


jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -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
Expand All @@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
config.json
.env
vendor
vendor
dist/
47 changes: 47 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -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:"
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/<spotify_playlist_id>.json`.
- Save Tidal playlist writes the Tidal playlist to `/data/tidal/<tidal_playlist_id>.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
Expand Down
16 changes: 16 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion convert/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
102 changes: 61 additions & 41 deletions convert/spotify_tidal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}

}
Expand Down
Loading

0 comments on commit 07242f7

Please sign in to comment.