Skip to content

Version 3.16.0#895

Open
zahardev wants to merge 22 commits intomainfrom
develop
Open

Version 3.16.0#895
zahardev wants to merge 22 commits intomainfrom
develop

Conversation

@zahardev
Copy link
Copy Markdown
Collaborator

@zahardev zahardev commented May 4, 2026

Summary by CodeRabbit

  • New Features

    • Admin: “Default Podcast” toggle UI and styles; plugin version bumped to 3.16.0‑alpha.
    • Player: improved accessibility, keyboard handling, and live-region announce feedback.
    • Passthrough: support for dynamic episode URLs plus campaign-based passthrough settings.
  • Bug Fixes

    • Image delete now clears related hidden IDs.
    • Membership sync: more reliable single‑sync queue processing and conflict handling.
    • Admin notices: disconnect notices now tracked/removed more reliably.
  • Tests

    • Added/updated tests for membership sync, queue reconciliation, player markup, AJAX connect, and album‑art selection.

zahardev added 8 commits May 4, 2026 13:08
#838

Replace delete_option() with race-safe reconciliation that re-reads the
option from DB and only removes processed users whose data hasn't changed,
preserving entries written by concurrent requests (Scenario E).

Co-Authored-By: Claude via AIContext
Co-Authored-By: Claude via AIContext
The delete handler only cleared the URL field, leaving the attachment
ID intact so the player kept showing stale artwork.

Co-Authored-By: Claude via AIContext
sync_membership() now guards against null membership lookups.
process_single_sync() clears the queue option at the retry cap.
Toggle checkbox is now properly associated with its label.

Co-Authored-By: Claude via AIContext
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a "default podcast" toggle with styles and template, improves Castos player accessibility and i18n, refactors WooCommerce single-sync queuing/processing with reconciliation, uses featured image as album-art fallback, adjusts admin notices/Castos connect flow, updates related JS/PHP/templates/tests, and bumps plugin version to 3.16.0-alpha.

Changes

Default Podcast Toggle

Layer / File(s) Summary
Styles
assets/admin/scss/_admin.scss, assets/admin/css/admin.css
Adds .ssp-default-podcast-label and .ssp-toggle CSS (hidden checkbox, slider track/thumb, checked-state, focus ring, label alignment).
Settings Template
templates/settings/podcast-default-toggle.php
New template rendering either "This is the default podcast" or a ssp_default_podcast checkbox toggle with slider and localized text.
Series Controller
php/classes/controllers/class-series-controller.php
Renders toggle in edit_series_term_meta_fields() via new show_default_podcast_toggle(); save_series_meta() calls maybe_set_default_series() to persist default-series option.
Admin JS
assets/js/admin.js
.ssp-image-delete handler now also clears hidden *_id field and triggers change alongside existing preview/field resets.

Player Accessibility & i18n

Layer / File(s) Summary
Player Localization
php/classes/controllers/class-players-controller.php
When ssp-castos-player is enqueued, localizes ssp_player_i18n (playback_speed, copied) with a static guard.
Player JS
assets/js/castos-player.js
Reads window.ssp_player_i18n; updates ARIA (speed/volume controls, aria-expanded), manages playlist aria-selected and item role="option"/tabindex="0", announces copy via SR live region, refactors togglePanel to accept trigger and adds Escape handling.
Player Template
templates/players/castos-player.php
Markup updated: container uses role="region" + aria-label; controls gain aria-label/aria-pressed; timer set aria-live="off"; panels start aria-expanded="false"; close controls converted to <button aria-label="Close">; share links get aria-label; playlist role="listbox" and items role="option" tabindex="0" with aria-selected.
Tests
tests/WPUnit/PlayersControllerTest.php
Expected player HTML assertions updated to match new ARIA attributes and semantic changes.

WooCommerce Memberships Single-Sync Refactor

Layer / File(s) Summary
Queueing Hooking
php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
Replaced inline closure with sync_membership() to enqueue add/revoke work; membership listener now calls sync_membership().
Queue Preparation
php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
prepare_single_sync() now uses clean_id_list() to filter/deduplicate IDs, resets attempts, and schedules the single-sync event.
Processing Loop
php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
Added process_single_sync() to snapshot the queue option, process each user's add/revoke lists (excluding series still allowed by other memberships), track attempts/failures, reschedule if pending, and delete the option when drained.
Reconciliation
php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
Added reconcile_sync_data() to reload option bypassing cache, remove successfully processed users that match the snapshot, and preserve concurrent writes.
Helpers / Cleaning
php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
Added clean_id_list() to filter falsy values, deduplicate, and reindex ID lists.
Tests
tests/WPUnit/WCMembershipsIntegratorTest.php
New/expanded test suite: clears option in setUp/tearDown, membership stubs, reflection helper, tests for queueing, null filtering, merge/dedup semantics, and concurrency reconciliation scenarios.

Episode Album Art & Tests

Layer / File(s) Summary
Repository Change
php/classes/repositories/class-episode-repository.php
get_album_art() now attempts the post featured image thumbnail as a fallback (no squareness check) and returns it via ssp_album_art filter with source featured_image.
Tests
tests/WPUnit/EpisodeRepositoryAlbumArtTest.php
New tests validate featured-image fallback, skipping when cover_image_id exists, accepting non-square featured images, and placeholder returned when no featured image exists.

Admin Notices & Castos Connect

Layer / File(s) Summary
Notifications Handler
php/classes/handlers/class-admin-notifications-handler.php
add_constant_notice() signature adds optional $key parameter and stores notices indexed by sanitized key.
Castos Handler
php/classes/handlers/class-castos-handler.php
Adds DISCONNECT_NOTICE_KEY constant and calls add_constant_notice(..., DISCONNECT_NOTICE_KEY) in disconnect().
Ajax Handler
php/classes/handlers/class-ajax-handler.php
connect_castos() removes the disconnect constant notice (DISCONNECT_NOTICE_KEY) after setting the token and before adding a success flash notice.
Tests
tests/WPUnit/AjaxHandlerTest.php
Adds test asserting connect_castos removes the disconnect notice on successful connect; test teardown also clears $_GET['api_token'].

Miscellaneous / Release

Layer / File(s) Summary
Sync Deduplication
php/classes/integrations/class-abstract-integrator.php
sync_user() removes any IDs from $revoke_series_ids that also appear in $add_series_ids (add wins).
Admin CSS Output
assets/admin/css/admin.css
Compiled CSS for the new .ssp-default-podcast-label and .ssp-toggle UI added.
Version Bump
seriously-simple-podcasting.php
Plugin header Version and SSP_VERSION constant updated from 3.15.03.16.0-alpha.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin User
    participant SeriesUI as Series Editor
    participant SeriesCtrl as Series Controller
    participant Store as Options Store
    participant PlayerCtrl as Players Controller
    participant PlayerJS as Player JavaScript
    participant Player as Castos Player
    participant WCInt as WC Memberships Integrator
    participant Cron as WP Cron

    Admin->>SeriesUI: Toggle "Set as default podcast"
    SeriesUI->>SeriesCtrl: POST ssp_default_podcast
    SeriesCtrl->>SeriesCtrl: maybe_set_default_series()
    SeriesCtrl->>Store: update_option('default_series', term_id)
    Store-->>SeriesCtrl: saved

    PlayerCtrl->>PlayerJS: wp_localize_script -> provides i18n
    Player->>PlayerJS: open share panel
    PlayerJS->>PlayerJS: togglePanel(panel, triggerBtn) -> set aria-expanded
    Player->>PlayerJS: copy link
    PlayerJS->>PlayerJS: copyLink() -> announceToScreenReader("Copied")

    WCInt->>WCInt: sync_membership(membership)
    WCInt->>WCInt: prepare_single_sync() (write queue)
    Cron->>WCInt: process_single_sync()
    WCInt->>WCInt: snapshot option, process users, reconcile_sync_data()
    WCInt->>Cron: reschedule if pending
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A tiny toggle, soft and bright,

I set the default with a gentle bite,
Players whisper labels clear and kind,
Queues reconcile so no one's left behind,
Version hopped up — new features to find!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Version 3.16.0' accurately reflects the main objective of this PR, which bumps the plugin version from 3.15.0 to 3.16.0-alpha and consolidates multiple feature additions and refactorings related to accessibility, series default selection, WooCommerce memberships, and passthrough functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Comment @coderabbitai help to get the list of available commands and usage tips.

Adds ARIA attributes for screen reader support: aria-pressed on mute toggle,
dynamic aria-label on speed button, aria-expanded on panel triggers, Escape
to close panels, aria-selected on playlist items, and aria-live announcement
for copy actions. Close buttons changed from div to button elements.

Co-Authored-By: Claude via AIContext
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/js/castos-player.js`:
- Around line 258-271: In togglePanel, when closing a panel (the branch where
panel.classList.contains('open') is true), after setting
triggerBtn.setAttribute('aria-expanded','false') move focus back to the
triggerBtn so the hidden close control is not left focused (i.e., call focus()
on triggerBtn when it exists) before returning; keep the existing behavior when
opening (panel.focus()) unchanged and ensure you only call triggerBtn.focus() if
triggerBtn is provided.
- Around line 273-293: The copyLink function currently always calls
announceToScreenReader('Copied') regardless of whether
document.execCommand('Copy') succeeded; change copyLink to capture the return
value (or catch exceptions) from document.execCommand('copy') and only call
announceToScreenReader(i18n.copied || 'Copied') when the command returns true
(or no error thrown). Keep announceToScreenReader as-is for updating the live
region, but gate its invocation in copyLink so screen readers are only notified
on successful copy; reference the copyLink and announceToScreenReader functions
when making this change.

In `@php/classes/controllers/class-series-controller.php`:
- Around line 354-379: The handler-level caching of the default series ID causes
stale reads: after maybe_set_default_series() calls ssp_update_option() the
subsequent save_series_data_to_castos() still uses the old cached value from
Series_Handler::default_series_id() (and similar caches in Settings_Handler and
Series_Walker). Fix by invalidating or refreshing the cached value immediately
after updating the option—e.g., after ssp_update_option( 'default_series', (int)
$term_id ) in maybe_set_default_series() clear the handler cache (unset
$this->default_series_id or call a refresh method) or make
save_series_data_to_castos() fetch the live value (call
ssp_get_default_series_id() directly or provide a getter that bypasses the
cached property) so the updated default is used when syncing to Castos.

In `@php/classes/integrations/woocommerce/class-wc-memberships-integrator.php`:
- Line 380: The call to sync_membership() should be guarded because
get_user_membership() can return null; modify the code around the
get_user_membership() call so you assign $user_membership =
$this->get_user_membership(...) then check if $user_membership is truthy (not
null) before invoking $this->sync_membership($user_membership), and skip or
handle the null case (e.g., return or log) to avoid dereferencing a null
membership.

In `@templates/players/castos-player.php`:
- Around line 190-194: The playlist items are focusable but not
keyboard-activatable; update the template element rendering (playlist__item with
data-episode) and the dynamic item creation in assets/js/castos-player.js to
support keyboard activation: either change each list item to a native <button>
element (preserving data-episode and active/aria-selected state) or add a
keydown listener to the playlist__item elements that triggers the same selection
logic on Enter (key === "Enter") and Space (key === " " or key === "Spacebar")
and prevents default on Space; ensure the handler mirrors the existing click
handler and also sets aria-selected and active class consistently for both
static and appended items.

In `@tests/WPUnit/WCMembershipsIntegratorTest.php`:
- Around line 9-12: Add the same scheduled-hook clearing that you have in
tearDown() to setUp() so pre-existing scheduled single-sync state cannot leak
into the first test; in setUp() (alongside
delete_option(WC_Memberships_Integrator::SINGLE_SYNC_DATA_OPTION)) call the same
wp_clear_scheduled_hook or equivalent routine used in tearDown() to unschedule
the single-sync hook for full test isolation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc48a33d-bd05-4fca-b5d8-f596a0034a27

📥 Commits

Reviewing files that changed from the base of the PR and between 5b8ef6e and df6baa0.

📒 Files selected for processing (13)
  • assets/admin/css/admin.css
  • assets/admin/scss/_admin.scss
  • assets/js/admin.js
  • assets/js/castos-player.js
  • php/classes/controllers/class-players-controller.php
  • php/classes/controllers/class-series-controller.php
  • php/classes/integrations/class-abstract-integrator.php
  • php/classes/integrations/woocommerce/class-wc-memberships-integrator.php
  • seriously-simple-podcasting.php
  • templates/players/castos-player.php
  • templates/settings/podcast-default-toggle.php
  • tests/WPUnit/PlayersControllerTest.php
  • tests/WPUnit/WCMembershipsIntegratorTest.php

Comment thread assets/js/castos-player.js
Comment thread assets/js/castos-player.js
Comment thread php/classes/controllers/class-series-controller.php
Comment thread templates/players/castos-player.php
Comment thread tests/WPUnit/WCMembershipsIntegratorTest.php
zahardev added 4 commits May 4, 2026 13:47
sync_membership() guards against null membership lookups.
process_single_sync() clears stale queue at retry cap instead of
silently abandoning stranded users.

Co-Authored-By: Claude via AIContext
Reuse reconcile_sync_data() to drop only capped snapshot users
instead of unconditionally deleting the queue option.

Co-Authored-By: Claude via AIContext
Falls back to the post's featured image in get_album_art() after
cover/series/feed images and before the placeholder. Skips the
squareness check since the HTML5 player handles non-square via CSS.

Co-Authored-By: Claude via AIContext
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@php/classes/repositories/class-episode-repository.php`:
- Around line 863-867: The code in class-episode-repository.php reads
$image_data_array['src'] directly after calling ssp_get_attachment_image_src()
which can return an empty array and causes an undefined offset warning; update
the check to guard the payload before accessing ['src']—for example, use the
same pattern as other call sites by using ssp_is_image_square($image_data_array)
or at minimum verify isset($image_data_array['src']) && !
empty($image_data_array['src']) before returning apply_filters('ssp_album_art',
...); change the block around $thumb_id / $image_data_array to perform that safe
check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 44d21368-6d1e-4a5c-a106-3998644a91d3

📥 Commits

Reviewing files that changed from the base of the PR and between 11ea64e and 5e594a1.

📒 Files selected for processing (2)
  • php/classes/repositories/class-episode-repository.php
  • tests/WPUnit/EpisodeRepositoryAlbumArtTest.php

Comment thread php/classes/repositories/class-episode-repository.php
zahardev added 4 commits May 5, 2026 13:48
Disconnect notice was persisting after successful reconnection because
nothing removed it from the constant notices option.

Co-Authored-By: Claude via AIContext
Prevents unsanitized strings from rendering in HTML data attributes.

Co-Authored-By: Claude via AIContext
Enable passthrough URLs for podcasts with active campaigns, not just ads.
Castos can push status changes via PUT /ssp/v1/podcasts/{series_id} for
immediate activation; hourly cron sync serves as backup.

Co-Authored-By: Claude via AIContext
Co-Authored-By: Claude via AIContext
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
php/classes/rest/class-episodes-rest-controller.php (1)

324-346: 💤 Low value

Consider batching transient deletions for large sites.

The flush_episode_file_data_transients() method queries all episodes with posts_per_page => -1, which could cause memory pressure on sites with thousands of episodes. While this is called infrequently (only when passthrough settings change), consider batching:

♻️ Suggested improvement
 protected function flush_episode_file_data_transients( $series_id ) {
     $args = array(
         'post_type'      => ssp_post_types(),
         'post_status'    => 'any',
-        'posts_per_page' => -1,
+        'posts_per_page' => 500,
         'fields'         => 'ids',
     );

     if ( $series_id ) {
         $args['tax_query'] = array(
             array(
                 'taxonomy' => ssp_series_taxonomy(),
                 'field'    => 'term_id',
                 'terms'    => $series_id,
             ),
         );
     }

-    $episode_ids = get_posts( $args );
+    $paged = 1;
+    do {
+        $args['paged'] = $paged;
+        $episode_ids = get_posts( $args );
 
-    foreach ( $episode_ids as $episode_id ) {
-        delete_transient( 'ssp_episode_file_data_' . $episode_id );
-    }
+        foreach ( $episode_ids as $episode_id ) {
+            delete_transient( 'ssp_episode_file_data_' . $episode_id );
+        }
+        $paged++;
+    } while ( count( $episode_ids ) === 500 );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@php/classes/rest/class-episodes-rest-controller.php` around lines 324 - 346,
flush_episode_file_data_transients currently calls get_posts with posts_per_page
=> -1 which can OOM on large sites; change it to iterate in batches (e.g., set
posts_per_page to a reasonable batch size like 100 and use a paged loop or
WP_Query with 'fields'=>'ids' and 'paged' to fetch each page) and call
delete_transient('ssp_episode_file_data_' . $episode_id) for each ID as you
stream through results; ensure you reset query state (wp_reset_postdata) if
using WP_Query and stop looping when no more posts are returned to avoid loading
all episode IDs into memory at once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@php/classes/controllers/class-passthrough-controller.php`:
- Around line 107-121: The get_episode_file_data() method currently caches any
response from castos_handler->get_episode_file_data into the transient
'ssp_episode_file_data_{episode_id}' for WEEK_IN_SECONDS; change this so failed
API responses are not cached (or are cached with a much shorter TTL): after
calling $this->castos_handler->get_episode_file_data( $castos_episode_id )
inspect the returned $file_data and if it's falsy or if
isset($file_data->success) && $file_data->success === false then either skip
set_transient entirely or call set_transient with a short TTL (e.g.
MINUTE_IN_SECONDS * 5) instead of WEEK_IN_SECONDS; update the logic in
get_episode_file_data() accordingly so only successful responses get the long
cache.

---

Nitpick comments:
In `@php/classes/rest/class-episodes-rest-controller.php`:
- Around line 324-346: flush_episode_file_data_transients currently calls
get_posts with posts_per_page => -1 which can OOM on large sites; change it to
iterate in batches (e.g., set posts_per_page to a reasonable batch size like 100
and use a paged loop or WP_Query with 'fields'=>'ids' and 'paged' to fetch each
page) and call delete_transient('ssp_episode_file_data_' . $episode_id) for each
ID as you stream through results; ensure you reset query state
(wp_reset_postdata) if using WP_Query and stop looping when no more posts are
returned to avoid loading all episode IDs into memory at once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0220165d-3487-4939-8c9c-332c81e1d5f2

📥 Commits

Reviewing files that changed from the base of the PR and between 75da8f2 and fdf4115.

📒 Files selected for processing (10)
  • php/classes/controllers/class-ads-controller.php
  • php/classes/controllers/class-app-controller.php
  • php/classes/controllers/class-cron-controller.php
  • php/classes/controllers/class-passthrough-controller.php
  • php/classes/entities/class-api-podcast.php
  • php/classes/entities/class-episode-file-data.php
  • php/classes/rest/class-episodes-rest-controller.php
  • php/classes/rest/class-rest-api-controller.php
  • php/includes/ssp-functions.php
  • tests/WPUnit/SSPEpisodeFunctionsTest.php
💤 Files with no reviewable changes (1)
  • php/classes/controllers/class-ads-controller.php
✅ Files skipped from review due to trivial changes (2)
  • php/classes/rest/class-rest-api-controller.php
  • php/classes/entities/class-api-podcast.php

Comment thread php/classes/controllers/class-passthrough-controller.php
zahardev added 5 commits May 7, 2026 09:06
RSS 2.0 only allows <image> at the channel level. The bare tag in
feed items broke W3C validation and Spotify ingestion.

Co-Authored-By: Claude via AIContext
Co-Authored-By: Claude via AIContext
Restore focus to trigger button on panel close, gate copy announcement
on success, add keyboard activation for playlist items, clear scheduled
hook in test setUp, and use short TTL for failed API responses.

Co-Authored-By: Claude via AIContext
Bumps stable tag and package.json version, adds changelog covering
campaigns support, a11y improvements, feed fixes, and WC sync updates.

Co-Authored-By: Claude via AIContext
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.

1 participant