diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 803c062579f3511d8cc000cf133d022058831e79..3a9f533ee94c1544a9452ff0a004ad6d0da90f7f 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -194,6 +194,14 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
     $routes->get('feed', 'FeedController/$1');
 });
 
+// audio routes
+$routes->head('audio/@(:podcastHandle)/(:slug)', 'EpisodeController::audio/$1/$2', [
+    'as' => 'episode-audio',
+],);
+$routes->get('audio/@(:podcastHandle)/(:slug)', 'EpisodeController::audio/$1/$2', [
+    'as' => 'episode-audio',
+],);
+
 // Other pages
 $routes->get('/credits', 'CreditsController', [
     'as' => 'credits',
diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php
index 5b59cf55472d99b415edaf260df6a1ccc081c68a..03551e355101680d11fc1ac543326874bd787c50 100644
--- a/app/Controllers/EpisodeController.php
+++ b/app/Controllers/EpisodeController.php
@@ -19,12 +19,14 @@ use App\Models\PodcastModel;
 use App\Models\PostModel;
 use CodeIgniter\Database\BaseBuilder;
 use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\HTTP\Response;
 use CodeIgniter\HTTP\ResponseInterface;
 use Config\Services;
 use Modules\Analytics\AnalyticsTrait;
 use Modules\Fediverse\Objects\OrderedCollectionObject;
 use Modules\Fediverse\Objects\OrderedCollectionPage;
+use Modules\PremiumPodcasts\Models\SubscriptionModel;
 use SimpleXMLElement;
 
 class EpisodeController extends BaseController
@@ -329,4 +331,82 @@ class EpisodeController extends BaseController
             ->setHeader('Access-Control-Allow-Origin', '*')
             ->setBody($collection->toJSON());
     }
+
+    public function audio(): RedirectResponse | ResponseInterface
+    {
+        // check if episode is premium?
+        $subscription = null;
+
+        // check if podcast is already unlocked before any token validation
+        if ($this->episode->is_premium && ($subscription = service('premium_podcasts')->subscription(
+            $this->episode->podcast->handle
+        )) === null) {
+            // look for token as GET parameter
+            if (($token = $this->request->getGet('token')) === null) {
+                return $this->response->setStatusCode(401)
+                    ->setJSON([
+                        'errors' => [
+                            'status' => 401,
+                            'title' => 'Unauthorized',
+                            'detail' => 'Episode is premium, you must provide a token to unlock it.',
+                        ],
+                    ]);
+            }
+
+            // check if there's a valid subscription for the provided token
+            if (($subscription = (new SubscriptionModel())->validateSubscription(
+                $this->episode->podcast->handle,
+                $token
+            )) === null) {
+                return $this->response->setStatusCode(401, 'Invalid token!')
+                    ->setJSON([
+                        'errors' => [
+                            'status' => 401,
+                            'title' => 'Unauthorized',
+                            'detail' => 'Invalid token!',
+                        ],
+                    ]);
+            }
+        }
+
+        $session = Services::session();
+        $session->start();
+
+        $serviceName = '';
+        if ($this->request->getGet('_from')) {
+            $serviceName = $this->request->getGet('_from');
+        } elseif ($session->get('embed_domain') !== null) {
+            $serviceName = $session->get('embed_domain');
+        } elseif ($session->get('referer') !== null && $session->get('referer') !== '- Direct -') {
+            $serviceName = parse_url((string) $session->get('referer'), PHP_URL_HOST);
+        }
+
+        $audioFileSize = $this->episode->audio->file_size;
+        $audioFileHeaderSize = $this->episode->audio->header_size;
+        $audioDuration = $this->episode->audio->duration;
+
+        // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
+        // - if audio is less than or equal to 60s, then take the audio file_size
+        // - if audio is more than 60s, then take the audio file_header_size + 60s
+        $bytesThreshold = $audioDuration <= 60
+            ? $audioFileSize
+            : $audioFileHeaderSize +
+                (int) floor((($audioFileSize - $audioFileHeaderSize) / $audioDuration) * 60);
+
+        helper('analytics');
+        podcast_hit(
+            $this->episode->podcast_id,
+            $this->episode->id,
+            $bytesThreshold,
+            $audioFileSize,
+            $audioDuration,
+            $this->episode->published_at->getTimestamp(),
+            $serviceName,
+            $subscription !== null ? $subscription->id : null
+        );
+
+        $analyticsConfig = config('Analytics');
+
+        return redirect()->to($analyticsConfig->getAudioUrl($this->episode, $this->request->getGet()));
+    }
 }
diff --git a/app/Controllers/FeedController.php b/app/Controllers/FeedController.php
index e323ede85d765b951d0d7f3910c3fb67b539e6e5..290de8c476515f3176090a01f98be2cb0fd5892e 100644
--- a/app/Controllers/FeedController.php
+++ b/app/Controllers/FeedController.php
@@ -3,14 +3,13 @@
 declare(strict_types=1);
 
 /**
- * @copyright  2020 Ad Aures
+ * @copyright  2022 Ad Aures
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
  */
 
 namespace App\Controllers;
 
-use App\Entities\Podcast;
 use App\Models\EpisodeModel;
 use App\Models\PodcastModel;
 use CodeIgniter\Controller;
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 56d8af8a41c1a6851e8b7066383198c924f9ea7d..ffe0b33ec482b4ad47c9acd1f4591d9be589546b 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -44,7 +44,7 @@ use RuntimeException;
  * @property string $title
  * @property int $audio_id
  * @property Audio $audio
- * @property string $audio_analytics_url
+ * @property string $audio_url
  * @property string $audio_web_url
  * @property string $audio_opengraph_url
  * @property string|null $description Holds text only description, striped of any markdown or html special characters
@@ -93,7 +93,7 @@ class Episode extends Entity
 
     protected ?Audio $audio = null;
 
-    protected string $audio_analytics_url;
+    protected string $audio_url;
 
     protected string $audio_web_url;
 
@@ -335,36 +335,19 @@ class Episode extends Entity
         return $this->chapters;
     }
 
-    public function getAudioAnalyticsUrl(): string
+    public function getAudioUrl(): string
     {
-        helper('analytics');
-
-        return generate_episode_analytics_url(
-            $this->podcast_id,
-            $this->id,
-            $this->getPodcast()
-                ->handle,
-            $this->attributes['slug'],
-            $this->getAudio()
-                ->file_extension,
-            $this->getAudio()
-                ->duration,
-            $this->getAudio()
-                ->file_size,
-            $this->getAudio()
-                ->header_size,
-            $this->published_at,
-        );
+        return url_to('episode-audio', $this->getPodcast()->handle, $this->slug);
     }
 
     public function getAudioWebUrl(): string
     {
-        return $this->getAudioAnalyticsUrl() . '?_from=-+Website+-';
+        return $this->getAudioUrl() . '?_from=-+Website+-';
     }
 
     public function getAudioOpengraphUrl(): string
     {
-        return $this->getAudioAnalyticsUrl() . '?_from=-+Open+Graph+-';
+        return $this->getAudioUrl() . '?_from=-+Open+Graph+-';
     }
 
     /**
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index faee3708fbb4f600cb22282160172b40066b87e3..28d81e31513650f0624a4d06fcdc0839ef32dbc3 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -286,7 +286,7 @@ if (! function_exists('get_rss_feed')) {
 
             $enclosure->addAttribute(
                 'url',
-                $episode->audio_analytics_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
+                $episode->audio_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
             );
             $enclosure->addAttribute('length', (string) $episode->audio->file_size);
             $enclosure->addAttribute('type', $episode->audio->file_mimetype);
diff --git a/app/Helpers/seo_helper.php b/app/Helpers/seo_helper.php
index f9d870b00524a83139aa918997e6331bc14bf3d1..6d535739c63db7f4f2b28bda3331146f0b9d8fde 100644
--- a/app/Helpers/seo_helper.php
+++ b/app/Helpers/seo_helper.php
@@ -87,7 +87,7 @@ if (! function_exists('get_episode_metatags')) {
                 'timeRequired' => iso8601_duration($episode->audio->duration),
                 'duration' => iso8601_duration($episode->audio->duration),
                 'associatedMedia' => new Thing('MediaObject', [
-                    'contentUrl' => $episode->audio->file_url,
+                    'contentUrl' => $episode->audio_url,
                 ]),
                 'partOfSeries' => new Thing('PodcastSeries', [
                     'name' => $episode->podcast->title,
diff --git a/app/Libraries/PodcastEpisode.php b/app/Libraries/PodcastEpisode.php
index 9b2bf169b68f190abc359ca5438f8a40a06bb1c1..7388808d9dda45caf91e6908c8b5c93f9548c3c9 100644
--- a/app/Libraries/PodcastEpisode.php
+++ b/app/Libraries/PodcastEpisode.php
@@ -58,13 +58,13 @@ class PodcastEpisode extends ObjectType
 
         // add audio file
         $this->audio = [
-            'id' => $episode->audio->file_url,
+            'id' => $episode->audio_url,
             'type' => 'Audio',
             'name' => esc($episode->title),
             'size' => $episode->audio->file_size,
             'duration' => $episode->audio->duration,
             'url' => [
-                'href' => $episode->audio->file_url,
+                'href' => $episode->audio_url,
                 'type' => 'Link',
                 'mediaType' => $episode->audio->file_mimetype,
             ],
diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php
index 488fe464c2d3827cd5165ca9f5ebb2a89bf8d4d3..e8bcf7421ba36f2cb6a8ced93b15095630e05aae 100644
--- a/modules/Admin/Controllers/PodcastController.php
+++ b/modules/Admin/Controllers/PodcastController.php
@@ -264,6 +264,10 @@ class PodcastController extends BaseController
             $this->request->getPost('other_categories') ?? [],
         );
 
+        // OP3
+        service('settings')
+            ->set('Analytics.enableOP3', $this->request->getPost('enable_op3') === 'yes', 'podcast:' . $newPodcastId);
+
         $db->transComplete();
 
         return redirect()->route('podcast-view', [$newPodcastId])->with(
@@ -373,6 +377,14 @@ class PodcastController extends BaseController
             $this->request->getPost('other_categories') ?? [],
         );
 
+        // enable/disable OP3?
+        service('settings')
+            ->set(
+                'Analytics.enableOP3',
+                $this->request->getPost('enable_op3') === 'yes',
+                'podcast:' . $this->podcast->id
+            );
+
         $db->transComplete();
 
         return redirect()->route('podcast-edit', [$this->podcast->id])->with(
diff --git a/modules/Admin/Language/en/Podcast.php b/modules/Admin/Language/en/Podcast.php
index 426b763b8b2347673d575c04fba3c43e96ff360e..2d46aff53156ca301dceb74443d75da60e0cd6e7 100644
--- a/modules/Admin/Language/en/Podcast.php
+++ b/modules/Admin/Language/en/Podcast.php
@@ -110,6 +110,10 @@ return [
         'premium' => 'Premium',
         'premium_by_default' => 'Episodes must be set as premium by default',
         'premium_by_default_hint' => 'Podcast episodes will be marked as premium by default. You can still choose to set some episodes, trailers or bonuses as public.',
+        'op3' => 'Open Podcast Prefix Project (OP3)',
+        'op3_hint' => 'Value your analytics data with OP3, an open-source and trusted third party analytics service. Share, validate and compare your analytics data with the open podcasting ecosystem.',
+        'op3_enable' => 'Enable OP3 analytics service',
+        'op3_enable_hint' => 'For security reasons, premium episodes\' analytics data will not be shared with OP3.',
         'payment_pointer' => 'Payment Pointer for Web Monetization',
         'payment_pointer_hint' =>
             'This is your where you will receive money thanks to Web Monetization',
diff --git a/modules/Analytics/Config/Analytics.php b/modules/Analytics/Config/Analytics.php
index 6caea6b0c981dfb344098e13386c8cadad6b0e11..2d08ad04193ca717526ce9697399c583c89a926b 100644
--- a/modules/Analytics/Config/Analytics.php
+++ b/modules/Analytics/Config/Analytics.php
@@ -4,7 +4,10 @@ declare(strict_types=1);
 
 namespace Modules\Analytics\Config;
 
+use App\Entities\Episode;
 use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\HTTP\URI;
+use Modules\Analytics\OP3;
 
 class Analytics extends BaseConfig
 {
@@ -39,14 +42,37 @@ class Analytics extends BaseConfig
     public string $salt = '';
 
     /**
-     * get the full audio file url
+     * --------------------------------------------------------------------------
+     * The Open Podcast Prefix Project Config
+     * --------------------------------------------------------------------------
      *
-     * @param string|string[] $audioPath
+     * @var array<string, string>
+     */
+    public array $OP3 = [
+        'host' => 'https://op3.dev/',
+    ];
+
+    public bool $enableOP3 = false;
+
+    /**
+     * get the full audio file url
      */
-    public function getAudioUrl(string | array $audioPath): string
+    public function getAudioUrl(Episode $episode, array $params): string
     {
-        helper('media');
+        helper(['media', 'setting']);
+
+        $audioFileURI = new URI(media_base_url($episode->audio->file_path));
+        $audioFileURI->setQueryArray($params);
+
+        // Wrap episode url with OP3 if episode is public and OP3 is enabled on this podcast
+        if (! $episode->is_premium && service('settings')->get(
+            'Analytics.enableOP3',
+            'podcast:' . $episode->podcast_id
+        )) {
+            $op3 = new OP3($this->OP3);
+            $audioFileURI = new URI($op3->wrap($audioFileURI, $episode));
+        }
 
-        return media_base_url($audioPath);
+        return (string) $audioFileURI;
     }
 }
diff --git a/modules/Analytics/Config/Routes.php b/modules/Analytics/Config/Routes.php
index 5285fbfd34eb9fe72c801daf58e90f5bb5409967..0526f1d8e23431979f21b5242f710339645e9365 100644
--- a/modules/Analytics/Config/Routes.php
+++ b/modules/Analytics/Config/Routes.php
@@ -53,21 +53,12 @@ $routes->group('', [
     $routes->get(config('Analytics')->gateway . '/(:class)/(:filter)', 'AnalyticsController::getData/$1/$2', [
         'as' => 'analytics-data-instance',
     ]);
-    // Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
-    $routes->head(
-        'audio/(:base64)/(:any)',
-        'EpisodeAnalyticsController::hit/$1/$2',
-        [
-            'as' => 'episode-analytics-hit',
-        ],
-    );
-    $routes->get(
-        'audio/(:base64)/(:any)',
-        'EpisodeAnalyticsController::hit/$1/$2',
-        [
-            'as' => 'episode-analytics-hit',
-        ],
-    );
+
+    /**
+     * @deprecated Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
+     */
+    $routes->head('audio/(:base64)/(:any)', 'EpisodeAnalyticsController::hit/$1/$2',);
+    $routes->get('audio/(:base64)/(:any)', 'EpisodeAnalyticsController::hit/$1/$2',);
 });
 
 // Show the Unknown UserAgents
diff --git a/modules/Analytics/Controllers/EpisodeAnalyticsController.php b/modules/Analytics/Controllers/EpisodeAnalyticsController.php
index 8631af4b3e59419a36123021fe9f23d78ee5d2c5..b5ee229ca5b24360bfc6fa76c4be90f6c33937cf 100644
--- a/modules/Analytics/Controllers/EpisodeAnalyticsController.php
+++ b/modules/Analytics/Controllers/EpisodeAnalyticsController.php
@@ -17,13 +17,13 @@ use CodeIgniter\Exceptions\PageNotFoundException;
 use CodeIgniter\HTTP\RedirectResponse;
 use CodeIgniter\HTTP\RequestInterface;
 use CodeIgniter\HTTP\ResponseInterface;
-use Config\Services;
 use Modules\Analytics\Config\Analytics;
-use Modules\PremiumPodcasts\Models\SubscriptionModel;
 use Psr\Log\LoggerInterface;
 
 class EpisodeAnalyticsController extends Controller
 {
+    public mixed $config;
+
     /**
      * An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
      * other controllers that extend Analytics.
@@ -32,7 +32,7 @@ class EpisodeAnalyticsController extends Controller
      */
     protected $helpers = ['analytics'];
 
-    protected Analytics $config;
+    protected Analytics $analyticsConfig;
 
     /**
      * Constructor.
@@ -52,70 +52,26 @@ class EpisodeAnalyticsController extends Controller
         $this->config = config('Analytics');
     }
 
-    public function hit(string $base64EpisodeData, string ...$audioPath): RedirectResponse|ResponseInterface
+    /**
+     * @deprecated Replaced by EpisodeController::audio method
+     */
+    public function hit(string $base64EpisodeData, string ...$audioPath): RedirectResponse
     {
-        $session = Services::session();
-        $session->start();
-
-        $serviceName = '';
-        if ($this->request->getGet('_from')) {
-            $serviceName = $this->request->getGet('_from');
-        } elseif ($session->get('embed_domain') !== null) {
-            $serviceName = $session->get('embed_domain');
-        } elseif ($session->get('referer') !== null && $session->get('referer') !== '- Direct -') {
-            $serviceName = parse_url((string) $session->get('referer'), PHP_URL_HOST);
-        }
-
         $episodeData = unpack(
             'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
             base64_url_decode($base64EpisodeData),
         );
 
-        if (! $episodeData) {
+        if ($episodeData === false) {
             throw PageNotFoundException::forPageNotFound();
         }
 
-        // check if episode is premium?
         $episode = (new EpisodeModel())->getEpisodeById($episodeData['episodeId']);
 
         if (! $episode instanceof Episode) {
-            return $this->response->setStatusCode(404);
-        }
-
-        $subscription = null;
-
-        // check if podcast is already unlocked before any token validation
-        if ($episode->is_premium && ($subscription = service('premium_podcasts')->subscription(
-            $episode->podcast->handle
-        )) === null) {
-            // look for token as GET parameter
-            if (($token = $this->request->getGet('token')) === null) {
-                return $this->response->setStatusCode(
-                    401,
-                    'Episode is premium, you must provide a token to unlock it.'
-                );
-            }
-
-            // check if there's a valid subscription for the provided token
-            if (($subscription = (new SubscriptionModel())->validateSubscription(
-                $episode->podcast->handle,
-                $token
-            )) === null) {
-                return $this->response->setStatusCode(401, 'Invalid token!');
-            }
+            throw PageNotFoundException::forPageNotFound();
         }
 
-        podcast_hit(
-            $episodeData['podcastId'],
-            $episodeData['episodeId'],
-            $episodeData['bytesThreshold'],
-            $episodeData['fileSize'],
-            $episodeData['duration'],
-            $episodeData['publicationDate'],
-            $serviceName,
-            $subscription !== null ? $subscription->id : null
-        );
-
-        return redirect()->to($this->config->getAudioUrl($episode->audio->file_path));
+        return redirect()->route('episode-audio', [$episode->podcast->handle, $episode->slug]);
     }
 }
diff --git a/modules/Analytics/Helpers/analytics_helper.php b/modules/Analytics/Helpers/analytics_helper.php
index f5ef255a8c0ecfc9269f2d5a16df4c6a39af6e2a..cdd5c698fddbf73d0d5aa86de257c20aee5a552f 100644
--- a/modules/Analytics/Helpers/analytics_helper.php
+++ b/modules/Analytics/Helpers/analytics_helper.php
@@ -34,45 +34,6 @@ if (! function_exists('base64_url_decode')) {
     }
 }
 
-if (! function_exists('generate_episode_analytics_url')) {
-    /**
-     * Builds the episode analytics url that redirects to the audio file url after analytics hit.
-     */
-    function generate_episode_analytics_url(
-        int $podcastId,
-        int $episodeId,
-        string $podcastHandle,
-        string $episodeSlug,
-        string $audioExtension,
-        float $audioDuration,
-        int $audioFileSize,
-        int $audioFileHeaderSize,
-        \CodeIgniter\I18n\Time $publicationDate
-    ): string {
-        return url_to(
-            'episode-analytics-hit',
-            base64_url_encode(
-                pack(
-                    'I*',
-                    $podcastId,
-                    $episodeId,
-                    // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
-                    // - if audio is less than or equal to 60s, then take the audio file_size
-                    // - if audio is more than 60s, then take the audio file_header_size + 60s
-                    $audioDuration <= 60
-                        ? $audioFileSize
-                        : $audioFileHeaderSize +
-                            floor((($audioFileSize - $audioFileHeaderSize) / $audioDuration) * 60),
-                    $audioFileSize,
-                    $audioDuration,
-                    $publicationDate->getTimestamp(),
-                ),
-            ),
-            $podcastHandle . '/' . $episodeSlug . '.' . $audioExtension,
-        );
-    }
-}
-
 if (! function_exists('set_user_session_deny_list_ip')) {
     /**
      * Set user country in session variable, for analytic purposes
diff --git a/modules/Analytics/OP3.php b/modules/Analytics/OP3.php
new file mode 100644
index 0000000000000000000000000000000000000000..053ab5b7e7070d7b0be2704f9210a9de3d2321e1
--- /dev/null
+++ b/modules/Analytics/OP3.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright  2021 Ad Aures
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace Modules\Analytics;
+
+use App\Entities\Episode;
+use CodeIgniter\HTTP\URI;
+
+class OP3
+{
+    protected string $host;
+
+    /**
+     * @param array<string, string> $config
+     */
+    public function __construct(array $config)
+    {
+        $this->host = rtrim($config['host'], '/');
+    }
+
+    public function wrap(URI $audioURI, Episode $episode): string
+    {
+        return $this->host . '/e,pg=' . $episode->podcast->guid . '/' . $audioURI;
+    }
+}
diff --git a/themes/cp_admin/episode/soundbites_new.php b/themes/cp_admin/episode/soundbites_new.php
index f5609494fcde68ae9ede14dd718ee6ce31ddf4c1..b686de27fa9e5d59bc9dc3256741af5cb3e5e278 100644
--- a/themes/cp_admin/episode/soundbites_new.php
+++ b/themes/cp_admin/episode/soundbites_new.php
@@ -21,7 +21,7 @@
         class="max-w-sm"
     />
     <audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>" class="mt-8">
-        <audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
+        <audio slot="audio" src="<?= $episode->audio_url ?>" preload="auto">
             Your browser does not support the <code>audio</code> element.
         </audio>
         <input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php
index 3f3ca52a2784de2f89b7f1394c7f91f8e4cd67ec..a2b60bd67b9fcb0b9e18f1e7d04dd07c1cf2ded5 100644
--- a/themes/cp_admin/episode/video_clips_new.php
+++ b/themes/cp_admin/episode/video_clips_new.php
@@ -18,7 +18,7 @@
         <img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" loading="lazy" />
     </video-clip-previewer>
     <audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>">
-        <audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
+        <audio slot="audio" src="<?= $episode->audio_url ?>" preload="auto">
             Your browser does not support the <code>audio</code> element.
         </audio>
         <input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
diff --git a/themes/cp_admin/podcast/create.php b/themes/cp_admin/podcast/create.php
index 9906da3cb0cc9e0e33ed399b97b049f8fd644389..74492e18add0298e4aa18e34a0fd05e2858710b4 100644
--- a/themes/cp_admin/podcast/create.php
+++ b/themes/cp_admin/podcast/create.php
@@ -153,6 +153,14 @@
         <?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
 </Forms.Section>
 
+<Forms.Section
+    title="<?= lang('Podcast.form.op3') ?>"
+    subtitle="<?= lang('Podcast.form.op3_hint') ?>">
+
+    <a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline focus:ring-accent"><Icon glyph="link" class="text-sm"/>op3.dev</a>
+    <Forms.Toggler name="enable_op3" value="yes" checked="false" hint="<?= lang('Podcast.form.op3_enable_hint') ?>"><?= lang('Podcast.form.op3_enable') ?></Forms.Toggler>
+</Forms.Section>
+
 <Forms.Section
     title="<?= lang('Podcast.form.location_section_title') ?>"
     subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
diff --git a/themes/cp_admin/podcast/edit.php b/themes/cp_admin/podcast/edit.php
index 6f01f9d8cffc0c9652b9b610c4e9354a3a5ca1b8..4995fd7f55a3f3c3aa4ea4d57feb066c11cba39b 100644
--- a/themes/cp_admin/podcast/edit.php
+++ b/themes/cp_admin/podcast/edit.php
@@ -174,6 +174,15 @@
         <?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
 </Forms.Section>
 
+<Forms.Section
+    title="<?= lang('Podcast.form.op3') ?>"
+    subtitle="<?= lang('Podcast.form.op3_hint') ?>">
+
+    <a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline focus:ring-accent"><Icon glyph="link" class="text-sm"/>op3.dev</a>
+    <Forms.Toggler name="enable_op3" value="yes" checked="<?= service('settings')
+            ->get('Analytics.enableOP3', 'podcast:' . $podcast->id) ? 'true' : 'false' ?>" hint="<?= lang('Podcast.form.op3_enable_hint') ?>"><?= lang('Podcast.form.op3_enable') ?></Forms.Toggler>
+</Forms.Section>
+
 <Forms.Section
     title="<?= lang('Podcast.form.location_section_title') ?>"
     subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
diff --git a/themes/cp_app/embed.php b/themes/cp_app/embed.php
index 072160b96814e9c17c6b6c048fac0334ae13452e..afc1c05de3a7ce85666e6c58f7746df9576a8a8e 100644
--- a/themes/cp_app/embed.php
+++ b/themes/cp_app/embed.php
@@ -45,7 +45,7 @@
                 style="--vm-player-box-shadow:0; --vm-player-theme: hsl(var(--color-accent-base)); --vm-control-focus-color: hsl(var(--color-accent-contrast)); --vm-control-spacing: 4px; --vm-menu-item-focus-bg: hsl(var(--color-background-highlight)); --vm-control-icon-size: 24px; <?= str_ends_with($theme, 'transparent') ? '--vm-controls-bg: transparent;' : '' ?>"
             >
             <vm-audio preload="none">
-                <?php $source = auth()->loggedIn() ? $episode->audio->file_url : $episode->audio_analytics_url .
+                <?php $source = auth()->loggedIn() ? $episode->audio_url : $episode->audio_url .
                     (isset($_SERVER['HTTP_REFERER'])
                         ? '?_from=' .
                             parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)