# SDAweb Channels for YouTube — Developer Hooks

This plugin exposes WordPress action and filter hooks so other developers can
extend or customize behavior **without forking the plugin**. All hooks listed
here are considered part of the public API: their names and signatures will
not be changed within a major version, and any deprecation will go through
the standard `_deprecated_hook()` cycle.

If you find yourself needing a hook that isn't listed here, please open an
issue or feature request — adding new hooks is cheap and we'd rather support
your use case than have you fork the plugin.

All hooks are introduced in **version 1.5.0** unless otherwise noted.

---

## Filters

### `sdawchfo_video_query_args`

Filter the arguments passed to the internal video query before the database
query runs. This is the most powerful hook in the plugin: anything you change
here affects which videos come out of `SDAWCHFO_Video_Manager::get_videos()`,
which in turn powers every shortcode, the block, and the public REST endpoint.

**Arguments:**
- `array $args` — Query arguments. Keys: `channel_id` (string|array), `sort`
  (`'recent'` or `'popular'`), `count` (int), `offset` (int), `exclude_shorts`
  (bool), `live_only` (bool), `live_status` (string), `exclude_live` (bool).

**Returns:** The (possibly modified) `$args` array.

**Example — always exclude Shorts site-wide:**

```php
add_filter( 'sdawchfo_video_query_args', function( $args ) {
    $args['exclude_shorts'] = true;
    return $args;
} );
```

**Example — force the popular sort during a fundraising campaign:**

```php
add_filter( 'sdawchfo_video_query_args', function( $args ) {
    if ( get_option( 'my_campaign_active' ) ) {
        $args['sort'] = 'popular';
    }
    return $args;
} );
```

---

### `sdawchfo_video_card_classes`

Filter the CSS classes applied to a single video card wrapper `<div>`.

**Arguments:**
- `array  $card_classes` — Array of CSS classes. Default: `['sdawchfo-video-card']`.
- `object $video` — The video row from the database.

**Returns:** Array of class names. **Always include `sdawchfo-video-card`** —
removing it will break plugin styles.

**Example — add a custom class to live videos for special styling:**

```php
add_filter( 'sdawchfo_video_card_classes', function( $classes, $video ) {
    if ( ! empty( $video->live_status ) && $video->live_status === 'live' ) {
        $classes[] = 'my-theme-live-card';
    }
    return $classes;
}, 10, 2 );
```

---

### `sdawchfo_thumbnail_url`

Filter the URL used for a video card's thumbnail image. Lets you swap to a
CDN, a local sideloaded copy, an image proxy, or a placeholder.

**Arguments:**
- `string $thumb` — The thumbnail URL the plugin was about to use.
- `object $video` — The video row from the database (use this to access all
  three thumbnail sizes: `thumbnail_medium`, `thumbnail_high`, `thumbnail_maxres`).
- `string $thumb_quality` — The configured thumbnail quality setting
  (`'medium'`, `'high'`, or `'maxres'`).

**Returns:** A thumbnail URL string.

**Example — route all YouTube thumbnails through a Cloudflare image proxy:**

```php
add_filter( 'sdawchfo_thumbnail_url', function( $thumb ) {
    return str_replace( 'i.ytimg.com', 'images.example.com/yt', $thumb );
} );
```

**Example — use a placeholder for adult-content channels:**

```php
add_filter( 'sdawchfo_thumbnail_url', function( $thumb, $video ) {
    if ( in_array( $video->channel_id, get_option( 'my_blocked_channels', [] ), true ) ) {
        return plugin_dir_url( __FILE__ ) . 'images/blocked-thumb.png';
    }
    return $thumb;
}, 10, 2 );
```

---

### `sdawchfo_inline_css`

Filter the inline CSS string the plugin generates from your appearance
settings, just before it is cached and injected into the page via
`wp_add_inline_style()`. The filtered value is what ends up in the
`sdawchfo_inline_css` transient.

**Arguments:**
- `string $css` — The generated CSS string. May be empty if no settings differ
  from defaults.

**Returns:** A CSS string.

**Example — append a media query forcing larger play buttons on touch screens:**

```php
add_filter( 'sdawchfo_inline_css', function( $css ) {
    $css .= '@media (pointer: coarse) { .sdawchfo-play-btn { transform: scale(1.2); } }';
    return $css;
} );
```

> **Tip:** Cached for 24 hours. After changing your filter, clear the transient
> with `delete_transient( 'sdawchfo_inline_css' )` to see the change immediately.

---

### `sdawchfo_video_display_date`

Filter the timestamp used for the "X ago" relative-date label on each video
card. By default the plugin uses `actual_start_at` when the video was a live
broadcast that has aired (because YouTube's `publishedAt` for scheduled
broadcasts is the *schedule-creation* time, not the airdate), and otherwise
falls back to `published_at`.

**Arguments:**
- `string|null $display_date` — The chosen timestamp in `Y-m-d H:i:s` UTC
  format, or `null` if neither field is populated.
- `object      $video` — The full video row from the database. Useful fields:
  `published_at`, `actual_start_at`, `scheduled_at`, `live_status`.

**Returns:** A `Y-m-d H:i:s` UTC timestamp string, any `strtotime()`-parseable
string, or `null` to suppress the date label.

**Introduced in:** 1.5.3.

**Example — always display the original upload/schedule date, never the airdate:**

```php
add_filter( 'sdawchfo_video_display_date', function( $date, $video ) {
    return $video->published_at;
}, 10, 2 );
```

**Example — prefer actual end time for completed broadcasts (if you extend the
schema to store it):**

```php
add_filter( 'sdawchfo_video_display_date', function( $date, $video ) {
    if ( ! empty( $video->actual_end_at ) ) {
        return $video->actual_end_at;
    }
    return $date;
}, 10, 2 );
```

---

### `sdawchfo_rest_videos_response`

Filter the response payload returned by the public `GET /sdawchfo/v1/videos`
REST endpoint, just before it is wrapped in a `WP_REST_Response`. Use this to
add custom fields a frontend script needs, strip data you don't want exposed,
or wrap the response in a different envelope.

**Arguments:**
- `array $payload` — The response array. Default keys: `videos` (array of
  video rows), `html` (rendered HTML for Load More), `has_more` (bool).
- `WP_REST_Request $request` — The original REST request object.

**Returns:** The (possibly modified) payload array.

**Example — append a "newest published_at" field for client-side filtering:**

```php
add_filter( 'sdawchfo_rest_videos_response', function( $payload ) {
    $payload['newest_published_at'] = ! empty( $payload['videos'] )
        ? $payload['videos'][0]->published_at
        : null;
    return $payload;
} );
```

---

### `shortcode_atts_{$tag}` *(WordPress core, not plugin-specific)*

WordPress already provides per-shortcode attribute filters through core. To
modify attributes after `shortcode_atts()` runs but before the plugin processes
them, hook one of these:

- `shortcode_atts_sdawchfo_channel`
- `shortcode_atts_sdawchfo_channels`
- `shortcode_atts_sdawchfo_feed`
- `shortcode_atts_sdawchfo_live`

**Example — set a default `count` of 8 for all `[sdawchfo_feed]` instances:**

```php
add_filter( 'shortcode_atts_sdawchfo_feed', function( $out ) {
    if ( empty( $out['count'] ) ) {
        $out['count'] = 8;
    }
    return $out;
} );
```

Reference: <https://developer.wordpress.org/reference/functions/shortcode_atts/>

---

## Actions

### `sdawchfo_before_refresh_channel`

Fires immediately before a channel's videos are refreshed from the YouTube
Data API. Useful for logging, analytics, or temporarily disabling other
behavior during a refresh.

**Arguments:**
- `string $yt_channel_id` — The YouTube channel ID being refreshed.
- `object $channel` — The channel row from the database.

**Example — write to a custom log:**

```php
add_action( 'sdawchfo_before_refresh_channel', function( $yt_channel_id, $channel ) {
    error_log( "[YT refresh] starting {$channel->title} ({$yt_channel_id})" );
}, 10, 2 );
```

---

### `sdawchfo_after_refresh_channel`

Fires after a channel's videos have been successfully refreshed from the
YouTube API. Receives the number of videos stored in this run. **Does not
fire on errors** — wrap your callback in `is_wp_error()` checks if you also
want to react to failures (use `sdawchfo_before_refresh_channel` for that).

**Arguments:**
- `string $yt_channel_id` — The YouTube channel ID that was refreshed.
- `object $channel` — The channel row from the database.
- `int    $stored` — Number of videos stored in this refresh.

**Example — bust your page cache when a channel updates:**

```php
add_action( 'sdawchfo_after_refresh_channel', function( $yt_channel_id, $channel, $stored ) {
    if ( $stored > 0 && function_exists( 'wp_cache_flush' ) ) {
        wp_cache_flush();
    }
}, 10, 3 );
```

**Example — Slack notification when a tracked channel uploads new content:**

```php
add_action( 'sdawchfo_after_refresh_channel', function( $yt_channel_id, $channel, $stored ) {
    if ( $stored > 0 ) {
        wp_remote_post( 'https://hooks.slack.com/services/...', array(
            'body' => wp_json_encode( array(
                'text' => "Refreshed {$channel->title}: {$stored} videos in cache.",
            ) ),
        ) );
    }
}, 10, 3 );
```

---

### `sdawchfo_stale_videos_purged`

Fires when the plugin removes stale video rows from its local cache. There
are two cases this fires for: (1) when the YouTube playlist returned zero
videos, meaning all of a channel's videos are gone (the `$reason` argument
will be `'empty_playlist'`), and (2) when individual videos have disappeared
from the playlist while others remain (`$reason` will be `'not_in_playlist'`).

**Arguments:**
- `string $channel_id` — The YouTube channel ID whose videos were purged.
- `int    $deleted` — Number of rows deleted.
- `string $reason` — Either `'empty_playlist'` or `'not_in_playlist'`.

**Example — log unusual purges:**

```php
add_action( 'sdawchfo_stale_videos_purged', function( $channel_id, $deleted, $reason ) {
    if ( $reason === 'empty_playlist' ) {
        error_log( "[YT] Channel {$channel_id} returned an empty playlist — {$deleted} cached videos removed." );
    }
}, 10, 3 );
```

---

## Patterns and conventions

- **Hook names use the `sdawchfo_` prefix** consistently with the rest of the
  plugin's namespace.
- **Filters return their first argument**, modified or not. Returning `null`
  or omitting `return` will break the plugin.
- **Actions don't return anything.** Don't `return false` from an action
  callback expecting it to cancel the operation — it won't.
- **Don't throw exceptions from hooks.** Other code (including the plugin
  itself) will not catch them and your site will white-screen.
- **Hook callbacks run inside the plugin's call stack**, so heavy work
  (HTTP calls, large DB queries) will slow down the page that triggered the
  hook. For background work, schedule a one-off `wp_schedule_single_event()`
  inside your callback instead.
- **All hooks are stable within major versions.** A 1.x release will not
  remove or rename a documented hook. New hooks may be added in minor
  versions.

## Requesting a new hook

If you want to customize something the existing hooks don't cover, please
open an issue at the plugin's wordpress.org support forum and describe what
you're trying to do. Adding a new hook is usually a one-line change for us
and saves you from forking — that's a win for everyone.
