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 1438 additions and 1064 deletions
<?php
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 JamesHeinrich\GetID3\GetID3;
use App\Entities\Episode;
use CodeIgniter\I18n\Time;
use JamesHeinrich\GetID3\WriteTags;
use Modules\Media\FileManagers\FileManagerInterface;
/**
* Gets audio file metadata and ID3 info
*
* @param UploadedFile $file
*
* @return array
*/
function get_file_tags($file)
{
$getID3 = new GetID3();
$FileInfo = $getID3->analyze($file);
return [
'filesize' => $FileInfo['filesize'],
'mime_type' => $FileInfo['mime_type'],
'avdataoffset' => $FileInfo['avdataoffset'],
'playtime_seconds' => $FileInfo['playtime_seconds'],
];
}
if (! function_exists('write_audio_file_tags')) {
/**
* Write audio file metadata / ID3 tags
*/
function write_audio_file_tags(Episode $episode): void
{
helper('media');
/**
* Write audio file metadata / ID3 tags
*
* @param App\Entities\Episode $episode
*
* @return UploadedFile
*/
function write_enclosure_tags($episode)
{
$TextEncoding = 'UTF-8';
$TextEncoding = 'UTF-8';
// Initialize getID3 tag-writing module
$tagwriter = new WriteTags();
$tagwriter->filename = $episode->enclosure_media_path;
// Initialize getID3 tag-writing module
$tagwriter = new WriteTags();
$tagwriter->filename = $episode->audio->file_name;
// set various options (optional)
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
// set various options (optional)
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
$cover = new \CodeIgniter\Files\File($episode->image->id3_path);
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$APICdata = file_get_contents($cover->getRealPath());
$APICdata = (string) $fileManager->getFileContents($episode->cover->id3_key);
// TODO: variables used for podcast specific tags
// $podcast_url = $episode->podcast->link;
// $podcast_feed_url = $episode->podcast->feed_url;
// $episode_media_url = $episode->link;
// TODO: variables used for podcast specific tags
// $podcastUrl = $episode->podcast->link;
// $podcastFeedUrl = $episode->podcast->feed_url;
// $episodeMediaUrl = $episode->link;
// populate data array
$TagData = [
'title' => [$episode->title],
'artist' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'album' => [$episode->podcast->title],
'year' => [
$episode->published_at ? $episode->published_at->format('Y') : '',
],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [strval($episode->number)],
'copyright_message' => [$episode->podcast->copyright],
'publisher' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
// populate data array
$TagData = [
'title' => [esc($episode->title)],
'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
'album' => [esc($episode->podcast->title)],
'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright],
'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url],
// 'podcast' => [],
// 'podcast_identifier' => [$episode_media_url],
// 'podcast_feed' => [$podcast_feed_url],
// 'podcast_description' => [$podcast->description_markdown],
];
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url],
// 'podcast' => [],
// 'podcast_identifier' => [$episode_media_url],
// 'podcast_feed' => [$podcast_feed_url],
// 'podcast_description' => [$podcast->description_markdown],
];
$TagData['attached_picture'][] = [
'picturetypeid' => 2, // Cover. More: module.tag.id3v2.php
'data' => $APICdata,
'description' => 'cover',
'mime' => $cover->getMimeType(),
];
$TagData['attached_picture'][] = [
// picturetypeid == Cover. More: module.tag.id3v2.php
'picturetypeid' => 2,
'data' => $APICdata,
'description' => 'cover',
'mime' => $episode->cover->file_mimetype,
];
$tagwriter->tag_data = $TagData;
$tagwriter->tag_data = $TagData;
// write tags
if ($tagwriter->WriteTags()) {
echo 'Successfully wrote tags<br>';
if (!empty($tagwriter->warnings)) {
echo 'There were some warnings:<br>' .
implode('<br><br>', $tagwriter->warnings);
// write tags
if ($tagwriter->WriteTags()) {
// Successfully wrote tags
if ($tagwriter->warnings !== []) {
log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings));
}
} else {
log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors));
}
} else {
echo 'Failed to write tags!<br>' .
implode('<br><br>', $tagwriter->errors);
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
/**
* Fetches places from Nominatim OpenStreetMap
*
* @param string $locationName
*
* @return array|null
*/
function fetch_osm_location($locationName)
{
$osmObject = null;
if (!empty($locationName)) {
try {
$client = \Config\Services::curlrequest();
$response = $client->request(
'GET',
'https://nominatim.openstreetmap.org/search.php?q=' .
urlencode($locationName) .
'&polygon_geojson=1&format=jsonv2',
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
],
]
);
$places = json_decode($response->getBody(), true);
$osmObject = [
'geo' =>
empty($places[0]['lat']) || empty($places[0]['lon'])
? null
: "geo:{$places[0]['lat']},{$places[0]['lon']}",
'osmid' => empty($places[0]['osm_type'])
? null
: strtoupper(substr($places[0]['osm_type'], 0, 1)) .
$places[0]['osm_id'],
];
} catch (\Exception $e) {
//If things go wrong the show must go on
log_message('critical', $e);
}
}
return $osmObject;
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\ResponseInterface;
/**
* Saves a file to the corresponding podcast folder in `public/media`
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $file
* @param string $podcast_name
* @param string $file_name
*
* @return string The episode's file path in media root
*/
function save_media($file, $folder, $mediaName)
{
$file_name = $mediaName . '.' . $file->getExtension();
$mediaRoot = config('App')->mediaRoot . '/' . $folder;
if (!file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true);
touch($mediaRoot . '/index.html');
}
// move to media folder and overwrite file if already existing
$file->move($mediaRoot . '/', $file_name, true);
return $folder . '/' . $file_name;
}
/**
* @param string $fileUrl
* @return File
*/
function download_file($fileUrl)
{
var_dump($fileUrl);
$client = \Config\Services::curlrequest();
$response = $client->get($fileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
]);
// redirect to new file location
$newFileUrl = $fileUrl;
while (
in_array(
$response->getStatusCode(),
[
ResponseInterface::HTTP_MOVED_PERMANENTLY,
ResponseInterface::HTTP_FOUND,
ResponseInterface::HTTP_SEE_OTHER,
ResponseInterface::HTTP_NOT_MODIFIED,
ResponseInterface::HTTP_TEMPORARY_REDIRECT,
ResponseInterface::HTTP_PERMANENT_REDIRECT,
],
true,
)
) {
$newFileUrl = (string) trim(
$response->getHeader('location')->getValue(),
);
$response = $client->get($newFileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
'http_errors' => false,
]);
}
$tmpFilename =
time() .
'_' .
bin2hex(random_bytes(10)) .
'.' .
pathinfo(parse_url($newFileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
$tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpFilePath, $response->getBody());
return new \CodeIgniter\Files\File($tmpFilePath);
}
/**
* Prefixes the root media path to a given uri
*
* @param mixed $uri URI string or array of URI segments
* @return string
*/
function media_path($uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
return config('App')->mediaRoot . '/' . $uri;
}
/**
* Return the media base URL to use in views
*
* @param mixed $uri URI string or array of URI segments
* @param string $protocol
* @return string
*/
function media_url($uri = '', string $protocol = null): string
{
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
}
function media_base_url($uri = '')
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
return rtrim(config('App')->mediaBaseURL, '/') .
'/' .
config('App')->mediaRoot .
'/' .
$uri;
}
<?php
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/
*/
/**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`
*
* @param mixed $http_accept_language
*
* @return string|null ISO 639-1 language code or null
*/
function get_browser_language($http_accept_language)
{
$langs = explode(',', $http_accept_language);
if (!empty($langs)) {
if (! function_exists('get_browser_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 = null): string
{
if ($httpAcceptLanguage === null) {
return config('App')->defaultLocale;
}
$langs = explode(',', $httpAcceptLanguage);
return substr($langs[0], 0, 2);
}
return null;
}
/**
* Check if a string starts with some characters
*
* @param string $string
* @param string $query
*
* @return bool
*/
function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
}
if (! function_exists('slugify')) {
function slugify(string $text, int $maxLength = 128): string
{
// trim text to the nearest whole word if too long
if (strlen($text) > $maxLength) {
$text = substr($text, 0, strrpos(substr($text, 0, $maxLength), ' '));
}
function slugify($text)
{
if (empty($text)) {
return 'n-a';
$slugify = new Slugify();
return $slugify->slugify($text);
}
// replace non letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$unwanted_array = [
'Š' => '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',
'ý' => 'y',
'þ' => 'b',
'ÿ' => 'y',
'Ŕ' => 'R',
'ŕ' => 'r',
'/' => '-',
' ' => '-',
];
$text = strtr($text, $unwanted_array);
// 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;
}
//--------------------------------------------------------------------
if (!function_exists('format_duration')) {
if (! function_exists('format_duration')) {
/**
* Formats duration in seconds to an hh:mm:ss string
* Formats duration in seconds to an hh:mm:ss string.
*
* ⚠️ This uses php's gmdate function so any duration > 86000 seconds (24 hours) will not be formatted properly.
*
* @param int $seconds seconds to format
* @param string $separator
*/
function format_duration(int $seconds, bool $showLeadingZeros = false): string
{
if ($showLeadingZeros) {
return gmdate('H:i:s', $seconds);
}
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.
*
* ⚠️ This uses php's gmdate function so any duration > 86000 seconds (24 hours) will not be formatted properly.
*
* @return string
* @param int $seconds seconds to format
*/
function format_duration($seconds, $separator = ':')
function format_duration_symbol(int $seconds): string
{
return sprintf(
'%02d%s%02d%s%02d',
floor($seconds / 3600),
$separator,
($seconds / 60) % 60,
$separator,
$seconds % 60
);
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\m\i\n s\s', $seconds), '0');
}
return gmdate('h\h i\m\i\n s\s', $seconds);
}
}
//--------------------------------------------------------------------
if (! function_exists('generate_random_salt')) {
function generate_random_salt(int $length = 64): string
{
$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
*/
function file_upload_max_size(): float
{
static $max_size = -1;
if ($max_size < 0) {
// Start with post_max_size.
$post_max_size = parse_size((string) ini_get('post_max_size'));
if ($post_max_size > 0) {
$max_size = $post_max_size;
}
// If upload_max_size is less, then reduce. Except if upload_max_size is
// zero, which indicates no limit.
$upload_max = parse_size((string) ini_get('upload_max_filesize'));
if ($upload_max > 0 && $upload_max < $max_size) {
$max_size = $upload_max;
}
}
return $max_size;
}
}
if (! function_exists('parse_size')) {
function parse_size(string $size): float
{
$unit = (string) preg_replace('~[^bkmgtpezy]~i', '', $size); // Remove the non-unit characters from the 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 * 1024 ** ((float) stripos('bkmgtpezy', $unit[0])));
}
return round($size);
}
}
if (! function_exists('format_bytes')) {
/**
* Adapted from https://stackoverflow.com/a/2510459
*/
function formatBytes(float $bytes, bool $is_binary = false, int $precision = 2): string
{
$units = $is_binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB'] : ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1);
$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};
}
}
<?php
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\Models\PageModel;
/**
* Returns instance pages as links inside nav tag
*
* @param string $class
* @return string html pages navigation
*/
function render_page_links($class = null)
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
]);
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',
if (! function_exists('render_page_links')) {
/**
* Returns instance pages as links inside nav tag
*
* @return string html pages navigation
*/
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',
]);
}
if ($podcastHandle !== null) {
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
'class' => 'px-2 py-1 underline hover:no-underline',
]);
}
return '<nav class="' . $class . '">' . $links . '</nav>';
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'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',
]);
foreach ($pages as $page) {
$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',
]);
}
return '<nav class="' . $class . '">' . $links . '</nav>';
}
}
<?php
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
/**
* Fetches persons from an episode
*
* @param array $persons
* @param array &$personsArray
*/
function construct_person_array($persons, &$personsArray)
{
foreach ($persons as $person) {
if (array_key_exists($person->person->id, $personsArray)) {
$personsArray[$person->person->id]['roles'] .=
empty($person->person_group) || empty($person->person_role)
? ''
: (empty($personsArray[$person->person->id]['roles'])
? ''
: ', ') .
lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
);
} else {
$personsArray[$person->person->id] = [
'full_name' => $person->person->full_name,
'information_url' => $person->person->information_url,
'thumbnail_url' => $person->person->image->thumbnail_url,
'roles' =>
empty($person->person_group) || empty($person->person_role)
? ''
: lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
),
];
}
}
}
<?php
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\Libraries\SimpleRSSElement;
use App\Entities\Category;
use App\Entities\Location;
use App\Entities\Podcast;
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')) {
/**
* Generates the rss feed for a given podcast entity
*
* @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 = '',
?Subscription $subscription = null,
?string $token = null,
): string {
/** @var Plugins $plugins */
$plugins = service('plugins');
$episodes = $podcast->episodes;
$rss = new RssFeed();
$plugins->rssBeforeChannel($podcast);
$channel = $rss->addChild('channel');
$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');
}
/**
* Generates the rss feed for a given podcast entity
*
* @param App\Entities\Podcast $podcast
* @param string $service 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, $serviceSlug = '')
{
$episodes = $podcast->episodes;
if ($podcast->new_feed_url !== null) {
$channel->addChild('new-feed-url', $podcast->new_feed_url, RssFeed::ITUNES_NAMESPACE);
}
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
// 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 - https://castopod.org/');
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$podcast_namespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
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();
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='$itunes_namespace' xmlns:podcast='$podcast_namespace' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
);
(new PodcastModel())->save($podcast);
}
$channel = $rss->addChild('channel');
$channel->addChild('guid', $podcast->guid, RssFeed::PODCAST_NAMESPACE);
$channel->addChild('title', $podcast->title, null, false);
$channel->addChildWithCDATA('description', $podcast->description_html);
$atom_link = $channel->addChild(
'atom:link',
null,
'http://www.w3.org/2005/Atom',
);
$atom_link->addAttribute('href', $podcast->feed_url);
$atom_link->addAttribute('rel', 'self');
$atom_link->addAttribute('type', 'application/rss+xml');
$itunesImage = $channel->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
if (!empty($podcast->new_feed_url)) {
$channel->addChild(
'new-feed-url',
$podcast->new_feed_url,
$itunes_namespace,
);
}
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
// 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 0.0.0-development - https://castopod.org/',
);
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html);
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
$itunes_image->addAttribute('href', $podcast->image->original_url);
$channel->addChild('language', $podcast->language_code);
if (!empty($podcast->location_name)) {
$locationElement = $channel->addChild(
'location',
htmlspecialchars($podcast->location_name),
$podcast_namespace,
);
if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $podcast->location_geo);
}
if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osm', $podcast->location_osmid);
}
}
if (!empty($podcast->payment_pointer)) {
$valueElement = $channel->addChild('value', null, $podcast_namespace);
$valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', '');
$valueElement->addAttribute('suggested', '');
$recipientElement = $valueElement->addChild(
'valueRecipient',
null,
$podcast_namespace,
);
$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',
$podcast_namespace,
)
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) {
$channel->addChild(
'previousUrl',
$podcast->imported_feed_url,
$podcast_namespace,
);
}
$channel->addChild('language', $podcast->language_code);
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);
}
foreach ($podcast->podcastingPlatforms as $podcastingPlatform) {
$podcastingPlatformElement = $channel->addChild(
'id',
null,
$podcast_namespace,
);
$podcastingPlatformElement->addAttribute(
'platform',
$podcastingPlatform->slug,
);
if (!empty($podcastingPlatform->link_content)) {
$podcastingPlatformElement->addAttribute(
'id',
$podcastingPlatform->link_content,
);
}
if (!empty($podcastingPlatform->link_url)) {
$podcastingPlatformElement->addAttribute(
'url',
htmlspecialchars($podcastingPlatform->link_url),
);
if ($podcast->location->osm !== null) {
$locationElement->addAttribute('osm', $podcast->location->osm);
}
}
}
foreach ($podcast->socialPlatforms as $socialPlatform) {
$socialPlatformElement = $channel->addChild(
'social',
$socialPlatform->link_content,
$podcast_namespace,
);
$socialPlatformElement->addAttribute('platform', $socialPlatform->slug);
if (!empty($socialPlatform->link_url)) {
$socialPlatformElement->addAttribute(
'url',
htmlspecialchars($socialPlatform->link_url),
);
}
}
$channel
->addChild('locked', $podcast->is_locked ? 'yes' : 'no', RssFeed::PODCAST_NAMESPACE)
->addAttribute('owner', $podcast->owner_email);
foreach ($podcast->fundingPlatforms as $fundingPlatform) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->link_content,
$podcast_namespace,
);
$fundingPlatformElement->addAttribute(
'platform',
$fundingPlatform->slug,
);
if (!empty($socialPlatform->link_url)) {
$fundingPlatformElement->addAttribute(
'url',
htmlspecialchars($fundingPlatform->link_url),
);
if ($podcast->imported_feed_url !== null) {
$channel->addChild('previousUrl', $podcast->imported_feed_url, RssFeed::PODCAST_NAMESPACE);
}
}
foreach ($podcast->persons as $podcastPerson) {
$podcastPersonElement = $channel->addChild(
'person',
htmlspecialchars($podcastPerson->person->full_name),
$podcast_namespace,
);
if (
!empty($podcastPerson->person_role) &&
!empty($podcastPerson->person_group)
) {
$podcastPersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
[],
'en',
),
),
);
foreach ($podcast->podcasting_platforms as $podcastingPlatform) {
$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', $podcastingPlatform->link_url);
}
}
if (!empty($podcastPerson->person_group)) {
$podcastPersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
[],
'en',
),
),
);
$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) {
$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) {
$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',
);
}
}
$podcastPersonElement->addAttribute(
'img',
$podcastPerson->person->image->large_url,
);
if (!empty($podcastPerson->person->information_url)) {
$podcastPersonElement->addAttribute(
'href',
$podcastPerson->person->information_url,
foreach ($podcast->funding_platforms as $fundingPlatform) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->account_id,
RssFeed::PODCAST_NAMESPACE,
);
$fundingPlatformElement->addAttribute('platform', $fundingPlatform->slug);
if ($fundingPlatform->link_url !== null) {
$fundingPlatformElement->addAttribute('url', $fundingPlatform->link_url);
}
}
}
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
add_category_tag($channel, $other_category);
}
foreach ($podcast->persons as $person) {
foreach ($person->roles as $role) {
$personElement = $channel->addChild('person', $person->full_name, RssFeed::PODCAST_NAMESPACE);
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace,
);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunes_namespace,
);
$channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunes_namespace);
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
$channel->addChild('type', $podcast->type, $itunes_namespace);
$podcast->copyright && $channel->addChild('copyright', $podcast->copyright);
$podcast->is_blocked &&
$channel->addChild('block', 'Yes', $itunes_namespace);
$podcast->is_completed &&
$channel->addChild('complete', 'Yes', $itunes_namespace);
$image = $channel->addChild('image');
$image->addChild('url', $podcast->image->feed_url);
$image->addChild('title', $podcast->title);
$image->addChild('link', $podcast->link);
if (!empty($podcast->custom_rss)) {
array_to_rss(
[
'elements' => $podcast->custom_rss,
],
$channel,
);
}
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
foreach ($episodes as $episode) {
$item = $channel->addChild('item');
$item->addChild('title', $episode->title);
$enclosure = $item->addChild('enclosure');
$enclosure->addAttribute(
'url',
$episode->enclosure_url .
(empty($serviceSlug)
? ''
: '?_from=' . urlencode($serviceSlug)),
);
$enclosure->addAttribute('length', $episode->enclosure_filesize);
$enclosure->addAttribute('type', $episode->enclosure_mimetype);
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
}
$item->addChild('guid', $episode->guid);
$item->addChild(
'pubDate',
$episode->published_at->format(DATE_RFC1123),
);
if (!empty($episode->location_name)) {
$locationElement = $item->addChild(
'location',
htmlspecialchars($episode->location_name),
$podcast_namespace,
);
if (!empty($episode->location_geo)) {
$locationElement->addAttribute('geo', $episode->location_geo);
}
if (!empty($episode->location_osmid)) {
$locationElement->addAttribute('osm', $episode->location_osmid);
$personElement->addAttribute(
'role',
lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en'),
);
$personElement->addAttribute(
'group',
lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en'),
);
}
}
$item->addChildWithCDATA(
'description',
$episode->getDescriptionHtml($serviceSlug),
);
$item->addChild(
'duration',
$episode->enclosure_duration,
$itunes_namespace,
);
$item->addChild('link', $episode->link);
$episode_itunes_image = $item->addChild(
'image',
null,
$itunes_namespace,
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
add_category_tag($channel, $other_category);
}
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
RssFeed::ITUNES_NAMESPACE,
);
$episode_itunes_image->addAttribute('href', $episode->image->feed_url);
$episode->parental_advisory &&
$item->addChild(
'explicit',
$episode->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace,
);
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$channel->addChild('link', $podcast->link);
$episode->number &&
$item->addChild('episode', $episode->number, $itunes_namespace);
$episode->season_number &&
$item->addChild(
'season',
$episode->season_number,
$itunes_namespace,
);
$item->addChild('episodeType', $episode->type, $itunes_namespace);
$owner = $channel->addChild('owner', null, RssFeed::ITUNES_NAMESPACE);
if ($episode->transcript) {
$transcriptElement = $item->addChild(
'transcript',
null,
$podcast_namespace,
);
$transcriptElement->addAttribute('url', $episode->transcriptUrl);
$transcriptElement->addAttribute(
'type',
Mimes::guessTypeFromExtension(
pathinfo($episode->transcript_uri, PATHINFO_EXTENSION),
),
);
$transcriptElement->addAttribute(
'language',
$podcast->language_code,
);
}
$owner->addChild('name', $podcast->owner_name, RssFeed::ITUNES_NAMESPACE, false);
$owner->addChild('email', $podcast->owner_email, RssFeed::ITUNES_NAMESPACE);
if ($episode->chapters) {
$chaptersElement = $item->addChild(
'chapters',
null,
$podcast_namespace,
);
$chaptersElement->addAttribute('url', $episode->chaptersUrl);
$chaptersElement->addAttribute('type', 'application/json+chapters');
$channel->addChild('type', $podcast->type, RssFeed::ITUNES_NAMESPACE);
$podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright);
if ($podcast->is_blocked || $subscription instanceof Subscription) {
$channel->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
foreach ($episode->soundbites as $soundbite) {
$soundbiteElement = $item->addChild(
'soundbite',
empty($soundbite->label) ? null : $soundbite->label,
$podcast_namespace,
);
$soundbiteElement->addAttribute(
'start_time',
$soundbite->start_time,
);
$soundbiteElement->addAttribute('duration', $soundbite->duration);
if ($podcast->is_completed) {
$channel->addChild('complete', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
foreach ($episode->persons as $episodePerson) {
$episodePersonElement = $item->addChild(
'person',
htmlspecialchars($episodePerson->person->full_name),
$podcast_namespace,
$image = $channel->addChild('image');
$image->addChild('url', $podcast->cover->feed_url);
$image->addChild('title', $podcast->title, null, false);
$image->addChild('link', $podcast->link);
// 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_url . ($enclosureParams === '' ? '' : '?' . $enclosureParams),
);
if (
!empty($episodePerson->person_role) &&
!empty($episodePerson->person_group)
) {
$episodePersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
[],
'en',
),
),
$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 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->description_html);
$item->addChild('duration', (string) round($episode->audio->duration), RssFeed::ITUNES_NAMESPACE);
$item->addChild('link', $episode->link);
$episodeItunesImage = $item->addChild('image', null, RssFeed::ITUNES_NAMESPACE);
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
$episode->parental_advisory &&
$item->addChild(
'explicit',
$episode->parental_advisory === 'explicit'
? 'true'
: 'false',
RssFeed::ITUNES_NAMESPACE,
);
$episode->number &&
$item->addChild('episode', (string) $episode->number, RssFeed::ITUNES_NAMESPACE);
$episode->season_number &&
$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);
}
}
if (!empty($episodePerson->person_group)) {
$episodePersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
[],
'en',
),
),
// add link to episode comments as podcast-activity format
$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->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),
);
}
$episodePersonElement->addAttribute(
'img',
$episodePerson->person->image->large_url,
);
if (!empty($episodePerson->person->information_url)) {
$episodePersonElement->addAttribute(
'href',
$episodePerson->person->information_url,
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),
) ?? 'text/html',
);
// Castopod only allows for captions (SubRip files)
$transcriptElement->addAttribute('rel', 'captions');
// TODO: allow for multiple languages
$transcriptElement->addAttribute('language', $podcast->language_code);
}
}
$episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace);
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');
}
if (!empty($episode->custom_rss)) {
array_to_rss(
[
'elements' => $episode->custom_rss,
],
$item,
);
}
}
foreach ($episode->soundbites as $soundbite) {
// TODO: differentiate video from soundbites?
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, RssFeed::PODCAST_NAMESPACE);
$soundbiteElement->addAttribute('startTime', (string) $soundbite->start_time);
$soundbiteElement->addAttribute('duration', (string) round($soundbite->duration, 3));
}
return $rss->asXML();
}
foreach ($episode->persons as $person) {
foreach ($person->roles as $role) {
$personElement = $item->addChild('person', esc($person->full_name), RssFeed::PODCAST_NAMESPACE);
/**
* Adds <itunes:category> and <category> tags to node for a given category
*
* @param \SimpleXMLElement $node
* @param \App\Entities\Category $category
*
* @return void
*/
function add_category_tag($node, $category)
{
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunes_category = $node->addChild('category', null, $itunes_namespace);
$itunes_category->addAttribute(
'text',
$category->parent
? $category->parent->apple_category
: $category->apple_category,
);
if ($category->parent) {
$itunes_category_child = $itunes_category->addChild(
'category',
null,
$itunes_namespace,
);
$itunes_category_child->addAttribute('text', $category->apple_category);
$node->addChild('category', $category->parent->apple_category);
}
$node->addChild('category', $category->apple_category);
}
$personElement->addAttribute(
'role',
esc(lang("PersonsTaxonomy.persons.{$role->group}.roles.{$role->role}.label", [], 'en')),
);
/**
* Converts XML to array
*
* @param \SimpleRSSElement $xmlNode
*
* @return array
*/
function rss_to_array($xmlNode)
{
$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'] = $xmlNode->getName();
$arrayNode['namespace'] = $xmlNode->getNamespaces(false);
if (count($xmlNode->attributes()) > 0) {
foreach ($xmlNode->attributes() as $key => $value) {
$arrayNode['attributes'][$key] = (string) $value;
}
}
$textcontent = trim((string) $xmlNode);
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
$personElement->addAttribute(
'group',
esc(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
);
$personElement->addAttribute('img', get_avatar_url($person, 'medium'));
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
}
}
}
if ($episode->is_blocked) {
$item->addChild('block', 'Yes', RssFeed::ITUNES_NAMESPACE);
}
$plugins->rssAfterItem($episode, $item);
}
return $rss->asXML();
}
return $arrayNode;
}
/**
* Inserts array (converted to XML node) in XML node
*
* @param array $arrayNode
* @param \SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
*
*/
function array_to_rss($arrayNode, &$xmlNode)
{
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
array_key_exists('content', $childArrayNode)
? $childArrayNode['content']
: null,
empty($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);
if (! function_exists('add_category_tag')) {
/**
* Adds <itunes:category> and <category> tags to node for a given category
*/
function add_category_tag(RssFeed $node, Category $category): void
{
$itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
$itunesCategory->addAttribute(
'text',
$category->parent instanceof Category
? $category->parent->apple_category
: $category->apple_category,
);
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);
}
return $xmlNode;
}
<?php
declare(strict_types=1);
use App\Entities\Actor;
use App\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Page;
use App\Entities\Podcast;
use App\Entities\Post;
use App\Libraries\HtmlHead;
use Melbahja\Seo\Schema;
use Melbahja\Seo\Schema\Thing;
use Modules\Fediverse\Entities\PreviewCard;
/**
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
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,
'headline' => $podcast->title,
'url' => current_url(),
'sameAs' => $podcast->link,
'identifier' => $podcast->guid,
'image' => $podcast->cover->feed_url,
'description' => $podcast->description,
'webFeed' => $podcast->feed_url,
'accessMode' => 'auditory',
'author' => $podcast->owner_name,
'creator' => $podcast->owner_name,
'publisher' => $podcast->publisher,
'inLanguage' => $podcast->language_code,
'genre' => $category,
]),
);
/** @var HtmlHead $head */
$head = service('html_head');
$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['og']['width'])
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
->og('locale', $podcast->language_code)
->og('site_name', esc(service('settings')->get('App.siteName')))
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'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('set_episode_metatags')) {
function set_episode_metatags(Episode $episode): void
{
$schema = new Schema(
new Thing('PodcastEpisode', [
'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_url,
]),
'partOfSeries' => new Thing('PodcastSeries', [
'name' => $episode->podcast->title,
'url' => $episode->podcast->link,
]),
]),
);
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title($episode->title)
->description(esc($episode->description))
->image((string) $episode->cover->og_url, 'player')
->canonical($episode->link)
->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_opengraph_url)
->og('audio:type', $episode->audio->file_mimetype)
->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', esc($episode->podcast->owner_name))
->twitter('player', $episode->getEmbedUrl('light'))
->twitter('player:width', (string) config('Embed')->width)
->twitter('player:height', (string) config('Embed')->height)
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'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('set_post_metatags')) {
function set_post_metatags(Post $post): void
{
$socialMediaPosting = new Thing('SocialMediaPosting', [
'@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,
]),
'text' => $post->message,
]);
if ($post->episode_id !== null) {
$socialMediaPosting->__set('sharedContent', new Thing('Audio', [
'headline' => $post->episode->title,
'url' => $post->episode->link,
'author' => new Thing('Person', [
'name' => $post->episode->podcast->owner_name,
]),
]));
} 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', [
'name' => $post->preview_card->author_name,
]),
]));
}
$schema = new Schema($socialMediaPosting);
/** @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', esc(service('settings')->get('App.siteName')))
->tag('link', null, [
'rel' => 'alternate',
'type' => 'application/activity+json',
'href' => url_to('post', esc($post->actor->username), $post->id),
])->appendRawContent((string) $schema);
}
}
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',
esc($episodeComment->actor->username),
$episodeComment->episode->slug,
$episodeComment->id,
),
'datePublished' => $episodeComment->created_at->format(DATE_ATOM),
'author' => new Thing('Person', [
'name' => $episodeComment->actor->display_name,
'url' => $episodeComment->actor->uri,
]),
'text' => $episodeComment->message,
'upvoteCount' => $episodeComment->likes_count,
]));
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(lang('Comment.title', [
'actorDisplayName' => $episodeComment->actor->display_name,
'episodeTitle' => $episodeComment->episode->title,
]))
->description($episodeComment->message)
->image($episodeComment->actor->avatar_image_url)
->canonical((string) current_url())
->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,
),
])->appendRawContent((string) $schema);
}
}
if (! function_exists('set_follow_metatags')) {
function set_follow_metatags(Actor $actor): void
{
/** @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', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('set_remote_actions_metatags')) {
function set_remote_actions_metatags(Post $post, string $action): void
{
/** @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', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('set_home_metatags')) {
function set_home_metatags(): void
{
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(service('settings')->get('App.siteName'))
->description(esc(service('settings')->get('App.siteDescription')))
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('set_page_metatags')) {
function set_page_metatags(Page $page): void
{
/** @var HtmlHead $head */
$head = service('html_head');
$head
->title(
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
'settings',
)->get('App.siteName'),
)
->description(esc(service('settings')->get('App.siteDescription')))
->image(get_site_icon_url('512'))
->canonical((string) current_url())
->og('site_name', esc(service('settings')->get('App.siteName')));
}
}
if (! function_exists('iso8601_duration')) {
// From https://stackoverflow.com/a/40761380
function iso8601_duration(float $seconds): string
{
$days = floor($seconds / 86400);
$seconds = (int) $seconds % 86400;
$hours = floor($seconds / 3600);
$seconds %= 3600;
$minutes = floor($seconds / 60);
$seconds %= 60;
return sprintf('P%dDT%dH%dM%dS', $days, $hours, $minutes, $seconds);
}
}
<?php
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/
*/
/**
* 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 = '')
{
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
if ($class !== '') {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
}
return $svg_contents;
}
if (! function_exists('svg')) {
/**
* Returns the inline svg image
*
* @param string $name name of the image file without the .svg extension
* @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) {
return str_replace('<svg', '<svg class="' . $class . '"', $svgContents);
}
/**
* 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
* @return string svg contents
*/
function svg($name, $class = null)
{
$svg_contents = file_get_contents('assets/images/' . $name . '.svg');
if ($class) {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
return $svgContents;
}
return $svg_contents;
}
<?php
if (!function_exists('host_url')) {
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use CodeIgniter\HTTP\URI;
if (! function_exists('host_url')) {
/**
* Return the host URL to use in views
*
* @return string|false
*/
function host_url()
function host_url(): ?string
{
if (isset($_SERVER['HTTP_HOST'])) {
$superglobals = service('superglobals');
if ($superglobals->server('HTTP_HOST') !== null) {
$protocol =
(!empty($_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 false;
return null;
}
}
if (!function_exists('current_season_url')) {
/**
* Return the podcast URL with season number to use in views
*
* @return string
*/
function current_season_url()
{
$season_query_string = '';
if (isset($_GET['season'])) {
$season_query_string = '?season=' . $_GET['season'];
} elseif (isset($_GET['year'])) {
$season_query_string = '?year=' . $_GET['year'];
}
return current_url() . $season_query_string;
}
}
//--------------------------------------------------------------------
if (!function_exists('location_url')) {
/**
* Return the host URL to use in views
*/
if (! function_exists('current_domain')) {
/**
* Returns URL to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
* Returns instance's domain name
*/
function location_url($locationName, $locationGeo, $locationOsmid)
function current_domain(): string
{
$uri = '';
if (!empty($locationOsmid)) {
$uri =
'https://www.openstreetmap.org/' .
['N' => 'node', 'W' => 'way', 'R' => 'relation'][
substr($locationOsmid, 0, 1)
] .
'/' .
substr($locationOsmid, 1);
} elseif (!empty($locationGeo)) {
$uri =
'https://www.openstreetmap.org/#map=17/' .
str_replace(',', '/', substr($locationGeo, 4));
} elseif (!empty($locationName)) {
$uri =
'https://www.openstreetmap.org/search?query=' .
urlencode($locationName);
}
/** @var URI $uri */
$uri = current_url(true);
return $uri;
return $uri->getHost() . ($uri->getPort() ? ':' . $uri->getPort() : '');
}
}
//--------------------------------------------------------------------
if (!function_exists('extract_params_from_episode_uri')) {
if (! function_exists('extract_params_from_episode_uri')) {
/**
* Returns podcast name and episode slug from episode string uri
* Returns podcast name and episode slug from episode string
*
* @param URI $episodeUri
* @return string|null
* @return array<string, string>|null
*/
function extract_params_from_episode_uri($episodeUri)
function extract_params_from_episode_uri(URI $episodeUri): ?array
{
preg_match(
'/@(?P<podcastName>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,191})/',
'~@(?P<podcastHandle>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,128})~',
$episodeUri->getPath(),
$matches
$matches,
);
if ($matches === []) {
return null;
}
if (
$matches &&
array_key_exists('podcastName', $matches) &&
array_key_exists('episodeSlug', $matches)
! array_key_exists('podcastHandle', $matches) ||
! array_key_exists('episodeSlug', $matches)
) {
return [
'podcastName' => $matches['podcastName'],
'episodeSlug' => $matches['episodeSlug'],
];
return null;
}
return null;
return [
'podcastHandle' => $matches['podcastHandle'],
'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',
];