mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-23 16:05:34 +00:00
feat(analytics): add OP3 analytics service option + update episode audio url
This commit is contained in:
parent
7fbbd08da6
commit
16527ed529
@ -194,6 +194,14 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
|||||||
$routes->get('feed', 'FeedController/$1');
|
$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
|
// Other pages
|
||||||
$routes->get('/credits', 'CreditsController', [
|
$routes->get('/credits', 'CreditsController', [
|
||||||
'as' => 'credits',
|
'as' => 'credits',
|
||||||
|
@ -19,12 +19,14 @@ use App\Models\PodcastModel;
|
|||||||
use App\Models\PostModel;
|
use App\Models\PostModel;
|
||||||
use CodeIgniter\Database\BaseBuilder;
|
use CodeIgniter\Database\BaseBuilder;
|
||||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||||
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
use CodeIgniter\HTTP\Response;
|
use CodeIgniter\HTTP\Response;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Config\Services;
|
use Config\Services;
|
||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||||
|
use Modules\PremiumPodcasts\Models\SubscriptionModel;
|
||||||
use SimpleXMLElement;
|
use SimpleXMLElement;
|
||||||
|
|
||||||
class EpisodeController extends BaseController
|
class EpisodeController extends BaseController
|
||||||
@ -329,4 +331,82 @@ class EpisodeController extends BaseController
|
|||||||
->setHeader('Access-Control-Allow-Origin', '*')
|
->setHeader('Access-Control-Allow-Origin', '*')
|
||||||
->setBody($collection->toJSON());
|
->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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,13 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright 2020 Ad Aures
|
* @copyright 2022 Ad Aures
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Entities\Podcast;
|
|
||||||
use App\Models\EpisodeModel;
|
use App\Models\EpisodeModel;
|
||||||
use App\Models\PodcastModel;
|
use App\Models\PodcastModel;
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
|
@ -44,7 +44,7 @@ use RuntimeException;
|
|||||||
* @property string $title
|
* @property string $title
|
||||||
* @property int $audio_id
|
* @property int $audio_id
|
||||||
* @property Audio $audio
|
* @property Audio $audio
|
||||||
* @property string $audio_analytics_url
|
* @property string $audio_url
|
||||||
* @property string $audio_web_url
|
* @property string $audio_web_url
|
||||||
* @property string $audio_opengraph_url
|
* @property string $audio_opengraph_url
|
||||||
* @property string|null $description Holds text only description, striped of any markdown or html special characters
|
* @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 ?Audio $audio = null;
|
||||||
|
|
||||||
protected string $audio_analytics_url;
|
protected string $audio_url;
|
||||||
|
|
||||||
protected string $audio_web_url;
|
protected string $audio_web_url;
|
||||||
|
|
||||||
@ -335,36 +335,19 @@ class Episode extends Entity
|
|||||||
return $this->chapters;
|
return $this->chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAudioAnalyticsUrl(): string
|
public function getAudioUrl(): string
|
||||||
{
|
{
|
||||||
helper('analytics');
|
return url_to('episode-audio', $this->getPodcast()->handle, $this->slug);
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAudioWebUrl(): string
|
public function getAudioWebUrl(): string
|
||||||
{
|
{
|
||||||
return $this->getAudioAnalyticsUrl() . '?_from=-+Website+-';
|
return $this->getAudioUrl() . '?_from=-+Website+-';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAudioOpengraphUrl(): string
|
public function getAudioOpengraphUrl(): string
|
||||||
{
|
{
|
||||||
return $this->getAudioAnalyticsUrl() . '?_from=-+Open+Graph+-';
|
return $this->getAudioUrl() . '?_from=-+Open+Graph+-';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -286,7 +286,7 @@ if (! function_exists('get_rss_feed')) {
|
|||||||
|
|
||||||
$enclosure->addAttribute(
|
$enclosure->addAttribute(
|
||||||
'url',
|
'url',
|
||||||
$episode->audio_analytics_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
|
$episode->audio_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
|
||||||
);
|
);
|
||||||
$enclosure->addAttribute('length', (string) $episode->audio->file_size);
|
$enclosure->addAttribute('length', (string) $episode->audio->file_size);
|
||||||
$enclosure->addAttribute('type', $episode->audio->file_mimetype);
|
$enclosure->addAttribute('type', $episode->audio->file_mimetype);
|
||||||
|
@ -87,7 +87,7 @@ if (! function_exists('get_episode_metatags')) {
|
|||||||
'timeRequired' => iso8601_duration($episode->audio->duration),
|
'timeRequired' => iso8601_duration($episode->audio->duration),
|
||||||
'duration' => iso8601_duration($episode->audio->duration),
|
'duration' => iso8601_duration($episode->audio->duration),
|
||||||
'associatedMedia' => new Thing('MediaObject', [
|
'associatedMedia' => new Thing('MediaObject', [
|
||||||
'contentUrl' => $episode->audio->file_url,
|
'contentUrl' => $episode->audio_url,
|
||||||
]),
|
]),
|
||||||
'partOfSeries' => new Thing('PodcastSeries', [
|
'partOfSeries' => new Thing('PodcastSeries', [
|
||||||
'name' => $episode->podcast->title,
|
'name' => $episode->podcast->title,
|
||||||
|
@ -58,13 +58,13 @@ class PodcastEpisode extends ObjectType
|
|||||||
|
|
||||||
// add audio file
|
// add audio file
|
||||||
$this->audio = [
|
$this->audio = [
|
||||||
'id' => $episode->audio->file_url,
|
'id' => $episode->audio_url,
|
||||||
'type' => 'Audio',
|
'type' => 'Audio',
|
||||||
'name' => esc($episode->title),
|
'name' => esc($episode->title),
|
||||||
'size' => $episode->audio->file_size,
|
'size' => $episode->audio->file_size,
|
||||||
'duration' => $episode->audio->duration,
|
'duration' => $episode->audio->duration,
|
||||||
'url' => [
|
'url' => [
|
||||||
'href' => $episode->audio->file_url,
|
'href' => $episode->audio_url,
|
||||||
'type' => 'Link',
|
'type' => 'Link',
|
||||||
'mediaType' => $episode->audio->file_mimetype,
|
'mediaType' => $episode->audio->file_mimetype,
|
||||||
],
|
],
|
||||||
|
@ -264,6 +264,10 @@ class PodcastController extends BaseController
|
|||||||
$this->request->getPost('other_categories') ?? [],
|
$this->request->getPost('other_categories') ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// OP3
|
||||||
|
service('settings')
|
||||||
|
->set('Analytics.enableOP3', $this->request->getPost('enable_op3') === 'yes', 'podcast:' . $newPodcastId);
|
||||||
|
|
||||||
$db->transComplete();
|
$db->transComplete();
|
||||||
|
|
||||||
return redirect()->route('podcast-view', [$newPodcastId])->with(
|
return redirect()->route('podcast-view', [$newPodcastId])->with(
|
||||||
@ -373,6 +377,14 @@ class PodcastController extends BaseController
|
|||||||
$this->request->getPost('other_categories') ?? [],
|
$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();
|
$db->transComplete();
|
||||||
|
|
||||||
return redirect()->route('podcast-edit', [$this->podcast->id])->with(
|
return redirect()->route('podcast-edit', [$this->podcast->id])->with(
|
||||||
|
@ -110,6 +110,10 @@ return [
|
|||||||
'premium' => 'Premium',
|
'premium' => 'Premium',
|
||||||
'premium_by_default' => 'Episodes must be set as premium by default',
|
'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.',
|
'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' => 'Payment Pointer for Web Monetization',
|
||||||
'payment_pointer_hint' =>
|
'payment_pointer_hint' =>
|
||||||
'This is your where you will receive money thanks to Web Monetization',
|
'This is your where you will receive money thanks to Web Monetization',
|
||||||
|
@ -4,7 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Modules\Analytics\Config;
|
namespace Modules\Analytics\Config;
|
||||||
|
|
||||||
|
use App\Entities\Episode;
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
use CodeIgniter\HTTP\URI;
|
||||||
|
use Modules\Analytics\OP3;
|
||||||
|
|
||||||
class Analytics extends BaseConfig
|
class Analytics extends BaseConfig
|
||||||
{
|
{
|
||||||
@ -39,14 +42,37 @@ class Analytics extends BaseConfig
|
|||||||
public string $salt = '';
|
public string $salt = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the full audio file url
|
* --------------------------------------------------------------------------
|
||||||
|
* The Open Podcast Prefix Project Config
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
*
|
*
|
||||||
* @param string|string[] $audioPath
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
public function getAudioUrl(string | array $audioPath): string
|
public array $OP3 = [
|
||||||
{
|
'host' => 'https://op3.dev/',
|
||||||
helper('media');
|
];
|
||||||
|
|
||||||
return media_base_url($audioPath);
|
public bool $enableOP3 = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the full audio file url
|
||||||
|
*/
|
||||||
|
public function getAudioUrl(Episode $episode, array $params): string
|
||||||
|
{
|
||||||
|
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 (string) $audioFileURI;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,21 +53,12 @@ $routes->group('', [
|
|||||||
$routes->get(config('Analytics')->gateway . '/(:class)/(:filter)', 'AnalyticsController::getData/$1/$2', [
|
$routes->get(config('Analytics')->gateway . '/(:class)/(:filter)', 'AnalyticsController::getData/$1/$2', [
|
||||||
'as' => 'analytics-data-instance',
|
'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)',
|
* @deprecated Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
|
||||||
'EpisodeAnalyticsController::hit/$1/$2',
|
*/
|
||||||
[
|
$routes->head('audio/(:base64)/(:any)', 'EpisodeAnalyticsController::hit/$1/$2',);
|
||||||
'as' => 'episode-analytics-hit',
|
$routes->get('audio/(:base64)/(:any)', 'EpisodeAnalyticsController::hit/$1/$2',);
|
||||||
],
|
|
||||||
);
|
|
||||||
$routes->get(
|
|
||||||
'audio/(:base64)/(:any)',
|
|
||||||
'EpisodeAnalyticsController::hit/$1/$2',
|
|
||||||
[
|
|
||||||
'as' => 'episode-analytics-hit',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the Unknown UserAgents
|
// Show the Unknown UserAgents
|
||||||
|
@ -17,13 +17,13 @@ use CodeIgniter\Exceptions\PageNotFoundException;
|
|||||||
use CodeIgniter\HTTP\RedirectResponse;
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Config\Services;
|
|
||||||
use Modules\Analytics\Config\Analytics;
|
use Modules\Analytics\Config\Analytics;
|
||||||
use Modules\PremiumPodcasts\Models\SubscriptionModel;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class EpisodeAnalyticsController extends Controller
|
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
|
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
|
||||||
* other controllers that extend Analytics.
|
* other controllers that extend Analytics.
|
||||||
@ -32,7 +32,7 @@ class EpisodeAnalyticsController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected $helpers = ['analytics'];
|
protected $helpers = ['analytics'];
|
||||||
|
|
||||||
protected Analytics $config;
|
protected Analytics $analyticsConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@ -52,70 +52,26 @@ class EpisodeAnalyticsController extends Controller
|
|||||||
$this->config = config('Analytics');
|
$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(
|
$episodeData = unpack(
|
||||||
'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
|
'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
|
||||||
base64_url_decode($base64EpisodeData),
|
base64_url_decode($base64EpisodeData),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $episodeData) {
|
if ($episodeData === false) {
|
||||||
throw PageNotFoundException::forPageNotFound();
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if episode is premium?
|
|
||||||
$episode = (new EpisodeModel())->getEpisodeById($episodeData['episodeId']);
|
$episode = (new EpisodeModel())->getEpisodeById($episodeData['episodeId']);
|
||||||
|
|
||||||
if (! $episode instanceof Episode) {
|
if (! $episode instanceof Episode) {
|
||||||
return $this->response->setStatusCode(404);
|
throw PageNotFoundException::forPageNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
$subscription = null;
|
return redirect()->route('episode-audio', [$episode->podcast->handle, $episode->slug]);
|
||||||
|
|
||||||
// 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!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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')) {
|
if (! function_exists('set_user_session_deny_list_ip')) {
|
||||||
/**
|
/**
|
||||||
* Set user country in session variable, for analytic purposes
|
* Set user country in session variable, for analytic purposes
|
||||||
|
32
modules/Analytics/OP3.php
Normal file
32
modules/Analytics/OP3.php
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@
|
|||||||
class="max-w-sm"
|
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-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.
|
Your browser does not support the <code>audio</code> element.
|
||||||
</audio>
|
</audio>
|
||||||
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" loading="lazy" />
|
<img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" loading="lazy" />
|
||||||
</video-clip-previewer>
|
</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-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.
|
Your browser does not support the <code>audio</code> element.
|
||||||
</audio>
|
</audio>
|
||||||
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
||||||
|
@ -153,6 +153,14 @@
|
|||||||
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
|
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
|
||||||
</Forms.Section>
|
</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
|
<Forms.Section
|
||||||
title="<?= lang('Podcast.form.location_section_title') ?>"
|
title="<?= lang('Podcast.form.location_section_title') ?>"
|
||||||
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
|
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
|
||||||
|
@ -174,6 +174,15 @@
|
|||||||
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
|
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
|
||||||
</Forms.Section>
|
</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
|
<Forms.Section
|
||||||
title="<?= lang('Podcast.form.location_section_title') ?>"
|
title="<?= lang('Podcast.form.location_section_title') ?>"
|
||||||
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
|
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
|
||||||
|
@ -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;' : '' ?>"
|
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">
|
<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'])
|
(isset($_SERVER['HTTP_REFERER'])
|
||||||
? '?_from=' .
|
? '?_from=' .
|
||||||
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user