Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/on-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: on-push
on: pull_request

jobs:
lint:
name: python
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- run: uv sync

- run: uv run ruff check .
64 changes: 64 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: "Publish"

on:
release:
types:
- published

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5

- name: "Set up Python"
uses: actions/setup-python@v6
with:
python-version-file: ".python-version"

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Set package version from tag
env:
VERSION_UNFORMATTED: ${{ github.event.release.tag_name }}
run: |
VERSION="${VERSION_UNFORMATTED#v}"
echo "Bumping version to $VERSION"
uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "$VERSION"

- name: Build
run: uv build

- name: Upload distribution
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
if-no-files-found: error

publish:
runs-on: ubuntu-latest
needs: build
environment:
name: release
url: https://pypi.org/p/xspfify
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Download distribution
uses: actions/download-artifact@v4
with:
name: dist
path: dist

# https://github.com/marketplace/actions/pypi-publish
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
output/
.creds
*.zip

# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,windows,linux,macos,python,jetbrains+all
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,windows,linux,macos,python,jetbrains+all
Expand Down
97 changes: 62 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
[XSPF](https://xspf.org/) generator

## Commands

```
# Running locally
uv run python -m main
# Linting
uv run ruff format
```

## Old Development Notes:

Make sure python 3.0 or later is installed on your machine

Visit https://developer.spotify.com/console/get-playlist-tracks/ and obtain an OAUTH token for your Spotify account. These last a limited amount of time (1 hour?) but you can get another one if it expires

- in a text editor, open the xspfify/main.py file and edit the following values:

- set the value of OAUTH_TOKEN to the value obtained above
- set the value of SPOTIFY_USERNAME to your username (otherwise you'll end up with copies of my playlists!)
- set the value of OUTPUT_PATH to a directory path that exists on your computer

- open the console and navigate to the xspfify directory, then run the following commands, one at a time:
> python3 -m venv venv
> source venv/bin/activate
> (venv)> easy_install requests
> (venv)> python
>
> > > import main
> > > main.backup_playlists_to_xspf()

XSPF is an open source format for portable playlists. (https://www.xspf.org/)

This script produces a minimal playlist with just artist name and song title, for maximum compatibility. It should be easy to add additional fields to the output, by modifying the get_basic_track_details() method.
# XSPF generator

CLI to export your Spotify playlists to [XSPF](https://xspf.org/) files for portable backups.

## Prerequisites

- Python 3.12+
- [uv](https://docs.astral.sh/uv/) for dependency management and virtualenvs
- A Spotify application (create one in https://developer.spotify.com/dashboard) to obtain a Client ID, Client Secret, and Redirect URL

## Getting started

1) Install uv if you do not already have it:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2) Install dependencies into a uv-managed virtual environment:
```bash
uv sync
```
3) Run the exporter (uv will activate the env automatically):
```bash
uv run python -m xspfify.main -o output
```
- You will be prompted for your Spotify Client ID, Client Secret, and Redirect URL. The redirect URL must match one configured for your Spotify app (e.g. `http://127.0.0.1:8000/callback`).
- Playlists will be written as `.xspf` files into the folder provided by `-o` (defaults to `./output`).

## Package for pipx

1. Build distributable artifacts with uv:
```bash
uv build
```
This creates `dist/*.whl` and `dist/*.tar.gz` that pipx can consume.
2. Share one of the artifacts (or host it in an index such as PyPI).
3. On another machine, install and run the CLI with pipx:
```bash
pipx install /path/to/dist/xspfify-0.1.0-py3-none-any.whl
xspfify -o output
```
Replace the path with the actual wheel location or git/URL you published. Because `pyproject.toml` defines a console script entry point, pipx will expose the `xspfify` command automatically.

## Development workflows

- Format / lint:
```bash
uv run ruff format .
uv run ruff check .
```
- Add new dependencies:
```bash
uv add package-name
uv add --group dev dev-package-name
```
- Update all locked dependencies:
```bash
uv lock --upgrade
```

## Notes

The generated playlists include artist, album, and track title for broad compatibility. Extend the payload by updating the `get_playlist_tracks` pipeline in `spotify_utils.py` and `playlist_from_spotify_items` in `xspf_utils.py`.
22 changes: 21 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "xspfify"
version = "0.1.0"
version = "0.0.1"
description = "CLI tool to export spotify playlists to flat files"
readme = "README.md"
requires-python = ">=3.12"
Expand All @@ -10,7 +14,23 @@ dependencies = [
"xspf-lib>=0.3.4",
]

[project.urls]
homepage = "https://github.com/aaronclong/xspfify"

[project.scripts]
xspfify = "xspfify.main:main"

[dependency-groups]
dev = [
"ruff>=0.14.5",
]

[tool.hatch.build.targets.wheel]
packages = ["src/xspfify"]

[tool.hatch.build.targets.sdist]
include = [
"src/xspfify",
"README.md",
"LICENSE",
]
Empty file added src/xspfify/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion core_utils.py → src/xspfify/core_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from getpass import getpass
from pathlib import Path

from dtos import SpotifyClientAuth
from .dtos import SpotifyClientAuth


def setup_logger():
Expand Down
File renamed without changes.
15 changes: 9 additions & 6 deletions main.py → src/xspfify/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
# -*- coding: utf-8 -*-
from pathlib import Path

from pathvalidate import sanitize_filename
from spotipy import Spotify

import core_utils
import xspf_utils
from spotify_utils import authenticate_spotify, get_playlist_tracks, get_playlists
from pathvalidate import sanitize_filename
from spotipy import Spotify

from . import core_utils, xspf_utils
from .spotify_utils import (
authenticate_spotify,
get_playlist_tracks,
get_playlists,
)

logger = core_utils.setup_logger()
parser = core_utils.create_arg_parser()
Expand Down
2 changes: 1 addition & 1 deletion spotify_utils.py → src/xspfify/spotify_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from spotipy import Spotify
from spotipy.oauth2 import SpotifyOAuth

from dtos import SpotifyClientAuth, TrackInfo
from .dtos import SpotifyClientAuth, TrackInfo


def authenticate_spotify(
Expand Down
2 changes: 1 addition & 1 deletion xspf_utils.py → src/xspfify/xspf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from xspf_lib import Playlist, Track

from dtos import TrackInfo
from .dtos import TrackInfo


def spotify_item_to_track(item: TrackInfo) -> Track:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.