Skip to content

Add Spotify and Deezer playlist import support#33

Merged
Blueion76 merged 4 commits into
devfrom
copilot/add-spotify-deezer-importer
Mar 10, 2026
Merged

Add Spotify and Deezer playlist import support#33
Blueion76 merged 4 commits into
devfrom
copilot/add-spotify-deezer-importer

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 10, 2026

  • Create octogen/api/spotify.py with SpotifyImporter class
  • Create octogen/api/deezer.py with DeezerImporter class
  • Update requirements.txt with spotipy>=2.24.0 and deezer-python>=0.10.0
  • Update octogen/config.py to add Spotify and Deezer config blocks
  • Update octogen/main.py to import, initialize, and run Spotify/Deezer importers
  • Update ENV_VARS.md to document new environment variables
  • Rename _extract_playlist_idextract_playlist_id (public) in both importer classes
  • Wrap importer constructors in try/except (ImportError + Exception) for graceful degradation
  • Include Spotify/Deezer in music-source gating check (_validate_env_config + runtime guard)
  • Add playlist_urls validation for Spotify and Deezer when enabled
  • Update docs (ENV_VARS.md) to consistently list all 6 music sources
Original prompt

Overview

Add support for importing playlists from Spotify and Deezer into Navidrome via OctoGen. This feature should follow the same architectural patterns already established in the project (e.g. how LastFMAPI and ListenBrainzAPI are integrated as optional services).

The existing codebase structure (on the dev branch) to match:

  • octogen/api/ — API client modules (e.g. lastfm.py, listenbrainz.py, navidrome.py)
  • octogen/main.pyOctoGenEngine class where all services are initialized and run
  • octogen/config.py — env-var-based config loading
  • octogen/models/config_models.py — Pydantic config models
  • requirements.txt — Python dependencies

New Files to Create

octogen/api/spotify.py

Create a SpotifyImporter class that:

  • Uses the Spotipy library (spotipy) with SpotifyClientCredentials OAuth2 for authentication
  • Provides a get_playlist_tracks(playlist_id_or_url: str) -> List[Dict] method
    • Accepts a Spotify playlist URL (e.g. https://open.spotify.com/playlist/...) or bare playlist ID
    • Extracts the playlist ID from the URL if needed
    • Returns a list of dicts: [{"artist": "...", "title": "..."}]
    • Handles pagination (Spotify returns max 100 tracks per page)
    • Skips tracks where artist or title is missing/empty
  • Provides a get_user_playlists(limit: int = 20) -> List[Dict] method (future use / listing)
  • Has a check_connection() -> bool method that verifies credentials work
  • Graceful error handling with appropriate logger.warning/error calls
  • Must NOT require user OAuth — use client credentials flow only (app-level auth, read public playlists)

Dependencies: spotipy>=2.24.0

octogen/api/deezer.py

Create a DeezerImporter class that:

  • Uses the deezer-python library (deezer-python) — no auth required for public playlists
  • Provides a get_playlist_tracks(playlist_id_or_url: str) -> List[Dict] method
    • Accepts a Deezer playlist URL (e.g. https://www.deezer.com/playlist/...) or bare playlist ID (integer or string)
    • Extracts the playlist ID from the URL if needed
    • Returns a list of dicts: [{"artist": "...", "title": "..."}]
    • Handles the deezer-python pagination/generator if applicable
    • Skips tracks where artist or title is missing
  • Has a check_connection() -> bool method
  • Graceful error handling

Dependencies: deezer-python>=0.10.0


Files to Modify

requirements.txt

Add the two new dependencies:

spotipy>=2.24.0
deezer-python>=0.10.0

octogen/config.py

In load_config_from_env(), add Spotify and Deezer config blocks:

"spotify": {
    "enabled": os.getenv("SPOTIFY_IMPORT_ENABLED", "false").lower() == "true",
    "client_id": load_secret("SPOTIFY_CLIENT_ID"),
    "client_secret": load_secret("SPOTIFY_CLIENT_SECRET"),
    "playlist_urls": os.getenv("SPOTIFY_PLAYLIST_URLS", ""),  # comma-separated
},
"deezer": {
    "enabled": os.getenv("DEEZER_IMPORT_ENABLED", "false").lower() == "true",
    "playlist_urls": os.getenv("DEEZER_PLAYLIST_URLS", ""),  # comma-separated
},

octogen/main.py

In the OctoGenEngine class:

  1. Imports: Add imports for SpotifyImporter and DeezerImporter at the top.

  2. _load_config_from_env(): Add spotify and deezer keys to the config dict, mirroring the pattern used for lastfm and audiomuse:

"spotify": {
    "enabled": self._get_env_bool("SPOTIFY_IMPORT_ENABLED", False),
    "client_id": os.getenv("SPOTIFY_CLIENT_ID", ""),
    "client_secret": os.getenv("SPOTIFY_CLIENT_SECRET", ""),
    "playlist_urls": os.getenv("SPOTIFY_PLAYLIST_URLS", ""),
},
"deezer": {
    "enabled": self._get_env_bool("DEEZER_IMPORT_ENABLED", False),
    "playlist_urls": os.getenv("DEEZER_PLAYLIST_URLS", ""),
},
  1. __init__(): Initialize self.spotify_importer and self.deezer_importer similarly to how self.lastfm and self.listenbrainz are initialized:
self.spotify_importer: Optional[SpotifyImporter] = None
if self.config.get("spotify", {}).get("enabled", False):
    self.spotify_importer = SpotifyImporter(
        client_id=self.config["spotify"]["client_id"],
        client_secret=self.config["spotify"]["client_secret"],
    )
    if not self.spotify_importer.check_connection():
        logger.warning("⚠️ Spotify credentials invalid or connection failed")
        self.spotify_importer = None
    else:
        logger.info("✅ Spotify importer connected")

self.deezer_importer: Optional[DeezerImporter] = None
if self.config.get("deezer", {}).get("enabled", False):
    self.deezer_importer = DeezerImporter()
    if not self.deezer_importer.check_connection():
        logger.warning("⚠️ Deezer connection failed")
        self.deezer_importer = None
    else:
        logger.info("✅ Deezer importer connected")
  1. _validate_env_config(): Add Spotify validation — if SPOTIFY_IMPORT_ENABLED=true but SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET is missing, append an er...

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits March 10, 2026 19:30
Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for importing playlists from Spotify and Deezer Add Spotify and Deezer playlist import support Mar 10, 2026
@Blueion76 Blueion76 marked this pull request as ready for review March 10, 2026 19:35
Copilot AI review requested due to automatic review settings March 10, 2026 19:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional Spotify and Deezer playlist importers that fetch public playlist tracks and feed them into OctoGen’s existing create_playlist pipeline, plus corresponding env/config/docs updates.

Changes:

  • Introduces SpotifyImporter (Spotipy client-credentials) and DeezerImporter (deezer-python) for public playlist imports.
  • Extends OctoGenEngine env config + run loop to optionally import playlists from comma-separated URL lists.
  • Updates requirements and environment-variable documentation for the new import features.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
requirements.txt Adds Spotipy + deezer-python dependencies.
octogen/api/spotify.py New Spotify importer with pagination and field filtering.
octogen/api/deezer.py New Deezer importer with URL parsing and track extraction.
octogen/main.py Wires importers into config loading, validation, initialization, run loop, and service summaries.
octogen/config.py Adds spotify/deezer blocks to the (non-main) env loader for parity.
ENV_VARS.md Documents new env vars and updates the summary table + “music source” note.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread octogen/main.py
Comment on lines +1325 to +1328
if tracks:
playlist_id = self.spotify_importer._extract_playlist_id(playlist_url)
playlist_name = f"Spotify: {playlist_id}"
self.create_playlist(playlist_name, tracks, max_songs=100)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid calling the importer's private method _extract_playlist_id from outside the class. Expose a public helper (e.g., extract_playlist_id) or have get_playlist_tracks return the normalized playlist_id alongside tracks, so OctoGenEngine doesn't depend on private implementation details.

Copilot uses AI. Check for mistakes.
Comment thread octogen/main.py
Comment on lines +1350 to +1353
if tracks:
playlist_id = self.deezer_importer._extract_playlist_id(playlist_url)
playlist_name = f"Deezer: {playlist_id}"
self.create_playlist(playlist_name, tracks, max_songs=100)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid calling the importer's private method _extract_playlist_id from outside the class. Expose a public helper (e.g., extract_playlist_id) or have get_playlist_tracks return the normalized playlist_id alongside tracks, so OctoGenEngine doesn't depend on private implementation details.

Copilot uses AI. Check for mistakes.
Comment thread ENV_VARS.md
| **Total** | **60** | |

**Note**: At least one music source must be configured: LLM, AudioMuse-AI, Last.fm, or ListenBrainz.
**Note**: At least one music source must be configured: LLM, AudioMuse-AI, Last.fm, ListenBrainz, Spotify Import, or Deezer Import.
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note here says Spotify Import/Deezer Import count as acceptable "music sources", but octogen/main.py currently treats only LLM/AudioMuse/Last.fm/ListenBrainz as sources and will exit early if those aren't enabled. Either update the code to include Spotify/Deezer in the source checks, or adjust this documentation to match actual behavior.

Copilot uses AI. Check for mistakes.
Comment thread octogen/main.py Outdated
Comment on lines +152 to +169
self.spotify_importer = SpotifyImporter(
client_id=self.config["spotify"]["client_id"],
client_secret=self.config["spotify"]["client_secret"],
)
if not self.spotify_importer.check_connection():
logger.warning("⚠️ Spotify credentials invalid or connection failed")
self.spotify_importer = None
else:
logger.info("✅ Spotify importer connected")

self.deezer_importer: Optional[DeezerImporter] = None
if self.config.get("deezer", {}).get("enabled", False):
self.deezer_importer = DeezerImporter()
if not self.deezer_importer.check_connection():
logger.warning("⚠️ Deezer connection failed")
self.deezer_importer = None
else:
logger.info("✅ Deezer importer connected")
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SpotifyImporter/DeezerImporter constructors can raise ImportError when optional dependencies aren't installed. Because this initialization isn't wrapped in try/except, enabling Spotify/Deezer will crash the whole app instead of gracefully disabling the importer as the surrounding pattern implies. Catch ImportError (and optionally other init errors), log a warning, and keep the importer as None.

Suggested change
self.spotify_importer = SpotifyImporter(
client_id=self.config["spotify"]["client_id"],
client_secret=self.config["spotify"]["client_secret"],
)
if not self.spotify_importer.check_connection():
logger.warning("⚠️ Spotify credentials invalid or connection failed")
self.spotify_importer = None
else:
logger.info("✅ Spotify importer connected")
self.deezer_importer: Optional[DeezerImporter] = None
if self.config.get("deezer", {}).get("enabled", False):
self.deezer_importer = DeezerImporter()
if not self.deezer_importer.check_connection():
logger.warning("⚠️ Deezer connection failed")
self.deezer_importer = None
else:
logger.info("✅ Deezer importer connected")
try:
spotify_importer = SpotifyImporter(
client_id=self.config["spotify"]["client_id"],
client_secret=self.config["spotify"]["client_secret"],
)
except ImportError as e:
logger.warning(
"⚠️ Spotify importer disabled: missing optional dependency (%s)",
e,
)
except Exception as e:
logger.warning(
"⚠️ Spotify importer disabled due to initialization error: %s",
e,
)
else:
if not spotify_importer.check_connection():
logger.warning("⚠️ Spotify credentials invalid or connection failed")
self.spotify_importer = None
else:
self.spotify_importer = spotify_importer
logger.info("✅ Spotify importer connected")
self.deezer_importer: Optional[DeezerImporter] = None
if self.config.get("deezer", {}).get("enabled", False):
try:
deezer_importer = DeezerImporter()
except ImportError as e:
logger.warning(
"⚠️ Deezer importer disabled: missing optional dependency (%s)",
e,
)
except Exception as e:
logger.warning(
"⚠️ Deezer importer disabled due to initialization error: %s",
e,
)
else:
if not deezer_importer.check_connection():
logger.warning("⚠️ Deezer connection failed")
self.deezer_importer = None
else:
self.deezer_importer = deezer_importer
logger.info("✅ Deezer importer connected")

Copilot uses AI. Check for mistakes.
Comment thread octogen/main.py
if not self.config["spotify"].get("client_id"):
errors.append("Spotify import is enabled but SPOTIFY_CLIENT_ID is empty")
if not self.config["spotify"].get("client_secret"):
errors.append("Spotify import is enabled but SPOTIFY_CLIENT_SECRET is empty")
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Spotify import is enabled, config validation currently only checks credentials, but not that SPOTIFY_PLAYLIST_URLS is set. Also, Spotify/Deezer imports are not counted as valid "music sources" in the earlier source check in this function, so enabling only these imports will still trigger the "No music source configured" exit. Add validation for playlist_urls (and Deezer's playlist_urls) and include Spotify/Deezer in the music-source gating logic.

Suggested change
errors.append("Spotify import is enabled but SPOTIFY_CLIENT_SECRET is empty")
errors.append("Spotify import is enabled but SPOTIFY_CLIENT_SECRET is empty")
if not self.config["spotify"].get("playlist_urls"):
errors.append("Spotify import is enabled but SPOTIFY_PLAYLIST_URLS is empty")

Copilot uses AI. Check for mistakes.
Comment thread octogen/main.py
Comment on lines +1311 to +1316
# --- SPOTIFY IMPORT ---
if should_generate_regular and self.spotify_importer:
logger.info("=" * 70)
logger.info("SPOTIFY PLAYLIST IMPORT")
logger.info("=" * 70)
try:
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Spotify import block may never run when Spotify/Deezer are the only enabled sources: earlier in run() the "No playlists generated and no alternative services configured" guard does not consider spotify_importer/deezer_importer and exits before reaching here. Update that guard to include these importers so playlist import can function independently of AI/Last.fm/ListenBrainz/AudioMuse.

Copilot uses AI. Check for mistakes.
@Blueion76
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

…ource gating, URL validation

Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 10, 2026

@copilot apply changes based on the comments in this thread

All review feedback addressed in commit 526c5af:

  1. Private method → public: _extract_playlist_id renamed to extract_playlist_id in both SpotifyImporter and DeezerImporter; all call sites updated.
  2. Graceful init: Importer constructors are now wrapped in try/except ImportError + except Exception so a missing optional dependency or any other init error logs a warning and leaves the importer as None instead of crashing.
  3. Music-source gating: Spotify and Deezer are now counted as valid music sources in _validate_env_config (no more "No music source configured" exit when only import services are enabled) and in the runtime guard in run().
  4. playlist_urls validation: Added checks so enabling Spotify or Deezer without setting their _PLAYLIST_URLS variable produces a clear config error.
  5. Docs: Both ENV_VARS.md references (AI_API_KEY notes + summary table) now consistently list all 6 music sources.

@Blueion76 Blueion76 merged commit b18eb7d into dev Mar 10, 2026
@Blueion76 Blueion76 deleted the copilot/add-spotify-deezer-importer branch March 10, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants