Add Spotify and Deezer playlist import support#33
Conversation
Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
There was a problem hiding this comment.
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) andDeezerImporter(deezer-python) for public playlist imports. - Extends
OctoGenEngineenv 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.
| 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) |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| | **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. |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| 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") |
| 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") |
There was a problem hiding this comment.
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.
| 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") |
| # --- SPOTIFY IMPORT --- | ||
| if should_generate_regular and self.spotify_importer: | ||
| logger.info("=" * 70) | ||
| logger.info("SPOTIFY PLAYLIST IMPORT") | ||
| logger.info("=" * 70) | ||
| try: |
There was a problem hiding this comment.
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 apply changes based on the comments in this thread |
…ource gating, URL validation Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com>
All review feedback addressed in commit
|
octogen/api/spotify.pywithSpotifyImporterclassoctogen/api/deezer.pywithDeezerImporterclassrequirements.txtwithspotipy>=2.24.0anddeezer-python>=0.10.0octogen/config.pyto add Spotify and Deezer config blocksoctogen/main.pyto import, initialize, and run Spotify/Deezer importersENV_VARS.mdto document new environment variables_extract_playlist_id→extract_playlist_id(public) in both importer classes_validate_env_config+ runtime guard)playlist_urlsvalidation for Spotify and Deezer when enabledOriginal 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
LastFMAPIandListenBrainzAPIare integrated as optional services).The existing codebase structure (on the
devbranch) to match:octogen/api/— API client modules (e.g.lastfm.py,listenbrainz.py,navidrome.py)octogen/main.py—OctoGenEngineclass where all services are initialized and runoctogen/config.py— env-var-based config loadingoctogen/models/config_models.py— Pydantic config modelsrequirements.txt— Python dependenciesNew Files to Create
octogen/api/spotify.pyCreate a
SpotifyImporterclass that:spotipy) withSpotifyClientCredentialsOAuth2 for authenticationget_playlist_tracks(playlist_id_or_url: str) -> List[Dict]methodhttps://open.spotify.com/playlist/...) or bare playlist ID[{"artist": "...", "title": "..."}]get_user_playlists(limit: int = 20) -> List[Dict]method (future use / listing)check_connection() -> boolmethod that verifies credentials worklogger.warning/errorcallsDependencies:
spotipy>=2.24.0octogen/api/deezer.pyCreate a
DeezerImporterclass that:deezer-python) — no auth required for public playlistsget_playlist_tracks(playlist_id_or_url: str) -> List[Dict]methodhttps://www.deezer.com/playlist/...) or bare playlist ID (integer or string)[{"artist": "...", "title": "..."}]check_connection() -> boolmethodDependencies:
deezer-python>=0.10.0Files to Modify
requirements.txtAdd the two new dependencies:
octogen/config.pyIn
load_config_from_env(), add Spotify and Deezer config blocks:octogen/main.pyIn the
OctoGenEngineclass:Imports: Add imports for
SpotifyImporterandDeezerImporterat the top._load_config_from_env(): Addspotifyanddeezerkeys to the config dict, mirroring the pattern used forlastfmandaudiomuse:__init__(): Initializeself.spotify_importerandself.deezer_importersimilarly to howself.lastfmandself.listenbrainzare initialized:_validate_env_config(): Add Spotify validation — ifSPOTIFY_IMPORT_ENABLED=truebutSPOTIFY_CLIENT_IDorSPOTIFY_CLIENT_SECRETis 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.