Give eligible listeners a signed, expiring transcript address instead of the permanent one
Read the limits first: this narrows who holds a working address, it does not lock the file. Transcription Service serves each finished transcript at a permanent public address, with no sign-in, because podcast apps fetch it that way. Its visibility setting decides who is given that address. This recipe replaces the address in the feeds of listeners the setting already allows, so what they can share stops working after a while. What it cannot do:
- The permanent address keeps answering for anyone who already has it. There is no switch to stop it.
- A formatted transcript’s feed also carries the permanent
.htmladdress, and this recipe’s code swaps only.vtt. - The on-page download links give eligible visitors the permanent addresses. Return
[]frombenecaster_transcript_download_formatsto remove them. - Feeds are not the only place it is given out. The episode’s
wp/v2response still carries it for viewers the setting allows and for anyone who can edit the episode, and the episode editor shows it. Step 3 removes it fromwp/v2for non-editors. - A feed is compiled once and cached, keyed by the show and the listener’s holdings, never by who is asking or when. So the signature can depend on the holdings, not on the person, and the expiry is fixed when the feed compiles: make it comfortably longer than your feed’s cache lifetime, or the cached feed hands out dead addresses. Two listeners with the same holdings get the same address.
How it works:
- Hook at priority 10 or later. The add-on filters the feed’s transcript entries at priority 5 and has already removed them for every listener its setting excludes, so you change only the entries still there.
- Match only the add-on’s own addresses, as the pattern does: a transcript link the podcaster entered is theirs, and the add-on’s setting does not govern it either.
- Serve the signed address by reading the add-on’s route internally, never by redirecting, which would hand out the permanent address again. The internal request returns the WebVTT text; use
.txtfor plain text. An unfinished or unknown transcript answers 404. - A transcript later set to Hidden, or moved to tiers this listener does not hold, drops out of their feed on the next compile, but a signed address they already have keeps working until it expires: that is what the expiry is for.
- After changing the expiry rule, clear the show’s cached feeds with
do_action( 'benecaster_clear_feed_cache', $show_id ).
Code
<?php
// 1. Swap the address. Priority 10 or later: Transcription Service filters at 5 and has
// already removed the entry for every listener its setting excludes.
add_filter( 'benecaster_feed_podcast_episode_tags', function ( array $tags ): array {
if ( isset( $tags['transcript']['url'] ) ) {
$tags['transcript'] = [ $tags['transcript'] ]; // Benecaster seeds ONE entry, not a list.
}
foreach ( $tags['transcript'] ?? [] as $i => $entry ) {
if ( preg_match( '#/benecaster-transcription/v1/transcripts/([a-f0-9]{32})\.vtt#', (string) ( $entry['url'] ?? '' ), $m ) ) {
$expires = ( intdiv( time(), DAY_IN_SECONDS ) + 8 ) * DAY_IN_SECONDS; // 7–8 days, stable across a day of cached compiles
$sig = hash_hmac( 'sha256', $m[1] . '|' . $expires, wp_salt( 'auth' ) );
$tags['transcript'][ $i ]['url'] = add_query_arg( [ 'my_transcript' => $m[1], 'exp' => $expires, 'sig' => $sig ], home_url( '/' ) );
}
}
return $tags;
}, 10 );
// 2. Serve the signed address. Read the file through the add-on's own route, internally:
// never redirect to the permanent address, which would hand it out again.
add_action( 'template_redirect', function (): void {
$key = (string) ( $_GET['my_transcript'] ?? '' );
if ( 1 !== preg_match( '/^[a-f0-9]{32}$/', $key ) ) {
return;
}
$exp = (int) ( $_GET['exp'] ?? 0 );
$sig = (string) ( $_GET['sig'] ?? '' );
if ( $exp < time() || ! hash_equals( hash_hmac( 'sha256', $key . '|' . $exp, wp_salt( 'auth' ) ), $sig ) ) {
status_header( 410 );
exit;
}
$response = rest_do_request( new WP_REST_Request( 'GET', '/benecaster-transcription/v1/transcripts/' . $key . '.vtt' ) );
if ( 200 !== $response->get_status() ) {
status_header( 404 );
exit;
}
header( 'Content-Type: text/vtt; charset=utf-8' );
header( 'Access-Control-Allow-Origin: *' );
header( 'X-Robots-Tag: noindex' );
echo $response->get_data(); // The WebVTT body, already escaped by the add-on.
exit;
} );
// 3. Optional: stop WordPress's own episode route giving the permanent address to non-editors.
add_filter( 'rest_prepare_benecaster_episode', function ( $response, $post ) {
if ( $response instanceof WP_REST_Response && ! current_user_can( 'edit_post', $post->ID ) ) {
$data = $response->get_data();
unset( $data['meta']['_benecaster_podcast2_transcript_url'], $data['meta']['_benecaster_podcast2_transcript_type'] );
$response->set_data( $data );
}
return $response;
}, 30, 2 );
Hooks Used
Need this built rather than just documented? See our services →