Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Show changes
Showing
with 1017 additions and 484 deletions
......@@ -2,20 +2,31 @@
declare(strict_types=1);
use App\Entities\Person;
use App\Entities\Podcast;
use Cocur\Slugify\Slugify;
use Config\Images;
use Modules\Media\Entities\Image;
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (! function_exists('get_browser_language')) {
/**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`. Returns Castopod's default
* locale if `HTTP_ACCEPT_LANGUAGE` is null.
*
* @return string ISO 639-1 language code
*/
function get_browser_language(string $httpAcceptLanguage): string
function get_browser_language(?string $httpAcceptLanguage = null): string
{
if ($httpAcceptLanguage === null) {
return config('App')->defaultLocale;
}
$langs = explode(',', $httpAcceptLanguage);
return substr($langs[0], 0, 2);
......@@ -30,105 +41,8 @@ if (! function_exists('slugify')) {
$text = substr($text, 0, strrpos(substr($text, 0, $maxLength), ' '));
}
// replace non letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$unwanted = [
'Š' => 'S',
'š' => 's',
'Đ' => 'Dj',
'đ' => 'dj',
'Ž' => 'Z',
'ž' => 'z',
'Č' => 'C',
'č' => 'c',
'Ć' => 'C',
'ć' => 'c',
'À' => 'A',
'Á' => 'A',
'Â' => 'A',
'Ã' => 'A',
'Ä' => 'A',
'Å' => 'A',
'Æ' => 'AE',
'Ç' => 'C',
'È' => 'E',
'É' => 'E',
'Ê' => 'E',
'Ë' => 'E',
'Ì' => 'I',
'Í' => 'I',
'Î' => 'I',
'Ï' => 'I',
'Ñ' => 'N',
'Ò' => 'O',
'Ó' => 'O',
'Ô' => 'O',
'Õ' => 'O',
'Ö' => 'O',
'Ø' => 'O',
'Œ' => 'OE',
'Ù' => 'U',
'Ú' => 'U',
'Û' => 'U',
'Ü' => 'U',
'Ý' => 'Y',
'Þ' => 'B',
'ß' => 'Ss',
'à' => 'a',
'á' => 'a',
'â' => 'a',
'ã' => 'a',
'ä' => 'a',
'å' => 'a',
'æ' => 'ae',
'ç' => 'c',
'è' => 'e',
'é' => 'e',
'ê' => 'e',
'ë' => 'e',
'ì' => 'i',
'í' => 'i',
'î' => 'i',
'ï' => 'i',
'ð' => 'o',
'ñ' => 'n',
'ò' => 'o',
'ó' => 'o',
'ô' => 'o',
'õ' => 'o',
'ö' => 'o',
'ø' => 'o',
'œ' => 'OE',
'ù' => 'u',
'ú' => 'u',
'û' => 'u',
'ý' => 'y',
'þ' => 'b',
'ÿ' => 'y',
'Ŕ' => 'R',
'ŕ' => 'r',
'/' => '-',
' ' => '-',
];
$text = strtr($text, $unwanted);
// transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
// remove unwanted characters
$text = preg_replace('~[^\-\w]+~', '', $text);
// trim
$text = trim($text, '-');
// remove duplicate -
$text = preg_replace('~-+~', '-', $text);
// lowercase
$text = strtolower($text);
return $text;
$slugify = new Slugify();
return $slugify->slugify($text);
}
}
......@@ -151,19 +65,21 @@ if (! function_exists('format_duration')) {
if ($seconds < 60) {
return '0:' . sprintf('%02d', $seconds);
}
if ($seconds < 3600) {
// < 1 hour: returns MM:SS
return ltrim(gmdate('i:s', $seconds), '0');
}
if ($seconds < 36000) {
// < 10 hours: returns H:MM:SS
return ltrim(gmdate('H:i:s', $seconds), '0');
}
return gmdate('H:i:s', $seconds);
}
}
if (! function_exists('format_duration_symbol')) {
/**
* Formats duration in seconds to an hh(h) mm(min) ss(s) string. Doesn't show leading zeros if any.
......@@ -177,39 +93,42 @@ if (! function_exists('format_duration_symbol')) {
if ($seconds < 60) {
return $seconds . 's';
}
if ($seconds < 3600) {
// < 1 hour: returns MM:SS
return ltrim(gmdate('i\m\i\n s\s', $seconds), '0');
}
if ($seconds < 36000) {
// < 10 hours: returns H:MM:SS
return ltrim(gmdate('h\h i\min s\s', $seconds), '0');
return ltrim(gmdate('h\h i\m\i\n s\s', $seconds), '0');
}
return gmdate('h\h i\min s\s', $seconds);
return gmdate('h\h i\m\i\n s\s', $seconds);
}
}
//--------------------------------------------------------------------
if (! function_exists('podcast_uuid')) {
/**
* Generate UUIDv5 for podcast. For more information, see
* https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid
*/
function podcast_uuid(string $feedUrl): string
if (! function_exists('generate_random_salt')) {
function generate_random_salt(int $length = 64): string
{
$uuid = service('uuid');
// 'ead4c236-bf58-58c6-a2c6-a6b28d128cb6' is the uuid of the podcast namespace
return $uuid->uuid5('ead4c236-bf58-58c6-a2c6-a6b28d128cb6', $feedUrl)
->toString();
$salt = '';
while (strlen($salt) < $length) {
$charNumber = random_int(33, 126);
// Exclude " ' \ `
if (! in_array($charNumber, [34, 39, 92, 96], true)) {
$salt .= chr($charNumber);
}
}
return $salt;
}
}
//--------------------------------------------------------------------
if (! function_exists('file_upload_max_size')) {
/**
* Returns a file size limit in bytes based on the PHP upload_max_filesize and post_max_size Adapted from:
* https://stackoverflow.com/a/25370978
......@@ -232,6 +151,7 @@ if (! function_exists('file_upload_max_size')) {
$max_size = $upload_max;
}
}
return $max_size;
}
}
......@@ -243,7 +163,7 @@ if (! function_exists('parse_size')) {
$size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size.
if ($unit !== '') {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(1024, (float) stripos('bkmgtpezy', $unit[0])));
return round($size * 1024 ** ((float) stripos('bkmgtpezy', $unit[0])));
}
return round($size);
......@@ -254,16 +174,98 @@ if (! function_exists('format_bytes')) {
/**
* Adapted from https://stackoverflow.com/a/2510459
*/
function formatBytes(float $bytes, int $precision = 2): string
function formatBytes(float $bytes, bool $is_binary = false, int $precision = 2): string
{
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
$units = $is_binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB'] : ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
$bytes /= ($is_binary ? 1024 : 1000) ** $pow;
return round($bytes, $precision) . $units[$pow];
}
}
if (! function_exists('get_site_icon_url')) {
function get_site_icon_url(string $size): string
{
if (config('App')->siteIcon['ico'] === service('settings')->get('App.siteIcon')['ico']) {
// return default site icon url
return base_url(service('settings')->get('App.siteIcon')[$size]);
}
return service('file_manager')->getUrl(service('settings')->get('App.siteIcon')[$size]);
}
}
if (! function_exists('get_podcast_banner')) {
function get_podcast_banner_url(Podcast $podcast, string $size): string
{
if (! $podcast->banner instanceof Image) {
$defaultBanner = config('Images')
->podcastBannerDefaultPaths[service('settings')->get('App.theme')] ?? config(
Images::class,
)->podcastBannerDefaultPaths['default'];
$sizes = config('Images')
->podcastBannerSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default site icon url
return base_url(
change_file_path($defaultBanner['path'], '_' . $size, $sizeConfig['extension'] ?? null),
);
}
$sizeKey = $size . '_url';
return $podcast->banner->{$sizeKey};
}
}
if (! function_exists('get_podcast_banner_mimetype')) {
function get_podcast_banner_mimetype(Podcast $podcast, string $size): string
{
if (! $podcast->banner instanceof Image) {
$sizes = config('Images')
->podcastBannerSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default site icon url
return array_key_exists('mimetype', $sizeConfig) ? $sizeConfig['mimetype'] : config(
Images::class,
)->podcastBannerDefaultMimeType;
}
$mimetype = $size . '_mimetype';
return $podcast->banner->{$mimetype};
}
}
if (! function_exists('get_avatar_url')) {
function get_avatar_url(Person $person, string $size): string
{
if (! $person->avatar instanceof Image) {
$defaultAvatarPath = config('Images')
->avatarDefaultPath;
$sizes = config('Images')
->personAvatarSizes;
$sizeConfig = $sizes[$size];
helper('filesystem');
// return default avatar url
return base_url(change_file_path($defaultAvatarPath, '_' . $size, $sizeConfig['extension'] ?? null));
}
$sizeKey = $size . '_url';
return $person->avatar->{$sizeKey};
}
}
......@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
......@@ -16,21 +16,36 @@ if (! function_exists('render_page_links')) {
*
* @return string html pages navigation
*/
function render_page_links(string $class = null): string
function render_page_links(?string $class = null, ?string $podcastHandle = null): string
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
if ($podcastHandle !== null) {
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
$links .= anchor(route_to('map'), lang('Page.map.title'), [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
'class' => 'px-2 py-1 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
$links .= anchor($page->link, esc($page->title), [
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
// if set in .env, add legal notice link at the end of page links
if (config('App')->legalNoticeURL !== null) {
$links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
'class' => 'px-2 py-1 underline hover:no-underline',
'target' => '_blank',
'rel' => 'noopener noreferrer',
]);
}
......
......@@ -3,16 +3,21 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Entities\Category;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Libraries\SimpleRSSElement;
use App\Libraries\RssFeed;
use App\Models\PodcastModel;
use CodeIgniter\I18n\Time;
use Config\Mimes;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Transcript;
use Modules\Plugins\Core\Plugins;
use Modules\PremiumPodcasts\Entities\Subscription;
if (! function_exists('get_rss_feed')) {
/**
......@@ -21,91 +26,165 @@ if (! function_exists('get_rss_feed')) {
* @param string $serviceSlug The name of the service that fetches the RSS feed for future reference when the audio file is eventually downloaded
* @return string rss feed as xml
*/
function get_rss_feed(Podcast $podcast, string $serviceSlug = ''): string
{
$episodes = $podcast->episodes;
function get_rss_feed(
Podcast $podcast,
string $serviceSlug = '',
?Subscription $subscription = null,
?string $token = null,
): string {
/** @var Plugins $plugins */
$plugins = service('plugins');
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$episodes = $podcast->episodes;
$podcastNamespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$rss = new RssFeed();
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunesNamespace}' xmlns:podcast='{$podcastNamespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
);
$plugins->rssBeforeChannel($podcast);
$channel = $rss->addChild('channel');
$atomLink = $channel->addChild('atom:link', null, 'http://www.w3.org/2005/Atom');
$atomLink = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
$atomLink->addAttribute('href', $podcast->feed_url);
$atomLink->addAttribute('rel', 'self');
$atomLink->addAttribute('type', 'application/rss+xml');
// websub: add links to hubs defined in config
$websubHubs = config('WebSub')
->hubs;
foreach ($websubHubs as $websubHub) {
$atomLinkHub = $channel->addChild('link', null, RssFeed::ATOM_NAMESPACE);
$atomLinkHub->addAttribute('href', $websubHub);
$atomLinkHub->addAttribute('rel', 'hub');
$atomLinkHub->addAttribute('type', 'application/rss+xml');
}
if ($podcast->new_feed_url !== null) {
$channel->addChild('new-feed-url', $podcast->new_feed_url, $itunesNamespace);
$channel->addChild('new-feed-url', $podcast->new_feed_url, RssFeed::ITUNES_NAMESPACE);
}
// the last build date corresponds to the creation of the feed.xml cache
$channel->addChild('lastBuildDate', (new Time('now'))->format(DATE_RFC1123));
$channel->addChild('generator', 'Castopod Host - https://castopod.org/');
$channel->addChild('generator', 'Castopod - https://castopod.org/');
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$channel->addChild('guid', $podcast->guid, $podcastNamespace);
if ($podcast->guid === '') {
// FIXME: guid shouldn't be empty here as it should be filled upon Podcast creation
$uuid = service('uuid');
// 'ead4c236-bf58-58c6-a2c6-a6b28d128cb6' is the uuid of the podcast namespace
$podcast->guid = $uuid->uuid5('ead4c236-bf58-58c6-a2c6-a6b28d128cb6', $podcast->feed_url)
->toString();
(new PodcastModel())->save($podcast);
}
$channel->addChild('guid', $podcast->guid, RssFeed::PODCAST_NAMESPACE);
$channel->addChild('title', $podcast->title, null, false);
$channel->addChildWithCDATA('description', $podcast->description_html);
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
$itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
$channel->addChild('language', $podcast->language_code);
if ($podcast->location !== null) {
$locationElement = $channel->addChild(
'location',
htmlspecialchars($podcast->location->name),
$podcastNamespace,
);
if ($podcast->location instanceof Location) {
$locationElement = $channel->addChild('location', $podcast->location->name, RssFeed::PODCAST_NAMESPACE);
if ($podcast->location->geo !== null) {
$locationElement->addAttribute('geo', $podcast->location->geo);
}
if ($podcast->location->osm !== null) {
$locationElement->addAttribute('osm', $podcast->location->osm);
}
}
if ($podcast->payment_pointer !== null) {
$valueElement = $channel->addChild('value', null, $podcastNamespace);
$valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', '');
$valueElement->addAttribute('suggested', '');
$recipientElement = $valueElement->addChild('valueRecipient', null, $podcastNamespace);
$recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'ILP');
$recipientElement->addAttribute('address', $podcast->payment_pointer);
$recipientElement->addAttribute('split', '100');
}
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', $podcastNamespace)
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
->addAttribute('owner', $podcast->owner_email);
if ($podcast->imported_feed_url !== null) {
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace);
$channel->addChild('previousUrl', $podcast->imported_feed_url, RssFeed::PODCAST_NAMESPACE);
}
foreach ($podcast->podcasting_platforms as $podcastingPlatform) {
$podcastingPlatformElement = $channel->addChild('id', null, $podcastNamespace);
$podcastingPlatformElement = $channel->addChild('id', null, RssFeed::PODCAST_NAMESPACE);
$podcastingPlatformElement->addAttribute('platform', $podcastingPlatform->slug);
if ($podcastingPlatform->account_id !== null) {
$podcastingPlatformElement->addAttribute('id', $podcastingPlatform->account_id);
}
if ($podcastingPlatform->link_url !== null) {
$podcastingPlatformElement->addAttribute('url', htmlspecialchars($podcastingPlatform->link_url));
$podcastingPlatformElement->addAttribute('url', $podcastingPlatform->link_url);
}
}
$castopodSocialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
$castopodSocialElement->addAttribute('priority', '1');
$castopodSocialElement->addAttribute('platform', 'castopod');
$castopodSocialElement->addAttribute('protocol', 'activitypub');
$castopodSocialElement->addAttribute('accountId', "@{$podcast->actor->username}@{$podcast->actor->domain}");
$castopodSocialElement->addAttribute('accountUrl', $podcast->link);
foreach ($podcast->social_platforms as $socialPlatform) {
$socialPlatformElement = $channel->addChild('social', $socialPlatform->account_id, $podcastNamespace,);
$socialPlatformElement->addAttribute('platform', $socialPlatform->slug);
$socialElement = $channel->addChild('social', null, RssFeed::PODCAST_NAMESPACE);
$socialElement->addAttribute('priority', '2');
$socialElement->addAttribute('platform', $socialPlatform->slug);
// TODO: get activitypub info somewhere else
if (in_array(
$socialPlatform->slug,
['mastodon', 'peertube', 'funkwhale', 'misskey', 'mobilizon', 'pixelfed', 'plume', 'writefreely'],
true,
)) {
$socialElement->addAttribute('protocol', 'activitypub');
} else {
$socialElement->addAttribute('protocol', $socialPlatform->slug);
}
if ($socialPlatform->account_id !== null) {
$socialElement->addAttribute('accountId', esc($socialPlatform->account_id));
}
if ($socialPlatform->link_url !== null) {
$socialPlatformElement->addAttribute('url', htmlspecialchars($socialPlatform->link_url));
$socialElement->addAttribute('accountUrl', esc($socialPlatform->link_url));
}
if ($socialPlatform->slug === 'mastodon') {
$socialSignUpelement = $socialElement->addChild('socialSignUp', null, RssFeed::PODCAST_NAMESPACE);
$socialSignUpelement->addAttribute('priority', '1');
$socialSignUpelement->addAttribute(
'homeUrl',
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
(string) $socialPlatform->link_url,
PHP_URL_HOST,
) . '/public',
);
$socialSignUpelement->addAttribute(
'signUpUrl',
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
(string) $socialPlatform->link_url,
PHP_URL_HOST,
) . '/auth/sign_up',
);
$castopodSocialSignUpelement = $castopodSocialElement->addChild(
'socialSignUp',
null,
RssFeed::PODCAST_NAMESPACE,
);
$castopodSocialSignUpelement->addAttribute('priority', '1');
$castopodSocialSignUpelement->addAttribute(
'homeUrl',
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
(string) $socialPlatform->link_url,
PHP_URL_HOST,
) . '/public',
);
$castopodSocialSignUpelement->addAttribute(
'signUpUrl',
parse_url((string) $socialPlatform->link_url, PHP_URL_SCHEME) . '://' . parse_url(
(string) $socialPlatform->link_url,
PHP_URL_HOST,
) . '/auth/sign_up',
);
}
}
......@@ -113,23 +192,19 @@ if (! function_exists('get_rss_feed')) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->account_id,
$podcastNamespace,
RssFeed::PODCAST_NAMESPACE,
);
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
if ($fundingPlatform->link_url !== null) {
$fundingPlatformElement->addAttribute('url', htmlspecialchars($fundingPlatform->link_url));
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);
}
}
foreach ($podcast->persons as $person) {
foreach ($person->roles as $role) {
$personElement = $channel->addChild(
'person',
htmlspecialchars($person->full_name),
$podcastNamespace,
);
$personElement = $channel->addChild('person', $person->full_name, RssFeed::PODCAST_NAMESPACE);
$personElement->addAttribute('img', $person->avatar->medium_url);
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
......@@ -137,14 +212,12 @@ if (! function_exists('get_rss_feed')) {
$personElement->addAttribute(
'role',
htmlspecialchars(
lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),
),
lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),
);
$personElement->addAttribute(
'group',
htmlspecialchars(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en'),
);
}
}
......@@ -158,30 +231,26 @@ if (! function_exists('get_rss_feed')) {
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunesNamespace,
RssFeed::ITUNES_NAMESPACE,
);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunesNamespace,
);
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunesNamespace);
$owner->addChild('name', $podcast->owner_name, $itunesNamespace);
$owner = $channel->addChild('owner', null, RssFeed::ITUNES_NAMESPACE);
$owner->addChild('email', $podcast->owner_email, $itunesNamespace);
$owner->addChild('name', $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$owner->addChild('email', $podcast->owner_email, RssFeed::ITUNES_NAMESPACE);
$channel->addChild('type', $podcast->type, $itunesNamespace);
$channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
$podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright);
if ($podcast->is_blocked) {
$channel->addChild('block', 'Yes', $itunesNamespace);
if ($podcast->is_blocked || $subscription instanceof Subscription) {
$channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
if ($podcast->is_completed) {
$channel->addChild('complete', 'Yes', $itunesNamespace);
$channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
$image = $channel->addChild('image');
......@@ -189,46 +258,49 @@ if (! function_exists('get_rss_feed')) {
$image->addChild('title', $podcast->title, null, false);
$image->addChild('link', $podcast->link);
if ($podcast->custom_rss !== null) {
array_to_rss([
'elements' => $podcast->custom_rss,
], $channel);
}
// run plugins hook at the end
$plugins->rssAfterChannel($podcast, $channel);
foreach ($episodes as $episode) {
if ($episode->is_premium && ! $subscription instanceof Subscription) {
continue;
}
$plugins->rssBeforeItem($episode);
$item = $channel->addChild('item');
$item->addChild('title', $episode->title, null, false);
$enclosure = $item->addChild('enclosure');
$enclosureParams = implode('&', array_filter([
$episode->is_premium ? 'token=' . $token : null,
$serviceSlug !== '' ? '_from=' . urlencode($serviceSlug) : null,
]));
$enclosure->addAttribute(
'url',
$episode->audio_file_analytics_url .
($serviceSlug === ''
? ''
: '?_from=' . urlencode($serviceSlug)),
$episode->audio_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
);
$enclosure->addAttribute('length', (string) $episode->audio->file_size);
$enclosure->addAttribute('type', $episode->audio->file_mimetype);
$item->addChild('guid', $episode->guid);
$item->addChild('pubDate', $episode->published_at->format(DATE_RFC1123));
if ($episode->location !== null) {
$locationElement = $item->addChild(
'location',
htmlspecialchars($episode->location->name),
$podcastNamespace,
);
if ($episode->location instanceof Location) {
$locationElement = $item->addChild('location', $episode->location->name, RssFeed::PODCAST_NAMESPACE);
if ($episode->location->geo !== null) {
$locationElement->addAttribute('geo', $episode->location->geo);
}
if ($episode->location->osm !== null) {
$locationElement->addAttribute('osm', $episode->location->osm);
}
}
$item->addChildWithCDATA('description', $episode->getDescriptionHtml($serviceSlug));
$item->addChild('duration', (string) $episode->audio->duration, $itunesNamespace);
$item->addChildWithCDATA('description', $episode->description_html);
$item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
$item->addChild('link', $episode->link);
$episodeItunesImage = $item->addChild('image', null, $itunesNamespace);
$episodeItunesImage = $item->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
$episode->parental_advisory &&
......@@ -237,66 +309,97 @@ if (! function_exists('get_rss_feed')) {
$episode->parental_advisory === 'explicit'
? 'true'
: 'false',
$itunesNamespace,
RssFeed::ITUNES_NAMESPACE,
);
$episode->number &&
$item->addChild('episode', (string) $episode->number, $itunesNamespace);
$item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
$episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $itunesNamespace);
$item->addChild('episodeType', $episode->type, $itunesNamespace);
$item->addChild('season', (string) $episode->season_number, RssFeed::ITUNES_NAMESPACE);
$item->addChild('episodeType', $episode->type, RssFeed::ITUNES_NAMESPACE);
// If episode is of type trailer, add podcast:trailer tag on channel level
if ($episode->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, RssFeed::PODCAST_NAMESPACE);
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
$trailer->addAttribute(
'url',
$episode->audio_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
);
$trailer->addAttribute('length', (string) $episode->audio->file_size);
$trailer->addAttribute('type', $episode->audio->file_mimetype);
if ($episode->season_number !== null) {
$trailer->addAttribute('season', (string) $episode->season_number);
}
}
// add link to episode comments as podcast-activity format
$comments = $item->addChild('comments', null, $podcastNamespace);
$comments = $item->addChild('comments', null, RssFeed::PODCAST_NAMESPACE);
$comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
$comments->addAttribute('contentType', 'application/podcast-activity+json');
if ($episode->transcript !== null) {
$transcriptElement = $item->addChild('transcript', null, $podcastNamespace);
if ($episode->getPosts()) {
$socialInteractUri = $episode->getPosts()[0]
->uri;
$socialInteractElement = $item->addChild('socialInteract', null, RssFeed::PODCAST_NAMESPACE);
$socialInteractElement->addAttribute('uri', $socialInteractUri);
$socialInteractElement->addAttribute('priority', '1');
$socialInteractElement->addAttribute('platform', 'castopod');
$socialInteractElement->addAttribute('protocol', 'activitypub');
$socialInteractElement->addAttribute(
'accountId',
"@{$podcast->actor->username}@{$podcast->actor->domain}",
);
$socialInteractElement->addAttribute(
'pubDate',
$episode->getPosts()[0]
->published_at->format(DateTime::ISO8601),
);
}
if ($episode->transcript instanceof Transcript) {
$transcriptElement = $item->addChild('transcript', null, RssFeed::PODCAST_NAMESPACE);
$transcriptElement->addAttribute('url', $episode->transcript->file_url);
$transcriptElement->addAttribute(
'type',
Mimes::guessTypeFromExtension(
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION)
pathinfo($episode->transcript->file_url, PATHINFO_EXTENSION),
) ?? 'text/html',
);
// Castopod only allows for captions (SubRip files)
$transcriptElement->addAttribute('rel', 'captions');
// TODO: allow for multiple languages
$transcriptElement->addAttribute('language', $podcast->language_code);
}
if ($episode->getChapters() !== null) {
$chaptersElement = $item->addChild('chapters', null, $podcastNamespace);
if ($episode->getChapters() instanceof Chapters) {
$chaptersElement = $item->addChild('chapters', null, RssFeed::PODCAST_NAMESPACE);
$chaptersElement->addAttribute('url', $episode->chapters->file_url);
$chaptersElement->addAttribute('type', 'application/json+chapters');
}
foreach ($episode->soundbites as $soundbite) {
// TODO: differentiate video from soundbites?
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, $podcastNamespace);
$soundbiteElement->addAttribute('start_time', (string) $soundbite->start_time);
$soundbiteElement->addAttribute('duration', (string) $soundbite->duration);
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, RssFeed::PODCAST_NAMESPACE);
$soundbiteElement->addAttribute('startTime', (string) $soundbite->start_time);
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
}
foreach ($episode->persons as $person) {
foreach ($person->roles as $role) {
$personElement = $item->addChild(
'person',
htmlspecialchars($person->full_name),
$podcastNamespace,
);
$personElement = $item->addChild('person', esc($person->full_name), RssFeed::PODCAST_NAMESPACE);
$personElement->addAttribute(
'role',
htmlspecialchars(
lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),
),
esc(lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en')),
);
$personElement->addAttribute(
'group',
htmlspecialchars(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
);
$personElement->addAttribute('img', $person->avatar->medium_url);
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
......@@ -305,14 +408,10 @@ if (! function_exists('get_rss_feed')) {
}
if ($episode->is_blocked) {
$item->addChild('block', 'Yes', $itunesNamespace);
$item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
if ($episode->custom_rss !== null) {
array_to_rss([
'elements' => $episode->custom_rss,
], $item);
}
$plugins->rssAfterItem($episode, $item);
}
return $rss->asXML();
......@@ -323,92 +422,22 @@ if (! function_exists('add_category_tag')) {
/**
* Adds <itunes:category> and <category> tags to node for a given category
*/
function add_category_tag(SimpleXMLElement $node, Category $category): void
function add_category_tag(RssFeed $node, Category $category): void
{
$itunesNamespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunesCategory = $node->addChild('category', '', $itunesNamespace);
$itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategory->addAttribute(
'text',
$category->parent !== null
$category->parent instanceof Category
? $category->parent->apple_category
: $category->apple_category,
);
if ($category->parent !== null) {
$itunesCategoryChild = $itunesCategory->addChild('category', '', $itunesNamespace);
if ($category->parent instanceof Category) {
$itunesCategoryChild = $itunesCategory->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategoryChild->addAttribute('text', $category->apple_category);
$node->addChild('category', $category->parent->apple_category);
}
$node->addChild('category', $category->apple_category);
}
}
if (! function_exists('rss_to_array')) {
/**
* Converts XML to array
*
* FIXME: param should be SimpleRSSElement
*
* @return array<string, mixed>
*/
function rss_to_array(SimpleXMLElement $rssNode): array
{
$nameSpaces = [
'',
'http://www.itunes.com/dtds/podcast-1.0.dtd',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
];
$arrayNode = [];
$arrayNode['name'] = $rssNode->getName();
$arrayNode['namespace'] = $rssNode->getNamespaces(false);
foreach ($rssNode->attributes() as $key => $value) {
$arrayNode['attributes'][$key] = (string) $value;
}
$textcontent = trim((string) $rssNode);
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
}
}
return $arrayNode;
}
}
if (! function_exists('array_to_rss')) {
/**
* Inserts array (converted to XML node) in XML node
*
* @param array<string, mixed> $arrayNode
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
*/
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode): SimpleRSSElement
{
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
$childArrayNode['content'] ?? null,
$childArrayNode['namespace'] === []
? null
: current($childArrayNode['namespace'])
);
if (array_key_exists('attributes', $childArrayNode)) {
foreach (
$childArrayNode['attributes']
as $attributeKey => $attributeValue
) {
$childXmlNode->addAttribute($attributeKey, $attributeValue);
}
}
array_to_rss($childArrayNode, $childXmlNode);
}
}
return $xmlNode;
$node->addChild('category', $category->apple_category);
}
}
......@@ -8,125 +8,135 @@ use App\Entities\EpisodeComment;
use App\Entities\Page;
use App\Entities\Podcast;
use App\Entities\Post;
use Melbahja\Seo\MetaTags;
use App\Libraries\HtmlHead;
use Melbahja\Seo\Schema;
use Melbahja\Seo\Schema\Thing;
use Modules\Fediverse\Entities\PreviewCard;
/**
* @copyright 2021 Podlibre
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (! function_exists('get_podcast_metatags')) {
function get_podcast_metatags(Podcast $podcast, string $page): string
if (! function_exists('set_podcast_metatags')) {
function set_podcast_metatags(Podcast $podcast, string $page): void
{
$category = '';
if ($podcast->category->parent_id !== null) {
$category .= $podcast->category->parent->apple_category . ' › ';
}
$category .= $podcast->category->apple_category;
$schema = new Schema(
new Thing('PodcastSeries', [
'name' => $podcast->title,
'url' => $podcast->link,
'image' => $podcast->cover->feed_url,
'name' => $podcast->title,
'headline' => $podcast->title,
'url' => current_url(),
'sameAs' => $podcast->link,
'identifier' => $podcast->guid,
'image' => $podcast->cover->feed_url,
'description' => $podcast->description,
'webFeed' => $podcast->feed_url,
'author' => new Thing('Person', [
'name' => $podcast->publisher,
]),
])
'webFeed' => $podcast->feed_url,
'accessMode' => 'auditory',
'author' => $podcast->owner_name,
'creator' => $podcast->owner_name,
'publisher' => $podcast->publisher,
'inLanguage' => $podcast->language_code,
'genre' => $category,
]),
);
$metatags = new MetaTags();
/** @var HtmlHead $head */
$head = service('html_head');
$metatags
->title(' ' . $podcast->title . " (@{$podcast->handle})" . ' • ' . lang('Podcast.' . $page))
->description(htmlspecialchars($podcast->description))
->image((string) $podcast->cover->large_url)
$head
->title(sprintf('%s (@%s) • %s', $podcast->title, $podcast->handle, lang('Podcast.' . $page)))
->description(esc($podcast->description))
->image((string) $podcast->cover->og_url)
->canonical((string) current_url())
->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
->og('image:width', (string) config('Images')->podcastCoverSizes['og']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
->og('locale', $podcast->language_code)
->og('site_name', service('settings')->get('App.siteName'))
->push('link', [
'rel' => 'alternate',
->og('site_name', esc(service('settings')->get('App.siteName')))
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'href' => url_to('podcast-activity', $podcast->handle),
]);
if ($podcast->payment_pointer) {
$metatags->meta('monetization', $podcast->payment_pointer);
}
return '<link type="application/rss+xml" rel="alternate" title="' . $podcast->title . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString();
'href' => url_to('podcast-activity', esc($podcast->handle)),
])->appendRawContent('<link type="application/rss+xml" rel="alternate" title="' . esc(
$podcast->title,
) . '" href="' . $podcast->feed_url . '" />' . $schema);
}
}
if (! function_exists('get_episode_metatags')) {
function get_episode_metatags(Episode $episode): string
if (! function_exists('set_episode_metatags')) {
function set_episode_metatags(Episode $episode): void
{
$schema = new Schema(
new Thing('PodcastEpisode', [
'url' => url_to('episode', $episode->podcast->handle, $episode->slug),
'name' => $episode->title,
'image' => $episode->cover->feed_url,
'description' => $episode->description,
'datePublished' => $episode->published_at->format(DATE_ISO8601),
'timeRequired' => iso8601_duration($episode->audio->duration),
'url' => url_to('episode', esc($episode->podcast->handle), $episode->slug),
'name' => $episode->title,
'image' => $episode->cover->feed_url,
'description' => $episode->description,
'datePublished' => $episode->published_at->format(DATE_ATOM),
'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,
'url' => $episode->podcast->link,
'url' => $episode->podcast->link,
]),
])
]),
);
$metatags = new MetaTags();
/** @var HtmlHead $head */
$head = service('html_head');
$metatags
$head
->title($episode->title)
->description(htmlspecialchars($episode->description))
->image((string) $episode->cover->large_url, 'player')
->description(esc($episode->description))
->image((string) $episode->cover->og_url, 'player')
->canonical($episode->link)
->og('site_name', service('settings')->get('App.siteName'))
->og('image:width', (string) config('Images')->podcastCoverSizes['large']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['large']['height'])
->og('site_name', esc(service('settings')->get('App.siteName')))
->og('image:width', (string) config('Images')->podcastCoverSizes['og']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
->og('locale', $episode->podcast->language_code)
->og('audio', $episode->audio_file_opengraph_url)
->og('audio', $episode->audio_opengraph_url)
->og('audio:type', $episode->audio->file_mimetype)
->meta('article:published_time', $episode->published_at->format(DATE_ISO8601))
->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601))
->meta('article:published_time', $episode->published_at->format(DATE_ATOM))
->meta('article:modified_time', $episode->updated_at->format(DATE_ATOM))
->twitter('audio:partner', $episode->podcast->publisher ?? '')
->twitter('audio:artist_name', $episode->podcast->owner_name)
->twitter('audio:artist_name', esc($episode->podcast->owner_name))
->twitter('player', $episode->getEmbedUrl('light'))
->twitter('player:width', (string) config('Embed')->width)
->twitter('player:height', (string) config('Embed')->height)
->push('link', [
'rel' => 'alternate',
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'href' => url_to('episode', $episode->podcast->handle, $episode->slug),
]);
if ($episode->podcast->payment_pointer) {
$metatags->meta('monetization', $episode->podcast->payment_pointer);
}
return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
) . '" title="' . $episode->title . ' oEmbed json" />' . PHP_EOL . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug)
) . '" title="' . $episode->title . ' oEmbed xml" />' . PHP_EOL . $schema->__toString();
'href' => $episode->link,
])
->appendRawContent('<link rel="alternate" type="application/json+oembed" href="' . base_url(
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug),
) . '" title="' . esc(
$episode->title,
) . ' oEmbed json" />' . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug),
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . $schema);
}
}
if (! function_exists('get_post_metatags')) {
function get_post_metatags(Post $post): string
if (! function_exists('set_post_metatags')) {
function set_post_metatags(Post $post): void
{
$socialMediaPosting = new Thing('SocialMediaPosting', [
'@id' => url_to('post', $post->actor->username, $post->id),
'datePublished' => $post->published_at->format(DATE_ISO8601),
'author' => new Thing('Person', [
'@id' => url_to('post', esc($post->actor->username), $post->id),
'datePublished' => $post->published_at->format(DATE_ATOM),
'author' => new Thing('Person', [
'name' => $post->actor->display_name,
'url' => $post->actor->uri,
'url' => $post->actor->uri,
]),
'text' => $post->message,
]);
......@@ -134,16 +144,16 @@ if (! function_exists('get_post_metatags')) {
if ($post->episode_id !== null) {
$socialMediaPosting->__set('sharedContent', new Thing('Audio', [
'headline' => $post->episode->title,
'url' => $post->episode->link,
'author' => new Thing('Person', [
'url' => $post->episode->link,
'author' => new Thing('Person', [
'name' => $post->episode->podcast->owner_name,
]),
]));
} elseif ($post->preview_card !== null) {
} elseif ($post->preview_card instanceof PreviewCard) {
$socialMediaPosting->__set('sharedContent', new Thing('WebPage', [
'headline' => $post->preview_card->title,
'url' => $post->preview_card->url,
'author' => new Thing('Person', [
'url' => $post->preview_card->url,
'author' => new Thing('Person', [
'name' => $post->preview_card->author_name,
]),
]));
......@@ -151,134 +161,132 @@ if (! function_exists('get_post_metatags')) {
$schema = new Schema($socialMediaPosting);
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(lang('Post.title', [
'actorDisplayName' => $post->actor->display_name,
]))
->description($post->message)
->image($post->actor->avatar_image_url)
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'))
->push('link', [
'rel' => 'alternate',
->og('site_name', esc(service('settings')->get('App.siteName')))
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'href' => url_to('post', $post->actor->username, $post->id),
]);
return $metatags->__toString() . PHP_EOL . $schema->__toString();
'href' => url_to('post', esc($post->actor->username), $post->id),
])->appendRawContent((string) $schema);
}
}
if (! function_exists('get_episode_comment_metatags')) {
function get_episode_comment_metatags(EpisodeComment $episodeComment): string
if (! function_exists('set_episode_comment_metatags')) {
function set_episode_comment_metatags(EpisodeComment $episodeComment): void
{
$schema = new Schema(new Thing('SocialMediaPosting', [
'@id' => url_to(
'episode-comment',
$episodeComment->actor->username,
esc($episodeComment->actor->username),
$episodeComment->episode->slug,
$episodeComment->id
$episodeComment->id,
),
'datePublished' => $episodeComment->created_at->format(DATE_ISO8601),
'author' => new Thing('Person', [
'datePublished' => $episodeComment->created_at->format(DATE_ATOM),
'author' => new Thing('Person', [
'name' => $episodeComment->actor->display_name,
'url' => $episodeComment->actor->uri,
'url' => $episodeComment->actor->uri,
]),
'text' => $episodeComment->message,
'text' => $episodeComment->message,
'upvoteCount' => $episodeComment->likes_count,
]));
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(lang('Comment.title', [
'actorDisplayName' => $episodeComment->actor->display_name,
'episodeTitle' => $episodeComment->episode->title,
'episodeTitle' => $episodeComment->episode->title,
]))
->description($episodeComment->message)
->image($episodeComment->actor->avatar_image_url)
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'))
->push('link', [
'rel' => 'alternate',
->og('site_name', esc(service('settings')->get('App.siteName')))
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'href' => url_to(
'episode-comment',
$episodeComment->actor->username,
$episodeComment->episode->slug,
$episodeComment->id
$episodeComment->id,
),
]);
return $metatags->__toString() . PHP_EOL . $schema->__toString();
])->appendRawContent((string) $schema);
}
}
if (! function_exists('get_follow_metatags')) {
function get_follow_metatags(Actor $actor): string
if (! function_exists('set_follow_metatags')) {
function set_follow_metatags(Actor $actor): void
{
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(lang('Podcast.followTitle', [
'actorDisplayName' => $actor->display_name,
]))
->description($actor->summary)
->image($actor->avatar_image_url)
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'));
return $metatags->__toString();
->og('site_name', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('get_remote_actions_metatags')) {
function get_remote_actions_metatags(Post $post, string $action): string
if (! function_exists('set_remote_actions_metatags')) {
function set_remote_actions_metatags(Post $post, string $action): void
{
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(lang('Fediverse.' . $action . '.title', [
'actorDisplayName' => $post->actor->display_name,
],))
->description($post->message)
->image($post->actor->avatar_image_url)
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'));
return $metatags->__toString();
->og('site_name', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('get_home_metatags')) {
function get_home_metatags(): string
if (! function_exists('set_home_metatags')) {
function set_home_metatags(): void
{
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(service('settings')->get('App.siteName'))
->description(service('settings')->get('App.siteDescription'))
->image(service('settings')->get('App.siteIcon')['512'])
->description(esc(service('settings')->get('App.siteDescription')))
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'));
->og('site_name', esc(service('settings')->get('App.siteName')));
return $metatags->__toString();
}
}
if (! function_exists('get_page_metatags')) {
function get_page_metatags(Page $page): string
if (! function_exists('set_page_metatags')) {
function set_page_metatags(Page $page): void
{
$metatags = new MetaTags();
$metatags
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
'settings'
)->get('App.siteName')
'settings',
)->get('App.siteName'),
)
->description(service('settings')->get('App.siteDescription'))
->image(service('settings')->get('App.siteIcon')['512'])
->description(esc(service('settings')->get('App.siteDescription')))
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', service('settings')->get('App.siteName'));
->og('site_name', esc(service('settings')->get('App.siteName')));
return $metatags->__toString();
}
}
......@@ -287,7 +295,7 @@ if (! function_exists('iso8601_duration')) {
function iso8601_duration(float $seconds): string
{
$days = floor($seconds / 86400);
$seconds %= 86400;
$seconds = (int) $seconds % 86400;
$hours = floor($seconds / 3600);
$seconds %= 3600;
......
......@@ -3,44 +3,26 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (! function_exists('icon')) {
/**
* Returns the inline svg icon
*
* @param string $name name of the icon file without the .svg extension
* @param string $class to be added to the svg string
* @return string svg contents
*/
function icon(string $name, string $class = ''): string
{
$svgContents = file_get_contents('assets/icons/' . $name . '.svg');
if ($class !== '') {
$svgContents = str_replace('<svg', '<svg class="' . $class . '"', $svgContents);
}
return $svgContents;
}
}
if (! function_exists('svg')) {
/**
* Returns the inline svg image
*
* @param string $name name of the image file without the .svg extension
* @param string $class to be added to the svg string
* @param string|null $class to be added to the svg string
* @return string svg contents
*/
function svg(string $name, ?string $class = null): string
{
$svgContents = file_get_contents('assets/images/' . $name . '.svg');
if ($class) {
$svgContents = str_replace('<svg', '<svg class="' . $class . '"', $svgContents);
return str_replace('<svg', '<svg class="' . $class . '"', $svgContents);
}
return $svgContents;
}
}
......@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
......@@ -16,13 +16,14 @@ if (! function_exists('host_url')) {
*/
function host_url(): ?string
{
if (isset($_SERVER['HTTP_HOST'])) {
$superglobals = service('superglobals');
if ($superglobals->server('HTTP_HOST') !== null) {
$protocol =
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
$_SERVER['SERVER_PORT'] === 443
($superglobals->server('HTTPS') !== null && $superglobals->server('HTTPS') !== 'off') ||
(int) $superglobals->server('SERVER_PORT') === 443
? 'https://'
: 'http://';
return $protocol . $_SERVER['HTTP_HOST'] . '/';
return $protocol . $superglobals->server('HTTP_HOST') . '/';
}
return null;
......@@ -31,6 +32,24 @@ if (! function_exists('host_url')) {
//--------------------------------------------------------------------
/**
* Return the host URL to use in views
*/
if (! function_exists('current_domain')) {
/**
* Returns instance's domain name
*/
function current_domain(): string
{
/** @var URI $uri */
$uri = current_url(true);
return $uri->getHost() . ($uri->getPort() ? ':' . $uri->getPort() : '');
}
}
//--------------------------------------------------------------------
if (! function_exists('extract_params_from_episode_uri')) {
/**
* Returns podcast name and episode slug from episode string
......@@ -58,7 +77,7 @@ if (! function_exists('extract_params_from_episode_uri')) {
return [
'podcastHandle' => $matches['podcastHandle'],
'episodeSlug' => $matches['episodeSlug'],
'episodeSlug' => $matches['episodeSlug'],
];
}
}
+ en/***
+ fr/***
+ pl/***
+ de/***
+ pt-br/***
+ nn-no/***
+ es/***
+ zh-hans/***
+ ca/***
+ br/***
+ sr-latn/***
- **
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'title' => "تعليق {actorDisplayName} على {episodeTitle}",
'back_to_comments' => 'العودة إلى التعليقات',
'form' => [
'episode_message_placeholder' => 'أكتب تعليقاً…',
'reply_to_placeholder' => 'رد على @{actorUsername}',
'submit' => 'ارسل',
'submit_reply' => 'رد',
],
'likes' => '{numberOfLikes, plural,
one {# like}
other {# likes}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
}',
'like' => 'Like',
'reply' => 'رد',
'view_replies' => 'View replies ({numberOfReplies})',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'احذف التعليق',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'yes' => 'نعم',
'no' => 'لا',
'cancel' => 'ألغِ',
'optional' => 'اختياري',
'close' => 'أغلق',
'home' => 'الرئيسية',
'explicit' => 'Explicit',
'powered_by' => 'Powered by {castopod}',
'go_back' => 'العودة',
'play_episode_button' => [
'play' => 'تشغيل',
'playing' => 'Playing',
],
'read_more' => 'اقرأ المزيد',
'read_less' => 'Read less',
'see_more' => 'الاطّلاع على المزيد',
'see_less' => 'See less',
'legal_notice' => 'Legal notice',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'season' => 'الموسم {seasonNumber}',
'season_abbr' => 'م{seasonNumber}',
'number' => 'الحلقة {episodeNumber}',
'number_abbr' => 'الحلقة {episodeNumber}',
'season_episode' => 'الموسم {seasonNumber} الحلقة {episodeNumber}',
'season_episode_abbr' => 'م{seasonNumber}:ح{episodeNumber}',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => 'أشخاص',
'back_to_episodes' => 'العودة إلى حلقات {podcast}',
'comments' => 'التعليقات',
'activity' => 'النشاط',
'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'وصف الحلقة',
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
other {# comments}
}',
'all_podcast_episodes' => 'كافة حلقات البودكاست',
'back_to_podcast' => 'العودة إلى البودكاست',
'preview' => [
'title' => 'Preview',
'not_published' => 'Not published',
'text' => '{publication_status, select,
published {This episode is not yet published.}
scheduled {This episode is scheduled for publication on {publication_date}.}
with_podcast {This episode will be published at the same time as the podcast.}
other {This episode is not yet published.}
}',
'publish' => 'Publish',
'publish_edit' => 'Edit publication',
],
'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
];
<?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/
*/
return [
'your_handle' => 'Your handle',
'your_handle_hint' => 'Enter the @username@domain you want to act from.',
'follow' => [
'label' => 'تابِع',
'title' => 'تابع {actorDisplayName}',
'subtitle' => 'إنك بصدد متابعة:',
'accountNotFound' => 'لا يمكن العثور على الحساب.',
'remoteFollowNotAllowed' => 'Seems like the account server does not allow remote follows…',
'submit' => 'اتمم المتابعة',
],
'favourite' => [
'title' => "Favourite {actorDisplayName}'s post",
'subtitle' => 'You are going to favourite:',
'submit' => 'Proceed to favourite',
],
'reblog' => [
'title' => "Share {actorDisplayName}'s post",
'subtitle' => 'You are going to share:',
'submit' => 'اتمم المشاركة',
],
'reply' => [
'title' => "Reply to {actorDisplayName}'s post",
'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply',
],
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'all_podcasts' => 'كافة البودكاستات',
'sort_by' => 'ترتيب حسب',
'sort_options' => [
'activity' => 'آخر نشاط',
'created_desc' => 'الأحدث أولًا',
'created_asc' => 'الأقدم أولاً',
],
'no_podcast' => 'لا يوجد أي بودكاست',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'back_to_home' => 'العودة إلى الرئيسية',
'map' => [
'title' => 'الخريطة',
'description' => 'Discover podcast episodes on {siteName} that are placed on a map! Travel through the map and listen to episodes that talk about specific locations.',
],
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'feed' => 'RSS Podcast feed',
'season' => 'الموسم {seasonNumber}',
'list_of_episodes_year' => 'حلَقات {year} ({episodeCount})',
'list_of_episodes_season' =>
'الموسم {seasonNumber} الحلقات ({episodeCount})',
'no_episode' => 'لم يتم العثور على أية حلقة!',
'follow' => 'متابعة',
'followTitle' => 'تابع {actorDisplayName} على الفديفرس!',
'followers' => '{numberOfFollowers, plural,
one {# follower}
other {# followers}
}',
'posts' => '{numberOfPosts, plural,
one {# post}
other {# posts}
}',
'links' => 'Links',
'activity' => 'النشاط',
'episodes' => 'الحلقات',
'episodes_title' => 'حلقات {podcastTitle}',
'about' => 'عن',
'stats' => [
'title' => 'الإحصائيات',
'number_of_seasons' => '{0, plural,
one {# season}
other {# seasons}
}',
'number_of_episodes' => '{0, plural,
one {# episode}
other {# episodes}
}',
'first_published_at' => 'First episode published on {0, date, medium}',
],
'sponsor' => 'الراعي',
'funding_links' => 'Funding links for {podcastTitle}',
'find_on' => 'Find {podcastTitle} on',
'listen_on' => 'Listen on',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => 'أشخاص',
'castopod_website' => 'Castopod (website)',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'title' => "{actorDisplayName}'s post",
'back_to_actor_posts' => 'العودة إلى منشورات {actor}',
'actor_shared' => 'شاركه {actor}',
'reply_to' => 'رد على @{actorUsername}',
'form' => [
'message_placeholder' => 'اكتب رسالة…',
'episode_message_placeholder' => 'Write a message for the episode…',
'episode_url_placeholder' => 'الوصلة الشبكية للبودكاست',
'reply_to_placeholder' => 'رد على @{actorUsername}',
'submit' => 'ارسل',
'submit_reply' => 'رد',
],
'favourites' => '{numberOfFavourites, plural,
one {# favourite}
other {# favourites}
}',
'reblogs' => '{numberOfReblogs, plural,
one {# share}
other {# shares}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
}',
'expand' => 'Expand post',
'block_actor' => 'احجب المستخدم @{actorUsername}',
'block_domain' => 'احجب النطاق @{actorDomain}',
'delete' => 'احذف المنشور',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'title' => "Evezhiadenn {actorDisplayName} evit {episodeTitle}",
'back_to_comments' => 'Distreiñ d\'an evezhiadennoù',
'form' => [
'episode_message_placeholder' => 'Skrivañ un evezhiadenn…',
'reply_to_placeholder' => 'Respont da @{actorUsername}',
'submit' => 'Kas',
'submit_reply' => 'Respont',
],
'likes' => '{numberOfLikes, plural,
one {# muiañ-karet}
two {# vuiañ-karet}
few {# muiañ-karet}
many {# muiañ-karet}
other {# muiañ-karet}
}',
'replies' => '{numberOfReplies, plural,
one {# respont}
two {# respont}
few {# respont}
many {# respont}
other {# respont}
}',
'like' => 'Muiañ-karet',
'reply' => 'Respont',
'view_replies' => 'Gwelet an evezhiadennoù ({numberOfReplies})',
'block_actor' => 'Stankañ an implijer·ez @{actorUsername}',
'block_domain' => 'Stankañ @{actorDomain}',
'delete' => 'Dilemel an evezhiadenn',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'yes' => 'Ya',
'no' => 'Ket',
'cancel' => 'Nullañ',
'optional' => 'Diret',
'close' => 'Serriñ',
'home' => 'Degemer',
'explicit' => 'Danvez evit an oadourien',
'powered_by' => 'Lusket gant {castopod}',
'go_back' => 'Mont war-gil',
'play_episode_button' => [
'play' => 'Lenn',
'playing' => 'O lenn',
],
'read_more' => 'Lenn muioc\'h',
'read_less' => 'Lenn nebeutoc\'h',
'see_more' => 'Gwelet muioc\'h',
'see_less' => 'Gwelet nebeutoc\'h',
'legal_notice' => 'Evezhiadennoù a-fet lezenn',
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'season' => 'Koulzad {seasonNumber}',
'season_abbr' => 'K{seasonNumber}',
'number' => 'Rann {episodeNumber}',
'number_abbr' => 'R. {episodeNumber}',
'season_episode' => 'Koulzad {seasonNumber} rann {episodeNumber}',
'season_episode_abbr' => 'K{seasonNumber}:R{episodeNumber}',
'persons' => '{personsCount, plural,
one {# den}
two {# zen}
few {# den}
many {# den}
other {# den}
}',
'persons_list' => 'Emellerien·ezed',
'back_to_episodes' => 'Mont da rannoù {podcast}',
'comments' => 'Evezhiadennoù',
'activity' => 'Oberiantiz',
'chapters' => 'Chabistroù',
'transcript' => 'Transcript',
'description' => 'Deskrivadur ar rann',
'number_of_comments' => '{numberOfComments, plural,
one {# evezhiadenn}
two {# evezhiadenn}
few {# evezhiadenn}
many {# evezhiadenn}
other {# evezhiadenn}
}',
'all_podcast_episodes' => 'Holl rannoù ar podkast',
'back_to_podcast' => 'Mont d\'ar podkast en-dro',
'preview' => [
'title' => 'Rakwel',
'not_published' => 'Diembann',
'text' => '{publication_status, select,
published {N\'eo ket bet embannet ar rann-mañ c\'hoazh.}
scheduled {Raktreset eo an embann a-benn an/ar {publication_date}.}
with_podcast {Ar rann-mañ a vo embannet war un dro gant ar podkast.}
other {N\'eo ket bet embannet ar rann-mañ c\'hoazh.}
}',
'publish' => 'Embann',
'publish_edit' => 'Kemmañ an embannadur',
],
'no_chapters' => 'N\'eus chabistr ebet evit ar rann.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
];
<?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/
*/
return [
'your_handle' => 'Ho tornell (ho lesanv)',
'your_handle_hint' => 'Skrivit an @anv@domani a fell deoc\'h ober gantañ.',
'follow' => [
'label' => 'Heuliañ',
'title' => 'Heuliañ {actorDisplayName}',
'subtitle' => 'Emaoc\'h o vont da heuliañ:',
'accountNotFound' => 'N\'eo ket bet kavet ar gont-se.',
'remoteFollowNotAllowed' => 'N\'eo ket aotreet heuliañ a-bell gant dafariad ar gont-se war a seblant…',
'submit' => 'Kenderc\'hel gant an heuliañ',
],
'favourite' => [
'title' => "Ouzhpennañ kemennadenn {actorDisplayName} d'ho re garetañ",
'subtitle' => 'Emaoc\'h o vont da ouzhpennañ d\'ho re garetañ:',
'submit' => 'Kenderc\'hel gant an ouzhpennañ d\'ho re garetañ',
],
'reblog' => [
'title' => "Rannañ kemennadenn {actorDisplayName}",
'subtitle' => 'Emaoc\'h o vont da rannañ:',
'submit' => 'Kenderc\'hel gant ar rannañ',
],
'reply' => [
'title' => "Respont da gemennadenn {actorDisplayName}",
'subtitle' => 'Emaoc\'h o vont da respont da:',
'submit' => 'Kenderc\'hel gant ar respont',
],
];
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'all_podcasts' => 'An holl bodkastoù',
'sort_by' => 'Rummañ dre',
'sort_options' => [
'activity' => 'Oberiantiz nevez',
'created_desc' => 'Ar re nevez da gentañ',
'created_asc' => 'A re goshañ da gentañ',
],
'no_podcast' => 'N\'eo bet kavet podkast ebet',
];