# Pushtunes

Pushtunes is a small tool to push your music from local sources (Subsonic-compatible server/Navidrome, a CSV file, etc.) to music streaming services. Currently only Spotify and YouTube Music are supported. See "Music streaming services" below for more.


## Installation

From PyPI using uv:

```bash
uv venv .venv
source .ven/bin/activate
uv pip install pushtunes
```

Or if using plain pip:

```
python3 -m venv .venv
source .venv/bin/activate
pip install pushtunes
```

### Installing from source instead

From the source directory, in a virtualenv, using uv:

```bash
uv venv .venv
source .venv/bin/activate
uv pip install .
```

Using plain old Python/pip:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install .
```

## Usage

Set your music service and source credentials (see below) and, for example:

```bash
# Push albums from Subsonic to Spotify
pushtunes push albums --from subsonic --to spotify

# Push individual tracks (starred/favorites) from Subsonic to Spotify
pushtunes push tracks --from subsonic --to spotify

# Push playlists from Subsonic to Spotify or YouTube Music
pushtunes push playlist --from subsonic --playlist-name=myplaylist --to spotify
pushtunes push playlist --from subsonic --playlist-name=myplaylist --to ytm

# Push from CSV file
pushtunes push tracks --from csv --csv-file=tracks.csv --to spotify
```

See `pushtunes --help`, `pushtunes push albums --help`, `pushtunes push tracks --help`, or `pushtunes push playlist --help` for more options.

### Mapping items that can't be found

It's very difficult to use unreliable metadata in your random assortment of audio files (well, sorry, just pointing it out) and map that to whatever the streaming services are using right this moment. What makes this even more challenging is that some artists changed names over the years or are known under different names depending on the region. Or there can be legal trouble when a band is not allowed to use its original name for a while, but then the rights revert to the band after some lawsuit. Depending on when you bought their music, they might no longer be known under that name to Spotify and YouTube Music.

So Pushtunes is in a bad position and has to do a lot of guessing. The similarity matching with `--similarity` can go some way to fixing this. But sometimes you just have stuff that doesn't match up. That's what the `--mappings-file` feature is for. See [Mappings](docs/Mappings.md) for how to use it.

### Exporting items based on their push status as CSV

You may want to export the result of an attempt to push some albums or tracks as a CSV file so you can retry just the failures, diagnose some issues, create a mappings file, etc. This can be done with `--export-csv`, see [Export-CSV](docs/Export-CSV.md) for more.


### Deleting items from target that aren't in the source

You can use the `--delete` option with `push albums` or `push tracks` to remove items from your target library that don't exist in your source. This is useful for keeping your streaming service library in sync with your local collection. Before any deletion occurs, Pushtunes will:

1. Show you exactly what will be deleted
2. Warn you about items that might be preserved with a lower similarity threshold
3. Create an automatic backup of everything to be deleted (with service IDs for exact restoration)
4. Ask for explicit confirmation before proceeding

```bash
# Sync Spotify library with Subsonic, removing albums not in Subsonic
pushtunes push albums --from subsonic --to spotify --delete
```

See [Deleting](docs/Deleting.md) for detailed information about the deletion workflow and safety features.


### Filtering

There is a very primitive filter language you can use to skip/filter out things you don't want pushed. It works like this:

```
field:'regex'
```

They can be chained together with comma and will be ORed.

**For albums:** The fields supported are `artist` and `album`. A real example:

```bash
pushtunes push albums --filter="artist:'Apocryphos, Kammarheit, Atrium Carceri', artist:'Black.*', album:'Sunset.*'" --from=csv --csv-file=test.csv  --to=spotify
```

This would skip the following things:

* All albums by the artist(s) called "Apocryphos, Kammarheit, Atrium Carceri" who did only one or two albums together
* Anything by "Black Hills", "Black Mountain", "Black Eyed Peas" and other artists starting with "Black" because of the regex `Black.*`
* In this particular case, the album "Sunset Mission" by "Bohren & der Club of Gore", but any album that starts with "Sunset" by any artist would be skipped using `Sunset.*`

**For tracks:** The fields supported are `artist`, `track`, and `album` (album is optional). Example:

```bash
pushtunes push tracks --filter="artist:'Volkor X', track:'.*Live.*'" --from=subsonic --to=spotify
```

This would skip tracks by "Volkor X" and any live tracks.

There's no way to combine with anything other than OR and there's no way to specify "skip this album but only by that artist". Sorry, I did say it's primitive!

### Similarity Matching

Pushtunes uses fuzzy matching to handle differences in how music services structure metadata. This applies to albums, tracks, and playlists.

**What gets matched:**

- **Case-insensitive**: "The Beatles" matches "the beatles"
- **Artist name variations**: "Artist A & Artist B" matches "Artist A, Artist B"
- **Featured artists**: Handles tracks where one service lists featured artists separately and another includes them in the title
- **Subset matching**: A track with artist "Perturbator" will match a track with artists "Perturbator, Greta Link" (similarity 0.95)
- **Soundtrack suffixes**: "Album Title (Original Soundtrack)" matches "Album Title"
- **Remaster handling**: By default, remasters are kept separate from originals to avoid accidentally replacing your original album with a remastered version

**Adjusting the threshold:**

The `--similarity` option controls how strict the matching is (default 0.8):

```bash
# Stricter matching (0.9 = 90% similar)
pushtunes push tracks --from subsonic --to spotify --similarity=0.9

# More lenient matching (0.7 = 70% similar)
pushtunes push albums --from csv --csv-file=albums.csv --to ytm --similarity=0.7
```

A higher value means tracks/albums must be more similar to match. If you're getting too many false matches, increase the threshold. If valid matches are being rejected, decrease it.

### Playlists

Pushtunes can push playlists between services and sources, preserving track order and handling conflicts intelligently.

**Basic usage:**

```bash
# Push a playlist by name
pushtunes push playlist --from subsonic --playlist-name=myplaylist --to spotify
```

**Handling conflicts:**

When a playlist with the same name already exists on the target service, you can control the behavior with `--on-conflict`:

- **`abort`** (default): Show differences and exit without making changes
  ```bash
  pushtunes push playlist --from subsonic --playlist-name=chill --to ytm --on-conflict=abort
  ```
  This will display:
  - Number of tracks in the existing playlist vs. source
  - Tracks in common
  - Tracks that would be added
  - Tracks that would be removed

- **`replace`**: Replace the entire playlist with tracks from source
  ```bash
  pushtunes push playlist --from subsonic --playlist-name=workout --to spotify --on-conflict=replace
  ```
  All existing tracks are removed and replaced with the source playlist.

- **`append`**: Add only missing tracks to the existing playlist
  ```bash
  pushtunes push playlist --from subsonic --playlist-name=favorites --to ytm --on-conflict=append
  ```
  Existing tracks are preserved, new tracks are added at the end.

- **`sync`**: Fully synchronize the playlist (add missing, remove extras)
  ```bash
  pushtunes push playlist --from subsonic --playlist-name=discover --to spotify --on-conflict=sync
  ```
  The target playlist will be made identical to the source, maintaining the same track order.

**Spotify-specific: Using playlist IDs**

Spotify allows duplicate playlist names, which can cause conflicts. For Spotify, you can target a specific playlist using its ID instead of relying on name matching:

```bash
# First push creates the playlist - note the Playlist ID in output
pushtunes push playlist --from subsonic --playlist-name=argon --to spotify
# Output: Playlist ID: 59EkFxUZG0CnApHQIgYVUK

# Subsequent pushes using the ID
pushtunes push playlist --from subsonic --playlist-name=argon --to spotify \
  --playlist-id=59EkFxUZG0CnApHQIgYVUK --on-conflict=sync
```

You can find playlist IDs in the Spotify web player URL or from the previous push output.

**Verbose output:**

Use `-v` or `--verbose` to see detailed track matching results and conflict information:

```bash
pushtunes push playlist --from subsonic --playlist-name=jazz --to spotify -v
```

## Music sources

### Subsonic (including Navidrome, etc.)

Set your Subsonic server's URL and credentials:

```
export SUBSONIC_URL=https://your-music.example.com
export SUBSONIC_USER=yourusername
export SUBSONIC_PASS="correct horse battery staple"
```

You are now ready to use the `--from=subsonic` source.

**Note on tracks:** When using `push tracks --from=subsonic`, pushtunes fetches your starred/favorite tracks from the Subsonic server using the `getStarred2` API endpoint.

### Jellyfin

Set your Jellyfin server's URL and credentials:

```
export JELLYFIN_URL=https://jellyfin.example.com
export JELLYFIN_USER=yourusername
export JELLYFIN_PASS="correct horse battery staple"
```

You are now ready to use the `--from=jellyfin` source.


### CSV files

You can import albums or tracks from CSV files.

**Album CSV format:**
```csv
artist,album,year
"Carbon Based Lifeforms","Interloper",2010
"Igorrr","Spirituality and Distortion",2020
```

For albums with multiple artists, you can use `artist2`, `artist3`, etc. columns (up to `artist10`):
```csv
artist,artist2,artist3,album,year
"Apocryphos","Kammarheit","Atrium Carceri","Echo",2016
```

**Track CSV format:**
```csv
artist,track,album,year
"Dire Straits","Sultans of Swing","Dire Straits",1978
"Thy Catafalque","Molekuláris Gépezetek","Róka Hasa Rádió",2009
```

For tracks, the `album` and `year` columns are optional. The CSV format supports only a single artist per track (the main artist).

## Music streaming services

The streaming service market is in a very sad state of affairs regarding APIs. Spotify has a good one, YouTube Music is almost unusable and working with it is only possible thanks to the people who maintain the unofficial ytmusicapi library. Deezer and Qobuz don't allow anyone to use their API anymore, requests for API keys go unanswered, documentation is being deleted and no up to date libraries exist. Tidal might still allow people to use the API but the Python support is incomplete and lacking the functions Pushtunes needs.

That's why realistically, YTM and Spotify are your choices. Here's how to authorize Pushtunes for them.

### YouTube Music

In the same virtualenv where you've installed Pushtunes, you'll have the tool `ytmusicapi`. The only currently working authorization is via intercepted browser headers, what a great deal of fun. Try the following:

```bash
ytmusicapi browser
```

Then, in your browser (Firefox as an example):

1. Browse to https://music.youtube.com
2. Log in so you can go to your YTM library
3. Click in the search field ("Search songs, albums, artists, podcasts")
4. Open the browser tools so you can see the network requests (Ctrl-Shift-I and click the "Network" tab)
5. Search for some words, doesn't matter what you search for. Submit the search with enter
6. You should see the POST request for "search" in the network tab. Right-click it, select "Copy value" -> "Copy request headers"
7. This is what you paste into the console, finishing with enter and then Ctrl-D

This will generate a browser.json file with your authorization. Note that this will expire after a short time (30 minutes?) so you'll have to do this again before you use Pushtunes again. You should now be ready to push some tunes.

### Spotify

On the [Spotify developer dashboard](https://developer.spotify.com/dashboard), create a new application. If it asks for redirect URIs (they are not important), try something like this:

```
http://127.0.0.1:8080
http://127.0.0.1:8080/callback
http://127.0.0.1:8080/
```

You will receive a client ID and client secret. Set them in the same shell where you're running Pushtunes:

```
export SPOTIFY_CLIENT_ID=your-client-id-here
export SPOTIFY_CLIENT_SECRET=your-client-secret-here
```

You should now be ready to push some tunes.
