Conversation
Fixes: - Bug music-assistant#1: Replace self.lookup_key with self.instance_id - Episodes were failing to convert due to missing attribute - Changed lines 169, 174, 215 in __init__.py - Tested and verified working Documentation: - Add STATUS.md with complete development tracking - Implementation status (24 features implemented) - Testing results from 2026-02-01 - Bug documentation (1 fixed, 1 resolved, 1 investigating) - API endpoints reference - Contributing guidelines - Changelog Testing: - Verified login, podcast list, episodes work correctly - Identified resume position issue for further investigation - Documented all findings in STATUS.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Episodes were always starting at 0:00 instead of resuming from the last played position. Investigation revealed two separate issues: 1. get_resume_position() was returning seconds, but Music Assistant expects milliseconds 2. StreamDetails was missing the allow_seek=True flag, which prevented ffmpeg from applying the -ss seek parameter Both issues have been fixed and tested. Episodes now correctly resume from their saved positions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated STATUS.md with extensive documentation of Pocketcasts API capabilities: - Mapped 7 Pocketcasts features to Music Assistant provider features (RECOMMENDATIONS, FAVORITE_PODCASTS_EDIT, PLAYLIST features, LYRICS, etc.) - Replaced all guessed API endpoints with verified endpoints from unofficial Pocketcasts API documentation - Documented 20+ additional API endpoints organized by domain and functionality - Created phased implementation priority plan (Phase 1-4) - Cleaned up duplicate Library Management items (subscribe/unsubscribe) - Added notes on unconfirmed features (Transcripts, Filters) This provides a clear roadmap for future Pocketcasts provider development. Reference: https://github.com/yfhyou/api_pocketcasts/blob/main/reference/endpoints.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added comprehensive documentation about Pocketcasts authentication tokens: Authentication Findings: - Pocketcasts API supports two token types: 1. Mobile/API tokens (scope: "mobile") - valid ~5 months, no refresh needed 2. Web player tokens (scope: "webplayer") - valid 1 hour, requires refresh - Current implementation uses mobile tokens (long-lived) - Token refresh is not required for our use case Documentation Updates: - Added "Authentication & Token Management" section explaining token types - Moved token refresh to Phase 4 (low priority) with rationale - Clarified that /user/token endpoint is for web player refresh only - Added explanation of JWT claims (iat, exp) in mobile tokens - Marked login error handling as tested and working This explains why the provider continues working after the supposed "1 hour expiry" - we're using mobile tokens that last months, not the short-lived web player tokens. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implemented 5 special browse folders that appear at root level: - Up Next: User's queued episodes - New Releases: Recent episodes from subscriptions - In Progress: Episodes currently being listened to - Starred: Favorited episodes - History: Recently played episodes API Client Changes: - Added get_up_next_episodes() for POST /up_next/list * Returns dict with UUIDs as keys (unique response format) - Added get_new_releases() for POST /user/new_releases - Added get_starred_episodes() for POST /user/starred - Added get_history() for POST /user/history - Fixed type hints in __aexit__ for proper async context manager Provider Changes: - Added _create_browse_folders() helper to generate folder list - Added _get_special_folder_episodes() to fetch folder-specific episodes - Updated browse() to show folders at root and handle folder paths - Special handling for Up Next endpoint's unique response format: * Returns episodes as dict with UUIDs as keys (not a list) * Modified iteration to handle both dict and list formats * Extract episode UUID from dict key when missing from data * Handle podcast field as both string (Up Next) and object (others) Testing: - All 5 browse folders tested and working - Episodes display correctly in all folders - Playback works from all folder types Documentation: - Updated STATUS.md with implementation details - Added changelog entry for 2026-02-02 - Documented special handling for Up Next endpoint - Updated test results and next steps Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add complete library management functionality allowing users to subscribe and unsubscribe from podcasts directly through Music Assistant, syncing changes back to Pocketcasts account. API Client Changes: - Add subscribe_podcast() method (POST /user/podcast/subscribe) - Add unsubscribe_podcast() method (POST /user/podcast/unsubscribe) - Enhanced error logging with response status and reason Provider Changes: - Implement library_add() for subscribing to podcasts - Implement library_remove() for unsubscribing from podcasts - Both methods delegate to base class for non-podcast media types - Full error handling and logging Testing: - Verified subscribe/unsubscribe work correctly in UI - Tested API error handling with invalid UUIDs - API validates UUID format (400 for malformed) - API accepts well-formed UUIDs even if non-existent (200) - Not a concern in practice - UUIDs come from Pocketcasts APIs Documentation: - Mark LIBRARY_PODCASTS_EDIT as fully implemented - Clarify FAVORITE_PODCASTS_EDIT not applicable to Pocketcasts - Document API behavior regarding UUID validation - Add comprehensive testing notes Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add on_played() callback for syncing playback progress every 30 seconds - Mark episodes as played when within 45 seconds of actual end - Use /user/episode endpoint for accurate duration (fixes early completion) - Add episode to Up Next via play_now() when starting playback - Archive completed episodes and remove from Up Next queue - Unarchive episodes when replaying from earlier position - Ignore MA's fully_played flag (uses wrong duration from static API) API methods added: - get_episode_details() - Fetch real duration and playback status - mark_episode_played() - Mark episode as played (status=3) - mark_episode_unplayed() - Reset to unplayed (status=1) - archive_episode() - Archive/unarchive episodes - remove_from_up_next() - Remove from Up Next queue - play_now() - Add to Up Next at top position Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reduced from 868 to 362 lines by consolidating redundant sections: - Merged feature status sections into single Feature Status table - Combined potential/priority/not-implemented into Roadmap section - Simplified known issues and testing status into compact tables - Condensed changelog while preserving key information Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🔒 Dependency Security Report✅ No dependency changes detected in this PR. |
|
Hello! I have updated this provider starting with the great work that @OzGav did. I was going to use a seperate API library, but decided to use the local api_client.py that was already created.
Overall this should be a usable provider with basic sync functions. |
|
This will be reviewed fully in due course however a couple of things. I wont be the code owner. You can remove the status.md file. You need to add icon.svg and icon_monochrome.svg |
Add provider icons (icon.svg, icon_monochrome.svg), fix bugs found in Copilot code review: correct provider field in _convert_podcast to use instance_id, add missing media_type to StreamDetails, fix str(None) bug in config handling, add exception chaining in get_podcast, cache episode duration in on_played to reduce API calls. Also remove STATUS.md from PR, fix manifest name to "Pocket Casts", remove duplicate import and unused PLAY_URL constant, and remove verbose search response logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 5 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
API client methods now raise PocketCastsAPIError on non-200 responses and LoginError on 401/403, instead of silently returning empty lists. This lets callers distinguish between "empty library" and "API failure", and ensures handle_async_init fails fast on auth problems. Added try/except to _get_special_folder_episodes, the one unprotected caller. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add SVG browse folder icons (up next, new releases, in progress, starred, history) matching the Pocket Casts web player style - Embed icons as base64 data URIs for self-contained operation - Add remotely_accessible=True to podcast thumbnail MediaItemImage - Refactor _create_browse_folders to use a loop with icon lookup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace get_podcast_episodes() + linear scan with get_episode_details() which fetches a single episode directly via /user/episode. This avoids downloading the entire episode list on every playback start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Normalize position (treat None as 0) since MA's mark_item_played allows seconds_played=None. Gate the "mark unplayed" branch on not is_playing to avoid conflating playback at position 0 with the explicit unplayed action. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inject the shared MA http_session into PocketCastsClient via constructor instead of creating and managing a standalone aiohttp.ClientSession. This is consistent with other providers and reuses the shared connector, User-Agent, and response class. Remove session ownership (context manager, close in unload) since the session lifecycle is managed by MA. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Case 2: Near or past the end (within 45 seconds of real duration) | ||
| # Threshold must exceed MA's 30-second callback interval to guarantee | ||
| # the last callback before episode end triggers completion. | ||
| if real_duration > 0 and position >= (real_duration - 45): |
There was a problem hiding this comment.
The 45-second threshold for marking episodes as played (line 924) and the 30-second callback interval (line 922) are magic numbers that would be clearer as named constants at the module or class level. Consider defining COMPLETION_THRESHOLD_SECONDS = 45 and CALLBACK_INTERVAL_SECONDS = 30 to make the relationship between these values explicit and easier to maintain if the callback interval changes.
| duration = int(ep.get("duration", 0)) | ||
|
|
||
| # Consider fully played if > 90% | ||
| fully_played = duration > 0 and (played_up_to / duration) > 0.9 |
There was a problem hiding this comment.
The 0.9 threshold for determining "fully played" status appears in four different places (lines 475, 659, 782, 836). This duplicated magic number should be extracted as a module-level or class-level constant like FULLY_PLAYED_THRESHOLD = 0.9 to ensure consistency and make it easier to adjust if needed.
| folders = [ | ||
| ("up_next", "Up Next"), | ||
| ("new_releases", "New Releases"), | ||
| ("in_progress", "In Progress"), | ||
| ("starred", "Starred"), | ||
| ("history", "History"), |
There was a problem hiding this comment.
The special folder names ("up_next", "new_releases", "in_progress", "starred", "history") are duplicated as string literals across multiple locations: line 558-563 (_create_browse_folders), line 506-515 (_get_special_folder_episodes), and line 614 (browse method check). Consider extracting these as class-level or module-level constants to avoid typos and make maintenance easier. For example, define a constant like SPECIAL_FOLDERS = ("up_next", "new_releases", "in_progress", "starred", "history") and use it consistently.
| if response.status != 200: | ||
| raise PocketCastsAPIError( | ||
| f"Failed to get episodes for {podcast_uuid}: {response.status}" |
There was a problem hiding this comment.
Unlike other API methods in this client, get_podcast_episodes does not check for 401/403 authentication errors before the general non-200 check. This endpoint uses the unauthenticated podcast-api.pocketcasts.com domain (line 91), so auth errors are unlikely. However, for consistency with the pattern established by all other methods (see lines 75-76, 113-114, 134-135, etc.), consider adding an explicit 401/403 check that raises LoginError, even if authentication isn't strictly required for this endpoint.
| if response.status != 200: | |
| raise PocketCastsAPIError( | |
| f"Failed to get episodes for {podcast_uuid}: {response.status}" | |
| if response.status in (401, 403): | |
| raise LoginError(f"Authentication failed with status {response.status}") | |
| if response.status != 200: | |
| text = await response.text() | |
| raise PocketCastsAPIError( | |
| f"Failed to get episodes for {podcast_uuid}: {response.status} - {text}" |
| # Create lookup maps for status | ||
| in_progress_map = {ep.get("uuid"): ep for ep in in_progress} | ||
| history_set = {ep.get("uuid") for ep in history} | ||
|
|
||
| for episode_data in episodes: | ||
| episode_item = self._convert_episode(episode_data, item_path) | ||
| if episode_item: | ||
| episode_uuid = episode_data.get("uuid") | ||
|
|
||
| # Check in-progress status | ||
| if episode_uuid in in_progress_map: | ||
| ip_data = in_progress_map[episode_uuid] | ||
| played_up_to = ip_data.get("playedUpTo", 0) | ||
| duration = ip_data.get("duration", episode_data.get("duration", 0)) | ||
|
|
||
| episode_item.resume_position_ms = played_up_to * 1000 | ||
|
|
||
| # Consider played if > 90% complete | ||
| if duration > 0: | ||
| episode_item.fully_played = (played_up_to / duration) > 0.9 | ||
|
|
||
| # If in history but not in-progress, likely fully played | ||
| elif episode_uuid in history_set: | ||
| episode_item.fully_played = True |
There was a problem hiding this comment.
The logic for enriching episodes with playback status (lines 640-663) is duplicated from get_podcast_episodes (lines 456-488). This includes creating the same lookup maps and applying the same status logic. Consider extracting this into a helper method like _enrich_episodes_with_status that takes episodes and applies the in-progress/history lookups, returning the enriched list. This would improve maintainability by ensuring both code paths use identical logic.
Summary
Adds a new music provider for Pocketcasts podcast service, allowing users to:
Features
LIBRARY_PODCASTSLIBRARY_PODCASTS_EDITBROWSESEARCHImplementation
manifest.json- Provider configurationapi_client.py- Custom API client for Pocketcasts (reverse-engineered API)__init__.py- Main provider implementationSTATUS.md- Development documentationPlayback Sync
Test plan
Notes
🤖 Generated with Claude Code