Compare commits

...

16 Commits

Author SHA1 Message Date
Yassine Doghri 381107a438 fix: update php-icons to set default icons for platforms icons 2024-04-26 17:26:55 +00:00
Yassine Doghri 9b534b21cf chore: add codeigniter-icons library as dependency + fix styling 2024-04-26 10:58:14 +00:00
Yassine Doghri d731ddd6c3 refactor(icons): use php-icons library to load icons 2024-04-26 10:54:23 +00:00
Yassine Doghri fe73e9fae9 fix(platforms): add platforms service + reduce memory consumption when rendering platform cards 2024-04-26 10:45:30 +00:00
Yassine Doghri d4a36f811b chore: update CodeIgniter to 4.5.1 + other dependencies to latest 2024-04-26 09:26:22 +00:00
Yassine Doghri 303a900f66 refactor(platforms): move platforms data in code instead of database
refs #457
2024-04-24 14:47:05 +00:00
Guy Martin (Dwev) 57e459e187 feat: support podcast:txt tag with verify use case
closes #468
2024-04-24 10:03:20 +00:00
Yassine Doghri a67f4acb3d chore(platform): add donorbox as funding platform
closes #467
2024-04-18 09:41:37 +00:00
Benjamin Bellamy b554561c01 chore(platforms): remove stitcher 2024-04-18 09:39:55 +00:00
semantic-release-bot 30a56546d3 chore(release): 1.11.0 [skip ci]
# [1.11.0](https://code.castopod.org/adaures/castopod/compare/v1.10.5...v1.11.0) (4/17/2024)

### Bug Fixes

* **premium:** set itunes:block on premium feeds to prevent indexing ([88851b0](88851b0226))
* **rss:** generate podcast guid if empty ([a5aef2a](a5aef2a63e)), closes [#450](https://code.castopod.org/adaures/castopod/issues/450)

### Features

* add trailer tags to rss if trailer episodes are present ([80fdd9c](80fdd9cfb4))
* add transcript display to episode page ([4d141fc](4d141fceae)), closes [#411](https://code.castopod.org/adaures/castopod/issues/411)
* **platforms:** add telegram to socials ([004f804](004f804045))
* **platforms:** add truefans.fm and episodes.fm ([d046ecc](d046ecc52f)), closes [#458](https://code.castopod.org/adaures/castopod/issues/458) [#459](https://code.castopod.org/adaures/castopod/issues/459)
2024-04-17 11:05:38 +00:00
crowdin 499005d798 chore(i18n): new Crowdin updates 2024-04-17 09:57:14 +00:00
Guy Martin (Dwev) 4d141fceae feat: add transcript display to episode page
+ fix transcript parser

closes #411
2024-04-17 09:13:07 +00:00
Yassine Doghri 88851b0226 fix(premium): set itunes:block on premium feeds to prevent indexing 2024-04-12 13:07:23 +00:00
Guy Martin (Dwev) d046ecc52f feat(platforms): add truefans.fm and episodes.fm
closes #458, #459
2024-04-12 11:16:33 +00:00
Dwev 80fdd9cfb4 feat: add trailer tags to rss if trailer episodes are present 2024-04-12 10:49:26 +00:00
Guy Martin (Dwev) 004f804045 feat(platforms): add telegram to socials 2024-04-12 10:26:54 +00:00
581 changed files with 18173 additions and 9351 deletions

View File

@ -4,7 +4,7 @@
# ⚠️ NOT optimized for production # ⚠️ NOT optimized for production
# should be used only for development purposes # should be used only for development purposes
#--------------------------------------------------- #---------------------------------------------------
FROM php:8.1-fpm FROM php:8.2-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>" LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"

View File

@ -12,7 +12,7 @@ services:
environment: environment:
CI_ENVIRONMENT: development CI_ENVIRONMENT: development
vite_environment: development vite_environment: development
app_forceGlobalSecureRequests: false app_forceGlobalSecureRequests: 0 #false
app_baseURL: http://localhost:8080/ app_baseURL: http://localhost:8080/
media_baseURL: http://localhost:8080/ media_baseURL: http://localhost:8080/
admin_gateway: cp-admin admin_gateway: cp-admin
@ -23,7 +23,7 @@ services:
database_default_username: castopod database_default_username: castopod
database_default_password: castopod database_default_password: castopod
database_default_DBPrefix: cp_ database_default_DBPrefix: cp_
restapi_enabled: true restapi_enabled: 1 #true
email_fromEmail: hello@castopod.local email_fromEmail: hello@castopod.local
email_SMTPCrypto: "" email_SMTPCrypto: ""
email_SMTPHost: mailpit email_SMTPHost: mailpit

1
.gitignore vendored
View File

@ -132,7 +132,6 @@ tmp/
/results/ /results/
/phpunit*.xml /phpunit*.xml
/.phpunit.*.cache
# js package manager # js package manager
yarn.lock yarn.lock

View File

@ -1,3 +1,27 @@
# [1.11.0](https://code.castopod.org/adaures/castopod/compare/v1.10.5...v1.11.0) (4/17/2024)
### Bug Fixes
- **premium:** set itunes:block on premium feeds to prevent indexing
([88851b0](https://code.castopod.org/adaures/castopod/commit/88851b022663d575a816f0e2f33f0353767dd52d))
- **rss:** generate podcast guid if empty
([a5aef2a](https://code.castopod.org/adaures/castopod/commit/a5aef2a63e464632f3941649d455672835989e6c)),
closes [#450](https://code.castopod.org/adaures/castopod/issues/450)
### Features
- add trailer tags to rss if trailer episodes are present
([80fdd9c](https://code.castopod.org/adaures/castopod/commit/80fdd9cfb4a95feac6ed0000435a013fc83e6892))
- add transcript display to episode page
([4d141fc](https://code.castopod.org/adaures/castopod/commit/4d141fceae56fa9e666b42c32a830ff9c68989db)),
closes [#411](https://code.castopod.org/adaures/castopod/issues/411)
- **platforms:** add telegram to socials
([004f804](https://code.castopod.org/adaures/castopod/commit/004f804045cd8e884361bb4318109fbdd7afc9a8))
- **platforms:** add truefans.fm and episodes.fm
([d046ecc](https://code.castopod.org/adaures/castopod/commit/d046ecc52f6ccd41d09f6de48e00d2c61d25d7f0)),
closes [#458](https://code.castopod.org/adaures/castopod/issues/458)
[#459](https://code.castopod.org/adaures/castopod/issues/459)
## [1.10.5](https://code.castopod.org/adaures/castopod/compare/v1.10.4...v1.10.5) (3/12/2024) ## [1.10.5](https://code.castopod.org/adaures/castopod/compare/v1.10.4...v1.10.5) (3/12/2024)
### Bug Fixes ### Bug Fixes

View File

@ -1,6 +1,2 @@
<IfModule authz_core_module> <IfModule authz_core_module> Require all denied </IfModule>
Require all denied <IfModule !authz_core_module> Deny from all </IfModule>
</IfModule>
<IfModule !authz_core_module>
Deny from all
</IfModule>

View File

@ -61,6 +61,30 @@ class App extends BaseConfig
*/ */
public string $uriProtocol = 'REQUEST_URI'; public string $uriProtocol = 'REQUEST_URI';
/*
*--------------------------------------------------------------------------
* Allowed URL Characters
*--------------------------------------------------------------------------
*
* This lets you specify which characters are permitted within your URLs.
* When someone tries to submit a URL with disallowed characters they will
* get a warning message.
*
* As a security measure you are STRONGLY encouraged to restrict URLs to
* as few characters as possible.
*
* By default, only these are allowed: `a-z 0-9~%.:_-`
*
* Set an empty string to allow all characters -- but only if you are insane.
*
* The configured value is actually a regular expression character group
* and it will be used as: '/\A[<permittedURIChars>]+\z/iu'
*
* DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
*
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-@';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Default Locale * Default Locale

View File

@ -29,23 +29,17 @@ class Autoload extends AutoloadConfig
* their location on the file system. These are used by the autoloader * their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated. * to locate files the first time they have been instantiated.
* *
* The '/app' and '/system' directories are already mapped for you. * The 'Config' (APPPATH . 'Config') and 'CodeIgniter' (SYSTEMPATH) are
* you may change the name of the 'App' namespace if you wish, * already mapped for you.
*
* You may change the name of the 'App' namespace if you wish,
* but this should be done prior to creating any namespaced classes, * but this should be done prior to creating any namespaced classes,
* else you will need to modify all of those classes for this to work. * else you will need to modify all of those classes for this to work.
* *
* Prototype:
*
* $psr4 = [
* 'CodeIgniter' => SYSTEMPATH,
* 'App' => APPPATH
* ];
*
* @var array<string, list<string>|string> * @var array<string, list<string>|string>
*/ */
public $psr4 = [ public $psr4 = [
APP_NAMESPACE => APPPATH, APP_NAMESPACE => APPPATH,
'Config' => APPPATH . 'Config/',
'Modules' => ROOTPATH . 'modules/', 'Modules' => ROOTPATH . 'modules/',
'Modules\Admin' => ROOTPATH . 'modules/Admin/', 'Modules\Admin' => ROOTPATH . 'modules/Admin/',
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/', 'Modules\Analytics' => ROOTPATH . 'modules/Analytics/',
@ -55,6 +49,7 @@ class Autoload extends AutoloadConfig
'Modules\Install' => ROOTPATH . 'modules/Install/', 'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Media' => ROOTPATH . 'modules/Media/', 'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/', 'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/', 'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/', 'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
'Modules\Update' => ROOTPATH . 'modules/Update/', 'Modules\Update' => ROOTPATH . 'modules/Update/',
@ -67,7 +62,6 @@ class Autoload extends AutoloadConfig
/** /**
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* Class Map
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* The class map provides a map of class names and their exact * The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have * location on the drive. Classes loaded in this manner will have
@ -114,5 +108,5 @@ class Autoload extends AutoloadConfig
* *
* @var list<string> * @var list<string>
*/ */
public $helpers = ['auth', 'setting']; public $helpers = ['auth', 'setting', 'icons'];
} }

View File

@ -11,8 +11,10 @@ declare(strict_types=1);
* *
* If you set 'display_errors' to '1', CI4's detailed error report will show. * If you set 'display_errors' to '1', CI4's detailed error report will show.
*/ */
error_reporting(E_ALL & ~E_DEPRECATED);
// If you want to suppress more types of errors.
// error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
ini_set('display_errors', '0'); ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------

View File

@ -48,25 +48,6 @@ class Cache extends BaseConfig
*/ */
public string $storePath = WRITEPATH . 'cache/'; public string $storePath = WRITEPATH . 'cache/';
/**
* --------------------------------------------------------------------------
* Cache Include Query String
* --------------------------------------------------------------------------
*
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* ['q'] = Enabled, but only take into account the specified list
* of query parameters.
*
* @var boolean|string[]
*/
public bool | array $cacheQueryString = false;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Key Prefix * Key Prefix
@ -170,4 +151,23 @@ class Cache extends BaseConfig
'redis' => RedisHandler::class, 'redis' => RedisHandler::class,
'wincache' => WincacheHandler::class, 'wincache' => WincacheHandler::class,
]; ];
/**
* --------------------------------------------------------------------------
* Web Page Caching: Cache Include Query String
* --------------------------------------------------------------------------
*
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* ['q'] = Enabled, but only take into account the specified list
* of query parameters.
*
* @var bool|list<string>
*/
public $cacheQueryString = false;
} }

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
| |
| NOTE: this constant is updated upon release with Continuous Integration. | NOTE: this constant is updated upon release with Continuous Integration.
*/ */
defined('CP_VERSION') || define('CP_VERSION', '1.10.5'); defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
/* /*
| -------------------------------------------------------------------- | --------------------------------------------------------------------

View File

@ -35,28 +35,28 @@ class ContentSecurityPolicy extends BaseConfig
/** /**
* Will default to self if not overridden * Will default to self if not overridden
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $defaultSrc = null; public string | array | null $defaultSrc = null;
/** /**
* Lists allowed scripts' URLs. * Lists allowed scripts' URLs.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $scriptSrc = 'self'; public string | array $scriptSrc = 'self';
/** /**
* Lists allowed stylesheets' URLs. * Lists allowed stylesheets' URLs.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $styleSrc = 'self'; public string | array $styleSrc = 'self';
/** /**
* Defines the origins from which images can be loaded. * Defines the origins from which images can be loaded.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $imageSrc = 'self'; public string | array $imageSrc = 'self';
@ -65,35 +65,35 @@ class ContentSecurityPolicy extends BaseConfig
* *
* Will default to self if not overridden * Will default to self if not overridden
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $baseURI = null; public string | array | null $baseURI = null;
/** /**
* Lists the URLs for workers and embedded frame contents * Lists the URLs for workers and embedded frame contents
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $childSrc = 'self'; public string | array $childSrc = 'self';
/** /**
* Limits the origins that you can connect to (via XHR, WebSockets, and EventSource). * Limits the origins that you can connect to (via XHR, WebSockets, and EventSource).
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $connectSrc = 'self'; public string | array $connectSrc = 'self';
/** /**
* Specifies the origins that can serve web fonts. * Specifies the origins that can serve web fonts.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $fontSrc; public string | array $fontSrc;
/** /**
* Lists valid endpoints for submission from `<form>` tags. * Lists valid endpoints for submission from `<form>` tags.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $formAction = 'self'; public string | array $formAction = 'self';
@ -102,47 +102,47 @@ class ContentSecurityPolicy extends BaseConfig
* `<embed>`, and `<applet>` tags. This directive can't be used in `<meta>` tags and applies only to non-HTML * `<embed>`, and `<applet>` tags. This directive can't be used in `<meta>` tags and applies only to non-HTML
* resources. * resources.
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $frameAncestors = null; public string | array | null $frameAncestors = null;
/** /**
* The frame-src directive restricts the URLs which may be loaded into nested browsing contexts. * The frame-src directive restricts the URLs which may be loaded into nested browsing contexts.
* *
* @var string[]|string|null * @var list<string>|string|null
*/ */
public string | array | null $frameSrc = null; public string | array | null $frameSrc = null;
/** /**
* Restricts the origins allowed to deliver video and audio. * Restricts the origins allowed to deliver video and audio.
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $mediaSrc = null; public string | array | null $mediaSrc = null;
/** /**
* Allows control over Flash and other plugins. * Allows control over Flash and other plugins.
* *
* @var string|string[] * @var list<string>|string
*/ */
public string | array $objectSrc = 'self'; public string | array $objectSrc = 'self';
/** /**
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $manifestSrc = null; public string | array | null $manifestSrc = null;
/** /**
* Limits the kinds of plugins a page may invoke. * Limits the kinds of plugins a page may invoke.
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $pluginTypes = null; public string | array | null $pluginTypes = null;
/** /**
* List of actions allowed. * List of actions allowed.
* *
* @var string|string[]|null * @var list<string>|string|null
*/ */
public string | array | null $sandbox = null; public string | array | null $sandbox = null;

107
app/Config/Cors.php Normal file
View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Cross-Origin Resource Sharing (CORS) Configuration
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
*/
class Cors extends BaseConfig
{
/**
* The default CORS configuration.
*
* @var array{
* allowedOrigins: list<string>,
* allowedOriginsPatterns: list<string>,
* supportsCredentials: bool,
* allowedHeaders: list<string>,
* exposedHeaders: list<string>,
* allowedMethods: list<string>,
* maxAge: int,
* }
*/
public array $default = [
/**
* Origins for the `Access-Control-Allow-Origin` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
*
* E.g.:
* - ['http://localhost:8080']
* - ['https://www.example.com']
*/
'allowedOrigins' => [],
/**
* Origin regex patterns for the `Access-Control-Allow-Origin` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
*
* NOTE: A pattern specified here is part of a regular expression. It will
* be actually `#\A<pattern>\z#`.
*
* E.g.:
* - ['https://\w+\.example\.com']
*/
'allowedOriginsPatterns' => [],
/**
* Weather to send the `Access-Control-Allow-Credentials` header.
*
* The Access-Control-Allow-Credentials response header tells browsers whether
* the server allows cross-origin HTTP requests to include credentials.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
*/
'supportsCredentials' => false,
/**
* Set headers to allow.
*
* The Access-Control-Allow-Headers response header is used in response to
* a preflight request which includes the Access-Control-Request-Headers to
* indicate which HTTP headers can be used during the actual request.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
*/
'allowedHeaders' => [],
/**
* Set headers to expose.
*
* The Access-Control-Expose-Headers response header allows a server to
* indicate which response headers should be made available to scripts running
* in the browser, in response to a cross-origin request.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
*/
'exposedHeaders' => [],
/**
* Set methods to allow.
*
* The Access-Control-Allow-Methods response header specifies one or more
* methods allowed when accessing a resource in response to a preflight
* request.
*
* E.g.:
* - ['GET', 'POST', 'PUT', 'DELETE']
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
*/
'allowedMethods' => [],
/**
* Set how many seconds the results of a preflight request can be cached.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
*/
'maxAge' => 7200,
];
}

View File

@ -45,6 +45,11 @@ class Database extends Config
'failover' => [], 'failover' => [],
'port' => 3306, 'port' => 3306,
'numberNative' => false, 'numberNative' => false,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
]; ];
/** /**
@ -64,7 +69,7 @@ class Database extends Config
'pConnect' => false, 'pConnect' => false,
'DBDebug' => true, 'DBDebug' => true,
'charset' => 'utf8', 'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci', 'DBCollat' => '',
'swapPre' => '', 'swapPre' => '',
'encrypt' => false, 'encrypt' => false,
'compress' => false, 'compress' => false,
@ -73,6 +78,11 @@ class Database extends Config
'port' => 3306, 'port' => 3306,
'foreignKeys' => true, 'foreignKeys' => true,
'busyTimeout' => 1000, 'busyTimeout' => 1000,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
]; ];
//-------------------------------------------------------------------- //--------------------------------------------------------------------

View File

@ -33,7 +33,7 @@ class Exceptions extends BaseConfig
* Any status codes here will NOT be logged if logging is turned on. * Any status codes here will NOT be logged if logging is turned on.
* By default, only 404 (Page Not Found) exceptions are ignored. * By default, only 404 (Page Not Found) exceptions are ignored.
* *
* @var int[] * @var list<int>
*/ */
public array $ignoreCodes = [404]; public array $ignoreCodes = [404];
@ -56,7 +56,7 @@ class Exceptions extends BaseConfig
* In order to specify 2 levels, use "/" to separate. * In order to specify 2 levels, use "/" to separate.
* ex. ['server', 'setup/password', 'secret_token'] * ex. ['server', 'setup/password', 'secret_token']
* *
* @var string[] * @var list<string>
*/ */
public array $sensitiveDataInTrace = []; public array $sensitiveDataInTrace = [];

View File

@ -11,22 +11,21 @@ use CodeIgniter\Config\BaseConfig;
*/ */
class Feature extends BaseConfig class Feature extends BaseConfig
{ {
/**
* Enable multiple filters for a route or not.
*
* If you enable this:
* - CodeIgniter\CodeIgniter::handleRequest() uses:
* - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter()
* - CodeIgniter\CodeIgniter::tryToRouteIt() uses:
* - CodeIgniter\Router\Router::getFilters(), instead of getFilter()
* - CodeIgniter\Router\Router::handle() uses:
* - property $filtersInfo, instead of $filterInfo
* - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute()
*/
public bool $multipleFilters = false;
/** /**
* Use improved new auto routing instead of the default legacy version. * Use improved new auto routing instead of the default legacy version.
*/ */
public bool $autoRoutesImproved = false; public bool $autoRoutesImproved = false;
/**
* Use filter execution order in 4.4 or before.
*/
public bool $oldFilterOrder = false;
/**
* The behavior of `limit(0)` in Query Builder.
*
* If true, `limit(0)` returns all records. (the behavior of 4.4.x or before in version 4.x.)
* If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.)
*/
public bool $limitZeroAsAll = true;
} }

View File

@ -8,8 +8,11 @@ use App\Filters\AllowCorsFilter;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF; use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\ForceHTTPS;
use CodeIgniter\Filters\Honeypot; use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars; use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\SecureHeaders; use CodeIgniter\Filters\SecureHeaders;
use Modules\Auth\Filters\PermissionFilter; use Modules\Auth\Filters\PermissionFilter;
@ -18,8 +21,10 @@ class Filters extends BaseConfig
/** /**
* Configures aliases for Filter classes to make reading things nicer and simpler. * Configures aliases for Filter classes to make reading things nicer and simpler.
* *
* @var array<string, class-string|list<class-string>> [filter_name => classname] * @var array<string, class-string|list<class-string>>
* or [filter_name => [classname1, classname2, ...]] *
* [filter_name => classname]
* or [filter_name => [classname1, classname2, ...]]
*/ */
public array $aliases = [ public array $aliases = [
'csrf' => CSRF::class, 'csrf' => CSRF::class,
@ -28,12 +33,41 @@ class Filters extends BaseConfig
'invalidchars' => InvalidChars::class, 'invalidchars' => InvalidChars::class,
'secureheaders' => SecureHeaders::class, 'secureheaders' => SecureHeaders::class,
'allow-cors' => AllowCorsFilter::class, 'allow-cors' => AllowCorsFilter::class,
'cors' => Cors::class,
'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class,
];
/**
* List of special required filters.
*
* The filters listed here are special. They are applied before and after
* other kinds of filters, and always applied even if a route does not exist.
*
* Filters set by default provide framework functionality. If removed,
* those functions will no longer work.
*
* @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
*
* @var array{before: list<string>, after: list<string>}
*/
public array $required = [
'before' => [
'forcehttps', // Force Global Secure Requests
'pagecache', // Web Page Caching
],
'after' => [
'pagecache', // Web Page Caching
'performance', // Performance Metrics
'toolbar', // Debug Toolbar
],
]; ];
/** /**
* List of filter aliases that are always applied before and after every request. * List of filter aliases that are always applied before and after every request.
* *
* @var array<string, array<string, array<string, string>>>|array<string, list<string>> * @var array<string, array<string, array<string, string|array<string>>>>>|array<string, list<string>>
*/ */
public array $globals = [ public array $globals = [
'before' => [ 'before' => [
@ -44,7 +78,6 @@ class Filters extends BaseConfig
// 'invalidchars', // 'invalidchars',
], ],
'after' => [ 'after' => [
'toolbar',
// 'honeypot', // 'honeypot',
// 'secureheaders', // 'secureheaders',
], ],
@ -53,12 +86,12 @@ class Filters extends BaseConfig
/** /**
* List of filter aliases that works on a particular HTTP method (GET, POST, etc.). * List of filter aliases that works on a particular HTTP method (GET, POST, etc.).
* *
* Example: 'post' => ['foo', 'bar'] * Example: 'POST' => ['foo', 'bar']
* *
* If you use this, you should disable auto-routing because auto-routing permits any HTTP method to access a * If you use this, you should disable auto-routing because auto-routing permits any HTTP method to access a
* controller. Accessing the controller with a method you dont expect could bypass the filter. * controller. Accessing the controller with a method you dont expect could bypass the filter.
* *
* @var array<string, string[]> * @var array<string, list<string>>
*/ */
public array $methods = []; public array $methods = [];
@ -67,7 +100,7 @@ class Filters extends BaseConfig
* *
* Example: 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']] * Example: 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
* *
* @var array<string, array<string, string[]>> * @var array<string, array<string, list<string>>>
*/ */
public array $filters = []; public array $filters = [];

View File

@ -24,7 +24,7 @@ class Format extends BaseConfig
* These formats are only checked when the data passed to the respond() * These formats are only checked when the data passed to the respond()
* method is an array. * method is an array.
* *
* @var string[] * @var list<string>
*/ */
public array $supportedResponseFormats = [ public array $supportedResponseFormats = [
'application/json', 'application/json',

View File

@ -28,8 +28,10 @@ class Generators extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $views = [ public array $views = [
'make:cell' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', 'make:cell' => [
'make:cell_view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', 'class' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php',
'view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php',
],
'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\BaseConfig;
use Kint\Parser\ConstructablePluginInterface; use Kint\Parser\ConstructablePluginInterface;
use Kint\Renderer\AbstractRenderer; use Kint\Renderer\AbstractRenderer;
use Kint\Renderer\Rich\TabPluginInterface; use Kint\Renderer\Rich\TabPluginInterface;
@ -20,7 +19,7 @@ use Kint\Renderer\Rich\ValuePluginInterface;
* *
* @see https://kint-php.github.io/kint/ for details on these settings. * @see https://kint-php.github.io/kint/ for details on these settings.
*/ */
class Kint extends BaseConfig class Kint
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -38,7 +38,7 @@ class Logger extends BaseConfig
* For a live site you'll usually enable Critical or higher (3) to be logged otherwise * For a live site you'll usually enable Critical or higher (3) to be logged otherwise
* your log files will fill up very fast. * your log files will fill up very fast.
* *
* @var int|int[] * @var int|list<int>
*/ */
public int | array $threshold = (ENVIRONMENT === 'production') ? 4 : 9; public int | array $threshold = (ENVIRONMENT === 'production') ? 4 : 9;
@ -75,7 +75,7 @@ class Logger extends BaseConfig
* Handlers are executed in the order defined in this array, starting with * Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down. * the handler on top and continuing down.
* *
* @var array<string, mixed> * @var array<class-string, array<string, int|list<string>|string>>
*/ */
public array $handlers = [ public array $handlers = [
/* /*

View File

@ -22,7 +22,7 @@ class Mimes
/** /**
* Map of extensions to mime types. * Map of extensions to mime types.
* *
* @var array<string, string|string[]> * @var array<string, list<string>|string>
*/ */
public static $mimes = [ public static $mimes = [
'hqx' => [ 'hqx' => [

34
app/Config/Optimize.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Config;
/**
* Optimization Configuration.
*
* NOTE: This class does not extend BaseConfig for performance reasons.
* So you cannot replace the property values with Environment Variables.
*
* @immutable
*/
class Optimize
{
/**
* --------------------------------------------------------------------------
* Config Caching
* --------------------------------------------------------------------------
*
* @see https://codeigniter.com/user_guide/concepts/factories.html#config-caching
*/
public bool $configCacheEnabled = false;
/**
* --------------------------------------------------------------------------
* Config Caching
* --------------------------------------------------------------------------
*
* @see https://codeigniter.com/user_guide/concepts/autoloader.html#file-locator-caching
*/
public bool $locatorCacheEnabled = false;
}

View File

@ -15,7 +15,6 @@ use CodeIgniter\Router\RouteCollection;
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}'); $routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}'); $routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}'); $routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply'); $routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent'); $routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder( $routes->addPlaceholder(
@ -128,6 +127,9 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [ $routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
'as' => 'episode-chapters', 'as' => 'episode-chapters',
]); ]);
$routes->get('transcript', 'EpisodeController::transcript/$1/$2', [
'as' => 'episode-transcript',
]);
$routes->options('comments', 'ActivityPubController::preflight'); $routes->options('comments', 'ActivityPubController::preflight');
$routes->get('comments', 'EpisodeController::comments/$1/$2', [ $routes->get('comments', 'EpisodeController::comments/$1/$2', [
'as' => 'episode-comments', 'as' => 'episode-comments',
@ -205,6 +207,9 @@ $routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [ $routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
'as' => 'episode-preview-chapters', 'as' => 'episode-preview-chapters',
]); ]);
$routes->get('/p/(:uuid)/transcript', 'EpisodePreviewController::transcript/$1', [
'as' => 'episode-preview-transcript',
]);
// Other pages // Other pages
$routes->get('/credits', 'CreditsController', [ $routes->get('/credits', 'CreditsController', [

View File

@ -12,13 +12,14 @@ use CodeIgniter\Config\Routing as BaseRouting;
class Routing extends BaseRouting class Routing extends BaseRouting
{ {
/** /**
* For Defined Routes.
* An array of files that contain route definitions. * An array of files that contain route definitions.
* Route files are read in order, with the first match * Route files are read in order, with the first match
* found taking precedence. * found taking precedence.
* *
* Default: APPPATH . 'Config/Routes.php' * Default: APPPATH . 'Config/Routes.php'
* *
* @var string[] * @var list<string>
*/ */
public array $routeFiles = [ public array $routeFiles = [
APPPATH . 'Config/Routes.php', APPPATH . 'Config/Routes.php',
@ -28,11 +29,13 @@ class Routing extends BaseRouting
ROOTPATH . 'modules/Auth/Config/Routes.php', ROOTPATH . 'modules/Auth/Config/Routes.php',
ROOTPATH . 'modules/Fediverse/Config/Routes.php', ROOTPATH . 'modules/Fediverse/Config/Routes.php',
ROOTPATH . 'modules/Install/Config/Routes.php', ROOTPATH . 'modules/Install/Config/Routes.php',
ROOTPATH . 'modules/Platforms/Config/Routes.php',
ROOTPATH . 'modules/PodcastImport/Config/Routes.php', ROOTPATH . 'modules/PodcastImport/Config/Routes.php',
ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php', ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php',
]; ];
/** /**
* For Defined Routes and Auto Routing.
* The default namespace to use for Controllers when no other * The default namespace to use for Controllers when no other
* namespace has been specified. * namespace has been specified.
* *
@ -41,6 +44,7 @@ class Routing extends BaseRouting
public string $defaultNamespace = 'App\Controllers'; public string $defaultNamespace = 'App\Controllers';
/** /**
* For Auto Routing.
* The default controller to use when no other controller has been * The default controller to use when no other controller has been
* specified. * specified.
* *
@ -49,6 +53,7 @@ class Routing extends BaseRouting
public string $defaultController = 'HomeController'; public string $defaultController = 'HomeController';
/** /**
* For Defined Routes and Auto Routing.
* The default method to call on the controller when no other * The default method to call on the controller when no other
* method has been set in the route. * method has been set in the route.
* *
@ -57,7 +62,8 @@ class Routing extends BaseRouting
public string $defaultMethod = 'index'; public string $defaultMethod = 'index';
/** /**
* Whether to translate dashes in URIs to underscores. * For Auto Routing.
* Whether to translate dashes in URIs for controller/method to underscores.
* Primarily useful when using the auto-routing. * Primarily useful when using the auto-routing.
* *
* Default: false * Default: false
@ -93,6 +99,7 @@ class Routing extends BaseRouting
public bool $autoRoute = false; public bool $autoRoute = false;
/** /**
* For Defined Routes.
* If TRUE, will enable the use of the 'prioritize' option * If TRUE, will enable the use of the 'prioritize' option
* when defining routes. * when defining routes.
* *
@ -101,7 +108,16 @@ class Routing extends BaseRouting
public bool $prioritize = false; public bool $prioritize = false;
/** /**
* Map of URI segments and namespaces. For Auto Routing (Improved). * For Defined Routes.
* If TRUE, matched multiple URI segments will be passed as one parameter.
*
* Default: false
*/
public bool $multipleSegmentsOneParam = false;
/**
* For Auto Routing (Improved).
* Map of URI segments and namespaces.
* *
* The key is the first URI segment. The value is the controller namespace. * The key is the first URI segment. The value is the controller namespace.
* E.g., * E.g.,
@ -112,4 +128,15 @@ class Routing extends BaseRouting
* @var array<string, string> [ uri_segment => namespace ] * @var array<string, string> [ uri_segment => namespace ]
*/ */
public array $moduleRoutes = []; public array $moduleRoutes = [];
/**
* For Auto Routing (Improved).
* Whether to translate dashes in URIs for controller/method to CamelCase.
* E.g., blog-controller -> BlogController
*
* If you enable this, $translateURIDashes is ignored.
*
* Default: false
*/
public bool $translateUriToCamelCase = false;
} }

View File

@ -80,9 +80,9 @@ class Security extends BaseConfig
* CSRF Redirect * CSRF Redirect
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Redirect to previous page with error on failure. * @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure
*/ */
public bool $redirect = false; public bool $redirect = (ENVIRONMENT === 'production');
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------

View File

@ -37,8 +37,8 @@ class Services extends BaseService
return static::getSharedInstance('router', $routes, $request); return static::getSharedInstance('router', $routes, $request);
} }
$routes = $routes ?? static::routes(); $routes ??= static::routes();
$request = $request ?? static::request(); $request ??= static::request();
return new Router($routes, $request); return new Router($routes, $request);
} }
@ -53,7 +53,7 @@ class Services extends BaseService
return static::getSharedInstance('negotiator', $request); return static::getSharedInstance('negotiator', $request);
} }
$request = $request ?? static::request(); $request ??= static::request();
return new Negotiate($request); return new Negotiate($request);
} }

View File

@ -101,4 +101,29 @@ class Session extends BaseConfig
* DB Group for the database session. * DB Group for the database session.
*/ */
public ?string $DBGroup = null; public ?string $DBGroup = null;
/**
* --------------------------------------------------------------------------
* Lock Retry Interval (microseconds)
* --------------------------------------------------------------------------
*
* This is used for RedisHandler.
*
* Time (microseconds) to wait if lock cannot be acquired.
* The default is 100,000 microseconds (= 0.1 seconds).
*/
public int $lockRetryInterval = 100_000;
/**
* --------------------------------------------------------------------------
* Lock Max Retries
* --------------------------------------------------------------------------
*
* This is used for RedisHandler.
*
* Maximum number of lock acquisition attempts.
* The default is 300 times. That is lock timeout is about 30 (0.1 * 300)
* seconds.
*/
public int $lockMaxRetries = 300;
} }

View File

@ -33,7 +33,7 @@ class Toolbar extends BaseConfig
* List of toolbar collectors that will be called when Debug Toolbar * List of toolbar collectors that will be called when Debug Toolbar
* fires up and collects data from. * fires up and collects data from.
* *
* @var string[] * @var list<class-string>
*/ */
public array $collectors = [ public array $collectors = [
Timers::class, Timers::class,
@ -51,7 +51,7 @@ class Toolbar extends BaseConfig
* Collect Var Data * Collect Var Data
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* If set to false var data from the views will not be colleted. Useful to * If set to false var data from the views will not be collected. Useful to
* avoid high memory usage when there are lots of data passed to the view. * avoid high memory usage when there are lots of data passed to the view.
*/ */
public bool $collectVarData = true; public bool $collectVarData = true;
@ -102,7 +102,7 @@ class Toolbar extends BaseConfig
* *
* NOTE: The ROOTPATH will be prepended to all values. * NOTE: The ROOTPATH will be prepended to all values.
* *
* @var string[] * @var list<string>
*/ */
public array $watchedDirectories = ['app', 'modules', 'themes']; public array $watchedDirectories = ['app', 'modules', 'themes'];
@ -114,7 +114,7 @@ class Toolbar extends BaseConfig
* Contains an array of file extensions that will be watched for changes and * Contains an array of file extensions that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not. * used to determine if the hot-reload feature should reload the page or not.
* *
* @var string[] * @var list<string>
*/ */
public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env']; public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env'];
} }

View File

@ -16,7 +16,7 @@ class Validation extends BaseConfig
/** /**
* Stores the classes that contain the rules that are available. * Stores the classes that contain the rules that are available.
* *
* @var string[] * @var list<string>
*/ */
public array $ruleSets = [ public array $ruleSets = [
Rules::class, Rules::class,

View File

@ -9,8 +9,8 @@ use CodeIgniter\View\ViewDecoratorInterface;
use ViewComponents\Decorator; use ViewComponents\Decorator;
/** /**
* @phpstan-type ParserCallable (callable(mixed): mixed) * @phpstan-type parser_callable (callable(mixed): mixed)
* @phpstan-type ParserCallableString (callable(mixed): mixed)&string * @phpstan-type parser_callable_string (callable(mixed): mixed)&string
*/ */
class View extends BaseView class View extends BaseView
{ {
@ -31,8 +31,8 @@ class View extends BaseView
* *
* Examples: { title|esc(js) } { created_on|date(Y-m-d)|esc(attr) } * Examples: { title|esc(js) } { created_on|date(Y-m-d)|esc(attr) }
* *
* @var array<string, string> * @var array<string, string>
* @phpstan-var array<string, ParserCallableString> * @phpstan-var array<string, parser_callable_string>
*/ */
public $filters = []; public $filters = [];
@ -40,8 +40,8 @@ class View extends BaseView
* Parser Plugins provide a way to extend the functionality provided by the core Parser by creating aliases that * Parser Plugins provide a way to extend the functionality provided by the core Parser by creating aliases that
* will be replaced with any callable. Can be single or tag pair. * will be replaced with any callable. Can be single or tag pair.
* *
* @var array<string, array<string>|callable|string> * @var array<string, callable|list<string>|string>
* @phpstan-var array<string, array<ParserCallableString>|ParserCallableString|ParserCallable> * @phpstan-var array<string, list<parser_callable_string>|parser_callable_string|parser_callable>
*/ */
public $plugins = []; public $plugins = [];
@ -51,7 +51,7 @@ class View extends BaseView
* *
* All classes must implement CodeIgniter\View\ViewDecoratorInterface * All classes must implement CodeIgniter\View\ViewDecoratorInterface
* *
* @var class-string<ViewDecoratorInterface>[] * @var list<class-string<ViewDecoratorInterface>>
*/ */
public array $decorators = [Decorator::class]; public array $decorators = [Decorator::class];
} }

View File

@ -13,8 +13,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all * BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. Extend this class in any new controllers: class Home extends BaseController * your controllers. Extend this class in any new controllers: class Home extends BaseController
* *
@ -41,7 +39,7 @@ abstract class BaseController extends Controller
* class instantiation. These helpers will be available * class instantiation. These helpers will be available
* to all other controllers that extend BaseController. * to all other controllers that extend BaseController.
* *
* @var string[] * @var list<string>
*/ */
protected $helpers = []; protected $helpers = [];

View File

@ -40,7 +40,7 @@ class EpisodeAudioController extends Controller
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all * An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend Analytics. * other controllers that extend Analytics.
* *
* @var string[] * @var list<string>
*/ */
protected $helpers = ['analytics']; protected $helpers = ['analytics'];

View File

@ -106,9 +106,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [ return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -157,9 +155,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [ return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -167,7 +163,7 @@ class EpisodeController extends BaseController
return $cachedView; return $cachedView;
} }
public function chapters(): String public function chapters(): string
{ {
// Prevent analytics hit when authenticated // Prevent analytics hit when authenticated
if (! auth()->loggedIn()) { if (! auth()->loggedIn()) {
@ -218,9 +214,71 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [ return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode 'cache_name' => $cacheName,
: DECADE, ]);
}
return $cachedView;
}
public function transcript(): string
{
// Prevent analytics hit when authenticated
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'transcript',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
// get transcript from json file
$data = [
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/transcript', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -273,9 +331,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [ return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -354,11 +410,9 @@ class EpisodeController extends BaseController
* get comments: aggregated replies from posts referring to the episode * get comments: aggregated replies from posts referring to the episode
*/ */
$episodeComments = model(PostModel::class) $episodeComments = model(PostModel::class)
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder { ->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
return $builder->select('id') ->from('fediverse_posts')
->from('fediverse_posts') ->where('episode_id', $this->episode->id))
->where('episode_id', $this->episode->id);
})
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');

View File

@ -13,7 +13,6 @@ namespace App\Controllers;
use App\Entities\Episode; use App\Entities\Episode;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Media\FileManagers\FileManagerInterface; use Modules\Media\FileManagers\FileManagerInterface;
class EpisodePreviewController extends BaseController class EpisodePreviewController extends BaseController
@ -45,7 +44,7 @@ class EpisodePreviewController extends BaseController
return $this->{$method}(...$params); return $this->{$method}(...$params);
} }
public function index(): RedirectResponse | string public function index(): string
{ {
helper('form'); helper('form');
@ -55,7 +54,7 @@ class EpisodePreviewController extends BaseController
]); ]);
} }
public function activity(): RedirectResponse | string public function activity(): string
{ {
helper('form'); helper('form');
@ -65,7 +64,7 @@ class EpisodePreviewController extends BaseController
]); ]);
} }
public function chapters(): RedirectResponse | string public function chapters(): string
{ {
$data = [ $data = [
'podcast' => $this->episode->podcast, 'podcast' => $this->episode->podcast,
@ -84,4 +83,30 @@ class EpisodePreviewController extends BaseController
helper('form'); helper('form');
return view('episode/preview-chapters', $data); return view('episode/preview-chapters', $data);
} }
public function transcript(): string
{
// get transcript from json file
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
helper('form');
return view('episode/preview-transcript', $data);
}
} }

View File

@ -79,13 +79,7 @@ class FeedController extends Controller
); );
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $this->response->setXML($found); return $this->response->setXML($found);

View File

@ -1,28 +0,0 @@
<?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/
*/
namespace App\Controllers;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
/*
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{
public function index(): ResponseInterface
{
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}

View File

@ -96,9 +96,7 @@ class PodcastController extends BaseController
); );
return view('podcast/activity', $data, [ return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -148,9 +146,7 @@ class PodcastController extends BaseController
); );
return view('podcast/about', $data, [ return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -270,9 +266,7 @@ class PodcastController extends BaseController
$this->podcast->id, $this->podcast->id,
); );
return view('podcast/episodes', $data, [ return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsVerifyTxtField adds 1 field to podcast table in database to support podcast:txt tag
*
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
class AddPodcastsVerifyTxtField extends BaseMigration
{
public function up(): void
{
$fields = [
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
];
$this->forge->addColumn('podcasts', $fields);
}
public function down(): void
{
$this->forge->dropColumn('podcasts', 'verify_txt');
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RefactorPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
'after' => 'podcast_id',
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE', 'platforms_podcast_id_foreign');
$this->forge->addUniqueKey(['podcast_id', 'type', 'slug']);
$this->forge->createTable('platforms_temp');
$platformsData = $this->db->table('podcasts_platforms')
->select('podcasts_platforms.*, type')
->join('platforms', 'platforms.slug = podcasts_platforms.platform_slug')
->get()
->getResultArray();
$data = [];
foreach ($platformsData as $platformData) {
$data[] = [
'podcast_id' => $platformData['podcast_id'],
'type' => $platformData['type'],
'slug' => $platformData['platform_slug'],
'link_url' => $platformData['link_url'],
'account_id' => $platformData['account_id'],
'is_visible' => $platformData['is_visible'],
];
}
if ($data !== []) {
$this->db->table('platforms_temp')
->insertBatch($data);
}
$this->forge->dropTable('platforms');
$this->forge->dropTable('podcasts_platforms');
$this->forge->renameTable('platforms_temp', 'platforms');
}
public function down(): void
{
// delete platforms
$this->forge->dropTable('platforms');
// recreate platforms and podcasts_platforms tables
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()'
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug', 'CASCADE');
$this->forge->createTable('podcasts_platforms');
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
/**
* CodeIgniter 4.5.1 introduces new DataCaster class that breaks deserialization of import queue tasks.
* This just removes them altogether.
*/
class ClearImportQueue extends Migration
{
public function up(): void
{
service('settings')->forget('Import.queue');
}
public function down(): void
{
// nothing
}
}

View File

@ -20,6 +20,5 @@ class AppSeeder extends Seeder
{ {
$this->call('CategorySeeder'); $this->call('CategorySeeder');
$this->call('LanguageSeeder'); $this->call('LanguageSeeder');
$this->call('PlatformSeeder');
} }
} }

View File

@ -20,7 +20,6 @@ class DevSeeder extends Seeder
{ {
$this->call('CategorySeeder'); $this->call('CategorySeeder');
$this->call('LanguageSeeder'); $this->call('LanguageSeeder');
$this->call('PlatformSeeder');
$this->call('DevSuperadminSeeder'); $this->call('DevSuperadminSeeder');
} }
} }

View File

@ -77,34 +77,32 @@ class FakePodcastsAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, (int) $probability1); $lineNumber < random_int(1, (int) $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = floor(exp(6 - $age / 20)) + 10; $probability2 = floor(exp(6 - $age / 20)) + 10;
$player = $player =
$jsonUserAgents[ $jsonUserAgents[
rand(1, count($jsonUserAgents) - 1) random_int(1, count($jsonUserAgents) - 1)
]; ];
$service = $service =
$jsonRSSUserAgents[ $jsonRSSUserAgents[
rand(1, count($jsonRSSUserAgents) - 1) random_int(1, count($jsonRSSUserAgents) - 1)
]['slug']; ]['slug'];
$app = isset($player['app']) ? $player['app'] : ''; $app = $player['app'] ?? '';
$device = isset($player['device']) $device = $player['device'] ?? '';
? $player['device'] $os = $player['os'] ?? '';
: ''; $isBot = $player['bot'] ?? 0;
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
$fakeIp = $fakeIp =
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255) . random_int(0, 255) .
'.' . '.' .
rand(0, 255); random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'); $cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
@ -115,9 +113,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
try { try {
$city = $cityReader->city($fakeIp); $city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null $countryCode = $city->country->isoCode ?? 'N/A';
? 'N/A'
: $city->country->isoCode;
$regionCode = $city->subdivisions === [] $regionCode = $city->subdivisions === []
? 'N/A' ? 'N/A'
@ -128,20 +124,20 @@ class FakePodcastsAnalyticsSeeder extends Seeder
//Bad luck, bad IP, nothing to do. //Bad luck, bad IP, nothing to do.
} }
$hits = rand(0, (int) $probability2); $hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [ $analyticsPodcasts[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600), 'duration' => random_int(60, 3600),
'bandwidth' => rand(1000000, 10000000), 'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits, 'hits' => $hits,
'unique_listeners' => $hits, 'unique_listeners' => $hits,
]; ];
$analyticsPodcastsByHour[] = [ $analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'hour' => rand(0, 23), 'hour' => random_int(0, 23),
'hits' => $hits, 'hits' => $hits,
]; ];
$analyticsPodcastsByCountry[] = [ $analyticsPodcastsByCountry[] = [

View File

@ -216,23 +216,23 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
for ( for (
$lineNumber = 0; $lineNumber = 0;
$lineNumber < rand(1, $probability1); $lineNumber < random_int(1, $probability1);
++$lineNumber ++$lineNumber
) { ) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$domain = $domain =
$this->domains[rand(0, count($this->domains) - 1)]; $this->domains[random_int(0, count($this->domains) - 1)];
$keyword = $keyword =
$this->keywords[ $this->keywords[
rand(0, count($this->keywords) - 1) random_int(0, count($this->keywords) - 1)
]; ];
$browser = $browser =
$this->browsers[ $this->browsers[
rand(0, count($this->browsers) - 1) random_int(0, count($this->browsers) - 1)
]; ];
$hits = rand(0, $probability2); $hits = random_int(0, $probability2);
$websiteByBrowser[] = [ $websiteByBrowser[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,

View File

@ -483,7 +483,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid; $this->attributes['guid'] = $guid ?? $this->getLink();
return $this; return $this;
} }

View File

@ -15,7 +15,6 @@ use App\Models\ActorModel;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
@ -32,6 +31,8 @@ use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel; use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image; use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel; use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel; use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException; use RuntimeException;
@ -528,7 +529,7 @@ class Podcast extends Entity
} }
if ($this->podcasting_platforms === null) { if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting'); $this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
} }
return $this->podcasting_platforms; return $this->podcasting_platforms;
@ -546,7 +547,7 @@ class Podcast extends Entity
} }
if ($this->social_platforms === null) { if ($this->social_platforms === null) {
$this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social'); $this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
} }
return $this->social_platforms; return $this->social_platforms;
@ -564,7 +565,7 @@ class Podcast extends Entity
} }
if ($this->funding_platforms === null) { if ($this->funding_platforms === null) {
$this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding'); $this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
} }
return $this->funding_platforms; return $this->funding_platforms;

View File

@ -35,7 +35,7 @@ if (! function_exists('hint_tooltip')) {
$tooltip .= ' ' . $class; $tooltip .= ' ' . $class;
} }
return $tooltip . '">' . icon('question') . '</span>'; return $tooltip . '">' . icon('question-fill') . '</span>';
} }
} }
@ -156,20 +156,20 @@ if (! function_exists('publication_button')) {
$label = lang('Episode.publish'); $label = lang('Episode.publish');
$route = route_to('episode-publish', $podcastId, $episodeId); $route = route_to('episode-publish', $podcastId, $episodeId);
$variant = 'primary'; $variant = 'primary';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
break; break;
case 'with_podcast': case 'with_podcast':
case 'scheduled': case 'scheduled':
$label = lang('Episode.publish_edit'); $label = lang('Episode.publish_edit');
$route = route_to('episode-publish_edit', $podcastId, $episodeId); $route = route_to('episode-publish_edit', $podcastId, $episodeId);
$variant = 'warning'; $variant = 'warning';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
break; break;
case 'published': case 'published':
$label = lang('Episode.unpublish'); $label = lang('Episode.unpublish');
$route = route_to('episode-unpublish', $podcastId, $episodeId); $route = route_to('episode-unpublish', $podcastId, $episodeId);
$variant = 'danger'; $variant = 'danger';
$iconLeft = 'cloud-off'; $iconLeft = 'cloud-off-fill'; // @icon('cloud-off-fill')
break; break;
default: default:
$label = ''; $label = '';
@ -350,7 +350,9 @@ if (! function_exists('location_link')) {
return anchor( return anchor(
$location->url, $location->url,
icon('map-pin', 'mr-2 flex-shrink-0') . '<span class="truncate">' . esc($location->name) . '</span>', icon('map-pin-2-fill', [
'class' => 'mr-2 flex-shrink-0',
]) . '<span class="truncate">' . esc($location->name) . '</span>',
[ [
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' . 'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' .
($class === '' ? '' : " {$class}"), ($class === '' ? '' : " {$class}"),

View File

@ -42,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) {
// populate data array // populate data array
$TagData = [ $TagData = [
'title' => [esc($episode->title)], 'title' => [esc($episode->title)],
'artist' => [ 'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'album' => [esc($episode->podcast->title)], 'album' => [esc($episode->podcast->title)],
'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''], 'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'], 'genre' => ['Podcast'],
'comment' => [$episode->description], 'comment' => [$episode->description],
'track_number' => [(string) $episode->number], 'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright], 'copyright_message' => [$episode->podcast->copyright],
'publisher' => [ 'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
$episode->podcast->publisher === null 'encoded_by' => ['Castopod'],
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url], // 'website' => [$podcast_url],

View File

@ -164,7 +164,7 @@ if (! function_exists('parse_size')) {
$size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size. $size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size.
if ($unit !== '') { if ($unit !== '') {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. // 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); return round($size);
@ -183,7 +183,7 @@ if (! function_exists('format_bytes')) {
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000)); $pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1); $pow = min($pow, count($units) - 1);
$bytes /= pow($is_binary ? 1024 : 1000, $pow); $bytes /= ($is_binary ? 1024 : 1000) ** $pow;
return round($bytes, $precision) . $units[$pow]; return round($bytes, $precision) . $units[$pow];
} }

View File

@ -17,7 +17,6 @@ use Config\Mimes;
use Modules\Media\Entities\Chapters; use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Transcript; use Modules\Media\Entities\Transcript;
use Modules\PremiumPodcasts\Entities\Subscription; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\WebSub\Config\WebSub;
if (! function_exists('get_rss_feed')) { if (! function_exists('get_rss_feed')) {
/** /**
@ -122,6 +121,12 @@ if (! function_exists('get_rss_feed')) {
->addAttribute('owner', $podcast->owner_email); ->addAttribute('owner', $podcast->owner_email);
} }
if ($podcast->verify_txt !== null) {
$channel
->addChild('txt', $podcast->verify_txt, $podcastNamespace)
->addAttribute('purpose', 'verify');
}
if ($podcast->imported_feed_url !== null) { if ($podcast->imported_feed_url !== null) {
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace); $channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace);
} }
@ -255,12 +260,7 @@ if (! function_exists('get_rss_feed')) {
$itunesNamespace, $itunesNamespace,
); );
$channel->addChild( $channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, $itunesNamespace, false);
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunesNamespace,
false
);
$channel->addChild('link', $podcast->link); $channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunesNamespace); $owner = $channel->addChild('owner', null, $itunesNamespace);
@ -274,7 +274,7 @@ if (! function_exists('get_rss_feed')) {
$channel->addChild('type', $podcast->type, $itunesNamespace); $channel->addChild('type', $podcast->type, $itunesNamespace);
$podcast->copyright && $podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright); $channel->addChild('copyright', $podcast->copyright);
if ($podcast->is_blocked) { if ($podcast->is_blocked || $subscription instanceof Subscription) {
$channel->addChild('block', 'Yes', $itunesNamespace); $channel->addChild('block', 'Yes', $itunesNamespace);
} }
@ -348,6 +348,21 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('season', (string) $episode->season_number, $itunesNamespace); $item->addChild('season', (string) $episode->season_number, $itunesNamespace);
$item->addChild('episodeType', $episode->type, $itunesNamespace); $item->addChild('episodeType', $episode->type, $itunesNamespace);
// If episode is of type trailer, add podcast:trailer tag on channel level
if ($episode->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace);
$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 podcast namespace tags for season and episode // add podcast namespace tags for season and episode
$episode->season_number && $episode->season_number &&
$item->addChild('season', (string) $episode->season_number, $podcastNamespace); $item->addChild('season', (string) $episode->season_number, $podcastNamespace);

View File

@ -8,39 +8,6 @@ declare(strict_types=1);
* @link https://castopod.org/ * @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
* @param string|null $type type of icon to be added
* @return string svg contents
*/
function icon(string $name, string $class = '', string $type = null): string
{
if ($type !== null) {
$name = $type . '/' . $name;
}
try {
$svgContents = file_get_contents('assets/icons/' . $name . '.svg');
} catch (Exception) {
if ($type !== null) {
return icon('default', $class, $type);
}
return '□';
}
if ($class !== '') {
return str_replace('<svg', '<svg class="' . $class . '"', $svgContents);
}
return $svgContents;
}
}
if (! function_exists('svg')) { if (! function_exists('svg')) {
/** /**
* Returns the inline svg image * Returns the inline svg image

View File

@ -24,6 +24,7 @@ return [
'comments' => 'التعليقات', 'comments' => 'التعليقات',
'activity' => 'النشاط', 'activity' => 'النشاط',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'وصف الحلقة', 'description' => 'وصف الحلقة',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -27,6 +27,7 @@ return [
'comments' => 'Evezhiadennoù', 'comments' => 'Evezhiadennoù',
'activity' => 'Oberiantiz', 'activity' => 'Oberiantiz',
'chapters' => 'Chabistroù', 'chapters' => 'Chabistroù',
'transcript' => 'Transcript',
'description' => 'Deskrivadur ar rann', 'description' => 'Deskrivadur ar rann',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# evezhiadenn} one {# evezhiadenn}
@ -50,4 +51,6 @@ return [
'publish_edit' => 'Kemmañ an embannadur', 'publish_edit' => 'Kemmañ an embannadur',
], ],
'no_chapters' => 'N\'eus chabistr ebet evit ar rann.', 'no_chapters' => 'N\'eus chabistr ebet evit ar rann.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comentaris', 'comments' => 'Comentaris',
'activity' => 'Activitat', 'activity' => 'Activitat',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descripció de l\'episodi', 'description' => 'Descripció de l\'episodi',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comentari} one {# comentari}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Kommentarer', 'comments' => 'Kommentarer',
'activity' => 'Aktivitet', 'activity' => 'Aktivitet',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episodebeskrivelse', 'description' => 'Episodebeskrivelse',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# kommentar} one {# kommentar}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -23,7 +23,8 @@ return [
'back_to_episodes' => 'Zurück zu Episoden von {podcast}', 'back_to_episodes' => 'Zurück zu Episoden von {podcast}',
'comments' => 'Kommentare', 'comments' => 'Kommentare',
'activity' => 'Aktivitäten', 'activity' => 'Aktivitäten',
'chapters' => 'Chapters', 'chapters' => 'Kapitel',
'transcript' => 'Transcript',
'description' => 'Beschreibung der Episode', 'description' => 'Beschreibung der Episode',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# Kommentar} one {# Kommentar}
@ -43,5 +44,7 @@ return [
'publish' => 'Veröffentlichen', 'publish' => 'Veröffentlichen',
'publish_edit' => 'Veröffentlichung bearbeiten', 'publish_edit' => 'Veröffentlichung bearbeiten',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'Für diese Episode sind keine Kapitel verfügbar.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -51,5 +51,5 @@ return [
other {# Personen} other {# Personen}
}', }',
'persons_list' => 'Mitwirkende', 'persons_list' => 'Mitwirkende',
'castopod_website' => 'Castopod (website)', 'castopod_website' => 'Castopod (Webseite)',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Σχόλια', 'comments' => 'Σχόλια',
'activity' => 'Δραστηριότητα', 'activity' => 'Δραστηριότητα',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Περιγραφή επεισοδίου', 'description' => 'Περιγραφή επεισοδίου',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# σχόλιο} one {# σχόλιο}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comments', 'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description', 'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comentarios', 'comments' => 'Comentarios',
'activity' => 'Actividad', 'activity' => 'Actividad',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descripción del episodio', 'description' => 'Descripción del episodio',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comentario} one {# comentario}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -0,0 +1,34 @@
<?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 comment for {episodeTitle}",
'back_to_comments' => 'Back to comments',
'form' => [
'episode_message_placeholder' => 'Write a comment…',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'submit' => 'Send',
'submit_reply' => 'Reply',
],
'likes' => '{numberOfLikes, plural,
one {# like}
other {# likes}
}',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
}',
'like' => 'Like',
'reply' => 'Reply',
'view_replies' => 'View replies ({numberOfReplies})',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete comment',
];

View File

@ -0,0 +1,30 @@
<?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' => 'Yes',
'no' => 'No',
'cancel' => 'Cancel',
'optional' => 'Optional',
'close' => 'Close',
'home' => 'Home',
'explicit' => 'Explicit',
'powered_by' => 'Powered by {castopod}',
'go_back' => 'Go back',
'play_episode_button' => [
'play' => 'Play',
'playing' => 'Playing',
],
'read_more' => 'Read more',
'read_less' => 'Read less',
'see_more' => 'See more',
'see_less' => 'See less',
'legal_notice' => 'Legal notice',
];

View File

@ -0,0 +1,50 @@
<?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' => 'Season {seasonNumber}',
'season_abbr' => 'S{seasonNumber}',
'number' => 'Episode {episodeNumber}',
'number_abbr' => 'Ep. {episodeNumber}',
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => 'Persons',
'back_to_episodes' => 'Back to episodes of {podcast}',
'comments' => 'Comments',
'activity' => 'Activity',
'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
other {# comments}
}',
'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go 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.',
];

View File

@ -0,0 +1,37 @@
<?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' => 'Follow',
'title' => 'Follow {actorDisplayName}',
'subtitle' => 'You are going to follow:',
'accountNotFound' => 'The account could not be found.',
'remoteFollowNotAllowed' => 'Seems like the account server does not allow remote follows…',
'submit' => 'Proceed to follow',
],
'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' => 'Proceed to share',
],
'reply' => [
'title' => "Reply to {actorDisplayName}'s post",
'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply',
],
];

20
app/Language/eu/Home.php Normal file
View File

@ -0,0 +1,20 @@
<?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' => 'All podcasts',
'sort_by' => 'Sort by',
'sort_options' => [
'activity' => 'Recent activity',
'created_desc' => 'Newest first',
'created_asc' => 'Oldest first',
],
'no_podcast' => 'No podcast found',
];

17
app/Language/eu/Page.php Normal file
View File

@ -0,0 +1,17 @@
<?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' => 'Back to home',
'map' => [
'title' => 'Map',
'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.',
],
];

View File

@ -0,0 +1,55 @@
<?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' => 'Season {seasonNumber}',
'list_of_episodes_year' => '{year} episodes ({episodeCount})',
'list_of_episodes_season' =>
'Season {seasonNumber} episodes ({episodeCount})',
'no_episode' => 'No episode found!',
'follow' => 'Follow',
'followTitle' => 'Follow {actorDisplayName} on the fediverse!',
'followers' => '{numberOfFollowers, plural,
one {# follower}
other {# followers}
}',
'posts' => '{numberOfPosts, plural,
one {# post}
other {# posts}
}',
'links' => 'Links',
'activity' => 'Activity',
'episodes' => 'Episodes',
'episodes_title' => 'Episodes of {podcastTitle}',
'about' => 'About',
'stats' => [
'title' => 'Stats',
'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' => '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' => 'Persons',
'castopod_website' => 'Castopod (website)',
];

40
app/Language/eu/Post.php Normal file
View File

@ -0,0 +1,40 @@
<?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' => 'Back to {actor} posts',
'actor_shared' => '{actor} shared',
'reply_to' => 'Reply to @{actorUsername}',
'form' => [
'message_placeholder' => 'Write a message…',
'episode_message_placeholder' => 'Write a message for the episode…',
'episode_url_placeholder' => 'Episode URL',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'submit' => 'Send',
'submit_reply' => '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' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete post',
];

View File

@ -23,6 +23,7 @@ return [
'comments' => 'دیدگاه‌ها', 'comments' => 'دیدگاه‌ها',
'activity' => 'فعّالیت', 'activity' => 'فعّالیت',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'شرح قسمت', 'description' => 'شرح قسمت',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
other {# نظر} other {# نظر}
@ -42,4 +43,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Commentaires', 'comments' => 'Commentaires',
'activity' => 'Activité', 'activity' => 'Activité',
'chapters' => 'Chapitres', 'chapters' => 'Chapitres',
'transcript' => 'Transcript',
'description' => 'Description de lépisode', 'description' => 'Description de lépisode',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# commentaire} one {# commentaire}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Modifier la publication', 'publish_edit' => 'Modifier la publication',
], ],
'no_chapters' => 'Aucun chapitre nest disponible pour cet épisode.', 'no_chapters' => 'Aucun chapitre nest disponible pour cet épisode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Commentaires', 'comments' => 'Commentaires',
'activity' => 'Activité', 'activity' => 'Activité',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Description de lépisode', 'description' => 'Description de lépisode',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# commentaire} one {# commentaire}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comments', 'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description', 'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -26,6 +26,7 @@ return [
'comments' => 'Beachdan', 'comments' => 'Beachdan',
'activity' => 'Gnìomhachd', 'activity' => 'Gnìomhachd',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Tuairisgeul an eapasoid', 'description' => 'Tuairisgeul an eapasoid',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# bheachd} one {# bheachd}
@ -48,4 +49,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comentarios', 'comments' => 'Comentarios',
'activity' => 'Actividade', 'activity' => 'Actividade',
'chapters' => 'Capítulos', 'chapters' => 'Capítulos',
'transcript' => 'Transcript',
'description' => 'Descrición do episodio', 'description' => 'Descrición do episodio',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comentario} one {# comentario}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Editar publicación', 'publish_edit' => 'Editar publicación',
], ],
'no_chapters' => 'Non hai capítulos dispoñibles para este episodio.', 'no_chapters' => 'Non hai capítulos dispoñibles para este episodio.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -23,6 +23,7 @@ return [
'comments' => 'Komentar', 'comments' => 'Komentar',
'activity' => 'Aktivitas', 'activity' => 'Aktivitas',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Keterangan episode', 'description' => 'Keterangan episode',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
other {# komentar} other {# komentar}
@ -42,4 +43,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Commenti', 'comments' => 'Commenti',
'activity' => 'Attività', 'activity' => 'Attività',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descrizione dell\'episodio', 'description' => 'Descrizione dell\'episodio',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Modifica pubblicazione', 'publish_edit' => 'Modifica pubblicazione',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -27,8 +27,8 @@ return [
}', }',
'like' => 'いいね', 'like' => 'いいね',
'reply' => '返信する', 'reply' => '返信する',
'view_replies' => 'View replies ({numberOfReplies})', 'view_replies' => '返信を見る ({numberOfReplies})',
'block_actor' => 'Block user @{actorUsername}', 'block_actor' => 'ユーザー @{actorUsername} をブロック',
'block_domain' => 'Block domain @{actorDomain}', 'block_domain' => 'ドメイン @{actorDomain} をブロック',
'delete' => 'コメントを削除する', 'delete' => 'コメントを削除する',
]; ];

View File

@ -13,10 +13,10 @@ return [
'no' => 'いいえ', 'no' => 'いいえ',
'cancel' => 'キャンセル', 'cancel' => 'キャンセル',
'optional' => 'Optional', 'optional' => 'Optional',
'close' => 'Close', 'close' => '閉じる',
'home' => 'ホーム', 'home' => 'ホーム',
'explicit' => 'Explicit', 'explicit' => '過激な内容を含む',
'powered_by' => 'Powered by {castopod}', 'powered_by' => '提供: {castopod}',
'go_back' => '戻る', 'go_back' => '戻る',
'play_episode_button' => [ 'play_episode_button' => [
'play' => '再生', 'play' => '再生',
@ -24,7 +24,7 @@ return [
], ],
'read_more' => '続きを読む', 'read_more' => '続きを読む',
'read_less' => '閉じる', 'read_less' => '閉じる',
'see_more' => 'See more', 'see_more' => 'もっと見る',
'see_less' => 'See less', 'see_less' => '表示を減らす',
'legal_notice' => 'Legal notice', 'legal_notice' => '法的事項',
]; ];

View File

@ -10,38 +10,40 @@ declare(strict_types=1);
return [ return [
'season' => 'シーズン {seasonNumber}', 'season' => 'シーズン {seasonNumber}',
'season_abbr' => 'S{seasonNumber}', 'season_abbr' => 'シーズン {seasonNumber}',
'number' => 'エピソード {episodeNumber}', 'number' => 'エピソード {episodeNumber}',
'number_abbr' => 'Ep. {episodeNumber}', 'number_abbr' => 'エピソード {episodeNumber}',
'season_episode' => 'シーズン {seasonNumber} エピソード {episodeNumber}', 'season_episode' => 'シーズン {seasonNumber} エピソード {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}', 'season_episode_abbr' => 'シーズン{seasonNumber}エピソード{episodeNumber}',
'persons' => '{personsCount, plural, 'persons' => '{personsCount, plural,
one {# person} other {# 人}
other {# persons}
}', }',
'persons_list' => 'Persons', 'persons_list' => '人物',
'back_to_episodes' => '{podcast} のエピソードに戻る', 'back_to_episodes' => '{podcast} のエピソードに戻る',
'comments' => 'コメント', 'comments' => 'コメント',
'activity' => 'アクティビティ', 'activity' => 'アクティビティ',
'chapters' => 'Chapters', 'chapters' => '章',
'description' => 'Episode description', 'transcript' => 'Transcript',
'description' => 'エピソードの詳細',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
other {# comments} other {# comments}
}', }',
'all_podcast_episodes' => 'All podcast episodes', 'all_podcast_episodes' => 'すべての Podcast エピソード',
'back_to_podcast' => 'ポッドキャストへ戻る', 'back_to_podcast' => 'ポッドキャストへ戻る',
'preview' => [ 'preview' => [
'title' => 'プレビュー', 'title' => 'プレビュー',
'not_published' => 'Not published', 'not_published' => '未公開',
'text' => '{publication_status, select, 'text' => '{publication_status, select,
published {This episode is not yet published.} published {このエピソードはまだ公開されていません}
scheduled {This episode is scheduled for publication on {publication_date}.} scheduled {このエピソードは {publication_date} に公開される予定です}
with_podcast {This episode will be published at the same time as the podcast.} with_podcast {このエピソードはPodCastと同時に公開されます}
other {This episode is not yet published.} other {このエピソードはまだ公開されていません。}
}', }',
'publish' => '公開する', 'publish' => '公開する',
'publish_edit' => 'Edit publication', 'publish_edit' => '出版物を編集',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -9,29 +9,29 @@ declare(strict_types=1);
*/ */
return [ return [
'your_handle' => 'Your handle', 'your_handle' => 'あなたのユーザー ID',
'your_handle_hint' => 'Enter the @username@domain you want to act from.', 'your_handle_hint' => 'Enter the @username@domain you want to act from.',
'follow' => [ 'follow' => [
'label' => 'フォロー', 'label' => 'フォロー',
'title' => '{actorDisplayName} をフォロー', 'title' => '{actorDisplayName} をフォロー',
'subtitle' => 'You are going to follow:', 'subtitle' => 'You are going to follow:',
'accountNotFound' => 'アカウントが見つかりませんでした', 'accountNotFound' => 'アカウントが見つかりませんでした',
'remoteFollowNotAllowed' => 'Seems like the account server does not allow remote follows…', 'remoteFollowNotAllowed' => 'このアカウントサーバーはリモートフォローを許可しておりません',
'submit' => 'Proceed to follow', 'submit' => 'フォローする',
], ],
'favourite' => [ 'favourite' => [
'title' => "Favourite {actorDisplayName}'s post", 'title' => "お気に入りの {actorDisplayName}の投稿",
'subtitle' => 'You are going to favourite:', 'subtitle' => 'You are going to favourite:',
'submit' => 'Proceed to favourite', 'submit' => 'お気に入り登録する',
], ],
'reblog' => [ 'reblog' => [
'title' => "Share {actorDisplayName}'s post", 'title' => "Share {actorDisplayName}'s post",
'subtitle' => 'You are going to share:', 'subtitle' => 'You are going to share:',
'submit' => 'Proceed to share', 'submit' => '共有する',
], ],
'reply' => [ 'reply' => [
'title' => "Reply to {actorDisplayName}'s post", 'title' => "Reply to {actorDisplayName}'s post",
'subtitle' => 'You are going to reply to:', 'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply', 'submit' => '返信する',
], ],
]; ];

View File

@ -9,9 +9,9 @@ declare(strict_types=1);
*/ */
return [ return [
'back_to_home' => 'Back to home', 'back_to_home' => 'ホームへ戻る',
'map' => [ 'map' => [
'title' => '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.', 'description' => '{siteName} でpodcastのエピソードを見つけましょうマップを旅して、特定の場所について話すエピソードを聞きましょう。',
], ],
]; ];

View File

@ -9,29 +9,27 @@ declare(strict_types=1);
*/ */
return [ return [
'feed' => 'RSS Podcast feed', 'feed' => 'RSS PodCastフィード',
'season' => 'Season {seasonNumber}', 'season' => 'シーズン {seasonNumber}',
'list_of_episodes_year' => '{year} episodes ({episodeCount})', 'list_of_episodes_year' => '{year} エピソード ({episodeCount})',
'list_of_episodes_season' => 'list_of_episodes_season' =>
'Season {seasonNumber} episodes ({episodeCount})', 'シーズン {seasonNumber} エピソード({episodeCount}',
'no_episode' => 'No episode found!', 'no_episode' => 'エピソードが見つかりませんでした',
'follow' => 'Follow', 'follow' => 'フォロー',
'followTitle' => 'Follow {actorDisplayName} on the fediverse!', 'followTitle' => 'Fediverseで {actorDisplayName} をフォロー!',
'followers' => '{numberOfFollowers, plural, 'followers' => '{numberOfFollowers, plural,
one {# follower} other {# 人のフォロワー}
other {# followers}
}', }',
'posts' => '{numberOfPosts, plural, 'posts' => '{numberOfPosts, plural,
one {# post} other {#件の投稿}
other {# posts}
}', }',
'links' => 'Links', 'links' => 'リンク',
'activity' => 'Activity', 'activity' => 'アクティビティー',
'episodes' => 'Episodes', 'episodes' => 'エピソード',
'episodes_title' => 'Episodes of {podcastTitle}', 'episodes_title' => '{podcastTitle} のエピソード',
'about' => 'About', 'about' => '概要',
'stats' => [ 'stats' => [
'title' => 'Stats', 'title' => '統計',
'number_of_seasons' => '{0, plural, 'number_of_seasons' => '{0, plural,
one {# season} one {# season}
other {# seasons} other {# seasons}

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comments', 'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description', 'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comments', 'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description', 'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Reacties', 'comments' => 'Reacties',
'activity' => 'Activiteiten', 'activity' => 'Activiteiten',
'chapters' => 'Hoofdstukken', 'chapters' => 'Hoofdstukken',
'transcript' => 'Transcript',
'description' => 'Omschrijving aflevering', 'description' => 'Omschrijving aflevering',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# reactie} one {# reactie}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Publicatie bewerken', 'publish_edit' => 'Publicatie bewerken',
], ],
'no_chapters' => 'Voor deze aflevering zijn geen hoofdstukken beschikbaar.', 'no_chapters' => 'Voor deze aflevering zijn geen hoofdstukken beschikbaar.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Kommentarar', 'comments' => 'Kommentarar',
'activity' => 'Aktivitet', 'activity' => 'Aktivitet',
'chapters' => 'Kapittel', 'chapters' => 'Kapittel',
'transcript' => 'Transcript',
'description' => 'Skildring av episoden', 'description' => 'Skildring av episoden',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# kommentar} one {# kommentar}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Rediger publiseringa', 'publish_edit' => 'Rediger publiseringa',
], ],
'no_chapters' => 'Det finst ingen kapittel for denne episoden.', 'no_chapters' => 'Det finst ingen kapittel for denne episoden.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comentaris', 'comments' => 'Comentaris',
'activity' => 'Activitat', 'activity' => 'Activitat',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descripcion de lepisòdi', 'description' => 'Descripcion de lepisòdi',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comentari} one {# comentari}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Modificar la publicacion', 'publish_edit' => 'Modificar la publicacion',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -25,6 +25,7 @@ return [
'comments' => 'Komentarze', 'comments' => 'Komentarze',
'activity' => 'Aktywność', 'activity' => 'Aktywność',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Opis odcinka', 'description' => 'Opis odcinka',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# komentarz} one {# komentarz}
@ -46,4 +47,6 @@ return [
'publish_edit' => 'Edytuj publikację', 'publish_edit' => 'Edytuj publikację',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comentários', 'comments' => 'Comentários',
'activity' => 'Atividade', 'activity' => 'Atividade',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descrição do episódio', 'description' => 'Descrição do episódio',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comentário} one {# comentário}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Editar Publicação', 'publish_edit' => 'Editar Publicação',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Comments', 'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Episode description', 'description' => 'Episode description',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# comment} one {# comment}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -25,6 +25,7 @@ return [
'comments' => 'Comentarii', 'comments' => 'Comentarii',
'activity' => 'Activitate', 'activity' => 'Activitate',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Descrierea episodului', 'description' => 'Descrierea episodului',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# răspuns} one {# răspuns}
@ -46,4 +47,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -26,6 +26,7 @@ return [
'comments' => 'Комментарии', 'comments' => 'Комментарии',
'activity' => 'Активность', 'activity' => 'Активность',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Описание серии', 'description' => 'Описание серии',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# комментарий} one {# комментарий}
@ -48,4 +49,6 @@ return [
'publish_edit' => 'Редактировать публикацию', 'publish_edit' => 'Редактировать публикацию',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -26,6 +26,7 @@ return [
'comments' => 'Komentáre', 'comments' => 'Komentáre',
'activity' => 'Aktivita', 'activity' => 'Aktivita',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Popis epizódy', 'description' => 'Popis epizódy',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# komentár} one {# komentár}
@ -48,4 +49,6 @@ return [
'publish_edit' => 'Upraviť zverejnené', 'publish_edit' => 'Upraviť zverejnené',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Komentari', 'comments' => 'Komentari',
'activity' => 'Aktivnosti', 'activity' => 'Aktivnosti',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Opis epizode', 'description' => 'Opis epizode',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# komentar} one {# komentar}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Uredi objavu', 'publish_edit' => 'Uredi objavu',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => 'Kommentarer', 'comments' => 'Kommentarer',
'activity' => 'Aktivitet', 'activity' => 'Aktivitet',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Beskrivning av avsnitt', 'description' => 'Beskrivning av avsnitt',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# kommentar} one {# kommentar}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -26,6 +26,7 @@ return [
'comments' => 'Коментарі', 'comments' => 'Коментарі',
'activity' => 'Активність', 'activity' => 'Активність',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => 'Опис Серії', 'description' => 'Опис Серії',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# коментар} one {# коментар}
@ -48,4 +49,6 @@ return [
'publish_edit' => 'Редагувати публікацію', 'publish_edit' => 'Редагувати публікацію',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -24,6 +24,7 @@ return [
'comments' => '评论', 'comments' => '评论',
'activity' => '活动', 'activity' => '活动',
'chapters' => 'Chapters', 'chapters' => 'Chapters',
'transcript' => 'Transcript',
'description' => '剧集描述', 'description' => '剧集描述',
'number_of_comments' => '{numberOfComments, plural, 'number_of_comments' => '{numberOfComments, plural,
other {# 评论} other {# 评论}
@ -44,4 +45,6 @@ return [
'publish_edit' => 'Edit publication', 'publish_edit' => 'Edit publication',
], ],
'no_chapters' => 'No chapters are available for this episode.', 'no_chapters' => 'No chapters are available for this episode.',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
]; ];

View File

@ -0,0 +1,32 @@
<?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,
other {# 讚}
}',
'replies' => '{numberOfReplies, plural,
other {# 回覆}
}',
'like' => '讚',
'reply' => '回覆',
'view_replies' => '檢視回覆 ({numberOfReplies})',
'block_actor' => '封鎖使用者 @{actorUsername}',
'block_domain' => '封鎖網域 @{actorDomain}',
'delete' => '刪除評論',
];

View File

@ -0,0 +1,30 @@
<?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' => '露骨內容',
'powered_by' => '由 {castopod} 提供支援',
'go_back' => '返回',
'play_episode_button' => [
'play' => '播放',
'playing' => '播放中',
],
'read_more' => '閱讀更多',
'read_less' => '顯示更少',
'see_more' => '顯示更多',
'see_less' => '顯示較少',
'legal_notice' => '法律聲明',
];

View File

@ -0,0 +1,50 @@
<?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' => 'S{seasonNumber}',
'number' => '第 {episodeNumber} 集',
'number_abbr' => 'Ep. {episodeNumber}',
'season_episode' => '第{seasonNumber} 季第{episodeNumber} 集',
'season_episode_abbr' => 'S{seasonNumber}:E{episodeNumber}',
'persons' => '{personsCount, plural,
one {# person}
other {# persons}
}',
'persons_list' => '人物',
'back_to_episodes' => '回到劇集 {podcast} 中',
'comments' => '註釋',
'activity' => '活動',
'chapters' => '章',
'transcript' => 'Transcript',
'description' => '節目介紹',
'number_of_comments' => '{numberOfComments, plural,
one {# 評論}
other {# 評論}
}',
'all_podcast_episodes' => '所有播客劇集',
'back_to_podcast' => '返回至播客',
'preview' => [
'title' => '預覽',
'not_published' => '未發佈',
'text' => '{publication_status, select,
published {本集尚未發佈。}
scheduled {本集排程於 {publication_date} 發佈}
with_podcast {本集將會與此播客同時發佈。}
other {本集尚未發佈。}
}',
'publish' => '發佈',
'publish_edit' => '編輯公開程度',
],
'no_chapters' => '本劇集未有章節',
'download_transcript' => 'Download transcript ({extension})',
'no_transcript' => 'No transcript available for this episode.',
];

Some files were not shown because too many files have changed in this diff Show More