Compare commits

..

814 commits

Author SHA1 Message Date
Yassine Doghri
bc041702dd
docs: update CODE_OF_CONDUCT.md to be based on Contributor Covenant 3.0 2026-03-01 12:40:08 +00:00
Yassine Doghri
c13bbdffdf
fix(docker): add arch-specific supercronic and s6-overlay services
fix #580
2026-02-24 21:04:00 +01:00
Yassine Doghri
aad17646f1
build(docs): change wrong Aside type from warning to caution in docker page 2026-02-19 23:15:44 +00:00
Yassine Doghri
385a3cb13a
ci(docker): edit CI Dockerfile to install pnpm using corepack 2026-02-19 21:44:03 +00:00
Yassine Doghri
ed57e13b40
feat: set min PHP version to 8.5 + upgrade CI4 to 4.7
update all dependencies to latest
2026-02-19 16:23:20 +00:00
Yassine Doghri
6b302ad8bf
fix(player): load icons locally instead of relying on vimejs picking them from third party scripts
closes #551
2026-02-19 13:16:55 +00:00
Yassine Doghri
cc86ce030f
fix(emails): display verification link in clear text for email clients only displaying text
closes #328
2026-02-19 12:44:54 +00:00
Yassine Doghri
3943441683
build(docker): set major version channel tags to prevent major breaking changes (1, 2-next, ...) 2026-02-19 12:44:23 +00:00
Yassine Doghri
b747967a18
build(docker): remove --progress flag for rsync when bundling castopod
this reduces logs verbosity for easier debugging
2026-02-17 22:22:26 +01:00
Yassine Doghri
a585362827
chore: update CI files after update from 4.6.3 to 4.6.5 2026-02-17 21:09:55 +00:00
Yassine Doghri
abf214757c
docs(docker): add tip to encourage users to pin the castopod image version during production 2026-02-17 21:04:11 +00:00
Yassine Doghri
f01de13637
build(docker): push amd64 image before overwriting manifest with both amd64 and arm64 platforms 2026-02-17 22:02:46 +01:00
Yassine Doghri
77826552f1
fix(docker): create optimized builder with docker-container driver for arm64 builds
closes #580
2026-02-17 20:32:17 +01:00
Yassine Doghri
e5fb676cb6
feat(docker): replace all-in-one image with FrankenPHP and Caddy based image + discard other images
- use serversideup/php as a base image
- remove nginx unit base
- remove app / webserver images
- add bundle stage to remove pipeline dependency
- update docker setup docs
- edit gitlabci rules and release logic
2026-02-17 19:31:24 +00:00
Yassine Doghri
49a43d08cc fix(fediverse): match episode posts replies fields with comments in union query
fixes #577
2025-12-20 18:54:19 +00:00
Andreas Grupp
89d0fe4a7e fix(fediverse): access to URI in 'object' instead of going down with '->id' in delete case 2025-11-03 10:35:35 +01:00
kloo kloo
950d42c838 fix: edit Podcast.php to clarify the followers are Fediverse followers 2025-10-13 12:37:17 +00:00
kloo kloo
6be7a1f4d7 fix: edit Platforms.php to alphabetize all broadcast platforms + minor UI labels edits 2025-10-13 12:36:51 +00:00
Paul Cutler
4c46c15e39 docs: update docs to expand Persons information and fix broken chapters.json link 2025-10-13 12:36:37 +00:00
Yassine Doghri
bbfaa1bfc3 chore: update php and js dependencies to latest 2025-10-07 13:32:48 +00:00
Yassine Doghri
85503ee282 docs(plugins): clear up some ideas and fix links
update castopod-plugins-manager + other dependencies to latest
2025-10-06 16:53:59 +00:00
Yassine Doghri
265cbbac09 fix(bundle): edit rsync filter to include resources/icons directory 2025-10-03 13:07:26 +00:00
Yassine Doghri
e291b6239c docs(plugins): add install and share instructions with the official plugins repository for discovery 2025-10-03 12:51:03 +00:00
Yassine Doghri
40f671c8b6 docs: add plugin manifest schema definition as a page in the docs root
update js and php dependencies
2025-09-30 19:31:15 +00:00
Yassine Doghri
3d0db5c64a feat(plugins): add spark commands to install, add, update and remove plugins using adaures' cpm
update js & php dependencies to latest and fix rector, phpstan and ecs issues
2025-09-22 17:34:36 +00:00
Yassine Doghri
b5a403b990 chore: replace twitter links by bluesky in docs
+ update dependencies to latest
2025-08-31 09:48:01 +00:00
Yassine Doghri
835f099f2e docs: update starlight to 0.35.2 + update docker images to latest 2025-08-29 10:37:01 +00:00
Yassine Doghri
9dffc8d5f1 ci(php-icons): fix local icon sets path 2025-08-26 08:10:36 +00:00
Yassine Doghri
8ec42c33ff fix(fediverse): add is_private field to posts to flag private posts and hide them from public views 2025-08-25 18:32:09 +00:00
Yassine Doghri
346c00e7b5 chore: update CI to v4.6.3 + all php and js dependencies 2025-08-25 18:09:41 +00:00
kloo kloo
96b2df15b0 chore: add discourse social network 2025-05-20 14:23:01 +02:00
Paul Cutler
61d6a6b60f docs: fix broken note using Aside tag 2025-05-20 13:32:24 +02:00
Yassine Doghri
00870ceff2 ci: skip ssl when connecting to mariadb test database 2025-03-14 13:45:36 +00:00
Yassine Doghri
31fee52208 docs: update database and php requirements to LTS versions 2025-03-14 13:44:55 +00:00
Yassine Doghri
567d5e01a3 feat(plugins): add submodule boolean property to manifest schema 2025-03-14 13:02:55 +00:00
Yassine Doghri
94cea0ce91 feat: set min PHP version to 8.4
update CI4 to 4.6.0 + use codeigniter-vite and vite-plugin-codeigniter to load assets
2025-03-14 12:54:51 +00:00
Paul Cutler
0e4e301b81 docs: fix broken Note in instance / import podcast by using Aside tag 2025-02-27 15:52:36 +00:00
Paul Cutler
f8fb25f52d docs: update docs with typo fixes 2025-02-27 15:48:42 +00:00
Paul Cutler
93b4741333 docs: update CONTRIBUTING-DEV.md with how to update documentation 2025-02-27 15:43:14 +00:00
Yassine Doghri
5578104207 ci(docs): update pnpm install script to v10 2025-02-27 11:44:56 +00:00
Yassine Doghri
5dce8cb949 fix: update api schema to pass form data when publishing an episode
closes #553
2025-02-27 11:09:45 +00:00
Yassine Doghri
1e6477db67 docs(readme): update logo & sponsor images + all-contributors list 2025-01-04 11:28:21 +00:00
Yassine Doghri
0265775177 fix(analytics): edit permission filters to include podcast id in routes 2024-12-30 15:50:05 +00:00
semantic-release-bot
c9fabe8888 chore(release): 2.0.0-next.3 [skip ci]
## [2.0.0-next.3](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.2...v2.0.0-next.3) (2024-12-30)

### Features

* **api:** add Episode create and publish endpoints ([a90cdfd](a90cdfdcdb))
* **image:** add image size's width and height ([f50098e](f50098ec89))
* **plugins:** add defaultValue for all field types ([d3a98db](d3a98db6d0))
* **plugins:** add group field type + multiple option to render field arrays ([11ccd0e](11ccd0ebe7))
* **plugins:** add html field type + CodeEditor component + rework html head generation ([8cf9c6d](8cf9c6dc83))
* **rss:** add option for 301 redirect to new feed url ([8402cc2](8402cc29d2))

### Bug Fixes

* add downloads_count to episodes table, computed every hour ([f981937](f981937645))
* allow passing json to app.proxyIPs config to set it ([cbf739e](cbf739e95c))
* **api:** cast integers when creating episode ([775b302](775b302f7c))
* **docker-image:** clear cache to account for new assets and data structure changes ([63c763f](63c763f941)), closes [#510](https://code.castopod.org/adaures/castopod/issues/510)
* edit remap functions to get episode in episode admin controllers ([9f74cca](9f74cca342))
* **episode:** do not change slug when editing episode title ([a83afb0](a83afb0004)), closes [#513](https://code.castopod.org/adaures/castopod/issues/513)
* **fediverse:** add "processing" and "failed" statuses to better manage broadcast load ([1d7583d](1d7583d738)), closes [#511](https://code.castopod.org/adaures/castopod/issues/511)
* **icons:** set correct names for lock and lock-unlock icons in premium banner ([37ee6d3](37ee6d35b4))
* **plugins:** clear cache after activating or deactivating plugin ([08c7df2](08c7df2a5d))
* **plugins:** delete relevant cache when submitting settings ([00bd4c0](00bd4c02ee))
* **podcast-model:** always query podcast from database when clearing cache ([d30c49c](d30c49cdff))
* **premium-podcasts:** update query to validate subscription ([2b1bbf3](2b1bbf3430))
* **preview:** delete episode preview cache after editing episode ([732d429](732d42923d)), closes [#514](https://code.castopod.org/adaures/castopod/issues/514)
* **release:** add conventional-changelog-conventionalcommits for CHANGELOG generation ([6934c8a](6934c8aa8f))
* **rss:** add subscription id to cache name to prevent premium feeds from overlapping ([74f9325](74f9325946))
* set user as www-data when running cron jobs in docker's supervisord config ([65d74f1](65d74f14e6))
* typo in EpisodeController remap function to get episode ([f288a75](f288a750f5))
* update select and multi-select options to value/label arrays ([63f93f5](63f93f585b))

### Internal

* **plugins:** create Field objects per field type in settings forms + handle rendering in class ([34be5bc](34be5bccab))
* remove fields from podcast and episode entities to be replaced with plugins ([b869acb](b869acb3a9))
* rename controller methods for views and actions to be more consistent ([85704bf](85704bfbe0))
* update CodeIgniter to v4.5.6 ([f295e9a](f295e9aa4c))
* update codigniter-icons to v1.0.1 ([fa6967e](fa6967e65c))
* update js dependencies to latest ([70c9797](70c97971fc))
2024-12-30 12:31:21 +00:00
Yassine Doghri
6934c8aa8f fix(release): add conventional-changelog-conventionalcommits for CHANGELOG generation 2024-12-30 12:21:30 +00:00
Yassine Doghri
70c97971fc chore: update js dependencies to latest 2024-12-30 12:02:51 +00:00
Yassine Doghri
9f74cca342 fix: edit remap functions to get episode in episode admin controllers 2024-12-29 16:06:00 +00:00
Yassine Doghri
f295e9aa4c chore: update CodeIgniter to v4.5.6
+ update php dependencies to latest
2024-12-29 16:02:08 +00:00
Yassine Doghri
fc2e7a0d83 docs(api): add instructions to enable and use API 2024-12-29 14:03:23 +00:00
Yassine Doghri
f981937645 fix: add downloads_count to episodes table, computed every hour
This removes computing latency when retrieving episodes list with download count in admin.
The more
analytics records, the more it took to calculate the sum of hits to get the downloads count for each
episode.
2024-12-29 13:24:42 +00:00
Yassine Doghri
f288a750f5 fix: typo in EpisodeController remap function to get episode
- fix defaultValue being empty string when cast as array
- fix initial styles for select to reduce
content layout shift
2024-12-29 13:21:50 +00:00
Yassine Doghri
7e8f0003d1 build(release): update semantic-release config to include internal changes
[ci skip]
2024-12-26 13:41:27 +00:00
Yassine Doghri
888d610c2d docs(api): add available operations based on openapi schema
use starlight-openapi plugin to generate docs

closes #536
2024-12-26 13:01:53 +00:00
Nate Ritter
775b302f7c fix(api): cast integers when creating episode 2024-12-25 11:29:11 +00:00
Yassine Doghri
09256b4eb7 docs(user-guide): update links for consistency + remove missing monetization links 2024-12-25 11:28:51 +00:00
Paul Cutler
0736050d1a docs: add user guide section 2024-12-25 11:26:34 +00:00
Nate Ritter
a90cdfdcdb feat(api): add Episode create and publish endpoints 2024-12-25 11:22:29 +00:00
Yassine Doghri
8402cc29d2 feat(rss): add option for 301 redirect to new feed url 2024-12-25 11:22:13 +00:00
Yassine Doghri
08c7df2a5d fix(plugins): clear cache after activating or deactivating plugin 2024-12-23 16:09:17 +00:00
Yassine Doghri
34be5bccab refactor(plugins): create Field objects per field type in settings forms + handle rendering in class
update manifest.schema.json to have defaultValue type differ based on field type
2024-12-23 15:35:47 +00:00
Yassine Doghri
d3a98db6d0 feat(plugins): add defaultValue for all field types 2024-12-19 12:33:57 +00:00
Yassine Doghri
00bd4c02ee fix(plugins): delete relevant cache when submitting settings 2024-12-18 17:50:33 +00:00
Yassine Doghri
85704bfbe0 refactor: rename controller methods for views and actions to be more consistent
add PermalinkEditor component
2024-12-18 16:05:25 +00:00
Yassine Doghri
8cf9c6dc83 feat(plugins): add html field type + CodeEditor component + rework html head generation
update php and js packages to latest
2024-12-17 15:11:45 +00:00
Yassine Doghri
b869acb3a9 refactor: remove fields from podcast and episode entities to be replaced with plugins 2024-12-15 17:34:36 +00:00
Yassine Doghri
11ccd0ebe7 feat(plugins): add group field type + multiple option to render field arrays
- update docs
- render hint and helper options for all fields
- replace option's hint with
description
2024-12-10 15:57:06 +00:00
Yassine Doghri
f50098ec89 feat(image): add image size's width and height
escape plugin description + replace codeigniter-icons with php-icons v1.2
2024-11-07 12:56:46 +00:00
Paul Cutler
77e55835c0 docs: update command to run vite dev server 2024-11-06 13:19:59 +00:00
Yassine Doghri
fa6967e65c refactor: update codigniter-icons to v1.0.1 2024-11-06 13:19:59 +00:00
Paul Cutler
ea720e01ba docs: update Contributing docs to fix broken link and update spelling and grammar 2024-11-06 13:19:59 +00:00
Yassine Doghri
cbf739e95c fix: allow passing json to app.proxyIPs config to set it 2024-11-06 13:19:59 +00:00
Yassine Doghri
63f93f585b fix: update select and multi-select options to value/label arrays
add hint to select options + update dependencies to latest
2024-11-06 13:19:59 +00:00
Yassine Doghri
65d74f14e6 fix: set user as www-data when running cron jobs in docker's supervisord config
This prevents any ownership issue when cron tasks create cache files
2024-11-06 13:19:59 +00:00
Yassine Doghri
1667f5b202 build: update CI4 to v4.5.5 + php and js packages to latest 2024-11-06 13:19:59 +00:00
Yassine Doghri
1d7583d738 fix(fediverse): add "processing" and "failed" statuses to better manage broadcast load
fixes #511
2024-11-06 13:19:59 +00:00
Yassine Doghri
d30c49cdff fix(podcast-model): always query podcast from database when clearing cache
this prevents from having any unexpected caching side effects
2024-11-06 13:19:59 +00:00
Yassine Doghri
a83afb0004 fix(episode): do not change slug when editing episode title
fixes #513
2024-11-06 13:19:59 +00:00
Yassine Doghri
732d42923d fix(preview): delete episode preview cache after editing episode
fixes #514
2024-11-06 13:19:59 +00:00
Yassine Doghri
63c763f941 fix(docker-image): clear cache to account for new assets and data structure changes
fixes #510
2024-11-06 13:19:58 +00:00
Yassine Doghri
a68959c906 build: update CI4 to 4.5.4 + php and js dependencies to latest 2024-11-06 13:19:58 +00:00
Yassine Doghri
74f9325946 fix(rss): add subscription id to cache name to prevent premium feeds from overlapping 2024-11-06 13:19:58 +00:00
Yassine Doghri
2b1bbf3430 fix(premium-podcasts): update query to validate subscription 2024-11-06 13:19:58 +00:00
Yassine Doghri
37ee6d35b4 fix(icons): set correct names for lock and lock-unlock icons in premium banner 2024-11-06 13:19:58 +00:00
semantic-release-bot
3cd30205d9 chore(release): 2.0.0-next.2 [skip ci]
# [2.0.0-next.2](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.1...v2.0.0-next.2) (2024-07-08)

### Bug Fixes

* **audio-player:** set player icons to default instead of missing Castopod's ([0ba0a25](0ba0a25b11))
* broken icon call in frontend default pages template ([3228362](322836254e))
* **manifest:** set repository url as required in docstring typings ([a8c81b3](a8c81b3fa1))
* set correct icons parameters in map and funding links views ([5d35524](5d35524875)), closes [#500](https://code.castopod.org/adaures/castopod/issues/500)

### Features

* **plugins:** add `minCastopodVersion` to denote incompatibility with previous Castopod versions ([fc9ea75](fc9ea7597e))
* **plugins:** load and display LICENSE.md file if found in plugin's directory ([fee7905](fee7905935))
2024-07-08 16:12:36 +00:00
Yassine Doghri
53232d3b61 docs(security): add disclaimer aside to third-party plugins section 2024-07-08 14:06:56 +00:00
Yassine Doghri
7405f8897d docs(security): add disclaimer for third-party plugins and how to mitigate potential security risks 2024-07-05 17:43:02 +00:00
Yassine Doghri
fc9ea7597e feat(plugins): add minCastopodVersion to denote incompatibility with previous Castopod versions 2024-07-05 16:47:01 +00:00
Yassine Doghri
fee7905935 feat(plugins): load and display LICENSE.md file if found in plugin's directory 2024-07-05 16:44:35 +00:00
Yassine Doghri
1a439083a2 docs: fix typo in comments in auth file 2024-07-04 15:54:17 +00:00
Yassine Doghri
0ba0a25b11 fix(audio-player): set player icons to default instead of missing Castopod's 2024-07-04 14:44:17 +00:00
Yassine Doghri
c21864ee25 docs: add "latest" option to DocsVersionSelect based on main branch 2024-07-04 13:54:59 +00:00
crowdin
1c5fe1fea6 chore(i18n): new Crowdin updates 2024-07-04 13:54:43 +00:00
Yassine Doghri
a8c81b3fa1 fix(manifest): set repository url as required in docstring typings 2024-07-04 13:32:44 +00:00
Aonrud
322836254e fix: broken icon call in frontend default pages template 2024-07-04 13:27:36 +00:00
Yassine Doghri
e9c04548de build: update CI to 4.5.3 + php and js dependencies to latest 2024-07-04 13:27:10 +00:00
Yassine Doghri
5d35524875 fix: set correct icons parameters in map and funding links views
fixes #500
2024-07-04 13:26:59 +00:00
Yassine Doghri
7a8cd4c730 docs: fix typo for "Introduction" label 2024-07-04 13:26:43 +00:00
Yassine Doghri
5339669ea6 build(composer): update version 2.0.0-next to be 2.0.0-dev in composer.json 2024-07-04 13:15:00 +00:00
semantic-release-bot
0eeedb9dc6 chore(release): 2.0.0-next.1 [skip ci]
# [2.0.0-next.1](https://code.castopod.org/adaures/castopod/compare/v1.11.0...v2.0.0-next.1) (6/19/2024)

### Bug Fixes

* add missing php-icons config file to bundle ([56612f0](56612f0c76))
* **docs:** add base to og image using env variable ([fe67659](fe676590f2))
* **import:** rewrite download_file helper to output curl response directly to file ([eb7ad2f](eb7ad2f7e1))
* include app/Resources/icons folder to bundle ([3fd5efc](3fd5efc795))
* **platforms:** add platforms service + reduce memory consumption when rendering platform cards ([fe73e9f](fe73e9fae9))
* set owner email visibility when editing podcast ([fc4f982](fc4f982556)), closes [#473](https://code.castopod.org/adaures/castopod/issues/473)

### Build System

* release next major version as prerelease ([8275226](827522643e))

### Features

* add Plugins module with base files for plugins architecture ([7253e13](7253e13ac2))
* **plugins:** abstract settings form for general, podcast and episode types ([b62b483](b62b483ad9))
* **plugins:** activate / deactivate plugin using settings table ([27d2a1b](27d2a1b0ff))
* **plugins:** add aside with plugin metadata next to plugin's readme ([dfb7888](dfb7888aeb))
* **plugins:** add before channel/item hooks to allow podcast/episode data edit when generating rss ([80d2c48](80d2c48ee2))
* **plugins:** add json schema definition for plugin manifest ([b5eddf3](b5eddf351f))
* **plugins:** add methods to easily retrieve general, podcast and episode settings in hooks methods ([3a900bb](3a900bbab6))
* **plugins:** add new field types + validate & cast user data before storing settings ([6f833fc](6f833fc76a))
* **plugins:** add options to manifest for building forms and storing plugin settings ([3d8aedf](3d8aedf9c3))
* **plugins:** add settings page for podcast and episode if defined in the plugin's manifest ([89ac92f](89ac92fb41))
* **plugins:** add siteHead hook to add custom meta tags to public pages ([e80a33b](e80a33bf2a))
* **plugins:** display errors when plugin is invalid instead of crashing ([8ec7909](8ec79097bb))
* **plugins:** handle empty states and long strings in UI ([45ac2a4](45ac2a4be9))
* **plugins:** load and validate plugin manifest.json ([1510e36](1510e36c0a))
* **plugins:** load plugins using file locator service ([587938d](587938d2bf))
* **plugins:** load README.md file to view plugin's instructions in UI ([e6bfdfc](e6bfdfc390))
* **plugins:** register plugins using Plugin.php file instead of namespace + simplify i18n structure ([2035c39](2035c39fd1))
* **plugins:** uninstall plugins via CLI and admin UI ([9a80de4](9a80de4068))
* set owner email to hidden by default in podcast create form ([7a6d9df](7a6d9df6db))
* support podcast:txt tag with verify use case ([57e459e](57e459e187)), closes [#468](https://code.castopod.org/adaures/castopod/issues/468)

### BREAKING CHANGES

* next major release including plugins architecture
2024-06-19 10:12:35 +00:00
Yassine Doghri
827522643e build: release next major version as prerelease
- edit .releaserc + gitlab-ci to add next branch
- add plugins folder to bundle

BREAKING CHANGE: next major release including plugins architecture
2024-06-19 10:00:45 +00:00
Yassine Doghri
e417d45b14 docs(plugins): fill up rest of manifest and hooks reference + creating a plugin 2024-06-14 15:53:33 +00:00
Yassine Doghri
cc6495dc7c refactor(plugins): set settings properties as fields objects 2024-06-14 15:53:33 +00:00
Yassine Doghri
8f8c61eaae docs(plugins): add experimental plugins section + plugins:create command to create plugin via CLI 2024-06-14 15:53:33 +00:00
Yassine Doghri
91dc8c8325 test(plugins): add test suite for Plugins service 2024-06-14 15:53:33 +00:00
Yassine Doghri
3a900bbab6 feat(plugins): add methods to easily retrieve general, podcast and episode settings in hooks methods 2024-06-14 15:53:33 +00:00
Yassine Doghri
2035c39fd1 feat(plugins): register plugins using Plugin.php file instead of namespace + simplify i18n structure 2024-06-14 15:53:33 +00:00
Yassine Doghri
d7b9730d7e docs(ci): build and deploy docs for next branch 2024-06-14 15:53:33 +00:00
Yassine Doghri
b5bd2db28f build(php): upgrade min php version to 8.3 2024-06-14 15:53:33 +00:00
Yassine Doghri
e2a90def88 test(plugins): add test cases for loading manifest data 2024-06-14 15:53:33 +00:00
Yassine Doghri
014facd5a1 refactor(plugins): rename manifest schema 2024-06-14 15:53:33 +00:00
Yassine Doghri
80d2c48ee2 feat(plugins): add before channel/item hooks to allow podcast/episode data edit when generating rss 2024-06-14 15:53:33 +00:00
Yassine Doghri
8ec79097bb feat(plugins): display errors when plugin is invalid instead of crashing 2024-06-14 15:53:33 +00:00
Yassine Doghri
45ac2a4be9 feat(plugins): handle empty states and long strings in UI 2024-06-14 15:53:33 +00:00
Yassine Doghri
b62b483ad9 feat(plugins): abstract settings form for general, podcast and episode types
update filter permission logic for replacing router param
2024-06-14 15:53:33 +00:00
Yassine Doghri
6f833fc76a feat(plugins): add new field types + validate & cast user data before storing settings
+ refactor form fields components
2024-06-14 15:53:33 +00:00
Yassine Doghri
82714e7155 style(buttons): add tint to variants 2024-06-14 15:53:33 +00:00
Yassine Doghri
dfb7888aeb feat(plugins): add aside with plugin metadata next to plugin's readme
- enhance plugin card ui
- refactor components to be more consistent
- invert toggler label for better UX
- edit view components regex
2024-06-14 15:53:33 +00:00
Yassine Doghri
e6bfdfc390 feat(plugins): load README.md file to view plugin's instructions in UI 2024-06-14 15:53:32 +00:00
Yassine Doghri
1510e36c0a feat(plugins): load and validate plugin manifest.json 2024-06-14 15:53:32 +00:00
Yassine Doghri
b5eddf351f feat(plugins): add json schema definition for plugin manifest 2024-06-14 15:53:32 +00:00
Yassine Doghri
896f00661f refactor(plugins): redefine plugins folder structure to vendor/package 2024-06-14 15:53:32 +00:00
Yassine Doghri
9a80de4068 feat(plugins): uninstall plugins via CLI and admin UI 2024-06-14 15:53:32 +00:00
Yassine Doghri
89ac92fb41 feat(plugins): add settings page for podcast and episode if defined in the plugin's manifest
- rename options to settings
2024-06-14 15:53:32 +00:00
Yassine Doghri
3d8aedf9c3 feat(plugins): add options to manifest for building forms and storing plugin settings 2024-06-14 15:53:32 +00:00
Yassine Doghri
e80a33bf2a feat(plugins): add siteHead hook to add custom meta tags to public pages 2024-06-14 15:53:32 +00:00
Yassine Doghri
27d2a1b0ff feat(plugins): activate / deactivate plugin using settings table
+ load plugin icon
+ add pagination
+ autoload plugins in Config/Autoload.php to handle plugin
i18n
+ style plugin cards
2024-06-14 15:53:32 +00:00
Yassine Doghri
587938d2bf feat(plugins): load plugins using file locator service 2024-06-14 15:53:32 +00:00
Yassine Doghri
7253e13ac2 feat: add Plugins module with base files for plugins architecture 2024-06-14 15:53:32 +00:00
Yassine Doghri
3fd5efc795 fix: include app/Resources/icons folder to bundle 2024-06-14 15:49:25 +00:00
Yassine Doghri
56612f0c76 fix: add missing php-icons config file to bundle 2024-06-14 08:57:36 +00:00
Yassine Doghri
eb7ad2f7e1 fix(import): rewrite download_file helper to output curl response directly to file
This prevents memory exhaustion when downloading large files
2024-06-05 18:46:34 +00:00
Yassine Doghri
281eefc6a3 build(docs): add type declarations for virtual:starlight 2024-05-30 09:42:03 +00:00
Yassine Doghri
083a766e4e docs: add DocsVersion component to navigate through different docs versions 2024-05-29 17:28:24 +00:00
Yassine Doghri
fe676590f2 fix(docs): add base to og image using env variable 2024-05-28 09:51:28 +00:00
Yassine Doghri
2ca9418138 ci(docs): fix i18n-filter and build outDir path 2024-05-24 10:40:53 +00:00
Yassine Doghri
b345c7ecd2 ci(docs): fix typo on outDir path when building docs 2024-05-24 09:27:24 +00:00
crowdin
6dc98b329b chore(i18n): update Crowdin configuration file 2024-05-22 16:52:13 +00:00
Yassine Doghri
d88b041d2c docs: change vitepress with astro's starlight
- change language keys to kebab-case
- add new languages to docs: ca, de, es, sr-latn, zh-hans
2024-05-21 16:07:56 +00:00
crowdin
70f56a73ff chore(i18n): new Crowdin updates 2024-05-21 11:16:15 +00:00
Yassine Doghri
bb628f355f refactor: add modules folder to phpstan paths + fix errors 2024-04-28 16:41:24 +00:00
Yassine Doghri
7a6d9df6db feat: set owner email to hidden by default in podcast create form 2024-04-28 10:19:35 +00:00
Yassine Doghri
fc4f982556 fix: set owner email visibility when editing podcast
fixes #473
2024-04-28 10:16:23 +00:00
Yassine Doghri
51b064d67a refactor(icons): use php-icons library to load and display icons 2024-04-26 17:57:25 +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
Yassine Doghri
a5aef2a63e fix(rss): generate podcast guid if empty
closes #450
2024-04-06 11:50:12 +00:00
Yassine Doghri
13db54ccce build(devcontainer): move dev docker files to .devcontainer and set dev environment in app service
- add mailpit service to debug email
- remove s3 service
2024-03-28 12:04:12 +00:00
semantic-release-bot
9d7d11cefa chore(release): 1.10.5 [skip ci]
## [1.10.5](https://code.castopod.org/adaures/castopod/compare/v1.10.4...v1.10.5) (3/12/2024)

### Bug Fixes

* **file-uploads:** validate chapters json content + remove permit_empty rule to uploaded files ([6289c42](6289c42b11)), closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
2024-03-12 11:28:38 +00:00
Yassine Doghri
bec4f93837 docs(docker): add redis password to docker-compose example
closes #408
2024-03-12 11:11:21 +00:00
Yassine Doghri
523b2c610e chore: add bluesky as social media platform 2024-03-12 09:32:22 +00:00
crowdin
bd205d56ca chore(i18n): new Crowdin updates 2024-03-12 09:32:22 +00:00
Yassine Doghri
c24850bda9 build(i18n): include Breton and Serbian (Latin) languages to Castopod bundle 2024-03-12 09:32:22 +00:00
crowdin
656627050a chore(i18n): new Crowdin updates 2024-03-12 09:32:22 +00:00
Yassine Doghri
cdeb8bf26e build(devcontainer): add migration and DevSeed command to run post devcontainer creation
update dev setup docs + build and deploy docs everytime
2024-03-12 09:32:22 +00:00
Yassine Doghri
6289c42b11 fix(file-uploads): validate chapters json content + remove permit_empty rule to uploaded files
refs #445
2024-03-12 09:32:22 +00:00
semantic-release-bot
37f2d2d21a chore(release): 1.10.4 [skip ci]
## [1.10.4](https://code.castopod.org/adaures/castopod/compare/v1.10.3...v1.10.4) (2/26/2024)

### Bug Fixes

* display chapters in episode preview page ([797516a](797516a2ec)), closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
2024-02-26 12:11:00 +00:00
crowdin
83b6571a81 chore(i18n): new Crowdin updates 2024-02-26 11:09:03 +00:00
Guy Martin
797516a2ec fix: display chapters in episode preview page
fixes #445
2024-02-26 10:24:49 +00:00
crowdin
1e208c55ca chore(i18n): new Crowdin updates 2024-02-22 10:11:10 +00:00
semantic-release-bot
efa5acd415 chore(release): 1.10.3 [skip ci]
## [1.10.3](https://code.castopod.org/adaures/castopod/compare/v1.10.2...v1.10.3) (2/21/2024)

### Bug Fixes

* **chapters:** use episode cover when chapter img is an empty string ([a343de4](a343de4cf6)), closes [#444](https://code.castopod.org/adaures/castopod/issues/444)
* **import:** set episodes as premium if podcast is set as premium by default ([dfd66be](dfd66beebf))
2024-02-21 15:16:02 +00:00
Yassine Doghri
3187b0144f ci: set specific mariadb version for mariadb service in tests 2024-02-21 14:43:53 +00:00
Yassine Doghri
a343de4cf6 fix(chapters): use episode cover when chapter img is an empty string
fixes #444
2024-02-21 14:14:29 +00:00
Yassine Doghri
dfd66beebf fix(import): set episodes as premium if podcast is set as premium by default 2024-02-21 12:57:16 +00:00
semantic-release-bot
6c3dee2131 chore(release): 1.10.2 [skip ci]
## [1.10.2](https://code.castopod.org/adaures/castopod/compare/v1.10.1...v1.10.2) (2/20/2024)

### Bug Fixes

* **podcast-import:** move closing parenthasis when checking for owner name and email existence ([cec7815](cec78155f9))
2024-02-20 15:35:03 +00:00
Yassine Doghri
cec78155f9 fix(podcast-import): move closing parenthasis when checking for owner name and email existence
This fixes a bug introduced in 1.10.0, having imports blocked and showing "1" as error.
2024-02-20 15:25:01 +00:00
semantic-release-bot
867dfad9ae chore(release): 1.10.1 [skip ci]
## [1.10.1](https://code.castopod.org/adaures/castopod/compare/v1.10.0...v1.10.1) (2/20/2024)

### Bug Fixes

* **fediverse:** use config name to get Fediverse config properties instead of hardcoded class string ([5fd0980](5fd0980ff7))
2024-02-20 10:36:40 +00:00
Yassine Doghri
5fd0980ff7 fix(fediverse): use config name to get Fediverse config properties instead of hardcoded class string 2024-02-20 10:01:16 +00:00
Yassine Doghri
4af40b5a71 ci: bump alpine version in docker ci image 2024-02-20 09:43:32 +00:00
semantic-release-bot
80c114287f chore(release): 1.10.0 [skip ci]
# [1.10.0](https://code.castopod.org/adaures/castopod/compare/v1.9.0...v1.10.0) (2/19/2024)

### Bug Fixes

* **op3:** move op3 prefix to enclosure url instead of audio proxy ([d580369](d580369235))
* **podcast-import:** rollback transaction before exception is thrown ([419bb04](419bb04716)), closes [#429](https://code.castopod.org/adaures/castopod/issues/429) [#319](https://code.castopod.org/adaures/castopod/issues/319) [#443](https://code.castopod.org/adaures/castopod/issues/443) [#438](https://code.castopod.org/adaures/castopod/issues/438)

### Features

* add podcast:season and podcast:episode tags to rss feed ([98c6658](98c6658840))
* add support for podcasting 2.0 "medium" tag with podcast, music and audiobook ([630e788](630e788f0e)), closes [#439](https://code.castopod.org/adaures/castopod/issues/439)
* display chapters in episode's public page ([87cc437](87cc437e1e)), closes [#423](https://code.castopod.org/adaures/castopod/issues/423)
* support VTT transcript file format in addition to SRT ([7071b4b](7071b4b6f4)), closes [#433](https://code.castopod.org/adaures/castopod/issues/433)
2024-02-19 12:35:11 +00:00
Yassine Doghri
419bb04716 fix(podcast-import): rollback transaction before exception is thrown
This allows errors' messages to resurface and prevent the script of having the generic "Process was
killed." error.

fixes #429, closes #319, #443, #438
2024-02-19 11:08:00 +00:00
Yassine Doghri
d0a94dd2cb chore: update php and js dependencies to latest 2024-02-17 13:01:39 +00:00
Guy Martin
87cc437e1e feat: display chapters in episode's public page
closes #423
2024-02-17 12:02:38 +00:00
Guy Martin
98c6658840 feat: add podcast:season and podcast:episode tags to rss feed 2024-02-15 11:36:09 +00:00
Yassine Doghri
d580369235 fix(op3): move op3 prefix to enclosure url instead of audio proxy 2024-02-12 16:55:09 +00:00
Yassine Doghri
94ceba6081 chore(media): remove media Routes file from Routing config 2024-02-12 13:23:30 +00:00
Guy Martin
7071b4b6f4 feat: support VTT transcript file format in addition to SRT
closes #433
2024-02-09 16:34:50 +00:00
crowdin
d02ac93867 chore(i18n): new Crowdin updates 2024-02-05 17:03:36 +00:00
Guy Martin
630e788f0e feat: add support for podcasting 2.0 "medium" tag with podcast, music and audiobook
closes #439
2024-02-05 16:51:04 +00:00
semantic-release-bot
bc4f93d2b7 chore(release): 1.9.0 [skip ci]
# [1.9.0](https://code.castopod.org/adaures/castopod/compare/v1.8.2...v1.9.0) (1/31/2024)

### Bug Fixes

* **i18n:** escape language strings in form fields to prevent them from disappearing ([3cb5ffd](3cb5ffd25b)), closes [#412](https://code.castopod.org/adaures/castopod/issues/412)
* **podcast-about:** update stats query to discard scheduled episodes from episodes number ([67c037c](67c037c9eb))
* **premium-subs:** clear subscription list cache after insert ([2accb0f](2accb0f765)), closes [#430](https://code.castopod.org/adaures/castopod/issues/430)
* **s3:** remove proxy, set objects acl to public-read, and serve files using their public urls ([6a77a9d](6a77a9d2f2))

### Features

* add actor domain to handle in follow page ([de099ac](de099ac643))
* **admin:** add podcast's OP3 analytics dashboard link ([5f3752b](5f3752b443))
2024-01-31 10:00:05 +00:00
Yassine Doghri
6a77a9d2f2 fix(s3): remove proxy, set objects acl to public-read, and serve files using their public urls 2024-01-30 15:26:22 +00:00
Guy Martin
de099ac643 feat: add actor domain to handle in follow page 2024-01-30 15:18:02 +00:00
Yassine Doghri
76e1251ece docs(all-contributors): add Guy Martin to list of contributors 2024-01-25 13:23:24 +00:00
Yassine Doghri
67c037c9eb fix(podcast-about): update stats query to discard scheduled episodes from episodes number 2024-01-25 11:58:39 +00:00
Yassine Doghri
2accb0f765 fix(premium-subs): clear subscription list cache after insert
fixes #430
2024-01-24 17:33:58 +00:00
Yassine Doghri
3cb5ffd25b fix(i18n): escape language strings in form fields to prevent them from disappearing
fixes #412
2024-01-24 16:48:23 +00:00
Guy Martin
5f3752b443 feat(admin): add podcast's OP3 analytics dashboard link 2024-01-23 13:19:53 +00:00
semantic-release-bot
a12327da8e chore(release): 1.8.2 [skip ci]
## [1.8.2](https://code.castopod.org/adaures/castopod/compare/v1.8.1...v1.8.2) (1/17/2024)

### Bug Fixes

* **transcript:** add condition when concatenating sub text to prevent second line duplication ([6cbfec0](6cbfec0d7d))
2024-01-17 10:16:14 +00:00
crowdin
f303171fc5 chore(i18n): new Crowdin updates 2024-01-17 10:05:59 +00:00
Yassine Doghri
95d0861659 chore(video-clips): reduce the number of videoClipWorkers to 1 by default 2024-01-17 09:33:49 +00:00
Yassine Doghri
6cbfec0d7d fix(transcript): add condition when concatenating sub text to prevent second line duplication 2024-01-17 09:24:22 +00:00
semantic-release-bot
28a31ca03b chore(release): 1.8.1 [skip ci]
## [1.8.1](https://code.castopod.org/adaures/castopod/compare/v1.8.0...v1.8.1) (1/16/2024)

### Bug Fixes

* **models:** set updatedField as empty string when not used ([164f4d3](164f4d3be7))
2024-01-16 10:07:25 +00:00
Yassine Doghri
164f4d3be7 fix(models): set updatedField as empty string when not used 2024-01-16 09:26:14 +00:00
semantic-release-bot
9899870e28 chore(release): 1.8.0 [skip ci]
# [1.8.0](https://code.castopod.org/adaures/castopod/compare/v1.7.4...v1.8.0) (1/15/2024)

### Bug Fixes

* **episode-form:** add required validation rules for title and slug ([30a3473](30a3473863)), closes [#420](https://code.castopod.org/adaures/castopod/issues/420)
* **import:** check for empty string when generating podcast guid for feeds not including one ([ac5336f](ac5336fbc5))
* **install:** add created superadmin to most powerful group in instance, ie. superadmin ([2ed511f](2ed511f8a0))
* **persons:** delete person avatar when deleting a person ([c1ec98c](c1ec98c956)), closes [#419](https://code.castopod.org/adaures/castopod/issues/419)
* **platforms:** add matrix.org as a social platform ([9178c3f](9178c3f3af)), closes [#421](https://code.castopod.org/adaures/castopod/issues/421)

### Features

* **admin:** add tooltip for not authorized routes ([f7f9baf](f7f9bafc3e))
* **admin:** emphasize unprivileged items in sidebar with "prohibited" icon ([0bd7dde](0bd7ddea58))
* allow hiding owner's email in public RSS feed ([222e02a](222e02a2af))
* **persons:** order persons by full_name ASC for easier list scanning ([68a599f](68a599fee0)), closes [#418](https://code.castopod.org/adaures/castopod/issues/418)
2024-01-15 16:31:11 +00:00
crowdin
2c3cb85a35 chore(i18n): new Crowdin updates 2024-01-15 14:59:13 +00:00
Yassine Doghri
2ed511f8a0 fix(install): add created superadmin to most powerful group in instance, ie. superadmin 2024-01-15 14:34:11 +00:00
Yassine Doghri
19799f496d chore(all-contributors): add code contribution to ewen 2024-01-15 14:34:11 +00:00
Yassine Doghri
f7f9bafc3e feat(admin): add tooltip for not authorized routes 2024-01-15 14:34:11 +00:00
Ewen Korr
0bd7ddea58 feat(admin): emphasize unprivileged items in sidebar with "prohibited" icon 2024-01-15 14:34:11 +00:00
Yassine Doghri
68a599fee0 feat(persons): order persons by full_name ASC for easier list scanning
closes #418
2024-01-15 14:34:11 +00:00
Yassine Doghri
6f8217e1a6 chore: update CI4 + shield + other php and js packages 2024-01-15 14:34:11 +00:00
Ewen Korr
222e02a2af feat: allow hiding owner's email in public RSS feed 2024-01-15 14:34:11 +00:00
Yassine Doghri
9178c3f3af fix(platforms): add matrix.org as a social platform
closes #421
2024-01-15 14:34:11 +00:00
Yassine Doghri
c1ec98c956 fix(persons): delete person avatar when deleting a person
fixes #419
2024-01-15 14:34:11 +00:00
Yassine Doghri
30a3473863 fix(episode-form): add required validation rules for title and slug
fixes #420
2024-01-15 14:34:11 +00:00
Yassine Doghri
ac5336fbc5 fix(import): check for empty string when generating podcast guid for feeds not including one 2024-01-15 14:34:11 +00:00
semantic-release-bot
1001ec6b76 chore(release): 1.7.4 [skip ci]
## [1.7.4](https://code.castopod.org/adaures/castopod/compare/v1.7.3...v1.7.4) (1/3/2024)

### Bug Fixes

* **media:** add missing HEAD route for static assets served with S3 ([b61a32c](b61a32c8a9))
2024-01-03 15:07:01 +00:00
Yassine Doghri
b61a32c8a9 fix(media): add missing HEAD route for static assets served with S3 2024-01-03 14:57:44 +00:00
semantic-release-bot
cc85637e18 chore(release): 1.7.3 [skip ci]
## [1.7.3](https://code.castopod.org/adaures/castopod/compare/v1.7.2...v1.7.3) (12/21/2023)

### Bug Fixes

* **analytics:** upgrade opawg's user-agents-php to user-agents-v2-php ([8cd7886](8cd7886676))
* **platforms:** add Threads and YouTube Music ([9264a2d](9264a2d74c))
2023-12-21 16:34:24 +00:00
crowdin
af6fe1e4ef chore(i18n): new Crowdin updates
+ sync composer.lock file using composer update
2023-12-21 16:12:09 +00:00
Yassine Doghri
8cd7886676 fix(analytics): upgrade opawg's user-agents-php to user-agents-v2-php
update php and js dependencies to latest
2023-12-21 15:48:54 +00:00
Yassine Doghri
9264a2d74c fix(platforms): add Threads and YouTube Music 2023-12-21 15:48:54 +00:00
semantic-release-bot
98ed36d7a4 chore(release): 1.7.2 [skip ci]
## [1.7.2](https://code.castopod.org/adaures/castopod/compare/v1.7.1...v1.7.2) (12/12/2023)

### Bug Fixes

* **episode-form:** render episode number optional when episode type is trailer or bonus ([694328f](694328f108))
2023-12-12 16:12:45 +00:00
Yassine Doghri
694328f108 fix(episode-form): render episode number optional when episode type is trailer or bonus 2023-12-12 15:45:38 +00:00
Yassine Doghri
f5189055ff build(dev): increase phpmyadmin's upload limit in docker-compose.yml 2023-12-05 13:08:08 +00:00
semantic-release-bot
aeaee8ae64 chore(release): 1.7.1 [skip ci]
## [1.7.1](https://code.castopod.org/adaures/castopod/compare/v1.7.0...v1.7.1) (12/1/2023)

### Bug Fixes

* **housekeeping:** add where clause to check episode_id is not null on reset comments count ([119742c](119742cdbb))
2023-12-01 09:46:02 +00:00
Yassine Doghri
119742cdbb fix(housekeeping): add where clause to check episode_id is not null on reset comments count 2023-11-30 15:46:00 +00:00
semantic-release-bot
de8b84c874 chore(release): 1.7.0 [skip ci]
# [1.7.0](https://code.castopod.org/adaures/castopod/compare/v1.6.5...v1.7.0) (11/29/2023)

### Bug Fixes

* **admin-ux:** hide navigation submenus in details panel for easier scanning ([b047a3c](b047a3c670))
* **admin:** remove episode title truncation + display description in two lines in episode list ([f4ffa30](f4ffa30ec4)), closes [#386](https://code.castopod.org/adaures/castopod/issues/386)
* **auth:** display error messages from validator ([5a834c0](5a834c0f89))
* **housekeeping:** remove unnecessary $tablePrefix variable when resetting post count ([97d793f](97d793f55e)), closes [#383](https://code.castopod.org/adaures/castopod/issues/383)
* **import:** handle bad values for location attributes ([642981f](642981fd35))
* **import:** use cocur/slugify library to handle non latin text ([4ca7f9c](4ca7f9ccae))
* move monetization outside of podcast form + add broadcast section to podcast menu ([dff8516](dff85168b3))
* **nodeinfo2:** import database config + use dynamic table prefix for active local actors query ([6a7ef01](6a7ef0109a))
* **persons:** set roles field as optional + set `Cast > Host` as default value ([02132dc](02132dc466)), closes [#347](https://code.castopod.org/adaures/castopod/issues/347)
* **platforms:** make platforms' websites and submit urls more prominent ([61cf8fa](61cf8fa3e2))
* **podcast-form:** move fediverse section below author section ([1861d67](1861d67971))
* reorder podcast form fields + extract sync feeds to its own form ([2d52fa1](2d52fa1046))

### Features

* **admin:** add rss feed link to podcast side navigation ([18e2633](18e2633a49))
* **icons:** update new Deezer logo ([f2d5b27](f2d5b272ac))
* **install:** init database and create superadmin using CLI ([02d4ba6](02d4ba69ac)), closes [#380](https://code.castopod.org/adaures/castopod/issues/380)
* **ux:** add episode description to episode cards ([5f8d413](5f8d413b84))
2023-11-29 19:23:23 +00:00
crowdin
34a2ebfd65 chore(i18n): new Crowdin updates 2023-11-29 17:27:10 +00:00
Yassine Doghri
2f1a5eb294 build: update shield to beta.8 + php and js dependencies to latest 2023-11-29 16:33:18 +00:00
Yassine Doghri
1861d67971 fix(podcast-form): move fediverse section below author section 2023-11-29 16:00:28 +00:00
Yassine Doghri
18e2633a49 feat(admin): add rss feed link to podcast side navigation 2023-11-21 17:15:04 +00:00
Yassine Doghri
61cf8fa3e2 fix(platforms): make platforms' websites and submit urls more prominent
+ show default podcast website (castopod) link first in links page
2023-11-17 17:29:05 +00:00
Yassine Doghri
dff85168b3 fix: move monetization outside of podcast form + add broadcast section to podcast menu 2023-11-17 17:29:05 +00:00
Yassine Doghri
02d4ba69ac feat(install): init database and create superadmin using CLI
closes #380
2023-11-17 17:29:05 +00:00
Yassine Doghri
97d793f55e fix(housekeeping): remove unnecessary $tablePrefix variable when resetting post count
fixes #383
2023-11-17 17:29:05 +00:00
Yassine Doghri
3d5fc14d5e build: upgrade CI4 + php and js dependencies to latest
closes #396
2023-11-17 17:29:05 +00:00
Yassine Doghri
f4ffa30ec4 fix(admin): remove episode title truncation + display description in two lines in episode list
fixes #386
2023-11-17 17:29:05 +00:00
Yassine Doghri
02132dc466 fix(persons): set roles field as optional + set Cast > Host as default value
fixes #347
2023-11-17 17:29:05 +00:00
Yassine Doghri
642981fd35 fix(import): handle bad values for location attributes 2023-11-17 17:29:05 +00:00
Yassine Doghri
6a7ef0109a fix(nodeinfo2): import database config + use dynamic table prefix for active local actors query 2023-11-17 17:29:05 +00:00
Yassine Doghri
2d52fa1046 fix: reorder podcast form fields + extract sync feeds to its own form
- update fields' styling
- update icons contents
2023-11-17 17:29:05 +00:00
Yassine Doghri
b047a3c670 fix(admin-ux): hide navigation submenus in details panel for easier scanning 2023-11-17 17:29:05 +00:00
Yassine Doghri
5f8d413b84 feat(ux): add episode description to episode cards 2023-11-17 17:29:05 +00:00
Benjamin Bellamy
f2d5b272ac feat(icons): update new Deezer logo 2023-11-12 20:06:53 +01:00
Yassine Doghri
4ca7f9ccae fix(import): use cocur/slugify library to handle non latin text 2023-11-08 13:51:34 +00:00
Romain de Laage
04b2d8bafa build(docker): update nginx unit image to 1.31.0 2023-10-23 11:04:51 +00:00
Yassine Doghri
5a834c0f89 fix(auth): display error messages from validator 2023-10-05 11:36:36 +00:00
semantic-release-bot
fcad25a551 chore(release): 1.6.5 [skip ci]
## [1.6.5](https://code.castopod.org/adaures/castopod/compare/v1.6.4...v1.6.5) (2023-09-26)

### Bug Fixes

* **fediverse:** use NoteObject including episode link in content (hotfix) ([ffa530e](ffa530e187))
2023-09-26 15:33:41 +00:00
Yassine Doghri
ffa530e187 fix(fediverse): use NoteObject including episode link in content (hotfix) 2023-09-26 15:20:25 +00:00
Yassine Doghri
2dd9cc9ef5 chore(phpstan): remove redundant dynamicConstantNames
+ update quality tools
2023-09-26 14:56:04 +00:00
semantic-release-bot
cc19c24668 chore(release): 1.6.4 [skip ci]
## [1.6.4](https://code.castopod.org/adaures/castopod/compare/v1.6.3...v1.6.4) (2023-09-17)

### Bug Fixes

* **fediverse:** do not cache remote action form + fix typo on post routes for passing post uuid ([4ecb42f](4ecb42f7c8))
* **fediverse:** update post controller namespace in routes ([3189f12](3189f12206))
2023-09-17 12:35:30 +00:00
Yassine Doghri
4ecb42f7c8 fix(fediverse): do not cache remote action form + fix typo on post routes for passing post uuid
+ remove unnecessary session->start() directive
2023-09-17 10:07:59 +00:00
Yassine Doghri
3189f12206 fix(fediverse): update post controller namespace in routes 2023-09-15 16:40:07 +00:00
semantic-release-bot
18fcb5ba3e chore(release): 1.6.3 [skip ci]
## [1.6.3](https://code.castopod.org/adaures/castopod/compare/v1.6.2...v1.6.3) (2023-09-14)

### Bug Fixes

* **fediverse:** add `index` to post controller-method to access post's jsonld contents ([35142d8](35142d8e56))
2023-09-14 13:59:36 +00:00
Yassine Doghri
3c4df01d18 docs(.env.example): add missing analytics.salt env variable 2023-09-14 13:46:37 +00:00
Yassine Doghri
35142d8e56 fix(fediverse): add index to post controller-method to access post's jsonld contents 2023-09-14 13:23:19 +00:00
semantic-release-bot
27a04bd0df chore(release): 1.6.2 [skip ci]
## [1.6.2](https://code.castopod.org/adaures/castopod/compare/v1.6.1...v1.6.2) (2023-09-11)

### Bug Fixes

* **migrations:** remove if exists modifier for drop index ([82013c9](82013c9cde)), closes [#382](https://code.castopod.org/adaures/castopod/issues/382)
2023-09-11 15:52:31 +00:00
Yassine Doghri
82013c9cde fix(migrations): remove if exists modifier for drop index
fixes #382
2023-09-11 15:43:14 +00:00
semantic-release-bot
daa11eb9c1 chore(release): 1.6.1 [skip ci]
## [1.6.1](https://code.castopod.org/adaures/castopod/compare/v1.6.0...v1.6.1) (2023-09-09)

### Bug Fixes

* **admin:** redirect root fediverse route to fediverse-blocked-actors ([ba5324e](ba5324ea19))
* **analytics:** show full referrer domain in web pages visits reports ([6be38e9](6be38e9fda)), closes [#367](https://code.castopod.org/adaures/castopod/issues/367)
* **auth:** overwrite Shield's PermissionFilter ([c6e8000](c6e8000bab))
* **auth:** update shield from v1.0.0-beta.3 to v1.0.0-beta.6 ([23842df](23842df03a))
* **platforms:** add missing tiktok to social platforms seed ([8dfdaf3](8dfdaf3215))
* remove fediverse prefix to prevent migration error + load routes during podcast import ([7ff1dbe](7ff1dbe903))
* **routes:** overwrite RouteCollection to include all routes + update js and php dependencies ([b4f1b91](b4f1b916bf))
* update Router to include latest CI changes with alternate-content logic ([ae57601](ae57601c83))
* use podcast-activity named route instead of not existing actor route ([3c35718](3c357183ca))
2023-09-09 12:30:01 +00:00
crowdin
d1b35312a4 chore(i18n): new Crowdin updates 2023-09-09 11:48:16 +00:00
Yassine Doghri
1c96a6f5da build: upgrade CI4 to 4.4.1 + update php and js dependencies to latest 2023-09-09 10:52:01 +00:00
Yassine Doghri
b63e953ca8 chore: update codeigniter-uuid to v1.0.2 to fix phpstan error
+ update js packages to latest
2023-09-09 10:23:34 +00:00
Yassine Doghri
ba5324ea19 fix(admin): redirect root fediverse route to fediverse-blocked-actors 2023-09-09 10:23:34 +00:00
Yassine Doghri
d100fe0999 refactor: fix styling and logic issues 2023-09-09 10:23:34 +00:00
Yassine Doghri
2c07070b2c refactor: use Validation::getValidated() when using $this->validate() in controllers 2023-09-09 10:23:34 +00:00
Yassine Doghri
ff0e681763 docs: update php version requirement in install page 2023-09-09 10:23:34 +00:00
Yassine Doghri
3c357183ca fix: use podcast-activity named route instead of not existing actor route 2023-09-09 10:23:34 +00:00
Yassine Doghri
77c2d08b6e build: add phpstan-codeigniter extension to manage config(), model() and service() functions 2023-09-09 10:23:34 +00:00
Yassine Doghri
ae57601c83 fix: update Router to include latest CI changes with alternate-content logic 2023-09-09 10:23:34 +00:00
Yassine Doghri
7ff1dbe903 fix: remove fediverse prefix to prevent migration error + load routes during podcast import
refactor migration queries to use forge functions
2023-09-09 10:23:34 +00:00
Yassine Doghri
072b3ff61d chore: update CI4 to v4.3.8 + update js and php dependencies 2023-09-09 10:23:34 +00:00
Yassine Doghri
b4f1b916bf fix(routes): overwrite RouteCollection to include all routes + update js and php dependencies 2023-09-09 10:23:34 +00:00
Yassine Doghri
ef04ce5c41 chore(composer.json): add dev script to serve castopod using spark 2023-09-09 10:23:34 +00:00
Yassine Doghri
981277ae14 build(ci4): update CodeIgniter to v4.3.6 2023-09-09 10:23:34 +00:00
Yassine Doghri
4ccb363a3d refactor(modules): add Registrars to declare filter aliases 2023-09-09 10:23:34 +00:00
Yassine Doghri
c6e8000bab fix(auth): overwrite Shield's PermissionFilter 2023-09-09 10:23:34 +00:00
Yassine Doghri
d68595932a build(ci4): update CodeIgniter to v4.2.12 2023-09-09 10:23:34 +00:00
Yassine Doghri
23842df03a fix(auth): update shield from v1.0.0-beta.3 to v1.0.0-beta.6
v1.0.0-beta.4 fixes a security issue "Password Shucking Vulnerability"
(https://github.com/codeigniter4/shield/security/advisories/GHSA-c5vj-f36q-p9vg)
2023-09-09 10:23:34 +00:00
Yassine Doghri
8dfdaf3215 fix(platforms): add missing tiktok to social platforms seed 2023-09-09 10:22:08 +00:00
Yassine Doghri
f1fe1b4764 docs(readme): remove beta note + update getting started section 2023-09-05 14:16:17 +00:00
Aonrud
6be38e9fda fix(analytics): show full referrer domain in web pages visits reports
fixes #367
2023-08-31 08:53:46 +00:00
semantic-release-bot
1eb680d617 chore(release): 1.6.0 [skip ci]
# [1.6.0](https://code.castopod.org/adaures/castopod/compare/v1.5.2...v1.6.0) (2023-08-28)

### Bug Fixes

* **home:** update where clause when getting all podcasts to prevent draft podcasts from showing up ([7a1eea5](7a1eea58d3))
* **media:** copy and delete temp file when saving instead of moving it for FS FileManager ([9346e78](9346e787bd)), closes [#338](https://code.castopod.org/adaures/castopod/issues/338)
* **media:** get path using media_path_absolute when saving media file ([754e7a6](754e7a6b4b))
* **media:** init file properties in setAttributes' Model method + set defaults to pathinfo data ([0775add](0775add678))
* **premium-podcasts:** show premium flag only when podcast has published premium episodes ([d10c4fd](d10c4fd753))
* **s3:** add a flag to serve media files by redirecting to a presigned url instead of default proxy ([11aa358](11aa3586a0))

### Features

* **episode:** add preview link in admin to view and share episode before publication ([7d21b35](7d21b3509e))
2023-08-28 14:28:59 +00:00
crowdin
b719be10c0 chore(i18n): new Crowdin updates 2023-08-28 14:13:45 +00:00
Yassine Doghri
d10c4fd753 fix(premium-podcasts): show premium flag only when podcast has published premium episodes 2023-08-28 14:01:33 +00:00
Yassine Doghri
7d21b3509e feat(episode): add preview link in admin to view and share episode before publication 2023-08-28 13:53:04 +00:00
Yassine Doghri
7a1eea58d3 fix(home): update where clause when getting all podcasts to prevent draft podcasts from showing up 2023-08-22 15:00:01 +00:00
Yassine Doghri
11aa3586a0 fix(s3): add a flag to serve media files by redirecting to a presigned url instead of default proxy 2023-08-21 13:34:48 +00:00
Yassine Doghri
754e7a6b4b fix(media): get path using media_path_absolute when saving media file 2023-08-06 13:49:06 +00:00
Yassine Doghri
9346e787bd fix(media): copy and delete temp file when saving instead of moving it for FS FileManager
+ throw exception instead silently failing file save

closes #338
2023-08-06 12:14:25 +00:00
Yassine Doghri
0775add678 fix(media): init file properties in setAttributes' Model method + set defaults to pathinfo data 2023-08-05 10:14:06 +00:00
Yassine Doghri
26a714d9c2 build(devcontainer): update network's subnet to 172.31.0.0/24 2023-08-02 19:05:12 +02:00
semantic-release-bot
6a9d14d24e chore(release): 1.5.2 [skip ci]
## [1.5.2](https://code.castopod.org/adaures/castopod/compare/v1.5.1...v1.5.2) (2023-07-31)

### Bug Fixes

* **credits:** remove undefined $podcast variable from page layout ([73a5b68](73a5b68087)), closes [#359](https://code.castopod.org/adaures/castopod/issues/359)
* **platforms:** change twitter to X + add buymeacoffee and kofi as funding ([d69b4e4](d69b4e4857)), closes [#353](https://code.castopod.org/adaures/castopod/issues/353) [#361](https://code.castopod.org/adaures/castopod/issues/361)
2023-07-31 11:24:44 +00:00
Yassine Doghri
d69b4e4857 fix(platforms): change twitter to X + add buymeacoffee and kofi as funding
+ fix a few typos

closes #353, #361
2023-07-31 11:06:44 +00:00
Yassine Doghri
73a5b68087 fix(credits): remove undefined $podcast variable from page layout
fixes #359
2023-07-31 11:06:44 +00:00
semantic-release-bot
ef9e897b27 chore(release): 1.5.1 [skip ci]
## [1.5.1](https://code.castopod.org/adaures/castopod/compare/v1.5.0...v1.5.1) (2023-07-29)

### Bug Fixes

* **admin-ui:** remove button labels on smaller screens in podcast view ([9cc5ffd](9cc5ffd143))
* **rss:** set srt transcripts' mimetype to application/x-subrip with rel="captions" attribute ([16a3fdb](16a3fdb56e)), closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
* **rss:** update podcast extension namespace ([6833dd0](6833dd05ab)), closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
2023-07-29 10:26:44 +00:00
Yassine Doghri
9cc5ffd143 fix(admin-ui): remove button labels on smaller screens in podcast view
- update components renderer regex to include special characters
- fix itunes_explicit mapping
during import
2023-07-29 09:57:35 +00:00
Yassine Doghri
16a3fdb56e fix(rss): set srt transcripts' mimetype to application/x-subrip with rel="captions" attribute
closes #360
2023-07-29 08:28:05 +00:00
Yassine Doghri
6833dd05ab fix(rss): update podcast extension namespace
refs #360
2023-07-29 08:27:43 +00:00
Yassine Doghri
3747fa2c2b build(docker-dev): use a network pool unlikely to get overlapped 2023-07-29 08:02:09 +00:00
semantic-release-bot
411b90b4b2 chore(release): 1.5.0 [skip ci]
# [1.5.0](https://code.castopod.org/adaures/castopod/compare/v1.4.7...v1.5.0) (2023-07-27)

### Bug Fixes

* **admin-ui:** truncate header title + remove sticky podcast banner card on mobile ([63c20da](63c20da5ff))

### Features

* add podcast links page including social, podcasting and funding links ([8ae2929](8ae292933a))
2023-07-27 13:03:19 +00:00
crowdin
dfa93ff8e3 chore(i18n): new Crowdin updates 2023-07-27 12:47:41 +00:00
Yassine Doghri
8ae292933a feat: add podcast links page including social, podcasting and funding links 2023-07-27 12:47:39 +00:00
Yassine Doghri
63c20da5ff fix(admin-ui): truncate header title + remove sticky podcast banner card on mobile 2023-07-27 12:47:33 +00:00
semantic-release-bot
8f9453b84a chore(release): 1.4.7 [skip ci]
## [1.4.7](https://code.castopod.org/adaures/castopod/compare/v1.4.6...v1.4.7) (2023-07-19)

### Bug Fixes

* **s3:** allow CORS for served static files ([9b955c9](9b955c9ce2))
2023-07-19 15:17:20 +00:00
Yassine Doghri
9b955c9ce2 fix(s3): allow CORS for served static files 2023-07-19 15:04:17 +00:00
semantic-release-bot
d184998ed5 chore(release): 1.4.6 [skip ci]
## [1.4.6](https://code.castopod.org/adaures/castopod/compare/v1.4.5...v1.4.6) (2023-07-11)

### Bug Fixes

* **fediverse:** expand object before sending accept follow request ([082cdc9](082cdc9ee7)), closes [#350](https://code.castopod.org/adaures/castopod/issues/350)
* **podcast-import:** remove error log when no import in queue, exit with success instead ([5e719f3](5e719f3e9e))
2023-07-11 12:57:10 +00:00
Yassine Doghri
5e719f3e9e fix(podcast-import): remove error log when no import in queue, exit with success instead 2023-07-11 10:28:19 +00:00
Yassine Doghri
082cdc9ee7 fix(fediverse): expand object before sending accept follow request
fixes #350
2023-07-05 14:57:05 +00:00
Yassine Doghri
7e20df6a58 build(docker): build castopod/castopod image in priority 2023-07-04 11:40:21 +00:00
semantic-release-bot
3c81ef129b chore(release): 1.4.5 [skip ci]
## [1.4.5](https://code.castopod.org/adaures/castopod/compare/v1.4.4...v1.4.5) (2023-07-04)

### Bug Fixes

* **s3:** handle range requests to serve media files ([41a5932](41a5932233))
2023-07-04 11:22:47 +00:00
Yassine Doghri
41a5932233 fix(s3): handle range requests to serve media files 2023-07-04 11:03:34 +00:00
semantic-release-bot
52383e0ecf chore(release): 1.4.4 [skip ci]
## [1.4.4](https://code.castopod.org/adaures/castopod/compare/v1.4.3...v1.4.4) (2023-07-02)

### Bug Fixes

* **audio-clipper:** init segment position on firstUpdate + improve UX by adding ghost handle ([aa68386](aa68386667)), closes [#351](https://code.castopod.org/adaures/castopod/issues/351)
* set resized images to 72dpi for compatibility with Apple Podcasts ([0b327cb](0b327cb4d9)), closes [#282](https://code.castopod.org/adaures/castopod/issues/282)
2023-07-02 10:19:12 +00:00
Yassine Doghri
0b327cb4d9 fix: set resized images to 72dpi for compatibility with Apple Podcasts
fixes #282
2023-07-02 09:42:54 +00:00
Yassine Doghri
aa68386667 fix(audio-clipper): init segment position on firstUpdate + improve UX by adding ghost handle
- clean web components and js modules
- update js dependencies to latest

fixes #351
2023-07-01 13:46:03 +00:00
semantic-release-bot
d15a068e0c chore(release): 1.4.3 [skip ci]
## [1.4.3](https://code.castopod.org/adaures/castopod/compare/v1.4.2...v1.4.3) (2023-06-29)

### Bug Fixes

* **video-clipper:** add -t option to ffmpeg command to stop generation after duration ([60814b8](60814b8d20)), closes [#341](https://code.castopod.org/adaures/castopod/issues/341)
2023-06-29 15:39:37 +00:00
crowdin
5d1edd7e4c chore(i18n): new Crowdin updates 2023-06-29 15:23:25 +00:00
Yassine Doghri
60814b8d20 fix(video-clipper): add -t option to ffmpeg command to stop generation after duration
fixes #341
2023-06-29 15:15:04 +00:00
Yassine Doghri
55c1d8904c docs: add KrzysztofDomanczyk in all-contributors 2023-06-29 08:00:21 +00:00
semantic-release-bot
3a5fdf2f54 chore(release): 1.4.2 [skip ci]
## [1.4.2](https://code.castopod.org/adaures/castopod/compare/v1.4.1...v1.4.2) (2023-06-27)

### Bug Fixes

* **fediverse:** check that actor's images mimetype is present or guess it otherwise ([06c4f15](06c4f15477)), closes [#348](https://code.castopod.org/adaures/castopod/issues/348)
* **podcast-import:** show cancel or retry action depending on task status ([e42258d](e42258de1f))
2023-06-27 10:57:23 +00:00
crowdin
c4bdde2b03 chore(i18n): new Crowdin updates 2023-06-27 10:47:52 +00:00
Yassine Doghri
2f681e9f78 build(dev): increase phpmyadmin's upload limit to facilitate testing imports 2023-06-26 16:47:25 +00:00
Yassine Doghri
e42258de1f fix(podcast-import): show cancel or retry action depending on task status 2023-06-26 12:05:17 +00:00
Yassine Doghri
06c4f15477 fix(fediverse): check that actor's images mimetype is present or guess it otherwise
fixes #348
2023-06-26 11:46:52 +00:00
Romain de Laage
233ece4b3a
build(docker): use common PHP configuration for Nginx Unit and FPM images 2023-06-26 11:24:45 +02:00
Yassine Doghri
3fee88ae6e chore: update js dependencies to latest 2023-06-23 10:01:40 +00:00
semantic-release-bot
b61dd57a37 chore(release): 1.4.1 [skip ci]
## [1.4.1](https://code.castopod.org/adaures/castopod/compare/v1.4.0...v1.4.1) (2023-06-22)

### Bug Fixes

* **podcast-import:** set default values for person group and role if not found in taxonomy ([aa46dca](aa46dca4e3))
2023-06-22 15:53:39 +00:00
Yassine Doghri
aa46dca4e3 fix(podcast-import): set default values for person group and role if not found in taxonomy
+ update podcast-feed and podcast-persons-taxonomy packages
2023-06-22 15:11:21 +00:00
Yassine Doghri
d50cbb09d1 ci: invert build stage with deploy stage because docker images take a long time to build 2023-06-21 18:30:05 +00:00
semantic-release-bot
36f7de3783 chore(release): 1.4.0 [skip ci]
# [1.4.0](https://code.castopod.org/adaures/castopod/compare/v1.3.5...v1.4.0) (2023-06-21)

### Bug Fixes

* **charts:** set duration charts label to HHhMM for listening time analytics ([3fc1d8e](3fc1d8e18d))
* **embed:** set height of player iframe from config ([4665741](4665741425))
* **s3:** serve files without cache if dummy cache handler + add http referer header to redirect ([30db9f0](30db9f0667))
* **s3:** use presigned request uri to serve static files ([cb92dc7](cb92dc73f1))
* **webmanifest:** import misc helper to get site_icon_url ([548a11d](548a11d501))

### Features

* **import:** run podcast imports' processes asynchronously using tasks ([d8e1d40](d8e1d4031d))
* **rest-api:** add endpoints for episodes and full text search for podcasts and episodes ([85505d4](85505d4b31)), closes [#296](https://code.castopod.org/adaures/castopod/issues/296)
2023-06-21 17:59:07 +00:00
crowdin
ad1ba4f8a1 chore(i18n): new Crowdin updates 2023-06-21 17:49:33 +00:00
Yassine Doghri
c62b6261ac docs(install): set required php version as 8.1 only 2023-06-21 16:35:44 +00:00
Yassine Doghri
da93217bef docs: remove castopod/video-clipper image mentions in the docs 2023-06-21 16:29:38 +00:00
Yassine Doghri
d8e1d4031d feat(import): run podcast imports' processes asynchronously using tasks
- use codeigniter4/tasks project to handle cron tasks
- use yassinedoghri/podcast-feed project to parse feeds for imports
2023-06-21 16:17:11 +00:00
Krzysztof Domańczy
85505d4b31 feat(rest-api): add endpoints for episodes and full text search for podcasts and episodes
closes #296
2023-06-21 10:07:31 +00:00
Romain de Laage
2b516fee14 build(docker): unify video clipper and php-fpm containers, switch to debian 2023-06-21 09:56:47 +00:00
Romain de Laage
bb3c8ba6d1
build(docker): include content type header for transcript files 2023-06-19 16:52:03 +02:00
Yassine Doghri
178bf998ab chore: update php and js dependencies to latest
+ migrate phpunit config file to new format
2023-06-19 10:33:11 +00:00
Yassine Doghri
30db9f0667 fix(s3): serve files without cache if dummy cache handler + add http referer header to redirect 2023-06-14 17:20:14 +00:00
Yassine Doghri
4c1a3e5015 refactor: fix some of phpstan's ignored errors 2023-06-13 16:05:02 +00:00
Yassine Doghri
0de9c1ad23 chore: update js dependencies to latest 2023-06-12 16:04:58 +00:00
Yassine Doghri
7bf31c6a8f build(docker): upgrade node to v18 for dev Dockerfile 2023-06-12 15:49:55 +00:00
Yassine Doghri
2a50f6e4d2 style: update ecs config to align associative arrays arrows
update composer dependencies to latest
2023-06-12 15:12:49 +00:00
Yassine Doghri
3fc1d8e18d fix(charts): set duration charts label to HHhMM for listening time analytics
+ fix stylelint issues
2023-06-08 14:42:32 +00:00
Yassine Doghri
d4d58b948b ci: update Dockerfile to latest alpine image 2023-06-05 16:20:19 +00:00
Yassine Doghri
1b50978559 chore: update js dependencies to latest 2023-06-05 12:08:32 +00:00
Yassine Doghri
cb92dc73f1 fix(s3): use presigned request uri to serve static files 2023-06-05 11:48:29 +00:00
Yassine Doghri
548a11d501 fix(webmanifest): import misc helper to get site_icon_url 2023-06-05 09:28:32 +00:00
Yassine Doghri
4665741425 fix(embed): set height of player iframe from config 2023-05-21 10:24:57 +00:00
semantic-release-bot
6c010fc5fd chore(release): 1.3.5 [skip ci]
## [1.3.5](https://code.castopod.org/adaures/castopod/compare/v1.3.4...v1.3.5) (2023-05-09)

### Bug Fixes

* replace essence with embera to create preview cards ([c682f03](c682f03a67))
2023-05-09 15:03:57 +00:00
crowdin
5fb43065ef chore(i18n): new Crowdin updates 2023-05-09 14:30:05 +00:00
Yassine Doghri
1ce13c6721 build(docker): build and push amd64 image first for castopod/castopod 2023-05-09 12:24:32 +00:00
Yassine Doghri
c682f03a67 fix: replace essence with embera to create preview cards 2023-05-09 11:55:16 +00:00
semantic-release-bot
fbd1a0cf0d chore(release): 1.3.4 [skip ci]
## [1.3.4](https://code.castopod.org/adaures/castopod/compare/v1.3.3...v1.3.4) (2023-05-05)

### Bug Fixes

* **import-update:** insert episodes incrementally into database ([108fdf8](108fdf84b8))
2023-05-05 17:17:28 +00:00
Yassine Doghri
71bd124596 build(docker): run arm64 build only on release branches
+ fix dockerfile path for castopod image
2023-05-05 17:09:23 +00:00
crowdin
80dfe46323 chore(i18n): new Crowdin updates 2023-05-05 14:28:51 +00:00
Romain de Laage
7c02774924 build(docker): add ability to configure timeouts, max body size and max memory limit 2023-05-05 14:18:42 +00:00
Yassine Doghri
108fdf84b8 fix(import-update): insert episodes incrementally into database 2023-05-05 12:56:57 +00:00
Yassine Doghri
b3b7f446b1 build(docker): remove arm64 build for images but unit to reduce build time 2023-05-04 12:41:01 +00:00
Romain de Laage
0999b02bba
build(docker): create context before builder 2023-04-28 17:57:28 +02:00
Romain de Laage
f966f039dd
build(docker): add TLS certificates to docker build tasks 2023-04-28 13:42:02 +02:00
Romain de Laage
c2ffc9aec3 build(docker): create builder before building images 2023-04-27 12:45:48 +00:00
Romain de Laage
44eb1646db build(docker): use buildx to build AMD and ARM images 2023-04-26 13:00:42 +00:00
Yassine Doghri
9c414ed1e7 docs: fix dead links in translated docs 2023-04-17 13:30:22 +00:00
semantic-release-bot
fb9b6ec54d chore(release): 1.3.3 [skip ci]
## [1.3.3](https://code.castopod.org/adaures/castopod/compare/v1.3.2...v1.3.3) (2023-04-17)

### Bug Fixes

* unnescape podcast title special characters in "find us on" section ([f727276](f727276f82)), closes [#323](https://code.castopod.org/adaures/castopod/issues/323)
* **websub:** add missing misc helper import ([855aacc](855aacce0b))
2023-04-17 12:29:21 +00:00
Yassine Doghri
f727276f82 fix: unnescape podcast title special characters in "find us on" section
fixes #323
2023-04-17 11:36:40 +00:00
Yassine Doghri
ebb9be985a chore(podcast-import): clean unnecessary critical log when importing episodes 2023-04-17 11:21:07 +00:00
Yassine Doghri
855aacce0b fix(websub): add missing misc helper import
+ add checks before clearing episode cache
2023-04-17 11:18:02 +00:00
semantic-release-bot
19fcb9b0f3 chore(release): 1.3.2 [skip ci]
## [1.3.2](https://code.castopod.org/adaures/castopod/compare/v1.3.1...v1.3.2) (2023-04-14)

### Bug Fixes

* remove path key when getting default avatar path ([c5a1359](c5a1359218))
* **s3:** serve files using media base url to allow for CDN setup ([502f53c](502f53c970))
2023-04-14 12:27:04 +00:00
Yassine Doghri
a00e45ea4c build: update js and php dependencies to latest 2023-04-14 11:22:12 +00:00
crowdin
23a47efefd chore(i18n): new Crowdin updates 2023-04-14 09:47:53 +00:00
Yassine Doghri
502f53c970 fix(s3): serve files using media base url to allow for CDN setup 2023-04-14 09:35:05 +00:00
Yassine Doghri
c5a1359218 fix: remove path key when getting default avatar path 2023-04-14 09:34:09 +00:00
semantic-release-bot
dfae166e4d chore(release): 1.3.1 [skip ci]
## [1.3.1](https://code.castopod.org/adaures/castopod/compare/v1.3.0...v1.3.1) (2023-04-13)

### Bug Fixes

* **s3:** add proxy to serve images from s3 to client ([a76724a](a76724a8cf)), closes [#321](https://code.castopod.org/adaures/castopod/issues/321)
2023-04-13 12:06:09 +00:00
Yassine Doghri
a76724a8cf fix(s3): add proxy to serve images from s3 to client
refs #321
2023-04-13 11:46:31 +00:00
semantic-release-bot
c5eb6ed590 chore(release): 1.3.0 [skip ci]
# [1.3.0](https://code.castopod.org/adaures/castopod/compare/v1.2.4...v1.3.0) (2023-04-03)

### Bug Fixes

* delete files using file_manager when deleting episode and podcast ([41d8efe](41d8efe6e7))

### Features

* **media:** set media storage directory as configurable ([7e1a470](7e1a470ba4))
2023-04-03 15:07:06 +00:00
crowdin
1a69bc48bb chore(i18n): new Crowdin updates 2023-04-01 12:38:40 +00:00
Yassine Doghri
41d8efe6e7 fix: delete files using file_manager when deleting episode and podcast
- add deleteAll method to file manager
- refactor deletePodcastImageSizes and
deletePersonImagesSizes implementations
2023-03-30 13:23:10 +00:00
misuzu
7e1a470ba4 feat(media): set media storage directory as configurable 2023-03-28 16:13:04 +00:00
semantic-release-bot
4503b05a8a chore(release): 1.2.4 [skip ci]
## [1.2.4](https://code.castopod.org/adaures/castopod/compare/v1.2.3...v1.2.4) (2023-03-23)

### Bug Fixes

* allow images to have .jpeg extension consistently ([ae5e12b](ae5e12be3b))
* **s3:** delete persons image sizes from bucket + add keyPrefix to config ([208c271](208c2715f9))
* **s3:** do not create bucket if not exists, check if healthy instead ([da7076f](da7076fc2d))

### Reverts

* **homepage:** remove redirect to install if database is not setup ([d4954e0](d4954e026d))
2023-03-23 12:48:05 +00:00
crowdin
90f757dc93 chore(i18n): new Crowdin updates 2023-03-23 12:13:46 +00:00
Yassine Doghri
4193946fe0 chore(health): remove returned 503 status code reason 2023-03-23 11:59:51 +00:00
Yassine Doghri
d4954e026d revert(homepage): remove redirect to install if database is not setup
After install, the redirect condition is kept even though it would never be triggered again.
2023-03-23 11:54:24 +00:00
Yassine Doghri
da7076fc2d fix(s3): do not create bucket if not exists, check if healthy instead
update php and js dependencies to latest
2023-03-23 11:46:21 +00:00
Romain de Laage
18f6b75dee
build(docker): use supervisord in unit image 2023-03-22 11:53:38 +01:00
Aonrud
ae5e12be3b fix: allow images to have .jpeg extension consistently 2023-03-21 18:06:54 +00:00
Yassine Doghri
208c2715f9 fix(s3): delete persons image sizes from bucket + add keyPrefix to config 2023-03-21 17:08:42 +00:00
semantic-release-bot
0a54b413b3 chore(release): 1.2.3 [skip ci]
## [1.2.3](https://code.castopod.org/adaures/castopod/compare/v1.2.2...v1.2.3) (2023-03-18)

### Bug Fixes

* **notifications:** set mark-all-as-read parameter to be podcast_id instead of actor_id ([2748f23](2748f23137))
2023-03-18 12:41:10 +00:00
Yassine Doghri
a72eb0ba3a docs: add the all in one castopod image in the docs 2023-03-18 12:24:35 +00:00
Yassine Doghri
2748f23137 fix(notifications): set mark-all-as-read parameter to be podcast_id instead of actor_id
This fixes a permission error when clicking on mark all as read
2023-03-18 12:18:08 +00:00
semantic-release-bot
496c89a7e9 chore(release): 1.2.2 [skip ci]
## [1.2.2](https://code.castopod.org/adaures/castopod/compare/v1.2.1...v1.2.2) (2023-03-18)

### Bug Fixes

* **migration:** change old media file_key to file_path ([a414142](a4141421aa)), closes [#314](https://code.castopod.org/adaures/castopod/issues/314)
2023-03-18 10:23:39 +00:00
Yassine Doghri
a4141421aa fix(migration): change old media file_key to file_path
fixes #314
2023-03-18 10:13:36 +00:00
semantic-release-bot
08acfd593c chore(release): 1.2.1 [skip ci]
## [1.2.1](https://code.castopod.org/adaures/castopod/compare/v1.2.0...v1.2.1) (2023-03-17)

### Bug Fixes

* change app.mediaBaseURL to media.baseURL in install, docker entrypoints and docs ([b3c6e05](b3c6e05e6f))
2023-03-17 17:46:20 +00:00
Yassine Doghri
b3c6e05e6f fix: change app.mediaBaseURL to media.baseURL in install, docker entrypoints and docs 2023-03-17 17:36:26 +00:00
semantic-release-bot
0cb2e99f03 chore(release): 1.2.0 [skip ci]
# [1.2.0](https://code.castopod.org/adaures/castopod/compare/v1.1.2...v1.2.0) (2023-03-17)

### Bug Fixes

* **analytics:** check the x_forwarded_for client header ([1111177](1111177eb7))
* **auth:** update podcast editors' permissions ([a9b6308](a9b630884b))
* **contributors:** add dash to prevent deleting permissions from other podcast ([5d2a2d4](5d2a2d49c4)), closes [#310](https://code.castopod.org/adaures/castopod/issues/310)
* display bandwidth limit on dashboard when set in .env ([a2a87ab](a2a87abf7c))
* **docker:** update nginx configuration ([8884598](8884598a56))
* **platforms:** update 'submit_url' for Antennapod ([9fc49a7](9fc49a7430))

### Features

* add downloads count to episode list ([b63c1dc](b63c1dc9b1))
* add health route to check if db, cache and file manager are ok ([1dde11f](1dde11f8e4))
* **media:** add s3 to manage media files ([d93fc98](d93fc98469))

### Reverts

* **install:** reset condition to look for instance owner before continuing install ([fc009f3](fc009f3d00))
2023-03-17 17:21:02 +00:00
Yassine Doghri
729edc9afa build(ci): add npm to docker/ci image for semantic release 2023-03-17 17:08:19 +00:00
Yassine Doghri
5d2a2d49c4 fix(contributors): add dash to prevent deleting permissions from other podcast
fixes #310
2023-03-17 16:34:44 +00:00
Yassine Doghri
1dde11f8e4 feat: add health route to check if db, cache and file manager are ok 2023-03-17 14:54:03 +00:00
Yassine Doghri
e1b66ed7ed ci: fix docs invalid tags and dead links 2023-03-17 09:59:10 +00:00
crowdin
d2151b74bd chore(i18n): new Crowdin updates 2023-03-16 16:45:41 +00:00
Yassine Doghri
dc34273826 docs: remove beta info status from index page 2023-03-16 15:59:46 +00:00
Yassine Doghri
73c2987d4c docs: add s3 config section in install page 2023-03-16 15:46:50 +00:00
Yassine Doghri
d93fc98469 feat(media): add s3 to manage media files
Users may choose between filesystem (FS) or S3 to store and manage their media files
2023-03-16 13:00:05 +00:00
Aonrud
9fc49a7430 fix(platforms): update 'submit_url' for Antennapod 2023-03-10 16:13:02 +00:00
Yassine Doghri
a9b630884b fix(auth): update podcast editors' permissions
`episodes.manage-notifications` should be `manage-notifications`
2023-03-07 14:55:49 +00:00
Yassine Doghri
b63c1dc9b1 feat: add downloads count to episode list 2023-02-28 16:53:58 +00:00
Yassine Doghri
fc009f3d00 revert(install): reset condition to look for instance owner before continuing install 2023-02-28 14:26:27 +00:00
Romain de Laage
ab275e978c build(docker): add alternative Nginx Unit image 2023-02-25 15:10:19 +00:00
Aonrud
1111177eb7 fix(analytics): check the x_forwarded_for client header 2023-02-24 15:38:14 +00:00
Yassine Doghri
b794d3433c ci: use sed instead of perl to rewrite castopod's composer version 2023-02-22 17:58:46 +00:00
Yassine Doghri
9ef58808fc ci: update bundle scripts to use pnpm exec + add openssh-client to ci docker image 2023-02-22 17:25:14 +00:00
Yassine Doghri
e0c3ddb07d ci: use pnpx in lint-commit-msg script 2023-02-22 17:00:03 +00:00
Yassine Doghri
05d27400a0 ci: add intl extension to ci docker image 2023-02-22 16:50:24 +00:00
Yassine Doghri
84a6447fd4 ci: fix lint and formatting issues 2023-02-22 16:29:45 +00:00
Yassine Doghri
34777598dd build: replace npm with pnpm + add Dockerfile for ci
update php and js dependencies to latest
2023-02-22 14:36:56 +00:00
Yassine Doghri
1b037a7adf docs: add ntp requirement to validate federation's incoming requests 2023-02-04 12:42:35 +00:00
Benjamin Bellamy
8884598a56 fix(docker): update nginx configuration 2023-01-16 16:26:56 +00:00
Yassine Doghri
a2a87abf7c fix: display bandwidth limit on dashboard when set in .env 2022-12-28 16:57:14 +00:00
semantic-release-bot
fa6bb2f492 chore(release): 1.1.2 [skip ci]
## [1.1.2](https://code.castopod.org/adaures/castopod/compare/v1.1.1...v1.1.2) (2022-12-14)

### Bug Fixes

* **analytics:** set EpisodeAudioController to init user session data ([77ccb30](77ccb30600))
2022-12-14 11:43:27 +00:00
crowdin
1cc9c11e8f chore(i18n): new Crowdin updates 2022-12-14 11:18:11 +00:00
Yassine Doghri
77ccb30600 fix(analytics): set EpisodeAudioController to init user session data 2022-12-14 10:02:36 +00:00
semantic-release-bot
998a8ee6b4 chore(release): 1.1.1 [skip ci]
## [1.1.1](https://code.castopod.org/adaures/castopod/compare/v1.1.0...v1.1.1) (2022-12-13)

### Bug Fixes

* **op3:** remove scheme when wraping audio URI ([0ad22e4](0ad22e49bc))
* **rss:** add file extension to enclosure url ([964cbba](964cbba54f))
2022-12-13 12:17:45 +00:00
Yassine Doghri
0ad22e49bc fix(op3): remove scheme when wraping audio URI 2022-12-13 11:56:49 +00:00
Yassine Doghri
964cbba54f fix(rss): add file extension to enclosure url 2022-12-13 11:34:50 +00:00
semantic-release-bot
948a3db48a chore(release): 1.1.0 [skip ci]
# [1.1.0](https://code.castopod.org/adaures/castopod/compare/v1.0.5...v1.1.0) (2022-12-09)

### Bug Fixes

* **notifications:** remove cache inconsistencies when marking notification as read ([46d7054](46d70541d3))
* **notifications:** retrieve activity from database instead of getting cache ([7fbbd08](7fbbd08da6))
* **podcast:soundbite:** rename start time attribute to follow spec ([689831c](689831c26c))

### Features

* **analytics:** add OP3 analytics service option + update episode audio url ([16527ed](16527ed529))
2022-12-09 17:19:37 +00:00
Yassine Doghri
46d70541d3 fix(notifications): remove cache inconsistencies when marking notification as read 2022-12-09 16:44:59 +00:00
crowdin
2e7b462d94 chore(i18n): new Crowdin updates 2022-12-09 15:42:46 +00:00
Yassine Doghri
16527ed529 feat(analytics): add OP3 analytics service option + update episode audio url 2022-12-09 15:04:42 +00:00
Yassine Doghri
7fbbd08da6 fix(notifications): retrieve activity from database instead of getting cache 2022-12-07 14:00:38 +00:00
Yassine Doghri
689831c26c fix(podcast:soundbite): rename start time attribute to follow spec 2022-12-02 15:32:27 +00:00
semantic-release-bot
6e4045bb0d chore(release): 1.0.5 [skip ci]
## [1.0.5](https://code.castopod.org/adaures/castopod/compare/v1.0.4...v1.0.5) (2022-11-25)

### Bug Fixes

* **router:** revert to CI4 v4.2.7 to include all routes ([c13cfa0](c13cfa0ea0))
2022-11-25 18:16:22 +00:00
crowdin
80666bc728 chore(i18n): new Crowdin updates 2022-11-25 17:44:19 +00:00
Yassine Doghri
c13cfa0ea0 fix(router): revert to CI4 v4.2.7 to include all routes 2022-11-24 15:30:24 +00:00
Yassine Doghri
2e95273d97 build(docker-dev): fix app container command 2022-11-23 16:57:26 +01:00
Yassine Doghri
4f7c17f420 chore: add missing translation keys for blocked actors and domains breadcrumb 2022-11-22 16:48:59 +00:00
semantic-release-bot
07d5ab5af7 chore(release): 1.0.4 [skip ci]
## [1.0.4](https://code.castopod.org/adaures/castopod/compare/v1.0.3...v1.0.4) (2022-11-21)

### Bug Fixes

* update actorUsername regex to get url_to actor ([1d6b177](1d6b177a55))
2022-11-21 14:25:08 +00:00
Yassine Doghri
1d6b177a55 fix: update actorUsername regex to get url_to actor
Since CI4 v4.2.8, the actor route is not retrieved anymore, this prevents users from creating a
podcast.
2022-11-18 17:38:40 +00:00
Yassine Doghri
2d82411788 docs(docker): rename ffmpeg service to video-clipper in docker-compose example 2022-11-17 16:21:02 +00:00
semantic-release-bot
f867ab6a61 chore(release): 1.0.3 [skip ci]
## [1.0.3](https://code.castopod.org/adaures/castopod/compare/v1.0.2...v1.0.3) (2022-11-17)

### Bug Fixes

* **dashboard-ui:** fill the blank gaps between cards on smaller screen sizes ([00836cc](00836cc368))
2022-11-17 15:30:13 +00:00
crowdin
b1e52ffac3 chore: new Crowdin updates 2022-11-17 14:42:46 +00:00
Yassine Doghri
b99f70cc60 ci(docker): revert docker job condition 2022-11-17 14:30:22 +00:00
Yassine Doghri
e960440854 docs(all-contributors): add missing translators in contributors list 2022-11-17 13:49:44 +00:00
Yassine Doghri
0eb223baa0 chore: update CodeIgniter to v4.2.10 2022-11-17 13:10:34 +00:00
Yassine Doghri
00836cc368 fix(dashboard-ui): fill the blank gaps between cards on smaller screen sizes 2022-11-10 16:25:04 +00:00
Yassine Doghri
5227b5fc29 refactor(webmonetization): update value tag to follow new WM proposal 2022-11-10 16:24:51 +00:00
Yassine Doghri
0bb1c9635a ci: fix docker build job rules by including main branch 2022-11-07 13:38:27 +00:00
semantic-release-bot
7ba5f15839 chore(release): 1.0.2 [skip ci]
## [1.0.2](https://code.castopod.org/adaures/castopod/compare/v1.0.1...v1.0.2) (2022-11-04)

### Bug Fixes

* **auth:** disallow registration by default ([379b9be](379b9be2b9))
* **contributors:** add prefix to podcast group to delete contributor ([9f785db](9f785db7ba))
* extract podcast ids from user groups using a regex ([e26215a](e26215a11f))
* **notifications:** add manage-notifications permission to podcast ([ed7c247](ed7c247bcb))
* **platforms:** convert special characters to htmlentities to validate url ([82310a2](82310a2e0b))
2022-11-04 11:24:09 +00:00
crowdin
fa90decdd1 chore(i18n): new Crowdin updates 2022-11-04 11:03:24 +00:00
Yassine Doghri
379b9be2b9 fix(auth): disallow registration by default 2022-11-04 10:39:26 +00:00
Yassine Doghri
9f785db7ba fix(contributors): add prefix to podcast group to delete contributor 2022-11-04 10:39:26 +00:00
Yassine Doghri
e26215a11f fix: extract podcast ids from user groups using a regex 2022-11-04 10:39:26 +00:00
Yassine Doghri
acb067a80e chore: update ci4/shield to v1.0.0-beta.3 2022-11-04 10:39:26 +00:00
Yassine Doghri
45d302be29 docs(dev-setup): add missing environment keys for development .env 2022-11-04 10:39:26 +00:00
Yassine Doghri
ed7c247bcb fix(notifications): add manage-notifications permission to podcast 2022-11-04 10:39:26 +00:00
Yassine Doghri
82310a2e0b fix(platforms): convert special characters to htmlentities to validate url
remove validate_url custom validator and replace with CI4's valid_url_strict
2022-11-04 10:39:26 +00:00
Yassine Doghri
67b6e30d24 build(docker): add --cleanup flag to each kaniko build
flag is used to clean the filesystem at the end of the build
2022-11-04 10:39:26 +00:00
Yassine Doghri
c69c0fbb40 build(docker): replace libjpeg-dev with libjpeg62-turbo-dev in development image 2022-11-04 10:39:26 +00:00
semantic-release-bot
a6395b5ce0 chore(release): 1.0.1 [skip ci]
## [1.0.1](https://code.castopod.org/adaures/castopod/compare/v1.0.0...v1.0.1) (2022-11-01)

### Bug Fixes

* **platforms:** trim platform url before validation and storage ([259fe5f](259fe5f697))
2022-11-01 18:10:12 +00:00
Romain de Laage
1192a228ef build(docker): add dedicated ffmpeg image to run video clips' scheduled tasks 2022-11-01 17:55:39 +00:00
Yassine Doghri
259fe5f697 fix(platforms): trim platform url before validation and storage
--> Having a URL with spaces in the beginning or end would cause the platform to be deleted
2022-11-01 15:15:39 +00:00
Romain de Laage
c94bd7cf81 build(docker): run automatic database migration in entrypoint 2022-10-24 16:05:49 +00:00
Yassine Doghri
3419369af0 docs(docker): add tags for specific versions and latest builds
update gitlabci: do not run docker build if CP_VERSION.env file is not present
2022-10-24 15:41:08 +00:00
semantic-release-bot
f9572e4125 chore(release): 1.0.0 [skip ci]
# 1.0.0 (2022-10-20)

### Bug Fixes

* **a11y:** replace active tab color to contrast with background on podcast and episode pages ([f3785e1](f3785e1401))
* **activity-pub:** cache issues when navigating to activity stream urls ([7bcbfb3](7bcbfb32f7))
* **activity-pub:** get database records using new model instances ([92536dd](92536ddb38))
* **activitypub:** add conditions for possibly missing actor properties + add user-agent to requests ([8fbf948](8fbf948fbb))
* **activitypub:** add target actor id to like / announce activities to send directly to note's actor ([962dd30](962dd305f5))
* **activitypub:** add target_actor_id for create activity to broadcast post reply ([0128a21](0128a21ec5))
* **activitypub:** allow cors on get requests for routes exposing acitivitypub objects ([2f24809](2f2480998f))
* **activitypub:** set created_by to null for reblog if no user + update episode oembed data ([209dfbd](209dfbd134))
* add admin-audio-player to vite config to have admin player show up ([93cb9b2](93cb9b2470))
* add application/octet-stream mimetype to mp3 and m4a extensions to prevent ext_in error ([339bef8](339bef878e)), closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
* add category_label component to include parent category in about podcast page ([74e7d68](74e7d68ac8))
* add explicit int conversion when formatting episode duration ([1253096](1253096197))
* add head request to analytics_hit route ([f0a2f0b](f0a2f0bea4))
* add href to castopod website on login page ([cc54257](cc54257351))
* add missing explicit badge for podcasts and episodes ([cdf9f9d](cdf9f9d53f))
* add open graph size for podcast images to replace the inadequate large format ([33aae1f](33aae1f793))
* add public/media folder to castopod bundle ([8053d35](8053d3521b)), closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
* add translation key for audio-clipper trim labels ([db191ac](db191ac31b))
* add underline and semibold font weight for prose links to have them stand out ([d4d8671](d4d867121c))
* add where condition to get episode count without deleted episodes ([7661734](7661734ed2)), closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
* **admin:** save block and lock switches ([b66c0af](b66c0afc8f))
* **analytics:** redirect to mp3 file even when referer was not set ([9fc388d](9fc388d154))
* **analytics:** remove charts empty values + remove useless language cache ([1678794](1678794153))
* **analytics:** set duration field to precise decimal as episode's audio file duration ([d772685](d772685405))
* **analytics:** set initial value for duration and bandwidth ([ee50539](ee50539591))
* **analytics:** update migrations to set decimal precision for latitude and longitude ([714d6b5](714d6b5d49))
* **analytics:** update service management so that it works with new OPAWG slug values ([7fe9d42](7fe9d42500))
* **audio-clipper:** add mouse position offset when stretching clip to prevent content from jumping ([602654b](602654b99b))
* **audio-clipper:** show audio playing progress + put waveform behind audio clipper ([01a09dc](01a09dc447))
* **avatar:** use default avatar when no avatar url has been set ([9d23c7e](9d23c7e7e1)), closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
* **bundle:** include modules and themes when copying files with rsync ([cd5bb88](cd5bb8835c))
* **bundle:** update vite input files path + add `set -e` in bash scripts to fail if command fails ([0ee53c7](0ee53c71ff))
* **cache:** add locale for podcast and episode pages + clear some persisting cache in models ([9cec8a8](9cec8a81cc)), closes [#42](https://code.castopod.org/adaures/castopod/issues/42) [#61](https://code.castopod.org/adaures/castopod/issues/61)
* **cache:** delete posts and comments pages cache when updating platform links ([f7c3e5b](f7c3e5bf4a)), closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
* **cache:** return a non cached view when connected ([e2e7358](e2e735815d))
* **cache:** suffix cache names with authenticated for credits, map and pages ([418a70b](418a70b2a6))
* cast actor_id to pass as int to set_interact_as_actor() function ([56a8e5d](56a8e5d7dd))
* **category:** remove uncategorized option to enforce users in choosing a category ([8c64f25](8c64f25a0e))
* change image size requirement hints ([ea20206](ea20206ee6))
* change message upon cancellation of episode publication ([9859c74](9859c7434c))
* check for database connection and podcasts table existence before redirecting to install ([eb74e81](eb74e81c3d))
* check that additional files are valid when creating episode ([eac5bc8](eac5bc876d))
* check that note has a preview_card_id before displaying it ([acb8b3a](acb8b3a401)), closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
* clear cache when deleting podcast banner ([99bb40b](99bb40b8bc))
* comment all cache clean after page update to prevent analytics cache deletion ([e6197a4](e6197a4972))
* **comments:** add comment view partials for public pages ([fcecbe1](fcecbe1c68))
* correct chart data ([4d3e9c8](4d3e9c8c02))
* correct percona compatibility issue ([e53f819](e53f819264))
* correct php-fpm issues ([1ef55d7](1ef55d7315))
* correct referrer bug ([ed69b2f](ed69b2f500))
* correction for servers with low int precision ([31b7828](31b7828e77))
* **cors:** add preflight option routes for episode, podcast and status objects ([a281abf](a281abfda4))
* declare typed properties in PHPDoc for php<7.4 ([14dd44d](14dd44d03d)), closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
* define podcast_id and platform_slug as foreign keys in podcasts_plaforms table ([6e9451a](6e9451a110))
* define podcastNamespaceLink value ([0d744d2](0d744d212d))
* **email:** set the correct url in the activation and forgot emails ([10fc6f1](10fc6f17c6)), closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
* **embeddable-player:** enable any ancestor when X-Frame-Options is set on server ([44a4962](44a4962e0b))
* **embed:** open embedded player's links in new tab ([4aa73d7](4aa73d71e3))
* **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` & `post_max_size` ([3b3c218](3b3c218b9c)), closes [#5](https://code.castopod.org/adaures/castopod/issues/5) [#86](https://code.castopod.org/adaures/castopod/issues/86)
* **episode-unpublish:** set consistent posts_counts' increments/decrements for actors and episodes ([8acdafd](8acdafd260)), closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
* **episodeCount:** add missing brackets to French language file ([c1b4112](c1b411265a))
* **episode:** replace guid's empty string value to null ([441052a](441052af8d))
* **episodes-page:** handle defaultQuery being null when no podcast episodes ([15183b7](15183b7eab)), closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
* **episodes-table:** set descriptions to be not null ([6774ec1](6774ec10fa))
* **episodes:** add publication status + set publication date to null when none has been set ([d882981](d882981b3a)), closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
* escape characters for `min` in format_duration_symbol ([3b6722a](3b6722a42b))
* escape generated feed tag values and remove new lines from public pages meta description ([6238a43](6238a43863)), closes [#57](https://code.castopod.org/adaures/castopod/issues/57) [#46](https://code.castopod.org/adaures/castopod/issues/46)
* expire default query cache upon scheduled episode publication ([b72e7c8](b72e7c8691)), closes [#81](https://code.castopod.org/adaures/castopod/issues/81)
* explicitly cast seconds to int in iso8601_duration helper function ([779653f](779653f75b))
* **fediverse:** set default castopod avatar url when actor avatar is not present ([460f52f](460f52f70e))
* **fediverse:** set model instances as non shared to prevent overlapping ([91128fa](91128fad7a))
* fix layout bugs in admin and update translation files ([a834171](a83417180c)), closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
* **follow:** add missing helpers to Actor controller ([ee53a73](ee53a732dc))
* **get_browser_language:** return defaultLocale if browser doesn't send user preferred language ([9cc2996](9cc2996261))
* handle HEAD requests on podcast_feed route ([74b2640](74b2640f2a)), closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
* **home:** remove hardcoded prefix in getAllPodcasts query ([92d5cc5](92d5cc50a3))
* **housekeeping:** replace the use of GLOB_BRACE with looping over file extensions ([42d92d0](42d92d0c8d)), closes [#154](https://code.castopod.org/adaures/castopod/issues/154)
* **housekeeping:** set default sizes value + ignore illegal IFD size error to proceed with script ([f21ca57](f21ca57603))
* **housekeeping:** use EpisodeModel's builder to reset comments count ([65e9c0b](65e9c0b05e))
* **htaccess:** add ? after index.php in RewriteRule ([d9d139e](d9d139eefa)), closes [#152](https://code.castopod.org/adaures/castopod/issues/152)
* **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be sent in any order ([b7f285e](b7f285e4e2))
* **images:** set default mimetype if none is specified when getting size info ([6e4acc6](6e4acc64ad))
* **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in download_file, closes [#103](https://code.castopod.org/adaures/castopod/issues/103) ([35b5be0](35b5be095f))
* **import:** add extension when downloading file without + truncate slug if too long ([c5f18bb](c5f18bb6dc))
* **import:** add validation for handle field to prevent Router.invalidParameterType error ([5bf7200](5bf7200fb3)), closes [#119](https://code.castopod.org/adaures/castopod/issues/119)
* **import:** cast description's SimpleXMLElement to string ([02d17be](02d17be4ff))
* **import:** remove query string from files url ([109c4aa](109c4aa1af))
* **import:** save media files during podcast import + set missing media fields ([a9989d8](a9989d841a))
* **import:** set default episode type if not set ([d7250ab](d7250ab03f))
* **import:** set episode and season numbers to null when not present in item tag ([3211398](3211398c78))
* **import:** use <image><url> tag when no <itunes:image> is present ([20e607a](20e607afb7))
* include missing variables on public ui's episode page and remote_actions ([193b373](193b373bc9))
* **input-component:** unset required attribute to prevent rendering it when false ([db9ac13](db9ac13860))
* **install:** add password validation when creating super admin ([5a2ca0c](5a2ca0cc4a))
* **install:** redirect manually to install wizard on first visit ([2ceaaca](2ceaaca44f))
* **install:** redirect to host_url install route on instanceConfig validation error ([99250b1](99250b1868))
* **install:** redirect to input baseUrl after instance config ([2426af7](2426af7de8)), closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
* **install:** set message block on forms to show error messages ([3a0a20d](3a0a20d59c)), closes [#157](https://code.castopod.org/adaures/castopod/issues/157)
* **interact-as:** set actor_id instead of podcast id upon login event ([5dfade7](5dfade7cf3)), closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
* **json-ld:** add missing properties to PodcastSeries object ([e97266c](e97266c5d4))
* keep subtitle line breaks when parsing srt file to json ([cfb3da6](cfb3da6592))
* **layouts:** replace holy-grail layout with tailwind config + widen public podcast layout ([be5a287](be5a28787f))
* **map:** update episode markers query to discard unpublished episodes ([b3caac4](b3caac45b1))
* **markdown-editor:** remove unnecessary buttons for podcast and episode editors + add extensions ([9c4f60e](9c4f60e00b))
* **md-editor:** build new markdown editor with lit + github/markdown-toolbar-element ([9ec1cb9](9ec1cb93da)), closes [#93](https://code.castopod.org/adaures/castopod/issues/93) [#94](https://code.castopod.org/adaures/castopod/issues/94) [#120](https://code.castopod.org/adaures/castopod/issues/120)
* **migrations:** ignore invalid utf8 chars for media files metadata + update transcript parser ([45e8f99](45e8f99e75))
* minor corrections ([13be386](13be386842))
* move analytics to helper ([d311917](d31191732e))
* move html escaping on credits page ([fbffdbd](fbffdbde78))
* **multiselect:** add missing class names in choices options for purge to work properly ([719538d](719538d0cc))
* **notifications:** add trigger after activities update + update insert trigger ([e5d16e8](e5d16e8711))
* **notifications:** notify actors after activities insert / update using model callback methods ([e08555a](e08555a4e9))
* **open-graph:** replace non existant episode description to podcast description in podcast page ([b02584e](b02584ee60))
* overwrite common lang function to escape returned string ([4c490c1](4c490c15bb)), closes [#196](https://code.castopod.org/adaures/castopod/issues/196) [#198](https://code.castopod.org/adaures/castopod/issues/198)
* overwrite getActorById to return app's Actor entity ([f2bc2f7](f2bc2f7e01))
* **package.json:** update destination of postcss generation scripts ([21413f8](21413f8af3))
* **pages:** add locale to page cache ([8f999ce](8f999ce2f7))
* **partner:** set correct image URL ([61554be](61554be12a))
* pass timezone to relative time component to show the localized time in the UI ([b9db936](b9db936461))
* **persons:** prevent overflow of persons list by adding horizontal scroll ([9e8995d](9e8995dc6e))
* **persons:** set person picture as optional for better ux ([7fdea63](7fdea63de7)), closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
* **platforms:** display platform link only when visible is toggled on ([6e503c8](6e503c8d61)), closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
* **player-styling:** revert vite to 2.8 to reference the player css ([e07d3af](e07d3afea9))
* **podcast-activity:** check if transcript and chapters are set before including them in audio ([5855a25](5855a25093))
* **podcast-import:** move guid attribute declaration for Episode entity to include slug data ([5d02ae3](5d02ae3990))
* **podcast:** use markdown description value for editor + set prose class to about description ([f304d97](f304d97b14)), closes [#156](https://code.castopod.org/adaures/castopod/issues/156)
* prefill description footer input when creating a new episode ([9ea5ca3](9ea5ca3169))
* **premium-podcasts:** display unlock button in embed when premium episode ([ca109ba](ca109ba3a8))
* **premium-podcasts:** remove cache in unlock form + redirect to podcast if podcast is not premium ([242352c](242352c4d9))
* **premium-podcasts:** return different cached page when podcast is unlocked ([b1303c5](b1303c5255))
* **pwa:** add scope to webmanifests to allow installing an app per podcast ([74c683e](74c683eb44))
* **pwa:** set app display as standalone in the webmanifests ([7aa37d2](7aa37d24ac))
* re-order graph values ([35f633b](35f633b4c7))
* redirect to non cached views when authenticated in public views ([482b47b](482b47ba6b))
* **release:** add missing version number to castopod-host package ([8f3e9d9](8f3e9d90c1))
* remove cache from remote follow form to display error messages ([90e4443](90e44437bd))
* remove defer from js script declaration as it is a module ([18ae557](18ae557e97))
* remove fixed size from podcast sidebar + rearrange account info + space out import radio inputs ([776eec6](776eec6f0d))
* remove heavy image cover data from audio file metadata ([f74403b](f74403bd7a))
* remove required for other_categories field and add podcast_id to latest podcasts query ([5417be0](5417be0049))
* remove required property to persons picture ([c546be3](c546be385b)), closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
* remove value escaping for form inputs and textareas ([bc6dea2](bc6dea2f8a))
* rename field status to task_status to get scheduled activities ([4ff82a5](4ff82a5f0a))
* rename issue_templates labels ([9f00305](9f00305844))
* rename MyAccount controller file ([e109df3](e109df3004)), closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
* rename podcast name to podcast handle to clarify field usage ([9dd4c77](9dd4c7741e)), closes [#126](https://code.castopod.org/adaures/castopod/issues/126)
* reorder fields as composite primary keys for analytics tables ([9660aa9](9660aa97c8))
* replace deletedField with published_at for episodes ([14d7d07](14d7d07822))
* replace getWebEnclosureUrl with getEnclosureWebUrl ([8122cea](8122ceaf8a))
* replace hardcoded style links with vite service + set default value for remote transcript url ([3f2e056](3f2e05608e)), closes [#149](https://code.castopod.org/adaures/castopod/issues/149) [#150](https://code.castopod.org/adaures/castopod/issues/150)
* replace website key for webpages in breadcrumb translate file ([50e32ff](50e32ff756))
* restore default podcast icon on public website ([342778b](342778bac3))
* revert to beta.1's codeigniter4 version ([e831411](e831411270))
* rewrite regenerate image function to use saveSizes method from Image entity ([3889912](38899124ec))
* **router:** check if Accept header is set before getting value ([10a2ae0](10a2ae0248)), closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
* **router:** trim URI slash to match same routes for URIs with and without trailing slash ([9e9375f](9e9375f9a2))
* **rss-import:** add Castopod user-agent, handle redirects for downloaded files, add Content namespace ([214243b](214243b3fe))
* **rss:** cast number type values to string in rss_helper ([7180ae9](7180ae9ec7)), closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
* **rss:** do not escape podcast and episode titles in the xml ([0dd3b7e](0dd3b7e0bf)), closes [#138](https://code.castopod.org/adaures/castopod/issues/138) [#71](https://code.castopod.org/adaures/castopod/issues/71)
* **rss:** remove escaping for publisher and owner name ([6fc6347](6fc6347846))
* **rss:** round episode durations and soundbites ([c9fb987](c9fb987fcf)), closes [#214](https://code.castopod.org/adaures/castopod/issues/214)
* **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified ([2271c14](2271c1445b)), closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
* **rss:** use originalPath instead of originalMediaPath in Image library ([b4012b7](b4012b7d2e))
* save transcript and chapters files to podcasts folder ([63f49c7](63f49c719f))
* **search-episodes:** add fallback sql query using LIKE for search query with less than 4 characters ([e66bf44](e66bf44341)), closes [#236](https://code.castopod.org/adaures/castopod/issues/236)
* **security:** add csrf filter + prevent xss attacks by escaping user input ([cd2e1e1](cd2e1e1dc3))
* set cache expiration to next note publish to show note on publication date ([0a66de3](0a66de3e6c))
* set episode description footer to null when empty value ([3a7d97d](3a7d97d660))
* set episode duration translation to hardcoded english ([c39efc9](c39efc9489)), closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
* set episode guid upon episode creation ([ad8b153](ad8b153f2a)), closes [#48](https://code.castopod.org/adaures/castopod/issues/48)
* set episode numbers during import + remove all custom form_helpers + minor ui issues ([99a3b8d](99a3b8d33e))
* set interact_as_actor for user upon password reset ([ad8f5f5](ad8f5f5a0f)), closes [#178](https://code.castopod.org/adaures/castopod/issues/178)
* set localized slug_field key as string in french language ([17fb29b](17fb29b209))
* set location to null when getting empty string ([71b1b5f](71b1b5f775))
* set storage limit as disk_total_space instead of free space ([7512e2e](7512e2ed1f))
* **settings:** add .jpg extension to site-icon file input to display all jpeg images ([f611a16](f611a16cd0))
* **socialinteract:** move social interact uri into uri attribute + update social data upon import ([12b2200](12b22008a2))
* sort episodes by published_at with unpublished episodes at the begining ([1686f84](1686f840d1)), closes [#249](https://code.castopod.org/adaures/castopod/issues/249)
* sort episodic podcasts by season ([d7b6794](d7b6794f68))
* **themes:** update themes stylesheet route and remove css extension ([e4e7e00](e4e7e0005e))
* **types:** update fake seeders types + fix bugs ([76a4bf3](76a4bf3441))
* **ui:** remove empty tooltip when hovering on sponsor button ([40aa661](40aa661289))
* unpublish episode before deleting it + add validation step before deletion ([f75bd76](f75bd76458)), closes [#112](https://code.castopod.org/adaures/castopod/issues/112) [#55](https://code.castopod.org/adaures/castopod/issues/55)
* update .htaccess for shared hosting config ([2379826](2379826352))
* update broken contributor dropdown fields ([e5b7515](e5b7515023))
* update condition in AnalyticsTrait ([fbc0967](fbc0967caa))
* update condition in home controller to redirect to install page ([33f1b91](33f1b91d55))
* update conditions when checking for empty max_episodes and season_number ([fbad0b5](fbad0b59f6))
* update form_textarea to prevent escaping value ([78548b5](78548b5cd7))
* update iso-369 language table seeder ([0c90db4](0c90db44c4))
* update ivoox podcasting icon ([f2b69a4](f2b69a4733))
* update MarkdownEditor component + restyle Button and other components ([b05d177](b05d177f1b))
* update purgecss content path for php helper files ([eb70bb4](eb70bb4f70)), closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
* update translations for settings' tasks to include what they should be used for ([06b1a8b](06b1a8b29b))
* use slash instead of backslash to call layout ([a80adb2](a80adb2295))
* use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries ([4e22a0d](4e22a0d5e4))
* **users:** remove required roles input when editing user + prevent owner's roles from being edited ([1c8af75](1c8af7550b)), closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
* **ux:** allow for empty message upon episode publication and warn user on submit ([33d01b8](33d01b8d4f)), closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
* **ux:** have podcast dashboard card link to podcast dashboard if only one podcast in instance ([7dabee5](7dabee58a1))
* **ux:** redirect user to install page on database error in home page ([9017e30](9017e30bf4))
* validate slug length when submitting episode form + clean permalink edit prefix ([b07ac09](b07ac093b2))
* **video-clips:** check if created video exists before recreating it and failing ([dff1208](dff1208725))
* **video-clips:** clear video clip cache after process has finished ([3ae6232](3ae6232585))
* **video-clips:** create unique temporary files for resources to be deleted after generation ([7f7c878](7f7c878cb6))
* **video-clips:** set audio codec to aac, fixing audio issue on twitter ([3c22c68](3c22c68ee8))
* **video-clips:** set longer podcast and episode lengths for squared format ([c030113](c0301134c2))
* **video-clips:** tweak portrait parameters to have subtitles display without overflowing ([2385b1a](2385b1a292))
* **video-clips:** update condition to check if ffmpeg is installed ([b57f0b6](b57f0b6eb6)), closes [#163](https://code.castopod.org/adaures/castopod/issues/163)
* **xml-editor:** escape xml editor's content + restyle form sections to prevent overflowing ([588590b](588590bd2c))
* **xml-editor:** prettify xml even without root node ([ca55c24](ca55c248d0))

### Features

* **activitypub:** add Podcast actor and PodcastEpisode object with comments ([9e1e5d2](9e1e5d2e86))
* add about page in admin with instance info + database update button ([d0836f3](d0836f3ee3))
* add alternate rss feed link tag to podcast page head ([a973c09](a973c097d5)), closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
* add analytics and unknown useragents ([ec92e65](ec92e65aa4))
* add audio-clipper toolbar + add video-clip-previewer ([0255753](02557539e6))
* add audio-clipper webcomponent (wip) ([21d4251](21d4251b9b))
* add autofocus to input field "Email or username" on login page ([19caed4](19caed4bce))
* add basic stats on podcast about page ([1670558](1670558473))
* add breadcrumb in admin area ([7fb1de2](7fb1de2cf3)), closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
* add cache to ActivityPub sql queries + cache activity and note pages ([2d297f4](2d297f45b3))
* add CDN url ([972bcbf](972bcbf65e)), closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
* add codemirror to display xml editor for custom rss field ([f15f262](f15f26240c))
* add cumulative listening time charts ([588b4d2](588b4d28da))
* add default icons to Alert component ([0d98001](0d9800123b))
* add DropdownMenu component + remove global audio player in admin ([abb7fba](abb7fbac27))
* add episode_numbering() component helper to display episode and season numbers ([3f4a6bd](3f4a6bd0b9))
* add french translation ([196920d](196920d62f))
* add heading component + update ecs rules to fix views ([23bdc6f](23bdc6f8e3))
* add housekeeping task to run after migrations ([89dee41](89dee41d58))
* add install wizard form to bootstrap database and create the first superadmin user ([cba871c](cba871c5df)), closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
* add instructions on production error page to ease Castopod debugging process ([9eab54e](9eab54e085)), closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
* add ISO 3166 country codes ([97cd94b](97cd94b474))
* add js audio player on podcast, admin and embeddable player pages + fix admon episodes ux ([0e14eb4](0e14eb4d3f)), closes [#131](https://code.castopod.org/adaures/castopod/issues/131)
* add label to sponsor button on podcast page ([c29c018](c29c018c7a)), closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
* add legalNoticeURL to app config for setting an external url to legal notice ([711843a](711843a0c8))
* add lock podcast according to the Podcastindex podcast-namespace to prevent unauthozized import ([72b3012](72b301272e))
* add map analytics, add episodes analytics, clean analytics page layout, translate countries ([07eae83](07eae83a00))
* add media entity and link documents, images and audio files to it ([6ecf286](6ecf2866cf))
* add notifications inbox for actors ([999999e](999999e3ef)), closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
* add Noto Sans Mono font to use for durations + button to access new video clip form in list ([7609bb6](7609bb6033))
* add npm for js dependencies + move src/ files to root folder ([cbb83a6](cbb83a6f30))
* add Open Graph and Twitter meta tags ([af970b8](af970b8bac)), closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
* add pages table to store custom instance pages (eg. legal-notice, cookie policy, etc.) ([9c224a8](9c224a8ac6)), closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
* add permanent delete feature for podcasts 🎉 ([dbb4030](dbb4030da4)), closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
* add platform models ([a333d29](a333d29196))
* add platforms form in podcast settings ([043f49c](043f49c784))
* add platforms tables ([ce59344](ce5934419a))
* add podcast banner field for each podcast + refactor images configuration ([4a8147b](4a8147bfbb))
* add premium podcasts to manage subscriptions for premium episodes ([3234500](3234500e2d)), closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
* add publish feature for podcasts and set draft by default ([3d363f2](3d363f2efe)), closes [#128](https://code.castopod.org/adaures/castopod/issues/128) [#220](https://code.castopod.org/adaures/castopod/issues/220)
* add remote_url alternative for transcript and chapters files ([3143c9a](3143c9ad36))
* add replied to post or comment to reply element ([d0f9c60](d0f9c6018f))
* add schema.org json-ld objects to podcasts, episodes, posts and comments pages ([902f959](902f959b30))
* add task to housekeeping setting for resetting all instance counts ([9303e51](9303e51bc5))
* add unique listeners analytics ([3a49258](3a4925816f))
* add update rss feed feature for podcasts to import their latest episodes ([5eb9dc1](5eb9dc168e)), closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
* add user permissions and basic groups to handle authorizations ([d58e518](d58e51874a)), closes [#3](https://code.castopod.org/adaures/castopod/issues/3) [#18](https://code.castopod.org/adaures/castopod/issues/18)
* add WebSub module for pushing feed updates to open hubs ([10d3f73](10d3f73786))
* **admin:** add instance wide dashboard with storage and bandwidth usage ([b1a6c02](b1a6c02e56)), closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
* **admin:** add search form in podcast episodes list ([6be5d12](6be5d12877)), closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
* **admin:** make header stick on scroll and show title + action buttons using css only ([d60498c](d60498c1be))
* **admin:** update admin layout for better ux + update brand pine colors ([d86142e](d86142ebe7))
* allow cross origin requests on episode comments ([e12f95a](e12f95aca1))
* **analytics-gdpr:** update cached personal data to expire at midnight ([0188b67](0188b67354))
* **analytics:** add 'other' group to pie charts in order to display more accurate data ([73acef9](73acef933f))
* **analytics:** add charts and data export ([78625c4](78625c471b))
* **analytics:** add current date and secret salt to analytics hash for improved privacy ([6f2e7c0](6f2e7c009c))
* **analytics:** add service name from rss user-agent ([7202b98](7202b9867b))
* **analytics:** add weekday and hour bar charts ([8ab3132](8ab313296b))
* **api:** add rest api with podcasts read endpoints ([e64001d](e64001d006)), closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
* apply colour theme to embed player ([9548337](9548337a7c)), closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
* **auth:** add auth.enable2FA config to enable two-factor authentication ([7213ed2](7213ed290c))
* build hashed static files to renew browser cache ([37c54d2](37c54d2477)), closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
* **cache:** add podcast and episode pages to cache + clear them after insert or update ([da0f047](da0f047281))
* **categories:** create model, entity, migrations and seeds ([f73b042](f73b042cc0))
* **clips:** setup clip entities and model + save video clip to have it generated in the background ([2f6fdf9](2f6fdf9091))
* **comments:** add comments to episodes + update naming of status to post ([bb4752c](bb4752c35e))
* **comments:** add like / undo like to comment + add comment page ([0c187ef](0c187ef7a9))
* **components:** add custom view renderer with ComponentRenderer adapted from bonfire2 ([a95de8b](a95de8bab0))
* create optimized & resized images upon upload ([02e4441](02e4441f98)), closes [#6](https://code.castopod.org/adaures/castopod/issues/6)
* **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and ❬item❭ ([6ecdaad](6ecdaad911))
* **datetime-picker:** set material_green theme to flatpickr ([3ce6541](3ce6541003))
* **devcontainer:** add devcontainer settings for dev environment ([69e7266](69e7266736))
* display castopod version in admin footer ([9f2574e](9f2574e6fb)), closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
* display legal disclaimer and warning on podcast import page ([2f07992](2f07992e55)), closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
* edit + delete podcast and episode ([ac5f0c7](ac5f0c7328))
* **embeddable-player:**  add embeddable player widget ([141788f](141788fa08))
* enhance admin ui with responsive design and ux improvements ([2d44b45](2d44b457a0)), closes [#31](https://code.castopod.org/adaures/castopod/issues/31) [#9](https://code.castopod.org/adaures/castopod/issues/9)
* enhance ui using javascript in admin area ([c0e66d5](c0e66d5f70))
* **episode-unpublish:** remove episode comments upon unpublish ([78acd7f](78acd7f5c0))
* **episode:** add form to allow editing episode's publication date to a past date ([d783d16](d783d16eb7)), closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
* **episodes:** add create form and view pages for episode ([f3b2c8b](f3b2c8b84f)), closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
* **episodes:** add migrations, model and entity for episodes table ([0444821](044482174e))
* **episodes:** replace all audio file URL parameters with base64 encoded data ([e1f65cd](e1f65cd3b5))
* **episodes:** replace soft delete with permanent delete ([eb9ff52](eb9ff522c2))
* **episodes:** schedule episode with future publication_date by using cache expiration time ([4f1e773](4f1e773c0f)), closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
* **fediverse:** implement activitypub protocols + update user interface ([2f525c0](2f525c0f6e)), closes [#69](https://code.castopod.org/adaures/castopod/issues/69) [#65](https://code.castopod.org/adaures/castopod/issues/65) [#85](https://code.castopod.org/adaures/castopod/issues/85) [#51](https://code.castopod.org/adaures/castopod/issues/51) [#91](https://code.castopod.org/adaures/castopod/issues/91) [#92](https://code.castopod.org/adaures/castopod/issues/92) [#88](https://code.castopod.org/adaures/castopod/issues/88)
* **fonts:** replace Montserrat with Inter for better readablity ([bfa11d0](bfa11d007d))
* **GDPR:** add GDPR.yml file to public/.well-known/ ([86bccc3](86bccc3d5c))
* **gdpr:** add purpose for granting access to premium content ([47d6d81](47d6d81b79))
* **home:** sort podcasts by recent activity + add dropdown menu to choose between sorting options ([7b89da6](7b89da6106)), closes [#164](https://code.castopod.org/adaures/castopod/issues/164)
* **housekeeping:** add clear_cache option to flush redis or files cache ([99bfac0](99bfac0b42))
* **i18n:** add 7 new languages + update german translations ([d021abb](d021abb52f))
* **i18n:** add german language as supported locale + create Language files from english source ([c220b31](c220b310ed))
* **i18n:** add Norwegian Nynorsk to supported locales ([ced61fc](ced61fc236))
* **i18n:** add Polish translation ([2d83b44](2d83b44add))
* **i18n:** add Spanish to supported locales ([e340b54](e340b54a84))
* **i18n:** add support for German and Brazilian Portuguese languages ([c9b9fe4](c9b9fe4ee8))
* **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca) locales ([48d1443](48d1443472))
* **icons:** add default icons for podcasting, social and funding platforms + remove complex icons ([5bcdfeb](5bcdfebe64)), closes [#166](https://code.castopod.org/adaures/castopod/issues/166) [#167](https://code.castopod.org/adaures/castopod/issues/167) [#170](https://code.castopod.org/adaures/castopod/issues/170)
* **icons:** add podnews icon to podcasting platforms ([5f42355](5f423557c2)), closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
* import podcast from an rss feed url ([9a5d5a1](9a5d5a15b4)), closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
* integrate stylized form components and update podcast edit page ([6536729](6536729546))
* make displayed publication time as relative time using @github/time-elements ([230e139](230e139e43))
* make episode description more visible on episode pages ([90533be](90533be029)), closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
* **map:** display geolocated episodes on a map page ([4357cc2](4357cc25cc))
* **media:** clean media api + create an entity per media type ([fafaa7e](fafaa7e689))
* **media:** save audio, images, transcripts and chapters to media for episode and persons ([58e2a00](58e2a00a87))
* **meta-tags:** add activitypub alternate links to podcast, episode, comment and post pages ([bd61752](bd61752be2))
* minor corrections to some tables ([3bf9420](3bf9420b59))
* **monetization:** add Web Monetization support ([96a6026](96a6026f1d))
* **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about the castopod instance ([88fddc8](88fddc81d7))
* **partner:** add link and image in episode description ([ad07bb9](ad07bb9330))
* **person:** add podcastindex.org namespace person tag ([8acd011](8acd011f13))
* **platforms:** add AntennaPod ([53e9cfd](53e9cfd61c))
* **platforms:** add Fediverse and some funding platforms, add link on logo ([afc3d50](afc3d50289))
* **platforms:** add helloasso ([16cb993](16cb993ee6))
* **platforms:** add missing newpodcastapps.com's platforms ([92dd370](92dd370e2f))
* **platforms:** add pod.link ([3d7a232](3d7a2320dd))
* **platforms:** add Podcast Index ([ad52b1c](ad52b1cc2b))
* **platforms:** add podfriend ([9fdc8d3](9fdc8d3293))
* **podcast-form:** add new_feed_url field to set an url when changing domain or host ([e7eec48](e7eec48e7b))
* **podcast-form:** update routes and redirect to podcast page ([12ce905](12ce905799))
* **podcast:** create a podcast using form ([1202ba3](1202ba3545))
* **podcasting 2.0:** update podcast:social tag to adhere to latest spec ([a597cf4](a597cf4ecf))
* prefill season and episode numbers + set episode number as mandatory for serial podcasts ([07d740b](07d740b79f)), closes [#134](https://code.castopod.org/adaures/castopod/issues/134) [#136](https://code.castopod.org/adaures/castopod/issues/136)
* **public-ui:** adapt public podcast and episode pages to wireframes ([40a0535](40a0535fc1)), closes [#30](https://code.castopod.org/adaures/castopod/issues/30) [#13](https://code.castopod.org/adaures/castopod/issues/13)
* **pwa:** add service-worker + webmanifest for each podcasts to have them install on devices ([fee2c1c](fee2c1c0d0))
* redesign public podcast and episode pages + remove any information clutter for better ux ([9321400](932140077c))
* replace form helper functions with components in admin template ([e64548b](e64548b982))
* replace slug field with interactive permalink component ([578022b](578022b8c5))
* restyle episode and person cards + add focus style to interactive elements for a11y ([a505a1d](a505a1de56))
* **rss:** add ˂podcast:guid˃ tag for channel ([1fab10e](1fab10eb0d))
* **rss:** add podcast-namespace tags for platforms + previousUrl tag ([dbba8dc](dbba8dc581)), closes [#73](https://code.castopod.org/adaures/castopod/issues/73) [#75](https://code.castopod.org/adaures/castopod/issues/75) [#76](https://code.castopod.org/adaures/castopod/issues/76) [#80](https://code.castopod.org/adaures/castopod/issues/80)
* **rss:** add podcast:comments tag to link to episode comments ([32e8c7c](32e8c7c16a))
* **rss:** add podcast:location tag ([c0a2282](c0a22829bd))
* **rss:** add rss feed route without the `.xml` extension ([94c0b7c](94c0b7c159)), closes [#247](https://code.castopod.org/adaures/castopod/issues/247)
* **rss:** add soundbites according to the podcastindex specs ([6b34617](6b34617d07)), closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
* **rss:** add transcript and chapters support ([e769d83](e769d83a93)), closes [#72](https://code.castopod.org/adaures/castopod/issues/72) [#82](https://code.castopod.org/adaures/castopod/issues/82)
* **rss:** generate rss feed from podcast entity ([c815ecd](c815ecd664))
* **rss:** update monetization tag so that it meets PodcastIndex requirements ([4c7ecbe](4c7ecbee83))
* **select:** enhance select input with choices.js ([910d457](910d457cf8))
* set app parameter forceGlobalSecureRequests = true forcing requests to go through https ([d9dff1b](d9dff1b8bf))
* set podcast / episode description in the pages description meta tag ([1c4a504](1c4a50442b)), closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
* **settings:** add general config for instance (site name, description and icon) ([5c56f3e](5c56f3e6f0))
* **settings:** add theme settings to set an accent color for all public pages ([5c529a8](5c529a83aa))
* simplify podcast page's layout for better ux ([2c0efc6](2c0efc6563))
* **soundbites:** add soundbite list and creation forms with audio-clipper component ([de19317](de19317138))
* style file inputs using tailwind's file class ([8208ab6](8208ab6785))
* **themes:** add ViewThemes library to set views in root themes folder ([7a27676](7a276764e6))
* **themes:** set different default banner per theme ([11c916f](11c916fe43))
* **themes:** set generic css variables for colors to enable instance themes ([a746a78](a746a781b4))
* toggle podcast sidebar on smaller screens ([f0205ec](f0205ec274))
* **transcript:** parse srt subtitles into json file + add max file size info below audio file input ([0098761](00987610a0))
* **ui:** create ViewComponents library to enable building class and view files components ([94872f2](94872f2338))
* update analytics so to meet IABv2 requirements ([03e23a2](03e23a28bf)), closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
* update pine colors + create charts components ([a50abc1](a50abc138d))
* **users:** add myth-auth to handle users crud + add admin gateway only accessible by login ([c63a077](c63a077618)), closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
* **ux:** remove admin dashboard and redirect directly to podcast list ([27c48b8](27c48b8fa9))
* **video-clip:** add video-clip page with video preview + logs ([42538dd](42538dd757))
* **video-clip:** generate video clips in the bg using a cron job + add video clip page + tidy up UI ([db0e427](db0e4272bd))
* **video-clips:** add dimensions for portrait and squared formats ([3af404d](3af404da3d))
* **video-clips:** add new themes + add castopod logo as a watermark ([1d1490b](1d1490b06a))
* **video-clips:** add route for scheduled video clips + list video clips with status ([2065ebb](2065ebbee5))
* **video-clips:** allow episodeNumbering text to stand in the indent of episodeTitle paragraph ([71a063d](71a063dac3))
* **video-clips:** generate a 16:9 video using ffmpeg ([35aa7ea](35aa7ea5d9))
* **video-clips:** generate subtitles clip using transcript json to have subtitles accross video ([3ce07e4](3ce07e455d))
* **video-clips:** replace hardcoded colors with config's theme colors ([e462abf](e462abf6d6))
* **vite:** add vite config to decouple it from CI_ENVIRONMENT ([8721719](8721719cd7))
* write id3v2 tags to episode's audio file ([4651d01](4651d01a84))

### Performance Improvements

* **cache:** update CI4 to use cache's deleteMatching method ([54b84f9](54b84f9684))
* **cache:** use deleteMatching method to prevent forgetting cached elements in models ([76afc0c](76afc0cfa2))
* defer javascript + lazy load images for faster page loads ([f0685e4](f0685e4479))
* **docker:** add redis caching service for development ([05ace8c](05ace8cff2))

### Reverts

* **install:** redirect to install in homepage if no database was set ([73f094d](73f094daf2))
* set deprecated config options back in App config ([433745f](433745f194))
* **soundbites:** remove soundbite table from episode's public page ([5dc0f19](5dc0f19656))
* use basic input file for episodes audio files instead of button for better UX ([d5f22fb](d5f22fbb38))

### BREAKING CHANGES

* **analytics:** analytics_podcasts_by_player table and analytics_podcasts procedure were updated
2022-10-20 08:17:21 +00:00
crowdin
d76a1d9fee chore: new Crowdin updates 2022-10-20 07:55:28 +00:00
Yassine Doghri
07780c5f6f refactor(migrations): set namespace to null to run all migrations during install and updates 2022-10-20 06:48:44 +00:00
Yassine Doghri
b07ac093b2 fix: validate slug length when submitting episode form + clean permalink edit prefix 2022-10-19 14:56:39 +00:00
Yassine Doghri
5a2ca0cc4a fix(install): add password validation when creating super admin 2022-10-19 11:47:26 +00:00
Yassine Doghri
73f094daf2 revert(install): redirect to install in homepage if no database was set 2022-10-19 11:35:08 +00:00
Yassine Doghri
0bab4c7af9 chore: remove testing update migration + rename auth migration 2022-10-19 11:02:05 +00:00
Yassine Doghri
1686f840d1 fix: sort episodes by published_at with unpublished episodes at the begining
set the right permissions for episode's publication date edit

fixes #249
2022-10-18 17:25:49 +00:00
Yassine Doghri
d0836f3ee3 feat: add about page in admin with instance info + database update button 2022-10-18 16:53:51 +00:00
Benjamin Bellamy
c668f1c151 chore(platforms): update amazon and podcastindex submit urls 2022-10-17 15:49:31 +00:00
Yassine Doghri
3a57538572 build: set minimal php version to 8.1
closes #225
2022-10-17 14:17:50 +00:00
Yassine Doghri
c745fd8b28 ci(gitlabci): set base image with php8.0 tag 2022-10-17 14:04:47 +00:00
Romain de Laage
88fb618c28 build(docker): forward server name to the PHP application
fixes #246
2022-10-16 14:32:12 +00:00
Yassine Doghri
7213ed290c feat(auth): add auth.enable2FA config to enable two-factor authentication
+ update phpstan and rector configs
2022-10-16 13:35:48 +00:00
Yassine Doghri
c1287cbe6c refactor(auth): replace myth/auth with codeigniter/shield + define new roles
closes #222
2022-10-16 13:35:26 +00:00
semantic-release-bot
c760acc79d chore(release): 1.0.0-beta.24 [skip ci]
# [1.0.0-beta.24](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2022-10-14)

### Bug Fixes

* **router:** trim URI slash to match same routes for URIs with and without trailing slash ([9e9375f](9e9375f9a2))

### Features

* **episode:** add form to allow editing episode's publication date to a past date ([d783d16](d783d16eb7)), closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
* **rss:** add rss feed route without the `.xml` extension ([94c0b7c](94c0b7c159)), closes [#247](https://code.castopod.org/adaures/castopod/issues/247)
2022-10-14 16:32:44 +00:00
crowdin
d0d8be7fe3 chore: new Crowdin updates 2022-10-14 16:12:14 +00:00
Yassine Doghri
a3ebd6c9a4 refactor(view-components): use CI4's View Decorators to render components 2022-10-14 15:31:16 +00:00
Yassine Doghri
a182d96f18 chore: update CI4 to v4.2.7 + dependencies to latest 2022-10-14 14:56:30 +00:00
Yassine Doghri
d783d16eb7 feat(episode): add form to allow editing episode's publication date to a past date
This allows podcasters to reorganize their published episodes as they see fit

closes #97
2022-10-14 14:37:03 +00:00
Yassine Doghri
94c0b7c159 feat(rss): add rss feed route without the .xml extension
closes #247
2022-10-14 12:50:25 +00:00
Yassine Doghri
9e9375f9a2 fix(router): trim URI slash to match same routes for URIs with and without trailing slash 2022-10-14 12:40:51 +00:00
Romain de Laage
ab330e773e build(docker): add generic beta tag for web-server image 2022-10-01 07:10:37 +00:00
Yassine Doghri
17e75c4439 docs(docker): add generic tag for specific beta version 2022-09-30 17:10:57 +00:00
Yassine Doghri
87c65e601a docs(docker): fix available tags link to be more obvious 2022-09-30 09:51:35 +00:00
Yassine Doghri
32e1dbe996 docs: add new premium podcasts feature 2022-09-30 09:42:24 +00:00
Yassine Doghri
bac210105f docs(docker): add beta tag to supported tags + link to available tags 2022-09-30 09:34:29 +00:00
semantic-release-bot
104438ec5f chore(release): 1.0.0-beta.23 [skip ci]
# [1.0.0-beta.23](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2022-09-29)

### Bug Fixes

* **premium-podcasts:** display unlock button in embed when premium episode ([ca109ba](ca109ba3a8))
* **premium-podcasts:** remove cache in unlock form + redirect to podcast if podcast is not premium ([242352c](242352c4d9))
* **premium-podcasts:** return different cached page when podcast is unlocked ([b1303c5](b1303c5255))

### Features

* add instructions on production error page to ease Castopod debugging process ([9eab54e](9eab54e085)), closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
* add premium podcasts to manage subscriptions for premium episodes ([3234500](3234500e2d)), closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
* **gdpr:** add purpose for granting access to premium content ([47d6d81](47d6d81b79))
2022-09-29 14:40:00 +00:00
Yassine Doghri
47d6d81b79 feat(gdpr): add purpose for granting access to premium content 2022-09-29 14:09:19 +00:00
Yassine Doghri
ca109ba3a8 fix(premium-podcasts): display unlock button in embed when premium episode 2022-09-29 13:34:28 +00:00
Yassine Doghri
242352c4d9 fix(premium-podcasts): remove cache in unlock form + redirect to podcast if podcast is not premium 2022-09-29 11:10:39 +00:00
Yassine Doghri
b1303c5255 fix(premium-podcasts): return different cached page when podcast is unlocked
- clear podcast cache when setting subscription link
- update and add missing translation keys
2022-09-29 10:52:28 +00:00
Yassine Doghri
65173e5180 build(docker): set beta tag to production images for latest beta version 2022-09-28 15:18:05 +00:00
Yassine Doghri
3234500e2d feat: add premium podcasts to manage subscriptions for premium episodes
closes #193
2022-09-28 15:02:09 +00:00
Yassine Doghri
b6114d3d93 chore: update rector to latest and use parallel for faster processing
update composer dependencies to latest
2022-09-28 14:00:46 +00:00
Yassine Doghri
9eab54e085 feat: add instructions on production error page to ease Castopod debugging process
closes #224
2022-09-28 12:49:36 +00:00
Yassine Doghri
b2db783c8e docs(install): add email/smtp configuration options
closes #147
2022-09-26 14:55:00 +00:00
semantic-release-bot
eabe3f3b97 chore(release): 1.0.0-beta.22 [skip ci]
# [1.0.0-beta.22](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2022-09-23)

### Bug Fixes

* **fediverse:** set default castopod avatar url when actor avatar is not present ([460f52f](460f52f70e))
* **import:** set default episode type if not set ([d7250ab](d7250ab03f))
* **input-component:** unset required attribute to prevent rendering it when false ([db9ac13](db9ac13860))
* **notifications:** notify actors after activities insert / update using model callback methods ([e08555a](e08555a4e9))
* overwrite getActorById to return app's Actor entity ([f2bc2f7](f2bc2f7e01))
* remove heavy image cover data from audio file metadata ([f74403b](f74403bd7a))
* set storage limit as disk_total_space instead of free space ([7512e2e](7512e2ed1f))
* **ui:** remove empty tooltip when hovering on sponsor button ([40aa661](40aa661289))
* **users:** remove required roles input when editing user + prevent owner's roles from being edited ([1c8af75](1c8af7550b)), closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
* **ux:** have podcast dashboard card link to podcast dashboard if only one podcast in instance ([7dabee5](7dabee58a1))
2022-09-23 16:20:14 +00:00
crowdin
7b66f46ae2 chore: new Crowdin updates 2022-09-23 15:51:26 +00:00
Yassine Doghri
db9ac13860 fix(input-component): unset required attribute to prevent rendering it when false 2022-09-23 11:59:47 +00:00
Yassine Doghri
f74403bd7a fix: remove heavy image cover data from audio file metadata 2022-09-23 10:10:21 +00:00
Romain de Laage
e98ec8c950 build(docker): add email configuration for the docker image 2022-09-19 13:04:49 +00:00
Yassine Doghri
d7250ab03f fix(import): set default episode type if not set 2022-09-19 10:58:56 +00:00
Yassine Doghri
ddfa2965c3 build: change ffmpeg static release link to a specific version (5.1.1) 2022-09-14 15:47:42 +00:00
Yassine Doghri
94fae92e9a build(docker): extract ffmpeg tar directly into the ffmpeg folder
This fixes a bug where the mv ffmpeg command breaks if the upstream folder name of the ffmpeg tar
changes (version change).
2022-09-14 14:45:57 +00:00
Yassine Doghri
f2bc2f7e01 fix: overwrite getActorById to return app's Actor entity 2022-09-14 10:21:14 +00:00
Yassine Doghri
40aa661289 fix(ui): remove empty tooltip when hovering on sponsor button 2022-09-08 13:37:10 +00:00
Yassine Doghri
7dabee58a1 fix(ux): have podcast dashboard card link to podcast dashboard if only one podcast in instance 2022-09-08 13:34:17 +00:00
Yassine Doghri
8c4e02e953 build: add spark to Castopod bundle for handling CLI requests
useful to run commands and create CLI scripts
2022-09-08 12:50:25 +00:00
Yassine Doghri
e08555a4e9 fix(notifications): notify actors after activities insert / update using model callback methods
--> Remove sql triggers because most shared hosting plans prevent using them
2022-09-08 11:50:23 +00:00
Yassine Doghri
460f52f70e fix(fediverse): set default castopod avatar url when actor avatar is not present 2022-09-07 10:04:02 +00:00
Yassine Doghri
1c8af7550b fix(users): remove required roles input when editing user + prevent owner's roles from being edited
fixes #239
2022-09-07 09:41:58 +00:00
Yassine Doghri
7512e2ed1f fix: set storage limit as disk_total_space instead of free space 2022-09-07 08:27:28 +00:00
Yassine Doghri
707dd0800e docs(install): add missing user privileges for database 2022-09-06 16:21:26 +00:00
Yassine Doghri
8c4f0675c1 build(docker): add missing $ to get CP_VERSION's environment variable 2022-09-06 16:19:06 +00:00
semantic-release-bot
3bb7572abb chore(release): 1.0.0-beta.21 [skip ci]
# [1.0.0-beta.21](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2022-09-06)

### Bug Fixes

* **email:** set the correct url in the activation and forgot emails ([10fc6f1](10fc6f17c6)), closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
* **notifications:** add trigger after activities update + update insert trigger ([e5d16e8](e5d16e8711))

### Features

* **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca) locales ([48d1443](48d1443472))
2022-09-06 15:49:52 +00:00
Yassine Doghri
355f3bf674 docs(all-contributors): add irithys, caos30 and ghose as translators 2022-09-06 15:30:07 +00:00
Yassine Doghri
48d1443472 feat(i18n): add support for Simplified Chinese (zh-Hans) and Catalan (ca) locales 2022-09-06 15:30:07 +00:00
crowdin
3fc9734161 chore: new Crowdin updates 2022-09-06 14:55:46 +00:00
Yassine Doghri
f3c1ddb3b1 chore: update CI4 to 4.2.6 + composer and npm dependencies to latest 2022-09-06 12:25:43 +00:00
Yassine Doghri
2a2cb3efdb chore: upgrade CI4 to v4.2.5 + update other dependencies to latest
- add file associations for env and .rsync-filter files
- include version lens to .devcontainer config
2022-08-31 10:50:19 +00:00
Yassine Doghri
10fc6f17c6 fix(email): set the correct url in the activation and forgot emails
- add default values for Config/Email properties
- set mariadb to the minimal required version for development

fixes #204
2022-08-24 09:45:36 +00:00
Romain de Laage
029804b9f8 build(docker): add build for tagged versions on main, beta and alpha 2022-08-23 09:50:00 +00:00
crowdin
c4a07c29f1 chore: new Crowdin updates 2022-08-22 15:56:51 +00:00
Yassine Doghri
37f086075e chore: update CodeIgniter to 4.2.4 + other dependencies to latest 2022-08-22 15:36:29 +00:00
Ola Hneini
e5d16e8711 fix(notifications): add trigger after activities update + update insert trigger 2022-08-15 10:06:45 +00:00
semantic-release-bot
9222975856 chore(release): 1.0.0-beta.20 [skip ci]
# [1.0.0-beta.20](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2022-08-12)

### Bug Fixes

* add underline and semibold font weight for prose links to have them stand out ([d4d8671](d4d867121c))
* **router:** check if Accept header is set before getting value ([10a2ae0](10a2ae0248)), closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
* **search-episodes:** add fallback sql query using LIKE for search query with less than 4 characters ([e66bf44](e66bf44341)), closes [#236](https://code.castopod.org/adaures/castopod/issues/236)
* set interact_as_actor for user upon password reset ([ad8f5f5](ad8f5f5a0f)), closes [#178](https://code.castopod.org/adaures/castopod/issues/178)

### Features

* add label to sponsor button on podcast page ([c29c018](c29c018c7a)), closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
* add notifications inbox for actors ([999999e](999999e3ef)), closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
2022-08-12 16:37:29 +00:00
Ola Hneini
999999e3ef feat: add notifications inbox for actors
closes #215
2022-08-12 16:08:09 +00:00
root
c29c018c7a feat: add label to sponsor button on podcast page
closes #162
2022-08-12 16:08:09 +00:00
Ola Hneini
ad8f5f5a0f fix: set interact_as_actor for user upon password reset
closes #178
2022-08-12 16:06:26 +00:00
Yassine Doghri
10a2ae0248 fix(router): check if Accept header is set before getting value
fix deprecation warnings for getHeader and getHeaders methods

refs #228
2022-08-01 10:13:16 +00:00
Yassine Doghri
e66bf44341 fix(search-episodes): add fallback sql query using LIKE for search query with less than 4 characters
fixes #236
2022-07-28 15:11:25 +00:00
Yassine Doghri
d4d867121c fix: add underline and semibold font weight for prose links to have them stand out
+ fix typo: "whitespace-no-wrap" --> "whitespace-nowrap"
2022-07-22 16:45:55 +00:00
semantic-release-bot
fab916ee8a chore(release): 1.0.0-beta.19 [skip ci]
# [1.0.0-beta.19](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2022-07-21)

### Bug Fixes

* **episode-unpublish:** set consistent posts_counts' increments/decrements for actors and episodes ([8acdafd](8acdafd260)), closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
* **get_browser_language:** return defaultLocale if browser doesn't send user preferred language ([9cc2996](9cc2996261))

### Features

* **episode-unpublish:** remove episode comments upon unpublish ([78acd7f](78acd7f5c0))
2022-07-21 17:04:07 +00:00
Yassine Doghri
a40d636cfa docs(all-contributors): update contributors list 2022-07-21 16:49:16 +00:00
Yassine Doghri
78acd7f5c0 feat(episode-unpublish): remove episode comments upon unpublish 2022-07-21 16:37:22 +00:00
Yassine Doghri
8acdafd260 fix(episode-unpublish): set consistent posts_counts' increments/decrements for actors and episodes
Some episodes could not be unpublished because of out of range error when removing posts.

fixes #233
2022-07-21 13:56:10 +00:00
Yassine Doghri
9cc2996261 fix(get_browser_language): return defaultLocale if browser doesn't send user preferred language 2022-07-18 16:07:44 +00:00
crowdin
b5263107da chore: new Crowdin updates 2022-07-14 09:03:44 +00:00
Romain de Laage
78152d8412 docs: complete docker page and translate it to french
closes #230
2022-07-11 10:36:57 +00:00
Yassine Doghri
b7e2c09297 docs(docker): add page describing castopod's docker images and example usage with docker-compose
refs #230
2022-07-08 16:40:22 +00:00
semantic-release-bot
455efae00f chore(release): 1.0.0-beta.18 [skip ci]
# [1.0.0-beta.18](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2022-07-07)

### Bug Fixes

* **player-styling:** revert vite to 2.8 to reference the player css ([e07d3af](e07d3afea9))

### Features

* add legalNoticeURL to app config for setting an external url to legal notice ([711843a](711843a0c8))
2022-07-07 17:37:01 +00:00
Yassine Doghri
e07d3afea9 fix(player-styling): revert vite to 2.8 to reference the player css
A bug in the latest vite release (v2.9.13) prevents the player styling
from being referenced in the manifest.
2022-07-07 17:19:02 +00:00
Yassine Doghri
8cbc902a75 docs(contributions): recognize any kind of contribution with all-contributors spec
- add contributors list to readme and docs' index page
- add CONTRIBUTING.md file redirecting to docs website
- add "translate castopod" section to guidelines
2022-07-07 16:41:34 +00:00
Yassine Doghri
711843a0c8 feat: add legalNoticeURL to app config for setting an external url to legal notice 2022-07-07 10:08:04 +00:00
semantic-release-bot
98a8a480a7 chore(release): 1.0.0-beta.17 [skip ci]
# [1.0.0-beta.17](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2022-07-06)

### Bug Fixes

* explicitly cast seconds to int in iso8601_duration helper function ([779653f](779653f75b))
* **housekeeping:** use EpisodeModel's builder to reset comments count ([65e9c0b](65e9c0b05e))
* **rss:** round episode durations and soundbites ([c9fb987](c9fb987fcf)), closes [#214](https://code.castopod.org/adaures/castopod/issues/214)
* **xml-editor:** prettify xml even without root node ([ca55c24](ca55c248d0))

### Features

* add publish feature for podcasts and set draft by default ([3d363f2](3d363f2efe)), closes [#128](https://code.castopod.org/adaures/castopod/issues/128) [#220](https://code.castopod.org/adaures/castopod/issues/220)
* **admin:** add instance wide dashboard with storage and bandwidth usage ([b1a6c02](b1a6c02e56)), closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
* **datetime-picker:** set material_green theme to flatpickr ([3ce6541](3ce6541003))
2022-07-06 15:47:53 +00:00
Yassine Doghri
b1a6c02e56 feat(admin): add instance wide dashboard with storage and bandwidth usage
* add DashboardCard component
* add instance wide podcasts and episodes numbers
* add app.storageLimit environment variable
* divide bytes by 1000 instead of 1024 in stats sql queries

closes #216
2022-07-06 15:29:15 +00:00
Ola Hneini
3d363f2efe feat: add publish feature for podcasts and set draft by default
closes #128, #220
2022-07-05 16:39:20 +00:00
crowdin
9843ce3882 chore: new Crowdin updates 2022-07-04 13:27:21 +00:00
Yassine Doghri
3ce6541003 feat(datetime-picker): set material_green theme to flatpickr
upgrade docker dev's php version to 8.1
2022-07-04 12:52:55 +00:00
Yassine Doghri
81f790868a chore: update quality tools + rewrite some migration files
* update php packages to latest
* update rector and ecs config
* update ci4/settings package to v2.1
2022-07-03 16:42:20 +00:00
Yassine Doghri
ca55c248d0 fix(xml-editor): prettify xml even without root node
- update js dependencies to latest
- add indentWithTab to xml-editor
- tailwindcss 3.1: replace withOpacity with <alpha-value>
2022-07-03 08:07:20 +00:00
Yassine Doghri
c9fb987fcf fix(rss): round episode durations and soundbites
refs #214
2022-07-02 11:04:16 +00:00
Yassine Doghri
779653f75b fix: explicitly cast seconds to int in iso8601_duration helper function
episode page was unaccessible on with php8.1 and dev mode
2022-07-02 10:37:55 +00:00
Yassine Doghri
3b73c67250 refactor(rest-api): move rest api's enabled flag to the RestApi config 2022-06-29 16:22:10 +00:00
Yassine Doghri
68777dd54c refactor(docker): rename CP_HOST_BACK to CP_APP_HOSTNAME 2022-06-27 15:31:35 +00:00
Yassine Doghri
65e9c0b05e fix(housekeeping): use EpisodeModel's builder to reset comments count 2022-06-27 14:56:08 +00:00
Romain de Laage
0c0730be69 build(docker): add production-ready DockerFiles
- add docker images for app (castopod) and web-server (nginx)
- ci: deploy images continuously to a docker hub using kaniko
- ci: trigger docker-build-rolling on develop branch
- move development DockerFile to docker directory

closes #200
2022-06-26 12:03:22 +00:00
semantic-release-bot
de8b1df948 chore(release): 1.0.0-beta.16 [skip ci]
# [1.0.0-beta.16](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2022-06-24)

### Bug Fixes

* change image size requirement hints ([ea20206](ea20206ee6))

### Features

* add update rss feed feature for podcasts to import their latest episodes ([5eb9dc1](5eb9dc168e)), closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
* **admin:** add search form in podcast episodes list ([6be5d12](6be5d12877)), closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
* **api:** add rest api with podcasts read endpoints ([e64001d](e64001d006)), closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
2022-06-24 08:06:30 +00:00
Yassine Doghri
a37e407483 chore: create test database on mariadb init + harmonize test database credentials 2022-06-22 14:28:58 +00:00
Sebastian Janik
e64001d006 feat(api): add rest api with podcasts read endpoints
relates to #210
2022-06-22 11:06:53 +00:00
root
ea20206ee6 fix: change image size requirement hints 2022-06-17 16:49:29 +00:00
root
5eb9dc168e feat: add update rss feed feature for podcasts to import their latest episodes
closes #183
2022-06-17 16:46:59 +00:00
Yassine Doghri
6be5d12877 feat(admin): add search form in podcast episodes list
closes #26
2022-06-17 12:52:36 +00:00
Yassine Doghri
9f4a467ad4 chore: update CI4 to 4.2.1 2022-06-17 08:14:39 +00:00
semantic-release-bot
cca973e9c2 chore(release): 1.0.0-beta.15 [skip ci]
# [1.0.0-beta.15](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-06-14)

### Bug Fixes

* replace deletedField with published_at for episodes ([14d7d07](14d7d07822))

### Features

* add default icons to Alert component ([0d98001](0d9800123b))
* add permanent delete feature for podcasts 🎉 ([dbb4030](dbb4030da4)), closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
* apply colour theme to embed player ([9548337](9548337a7c)), closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
* **episodes:** replace soft delete with permanent delete ([eb9ff52](eb9ff522c2))
2022-06-14 13:35:04 +00:00
Yassine Doghri
14d7d07822 fix: replace deletedField with published_at for episodes
- remove delete_at field + soft delete for media and pages
- update CodeIgniter4 to 4.2.0 + update all starter files
- explicitly use builder() when creating queries from model
2022-06-13 16:30:34 +00:00
Ola Hneini
dbb4030da4 feat: add permanent delete feature for podcasts 🎉
closes #89
2022-06-07 11:13:06 +00:00
Jonas S
9548337a7c feat: apply colour theme to embed player
closes #201
2022-05-31 17:15:52 +00:00
Ola Hneini
0d9800123b feat: add default icons to Alert component 2022-05-16 11:57:39 +00:00
Ola Hneini
eb9ff522c2 feat(episodes): replace soft delete with permanent delete
+ add constraint to prevent deleting an episode when published
2022-05-05 15:48:16 +00:00
Ola Hneini
0345728739 docs: add note in getting started install section 2022-05-03 10:21:03 +00:00
semantic-release-bot
3240ed4177 chore(release): 1.0.0-beta.14 [skip ci]
# [1.0.0-beta.14](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-04-23)

### Bug Fixes

* **home:** remove hardcoded prefix in getAllPodcasts query ([92d5cc5](92d5cc50a3))
* overwrite common lang function to escape returned string ([4c490c1](4c490c15bb)), closes [#196](https://code.castopod.org/adaures/castopod/issues/196) [#198](https://code.castopod.org/adaures/castopod/issues/198)

### Features

* **i18n:** add Spanish to supported locales ([e340b54](e340b54a84))
2022-04-23 08:32:31 +00:00
Yassine Doghri
eff5d2fca6 docs: update french documentation 2022-04-22 15:59:22 +00:00
Yassine Doghri
e340b54a84 feat(i18n): add Spanish to supported locales 2022-04-22 09:51:59 +00:00
crowdin
cdc2e2edfb chore: new Crowdin updates 2022-04-22 07:56:31 +00:00
Yassine Doghri
4c490c15bb fix: overwrite common lang function to escape returned string
closes #196, fixes #198
2022-04-21 12:54:19 +00:00
Yassine Doghri
92d5cc50a3 fix(home): remove hardcoded prefix in getAllPodcasts query 2022-04-16 08:36:28 +00:00
Yassine Doghri
b5c0b72093 docs: add French language 2022-04-15 12:45:32 +00:00
Yassine Doghri
01640fa161 chore: new Crowdin updates
+ update rector config for crowdin compatibility
2022-04-15 12:16:12 +00:00
Yassine Doghri
204fe208ff chore: update crowdin.yml config 2022-04-15 11:54:33 +00:00
Yassine Doghri
e9adc216e4 docs: add Brazilian Portuguese and Norwegian Nynorsk languages 2022-04-15 09:47:48 +00:00
semantic-release-bot
e787b6b6e7 chore(release): 1.0.0-beta.13 [skip ci]
# [1.0.0-beta.13](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-04-14)

### Bug Fixes

* **rss:** remove escaping for publisher and owner name ([e2046e4](e2046e4b11))
* use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries ([853a6ba](853a6ba915))

### Features

* **i18n:** add Norwegian Nynorsk to supported locales ([744340d](744340df61))
2022-04-14 15:31:04 +00:00
Yassine Doghri
ced61fc236 feat(i18n): add Norwegian Nynorsk to supported locales 2022-04-14 15:04:09 +00:00
Yassine Doghri
1cbc7de623 chore: new Crowdin updates 2022-04-14 14:54:13 +00:00
Yassine Doghri
4e22a0d5e4 fix: use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries 2022-04-14 14:33:53 +00:00
Yassine Doghri
2306df1c98 ci: add path to i18n-filter script 2022-04-14 13:21:50 +00:00
Yassine Doghri
5564f77502 chore: new Crowdin updates 2022-04-14 13:11:07 +00:00
Yassine Doghri
b870ce55bc ci(gitlabci): filter out docs src languages not declared in .i18n-filter file before build 2022-04-14 12:09:40 +00:00
Yassine Doghri
79c553cbad chore: update Crowdin configuration file 2022-04-09 14:02:46 +00:00
Yassine Doghri
6fc6347846 fix(rss): remove escaping for publisher and owner name 2022-04-06 08:42:06 +00:00
semantic-release-bot
366eaeb09e chore(release): 1.0.0-beta.12 [skip ci]
# [1.0.0-beta.12](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-04-05)

### Bug Fixes

* update form_textarea to prevent escaping value ([78548b5](78548b5cd7))

### Features

* **i18n:** add support for German and Brazilian Portuguese languages ([19da003](19da003fd3))
2022-04-05 16:57:10 +00:00
Yassine Doghri
c9b9fe4ee8 feat(i18n): add support for German and Brazilian Portuguese languages
rename pt to pt-BR for brazilian portuguese
2022-04-05 16:23:47 +00:00
Yassine Doghri
7b71f7971d chore: new Crowdin updates
otetranome - Portuguese, Brazilian; Portuguese
forght - German
glottis0q - German
ernestoacostame - Spanish
Lucian I. Last (lil5) - Dutch
ButterflyOfFire (BoFFire) - Arabic
eorn - Breton
Angelos Chouvardas (achouvardas) - Greek
Russian Retro (retrograde) - Russian
cExplorer - German
GabiSnow - Spanish
CTHTC - German
Romain de Laage (rdelaage) - Spanish
LuuzViir (Spanish)
Samuel Roland (samuelroland) - French
thelama - Portuguese
2022-04-05 15:45:54 +00:00
Yassine Doghri
78548b5cd7 fix: update form_textarea to prevent escaping value
replace form_markdown_textarea with form_textarea
2022-04-05 12:08:56 +00:00
semantic-release-bot
73ad94c1d5 chore(release): 1.0.0-beta.11 [skip ci]
# [1.0.0-beta.11](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-04-01)

### Bug Fixes

* change message upon cancellation of episode publication ([9859c74](9859c7434c))
* prefill description footer input when creating a new episode ([9ea5ca3](9ea5ca3169))
* remove value escaping for form inputs and textareas ([bc6dea2](bc6dea2f8a))
* restore default podcast icon on public website ([342778b](342778bac3))
* **socialinteract:** move social interact uri into uri attribute + update social data upon import ([12b2200](12b22008a2))

### Features

* **analytics-gdpr:** update cached personal data to expire at midnight ([0188b67](0188b67354))
* **analytics:** add current date and secret salt to analytics hash for improved privacy ([6f2e7c0](6f2e7c009c))
* **i18n:** add 7 new languages + update german translations ([d021abb](d021abb52f))
* **i18n:** add german language as supported locale + create Language files from english source ([c220b31](c220b310ed))
* **icons:** add podnews icon to podcasting platforms ([5f42355](5f423557c2)), closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
2022-04-01 16:45:29 +00:00
Benjamin Bellamy
4274cb5d21 docs(gdpr.txt): add purpose block for analytics data 2022-04-01 16:35:34 +00:00
Benjamin Bellamy
0188b67354 feat(analytics-gdpr): update cached personal data to expire at midnight 2022-04-01 16:35:34 +00:00
Yassine Doghri
df8506bb40 build: filter out unsupported locales from the castopod bundle + remove german as supported locale
- add .rsync-filter files to Language folders in order to filter out unsupported locales
- remove
german as supported locale, awaiting completed translation files
- remove unneeded app/Resources/
folder from bundle
2022-04-01 16:35:34 +00:00
Benjamin Bellamy
6f2e7c009c feat(analytics): add current date and secret salt to analytics hash for improved privacy 2022-04-01 16:35:34 +00:00
crowdin
d021abb52f feat(i18n): add 7 new languages + update german translations
add Breton, Dutch, Indonesian, Italian, Portuguese, Spanish and Swedish
2022-04-01 16:35:34 +00:00
Yassine Doghri
5f423557c2 feat(icons): add podnews icon to podcasting platforms
closes #190
2022-04-01 16:35:34 +00:00
Yassine Doghri
5afbf3e964 chore(ci): fix quote escaping for rector 2022-04-01 16:35:34 +00:00
Yassine Doghri
12b22008a2 fix(socialinteract): move social interact uri into uri attribute + update social data upon import 2022-04-01 16:35:34 +00:00
Yassine Doghri
c220b310ed feat(i18n): add german language as supported locale + create Language files from english source 2022-04-01 16:35:34 +00:00
crowdin
e985a193dc chore(i18n): update French translations 2022-04-01 16:35:34 +00:00
crowdin
f3db223e33 chore: new Crowdin updates 2022-04-01 16:35:34 +00:00
Yassine Doghri
5c7732e7fd style(ecs): skip SingleQuoteFixer for language files 2022-04-01 16:35:34 +00:00
Yassine Doghri
ddf720c9a7 chore(crowdin): escape single quotes with backslash 2022-04-01 16:35:34 +00:00
Yassine Doghri
b27ce3d901 docs(l10n): add crowdin badge to docs + append message for crowdin commits 2022-04-01 16:35:34 +00:00
Yassine Doghri
bc6dea2f8a fix: remove value escaping for form inputs and textareas 2022-04-01 16:35:34 +00:00
Ola Hneini
9ea5ca3169 fix: prefill description footer input when creating a new episode 2022-04-01 16:35:34 +00:00
Crowdin
2b688f2184 chore: add Crowdin configuration file 2022-04-01 16:35:34 +00:00
Yassine Doghri
357886b931 refactor(translations): sort french translation keys for Countries.php in admin module 2022-04-01 16:35:34 +00:00
Yassine Doghri
abbe449c64 docs: update features to add WebSub and polish language + add exif requirements to install
closes #185
2022-04-01 16:35:34 +00:00
Yassine Doghri
82ccd9cdce refactor(rss): declare atom namespace in root rss element 2022-04-01 16:35:34 +00:00
Yassine Doghri
8646b4d88e chore: uncomment modules, tests and public paths for rector 2022-04-01 16:35:34 +00:00
Ola Hneini
9859c7434c fix: change message upon cancellation of episode publication 2022-04-01 16:35:34 +00:00
Yassine Doghri
47b972b63c chore: escape podcasting platform slug when getting icon 2022-04-01 16:35:34 +00:00
Ola Hneini
342778bac3 fix: restore default podcast icon on public website 2022-04-01 16:35:34 +00:00
semantic-release-bot
086a79c9de chore(release): 1.0.0-beta.10 [skip ci]
# [1.0.0-beta.10](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-03-15)

### Bug Fixes

* add explicit int conversion when formatting episode duration ([1253096](1253096197))
* add href to castopod website on login page ([cc54257](cc54257351))
* move html escaping on credits page ([fbffdbd](fbffdbde78))
* remove cache from remote follow form to display error messages ([90e4443](90e44437bd))

### Features

* add autofocus to input field "Email or username" on login page ([19caed4](19caed4bce))
* add WebSub module for pushing feed updates to open hubs ([10d3f73](10d3f73786))
* **GDPR:** add GDPR.yml file to public/.well-known/ ([86bccc3](86bccc3d5c))
2022-03-15 17:22:56 +00:00
Benjamin Bellamy
86bccc3d5c feat(GDPR): add GDPR.yml file to public/.well-known/ 2022-03-15 17:00:34 +00:00
Yassine Doghri
10d3f73786 feat: add WebSub module for pushing feed updates to open hubs 2022-03-15 16:47:35 +00:00
Yassine Doghri
1253096197 fix: add explicit int conversion when formatting episode duration 2022-03-15 15:50:07 +00:00
Ola Hneini
fbffdbde78 fix: move html escaping on credits page 2022-03-15 15:23:13 +00:00
Yassine Doghri
90e44437bd fix: remove cache from remote follow form to display error messages 2022-03-15 14:05:19 +00:00
Yassine Doghri
4a009de531 docs: add FUNDING.yml file to set up github sponsor button 2022-03-14 17:04:22 +00:00
Ola Hneini
19caed4bce feat: add autofocus to input field "Email or username" on login page 2022-03-10 11:00:17 +00:00
Benjamin Bellamy
e8076820c9 docs(gdpr): add GDPR.txt file following the gdpr-txt.org spec 2022-03-09 16:09:02 +00:00
Ola Hneini
cc54257351 fix: add href to castopod website on login page 2022-03-09 14:34:17 +00:00
semantic-release-bot
01041775e0 chore(release): 1.0.0-beta.9 [skip ci]
# [1.0.0-beta.9](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-03-04)

### Bug Fixes

* **cache:** delete posts and comments pages cache when updating platform links ([f7c3e5b](f7c3e5bf4a)), closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
* escape characters for `min` in format_duration_symbol ([3b6722a](3b6722a42b))
* **security:** add csrf filter + prevent xss attacks by escaping user input ([cd2e1e1](cd2e1e1dc3))
* update ivoox podcasting icon ([f2b69a4](f2b69a4733))
* **video-clips:** update condition to check if ffmpeg is installed ([b57f0b6](b57f0b6eb6)), closes [#163](https://code.castopod.org/adaures/castopod/issues/163)

### Features

* **i18n:** add Polish translation ([2d83b44](2d83b44add))
* **icons:** add default icons for podcasting, social and funding platforms + remove complex icons ([5bcdfeb](5bcdfebe64)), closes [#166](https://code.castopod.org/adaures/castopod/issues/166) [#167](https://code.castopod.org/adaures/castopod/issues/167) [#170](https://code.castopod.org/adaures/castopod/issues/170)
* make episode description more visible on episode pages ([90533be](90533be029)), closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
* **podcasting 2.0:** update podcast:social tag to adhere to latest spec ([a597cf4](a597cf4ecf))
2022-03-04 18:14:17 +00:00
Patryk Miś
2d83b44add feat(i18n): add Polish translation 2022-03-04 17:52:52 +00:00
Yassine Doghri
f2b69a4733 fix: update ivoox podcasting icon 2022-03-04 16:06:03 +00:00
Yassine Doghri
b57f0b6eb6 fix(video-clips): update condition to check if ffmpeg is installed
fixes #163
2022-03-04 15:40:38 +00:00
Yassine Doghri
4716a4a5b9 docs: add contributions welcome badge linking to castopod issues 2022-03-04 14:57:58 +00:00
Yassine Doghri
cd2e1e1dc3 fix(security): add csrf filter + prevent xss attacks by escaping user input
- update CI4 to v4.1.9's stable production package
- update php and js dependencies to latest
2022-03-04 14:36:32 +00:00
Yassine Doghri
a597cf4ecf feat(podcasting 2.0): update podcast:social tag to adhere to latest spec 2022-03-03 16:15:42 +00:00
Yassine Doghri
90533be029 feat: make episode description more visible on episode pages
- double the height of the episode description on episode pages
- reduce overlay fading by 70% to
make it more subtle

closes #171
2022-02-24 16:32:50 +00:00
Yassine Doghri
f7c3e5bf4a fix(cache): delete posts and comments pages cache when updating platform links
remove cache when adding comment and replies as well

fixes #169
2022-02-24 16:08:24 +00:00
Yassine Doghri
5bcdfebe64 feat(icons): add default icons for podcasting, social and funding platforms + remove complex icons
- add pleroma, misskey and fosspay platforms
- update adaures/ipcat-php to fix autoload issue
-
fix Breez and Podcast Guru icons

closes #166, #167, #170
2022-02-24 14:48:17 +00:00
Bastien Luneteau
3b6722a42b fix: escape characters for min in format_duration_symbol 2022-02-22 15:43:27 +00:00
Yassine Doghri
5e5e9b2b3f ci(gitlabci): skip bundle job for forked projects 2022-02-22 15:16:37 +00:00
Yassine Doghri
4a9ed75435 ci(gitlabci): skip running lint-commit-msg job during merge requests 2022-02-22 14:13:06 +00:00
Yassine Doghri
c16cf700ba build: update adaures/ipcat-php and adaures/podcast-persons-taxonomy references in composer.lock 2022-02-20 14:54:21 +00:00
Yassine Doghri
88858042d2 ci(gitlabci): trigger deployment of the castopod.org website after release 2022-02-20 14:32:35 +00:00
Yassine Doghri
d093530e14 docs: add security section to contributing guidelines 2022-02-20 14:27:04 +00:00
Yassine Doghri
f58f712aa6 chore: replace group slug from ad-aures to adaures in repo url
this fixes the adblocker loading viewer error
2022-02-20 13:23:57 +00:00
Yassine Doghri
3c89bc15ef docs(readme): update sponsors section + add license section 2022-02-20 10:04:42 +00:00
Yassine Doghri
b3888c2c7f docs: add open-graph meta tags + edit pages link
- rename castopod icons and logos
- fix nlnet logo link
- add castopod icon as logo
- set twitter
card to summary_large_image
- update README.md
2022-02-19 19:09:11 +00:00
Yassine Doghri
fa5b5f51a4 docs(init): create documentation website using vitepress
- rename podlibre to adaures
- rename castopod-host to castopod
- simplify README and redirect to docs site
- move INSTALL and UPDATE docs
- add new gitlabci pipeline to deploy docs
- upgrade node to v16 in Dockerfile
2022-02-19 16:06:11 +00:00
semantic-release-bot
472842fae6 chore(release): 1.0.0-beta.8 [skip ci]
# [1.0.0-beta.8](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-02-10)

### Features

* **podcast-form:** add new_feed_url field to set an url when changing domain or host ([e7eec48](e7eec48e7b))
2022-02-10 11:19:13 +00:00
Yassine Doghri
e7eec48e7b feat(podcast-form): add new_feed_url field to set an url when changing domain or host 2022-02-09 17:44:24 +00:00
semantic-release-bot
634961bcaa chore(release): 1.0.0-beta.7 [skip ci]
# [1.0.0-beta.7](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-02-05)

### Bug Fixes

* **activitypub:** allow cors on get requests for routes exposing acitivitypub objects ([2f24809](2f2480998f))
* **fediverse:** set model instances as non shared to prevent overlapping ([91128fa](91128fad7a))
* **htaccess:** add ? after index.php in RewriteRule ([d9d139e](d9d139eefa)), closes [#152](https://code.podlibre.org/podlibre/castopod-host/issues/152)

### Features

* **home:** sort podcasts by recent activity + add dropdown menu to choose between sorting options ([7b89da6](7b89da6106)), closes [#164](https://code.podlibre.org/podlibre/castopod-host/issues/164)
2022-02-05 17:18:45 +00:00
Yassine Doghri
7b89da6106 feat(home): sort podcasts by recent activity + add dropdown menu to choose between sorting options
fixes #164
2022-02-05 16:57:20 +00:00
Yassine Doghri
91128fad7a fix(fediverse): set model instances as non shared to prevent overlapping 2022-02-05 11:40:30 +00:00
Yassine Doghri
d9d139eefa fix(htaccess): add ? after index.php in RewriteRule
fixes #152
2022-02-05 11:02:25 +00:00
Yassine Doghri
2f2480998f fix(activitypub): allow cors on get requests for routes exposing acitivitypub objects 2022-02-05 10:57:02 +00:00
semantic-release-bot
412cf14604 chore(release): 1.0.0-beta.6 [skip ci]
# [1.0.0-beta.6](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-02-03)

### Bug Fixes

* **activitypub:** add conditions for possibly missing actor properties + add user-agent to requests ([8fbf948](8fbf948fbb))
* **activitypub:** add target actor id to like / announce activities to send directly to note's actor ([962dd30](962dd305f5))
* **activitypub:** add target_actor_id for create activity to broadcast post reply ([0128a21](0128a21ec5))
* **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be sent in any order ([b7f285e](b7f285e4e2))
* **install:** set message block on forms to show error messages ([3a0a20d](3a0a20d59c)), closes [#157](https://code.podlibre.org/podlibre/castopod-host/issues/157)
* **markdown-editor:** remove unnecessary buttons for podcast and episode editors + add extensions ([9c4f60e](9c4f60e00b))
* **podcast-activity:** check if transcript and chapters are set before including them in audio ([5855a25](5855a25093))
* **podcast:** use markdown description value for editor + set prose class to about description ([f304d97](f304d97b14)), closes [#156](https://code.podlibre.org/podlibre/castopod-host/issues/156)
2022-02-03 14:39:13 +00:00
Yassine Doghri
5855a25093 fix(podcast-activity): check if transcript and chapters are set before including them in audio 2022-02-03 14:12:47 +00:00
Yassine Doghri
0128a21ec5 fix(activitypub): add target_actor_id for create activity to broadcast post reply 2022-02-03 14:12:47 +00:00
Yassine Doghri
962dd305f5 fix(activitypub): add target actor id to like / announce activities to send directly to note's actor 2022-02-03 14:12:47 +00:00
Yassine Doghri
9c4f60e00b fix(markdown-editor): remove unnecessary buttons for podcast and episode editors + add extensions
update CommonMark to v2 + add Autolink, SmartPunct and DisallowedRawHtml extensions
2022-02-03 14:12:47 +00:00
Yassine Doghri
f304d97b14 fix(podcast): use markdown description value for editor + set prose class to about description
fixes #156
2022-02-03 14:12:47 +00:00
Yassine Doghri
8fbf948fbb fix(activitypub): add conditions for possibly missing actor properties + add user-agent to requests 2022-02-03 14:12:36 +00:00
Yassine Doghri
3a0a20d59c fix(install): set message block on forms to show error messages
fixes #157
2022-02-03 10:15:59 +00:00
Yassine Doghri
b7f285e4e2 fix(http-signature): update SIGNATURE_PATTERN allowing signature keys to be sent in any order
set algorithm key as optional and set defaults for both algorithm (rsa-sha256) and headers (date)
keys
2022-02-03 10:15:59 +00:00
semantic-release-bot
7bc2d42c8e chore(release): 1.0.0-beta.5 [skip ci]
# [1.0.0-beta.5](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-01-31)

### Bug Fixes

* **analytics:** set initial value for duration and bandwidth ([ee50539](ee50539591))
2022-01-31 17:23:53 +00:00
Benjamin Bellamy
ee50539591 fix(analytics): set initial value for duration and bandwidth 2022-01-31 16:49:26 +00:00
semantic-release-bot
06279cd90f chore(release): 1.0.0-beta.4 [skip ci]
# [1.0.0-beta.4](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-01-29)

### Bug Fixes

* **housekeeping:** replace the use of GLOB_BRACE with looping over file extensions ([42d92d0](42d92d0c8d)), closes [#154](https://code.podlibre.org/podlibre/castopod-host/issues/154)
* **housekeeping:** set default sizes value + ignore illegal IFD size error to proceed with script ([f21ca57](f21ca57603))

### Features

* **housekeeping:** add clear_cache option to flush redis or files cache ([99bfac0](99bfac0b42))
2022-01-29 17:03:15 +00:00
Yassine Doghri
99bfac0b42 feat(housekeeping): add clear_cache option to flush redis or files cache 2022-01-29 16:42:34 +00:00
Yassine Doghri
42d92d0c8d fix(housekeeping): replace the use of GLOB_BRACE with looping over file extensions
The GLOB_BRACE flag is not available on non GNU systems, such as Solaris or Alpine Linux.

fixes #154
2022-01-29 15:32:38 +00:00
Yassine Doghri
f21ca57603 fix(housekeeping): set default sizes value + ignore illegal IFD size error to proceed with script 2022-01-29 15:21:46 +00:00
semantic-release-bot
3bfcce3751 chore(release): 1.0.0-beta.3 [skip ci]
# [1.0.0-beta.3](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-01-28)

### Bug Fixes

* revert to beta.1's codeigniter4 version ([e831411](e831411270))
2022-01-28 18:13:40 +00:00
Yassine Doghri
e831411270 fix: revert to beta.1's codeigniter4 version 2022-01-28 18:04:10 +00:00
semantic-release-bot
f80e820b1d chore(release): 1.0.0-beta.2 [skip ci]
# [1.0.0-beta.2](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-01-28)

### Bug Fixes

* **migrations:** ignore invalid utf8 chars for media files metadata + update transcript parser ([45e8f99](45e8f99e75))
* **video-clips:** set audio codec to aac, fixing audio issue on twitter ([3c22c68](3c22c68ee8))
* **video-clips:** set longer podcast and episode lengths for squared format ([c030113](c0301134c2))
2022-01-28 15:26:24 +00:00
Yassine Doghri
c0301134c2 fix(video-clips): set longer podcast and episode lengths for squared format
update CI4 to latest version
2022-01-28 15:04:47 +00:00
Yassine Doghri
3c22c68ee8 fix(video-clips): set audio codec to aac, fixing audio issue on twitter 2022-01-28 11:06:45 +00:00
Yassine Doghri
45e8f99e75 fix(migrations): ignore invalid utf8 chars for media files metadata + update transcript parser
check that transcript parser constants are defined before declaring them
2022-01-28 10:31:30 +00:00
semantic-release-bot
d807ab9732 chore(release): 1.0.0-beta.1 [skip ci]
# 1.0.0-beta.1 (2022-01-23)

### Bug Fixes

* **a11y:** replace active tab color to contrast with background on podcast and episode pages ([f3785e1](f3785e1401))
* **activity-pub:** cache issues when navigating to activity stream urls ([7bcbfb3](7bcbfb32f7))
* **activity-pub:** get database records using new model instances ([92536dd](92536ddb38))
* **activitypub:** set created_by to null for reblog if no user + update episode oembed data ([209dfbd](209dfbd134))
* add admin-audio-player to vite config to have admin player show up ([93cb9b2](93cb9b2470))
* add application/octet-stream mimetype to mp3 and m4a extensions to prevent ext_in error ([339bef8](339bef878e)), closes [#145](https://code.podlibre.org/podlibre/castopod-host/issues/145)
* add category_label component to include parent category in about podcast page ([74e7d68](74e7d68ac8))
* add head request to analytics_hit route ([f0a2f0b](f0a2f0bea4))
* add missing explicit badge for podcasts and episodes ([cdf9f9d](cdf9f9d53f))
* add open graph size for podcast images to replace the inadequate large format ([33aae1f](33aae1f793))
* add public/media folder to castopod bundle ([8053d35](8053d3521b)), closes [#52](https://code.podlibre.org/podlibre/castopod-host/issues/52)
* add translation key for audio-clipper trim labels ([db191ac](db191ac31b))
* add where condition to get episode count without deleted episodes ([7661734](7661734ed2)), closes [#67](https://code.podlibre.org/podlibre/castopod-host/issues/67)
* **admin:** save block and lock switches ([b66c0af](b66c0afc8f))
* **analytics:** redirect to mp3 file even when referer was not set ([9fc388d](9fc388d154))
* **analytics:** remove charts empty values + remove useless language cache ([1678794](1678794153))
* **analytics:** set duration field to precise decimal as episode's audio file duration ([d772685](d772685405))
* **analytics:** update migrations to set decimal precision for latitude and longitude ([714d6b5](714d6b5d49))
* **analytics:** update service management so that it works with new OPAWG slug values ([7fe9d42](7fe9d42500))
* **audio-clipper:** add mouse position offset when stretching clip to prevent content from jumping ([602654b](602654b99b))
* **audio-clipper:** show audio playing progress + put waveform behind audio clipper ([01a09dc](01a09dc447))
* **avatar:** use default avatar when no avatar url has been set ([9d23c7e](9d23c7e7e1)), closes [#111](https://code.podlibre.org/podlibre/castopod-host/issues/111)
* **bundle:** include modules and themes when copying files with rsync ([cd5bb88](cd5bb8835c))
* **bundle:** update vite input files path + add `set -e` in bash scripts to fail if command fails ([0ee53c7](0ee53c71ff))
* **cache:** add locale for podcast and episode pages + clear some persisting cache in models ([9cec8a8](9cec8a81cc)), closes [#42](https://code.podlibre.org/podlibre/castopod-host/issues/42) [#61](https://code.podlibre.org/podlibre/castopod-host/issues/61)
* **cache:** return a non cached view when connected ([e2e7358](e2e735815d))
* **cache:** suffix cache names with authenticated for credits, map and pages ([418a70b](418a70b2a6))
* cast actor_id to pass as int to set_interact_as_actor() function ([56a8e5d](56a8e5d7dd))
* **category:** remove uncategorized option to enforce users in choosing a category ([8c64f25](8c64f25a0e))
* check for database connection and podcasts table existence before redirecting to install ([eb74e81](eb74e81c3d))
* check that additional files are valid when creating episode ([eac5bc8](eac5bc876d))
* check that note has a preview_card_id before displaying it ([acb8b3a](acb8b3a401)), closes [#114](https://code.podlibre.org/podlibre/castopod-host/issues/114)
* clear cache when deleting podcast banner ([99bb40b](99bb40b8bc))
* comment all cache clean after page update to prevent analytics cache deletion ([e6197a4](e6197a4972))
* **comments:** add comment view partials for public pages ([fcecbe1](fcecbe1c68))
* correct chart data ([4d3e9c8](4d3e9c8c02))
* correct percona compatibility issue ([e53f819](e53f819264))
* correct php-fpm issues ([1ef55d7](1ef55d7315))
* correct referrer bug ([ed69b2f](ed69b2f500))
* correction for servers with low int precision ([31b7828](31b7828e77))
* **cors:** add preflight option routes for episode, podcast and status objects ([a281abf](a281abfda4))
* declare typed properties in PHPDoc for php<7.4 ([14dd44d](14dd44d03d)), closes [#23](https://code.podlibre.org/podlibre/castopod-host/issues/23)
* define podcast_id and platform_slug as foreign keys in podcasts_plaforms table ([6e9451a](6e9451a110))
* define podcastNamespaceLink value ([0d744d2](0d744d212d))
* **embeddable-player:** enable any ancestor when X-Frame-Options is set on server ([44a4962](44a4962e0b))
* **embed:** open embedded player's links in new tab ([4aa73d7](4aa73d71e3))
* **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` & `post_max_size` ([3b3c218](3b3c218b9c)), closes [#5](https://code.podlibre.org/podlibre/castopod-host/issues/5) [#86](https://code.podlibre.org/podlibre/castopod-host/issues/86)
* **episodeCount:** add missing brackets to French language file ([c1b4112](c1b411265a))
* **episode:** replace guid's empty string value to null ([441052a](441052af8d))
* **episodes-page:** handle defaultQuery being null when no podcast episodes ([15183b7](15183b7eab)), closes [#100](https://code.podlibre.org/podlibre/castopod-host/issues/100)
* **episodes-table:** set descriptions to be not null ([6774ec1](6774ec10fa))
* **episodes:** add publication status + set publication date to null when none has been set ([d882981](d882981b3a)), closes [#70](https://code.podlibre.org/podlibre/castopod-host/issues/70)
* escape generated feed tag values and remove new lines from public pages meta description ([6238a43](6238a43863)), closes [#57](https://code.podlibre.org/podlibre/castopod-host/issues/57) [#46](https://code.podlibre.org/podlibre/castopod-host/issues/46)
* expire default query cache upon scheduled episode publication ([b72e7c8](b72e7c8691)), closes [#81](https://code.podlibre.org/podlibre/castopod-host/issues/81)
* fix layout bugs in admin and update translation files ([a834171](a83417180c)), closes [#40](https://code.podlibre.org/podlibre/castopod-host/issues/40)
* **follow:** add missing helpers to Actor controller ([ee53a73](ee53a732dc))
* handle HEAD requests on podcast_feed route ([74b2640](74b2640f2a)), closes [#79](https://code.podlibre.org/podlibre/castopod-host/issues/79)
* **images:** set default mimetype if none is specified when getting size info ([6e4acc6](6e4acc64ad))
* **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in download_file, closes [#103](https://code.podlibre.org/podlibre/castopod-host/issues/103) ([35b5be0](35b5be095f))
* **import:** add extension when downloading file without + truncate slug if too long ([c5f18bb](c5f18bb6dc))
* **import:** add validation for handle field to prevent Router.invalidParameterType error ([5bf7200](5bf7200fb3)), closes [#119](https://code.podlibre.org/podlibre/castopod-host/issues/119)
* **import:** cast description's SimpleXMLElement to string ([02d17be](02d17be4ff))
* **import:** remove query string from files url ([109c4aa](109c4aa1af))
* **import:** save media files during podcast import + set missing media fields ([a9989d8](a9989d841a))
* **import:** set episode and season numbers to null when not present in item tag ([3211398](3211398c78))
* **import:** use <image><url> tag when no <itunes:image> is present ([20e607a](20e607afb7))
* include missing variables on public ui's episode page and remote_actions ([193b373](193b373bc9))
* **install:** redirect manually to install wizard on first visit ([2ceaaca](2ceaaca44f))
* **install:** redirect to host_url install route on instanceConfig validation error ([99250b1](99250b1868))
* **install:** redirect to input baseUrl after instance config ([2426af7](2426af7de8)), closes [#53](https://code.podlibre.org/podlibre/castopod-host/issues/53)
* **interact-as:** set actor_id instead of podcast id upon login event ([5dfade7](5dfade7cf3)), closes [#104](https://code.podlibre.org/podlibre/castopod-host/issues/104)
* **json-ld:** add missing properties to PodcastSeries object ([e97266c](e97266c5d4))
* keep subtitle line breaks when parsing srt file to json ([cfb3da6](cfb3da6592))
* **layouts:** replace holy-grail layout with tailwind config + widen public podcast layout ([be5a287](be5a28787f))
* **map:** update episode markers query to discard unpublished episodes ([b3caac4](b3caac45b1))
* **md-editor:** build new markdown editor with lit + github/markdown-toolbar-element ([9ec1cb9](9ec1cb93da)), closes [#93](https://code.podlibre.org/podlibre/castopod-host/issues/93) [#94](https://code.podlibre.org/podlibre/castopod-host/issues/94) [#120](https://code.podlibre.org/podlibre/castopod-host/issues/120)
* minor corrections ([13be386](13be386842))
* move analytics to helper ([d311917](d31191732e))
* **multiselect:** add missing class names in choices options for purge to work properly ([719538d](719538d0cc))
* **open-graph:** replace non existant episode description to podcast description in podcast page ([b02584e](b02584ee60))
* **package.json:** update destination of postcss generation scripts ([21413f8](21413f8af3))
* **pages:** add locale to page cache ([8f999ce](8f999ce2f7))
* **partner:** set correct image URL ([61554be](61554be12a))
* pass timezone to relative time component to show the localized time in the UI ([b9db936](b9db936461))
* **persons:** prevent overflow of persons list by adding horizontal scroll ([9e8995d](9e8995dc6e))
* **persons:** set person picture as optional for better ux ([7fdea63](7fdea63de7)), closes [#125](https://code.podlibre.org/podlibre/castopod-host/issues/125)
* **platforms:** display platform link only when visible is toggled on ([6e503c8](6e503c8d61)), closes [#39](https://code.podlibre.org/podlibre/castopod-host/issues/39)
* **podcast-import:** move guid attribute declaration for Episode entity to include slug data ([5d02ae3](5d02ae3990))
* **pwa:** add scope to webmanifests to allow installing an app per podcast ([74c683e](74c683eb44))
* **pwa:** set app display as standalone in the webmanifests ([7aa37d2](7aa37d24ac))
* re-order graph values ([35f633b](35f633b4c7))
* redirect to non cached views when authenticated in public views ([482b47b](482b47ba6b))
* **release:** add missing version number to castopod-host package ([8f3e9d9](8f3e9d90c1))
* remove defer from js script declaration as it is a module ([18ae557](18ae557e97))
* remove fixed size from podcast sidebar + rearrange account info + space out import radio inputs ([776eec6](776eec6f0d))
* remove required for other_categories field and add podcast_id to latest podcasts query ([5417be0](5417be0049))
* remove required property to persons picture ([c546be3](c546be385b)), closes [#125](https://code.podlibre.org/podlibre/castopod-host/issues/125)
* rename field status to task_status to get scheduled activities ([4ff82a5](4ff82a5f0a))
* rename issue_templates labels ([9f00305](9f00305844))
* rename MyAccount controller file ([e109df3](e109df3004)), closes [#60](https://code.podlibre.org/podlibre/castopod-host/issues/60)
* rename podcast name to podcast handle to clarify field usage ([9dd4c77](9dd4c7741e)), closes [#126](https://code.podlibre.org/podlibre/castopod-host/issues/126)
* reorder fields as composite primary keys for analytics tables ([9660aa9](9660aa97c8))
* replace getWebEnclosureUrl with getEnclosureWebUrl ([8122cea](8122ceaf8a))
* replace hardcoded style links with vite service + set default value for remote transcript url ([3f2e056](3f2e05608e)), closes [#149](https://code.podlibre.org/podlibre/castopod-host/issues/149) [#150](https://code.podlibre.org/podlibre/castopod-host/issues/150)
* replace website key for webpages in breadcrumb translate file ([50e32ff](50e32ff756))
* rewrite regenerate image function to use saveSizes method from Image entity ([3889912](38899124ec))
* **rss-import:** add Castopod user-agent, handle redirects for downloaded files, add Content namespace ([214243b](214243b3fe))
* **rss:** cast number type values to string in rss_helper ([7180ae9](7180ae9ec7)), closes [#148](https://code.podlibre.org/podlibre/castopod-host/issues/148)
* **rss:** do not escape podcast and episode titles in the xml ([0dd3b7e](0dd3b7e0bf)), closes [#138](https://code.podlibre.org/podlibre/castopod-host/issues/138) [#71](https://code.podlibre.org/podlibre/castopod-host/issues/71)
* **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified ([2271c14](2271c1445b)), closes [#96](https://code.podlibre.org/podlibre/castopod-host/issues/96)
* **rss:** use originalPath instead of originalMediaPath in Image library ([b4012b7](b4012b7d2e))
* save transcript and chapters files to podcasts folder ([63f49c7](63f49c719f))
* set cache expiration to next note publish to show note on publication date ([0a66de3](0a66de3e6c))
* set episode description footer to null when empty value ([3a7d97d](3a7d97d660))
* set episode duration translation to hardcoded english ([c39efc9](c39efc9489)), closes [#64](https://code.podlibre.org/podlibre/castopod-host/issues/64)
* set episode guid upon episode creation ([ad8b153](ad8b153f2a)), closes [#48](https://code.podlibre.org/podlibre/castopod-host/issues/48)
* set episode numbers during import + remove all custom form_helpers + minor ui issues ([99a3b8d](99a3b8d33e))
* set localized slug_field key as string in french language ([17fb29b](17fb29b209))
* set location to null when getting empty string ([71b1b5f](71b1b5f775))
* **settings:** add .jpg extension to site-icon file input to display all jpeg images ([f611a16](f611a16cd0))
* sort episodic podcasts by season ([d7b6794](d7b6794f68))
* **themes:** update themes stylesheet route and remove css extension ([e4e7e00](e4e7e0005e))
* **types:** update fake seeders types + fix bugs ([76a4bf3](76a4bf3441))
* unpublish episode before deleting it + add validation step before deletion ([f75bd76](f75bd76458)), closes [#112](https://code.podlibre.org/podlibre/castopod-host/issues/112) [#55](https://code.podlibre.org/podlibre/castopod-host/issues/55)
* update .htaccess for shared hosting config ([2379826](2379826352))
* update broken contributor dropdown fields ([e5b7515](e5b7515023))
* update condition in AnalyticsTrait ([fbc0967](fbc0967caa))
* update condition in home controller to redirect to install page ([33f1b91](33f1b91d55))
* update conditions when checking for empty max_episodes and season_number ([fbad0b5](fbad0b59f6))
* update iso-369 language table seeder ([0c90db4](0c90db44c4))
* update MarkdownEditor component + restyle Button and other components ([b05d177](b05d177f1b))
* update purgecss content path for php helper files ([eb70bb4](eb70bb4f70)), closes [#59](https://code.podlibre.org/podlibre/castopod-host/issues/59)
* update translations for settings' tasks to include what they should be used for ([06b1a8b](06b1a8b29b))
* use slash instead of backslash to call layout ([a80adb2](a80adb2295))
* **ux:** allow for empty message upon episode publication and warn user on submit ([33d01b8](33d01b8d4f)), closes [#129](https://code.podlibre.org/podlibre/castopod-host/issues/129)
* **ux:** redirect user to install page on database error in home page ([9017e30](9017e30bf4))
* **video-clips:** check if created video exists before recreating it and failing ([dff1208](dff1208725))
* **video-clips:** clear video clip cache after process has finished ([3ae6232](3ae6232585))
* **video-clips:** create unique temporary files for resources to be deleted after generation ([7f7c878](7f7c878cb6))
* **video-clips:** tweak portrait parameters to have subtitles display without overflowing ([2385b1a](2385b1a292))
* **xml-editor:** escape xml editor's content + restyle form sections to prevent overflowing ([588590b](588590bd2c))

### Features

* **activitypub:** add Podcast actor and PodcastEpisode object with comments ([9e1e5d2](9e1e5d2e86))
* add alternate rss feed link tag to podcast page head ([a973c09](a973c097d5)), closes [#35](https://code.podlibre.org/podlibre/castopod-host/issues/35)
* add analytics and unknown useragents ([ec92e65](ec92e65aa4))
* add audio-clipper toolbar + add video-clip-previewer ([0255753](02557539e6))
* add audio-clipper webcomponent (wip) ([21d4251](21d4251b9b))
* add basic stats on podcast about page ([1670558](1670558473))
* add breadcrumb in admin area ([7fb1de2](7fb1de2cf3)), closes [#17](https://code.podlibre.org/podlibre/castopod-host/issues/17)
* add cache to ActivityPub sql queries + cache activity and note pages ([2d297f4](2d297f45b3))
* add CDN url ([972bcbf](972bcbf65e)), closes [#37](https://code.podlibre.org/podlibre/castopod-host/issues/37)
* add codemirror to display xml editor for custom rss field ([f15f262](f15f26240c))
* add cumulative listening time charts ([588b4d2](588b4d28da))
* add DropdownMenu component + remove global audio player in admin ([abb7fba](abb7fbac27))
* add episode_numbering() component helper to display episode and season numbers ([3f4a6bd](3f4a6bd0b9))
* add french translation ([196920d](196920d62f))
* add heading component + update ecs rules to fix views ([23bdc6f](23bdc6f8e3))
* add housekeeping task to run after migrations ([89dee41](89dee41d58))
* add install wizard form to bootstrap database and create the first superadmin user ([cba871c](cba871c5df)), closes [#2](https://code.podlibre.org/podlibre/castopod-host/issues/2)
* add ISO 3166 country codes ([97cd94b](97cd94b474))
* add js audio player on podcast, admin and embeddable player pages + fix admon episodes ux ([0e14eb4](0e14eb4d3f)), closes [#131](https://code.podlibre.org/podlibre/castopod-host/issues/131)
* add lock podcast according to the Podcastindex podcast-namespace to prevent unauthozized import ([72b3012](72b301272e))
* add map analytics, add episodes analytics, clean analytics page layout, translate countries ([07eae83](07eae83a00))
* add media entity and link documents, images and audio files to it ([6ecf286](6ecf2866cf))
* add Noto Sans Mono font to use for durations + button to access new video clip form in list ([7609bb6](7609bb6033))
* add npm for js dependencies + move src/ files to root folder ([cbb83a6](cbb83a6f30))
* add Open Graph and Twitter meta tags ([af970b8](af970b8bac)), closes [#41](https://code.podlibre.org/podlibre/castopod-host/issues/41)
* add pages table to store custom instance pages (eg. legal-notice, cookie policy, etc.) ([9c224a8](9c224a8ac6)), closes [#24](https://code.podlibre.org/podlibre/castopod-host/issues/24)
* add platform models ([a333d29](a333d29196))
* add platforms form in podcast settings ([043f49c](043f49c784))
* add platforms tables ([ce59344](ce5934419a))
* add podcast banner field for each podcast + refactor images configuration ([4a8147b](4a8147bfbb))
* add remote_url alternative for transcript and chapters files ([3143c9a](3143c9ad36))
* add replied to post or comment to reply element ([d0f9c60](d0f9c6018f))
* add schema.org json-ld objects to podcasts, episodes, posts and comments pages ([902f959](902f959b30))
* add task to housekeeping setting for resetting all instance counts ([9303e51](9303e51bc5))
* add unique listeners analytics ([3a49258](3a4925816f))
* add user permissions and basic groups to handle authorizations ([d58e518](d58e51874a)), closes [#3](https://code.podlibre.org/podlibre/castopod-host/issues/3) [#18](https://code.podlibre.org/podlibre/castopod-host/issues/18)
* **admin:** make header stick on scroll and show title + action buttons using css only ([d60498c](d60498c1be))
* **admin:** update admin layout for better ux + update brand pine colors ([d86142e](d86142ebe7))
* allow cross origin requests on episode comments ([e12f95a](e12f95aca1))
* **analytics:** add 'other' group to pie charts in order to display more accurate data ([73acef9](73acef933f))
* **analytics:** add charts and data export ([78625c4](78625c471b))
* **analytics:** add service name from rss user-agent ([7202b98](7202b9867b))
* **analytics:** add weekday and hour bar charts ([8ab3132](8ab313296b))
* build hashed static files to renew browser cache ([37c54d2](37c54d2477)), closes [#107](https://code.podlibre.org/podlibre/castopod-host/issues/107)
* **cache:** add podcast and episode pages to cache + clear them after insert or update ([da0f047](da0f047281))
* **categories:** create model, entity, migrations and seeds ([f73b042](f73b042cc0))
* **clips:** setup clip entities and model + save video clip to have it generated in the background ([2f6fdf9](2f6fdf9091))
* **comments:** add comments to episodes + update naming of status to post ([bb4752c](bb4752c35e))
* **comments:** add like / undo like to comment + add comment page ([0c187ef](0c187ef7a9))
* **components:** add custom view renderer with ComponentRenderer adapted from bonfire2 ([a95de8b](a95de8bab0))
* create optimized & resized images upon upload ([02e4441](02e4441f98)), closes [#6](https://code.podlibre.org/podlibre/castopod-host/issues/6)
* **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and ❬item❭ ([6ecdaad](6ecdaad911))
* **devcontainer:** add devcontainer settings for dev environment ([69e7266](69e7266736))
* display castopod version in admin footer ([9f2574e](9f2574e6fb)), closes [#68](https://code.podlibre.org/podlibre/castopod-host/issues/68)
* display legal disclaimer and warning on podcast import page ([2f07992](2f07992e55)), closes [#34](https://code.podlibre.org/podlibre/castopod-host/issues/34)
* edit + delete podcast and episode ([ac5f0c7](ac5f0c7328))
* **embeddable-player:**  add embeddable player widget ([141788f](141788fa08))
* enhance admin ui with responsive design and ux improvements ([2d44b45](2d44b457a0)), closes [#31](https://code.podlibre.org/podlibre/castopod-host/issues/31) [#9](https://code.podlibre.org/podlibre/castopod-host/issues/9)
* enhance ui using javascript in admin area ([c0e66d5](c0e66d5f70))
* **episodes:** add create form and view pages for episode ([f3b2c8b](f3b2c8b84f)), closes [#1](https://code.podlibre.org/podlibre/castopod-host/issues/1)
* **episodes:** add migrations, model and entity for episodes table ([0444821](044482174e))
* **episodes:** replace all audio file URL parameters with base64 encoded data ([e1f65cd](e1f65cd3b5))
* **episodes:** schedule episode with future publication_date by using cache expiration time ([4f1e773](4f1e773c0f)), closes [#47](https://code.podlibre.org/podlibre/castopod-host/issues/47)
* **fediverse:** implement activitypub protocols + update user interface ([2f525c0](2f525c0f6e)), closes [#69](https://code.podlibre.org/podlibre/castopod-host/issues/69) [#65](https://code.podlibre.org/podlibre/castopod-host/issues/65) [#85](https://code.podlibre.org/podlibre/castopod-host/issues/85) [#51](https://code.podlibre.org/podlibre/castopod-host/issues/51) [#91](https://code.podlibre.org/podlibre/castopod-host/issues/91) [#92](https://code.podlibre.org/podlibre/castopod-host/issues/92) [#88](https://code.podlibre.org/podlibre/castopod-host/issues/88)
* **fonts:** replace Montserrat with Inter for better readablity ([bfa11d0](bfa11d007d))
* import podcast from an rss feed url ([9a5d5a1](9a5d5a15b4)), closes [#21](https://code.podlibre.org/podlibre/castopod-host/issues/21)
* integrate stylized form components and update podcast edit page ([6536729](6536729546))
* make displayed publication time as relative time using @github/time-elements ([230e139](230e139e43))
* **map:** display geolocated episodes on a map page ([4357cc2](4357cc25cc))
* **media:** clean media api + create an entity per media type ([fafaa7e](fafaa7e689))
* **media:** save audio, images, transcripts and chapters to media for episode and persons ([58e2a00](58e2a00a87))
* **meta-tags:** add activitypub alternate links to podcast, episode, comment and post pages ([bd61752](bd61752be2))
* minor corrections to some tables ([3bf9420](3bf9420b59))
* **monetization:** add Web Monetization support ([96a6026](96a6026f1d))
* **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about the castopod instance ([88fddc8](88fddc81d7))
* **partner:** add link and image in episode description ([ad07bb9](ad07bb9330))
* **person:** add podcastindex.org namespace person tag ([8acd011](8acd011f13))
* **platforms:** add AntennaPod ([53e9cfd](53e9cfd61c))
* **platforms:** add Fediverse and some funding platforms, add link on logo ([afc3d50](afc3d50289))
* **platforms:** add helloasso ([16cb993](16cb993ee6))
* **platforms:** add missing newpodcastapps.com's platforms ([92dd370](92dd370e2f))
* **platforms:** add pod.link ([3d7a232](3d7a2320dd))
* **platforms:** add Podcast Index ([ad52b1c](ad52b1cc2b))
* **platforms:** add podfriend ([9fdc8d3](9fdc8d3293))
* **podcast-form:** update routes and redirect to podcast page ([12ce905](12ce905799))
* **podcast:** create a podcast using form ([1202ba3](1202ba3545))
* prefill season and episode numbers + set episode number as mandatory for serial podcasts ([07d740b](07d740b79f)), closes [#134](https://code.podlibre.org/podlibre/castopod-host/issues/134) [#136](https://code.podlibre.org/podlibre/castopod-host/issues/136)
* **public-ui:** adapt public podcast and episode pages to wireframes ([40a0535](40a0535fc1)), closes [#30](https://code.podlibre.org/podlibre/castopod-host/issues/30) [#13](https://code.podlibre.org/podlibre/castopod-host/issues/13)
* **pwa:** add service-worker + webmanifest for each podcasts to have them install on devices ([fee2c1c](fee2c1c0d0))
* redesign public podcast and episode pages + remove any information clutter for better ux ([9321400](932140077c))
* replace form helper functions with components in admin template ([e64548b](e64548b982))
* replace slug field with interactive permalink component ([578022b](578022b8c5))
* restyle episode and person cards + add focus style to interactive elements for a11y ([a505a1d](a505a1de56))
* **rss:** add ˂podcast:guid˃ tag for channel ([1fab10e](1fab10eb0d))
* **rss:** add podcast-namespace tags for platforms + previousUrl tag ([dbba8dc](dbba8dc581)), closes [#73](https://code.podlibre.org/podlibre/castopod-host/issues/73) [#75](https://code.podlibre.org/podlibre/castopod-host/issues/75) [#76](https://code.podlibre.org/podlibre/castopod-host/issues/76) [#80](https://code.podlibre.org/podlibre/castopod-host/issues/80)
* **rss:** add podcast:comments tag to link to episode comments ([32e8c7c](32e8c7c16a))
* **rss:** add podcast:location tag ([c0a2282](c0a22829bd))
* **rss:** add soundbites according to the podcastindex specs ([6b34617](6b34617d07)), closes [#83](https://code.podlibre.org/podlibre/castopod-host/issues/83)
* **rss:** add transcript and chapters support ([e769d83](e769d83a93)), closes [#72](https://code.podlibre.org/podlibre/castopod-host/issues/72) [#82](https://code.podlibre.org/podlibre/castopod-host/issues/82)
* **rss:** generate rss feed from podcast entity ([c815ecd](c815ecd664))
* **rss:** update monetization tag so that it meets PodcastIndex requirements ([4c7ecbe](4c7ecbee83))
* **select:** enhance select input with choices.js ([910d457](910d457cf8))
* set app parameter forceGlobalSecureRequests = true forcing requests to go through https ([d9dff1b](d9dff1b8bf))
* set podcast / episode description in the pages description meta tag ([1c4a504](1c4a50442b)), closes [#44](https://code.podlibre.org/podlibre/castopod-host/issues/44)
* **settings:** add general config for instance (site name, description and icon) ([5c56f3e](5c56f3e6f0))
* **settings:** add theme settings to set an accent color for all public pages ([5c529a8](5c529a83aa))
* simplify podcast page's layout for better ux ([2c0efc6](2c0efc6563))
* **soundbites:** add soundbite list and creation forms with audio-clipper component ([de19317](de19317138))
* style file inputs using tailwind's file class ([8208ab6](8208ab6785))
* **themes:** add ViewThemes library to set views in root themes folder ([7a27676](7a276764e6))
* **themes:** set different default banner per theme ([11c916f](11c916fe43))
* **themes:** set generic css variables for colors to enable instance themes ([a746a78](a746a781b4))
* toggle podcast sidebar on smaller screens ([f0205ec](f0205ec274))
* **transcript:** parse srt subtitles into json file + add max file size info below audio file input ([0098761](00987610a0))
* **ui:** create ViewComponents library to enable building class and view files components ([94872f2](94872f2338))
* update analytics so to meet IABv2 requirements ([03e23a2](03e23a28bf)), closes [#10](https://code.podlibre.org/podlibre/castopod-host/issues/10)
* update pine colors + create charts components ([a50abc1](a50abc138d))
* **users:** add myth-auth to handle users crud + add admin gateway only accessible by login ([c63a077](c63a077618)), closes [#11](https://code.podlibre.org/podlibre/castopod-host/issues/11)
* **ux:** remove admin dashboard and redirect directly to podcast list ([27c48b8](27c48b8fa9))
* **video-clip:** add video-clip page with video preview + logs ([42538dd](42538dd757))
* **video-clip:** generate video clips in the bg using a cron job + add video clip page + tidy up UI ([db0e427](db0e4272bd))
* **video-clips:** add dimensions for portrait and squared formats ([3af404d](3af404da3d))
* **video-clips:** add new themes + add castopod logo as a watermark ([1d1490b](1d1490b06a))
* **video-clips:** add route for scheduled video clips + list video clips with status ([2065ebb](2065ebbee5))
* **video-clips:** allow episodeNumbering text to stand in the indent of episodeTitle paragraph ([71a063d](71a063dac3))
* **video-clips:** generate a 16:9 video using ffmpeg ([35aa7ea](35aa7ea5d9))
* **video-clips:** generate subtitles clip using transcript json to have subtitles accross video ([3ce07e4](3ce07e455d))
* **video-clips:** replace hardcoded colors with config's theme colors ([e462abf](e462abf6d6))
* **vite:** add vite config to decouple it from CI_ENVIRONMENT ([8721719](8721719cd7))
* write id3v2 tags to episode's audio file ([4651d01](4651d01a84))

### Performance Improvements

* **cache:** update CI4 to use cache's deleteMatching method ([54b84f9](54b84f9684))
* **cache:** use deleteMatching method to prevent forgetting cached elements in models ([76afc0c](76afc0cfa2))
* defer javascript + lazy load images for faster page loads ([f0685e4](f0685e4479))
* **docker:** add redis caching service for development ([05ace8c](05ace8cff2))

### Reverts

* set deprecated config options back in App config ([433745f](433745f194))
* **soundbites:** remove soundbite table from episode's public page ([5dc0f19](5dc0f19656))
* use basic input file for episodes audio files instead of button for better UX ([d5f22fb](d5f22fbb38))

### BREAKING CHANGES

* **analytics:** analytics_podcasts_by_player table and analytics_podcasts procedure were updated
2022-01-23 20:23:27 +00:00
Yassine Doghri
74e7d68ac8 fix: add category_label component to include parent category in about podcast page 2022-01-23 19:58:30 +00:00
Yassine Doghri
e2e735815d fix(cache): return a non cached view when connected 2022-01-23 19:39:17 +00:00
Yassine Doghri
4aa73d71e3 fix(embed): open embedded player's links in new tab 2022-01-23 19:22:38 +00:00
Yassine Doghri
74c683eb44 fix(pwa): add scope to webmanifests to allow installing an app per podcast 2022-01-23 19:07:57 +00:00
Yassine Doghri
418a70b2a6 fix(cache): suffix cache names with authenticated for credits, map and pages 2022-01-23 19:00:08 +00:00
Yassine Doghri
18ae557e97 fix: remove defer from js script declaration as it is a module 2022-01-23 18:21:59 +00:00
Yassine Doghri
aa84b20d55 chore: remove DEPENDENCIES.md from bundle 2022-01-23 18:09:14 +00:00
Yassine Doghri
a2136849dd chore: run audit for npm + allow php-stan/extension-installer as a plugin in composer 2022-01-23 17:54:08 +00:00
Yassine Doghri
588590bd2c fix(xml-editor): escape xml editor's content + restyle form sections to prevent overflowing 2022-01-23 16:53:23 +00:00
Yassine Doghri
99bb40b8bc fix: clear cache when deleting podcast banner 2022-01-23 16:02:24 +00:00
Yassine Doghri
209dfbd134 fix(activitypub): set created_by to null for reblog if no user + update episode oembed data 2022-01-23 15:42:56 +00:00
Yassine Doghri
7aa37d24ac fix(pwa): set app display as standalone in the webmanifests 2022-01-23 14:37:44 +00:00
Yassine Doghri
33aae1f793 fix: add open graph size for podcast images to replace the inadequate large format 2022-01-23 12:14:15 +00:00
Yassine Doghri
67b73c1525 docs: update install, update and readme for next release
fix translation files
2022-01-22 14:44:07 +00:00
Yassine Doghri
f3785e1401 fix(a11y): replace active tab color to contrast with background on podcast and episode pages 2022-01-21 20:52:18 +00:00
Yassine Doghri
06b1a8b29b fix: update translations for settings' tasks to include what they should be used for 2022-01-21 19:51:36 +00:00
Yassine Doghri
cdf9f9d53f fix: add missing explicit badge for podcasts and episodes 2022-01-21 19:26:31 +00:00
Yassine Doghri
db191ac31b fix: add translation key for audio-clipper trim labels 2022-01-21 18:29:39 +00:00
Yassine Doghri
f0685e4479 perf: defer javascript + lazy load images for faster page loads 2022-01-21 18:07:43 +00:00
Yassine Doghri
ac6ddfc4fb style(video-clips): rename temp files to be easily identified 2022-01-21 17:39:29 +00:00
Yassine Doghri
dff1208725 fix(video-clips): check if created video exists before recreating it and failing
update seed scripts to prevent sql error when reloading install page
2022-01-21 17:25:27 +00:00
Yassine Doghri
2385b1a292 fix(video-clips): tweak portrait parameters to have subtitles display without overflowing 2022-01-21 15:24:17 +00:00
Yassine Doghri
93cb9b2470 fix: add admin-audio-player to vite config to have admin player show up 2022-01-21 14:56:17 +00:00
Yassine Doghri
7f7c878cb6 fix(video-clips): create unique temporary files for resources to be deleted after generation
- tempfile uniqueness ensures that each process lives in its independent context
- add
writable/temp folder to store video clips temporary resources
- add videoClipWorkers config to
Admin for specifying the number of ffmpeg processes to run in parallel
- update video clip preview
background to better suit the end result
2022-01-21 12:35:50 +00:00
Yassine Doghri
482b47ba6b fix: redirect to non cached views when authenticated in public views 2022-01-21 09:08:14 +00:00
Yassine Doghri
c1581c1fd4 refactor: rename audio_file urls to audio urls 2022-01-21 08:52:28 +00:00
Yassine Doghri
e97266c5d4 fix(json-ld): add missing properties to PodcastSeries object
rename episode activity cache
2022-01-20 16:50:26 +00:00
Yassine Doghri
cfb3da6592 fix: keep subtitle line breaks when parsing srt file to json
update job duration label for video clips list
2022-01-20 15:38:40 +00:00
Yassine Doghri
11c916fe43 feat(themes): set different default banner per theme 2022-01-20 14:51:31 +00:00
Yassine Doghri
e4e7e0005e fix(themes): update themes stylesheet route and remove css extension
removing the css extension prevents having the file hijacked by the web server config
2022-01-20 11:26:18 +00:00
Yassine Doghri
3ae6232585 fix(video-clips): clear video clip cache after process has finished 2022-01-19 19:09:35 +00:00
Yassine Doghri
8f999ce2f7 fix(pages): add locale to page cache 2022-01-19 18:41:00 +00:00
Yassine Doghri
8721719cd7 feat(vite): add vite config to decouple it from CI_ENVIRONMENT 2022-01-19 18:31:57 +00:00
Yassine Doghri
6e4acc64ad fix(images): set default mimetype if none is specified when getting size info 2022-01-19 12:09:12 +00:00
Yassine Doghri
17fb29b209 fix: set localized slug_field key as string in french language 2022-01-19 11:39:13 +00:00
Yassine Doghri
cc2cd8f558 chore: update js dependencies to latest 2022-01-19 11:02:01 +00:00
Yassine Doghri
0ee53c71ff fix(bundle): update vite input files path + add set -e in bash scripts to fail if command fails 2022-01-18 18:51:25 +00:00
Yassine Doghri
e10c071a85 docs: update DEPENDENCIES.md to point to composer.json and package.json files 2022-01-18 17:42:14 +00:00
Yassine Doghri
cd5bb8835c fix(bundle): include modules and themes when copying files with rsync 2022-01-18 17:36:34 +00:00
Yassine Doghri
9303e51bc5 feat: add task to housekeeping setting for resetting all instance counts
set two toggle switches to run housekeeping tasks seperately if needed
2022-01-14 17:42:55 +00:00
Yassine Doghri
e65e236bbc refactor: replace hardcoded media root with media_path helper method in settings controller 2022-01-13 16:45:08 +00:00
Yassine Doghri
89dee41d58 feat: add housekeeping task to run after migrations
add run housekeeping button in general settings page
2022-01-13 16:02:14 +00:00
Yassine Doghri
38899124ec fix: rewrite regenerate image function to use saveSizes method from Image entity 2022-01-11 10:51:10 +00:00
Yassine Doghri
241366130e chore(clips): clear video clip and soundbite cache on delete 2022-01-10 16:52:12 +00:00
Yassine Doghri
bd61752be2 feat(meta-tags): add activitypub alternate links to podcast, episode, comment and post pages 2022-01-10 16:05:16 +00:00
Yassine Doghri
3ce07e455d feat(video-clips): generate subtitles clip using transcript json to have subtitles accross video 2022-01-10 14:22:55 +00:00
Yassine Doghri
958c1213ed chore: update codeigniter4 after 4.1.7 hot fix release 2022-01-10 10:31:47 +00:00
Yassine Doghri
00987610a0 feat(transcript): parse srt subtitles into json file + add max file size info below audio file input
remove episode form warning + add javascript validation when uploading a file to check if it's too
big to upload
2022-01-09 16:37:13 +00:00
Yassine Doghri
1670558473 feat: add basic stats on podcast about page
number of seasons and episodes + publication date of the first episode
2022-01-06 16:52:13 +00:00
Yassine Doghri
88fddc81d7 feat(nodeinfo2): add .well-known route for nodeinfo2 containing metadata about the castopod instance 2022-01-06 14:26:32 +00:00
Yassine Doghri
5bf7200fb3 fix(import): add validation for handle field to prevent Router.invalidParameterType error
fixes #119
2022-01-05 16:15:36 +00:00
Yassine Doghri
07d740b79f feat: prefill season and episode numbers + set episode number as mandatory for serial podcasts
closes #134, #136
2022-01-05 16:01:44 +00:00
Yassine Doghri
d0cb964b0f refactor: harmonize redirects after submitting forms
go back to form after submitting an edit form
2022-01-05 14:58:53 +00:00
Yassine Doghri
6e9451a110 fix: define podcast_id and platform_slug as foreign keys in podcasts_plaforms table
update discord logo
2022-01-04 16:37:59 +00:00
Yassine Doghri
1fc3da139e chore: update CodeIgniter to latest dev version after 4.1.6 2022-01-04 15:40:27 +00:00
Yassine Doghri
de19317138 feat(soundbites): add soundbite list and creation forms with audio-clipper component 2022-01-03 13:52:07 +00:00
Yassine Doghri
602654b99b fix(audio-clipper): add mouse position offset when stretching clip to prevent content from jumping
update Forms.Section component to adapt to full width
2022-01-02 14:11:05 +00:00
Yassine Doghri
6809789206 refactor: set episode and podcast type hints next to each option 2021-12-31 10:21:57 +00:00
Yassine Doghri
ad5cd2c2e9 refactor: replace popperjs with floating-ui 2021-12-31 09:42:52 +00:00
Yassine Doghri
02557539e6 feat: add audio-clipper toolbar + add video-clip-previewer 2021-12-30 17:09:24 +00:00
Yassine Doghri
01a09dc447 fix(audio-clipper): show audio playing progress + put waveform behind audio clipper 2021-12-29 17:40:59 +00:00
Yassine Doghri
21d4251b9b feat: add audio-clipper webcomponent (wip) 2021-12-29 12:10:13 +00:00
Yassine Doghri
7609bb6033 feat: add Noto Sans Mono font to use for durations + button to access new video clip form in list 2021-12-29 12:10:13 +00:00
Yassine Doghri
db0e4272bd feat(video-clip): generate video clips in the bg using a cron job + add video clip page + tidy up UI 2021-12-29 12:10:13 +00:00
Yassine Doghri
42538dd757 feat(video-clip): add video-clip page with video preview + logs 2021-12-29 12:10:13 +00:00
Yassine Doghri
2065ebbee5 feat(video-clips): add route for scheduled video clips + list video clips with status 2021-12-29 12:10:13 +00:00
Yassine Doghri
2f6fdf9091 feat(clips): setup clip entities and model + save video clip to have it generated in the background 2021-12-29 12:10:13 +00:00
Yassine Doghri
057559183c refactor(migrations): replace 191 value with more relevant one 2021-12-29 12:10:13 +00:00
Yassine Doghri
a9989d841a fix(import): save media files during podcast import + set missing media fields 2021-12-29 12:10:13 +00:00
Yassine Doghri
58e2a00a87 feat(media): save audio, images, transcripts and chapters to media for episode and persons 2021-12-29 12:10:13 +00:00
Yassine Doghri
fafaa7e689 feat(media): clean media api + create an entity per media type 2021-12-29 12:10:13 +00:00
Yassine Doghri
b09acf6c65 build: update dependencies to latest
remove console.logs + add @github/hotkey to replace markdown toolbar shortcuts
2021-12-29 12:10:11 +00:00
Yassine Doghri
a7993b9916 chore(gitignore): untrack media/site icons 2021-12-29 12:09:32 +00:00
Yassine Doghri
6ecf2866cf feat: add media entity and link documents, images and audio files to it 2021-12-29 12:09:32 +00:00
Yassine Doghri
1d1490b06a feat(video-clips): add new themes + add castopod logo as a watermark
fix video colors to portray exact rgb values using libx264rgb encoding + image transparency issue
when overlaying images with php gd
2021-12-29 12:09:32 +00:00
Yassine Doghri
e462abf6d6 feat(video-clips): replace hardcoded colors with config's theme colors 2021-12-29 12:09:32 +00:00
Yassine Doghri
827ca03f61 refactor(color-themes): set themes declaration as a config property + generate css file dynamically 2021-12-29 12:09:32 +00:00
Yassine Doghri
71a063dac3 feat(video-clips): allow episodeNumbering text to stand in the indent of episodeTitle paragraph 2021-12-29 12:09:32 +00:00
Yassine Doghri
3af404da3d feat(video-clips): add dimensions for portrait and squared formats 2021-12-29 12:09:32 +00:00
Yassine Doghri
35aa7ea5d9 feat(video-clips): generate a 16:9 video using ffmpeg 2021-12-29 12:09:32 +00:00
Yassine Doghri
fee2c1c0d0 feat(pwa): add service-worker + webmanifest for each podcasts to have them install on devices
- configure service-worker using vite-plugin-pwa
- refactor Image entity to generate images of
different types based on size config
- add requirement for webp library for php gd to generate webp
images for instance
- add action to regenerate all instance images for eventual Images config
changes
- enhance google lighthouse metrics for pwa
2021-12-29 12:09:31 +00:00
Yassine Doghri
902f959b30 feat: add schema.org json-ld objects to podcasts, episodes, posts and comments pages
- refactor meta-tags by generating them in the controller and injecting them into the views
- use
`melbahja/seo` library to build opengraph and twitter meta-tags + schema.org objects
2021-12-29 12:06:13 +00:00
Yassine Doghri
5c529a83aa feat(settings): add theme settings to set an accent color for all public pages
set 6 base accent colors: pine, lake, jacaranda, crimson, amber and onyx
2021-12-29 12:06:13 +00:00
Yassine Doghri
a746a781b4 feat(themes): set generic css variables for colors to enable instance themes
- set new colors using the css variables for theming in tailwind.config.js
- replace admin and
public colors with new variable enabled colors
2021-12-29 12:06:10 +00:00
Yassine Doghri
f611a16cd0 fix(settings): add .jpg extension to site-icon file input to display all jpeg images 2021-12-29 12:04:04 +00:00
Yassine Doghri
4a8147bfbb feat: add podcast banner field for each podcast + refactor images configuration
- rename image fields on podcast, episode and persons for better clarity
- set different sizes
config for podcast cover, banner and persons avatars
- add tiny size for covers
- fix responsive
on admin forms
2021-12-29 12:04:04 +00:00
Yassine Doghri
5c56f3e6f0 feat(settings): add general config for instance (site name, description and icon) 2021-12-29 12:04:01 +00:00
Yassine Doghri
193b373bc9 fix: include missing variables on public ui's episode page and remote_actions 2021-12-29 12:03:17 +00:00
Yassine Doghri
776eec6f0d fix: remove fixed size from podcast sidebar + rearrange account info + space out import radio inputs 2021-12-29 12:03:17 +00:00
Yassine Doghri
d0f9c6018f feat: add replied to post or comment to reply element 2021-12-29 12:03:17 +00:00
Yassine Doghri
8208ab6785 feat: style file inputs using tailwind's file class 2021-12-29 12:03:17 +00:00
Yassine Doghri
f0205ec274 feat: toggle podcast sidebar on smaller screens
update tailwind to v3.0.0-alpha with its official plugins (forms, typography)
2021-12-29 12:03:15 +00:00
Yassine Doghri
c546be385b fix: remove required property to persons picture
fixes #125
2021-12-29 12:02:53 +00:00
Yassine Doghri
a505a1de56 feat: restyle episode and person cards + add focus style to interactive elements for a11y
fix components in follow and remote action pages by calling new instances directly
2021-12-29 12:02:53 +00:00
Yassine Doghri
025b2f42e6 refactor: replace "embeddable-player" occurrences to "embed" 2021-12-29 12:02:53 +00:00
Yassine Doghri
be5a28787f fix(layouts): replace holy-grail layout with tailwind config + widen public podcast layout
- add rounded classes with conditional border-radius depending on screen width
- add ring-castopod
class to use on focus states
2021-12-29 12:02:51 +00:00
Yassine Doghri
932140077c feat: redesign public podcast and episode pages + remove any information clutter for better ux
- add About podcast page
- use different layout for episode pages
- improve on user feedback with
design
- restructure app theme folders
- update js packages to latest versions
2021-12-29 12:02:14 +00:00
Yassine Doghri
e3bd9df00e refactor: update podcast card style + create partial for public navigation 2021-12-29 12:01:42 +00:00
Yassine Doghri
2c0efc6563 feat: simplify podcast page's layout for better ux 2021-12-29 12:01:40 +00:00
Yassine Doghri
c639c4148c docs(install): add missing CREATE permission to grant to database user
closes #142
2021-12-29 12:00:55 +00:00
Yassine Doghri
f75bd76458 fix: unpublish episode before deleting it + add validation step before deletion
fixes #112, closes #55
2021-12-29 12:00:55 +00:00
Yassine Doghri
b9db936461 fix: pass timezone to relative time component to show the localized time in the UI 2021-12-29 12:00:55 +00:00
Yassine Doghri
abb7fbac27 feat: add DropdownMenu component + remove global audio player in admin 2021-12-29 12:00:55 +00:00
Yassine Doghri
d60498c1be feat(admin): make header stick on scroll and show title + action buttons using css only 2021-12-29 12:00:53 +00:00
Yassine Doghri
99a3b8d33e fix: set episode numbers during import + remove all custom form_helpers + minor ui issues 2021-12-29 11:58:06 +00:00
Yassine Doghri
b05d177f1b fix: update MarkdownEditor component + restyle Button and other components 2021-12-29 11:55:43 +00:00
Yassine Doghri
746b518789 refactor: replace ui function components with class components + fix
soundbites js
2021-12-29 11:55:43 +00:00
Yassine Doghri
5413d09737 style: update app layout and map view styles 2021-12-29 11:55:43 +00:00
Yassine Doghri
bfa11d007d feat(fonts): replace Montserrat with Inter for better readablity 2021-12-29 11:55:43 +00:00
Yassine Doghri
e64548b982 feat: replace form helper functions with components in admin template 2021-12-29 11:55:43 +00:00
Yassine Doghri
6536729546 feat: integrate stylized form components and update podcast edit page 2021-12-29 11:55:43 +00:00
Yassine Doghri
23bdc6f8e3 feat: add heading component + update ecs rules to fix views 2021-12-29 11:55:43 +00:00
Yassine Doghri
a50abc138d feat: update pine colors + create charts components 2021-12-29 11:55:43 +00:00
Yassine Doghri
d86142ebe7 feat(admin): update admin layout for better ux + update brand pine colors 2021-12-29 11:55:41 +00:00
Yassine Doghri
7a276764e6 feat(themes): add ViewThemes library to set views in root themes folder
app, admin, install and authentication views are now located in root themes/ folder
2021-12-29 11:54:52 +00:00
Yassine Doghri
58c8839902 refactor(componentrenderer): update locateView using a lookupModules property in config 2021-12-29 11:54:52 +00:00
Yassine Doghri
a95de8bab0 feat(components): add custom view renderer with ComponentRenderer adapted from bonfire2
- update Component class structure and remove component helper function and ComponentLoader
- update residual activitypub naming to fediverse
2021-12-29 11:54:50 +00:00
Yassine Doghri
5083cd2fda refactor(modules): extract castopod parts into a modules/ folder for a scalable HMVC structure
- create Admin, Analytics, Auth, Fediverse and Install modules in the root modules/ folder
- rename
ActivityPub to Fediverse
2021-12-29 11:54:22 +00:00
Yassine Doghri
94872f2338 feat(ui): create ViewComponents library to enable building class and view files components
- replace some helper components and forms with class components in the ui
- create viewcomponents
service and load the component function to be used in views
2021-12-29 11:54:22 +00:00
Yassine Doghri
fcecbe1c68 fix(comments): add comment view partials for public pages 2021-12-29 11:54:22 +00:00
Yassine Doghri
0c187ef7a9 feat(comments): add like / undo like to comment + add comment page 2021-12-29 11:54:22 +00:00
Yassine Doghri
bb4752c35e feat(comments): add comments to episodes + update naming of status to post
- remove confusing counts for episode (total favourites, total reblogs)
- add comments section to
episode page to display episode comments + post replies linked to the episode
2021-12-29 11:54:22 +00:00
Yassine Doghri
3ff1364906 chore: update php dependencies to latest 2021-12-29 11:54:20 +00:00
Yassine Doghri
f15f26240c feat: add codemirror to display xml editor for custom rss field
- replace hardcoded label values for play_episode_button component
2021-12-29 11:37:16 +00:00
Yassine Doghri
578022b8c5 feat: replace slug field with interactive permalink component
- create permalink-edit web component with slug editing and permalink copy functionalities
- add
@github/clipboard-copy-element
- update npm packages
- replace vscode extension lit-html with
lit-plugin to get css intellisense
2021-12-29 11:37:16 +00:00
Yassine Doghri
230e139e43 feat: make displayed publication time as relative time using @github/time-elements 2021-12-29 11:37:16 +00:00
Yassine Doghri
0e14eb4d3f feat: add js audio player on podcast, admin and embeddable player pages + fix admon episodes ux
- use vimejs as audio player
- add global audio player + play episode buttons on public pages
-
refactor admin episodes list from a grid to a data table
- arrange episode cards to be more
readable

closes #131
2021-12-29 11:37:16 +00:00
Yassine Doghri
b72e7c8691 fix: expire default query cache upon scheduled episode publication
fix #81
2021-12-29 11:37:16 +00:00
Yassine Doghri
7fdea63de7 fix(persons): set person picture as optional for better ux
- use default avatar image if person image is not set
- add thumbnail and medium default avatar
images
- set default avatar images directly in public/media folder
- remove public/media's root
folder from .gitignore
- remove unnecessary copy:images script and cpy-cli package

closes #125
2021-12-29 11:37:16 +00:00
Yassine Doghri
0dd3b7e0bf fix(rss): do not escape podcast and episode titles in the xml
- add parameter to prevent escaping value in SimpleRSSElement's addChild method
- clean prosemirror residue (typedef + DEPENDENCIES.md)
- remove type definition generation in tsconfig

fixes #138, #71
2021-12-29 11:37:16 +00:00
Yassine Doghri
9ec1cb93da fix(md-editor): build new markdown editor with lit + github/markdown-toolbar-element
- create markdown-write-preview + markdown-preview webcomponents using lit
- create
form_markdown_editor helper form component
- simplify form_dropdown and form_multiselect
components
- fix partner fields display

fixes #93, #94, #120
2021-12-29 11:37:16 +00:00
Yassine Doghri
910d457cf8 feat(select): enhance select input with choices.js
- select fields come with a search field
- update language names by fixing missing caps in
LanguageSeeder
- add parent to categoryOptions
2021-12-29 11:37:16 +00:00
Yassine Doghri
9dd4c7741e fix: rename podcast name to podcast handle to clarify field usage
- podcast name was too vague and didn't come clearly for users: handle is more relevant
- update
package.json dependencies and remove unnused packages

closes #126
2021-12-29 11:37:16 +00:00
2997 changed files with 200247 additions and 80282 deletions

575
.all-contributorsrc Normal file
View file

@ -0,0 +1,575 @@
{
"projectName": "castopod",
"projectOwner": "adaures",
"repoType": "gitlab",
"repoHost": "https://code.castopod.org",
"files": ["README.md"],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "yassinedoghri",
"name": "Yassine Doghri",
"avatar_url": "https://avatars.githubusercontent.com/u/11021441?v=4",
"profile": "https://yassinedoghri.com",
"contributions": [
"code",
"bug",
"doc",
"review",
"maintenance",
"content",
"design",
"a11y",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"mentoring",
"infra",
"ideas",
"projectManagement",
{
"type": "blog",
"url": "https://blog.castopod.org/author/yassinedoghri/"
}
]
},
{
"login": "benjamin",
"name": "Benjamin Bellamy",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/2/avatar.png",
"profile": "https://code.castopod.org/benjamin",
"contributions": [
"code",
"bug",
"review",
"content",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"infra",
"ideas",
{
"type": "blog",
"url": "https://blog.castopod.org/author/benjamin-bellamy/"
},
"projectManagement",
"talk"
]
},
{
"login": "ola",
"name": "Ola Hneini",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://github.com/ola-hn",
"contributions": [
"code",
"review",
"doc",
"maintenance",
"question",
"ideas"
]
},
{
"login": "rdelaage",
"name": "Romain de Laage",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://mamot.fr/@rdelaage",
"contributions": [
"code",
"infra",
"doc",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas"
]
},
{
"login": "Lyonel",
"name": "Lyonel Bernard",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://twitter.com/lyonelbernard",
"contributions": ["bug", "question", "audio", "ideas"]
},
{
"login": "ctlw83",
"name": "Christopher Lagonick-Weitzel",
"avatar_url": "https://secure.gravatar.com/avatar/7c2a721b52d0763673a600e8f01bd745?s=80&d=identicon",
"profile": "https://www.crypticchameleon.com/",
"contributions": ["bug", "question", "audio", "ideas"]
},
{
"login": "ernestoacostame",
"name": "Ernesto Acosta",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://ernestoacosta.me/",
"contributions": [
"bug",
"audio",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"ideas"
]
},
{
"login": "3wen",
"name": "Ewen",
"avatar_url": "https://mastodon.fedi.bzh/system/accounts/avatars/000/000/002/original/6f387690a504ae46.jpg",
"profile": "https://mastodon.fedi.bzh/@ewen",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas",
"code"
]
},
{
"login": "Behel",
"name": "Bastien Luneteau",
"avatar_url": "https://secure.gravatar.com/avatar/ad63ee8ef8e3db8253d21e5012d2724f?s=80&d=identicon",
"profile": "https://code.castopod.org/Behel",
"contributions": ["code", "bug"]
},
{
"login": "cecillie",
"name": "Cécile Ricordeau",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://www.cecillie.fr/",
"contributions": ["design"]
},
{
"login": "PatrykMis",
"name": "Patryk Miś",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/PatrykMis",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "mspanc",
"name": "Marcin Lewandowski",
"avatar_url": "https://secure.gravatar.com/avatar/eed8337939641eac5ad0b570bd6acf96?s=80&d=identicon",
"profile": "https://code.castopod.org/mspanc",
"contributions": ["bug", "ideas"]
},
{
"login": "SJanik",
"name": "Sebastian Janik",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/SJanik",
"contributions": ["code"]
},
{
"login": "patryk",
"name": "Patryk Karczmarczyk",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/patryk",
"contributions": ["code"]
},
{
"login": "ddenis",
"name": "denis d",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/ddenis",
"contributions": ["bug", "ideas"]
},
{
"login": "douglaskastle",
"name": "Douglas Kastle",
"avatar_url": "https://secure.gravatar.com/avatar/b7e652ba4b6bcd440afa069e7f7bc9e6?s=80&d=identicon",
"profile": "https://code.castopod.org/douglaskastle",
"contributions": ["bug", "ideas"]
},
{
"login": "cExplorer",
"name": "cExplorer",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/cExplorer",
"contributions": [
"bug",
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "imacrea",
"name": "ImaCrea",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/imacrea",
"contributions": ["bug", "ideas"]
},
{
"login": "jonas",
"name": "Jonas S",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/jonas",
"contributions": ["code"]
},
{
"login": "yannL",
"name": "LEFEBVRE Yann",
"avatar_url": "https://secure.gravatar.com/avatar/9c46600ce566ec6d526370d8e104b1c8?s=80&d=identicon",
"profile": "https://code.castopod.org/yannL",
"contributions": ["bug"]
},
{
"login": "spaetz",
"name": "Sebastian Späth",
"avatar_url": "https://secure.gravatar.com/avatar/278e1af65e82993efd0ba7bbbacf6435?s=80&d=identicon",
"profile": "https://code.castopod.org/spaetz",
"contributions": ["bug", "ideas"]
},
{
"login": "rocky",
"name": "rocky III",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/rocky",
"contributions": ["bug"]
},
{
"login": "Regenpfeifer",
"name": "Hermann Josef Eckl",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/103/avatar.png",
"profile": "https://code.castopod.org/Regenpfeifer",
"contributions": ["bug"]
},
{
"login": "cyrilledel",
"name": "Delhaye Cyrille",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/cyrilledel",
"contributions": ["bug", "ideas"]
},
{
"login": "otetranome",
"name": "João Leandro",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/113/avatar.png",
"profile": "https://twitter.com/otetranome",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas"
]
},
{
"login": "achouvardas",
"name": "Angelos Chouvardas",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://achouvardas.eu/",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "eivind",
"name": "Eivind",
"avatar_url": "https://mastodon.fjerland.no/system/accounts/avatars/107/769/768/295/192/222/original/e5c985fea6487dcb.jpg",
"profile": "https://mastodon.fjerland.no/@eivind",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "forght",
"name": "forght",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15073833/large/82d1e2e443a6df7edc43a7405dfeeb75_default.png",
"profile": "https://crowdin.com/profile/forght",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "glottis0q",
"name": "glottis0q",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15209934/large/8b17ef6a7399f0b82a8198f87c224195.png",
"profile": "https://crowdin.com/profile/glottis0q",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "BoFFire",
"name": "ButterflyOfFire",
"avatar_url": "https://static.mstdn.fr/static/accounts/avatars/000/065/901/original/5908e93ad5447f15.png",
"profile": "https://mstdn.fr/@ButterflyOfFire",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "lil5",
"name": "Lucian I. Last",
"avatar_url": "https://avatars.githubusercontent.com/u/17646836?v=4",
"profile": "https://github.com/lil5",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "LuuzViir",
"name": "LuuzViir",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13166188/large/d03ab0abc7ce354b210d836955cd3805_default.png",
"profile": "https://crowdin.com/profile/luuzviir",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "cthtc",
"name": "CTHTC",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15211502/large/ed0651060cb8474a9519b5168bd377c1_default.png",
"profile": "https://crowdin.com/profile/cthtc",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "retrograde",
"name": "Russian Retro",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15021651/large/b10c4057f85bf4de49c7fdf01354ecde.jpeg",
"profile": "https://crowdin.com/profile/retrograde",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "mareklach",
"name": "Marek L'ach",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13572324/large/3eeba8d569c247ace33862bf4ef4748f.jpeg",
"profile": "https://crowdin.com/profile/mareklach",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "GunChleoc",
"name": "GunChleoc",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13043878/large/3223f7b606296a8b1c92c5de39c459a2_default.png",
"profile": "https://crowdin.com/profile/gunchleoc",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "GabiSnow",
"name": "GabiSnow",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15214858/large/5b083bdf9c9e9de67cc6ee72a6c8db18_default.png",
"profile": "https://crowdin.com/profile/gabisnow",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "bendaha",
"name": "bendaha",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15331656/large/cd92450d2c20202299fb3a0075903e20_default.png",
"profile": "https://crowdin.com/profile/bendaha",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "samuelroland",
"name": "Samuel Roland",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/14980053/large/3e154a37d03d6e98ae402ed3f930f4f5.png",
"profile": "https://crowdin.com/profile/samuelroland",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "dimregnier",
"name": "Dimitri Regnier",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://dimitriregnier.net/",
"contributions": ["ideas"]
},
{
"login": "irithys",
"name": "irithys",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15405614/large/3086461c47cce0a0c031925e5f943412.png",
"profile": "https://im.irithys.com/@thy",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "caos30",
"name": "Sergi",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://twitter.com/caos30",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "basen1982",
"name": "Andreas Olsson",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/basen1982",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "leonfrom",
"name": "leonfrom",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/leonfrom",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "agentcobra57",
"name": "agentcobra",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/agentcobra57",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "alephoto85",
"name": "Alessandro",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15094649/large/530391f54157af52ae33058ec15b0f99.jpg",
"profile": "https://crowdin.com/profile/alephoto85",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "liimee",
"name": "liimee",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/liimee",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "ahmedsabouni",
"name": "Ahmed Sabouni",
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
"profile": "https://github.com/ahmedsabouni",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "KrzysztofDomanczyk",
"name": "KrzysztofDomanczyk",
"avatar_url": "https://avatars.githubusercontent.com/u/75178474?v=4",
"profile": "https://github.com/KrzysztofDomanczyk",
"contributions": ["code"]
},
{
"login": "Dwev",
"name": "Guy Martin",
"avatar_url": "https://avatars.githubusercontent.com/u/46626050?v=4",
"profile": "https://github.com/Dwev",
"contributions": ["bug", "code"]
},
{
"login": "prcutler",
"name": "Paul Cutler",
"avatar_url": "https://avatars.githubusercontent.com/u/67276?v=4",
"profile": "https://github.com/prcutler",
"contributions": ["doc", "question", "ideas"]
},
{
"login": "nateritter",
"name": "Nate Ritter",
"avatar_url": "https://avatars.githubusercontent.com/u/198798?v=4",
"profile": "https://github.com/nateritter",
"contributions": ["code"]
}
],
"commitConvention": "none"
}

50
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,50 @@
####################################################
# Castopod development Docker file
####################################################
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
FROM php:8.5-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install server requirements
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update \
&& apt-get install --yes --no-install-recommends nodejs \
# gnupg to sign commits with gpg
gnupg \
openssh-client \
# cron for scheduled tasks
cron \
# unzip used by composer
unzip \
# required libraries to install php extensions using
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
libicu-dev \
libpng-dev \
libwebp-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
zlib1g-dev \
libzip-dev \
# ffmpeg for video encoding
ffmpeg \
# intl for Internationalization
&& docker-php-ext-install intl \
&& docker-php-ext-install zip \
# gd for image processing
&& docker-php-ext-configure gd --with-webp --with-jpeg --with-freetype \
&& docker-php-ext-install gd \
&& docker-php-ext-install exif \
&& docker-php-ext-enable exif \
# redis extension for cache
&& pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& docker-php-ext-enable redis \
# mysqli for database access
&& docker-php-ext-install mysqli \
&& docker-php-ext-enable mysqli

1
.devcontainer/crontab Normal file
View file

@ -0,0 +1 @@
* * * * * /usr/local/bin/php /workspaces/castopod/spark tasks:run >> /dev/null 2>&1

View file

@ -1,41 +1,70 @@
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/docker-existing-dockerfile
{
"name": "Castopod Host dev",
"dockerComposeFile": ["../docker-compose.yml", "./docker-compose.yml"],
"name": "castopod.local",
"dockerComposeFile": ["./docker-compose.yml"],
"service": "app",
"workspaceFolder": "/castopod-host",
"postCreateCommand": "composer install && npm install && npm run build:static",
"postStartCommand": "crontab ./crontab && cron && php spark serve --host 0.0.0.0",
"postAttachCommand": "crontab ./crontab && service cron reload",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"postCreateCommand": "composer install && pnpm install && pnpm run build:static && php spark migrate --all && php spark db:seed DevSeeder",
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0 --port ${APP_PORT:-8080}",
"postAttachCommand": "crontab .devcontainer/crontab && service cron reload",
"shutdownAction": "stopCompose",
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"editor.formatOnSave": true,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.formatOnSave": false
},
"color-highlight.markerType": "dot-before",
"files.associations": {
"*.xml.dist": "xml",
"spark": "php"
}
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/guiyomh/features/vim:0": {},
"ghcr.io/NicoVIII/devcontainer-features/pnpm:2": {}
},
"extensions": [
"mikestead.dotenv",
"bmewburn.vscode-intelephense-client",
"streetsidesoftware.code-spell-checker",
"naumovs.color-highlight",
"heybourn.headwind",
"wayou.vscode-todo-highlight",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"jamesbirtles.svelte-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"eamodio.gitlens",
"breezelin.phpstan",
"kasik96.latte"
]
"customizations": {
"vscode": {
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.formatOnSave": false
},
"css.validate": false,
"color-highlight.markerType": "dot-before",
"files.associations": {
"*.xml.dist": "xml",
"spark": "php",
"env": "dotenv",
".rsync-filter": "diff"
},
"json.schemas": [
{
"fileMatch": [
"plugins/**/manifest.json",
"tests/modules/Plugins/mocks/manifests/*.json",
"tests/modules/Plugins/mocks/plugins/**/manifest.json"
],
"url": "/workspaces/castopod/modules/Plugins/Manifest/manifest.schema.json"
}
]
},
"extensions": [
"astro-build.astro-vscode",
"bmewburn.vscode-intelephense-client",
"bradlc.vscode-tailwindcss",
"breezelin.phpstan",
"DavidAnson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"heybourn.headwind",
"jamesbirtles.svelte-vscode",
"kasik96.latte",
"mikestead.dotenv",
"naumovs.color-highlight",
"pflannery.vscode-versionlens",
"runem.lit-plugin",
"streetsidesoftware.code-spell-checker",
"stylelint.vscode-stylelint",
"unifiedjs.vscode-mdx",
"wayou.vscode-todo-highlight",
"yzhang.markdown-all-in-one",
"42Crunch.vscode-openapi"
]
}
}
}

View file

@ -1,10 +1,76 @@
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
# Mounts the project folder to '/workspace'. While this file is in .devcontainer,
# mounts are relative to the first file in the list, which is a level up.
- .:/castopod-host:cached
- ../..:/workspaces:cached
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
environment:
APP_PORT: ${APP_PORT:-8080} # used in devcontainer.json file
VITE_PORT: ${VITE_PORT:-5173} # used in ../vite.config.js file
CI_ENVIRONMENT: development
vite_environment: development
app_forceGlobalSecureRequests: 0 #false
app_baseURL: http://localhost:${APP_PORT:-8080}/
media_baseURL: http://localhost:${APP_PORT:-8080}/
admin_gateway: cp-admin
auth_gateway: cp-auth
analytics_salt: dev_analytics_salt
database_default_hostname: mariadb
database_default_database: castopod
database_default_username: castopod
database_default_password: castopod
database_default_DBPrefix: cp_
restapi_enabled: 1 #true
email_fromEmail: hello@castopod.local
email_SMTPCrypto: ""
email_SMTPHost: mailpit
email_SMTPUser: castopod
email_SMTPPass: castopod
email_SMTPPort: ${MAILPIT_SMTP_PORT:-1025}
depends_on:
- mariadb
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
mariadb:
image: mariadb:10.2
volumes:
- ./initdb:/docker-entrypoint-initdb.d
- mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: castopod
MYSQL_USER: castopod
MYSQL_PASSWORD: castopod
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
UPLOAD_LIMIT: 300M
ports:
- 8888:80
volumes:
- phpmyadmin:/sessions
depends_on:
- mariadb
mailpit:
image: axllent/mailpit
restart: always
volumes:
- mailpit:/data
ports:
- ${MAILPIT_WEBUI_PORT:-8025}:8025
- ${MAILPIT_SMTP_PORT:-1025}:1025
environment:
MP_MAX_MESSAGES: 5000
MP_DATA_FILE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
mariadb:
phpmyadmin:
mailpit:

View file

@ -0,0 +1,2 @@
CREATE DATABASE IF NOT EXISTS `test`;
GRANT ALL ON `test`.* TO 'castopod'@'%';

View file

@ -0,0 +1,5 @@
file_uploads = On
memory_limit = 512M
upload_max_filesize = 500M
post_max_size = 512M
max_execution_time = 300

68
.dockerignore Normal file
View file

@ -0,0 +1,68 @@
.env
.git/
node_modules/
vendor/
build/
docs/
scripts/
tests/
#-------------------------
# Temporary Files
#-------------------------
writable/cache/*
!writable/cache/index.html
writable/logs/*
!writable/logs/index.html
writable/session/*
!writable/session/index.html
writable/temp/*
!writable/temp/index.html
writable/uploads/*
!writable/uploads/index.html
writable/debugbar/*
!writable/debugbar/index.html
# public folder
public/*
!public/media
!public/.htaccess
!public/favicon.ico
!public/icon*
!public/castopod-banner*
!public/castopod-avatar*
!public/index.php
!public/robots.txt
!public/.well-known
!public/.well-known/GDPR.yml
public/assets/*
!public/assets/index.html
# public media folder
!public/media/podcasts
!public/media/persons
!public/media/site
public/media/podcasts/*
!public/media/podcasts/index.html
public/media/persons/*
!public/media/persons/index.html
public/media/site/*
!public/media/site/index.html
# Generated files
modules/Admin/Language/*/PersonsTaxonomy.php
# Castopod bundle & packages
castopod/
castopod-*.zip
castopod-*.tar.gz

View file

@ -2,7 +2,7 @@
# Example Environment Configuration file
#
# This file can be used as a starting point for
# your Castopod Host instance settings.
# your Castopod instance settings.
#
# For manual configuration:
# - copy this file's contents to a file named `.env`
@ -14,9 +14,10 @@
# Instance configuration
#--------------------------------------------------------------------
app.baseURL="https://YOUR_DOMAIN_NAME/"
app.mediaBaseURL="https://YOUR_MEDIA_DOMAIN_NAME/"
app.adminGateway="cp-admin"
app.authGateway="cp-auth"
media.baseURL="https://YOUR_MEDIA_DOMAIN_NAME/"
admin.gateway="cp-admin"
auth.gateway="cp-auth"
analytics.salt="RANDOM_STRING_OF_64_CHARACTERS"
#--------------------------------------------------------------------
# Database configuration
@ -27,6 +28,14 @@ database.default.username="root"
database.default.password="****"
database.default.DBPrefix="cp_"
#--------------------------------------------------------------------
# Email configuration
#--------------------------------------------------------------------
# email.fromEmail="your_email_address"
# email.SMTPHost="your_smtp_host"
# email.SMTPUser="your_smtp_user"
# email.SMTPPass="your_smtp_password"
#--------------------------------------------------------------------
# Cache configuration (advanced)
#
@ -41,3 +50,21 @@ cache.handler="file"
# cache.redis.password=null
# cache.redis.port=6379
# cache.redis.database=0
#--------------------------------------------------------------------
# S3 configuration
#--------------------------------------------------------------------
# media.fileManager="s3"
# media.s3.endpoint="your_s3_host"
# media.s3.key="your_s3_key"
# media.s3.secret="your_s3_secret"
# media.s3.region="your_s3_region"
#--------------------------------------------------------------------
# REST API configuration
#--------------------------------------------------------------------
# restapi.enabled=true
# restapi.basicAuthUsername=castopod
# restapi.basicAuthPassword=password
# restapi.basicAuth=true

View file

@ -1,18 +0,0 @@
{
"env": {
"browser": true,
"es2020": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {}
}

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
open_collective: castopod

66
.gitignore vendored
View file

@ -60,10 +60,14 @@ writable/logs/*
writable/session/*
!writable/session/index.html
writable/temp/*
!writable/temp/index.html
writable/uploads/*
!writable/uploads/index.html
writable/debugbar/*
!writable/debugbar/index.html
php_errors.log
@ -82,6 +86,7 @@ tests/coverage*
# Don't save phpunit under version control.
phpunit
.phpunit.cache
#-------------------------
# Composer
@ -102,15 +107,15 @@ _modules/*
.idea/
*.iml
# Netbeans
nbproject/
build/
nbbuild/
dist/
nbdist/
nbactions.xml
nb-configuration.xml
.nb-gradle/
# NetBeans
/nbproject/
/build/
/nbbuild/
/dist/
/nbdist/
/nbactions.xml
/nb-configuration.xml
/.nb-gradle/
# Sublime Text
*.tmlanguage.cache
@ -123,14 +128,16 @@ nb-configuration.xml
# Visual Studio Code
.vscode/
.history/
tmp/
/results/
/phpunit*.xml
/.phpunit.*.cache
# npm
# js package manager
yarn.lock
node_modules
.pnpm-store
# JS
.cache
@ -140,14 +147,21 @@ public/*
!public/media
!public/.htaccess
!public/favicon.ico
!public/icon*
!public/castopod-banner*
!public/castopod-avatar*
!public/index.php
!public/robots.txt
!public/.well-known
!public/.well-known/GDPR.yml
public/assets/*
!public/assets/index.html
# public media folder
public/media/*
!public/media/index.html
!public/media/podcasts
!public/media/persons
!public/media/site
public/media/podcasts/*
!public/media/podcasts/index.html
@ -155,19 +169,19 @@ public/media/podcasts/*
public/media/persons/*
!public/media/persons/index.html
public/media/site/*
!public/media/site/index.html
# Generated files
app/Language/en/PersonsTaxonomy.php
app/Language/fr/PersonsTaxonomy.php
modules/Admin/Language/*/PersonsTaxonomy.php
#-------------------------
# Docker volumes
#-------------------------
# Castopod bundle & packages
castopod/
castopod-*.zip
castopod-*.tar.gz
mariadb
phpmyadmin
sessions
# Castopod Host bundle & packages
castopod-host/
castopod-host-*.zip
castopod-host-*.tar.gz
# Plugins
plugins/*
!plugins/.gitkeep
writable/plugins.json
writable/plugins-lock.json

View file

@ -1,30 +1,52 @@
image: code.podlibre.org:5050/podlibre/castopod-host:latest
image: code.castopod.org:5050/adaures/castopod:ci-php8.5
stages:
- prepare
- quality
- bundle
- release
- deploy
- build
php-dependencies:
stage: prepare
script:
# Install all php dependencies
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs
cache:
key:
files:
- composer.lock
paths:
- .composer-cache
artifacts:
expire_in: 30 mins
paths:
- vendor/
expire_in: 30 mins
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
js-dependencies:
stage: prepare
script:
# Install all npm dependencies
- npm ci
# Install all js dependencies
- pnpm install
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
artifacts:
expire_in: 30 mins
paths:
- node_modules/
expire_in: 30 mins
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
lint-commit-msg:
stage: quality
@ -34,6 +56,10 @@ lint-commit-msg:
- ./scripts/lint-commit-msg.sh
dependencies:
- js-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- if: $CI_COMMIT_BRANCH =~ /^(develop|main|alpha|beta|next)$/
lint-php:
stage: quality
@ -46,25 +72,46 @@ lint-php:
- vendor/bin/rector process --dry-run --ansi
dependencies:
- php-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
lint-js:
stage: quality
script:
- npm run prettier
- npm run typecheck
- npm run lint
- npm run lint:css
- pnpm run format
- pnpm run typecheck
- pnpm run lint
- pnpm run lint:css
dependencies:
- js-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
tests:
stage: quality
services:
- mariadb:10.11
variables:
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
MYSQL_DATABASE: "test"
MYSQL_USER: "castopod"
MYSQL_PASSWORD: "castopod"
script:
- echo "SHOW DATABASES;" | mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip_ssl
# run phpunit without code coverage
# TODO: add code coverage
- vendor/bin/phpunit --no-coverage
dependencies:
- php-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
bundle:
stage: bundle
@ -76,19 +123,21 @@ bundle:
# make scripts/bundle.sh executable
- chmod +x ./scripts/bundle.sh
# bundle castopod-host with commit ref as version
# bundle castopod with commit ref as version
- ./scripts/bundle.sh ${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}
dependencies:
- php-dependencies
- js-dependencies
artifacts:
name: "castopod-host-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
name: "castopod-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
paths:
- castopod-host
except:
- main
- beta
- alpha
- castopod
rules:
- if: $CI_PROJECT_NAMESPACE != "adaures"
when: never
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/ || $CI_COMMIT_TAG
when: never
- when: on_success
release:
stage: release
@ -105,11 +154,45 @@ release:
- chmod +x ./scripts/package.sh
# run semantic-release script (configured in `.releaserc.json` file)
- npm run release
- pnpm run release
dependencies:
- php-dependencies
- js-dependencies
only:
- main
- alpha
- beta
artifacts:
paths:
- castopod
rules:
- if: $CI_PROJECT_NAMESPACE != "adaures"
when: never
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/
website:
stage: deploy
trigger: adaures/castopod.org
rules:
- if: $CI_PROJECT_NAMESPACE != "adaures"
when: never
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG
documentation:
stage: deploy
trigger:
include: docs/.gitlab-ci.yml
strategy: depend
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
docker:
stage: build
trigger:
include: docker/production/.gitlab-ci.yml
strategy: depend
rules:
- if: $CI_PROJECT_NAMESPACE != "adaures"
when: never
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG

View file

@ -6,7 +6,7 @@
1. [First step]
2. [Second step]
3. [and so on...]
3. [and so on]
### Expected behavior
@ -27,7 +27,7 @@ logs, and code as it's very hard to read otherwise.
- OS: [e.g. Ubuntu server]
- Browser: [e.g. chrome, safari]
- Web server: [eg. Apache]
- [any other relevant context...]
- [any other relevant context]
### Possible fixes

View file

@ -0,0 +1,17 @@
### Before submitting an issue
1. **Use the issue search** &mdash; check if the issue has already been
reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest release.
3. **Isolate the problem** &mdash; ideally create a
[reduced test case](https://css-tricks.com/reduced-test-cases/) and a live
example.
4. **Select an issue template** &mdash; choose a template from `bug` or
`feature-request` and fill out the info you deem necessary. The more context
we get, the easier it is to implement the feature or fix the bug you report.
Check out the [CONTRIBUTING manual](../../CONTRIBUTING.md) for more info.

View file

@ -1,7 +1,7 @@
### Is your feature request related to a problem? Please describe
A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
frustrated when []
### Describe the solution you'd like

View file

@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --verbose --edit "$1"
pnpm exec commitlint --verbose --edit "$1"

View file

@ -1,11 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# CaptainHook 5.10.0
INTERACTIVE="--no-interaction"
vendor/bin/captainhook $INTERACTIVE --configuration=captainhook.json --bootstrap=vendor/autoload.php hook:pre-commit "$@" <&0
npm run typecheck
npx lint-staged
pnpm run typecheck
pnpm exec lint-staged

View file

@ -1,6 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# CaptainHook 5.10.0
INTERACTIVE="--no-interaction"

View file

@ -2,7 +2,7 @@
"trailingComma": "es5",
"overrides": [
{
"files": "*.md",
"files": ["*.md", "*.mdx"],
"options": {
"proseWrap": "always"
}

View file

@ -1,17 +1,86 @@
{
"branches": [
"main",
{ "name": "alpha", "prerelease": true },
{ "name": "beta", "prerelease": true }
{
"name": "alpha",
"prerelease": true
},
{
"name": "beta",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{
"type": "docs",
"scope": "README",
"release": "patch"
},
{
"type": "refactor",
"scope": "core-*",
"release": "minor"
},
{
"type": "refactor",
"release": "patch"
}
],
"parserOpts": {
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
}
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"parserOpts": {
"noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
},
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "chore",
"section": "Internal",
"hidden": false
},
{
"type": "refactor",
"section": "Internal",
"hidden": false
},
{
"type": "perf",
"section": "Internal",
"hidden": false
}
]
}
}
],
"@semantic-release/changelog",
[
"@semantic-release/exec",
{
"prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh ${nextRelease.version} && npx prettier --write CHANGELOG.md"
"prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh ${nextRelease.version} && pnpm exec prettier --write CHANGELOG.md"
}
],
"@semantic-release/npm",
@ -24,21 +93,22 @@
"package.json",
"package-lock.json",
"CHANGELOG.md"
]
],
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/gitlab",
{
"gitlabUrl": "https://code.podlibre.org/",
"gitlabUrl": "https://code.castopod.org/",
"assets": [
{
"path": "castopod-host-*.zip",
"label": "Castopod Host Package (zip)"
"path": "castopod-*.zip",
"label": "Castopod Package (zip)"
},
{
"path": "castopod-host-*.tar.gz",
"label": "Castopod Host Package (tar.gz)"
"path": "castopod-*.tar.gz",
"label": "Castopod Package (tar.gz)"
}
]
}

View file

@ -1,14 +1,17 @@
# rsync filter rules to copy required files for Castopod Host's bundle
# rsync filter rules to copy required files for Castopod's bundle
- app/Views/_assets/
+ resources/icons/***
+ resources/
+ app/***
+ modules/***
+ plugins/***
+ public/***
+ themes/***
+ vendor/***
+ writable/***
+ .env.example
+ DEPENDENCIES.md
+ LICENSE.md
+ README.md
+ INSTALL.md
+ UPDATE.md
+ spark
+ php-icons.php
- **

View file

@ -1,5 +1,5 @@
{
"extends": "stylelint-config-recommended",
"extends": "stylelint-config-standard",
"rules": {
"at-rule-no-unknown": [
true,
@ -10,10 +10,24 @@
"responsive",
"variants",
"screen",
"layer"
"layer",
"config"
]
}
],
"no-descending-specificity": null
"at-rule-no-deprecated": [
true,
{
"ignoreAtRules": ["apply"]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme"]
}
],
"no-descending-specificity": null,
"selector-class-pattern": null
}
}

View file

View file

@ -1,4 +1,4 @@
# Authors
- [Benjamin Bellamy](https://code.podlibre.org/benjamin) <ben@podlibre.org>
- [Yassine Doghri](https://code.podlibre.org/yassine) <yassine@podlibre.org>
- [Benjamin Bellamy](https://code.castopod.org/benjamin) <ben@castopod.org>
- [Yassine Doghri](https://code.castopod.org/yassine) <yassine@castopod.org>

View file

@ -1,868 +1,3887 @@
# [1.0.0-alpha.80](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.79...v1.0.0-alpha.80) (2021-12-29)
## [2.0.0-next.3](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.2...v2.0.0-next.3) (2024-12-30)
### Features
- **api:** add Episode create and publish endpoints
([a90cdfd](https://code.castopod.org/adaures/castopod/commit/a90cdfdcdbde7a8fb520c6815d7b757947aea055))
- **image:** add image size's width and height
([f50098e](https://code.castopod.org/adaures/castopod/commit/f50098ec8926c8ae40718f5f128b6de7fe721b46))
- **plugins:** add defaultValue for all field types
([d3a98db](https://code.castopod.org/adaures/castopod/commit/d3a98db6d0112b5f59daddd2708c09dd2e595332))
- **plugins:** add group field type + multiple option to render field arrays
([11ccd0e](https://code.castopod.org/adaures/castopod/commit/11ccd0ebe71d476d8c0dbfe28edcf01f7f362b83))
- **plugins:** add html field type + CodeEditor component + rework html head
generation
([8cf9c6d](https://code.castopod.org/adaures/castopod/commit/8cf9c6dc833aedcccbc4cdb309b111f84d97d629))
- **rss:** add option for 301 redirect to new feed url
([8402cc2](https://code.castopod.org/adaures/castopod/commit/8402cc29d2d0c61b014a7e03e5ccce7d3c11782a))
### Bug Fixes
- add downloads_count to episodes table, computed every hour
([f981937](https://code.castopod.org/adaures/castopod/commit/f9819376455c371eb5bd3c84ad938698335a3d67))
- allow passing json to app.proxyIPs config to set it
([cbf739e](https://code.castopod.org/adaures/castopod/commit/cbf739e95cc0ad6e83a21353b8f4678e68d74f63))
- **api:** cast integers when creating episode
([775b302](https://code.castopod.org/adaures/castopod/commit/775b302f7c886e30e133c8a8c68764301b6c663b))
- **docker-image:** clear cache to account for new assets and data structure
changes
([63c763f](https://code.castopod.org/adaures/castopod/commit/63c763f941195b3758c4b91acd8c350a5e7bb9c2)),
closes [#510](https://code.castopod.org/adaures/castopod/issues/510)
- edit remap functions to get episode in episode admin controllers
([9f74cca](https://code.castopod.org/adaures/castopod/commit/9f74cca342fedd896977efd2e89d0143959f3c4f))
- **episode:** do not change slug when editing episode title
([a83afb0](https://code.castopod.org/adaures/castopod/commit/a83afb0004511db80337806577fbc36f8d777116)),
closes [#513](https://code.castopod.org/adaures/castopod/issues/513)
- **fediverse:** add "processing" and "failed" statuses to better manage
broadcast load
([1d7583d](https://code.castopod.org/adaures/castopod/commit/1d7583d738219574ae3d45d294dc94e7e406472b)),
closes [#511](https://code.castopod.org/adaures/castopod/issues/511)
- **icons:** set correct names for lock and lock-unlock icons in premium banner
([37ee6d3](https://code.castopod.org/adaures/castopod/commit/37ee6d35b4bb66ce23dc271fb846200d1be0e7f6))
- **plugins:** clear cache after activating or deactivating plugin
([08c7df2](https://code.castopod.org/adaures/castopod/commit/08c7df2a5d5be340490c78deeef823167eb1b2fc))
- **plugins:** delete relevant cache when submitting settings
([00bd4c0](https://code.castopod.org/adaures/castopod/commit/00bd4c02ee23b181d74e7731626bfec3b1ff4916))
- **podcast-model:** always query podcast from database when clearing cache
([d30c49c](https://code.castopod.org/adaures/castopod/commit/d30c49cdff380c15db4f1851631a255a5baffcbe))
- **premium-podcasts:** update query to validate subscription
([2b1bbf3](https://code.castopod.org/adaures/castopod/commit/2b1bbf34303ead927f433b5c7d5d888ca3799954))
- **preview:** delete episode preview cache after editing episode
([732d429](https://code.castopod.org/adaures/castopod/commit/732d42923d0d7a66ff1ebd5841458e4205060560)),
closes [#514](https://code.castopod.org/adaures/castopod/issues/514)
- **release:** add conventional-changelog-conventionalcommits for CHANGELOG
generation
([6934c8a](https://code.castopod.org/adaures/castopod/commit/6934c8aa8f0b7f9eea7c3f6f4089c56b2391d9a6))
- **rss:** add subscription id to cache name to prevent premium feeds from
overlapping
([74f9325](https://code.castopod.org/adaures/castopod/commit/74f9325946d03a0d4efce57045e41cc9454ff97c))
- set user as www-data when running cron jobs in docker's supervisord config
([65d74f1](https://code.castopod.org/adaures/castopod/commit/65d74f14e612be3757c9304518eee112705f5ff9))
- typo in EpisodeController remap function to get episode
([f288a75](https://code.castopod.org/adaures/castopod/commit/f288a750f580ab19b04a170cc76bf8769084e19d))
- update select and multi-select options to value/label arrays
([63f93f5](https://code.castopod.org/adaures/castopod/commit/63f93f585bec4a11022cc8c75deb34968cba2348))
### Internal
- **plugins:** create Field objects per field type in settings forms + handle
rendering in class
([34be5bc](https://code.castopod.org/adaures/castopod/commit/34be5bccabb7531afdcc6ebaf1dd39e4dfbe0677))
- remove fields from podcast and episode entities to be replaced with plugins
([b869acb](https://code.castopod.org/adaures/castopod/commit/b869acb3a988a3616d883a41c25d9c8409bd5518))
- rename controller methods for views and actions to be more consistent
([85704bf](https://code.castopod.org/adaures/castopod/commit/85704bfbe03fe5e38ff5e76a0e1cf0e5f1275f57))
- update CodeIgniter to v4.5.6
([f295e9a](https://code.castopod.org/adaures/castopod/commit/f295e9aa4ca3129df24a22779f7c19bba7fac370))
- update codigniter-icons to v1.0.1
([fa6967e](https://code.castopod.org/adaures/castopod/commit/fa6967e65cef1705b19cbb205132c4c751507d53))
- update js dependencies to latest
([70c9797](https://code.castopod.org/adaures/castopod/commit/70c97971fcf5bbeee826578057ae0e3afbbbd8a8))
# [2.0.0-next.2](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.1...v2.0.0-next.2) (2024-07-08)
### Bug Fixes
- **audio-player:** set player icons to default instead of missing Castopod's
([0ba0a25](https://code.castopod.org/adaures/castopod/commit/0ba0a25b11bd67aeeb47a8179b72152dfd4a36da))
- broken icon call in frontend default pages template
([3228362](https://code.castopod.org/adaures/castopod/commit/322836254e86be7878e21438177ee8f73f03a2fa))
- **manifest:** set repository url as required in docstring typings
([a8c81b3](https://code.castopod.org/adaures/castopod/commit/a8c81b3fa19a28dbd608027c231dcac31eafb38f))
- set correct icons parameters in map and funding links views
([5d35524](https://code.castopod.org/adaures/castopod/commit/5d355248753be24e3cf324144ff076f2fc23be88)),
closes [#500](https://code.castopod.org/adaures/castopod/issues/500)
### Features
- **plugins:** add `minCastopodVersion` to denote incompatibility with previous
Castopod versions
([fc9ea75](https://code.castopod.org/adaures/castopod/commit/fc9ea7597e454e5c7c7af043d29af7bbe119e342))
- **plugins:** load and display LICENSE.md file if found in plugin's directory
([fee7905](https://code.castopod.org/adaures/castopod/commit/fee7905935a9adf963b4485b437fe4d972c14b5f))
# [2.0.0-next.1](https://code.castopod.org/adaures/castopod/compare/v1.11.0...v2.0.0-next.1) (6/19/2024)
### Bug Fixes
- add missing php-icons config file to bundle
([56612f0](https://code.castopod.org/adaures/castopod/commit/56612f0c762aa2d98e3c8c77fba88ffdf6f46a44))
- **docs:** add base to og image using env variable
([fe67659](https://code.castopod.org/adaures/castopod/commit/fe676590f23a33bdbe8905d234760923c029e350))
- **import:** rewrite download_file helper to output curl response directly to
file
([eb7ad2f](https://code.castopod.org/adaures/castopod/commit/eb7ad2f7e1c0137f222f47e47062887de42c4824))
- include app/Resources/icons folder to bundle
([3fd5efc](https://code.castopod.org/adaures/castopod/commit/3fd5efc7956977acc19e53182f25b12813964a7d))
- **platforms:** add platforms service + reduce memory consumption when
rendering platform cards
([fe73e9f](https://code.castopod.org/adaures/castopod/commit/fe73e9fae9ea5d5ce946680aec194308bb2e620c))
- set owner email visibility when editing podcast
([fc4f982](https://code.castopod.org/adaures/castopod/commit/fc4f9825568cd4384c5b3cfe972accd146548807)),
closes [#473](https://code.castopod.org/adaures/castopod/issues/473)
### Build System
- release next major version as prerelease
([8275226](https://code.castopod.org/adaures/castopod/commit/827522643e9f8a5ea9be05b4847dc637f0f43a13))
### Features
- add Plugins module with base files for plugins architecture
([7253e13](https://code.castopod.org/adaures/castopod/commit/7253e13ac2118f6f165f54ea0cbcd63d51ab9205))
- **plugins:** abstract settings form for general, podcast and episode types
([b62b483](https://code.castopod.org/adaures/castopod/commit/b62b483ad9ff114a22a9ee52e1a1a2c9fa444d42))
- **plugins:** activate / deactivate plugin using settings table
([27d2a1b](https://code.castopod.org/adaures/castopod/commit/27d2a1b0ffba9454dd54cbb4251a2d179b09762a))
- **plugins:** add aside with plugin metadata next to plugin's readme
([dfb7888](https://code.castopod.org/adaures/castopod/commit/dfb7888aeb689b4066abc37084e08cd7f1d0f15d))
- **plugins:** add before channel/item hooks to allow podcast/episode data edit
when generating rss
([80d2c48](https://code.castopod.org/adaures/castopod/commit/80d2c48ee265cb32ed0d710c488292fcbc120044))
- **plugins:** add json schema definition for plugin manifest
([b5eddf3](https://code.castopod.org/adaures/castopod/commit/b5eddf351f6f6fa1c299fbac31cbd056ef232330))
- **plugins:** add methods to easily retrieve general, podcast and episode
settings in hooks methods
([3a900bb](https://code.castopod.org/adaures/castopod/commit/3a900bbab68b819cedf8943540d2ee0aeb6e8539))
- **plugins:** add new field types + validate & cast user data before storing
settings
([6f833fc](https://code.castopod.org/adaures/castopod/commit/6f833fc76a3aa6c6b87c27ad18a2fb90e537e21e))
- **plugins:** add options to manifest for building forms and storing plugin
settings
([3d8aedf](https://code.castopod.org/adaures/castopod/commit/3d8aedf9c34e6927b6d3b11445d5f0e669b347d7))
- **plugins:** add settings page for podcast and episode if defined in the
plugin's manifest
([89ac92f](https://code.castopod.org/adaures/castopod/commit/89ac92fb412a04231ce52fd6480c9ab893b19ef5))
- **plugins:** add siteHead hook to add custom meta tags to public pages
([e80a33b](https://code.castopod.org/adaures/castopod/commit/e80a33bf2ad4fe1b47037add7470a6c2770f4036))
- **plugins:** display errors when plugin is invalid instead of crashing
([8ec7909](https://code.castopod.org/adaures/castopod/commit/8ec79097bbdbcbce622518ef61c068f20e0ef74e))
- **plugins:** handle empty states and long strings in UI
([45ac2a4](https://code.castopod.org/adaures/castopod/commit/45ac2a4be96532b9456e6af1d26ba4ada3649303))
- **plugins:** load and validate plugin manifest.json
([1510e36](https://code.castopod.org/adaures/castopod/commit/1510e36c0acd2b254622ec230acd1d2461ee9bf3))
- **plugins:** load plugins using file locator service
([587938d](https://code.castopod.org/adaures/castopod/commit/587938d2bf307b823af143586b9ec9e9b44e8dc1))
- **plugins:** load README.md file to view plugin's instructions in UI
([e6bfdfc](https://code.castopod.org/adaures/castopod/commit/e6bfdfc3902705285701c13c8067fe0f538425c6))
- **plugins:** register plugins using Plugin.php file instead of namespace +
simplify i18n structure
([2035c39](https://code.castopod.org/adaures/castopod/commit/2035c39fd138a1fd408516bd1972ab6a02544c10))
- **plugins:** uninstall plugins via CLI and admin UI
([9a80de4](https://code.castopod.org/adaures/castopod/commit/9a80de40686bbf4288da21cc2a6dde8036580e47))
- set owner email to hidden by default in podcast create form
([7a6d9df](https://code.castopod.org/adaures/castopod/commit/7a6d9df6db8a6184b8250ced0475f3e741dde7f4))
- support podcast:txt tag with verify use case
([57e459e](https://code.castopod.org/adaures/castopod/commit/57e459e187ed048430f4137172e22396cd02bf81)),
closes [#468](https://code.castopod.org/adaures/castopod/issues/468)
### BREAKING CHANGES
- next major release including plugins architecture
# [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)
### Bug Fixes
- **file-uploads:** validate chapters json content + remove permit_empty rule to
uploaded files
([6289c42](https://code.castopod.org/adaures/castopod/commit/6289c42b1189f074c7e4e4cd9fbfd73bf26625c9)),
closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
## [1.10.4](https://code.castopod.org/adaures/castopod/compare/v1.10.3...v1.10.4) (2/26/2024)
### Bug Fixes
- display chapters in episode preview page
([797516a](https://code.castopod.org/adaures/castopod/commit/797516a2ec7d88704412a5cca50421e8eef38eec)),
closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
## [1.10.3](https://code.castopod.org/adaures/castopod/compare/v1.10.2...v1.10.3) (2/21/2024)
### Bug Fixes
- **chapters:** use episode cover when chapter img is an empty string
([a343de4](https://code.castopod.org/adaures/castopod/commit/a343de4cf6ba38561b8fe675fa9c38d9f0ecfec7)),
closes [#444](https://code.castopod.org/adaures/castopod/issues/444)
- **import:** set episodes as premium if podcast is set as premium by default
([dfd66be](https://code.castopod.org/adaures/castopod/commit/dfd66beebfcca1670b0a9d389e8e3f8d2d08d2f2))
## [1.10.2](https://code.castopod.org/adaures/castopod/compare/v1.10.1...v1.10.2) (2/20/2024)
### Bug Fixes
- **podcast-import:** move closing parenthasis when checking for owner name and
email existence
([cec7815](https://code.castopod.org/adaures/castopod/commit/cec78155f94a222edcf7964c0a2f3a3e0f46a98d))
## [1.10.1](https://code.castopod.org/adaures/castopod/compare/v1.10.0...v1.10.1) (2/20/2024)
### Bug Fixes
- **fediverse:** use config name to get Fediverse config properties instead of
hardcoded class string
([5fd0980](https://code.castopod.org/adaures/castopod/commit/5fd0980ff7101d45051a2daa3f635694f85609d7))
# [1.10.0](https://code.castopod.org/adaures/castopod/compare/v1.9.0...v1.10.0) (2/19/2024)
### Bug Fixes
- **op3:** move op3 prefix to enclosure url instead of audio proxy
([d580369](https://code.castopod.org/adaures/castopod/commit/d5803692357952d82d54efd8d3aa71de3a1c9571))
- **podcast-import:** rollback transaction before exception is thrown
([419bb04](https://code.castopod.org/adaures/castopod/commit/419bb04716088586b87b2c8f24a954ca8cfd6c76)),
closes [#429](https://code.castopod.org/adaures/castopod/issues/429)
[#319](https://code.castopod.org/adaures/castopod/issues/319)
[#443](https://code.castopod.org/adaures/castopod/issues/443)
[#438](https://code.castopod.org/adaures/castopod/issues/438)
### Features
- add podcast:season and podcast:episode tags to rss feed
([98c6658](https://code.castopod.org/adaures/castopod/commit/98c6658840eedd55bd6d8042f8a69c342b87cd71))
- add support for podcasting 2.0 "medium" tag with podcast, music and audiobook
([630e788](https://code.castopod.org/adaures/castopod/commit/630e788f0e1ddfe5de229bd415a8e15361efa746)),
closes [#439](https://code.castopod.org/adaures/castopod/issues/439)
- display chapters in episode's public page
([87cc437](https://code.castopod.org/adaures/castopod/commit/87cc437e1ead5486ed46ca37e2055aaf5c9445c1)),
closes [#423](https://code.castopod.org/adaures/castopod/issues/423)
- support VTT transcript file format in addition to SRT
([7071b4b](https://code.castopod.org/adaures/castopod/commit/7071b4b6f48cb9a2f766064f3a5c23f92b293718)),
closes [#433](https://code.castopod.org/adaures/castopod/issues/433)
# [1.9.0](https://code.castopod.org/adaures/castopod/compare/v1.8.2...v1.9.0) (1/31/2024)
### Bug Fixes
- **i18n:** escape language strings in form fields to prevent them from
disappearing
([3cb5ffd](https://code.castopod.org/adaures/castopod/commit/3cb5ffd25b9604a83cd12935e641dab7c88fba47)),
closes [#412](https://code.castopod.org/adaures/castopod/issues/412)
- **podcast-about:** update stats query to discard scheduled episodes from
episodes number
([67c037c](https://code.castopod.org/adaures/castopod/commit/67c037c9eb1e15c6945eaf74ec0ff30b33f4b704))
- **premium-subs:** clear subscription list cache after insert
([2accb0f](https://code.castopod.org/adaures/castopod/commit/2accb0f7652330b29c3adb85a2e1b0d5d83f1389)),
closes [#430](https://code.castopod.org/adaures/castopod/issues/430)
- **s3:** remove proxy, set objects acl to public-read, and serve files using
their public urls
([6a77a9d](https://code.castopod.org/adaures/castopod/commit/6a77a9d2f29c849775a3d1bcbd819f73f21d9aa6))
### Features
- add actor domain to handle in follow page
([de099ac](https://code.castopod.org/adaures/castopod/commit/de099ac64300b8edb86e387fde89c0a3e9472f46))
- **admin:** add podcast's OP3 analytics dashboard link
([5f3752b](https://code.castopod.org/adaures/castopod/commit/5f3752b4430f6f2d5f9e5f6a7a003bc4d2f9d487))
## [1.8.2](https://code.castopod.org/adaures/castopod/compare/v1.8.1...v1.8.2) (1/17/2024)
### Bug Fixes
- **transcript:** add condition when concatenating sub text to prevent second
line duplication
([6cbfec0](https://code.castopod.org/adaures/castopod/commit/6cbfec0d7d9bf85c8014d379026648857ea13373))
## [1.8.1](https://code.castopod.org/adaures/castopod/compare/v1.8.0...v1.8.1) (1/16/2024)
### Bug Fixes
- **models:** set updatedField as empty string when not used
([164f4d3](https://code.castopod.org/adaures/castopod/commit/164f4d3be74ec8d371fb40d7fe730f7b2940ca05))
# [1.8.0](https://code.castopod.org/adaures/castopod/compare/v1.7.4...v1.8.0) (1/15/2024)
### Bug Fixes
- **episode-form:** add required validation rules for title and slug
([30a3473](https://code.castopod.org/adaures/castopod/commit/30a34738635bf4f4a4c6b2a7174f7e439f0dfc6e)),
closes [#420](https://code.castopod.org/adaures/castopod/issues/420)
- **import:** check for empty string when generating podcast guid for feeds not
including one
([ac5336f](https://code.castopod.org/adaures/castopod/commit/ac5336fbc5fb8038de541dd06938a8beb2e8d733))
- **install:** add created superadmin to most powerful group in instance, ie.
superadmin
([2ed511f](https://code.castopod.org/adaures/castopod/commit/2ed511f8a0005dc06eda5afd6b1d13beee1eb9dd))
- **persons:** delete person avatar when deleting a person
([c1ec98c](https://code.castopod.org/adaures/castopod/commit/c1ec98c95656844712011ff30b84c397b78da311)),
closes [#419](https://code.castopod.org/adaures/castopod/issues/419)
- **platforms:** add matrix.org as a social platform
([9178c3f](https://code.castopod.org/adaures/castopod/commit/9178c3f3afa16e104d25ae159728e90a3bbd57c3)),
closes [#421](https://code.castopod.org/adaures/castopod/issues/421)
### Features
- **admin:** add tooltip for not authorized routes
([f7f9baf](https://code.castopod.org/adaures/castopod/commit/f7f9bafc3e56621fab2569d9d76baafe0a2e940d))
- **admin:** emphasize unprivileged items in sidebar with "prohibited" icon
([0bd7dde](https://code.castopod.org/adaures/castopod/commit/0bd7ddea58adf502121b83e5c09317e20912fb4e))
- allow hiding owner's email in public RSS feed
([222e02a](https://code.castopod.org/adaures/castopod/commit/222e02a2af9ecb8b8768a63d3054f4c3ef54e991))
- **persons:** order persons by full_name ASC for easier list scanning
([68a599f](https://code.castopod.org/adaures/castopod/commit/68a599fee08c71763b9336e14b1c0d9e28c4449b)),
closes [#418](https://code.castopod.org/adaures/castopod/issues/418)
## [1.7.4](https://code.castopod.org/adaures/castopod/compare/v1.7.3...v1.7.4) (1/3/2024)
### Bug Fixes
- **media:** add missing HEAD route for static assets served with S3
([b61a32c](https://code.castopod.org/adaures/castopod/commit/b61a32c8a9b10e129666804d533487430ce7432c))
## [1.7.3](https://code.castopod.org/adaures/castopod/compare/v1.7.2...v1.7.3) (12/21/2023)
### Bug Fixes
- **analytics:** upgrade opawg's user-agents-php to user-agents-v2-php
([8cd7886](https://code.castopod.org/adaures/castopod/commit/8cd78866762e26aa63c224dace6c247e0e9dc068))
- **platforms:** add Threads and YouTube Music
([9264a2d](https://code.castopod.org/adaures/castopod/commit/9264a2d74cc95278c9d84c99ef914fdbcaf8a97f))
## [1.7.2](https://code.castopod.org/adaures/castopod/compare/v1.7.1...v1.7.2) (12/12/2023)
### Bug Fixes
- **episode-form:** render episode number optional when episode type is trailer
or bonus
([694328f](https://code.castopod.org/adaures/castopod/commit/694328f10865b2fcd6436122de46866dae81f945))
## [1.7.1](https://code.castopod.org/adaures/castopod/compare/v1.7.0...v1.7.1) (12/1/2023)
### Bug Fixes
- **housekeeping:** add where clause to check episode_id is not null on reset
comments count
([119742c](https://code.castopod.org/adaures/castopod/commit/119742cdbb2c2f7f847692fb76f6ff1dbb2e25b6))
# [1.7.0](https://code.castopod.org/adaures/castopod/compare/v1.6.5...v1.7.0) (11/29/2023)
### Bug Fixes
- **admin-ux:** hide navigation submenus in details panel for easier scanning
([b047a3c](https://code.castopod.org/adaures/castopod/commit/b047a3c6707114d04c276758f2e543eef90d72f5))
- **admin:** remove episode title truncation + display description in two lines
in episode list
([f4ffa30](https://code.castopod.org/adaures/castopod/commit/f4ffa30ec4341f43e22b1f983781ad04c956aa25)),
closes [#386](https://code.castopod.org/adaures/castopod/issues/386)
- **auth:** display error messages from validator
([5a834c0](https://code.castopod.org/adaures/castopod/commit/5a834c0f8957fc016e73325a3c3ff05e524d0755))
- **housekeeping:** remove unnecessary $tablePrefix variable when resetting post
count
([97d793f](https://code.castopod.org/adaures/castopod/commit/97d793f55e7eb3b049980e5081950baa2bb1b881)),
closes [#383](https://code.castopod.org/adaures/castopod/issues/383)
- **import:** handle bad values for location attributes
([642981f](https://code.castopod.org/adaures/castopod/commit/642981fd358ccf118d3d7a957fb6be7933c016ac))
- **import:** use cocur/slugify library to handle non latin text
([4ca7f9c](https://code.castopod.org/adaures/castopod/commit/4ca7f9ccae1e352bf26a3b6db4de73bac7b84382))
- move monetization outside of podcast form + add broadcast section to podcast
menu
([dff8516](https://code.castopod.org/adaures/castopod/commit/dff85168b32a6df77425ef51865588ebcd8b8ba9))
- **nodeinfo2:** import database config + use dynamic table prefix for active
local actors query
([6a7ef01](https://code.castopod.org/adaures/castopod/commit/6a7ef0109a6e52144ca687b979ffe56fba66165b))
- **persons:** set roles field as optional + set `Cast > Host` as default value
([02132dc](https://code.castopod.org/adaures/castopod/commit/02132dc46640807e2bc4cfc406c911fa097f36fe)),
closes [#347](https://code.castopod.org/adaures/castopod/issues/347)
- **platforms:** make platforms' websites and submit urls more prominent
([61cf8fa](https://code.castopod.org/adaures/castopod/commit/61cf8fa3e2435ee2a9bdd8e711b8d69d4ca4ec4c))
- **podcast-form:** move fediverse section below author section
([1861d67](https://code.castopod.org/adaures/castopod/commit/1861d67971e2cc0c20ace091f037f6436437a50d))
- reorder podcast form fields + extract sync feeds to its own form
([2d52fa1](https://code.castopod.org/adaures/castopod/commit/2d52fa1046faf1b8d81304e35fc24a7874315e6e))
### Features
- **admin:** add rss feed link to podcast side navigation
([18e2633](https://code.castopod.org/adaures/castopod/commit/18e2633a49dbbeb57a685f129a2ab158397de61e))
- **icons:** update new Deezer logo
([f2d5b27](https://code.castopod.org/adaures/castopod/commit/f2d5b272ac385a978d7e173121faafe03d7a7200))
- **install:** init database and create superadmin using CLI
([02d4ba6](https://code.castopod.org/adaures/castopod/commit/02d4ba69ac007ebd1eccab428a98b54051aaf70c)),
closes [#380](https://code.castopod.org/adaures/castopod/issues/380)
- **ux:** add episode description to episode cards
([5f8d413](https://code.castopod.org/adaures/castopod/commit/5f8d413b84b236077a75934da9409f37d34cb4a5))
## [1.6.5](https://code.castopod.org/adaures/castopod/compare/v1.6.4...v1.6.5) (2023-09-26)
### Bug Fixes
- **fediverse:** use NoteObject including episode link in content (hotfix)
([ffa530e](https://code.castopod.org/adaures/castopod/commit/ffa530e187ff6488648a7cf749ca0173765a5d87))
## [1.6.4](https://code.castopod.org/adaures/castopod/compare/v1.6.3...v1.6.4) (2023-09-17)
### Bug Fixes
- **fediverse:** do not cache remote action form + fix typo on post routes for
passing post uuid
([4ecb42f](https://code.castopod.org/adaures/castopod/commit/4ecb42f7c82eb8d41d27c7b9705b3278ea04ab79))
- **fediverse:** update post controller namespace in routes
([3189f12](https://code.castopod.org/adaures/castopod/commit/3189f122067dc47d6de93c3185aca66d7df95e1a))
## [1.6.3](https://code.castopod.org/adaures/castopod/compare/v1.6.2...v1.6.3) (2023-09-14)
### Bug Fixes
- **fediverse:** add `index` to post controller-method to access post's jsonld
contents
([35142d8](https://code.castopod.org/adaures/castopod/commit/35142d8e565e828a977ba2b4de77c1b47a633beb))
## [1.6.2](https://code.castopod.org/adaures/castopod/compare/v1.6.1...v1.6.2) (2023-09-11)
### Bug Fixes
- **migrations:** remove if exists modifier for drop index
([82013c9](https://code.castopod.org/adaures/castopod/commit/82013c9cde901c54fdb3a833890aa693e8542627)),
closes [#382](https://code.castopod.org/adaures/castopod/issues/382)
## [1.6.1](https://code.castopod.org/adaures/castopod/compare/v1.6.0...v1.6.1) (2023-09-09)
### Bug Fixes
- **admin:** redirect root fediverse route to fediverse-blocked-actors
([ba5324e](https://code.castopod.org/adaures/castopod/commit/ba5324ea1942a3939f186e974d29fb393c54b253))
- **analytics:** show full referrer domain in web pages visits reports
([6be38e9](https://code.castopod.org/adaures/castopod/commit/6be38e9fda3d1436d81686e1a3a5e5b173e390a0)),
closes [#367](https://code.castopod.org/adaures/castopod/issues/367)
- **auth:** overwrite Shield's PermissionFilter
([c6e8000](https://code.castopod.org/adaures/castopod/commit/c6e8000bab54f4a32068578f750f4cf9d91bad89))
- **auth:** update shield from v1.0.0-beta.3 to v1.0.0-beta.6
([23842df](https://code.castopod.org/adaures/castopod/commit/23842df03ae28e416390e2436442b8e7c8340333))
- **platforms:** add missing tiktok to social platforms seed
([8dfdaf3](https://code.castopod.org/adaures/castopod/commit/8dfdaf321566050e9c53683e70864871eb55d618))
- remove fediverse prefix to prevent migration error + load routes during
podcast import
([7ff1dbe](https://code.castopod.org/adaures/castopod/commit/7ff1dbe9030768074b2fe7c7f570bfb9e7336f62))
- **routes:** overwrite RouteCollection to include all routes + update js and
php dependencies
([b4f1b91](https://code.castopod.org/adaures/castopod/commit/b4f1b916bfec53f071e8d0d900081c6d74486e53))
- update Router to include latest CI changes with alternate-content logic
([ae57601](https://code.castopod.org/adaures/castopod/commit/ae57601c838a7aa9469bae8038ac1c30d8c9a51e))
- use podcast-activity named route instead of not existing actor route
([3c35718](https://code.castopod.org/adaures/castopod/commit/3c357183ca51545787fcfc801b4a5829d9cd8ad6))
# [1.6.0](https://code.castopod.org/adaures/castopod/compare/v1.5.2...v1.6.0) (2023-08-28)
### Bug Fixes
- **home:** update where clause when getting all podcasts to prevent draft
podcasts from showing up
([7a1eea5](https://code.castopod.org/adaures/castopod/commit/7a1eea58d3cbc1982baaec21d87a36e218e1910a))
- **media:** copy and delete temp file when saving instead of moving it for FS
FileManager
([9346e78](https://code.castopod.org/adaures/castopod/commit/9346e787bd2a2c815533092279f96ae1fe0d9aae)),
closes [#338](https://code.castopod.org/adaures/castopod/issues/338)
- **media:** get path using media_path_absolute when saving media file
([754e7a6](https://code.castopod.org/adaures/castopod/commit/754e7a6b4b2c12cf50c1c8b166732dc3255f36fb))
- **media:** init file properties in setAttributes' Model method + set defaults
to pathinfo data
([0775add](https://code.castopod.org/adaures/castopod/commit/0775add67860b94a35b68c01b133ec8ec969f539))
- **premium-podcasts:** show premium flag only when podcast has published
premium episodes
([d10c4fd](https://code.castopod.org/adaures/castopod/commit/d10c4fd7538e6af8a5b0eb232a06522fe8c4bf8e))
- **s3:** add a flag to serve media files by redirecting to a presigned url
instead of default proxy
([11aa358](https://code.castopod.org/adaures/castopod/commit/11aa3586a04c166404954600235634cee77219df))
### Features
- **episode:** add preview link in admin to view and share episode before
publication
([7d21b35](https://code.castopod.org/adaures/castopod/commit/7d21b3509ec5d1aa65420efa038f44bcd235e64f))
## [1.5.2](https://code.castopod.org/adaures/castopod/compare/v1.5.1...v1.5.2) (2023-07-31)
### Bug Fixes
- **credits:** remove undefined $podcast variable from page layout
([73a5b68](https://code.castopod.org/adaures/castopod/commit/73a5b680875cc520fd15c529c01d44df728f9be2)),
closes [#359](https://code.castopod.org/adaures/castopod/issues/359)
- **platforms:** change twitter to X + add buymeacoffee and kofi as funding
([d69b4e4](https://code.castopod.org/adaures/castopod/commit/d69b4e4857fcb1ac1c05ac59c78d130788f00400)),
closes [#353](https://code.castopod.org/adaures/castopod/issues/353)
[#361](https://code.castopod.org/adaures/castopod/issues/361)
## [1.5.1](https://code.castopod.org/adaures/castopod/compare/v1.5.0...v1.5.1) (2023-07-29)
### Bug Fixes
- **admin-ui:** remove button labels on smaller screens in podcast view
([9cc5ffd](https://code.castopod.org/adaures/castopod/commit/9cc5ffd1439fdc86f46a03f4319cae32db95f84e))
- **rss:** set srt transcripts' mimetype to application/x-subrip with
rel="captions" attribute
([16a3fdb](https://code.castopod.org/adaures/castopod/commit/16a3fdb56e3f07185e75d106216f29519ccb25f7)),
closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
- **rss:** update podcast extension namespace
([6833dd0](https://code.castopod.org/adaures/castopod/commit/6833dd05ab51bc530d34fd4174ad732f623226c0)),
closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
# [1.5.0](https://code.castopod.org/adaures/castopod/compare/v1.4.7...v1.5.0) (2023-07-27)
### Bug Fixes
- **admin-ui:** truncate header title + remove sticky podcast banner card on
mobile
([63c20da](https://code.castopod.org/adaures/castopod/commit/63c20da5ffd500265f06fa38f2b2c963e14602af))
### Features
- add podcast links page including social, podcasting and funding links
([8ae2929](https://code.castopod.org/adaures/castopod/commit/8ae292933af15fa99856582ac24e985bfef37d5b))
## [1.4.7](https://code.castopod.org/adaures/castopod/compare/v1.4.6...v1.4.7) (2023-07-19)
### Bug Fixes
- **s3:** allow CORS for served static files
([9b955c9](https://code.castopod.org/adaures/castopod/commit/9b955c9ce25a06a9102b67ebe77375dc45d28f0f))
## [1.4.6](https://code.castopod.org/adaures/castopod/compare/v1.4.5...v1.4.6) (2023-07-11)
### Bug Fixes
- **fediverse:** expand object before sending accept follow request
([082cdc9](https://code.castopod.org/adaures/castopod/commit/082cdc9ee79d004c2ed748e3b8046e9141bf0242)),
closes [#350](https://code.castopod.org/adaures/castopod/issues/350)
- **podcast-import:** remove error log when no import in queue, exit with
success instead
([5e719f3](https://code.castopod.org/adaures/castopod/commit/5e719f3e9eb6cf48c3fd8ac97181638b24d03fc9))
## [1.4.5](https://code.castopod.org/adaures/castopod/compare/v1.4.4...v1.4.5) (2023-07-04)
### Bug Fixes
- **s3:** handle range requests to serve media files
([41a5932](https://code.castopod.org/adaures/castopod/commit/41a59322332c835808a32987aaf8ec6cafbf5fca))
## [1.4.4](https://code.castopod.org/adaures/castopod/compare/v1.4.3...v1.4.4) (2023-07-02)
### Bug Fixes
- **audio-clipper:** init segment position on firstUpdate + improve UX by adding
ghost handle
([aa68386](https://code.castopod.org/adaures/castopod/commit/aa683866671d14c0b9a11b09c74eb132673e5547)),
closes [#351](https://code.castopod.org/adaures/castopod/issues/351)
- set resized images to 72dpi for compatibility with Apple Podcasts
([0b327cb](https://code.castopod.org/adaures/castopod/commit/0b327cb4d9c92d0ae227a0f08ede3b29390df172)),
closes [#282](https://code.castopod.org/adaures/castopod/issues/282)
## [1.4.3](https://code.castopod.org/adaures/castopod/compare/v1.4.2...v1.4.3) (2023-06-29)
### Bug Fixes
- **video-clipper:** add -t option to ffmpeg command to stop generation after
duration
([60814b8](https://code.castopod.org/adaures/castopod/commit/60814b8d202419c2bdbf6abb7c2bde447537b7e9)),
closes [#341](https://code.castopod.org/adaures/castopod/issues/341)
## [1.4.2](https://code.castopod.org/adaures/castopod/compare/v1.4.1...v1.4.2) (2023-06-27)
### Bug Fixes
- **fediverse:** check that actor's images mimetype is present or guess it
otherwise
([06c4f15](https://code.castopod.org/adaures/castopod/commit/06c4f15477a568407a3d3c1e5e489bc0241bc1e9)),
closes [#348](https://code.castopod.org/adaures/castopod/issues/348)
- **podcast-import:** show cancel or retry action depending on task status
([e42258d](https://code.castopod.org/adaures/castopod/commit/e42258de1f331aac0cbb380b80cd8fc7f9d7dc18))
## [1.4.1](https://code.castopod.org/adaures/castopod/compare/v1.4.0...v1.4.1) (2023-06-22)
### Bug Fixes
- **podcast-import:** set default values for person group and role if not found
in taxonomy
([aa46dca](https://code.castopod.org/adaures/castopod/commit/aa46dca4e399bf2e544d62dcb4a9a0328e4e6c41))
# [1.4.0](https://code.castopod.org/adaures/castopod/compare/v1.3.5...v1.4.0) (2023-06-21)
### Bug Fixes
- **charts:** set duration charts label to HHhMM for listening time analytics
([3fc1d8e](https://code.castopod.org/adaures/castopod/commit/3fc1d8e18dc8119251c72dcaa7e5121246c2b194))
- **embed:** set height of player iframe from config
([4665741](https://code.castopod.org/adaures/castopod/commit/4665741425532f253a46a42ba05602047798dba2))
- **s3:** serve files without cache if dummy cache handler + add http referer
header to redirect
([30db9f0](https://code.castopod.org/adaures/castopod/commit/30db9f0667bf7f7a5f186ea667a524d1e3b502db))
- **s3:** use presigned request uri to serve static files
([cb92dc7](https://code.castopod.org/adaures/castopod/commit/cb92dc73f17543d32d1cdc24db72403a5c561a74))
- **webmanifest:** import misc helper to get site_icon_url
([548a11d](https://code.castopod.org/adaures/castopod/commit/548a11d501749fa61ef894fd8818abae5668554f))
### Features
- **import:** run podcast imports' processes asynchronously using tasks
([d8e1d40](https://code.castopod.org/adaures/castopod/commit/d8e1d4031d86de9a3889b74ae2a6d9c90af8a1da))
- **rest-api:** add endpoints for episodes and full text search for podcasts and
episodes
([85505d4](https://code.castopod.org/adaures/castopod/commit/85505d4b3181c96bc91619e3ab9b0601f8e1c120)),
closes [#296](https://code.castopod.org/adaures/castopod/issues/296)
## [1.3.5](https://code.castopod.org/adaures/castopod/compare/v1.3.4...v1.3.5) (2023-05-09)
### Bug Fixes
- replace essence with embera to create preview cards
([c682f03](https://code.castopod.org/adaures/castopod/commit/c682f03a67c6c0ebbcc6ff45d9a037f6f9823bde))
## [1.3.4](https://code.castopod.org/adaures/castopod/compare/v1.3.3...v1.3.4) (2023-05-05)
### Bug Fixes
- **import-update:** insert episodes incrementally into database
([108fdf8](https://code.castopod.org/adaures/castopod/commit/108fdf84b8dd458fc71a06a77d14069287ab8e42))
## [1.3.3](https://code.castopod.org/adaures/castopod/compare/v1.3.2...v1.3.3) (2023-04-17)
### Bug Fixes
- unnescape podcast title special characters in "find us on" section
([f727276](https://code.castopod.org/adaures/castopod/commit/f727276f820a8ef2c47947f40a37a4a157b509ef)),
closes [#323](https://code.castopod.org/adaures/castopod/issues/323)
- **websub:** add missing misc helper import
([855aacc](https://code.castopod.org/adaures/castopod/commit/855aacce0bf3841a876cd593e668e116149080aa))
## [1.3.2](https://code.castopod.org/adaures/castopod/compare/v1.3.1...v1.3.2) (2023-04-14)
### Bug Fixes
- remove path key when getting default avatar path
([c5a1359](https://code.castopod.org/adaures/castopod/commit/c5a1359218d61c0f78006f2bd5785e317f32bade))
- **s3:** serve files using media base url to allow for CDN setup
([502f53c](https://code.castopod.org/adaures/castopod/commit/502f53c9701da3b8da2caef1eb54df25b7d2d86a))
## [1.3.1](https://code.castopod.org/adaures/castopod/compare/v1.3.0...v1.3.1) (2023-04-13)
### Bug Fixes
- **s3:** add proxy to serve images from s3 to client
([a76724a](https://code.castopod.org/adaures/castopod/commit/a76724a8cfee700f6874f86b35616d61facc664e)),
closes [#321](https://code.castopod.org/adaures/castopod/issues/321)
# [1.3.0](https://code.castopod.org/adaures/castopod/compare/v1.2.4...v1.3.0) (2023-04-03)
### Bug Fixes
- delete files using file_manager when deleting episode and podcast
([41d8efe](https://code.castopod.org/adaures/castopod/commit/41d8efe6e71566eba44bfdfd00d1708ac4338366))
### Features
- **media:** set media storage directory as configurable
([7e1a470](https://code.castopod.org/adaures/castopod/commit/7e1a470ba42172eb4c3864ab3652e9f8b55d1ba8))
## [1.2.4](https://code.castopod.org/adaures/castopod/compare/v1.2.3...v1.2.4) (2023-03-23)
### Bug Fixes
- allow images to have .jpeg extension consistently
([ae5e12b](https://code.castopod.org/adaures/castopod/commit/ae5e12be3b15fe50cb2311abcbbc19ac23b592f6))
- **s3:** delete persons image sizes from bucket + add keyPrefix to config
([208c271](https://code.castopod.org/adaures/castopod/commit/208c2715f900371987c3b75a749fe937a3db1991))
- **s3:** do not create bucket if not exists, check if healthy instead
([da7076f](https://code.castopod.org/adaures/castopod/commit/da7076fc2d49d07708d5adaa99733487b7f52e20))
### Reverts
- **homepage:** remove redirect to install if database is not setup
([d4954e0](https://code.castopod.org/adaures/castopod/commit/d4954e026d5e0d48c5f15ed69d1ce71abb34d1a1))
## [1.2.3](https://code.castopod.org/adaures/castopod/compare/v1.2.2...v1.2.3) (2023-03-18)
### Bug Fixes
- **notifications:** set mark-all-as-read parameter to be podcast_id instead of
actor_id
([2748f23](https://code.castopod.org/adaures/castopod/commit/2748f2313797e50d8a2a7b87df09c0bc6e64360a))
## [1.2.2](https://code.castopod.org/adaures/castopod/compare/v1.2.1...v1.2.2) (2023-03-18)
### Bug Fixes
- **migration:** change old media file_key to file_path
([a414142](https://code.castopod.org/adaures/castopod/commit/a4141421aa1d6e89742b390b042382f729f965a9)),
closes [#314](https://code.castopod.org/adaures/castopod/issues/314)
## [1.2.1](https://code.castopod.org/adaures/castopod/compare/v1.2.0...v1.2.1) (2023-03-17)
### Bug Fixes
- change app.mediaBaseURL to media.baseURL in install, docker entrypoints and
docs
([b3c6e05](https://code.castopod.org/adaures/castopod/commit/b3c6e05e6fcd8a518eeedeefde28b61f879ba71d))
# [1.2.0](https://code.castopod.org/adaures/castopod/compare/v1.1.2...v1.2.0) (2023-03-17)
### Bug Fixes
- **analytics:** check the x_forwarded_for client header
([1111177](https://code.castopod.org/adaures/castopod/commit/1111177eb7fea4eba6d119b17acdf3bf416492ef))
- **auth:** update podcast editors' permissions
([a9b6308](https://code.castopod.org/adaures/castopod/commit/a9b630884bc318499ea7f03862d5752dd5f178e1))
- **contributors:** add dash to prevent deleting permissions from other podcast
([5d2a2d4](https://code.castopod.org/adaures/castopod/commit/5d2a2d49c489cd98f9c9ecbca35fd5d21a9cadfb)),
closes [#310](https://code.castopod.org/adaures/castopod/issues/310)
- display bandwidth limit on dashboard when set in .env
([a2a87ab](https://code.castopod.org/adaures/castopod/commit/a2a87abf7caea3c87bcf2d0988610cc07782de9e))
- **docker:** update nginx configuration
([8884598](https://code.castopod.org/adaures/castopod/commit/8884598a56d0e2550776ef4cee5e53558c20e009))
- **platforms:** update 'submit_url' for Antennapod
([9fc49a7](https://code.castopod.org/adaures/castopod/commit/9fc49a7430406f50e68318c5fd7c577ae1ebd9df))
### Features
- add downloads count to episode list
([b63c1dc](https://code.castopod.org/adaures/castopod/commit/b63c1dc9b1ed41626b99ba852a9a00ed417059ba))
- add health route to check if db, cache and file manager are ok
([1dde11f](https://code.castopod.org/adaures/castopod/commit/1dde11f8e42b66684a956068f5347e9289f4918b))
- **media:** add s3 to manage media files
([d93fc98](https://code.castopod.org/adaures/castopod/commit/d93fc98469ffe93913b65e539dec396891708c70))
### Reverts
- **install:** reset condition to look for instance owner before continuing
install
([fc009f3](https://code.castopod.org/adaures/castopod/commit/fc009f3d0058028bbbb6418603cf820c0f7cea80))
## [1.1.2](https://code.castopod.org/adaures/castopod/compare/v1.1.1...v1.1.2) (2022-12-14)
### Bug Fixes
- **analytics:** set EpisodeAudioController to init user session data
([77ccb30](https://code.castopod.org/adaures/castopod/commit/77ccb306009eb093147c56789535e754f3d85570))
## [1.1.1](https://code.castopod.org/adaures/castopod/compare/v1.1.0...v1.1.1) (2022-12-13)
### Bug Fixes
- **op3:** remove scheme when wraping audio URI
([0ad22e4](https://code.castopod.org/adaures/castopod/commit/0ad22e49bc488e96df5a41495f5b242559b64a45))
- **rss:** add file extension to enclosure url
([964cbba](https://code.castopod.org/adaures/castopod/commit/964cbba54f16556408bf8280c544a52e6be5c9fc))
# [1.1.0](https://code.castopod.org/adaures/castopod/compare/v1.0.5...v1.1.0) (2022-12-09)
### Bug Fixes
- **notifications:** remove cache inconsistencies when marking notification as
read
([46d7054](https://code.castopod.org/adaures/castopod/commit/46d70541d313c836ab0c078ba6121fe5fe956e62))
- **notifications:** retrieve activity from database instead of getting cache
([7fbbd08](https://code.castopod.org/adaures/castopod/commit/7fbbd08da6a37d08608900ad318e72815fe4b0c4))
- **podcast:soundbite:** rename start time attribute to follow spec
([689831c](https://code.castopod.org/adaures/castopod/commit/689831c26c756d454de432900d23bc09a37f890b))
### Features
- **analytics:** add OP3 analytics service option + update episode audio url
([16527ed](https://code.castopod.org/adaures/castopod/commit/16527ed529265f2925e205856c684e34175a8933))
## [1.0.5](https://code.castopod.org/adaures/castopod/compare/v1.0.4...v1.0.5) (2022-11-25)
### Bug Fixes
- **router:** revert to CI4 v4.2.7 to include all routes
([c13cfa0](https://code.castopod.org/adaures/castopod/commit/c13cfa0ea0679751521ca4157b953043ecc7974a))
## [1.0.4](https://code.castopod.org/adaures/castopod/compare/v1.0.3...v1.0.4) (2022-11-21)
### Bug Fixes
- update actorUsername regex to get url_to actor
([1d6b177](https://code.castopod.org/adaures/castopod/commit/1d6b177a55111ede01fba1c08499036d474533bc))
## [1.0.3](https://code.castopod.org/adaures/castopod/compare/v1.0.2...v1.0.3) (2022-11-17)
### Bug Fixes
- **dashboard-ui:** fill the blank gaps between cards on smaller screen sizes
([00836cc](https://code.castopod.org/adaures/castopod/commit/00836cc368c75ae2e23fa5dc4a53a5bb6eb2ce24))
## [1.0.2](https://code.castopod.org/adaures/castopod/compare/v1.0.1...v1.0.2) (2022-11-04)
### Bug Fixes
- **auth:** disallow registration by default
([379b9be](https://code.castopod.org/adaures/castopod/commit/379b9be2b99574fe4af4009b01128dba2c75f037))
- **contributors:** add prefix to podcast group to delete contributor
([9f785db](https://code.castopod.org/adaures/castopod/commit/9f785db7ba674638a6f456aa3626f3f8100911f1))
- extract podcast ids from user groups using a regex
([e26215a](https://code.castopod.org/adaures/castopod/commit/e26215a11fc23aa0ad5ccff8ee97d6c6e8a09c1a))
- **notifications:** add manage-notifications permission to podcast
([ed7c247](https://code.castopod.org/adaures/castopod/commit/ed7c247bcbbb450e5ff96418930d3b37ce912cc4))
- **platforms:** convert special characters to htmlentities to validate url
([82310a2](https://code.castopod.org/adaures/castopod/commit/82310a2e0b426e84501090bdd9c0cf592d1c0d53))
## [1.0.1](https://code.castopod.org/adaures/castopod/compare/v1.0.0...v1.0.1) (2022-11-01)
### Bug Fixes
- **platforms:** trim platform url before validation and storage
([259fe5f](https://code.castopod.org/adaures/castopod/commit/259fe5f697a833e268cde88e959bc19bd662edf6))
# 1.0.0 (2022-10-20)
### Bug Fixes
- **a11y:** replace active tab color to contrast with background on podcast and
episode pages
([f3785e1](https://code.castopod.org/adaures/castopod/commit/f3785e140147d085a2fb6a62ded87cdfe360f442))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **activitypub:** add conditions for possibly missing actor properties + add
user-agent to requests
([8fbf948](https://code.castopod.org/adaures/castopod/commit/8fbf948fbba22ffd33966a1b2ccd42e8f7c1f8a2))
- **activitypub:** add target actor id to like / announce activities to send
directly to note's actor
([962dd30](https://code.castopod.org/adaures/castopod/commit/962dd305f5d3f6eadc68f400e0e8f953827fe20d))
- **activitypub:** add target_actor_id for create activity to broadcast post
reply
([0128a21](https://code.castopod.org/adaures/castopod/commit/0128a21ec55dcc0a2fbf4081dadb4c4737735ba1))
- **activitypub:** allow cors on get requests for routes exposing acitivitypub
objects
([2f24809](https://code.castopod.org/adaures/castopod/commit/2f2480998f9abb34f02ab186c65d462a74b4e640))
- **activitypub:** set created_by to null for reblog if no user + update episode
oembed data
([209dfbd](https://code.castopod.org/adaures/castopod/commit/209dfbd134e1a2cc02e7c24c158d786fa4dda61d))
- add admin-audio-player to vite config to have admin player show up
([93cb9b2](https://code.castopod.org/adaures/castopod/commit/93cb9b24701c09b92820204a67c1fc1b3c044708))
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error
([339bef8](https://code.podlibre.org/podlibre/castopod-host/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.podlibre.org/podlibre/castopod-host/issues/145)
# [1.0.0-alpha.79](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.78...v1.0.0-alpha.79) (2021-12-20)
### Bug Fixes
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.podlibre.org/podlibre/castopod-host/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
# [1.0.0-alpha.78](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.77...v1.0.0-alpha.78) (2021-12-15)
### Bug Fixes
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
- add category_label component to include parent category in about podcast page
([74e7d68](https://code.castopod.org/adaures/castopod/commit/74e7d68ac834885c4b89ee6e7d60db2157165799))
- add explicit int conversion when formatting episode duration
([1253096](https://code.castopod.org/adaures/castopod/commit/1253096197a0d30692bdafa7152f250cd9a71acf))
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
- add href to castopod website on login page
([cc54257](https://code.castopod.org/adaures/castopod/commit/cc5425735184ad738aa0f38540f18e8971f8f56e))
- add missing explicit badge for podcasts and episodes
([cdf9f9d](https://code.castopod.org/adaures/castopod/commit/cdf9f9d53f2597f19455cb65c51da4677bb99327))
- add open graph size for podcast images to replace the inadequate large format
([33aae1f](https://code.castopod.org/adaures/castopod/commit/33aae1f7934e4962116e94e477dbf48e24971f5f))
- add public/media folder to castopod bundle
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
- add translation key for audio-clipper trim labels
([db191ac](https://code.castopod.org/adaures/castopod/commit/db191ac31bd16bad2a72afdb8b25c685adf86a6e))
- add underline and semibold font weight for prose links to have them stand out
([d4d8671](https://code.castopod.org/adaures/castopod/commit/d4d867121c50bded4176a53d7154cf1bb347e306))
- add where condition to get episode count without deleted episodes
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
- **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** set initial value for duration and bandwidth
([ee50539](https://code.castopod.org/adaures/castopod/commit/ee5053959154b1a2e5fbe4b43162968425206a26))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
- **audio-clipper:** add mouse position offset when stretching clip to prevent
content from jumping
([602654b](https://code.castopod.org/adaures/castopod/commit/602654b99b33ee8c29da080058a0aaea976cd484))
- **audio-clipper:** show audio playing progress + put waveform behind audio
clipper
([01a09dc](https://code.castopod.org/adaures/castopod/commit/01a09dc447b81c5412ceb45d6706a867939fd4dd))
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
- **bundle:** include modules and themes when copying files with rsync
([cd5bb88](https://code.castopod.org/adaures/castopod/commit/cd5bb8835c6e259408a8c13a2196a347e161da83))
- **bundle:** update vite input files path + add `set -e` in bash scripts to
fail if command fails
([0ee53c7](https://code.castopod.org/adaures/castopod/commit/0ee53c71ffadb8a6ddb1febd9f912bc99f5f7a0b))
- **cache:** add locale for podcast and episode pages + clear some persisting
cache in models
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61)
- **cache:** delete posts and comments pages cache when updating platform links
([f7c3e5b](https://code.castopod.org/adaures/castopod/commit/f7c3e5bf4ad43389bf8d58d2c4aaf16b81cbce00)),
closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
- **cache:** return a non cached view when connected
([e2e7358](https://code.castopod.org/adaures/castopod/commit/e2e735815d805a48eed2ea3288d060d0ddb253a3))
- **cache:** suffix cache names with authenticated for credits, map and pages
([418a70b](https://code.castopod.org/adaures/castopod/commit/418a70b2a670d8ba0ab6c15fa5faa41f6be55e53))
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- change image size requirement hints
([ea20206](https://code.castopod.org/adaures/castopod/commit/ea20206ee674eb54dd3ea188d2a2e2d41425df65))
- change message upon cancellation of episode publication
([9859c74](https://code.castopod.org/adaures/castopod/commit/9859c7434c2a3478ce035f7a4de20f594d63f5b0))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
- clear cache when deleting podcast banner
([99bb40b](https://code.castopod.org/adaures/castopod/commit/99bb40b8bc17b8ee2cd8468a82e46ea280c92cb6))
- comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- **comments:** add comment view partials for public pages
([fcecbe1](https://code.castopod.org/adaures/castopod/commit/fcecbe1c68b0d28d19454fba65caf3ab769fbc75))
- correct chart data
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
- define podcast_id and platform_slug as foreign keys in podcasts_plaforms table
([6e9451a](https://code.castopod.org/adaures/castopod/commit/6e9451a1103b43750fa70ad576de36af25ca29cb))
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
- **email:** set the correct url in the activation and forgot emails
([10fc6f1](https://code.castopod.org/adaures/castopod/commit/10fc6f17c6838a58348f32ccfd0cf05f9d3e172c)),
closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
- **embed:** open embedded player's links in new tab
([4aa73d7](https://code.castopod.org/adaures/castopod/commit/4aa73d71e3b8c0a6c3f75f4d1d45c4d693aba64c))
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
- **episode-unpublish:** set consistent posts_counts' increments/decrements for
actors and episodes
([8acdafd](https://code.castopod.org/adaures/castopod/commit/8acdafd26044e50a4d6ee451bf24ad66003c5bb3)),
closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
- escape characters for `min` in format_duration_symbol
([3b6722a](https://code.castopod.org/adaures/castopod/commit/3b6722a42b9e4330e5235d4ceed41c777159f4dc))
- escape generated feed tag values and remove new lines from public pages meta
description
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46)
- expire default query cache upon scheduled episode publication
([b72e7c8](https://code.castopod.org/adaures/castopod/commit/b72e7c8691c887e41107baea0a4d50a39eaf8c8b)),
closes [#81](https://code.castopod.org/adaures/castopod/issues/81)
- explicitly cast seconds to int in iso8601_duration helper function
([779653f](https://code.castopod.org/adaures/castopod/commit/779653f75b140942f731cbb238bc0667cc461307))
- **fediverse:** set default castopod avatar url when actor avatar is not
present
([460f52f](https://code.castopod.org/adaures/castopod/commit/460f52f70e493d619c28632db6c698e88f0ebb5f))
- **fediverse:** set model instances as non shared to prevent overlapping
([91128fa](https://code.castopod.org/adaures/castopod/commit/91128fad7a68e1f4e5acacba90b6899288699e61))
- fix layout bugs in admin and update translation files
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
- **get_browser_language:** return defaultLocale if browser doesn't send user
preferred language
([9cc2996](https://code.castopod.org/adaures/castopod/commit/9cc299626181048b85b629bbe7f5806a1f5d21ff))
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
- **home:** remove hardcoded prefix in getAllPodcasts query
([92d5cc5](https://code.castopod.org/adaures/castopod/commit/92d5cc50a3e533875cd894dccc417918102d4b7f))
- **housekeeping:** replace the use of GLOB_BRACE with looping over file
extensions
([42d92d0](https://code.castopod.org/adaures/castopod/commit/42d92d0c8dfe0c567c28f5bfdda129890fa4c2ec)),
closes [#154](https://code.castopod.org/adaures/castopod/issues/154)
- **housekeeping:** set default sizes value + ignore illegal IFD size error to
proceed with script
([f21ca57](https://code.castopod.org/adaures/castopod/commit/f21ca57603cfa503699b7e09a155e18d876d65fe))
- **housekeeping:** use EpisodeModel's builder to reset comments count
([65e9c0b](https://code.castopod.org/adaures/castopod/commit/65e9c0b05ea4992884149cb4a4b071bf31a20a1a))
- **htaccess:** add ? after index.php in RewriteRule
([d9d139e](https://code.castopod.org/adaures/castopod/commit/d9d139eefa03c28d1a064b3b32c9036193497e57)),
closes [#152](https://code.castopod.org/adaures/castopod/issues/152)
- **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be
sent in any order
([b7f285e](https://code.castopod.org/adaures/castopod/commit/b7f285e4e24247fedb94f030356fa6f291f525cc))
- **images:** set default mimetype if none is specified when getting size info
([6e4acc6](https://code.castopod.org/adaures/castopod/commit/6e4acc64ad256178cee7905402b48bafcd49f84c))
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.podlibre.org/podlibre/castopod-host/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
# [1.0.0-alpha.77](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.76...v1.0.0-alpha.77) (2021-11-23)
### Bug Fixes
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.podlibre.org/podlibre/castopod-host/commit/a281abfda475388a07943c169dab460cc2d4f944))
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
- **import:** add validation for handle field to prevent
Router.invalidParameterType error
([5bf7200](https://code.castopod.org/adaures/castopod/commit/5bf7200fb390f2447b29f24b495f24483cf7b205)),
closes [#119](https://code.castopod.org/adaures/castopod/issues/119)
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
- **import:** save media files during podcast import + set missing media fields
([a9989d8](https://code.castopod.org/adaures/castopod/commit/a9989d841a634f8cf6c04df25f40bb1e7d4fcdcc))
- **import:** set default episode type if not set
([d7250ab](https://code.castopod.org/adaures/castopod/commit/d7250ab03f9b032830c575ad58b51c8d60b7a49a))
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
- include missing variables on public ui's episode page and remote_actions
([193b373](https://code.castopod.org/adaures/castopod/commit/193b373bc94a5270acae99b637aa84b6cb2dedfe))
- **input-component:** unset required attribute to prevent rendering it when
false
([db9ac13](https://code.castopod.org/adaures/castopod/commit/db9ac13860bce58235a5da275910bea605a00626))
- **install:** add password validation when creating super admin
([5a2ca0c](https://code.castopod.org/adaures/castopod/commit/5a2ca0cc4ae85cc15960201c86f131cb822f714f))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
- **install:** redirect to input baseUrl after instance config
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
- **install:** set message block on forms to show error messages
([3a0a20d](https://code.castopod.org/adaures/castopod/commit/3a0a20d59cdae7f166325efb750eaa6e9800ba6e)),
closes [#157](https://code.castopod.org/adaures/castopod/issues/157)
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
- **json-ld:** add missing properties to PodcastSeries object
([e97266c](https://code.castopod.org/adaures/castopod/commit/e97266c5d4883a10f68b3685ecc0d1942f54d658))
- keep subtitle line breaks when parsing srt file to json
([cfb3da6](https://code.castopod.org/adaures/castopod/commit/cfb3da6592f2de23cb1a7ac420f19fc77fa338aa))
- **layouts:** replace holy-grail layout with tailwind config + widen public
podcast layout
([be5a287](https://code.castopod.org/adaures/castopod/commit/be5a28787fdb180b64d9bf570120eff7072ab9aa))
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
- **markdown-editor:** remove unnecessary buttons for podcast and episode
editors + add extensions
([9c4f60e](https://code.castopod.org/adaures/castopod/commit/9c4f60e00bcbd4f784f12d2a6fed357ad402ee2e))
- **md-editor:** build new markdown editor with lit +
github/markdown-toolbar-element
([9ec1cb9](https://code.castopod.org/adaures/castopod/commit/9ec1cb93da6f41124c48b8cf14ee6942e865bede)),
closes [#93](https://code.castopod.org/adaures/castopod/issues/93)
[#94](https://code.castopod.org/adaures/castopod/issues/94)
[#120](https://code.castopod.org/adaures/castopod/issues/120)
- **migrations:** ignore invalid utf8 chars for media files metadata + update
transcript parser
([45e8f99](https://code.castopod.org/adaures/castopod/commit/45e8f99e753cc02ec105e6f4d7fe026a205724f8))
- minor corrections
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- move html escaping on credits page
([fbffdbd](https://code.castopod.org/adaures/castopod/commit/fbffdbde78544c83138ee6234c62d43056f407b6))
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
- **notifications:** add trigger after activities update + update insert trigger
([e5d16e8](https://code.castopod.org/adaures/castopod/commit/e5d16e87119021fa5a43470d67ddfe5128e57f74))
- **notifications:** notify actors after activities insert / update using model
callback methods
([e08555a](https://code.castopod.org/adaures/castopod/commit/e08555a4e9a6c15eeba18273c63403f82eddae35))
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
- overwrite common lang function to escape returned string
([4c490c1](https://code.castopod.org/adaures/castopod/commit/4c490c15bb6642ad0b2aaddf08d8af25de99b4b0)),
closes [#196](https://code.castopod.org/adaures/castopod/issues/196)
[#198](https://code.castopod.org/adaures/castopod/issues/198)
- overwrite getActorById to return app's Actor entity
([f2bc2f7](https://code.castopod.org/adaures/castopod/commit/f2bc2f7e01aa166faa627df6fe4d5ed4887c16e5))
- **package.json:** update destination of postcss generation scripts
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- **pages:** add locale to page cache
([8f999ce](https://code.castopod.org/adaures/castopod/commit/8f999ce2f7ee1416c30cf58c84f67b3d11b3f142))
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
- pass timezone to relative time component to show the localized time in the UI
([b9db936](https://code.castopod.org/adaures/castopod/commit/b9db936461d4cb914958bb3256bb910bbd7ba815))
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
- **persons:** set person picture as optional for better ux
([7fdea63](https://code.castopod.org/adaures/castopod/commit/7fdea63de7e572810082c84fff3013af580df58b)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
- **player-styling:** revert vite to 2.8 to reference the player css
([e07d3af](https://code.castopod.org/adaures/castopod/commit/e07d3afea9af85b8361227e000fb64b502781668))
- **podcast-activity:** check if transcript and chapters are set before
including them in audio
([5855a25](https://code.castopod.org/adaures/castopod/commit/5855a250936f91641efef77650890a18d8e9917f))
- **podcast-import:** move guid attribute declaration for Episode entity to
include slug data
([5d02ae3](https://code.podlibre.org/podlibre/castopod-host/commit/5d02ae39908a9d743627135b372bf981134c4328))
# [1.0.0-alpha.76](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.75...v1.0.0-alpha.76) (2021-10-26)
### Bug Fixes
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328))
- **podcast:** use markdown description value for editor + set prose class to
about description
([f304d97](https://code.castopod.org/adaures/castopod/commit/f304d97b14e0ef383509cb3bba50beb55bf701ba)),
closes [#156](https://code.castopod.org/adaures/castopod/issues/156)
- prefill description footer input when creating a new episode
([9ea5ca3](https://code.castopod.org/adaures/castopod/commit/9ea5ca31697c70d176294f8aea37bd57d471fcf7))
- **premium-podcasts:** display unlock button in embed when premium episode
([ca109ba](https://code.castopod.org/adaures/castopod/commit/ca109ba3a8a08e661fd2484454b1983c3418f15d))
- **premium-podcasts:** remove cache in unlock form + redirect to podcast if
podcast is not premium
([242352c](https://code.castopod.org/adaures/castopod/commit/242352c4d9cd936de14e8e8a5d78ebf1287b1f95))
- **premium-podcasts:** return different cached page when podcast is unlocked
([b1303c5](https://code.castopod.org/adaures/castopod/commit/b1303c525517498b0edfb9885ff36e08c72628b5))
- **pwa:** add scope to webmanifests to allow installing an app per podcast
([74c683e](https://code.castopod.org/adaures/castopod/commit/74c683eb44398a84443ec17903c3e002bb5ea9b9))
- **pwa:** set app display as standalone in the webmanifests
([7aa37d2](https://code.castopod.org/adaures/castopod/commit/7aa37d24ac13a1ee160c01a56b43621d7efcfbbc))
- re-order graph values
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- redirect to non cached views when authenticated in public views
([482b47b](https://code.castopod.org/adaures/castopod/commit/482b47ba6bdab7f27fc5704a559567228e07cd14))
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- remove cache from remote follow form to display error messages
([90e4443](https://code.castopod.org/adaures/castopod/commit/90e44437bdf37d8024ef609b2f7336dbdfc3b974))
- remove defer from js script declaration as it is a module
([18ae557](https://code.castopod.org/adaures/castopod/commit/18ae557e97f1cef775cd1e75fb1fedee7f1c0cc9))
- remove fixed size from podcast sidebar + rearrange account info + space out
import radio inputs
([776eec6](https://code.castopod.org/adaures/castopod/commit/776eec6f0d533d6c92ebec16f7a9dbfcde1f41f4))
- remove heavy image cover data from audio file metadata
([f74403b](https://code.castopod.org/adaures/castopod/commit/f74403bd7a5089b760603abe36264e7615be0e78))
- remove required for other_categories field and add podcast_id to latest
podcasts query
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- remove required property to persons picture
([c546be3](https://code.castopod.org/adaures/castopod/commit/c546be385b243014243ae93356006cd126d2f00d)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- remove value escaping for form inputs and textareas
([bc6dea2](https://code.castopod.org/adaures/castopod/commit/bc6dea2f8ad1cf0aee0eaa93151332fbac7fb771))
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
- rename issue_templates labels
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
- rename podcast name to podcast handle to clarify field usage
([9dd4c77](https://code.castopod.org/adaures/castopod/commit/9dd4c7741eb1b7cb5fc214ff674697f3aa986df0)),
closes [#126](https://code.castopod.org/adaures/castopod/issues/126)
- reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace deletedField with published_at for episodes
([14d7d07](https://code.castopod.org/adaures/castopod/commit/14d7d078225cdc8980759273a5dc4163d9f84b06))
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
- replace hardcoded style links with vite service + set default value for remote
transcript url
([3f2e056](https://code.podlibre.org/podlibre/castopod-host/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.podlibre.org/podlibre/castopod-host/issues/149)
[#150](https://code.podlibre.org/podlibre/castopod-host/issues/150)
# [1.0.0-alpha.75](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.74...v1.0.0-alpha.75) (2021-10-05)
### Bug Fixes
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150)
- replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
- restore default podcast icon on public website
([342778b](https://code.castopod.org/adaures/castopod/commit/342778bac3c684328d72633961df1a2ebdc1330e))
- revert to beta.1's codeigniter4 version
([e831411](https://code.castopod.org/adaures/castopod/commit/e83141127080ccde44987195db46ba97fd6cc2ca))
- rewrite regenerate image function to use saveSizes method from Image entity
([3889912](https://code.castopod.org/adaures/castopod/commit/38899124ec27e94a8c798bc2db528f9f785eec20))
- **router:** check if Accept header is set before getting value
([10a2ae0](https://code.castopod.org/adaures/castopod/commit/10a2ae02484672d6a0fbc6e7b943519c5ec16cb6)),
closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
- **router:** trim URI slash to match same routes for URIs with and without
trailing slash
([9e9375f](https://code.castopod.org/adaures/castopod/commit/9e9375f9a2cd6102f827b36ec521f4c86a557c00))
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
- **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.podlibre.org/podlibre/castopod-host/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.podlibre.org/podlibre/castopod-host/issues/148)
# [1.0.0-alpha.74](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2021-09-28)
### Features
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.podlibre.org/podlibre/castopod-host/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
# [1.0.0-alpha.73](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.72...v1.0.0-alpha.73) (2021-09-22)
### Bug Fixes
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.podlibre.org/podlibre/castopod-host/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
# [1.0.0-alpha.72](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.71...v1.0.0-alpha.72) (2021-09-20)
### Bug Fixes
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.podlibre.org/podlibre/castopod-host/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
# [1.0.0-alpha.71](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.70...v1.0.0-alpha.71) (2021-09-17)
### Features
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.podlibre.org/podlibre/castopod-host/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
# [1.0.0-alpha.70](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.69...v1.0.0-alpha.70) (2021-08-31)
### Bug Fixes
- **partner:** set correct image URL
([61554be](https://code.podlibre.org/podlibre/castopod-host/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
# [1.0.0-alpha.69](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.68...v1.0.0-alpha.69) (2021-08-23)
### Bug Fixes
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.podlibre.org/podlibre/castopod-host/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
# [1.0.0-alpha.68](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.67...v1.0.0-alpha.68) (2021-08-19)
### Bug Fixes
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.podlibre.org/podlibre/castopod-host/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
# [1.0.0-alpha.67](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.66...v1.0.0-alpha.67) (2021-07-24)
### Features
- allow cross origin requests on episode comments
([e12f95a](https://code.podlibre.org/podlibre/castopod-host/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
# [1.0.0-alpha.66](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.65...v1.0.0-alpha.66) (2021-07-24)
### Features
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.podlibre.org/podlibre/castopod-host/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
# [1.0.0-alpha.65](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2021-07-22)
### Bug Fixes
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
- **rss:** do not escape podcast and episode titles in the xml
([0dd3b7e](https://code.castopod.org/adaures/castopod/commit/0dd3b7e0bf00d5a9eb80c93cba1efcada59ec3c1)),
closes [#138](https://code.castopod.org/adaures/castopod/issues/138)
[#71](https://code.castopod.org/adaures/castopod/issues/71)
- **rss:** remove escaping for publisher and owner name
([6fc6347](https://code.castopod.org/adaures/castopod/commit/6fc6347846c126618cb7ff50164181650308d0c0))
- **rss:** round episode durations and soundbites
([c9fb987](https://code.castopod.org/adaures/castopod/commit/c9fb987fcfbe17069ec68fdbc823777079ce574b)),
closes [#214](https://code.castopod.org/adaures/castopod/issues/214)
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- **search-episodes:** add fallback sql query using LIKE for search query with
less than 4 characters
([e66bf44](https://code.castopod.org/adaures/castopod/commit/e66bf44341175bc5a10fbf7dfa00b351e76136c2)),
closes [#236](https://code.castopod.org/adaures/castopod/issues/236)
- **security:** add csrf filter + prevent xss attacks by escaping user input
([cd2e1e1](https://code.castopod.org/adaures/castopod/commit/cd2e1e1dc37c53d32d00971c451c4800b8fd6107))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set episode duration translation to hardcoded english
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
- set episode guid upon episode creation
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48)
- set episode numbers during import + remove all custom form_helpers + minor ui
issues
([99a3b8d](https://code.castopod.org/adaures/castopod/commit/99a3b8d33e00482da50dd62bdaa9215a351a56e4))
- set interact_as_actor for user upon password reset
([ad8f5f5](https://code.castopod.org/adaures/castopod/commit/ad8f5f5a0fac7b0b9cc10a0b86200f014aca7553)),
closes [#178](https://code.castopod.org/adaures/castopod/issues/178)
- set localized slug_field key as string in french language
([17fb29b](https://code.castopod.org/adaures/castopod/commit/17fb29b20993b7deee4e252e0e3a4a2459ee0d98))
- set location to null when getting empty string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- set storage limit as disk_total_space instead of free space
([7512e2e](https://code.castopod.org/adaures/castopod/commit/7512e2ed1ff5656cd63a4fc2524296dbb8b4164a))
- **settings:** add .jpg extension to site-icon file input to display all jpeg
images
([f611a16](https://code.castopod.org/adaures/castopod/commit/f611a16cd0c1a389e1c5a287eaec9d2a927a4bb6))
- **socialinteract:** move social interact uri into uri attribute + update
social data upon import
([12b2200](https://code.castopod.org/adaures/castopod/commit/12b22008a237185cb736fc29352fab22421dad16))
- sort episodes by published_at with unpublished episodes at the begining
([1686f84](https://code.castopod.org/adaures/castopod/commit/1686f840d16f2bd3d71d7f222a59b8e6a838fd6e)),
closes [#249](https://code.castopod.org/adaures/castopod/issues/249)
- sort episodic podcasts by season
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
- **themes:** update themes stylesheet route and remove css extension
([e4e7e00](https://code.castopod.org/adaures/castopod/commit/e4e7e0005e931967dd6162588f1c5913dbf4603e))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- **ui:** remove empty tooltip when hovering on sponsor button
([40aa661](https://code.castopod.org/adaures/castopod/commit/40aa661289e1d1517fffcea5d257183bc9c458e4))
- unpublish episode before deleting it + add validation step before deletion
([f75bd76](https://code.castopod.org/adaures/castopod/commit/f75bd76458eeb01a2d37912695e33f77d03b7a69)),
closes [#112](https://code.castopod.org/adaures/castopod/issues/112)
[#55](https://code.castopod.org/adaures/castopod/issues/55)
- update .htaccess for shared hosting config
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.podlibre.org/podlibre/castopod-host/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
# [1.0.0-alpha.64](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2021-07-12)
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
- update form_textarea to prevent escaping value
([78548b5](https://code.castopod.org/adaures/castopod/commit/78548b5cd75ea7d6688d1945ff5449ea4f6bec68))
- update iso-369 language table seeder
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- update ivoox podcasting icon
([f2b69a4](https://code.castopod.org/adaures/castopod/commit/f2b69a47339c887f57883ec612f3d200e512ac1c))
- update MarkdownEditor component + restyle Button and other components
([b05d177](https://code.castopod.org/adaures/castopod/commit/b05d177f1b7f44fef043ac5eb41f07133a2cf52d))
- update purgecss content path for php helper files
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
- update translations for settings' tasks to include what they should be used
for
([06b1a8b](https://code.castopod.org/adaures/castopod/commit/06b1a8b29b6ce5d81c5570d250bdac4e0c9ee5ca))
- use slash instead of backslash to call layout
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
- use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries
([4e22a0d](https://code.castopod.org/adaures/castopod/commit/4e22a0d5e4b60941d41071f059aac80cbaf38fbf))
- **users:** remove required roles input when editing user + prevent owner's
roles from being edited
([1c8af75](https://code.castopod.org/adaures/castopod/commit/1c8af7550ba27d8c8473ae96acd21ad7731fd863)),
closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
- **ux:** have podcast dashboard card link to podcast dashboard if only one
podcast in instance
([7dabee5](https://code.castopod.org/adaures/castopod/commit/7dabee58a187abe92358d962da506a836e29cda3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- validate slug length when submitting episode form + clean permalink edit
prefix
([b07ac09](https://code.castopod.org/adaures/castopod/commit/b07ac093b2cae646f9a897bc9dfeeaef6eda6561))
- **video-clips:** check if created video exists before recreating it and
failing
([dff1208](https://code.castopod.org/adaures/castopod/commit/dff12087251b2b89e195604202094b5ddd9a0936))
- **video-clips:** clear video clip cache after process has finished
([3ae6232](https://code.castopod.org/adaures/castopod/commit/3ae62325856f6ff331a5d9ed901b9fa097ca7055))
- **video-clips:** create unique temporary files for resources to be deleted
after generation
([7f7c878](https://code.castopod.org/adaures/castopod/commit/7f7c878cb6ecf7b4a967b2af87da82bc6593081e))
- **video-clips:** set audio codec to aac, fixing audio issue on twitter
([3c22c68](https://code.castopod.org/adaures/castopod/commit/3c22c68ee81f77bd7fcf7e2739ee6af016407843))
- **video-clips:** set longer podcast and episode lengths for squared format
([c030113](https://code.castopod.org/adaures/castopod/commit/c0301134c2048dc29eb2b995e4d5c22c49444100))
- **video-clips:** tweak portrait parameters to have subtitles display without
overflowing
([2385b1a](https://code.castopod.org/adaures/castopod/commit/2385b1a2926d1344569836e18cb30adb4c604664))
- **video-clips:** update condition to check if ffmpeg is installed
([b57f0b6](https://code.castopod.org/adaures/castopod/commit/b57f0b6eb65dccf22cb4d55f93d18ca36857d7fc)),
closes [#163](https://code.castopod.org/adaures/castopod/issues/163)
- **xml-editor:** escape xml editor's content + restyle form sections to prevent
overflowing
([588590b](https://code.castopod.org/adaures/castopod/commit/588590bd2c0346e2465ff8f1930580d76a3bf068))
- **xml-editor:** prettify xml even without root node
([ca55c24](https://code.castopod.org/adaures/castopod/commit/ca55c248d0562a8529071c1f10be12f40ef50dda))
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.podlibre.org/podlibre/castopod-host/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
# [1.0.0-alpha.63](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2021-07-12)
### Features
- build hashed static files to renew browser cache
([37c54d2](https://code.podlibre.org/podlibre/castopod-host/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.podlibre.org/podlibre/castopod-host/issues/107)
# [1.0.0-alpha.62](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.61...v1.0.0-alpha.62) (2021-07-02)
### Bug Fixes
- **episode:** replace guid's empty string value to null
([441052a](https://code.podlibre.org/podlibre/castopod-host/commit/441052af8d99e6e317edefd1e58ad71799357088))
# [1.0.0-alpha.61](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2021-06-23)
### Bug Fixes
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.podlibre.org/podlibre/castopod-host/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.podlibre.org/podlibre/castopod-host/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.podlibre.org/podlibre/castopod-host/issues/129)
# [1.0.0-alpha.60](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2021-06-21)
### Features
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.podlibre.org/podlibre/castopod-host/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
# [1.0.0-alpha.59](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.58...v1.0.0-alpha.59) (2021-06-15)
### Bug Fixes
- check that additional files are valid when creating episode
([eac5bc8](https://code.podlibre.org/podlibre/castopod-host/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
# [1.0.0-alpha.58](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.57...v1.0.0-alpha.58) (2021-06-11)
### Bug Fixes
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.podlibre.org/podlibre/castopod-host/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.podlibre.org/podlibre/castopod-host/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.podlibre.org/podlibre/castopod-host/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.podlibre.org/podlibre/castopod-host/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.podlibre.org/podlibre/castopod-host/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.podlibre.org/podlibre/castopod-host/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.podlibre.org/podlibre/castopod-host/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set location to null when getting empty string
([71b1b5f](https://code.podlibre.org/podlibre/castopod-host/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- update condition in home controller to redirect to install page
([33f1b91](https://code.podlibre.org/podlibre/castopod-host/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.podlibre.org/podlibre/castopod-host/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.podlibre.org/podlibre/castopod-host/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.podlibre.org/podlibre/castopod-host/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.podlibre.org/podlibre/castopod-host/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.podlibre.org/podlibre/castopod-host/commit/76a4bf344160df679db29e236e7df7822970fb60))
- update broken contributor dropdown fields
([e5b7515](https://code.podlibre.org/podlibre/castopod-host/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.podlibre.org/podlibre/castopod-host/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- update condition in AnalyticsTrait
([fbc0967](https://code.podlibre.org/podlibre/castopod-host/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
### Performance Improvements
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.podlibre.org/podlibre/castopod-host/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.podlibre.org/podlibre/castopod-host/commit/433745f194c73407999b207090478563283876a5))
# [1.0.0-alpha.57](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.56...v1.0.0-alpha.57) (2021-05-12)
### Bug Fixes
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.podlibre.org/podlibre/castopod-host/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
# [1.0.0-alpha.56](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.55...v1.0.0-alpha.56) (2021-05-12)
### Bug Fixes
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.podlibre.org/podlibre/castopod-host/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
# [1.0.0-alpha.55](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.54...v1.0.0-alpha.55) (2021-05-03)
### Features
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
- add about page in admin with instance info + database update button
([d0836f3](https://code.castopod.org/adaures/castopod/commit/d0836f3ee360a836f815c59ea755f288501dc517))
- add alternate rss feed link tag to podcast page head
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
- add analytics and unknown useragents
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add audio-clipper toolbar + add video-clip-previewer
([0255753](https://code.castopod.org/adaures/castopod/commit/02557539e6eb48fc23ee2ee3b0c75aee3310965b))
- add audio-clipper webcomponent (wip)
([21d4251](https://code.castopod.org/adaures/castopod/commit/21d4251b9bcd5acb0f8a1761bc4edc34a3dbc228))
- add autofocus to input field "Email or username" on login page
([19caed4](https://code.castopod.org/adaures/castopod/commit/19caed4bce0daab9ccf6ab9645f44b60eb87de88))
- add basic stats on podcast about page
([1670558](https://code.castopod.org/adaures/castopod/commit/1670558473dba47219d470ff21d6224db6ab42ba))
- add breadcrumb in admin area
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
- add codemirror to display xml editor for custom rss field
([f15f262](https://code.castopod.org/adaures/castopod/commit/f15f26240cd5311fa9d07779f364b6639a501dec))
- add cumulative listening time charts
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
- add default icons to Alert component
([0d98001](https://code.castopod.org/adaures/castopod/commit/0d9800123b135e4fa1a2acd14a5e039c12174333))
- add DropdownMenu component + remove global audio player in admin
([abb7fba](https://code.castopod.org/adaures/castopod/commit/abb7fbac276d77b7d31a0aeba75d464f3ba3ad46))
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- add french translation
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add heading component + update ecs rules to fix views
([23bdc6f](https://code.castopod.org/adaures/castopod/commit/23bdc6f8e36b7e8dfbe32755a54dea59ad913432))
- add housekeeping task to run after migrations
([89dee41](https://code.castopod.org/adaures/castopod/commit/89dee41d583e57251ea9315402a757f03571d7ad))
- add install wizard form to bootstrap database and create the first superadmin
user
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
- add instructions on production error page to ease Castopod debugging process
([9eab54e](https://code.castopod.org/adaures/castopod/commit/9eab54e0853ccb8300d9f9b743cd84aefbf06549)),
closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
- add ISO 3166 country codes
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add js audio player on podcast, admin and embeddable player pages + fix admon
episodes ux
([0e14eb4](https://code.castopod.org/adaures/castopod/commit/0e14eb4d3f526b0fd256a6144f3fbfc3fe52a357)),
closes [#131](https://code.castopod.org/adaures/castopod/issues/131)
- add label to sponsor button on podcast page
([c29c018](https://code.castopod.org/adaures/castopod/commit/c29c018c7a543fc9398b5d7d11f086123e2b33f2)),
closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
- add legalNoticeURL to app config for setting an external url to legal notice
([711843a](https://code.castopod.org/adaures/castopod/commit/711843a0c81e1e2ec7a015431786df4ef32d5092))
- add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
- add map analytics, add episodes analytics, clean analytics page layout,
translate countries
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add media entity and link documents, images and audio files to it
([6ecf286](https://code.castopod.org/adaures/castopod/commit/6ecf2866cfcde31a0840f15c3340808ce14b44cf))
- add notifications inbox for actors
([999999e](https://code.castopod.org/adaures/castopod/commit/999999e3efab7b1aad7568e4fd114dc7bac04f38)),
closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
- add Noto Sans Mono font to use for durations + button to access new video clip
form in list
([7609bb6](https://code.castopod.org/adaures/castopod/commit/7609bb60330539aa91bfdafbb35c2d585624218a))
- add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
- add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
- add permanent delete feature for podcasts 🎉
([dbb4030](https://code.castopod.org/adaures/castopod/commit/dbb4030da49f9ea1f61759fb7c66d71fc29ea4a1)),
closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
- add platform models
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add podcast banner field for each podcast + refactor images configuration
([4a8147b](https://code.castopod.org/adaures/castopod/commit/4a8147bfbbd98d9badfc57a0f2a18bdd5812e802))
- add premium podcasts to manage subscriptions for premium episodes
([3234500](https://code.castopod.org/adaures/castopod/commit/3234500e2d967438ad140f65da801a543f43775d)),
closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
- add publish feature for podcasts and set draft by default
([3d363f2](https://code.castopod.org/adaures/castopod/commit/3d363f2efe99836ac05c305a2fa683e342f06561)),
closes [#128](https://code.castopod.org/adaures/castopod/issues/128)
[#220](https://code.castopod.org/adaures/castopod/issues/220)
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.podlibre.org/podlibre/castopod-host/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
# [1.0.0-alpha.54](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.53...v1.0.0-alpha.54) (2021-05-03)
### Features
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
- add replied to post or comment to reply element
([d0f9c60](https://code.castopod.org/adaures/castopod/commit/d0f9c6018f1af527099f3e26b5d824710fa11caf))
- add schema.org json-ld objects to podcasts, episodes, posts and comments pages
([902f959](https://code.castopod.org/adaures/castopod/commit/902f959b30a10839684f093eb86edebc5d826a0b))
- add task to housekeeping setting for resetting all instance counts
([9303e51](https://code.castopod.org/adaures/castopod/commit/9303e51bc50d730a8026f58984e83b840360ee88))
- add unique listeners analytics
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add update rss feed feature for podcasts to import their latest episodes
([5eb9dc1](https://code.castopod.org/adaures/castopod/commit/5eb9dc168eb9af04767829b76242c9120f55d46d)),
closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
- add user permissions and basic groups to handle authorizations
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18)
- add WebSub module for pushing feed updates to open hubs
([10d3f73](https://code.castopod.org/adaures/castopod/commit/10d3f73786ba141e27a822b2585c4a244ee92c14))
- **admin:** add instance wide dashboard with storage and bandwidth usage
([b1a6c02](https://code.castopod.org/adaures/castopod/commit/b1a6c02e56fdc01a7ff69fa7e7dd8ea71380b7ba)),
closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
- **admin:** add search form in podcast episodes list
([6be5d12](https://code.castopod.org/adaures/castopod/commit/6be5d12877342a7c56e25ea8dd15a975c6ce45ac)),
closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
- **admin:** make header stick on scroll and show title + action buttons using
css only
([d60498c](https://code.castopod.org/adaures/castopod/commit/d60498c1beb970a14eeb3bbe02d1b1d8116624b0))
- **admin:** update admin layout for better ux + update brand pine colors
([d86142e](https://code.castopod.org/adaures/castopod/commit/d86142ebe7cd7582835f180b79fbeaaaba703528))
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
- **analytics-gdpr:** update cached personal data to expire at midnight
([0188b67](https://code.castopod.org/adaures/castopod/commit/0188b67354a756f0c926edd7b46623ab5b20c12b))
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
- **analytics:** add charts and data export
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
- **analytics:** add current date and secret salt to analytics hash for improved
privacy
([6f2e7c0](https://code.castopod.org/adaures/castopod/commit/6f2e7c009c24830d4f08633bfbde3b75f40bf215))
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
- **api:** add rest api with podcasts read endpoints
([e64001d](https://code.castopod.org/adaures/castopod/commit/e64001d00604bcf587ec5e9a631282f212df450d)),
closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
- apply colour theme to embed player
([9548337](https://code.castopod.org/adaures/castopod/commit/9548337a7c49879e8b58c2dfece46e3cfc9517eb)),
closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
- **auth:** add auth.enable2FA config to enable two-factor authentication
([7213ed2](https://code.castopod.org/adaures/castopod/commit/7213ed290c977ce8723f6d92addadc03913576ee))
- build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
- **cache:** add podcast and episode pages to cache + clear them after insert or
update
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **clips:** setup clip entities and model + save video clip to have it
generated in the background
([2f6fdf9](https://code.castopod.org/adaures/castopod/commit/2f6fdf9091d52ca49709fc82621ba1c6dd0e817d))
- **comments:** add comments to episodes + update naming of status to post
([bb4752c](https://code.castopod.org/adaures/castopod/commit/bb4752c35e086664f5fd75fdc0d56546a1e356f6))
- **comments:** add like / undo like to comment + add comment page
([0c187ef](https://code.castopod.org/adaures/castopod/commit/0c187ef7a9278a60bcc6e5ee4d69d948b51e5c54))
- **components:** add custom view renderer with ComponentRenderer adapted from
bonfire2
([a95de8b](https://code.castopod.org/adaures/castopod/commit/a95de8bab010f6b01c598da72191abe97e473687))
- create optimized & resized images upon upload
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6)
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
- **datetime-picker:** set material_green theme to flatpickr
([3ce6541](https://code.castopod.org/adaures/castopod/commit/3ce6541003260677e722a916ad6bc83ef47c4371))
- **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
- display legal disclaimer and warning on podcast import page
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
- edit + delete podcast and episode
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
- enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9)
- enhance ui using javascript in admin area
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- **episode-unpublish:** remove episode comments upon unpublish
([78acd7f](https://code.castopod.org/adaures/castopod/commit/78acd7f5c057c82507d801c424040296dbaba586))
- **episode:** add form to allow editing episode's publication date to a past
date
([d783d16](https://code.castopod.org/adaures/castopod/commit/d783d16eb73d3f896a3dea39a766b4e963e53abf)),
closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
- **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
- **episodes:** replace soft delete with permanent delete
([eb9ff52](https://code.castopod.org/adaures/castopod/commit/eb9ff522c25af8ceb2ed08614b581757ee791d42))
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
- **fonts:** replace Montserrat with Inter for better readablity
([bfa11d0](https://code.castopod.org/adaures/castopod/commit/bfa11d007d04b8ac714c8cf3b8050a6aaf177a26))
- **GDPR:** add GDPR.yml file to public/.well-known/
([86bccc3](https://code.castopod.org/adaures/castopod/commit/86bccc3d5cc9562b89196f1766ac91cdc8ad786d))
- **gdpr:** add purpose for granting access to premium content
([47d6d81](https://code.castopod.org/adaures/castopod/commit/47d6d81b798ec3ed467e0f4339c98c8a6b80cecd))
- **home:** sort podcasts by recent activity + add dropdown menu to choose
between sorting options
([7b89da6](https://code.castopod.org/adaures/castopod/commit/7b89da6106c150708782d39ed2742fe416c41e89)),
closes [#164](https://code.castopod.org/adaures/castopod/issues/164)
- **housekeeping:** add clear_cache option to flush redis or files cache
([99bfac0](https://code.castopod.org/adaures/castopod/commit/99bfac0b428a4bc6fe8bfd10a355dfd93f42ba5c))
- **i18n:** add 7 new languages + update german translations
([d021abb](https://code.castopod.org/adaures/castopod/commit/d021abb52f5525d93810e25df2b453c918d7bc8b))
- **i18n:** add german language as supported locale + create Language files from
english source
([c220b31](https://code.castopod.org/adaures/castopod/commit/c220b310ed59cad188af044b1fed0c39efc7da5b))
- **i18n:** add Norwegian Nynorsk to supported locales
([ced61fc](https://code.castopod.org/adaures/castopod/commit/ced61fc2364f954c1f6e0208b572faf5741498a8))
- **i18n:** add Polish translation
([2d83b44](https://code.castopod.org/adaures/castopod/commit/2d83b44add9e4e00766a1f326377ed892f48ad73))
- **i18n:** add Spanish to supported locales
([e340b54](https://code.castopod.org/adaures/castopod/commit/e340b54a84d7dcdf9ba910fe7ff39c453fac0968))
- **i18n:** add support for German and Brazilian Portuguese languages
([c9b9fe4](https://code.castopod.org/adaures/castopod/commit/c9b9fe4ee893de9a1df7f8269c39d08a90d205d6))
- **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca)
locales
([48d1443](https://code.castopod.org/adaures/castopod/commit/48d14434727c3310a391160c7af02c56b7e20425))
- **icons:** add default icons for podcasting, social and funding platforms +
remove complex icons
([5bcdfeb](https://code.castopod.org/adaures/castopod/commit/5bcdfebe6489b5d6b90f3c828b014ec4e9a7e7e1)),
closes [#166](https://code.castopod.org/adaures/castopod/issues/166)
[#167](https://code.castopod.org/adaures/castopod/issues/167)
[#170](https://code.castopod.org/adaures/castopod/issues/170)
- **icons:** add podnews icon to podcasting platforms
([5f42355](https://code.castopod.org/adaures/castopod/commit/5f423557c2b78fd7c38c5e0caab6c6c80d21e36e)),
closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
- import podcast from an rss feed url
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
- integrate stylized form components and update podcast edit page
([6536729](https://code.castopod.org/adaures/castopod/commit/653672954606a23796e8a7bda3c34fd6b92f84e0))
- make displayed publication time as relative time using @github/time-elements
([230e139](https://code.castopod.org/adaures/castopod/commit/230e139e43324b9ebef06ca8f6e13b3d9a7bdc70))
- make episode description more visible on episode pages
([90533be](https://code.castopod.org/adaures/castopod/commit/90533be0298249e5527870c01329fce5f94ec2dc)),
closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
- **media:** clean media api + create an entity per media type
([fafaa7e](https://code.castopod.org/adaures/castopod/commit/fafaa7e689b17f09a2b056081fa1f4fc53bf716b))
- **media:** save audio, images, transcripts and chapters to media for episode
and persons
([58e2a00](https://code.castopod.org/adaures/castopod/commit/58e2a00a87fa7d5b188e13cc521d94f0cfddba50))
- **meta-tags:** add activitypub alternate links to podcast, episode, comment
and post pages
([bd61752](https://code.castopod.org/adaures/castopod/commit/bd61752be2f574323b05d1d0aee0df55adf9a74e))
- minor corrections to some tables
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
- **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about
the castopod instance
([88fddc8](https://code.castopod.org/adaures/castopod/commit/88fddc81d730978f2a4d8a671936b54041e3fe45))
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
- **podcast-form:** add new_feed_url field to set an url when changing domain or
host
([e7eec48](https://code.castopod.org/adaures/castopod/commit/e7eec48e7bc06a9aa907db01ed3e5b536e7dd8be))
- **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **podcast:** create a podcast using form
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- **podcasting 2.0:** update podcast:social tag to adhere to latest spec
([a597cf4](https://code.castopod.org/adaures/castopod/commit/a597cf4ecfa6807a3413177d99c816056a7e7c45))
- prefill season and episode numbers + set episode number as mandatory for
serial podcasts
([07d740b](https://code.castopod.org/adaures/castopod/commit/07d740b79f9283e389e723954f680f909ce5de4a)),
closes [#134](https://code.castopod.org/adaures/castopod/issues/134)
[#136](https://code.castopod.org/adaures/castopod/issues/136)
- **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13)
- **pwa:** add service-worker + webmanifest for each podcasts to have them
install on devices
([fee2c1c](https://code.castopod.org/adaures/castopod/commit/fee2c1c0d0d03c4ff0a6a207b0a5e0c22bb7b13a))
- redesign public podcast and episode pages + remove any information clutter for
better ux
([9321400](https://code.castopod.org/adaures/castopod/commit/932140077c671f0486a2cd08ceb6126c7ecde87f))
- replace form helper functions with components in admin template
([e64548b](https://code.castopod.org/adaures/castopod/commit/e64548b982ba47ff35f2272e2e30dd85eeba950b))
- replace slug field with interactive permalink component
([578022b](https://code.castopod.org/adaures/castopod/commit/578022b8c5163ffaf8db5870ed5ec9d5d9536477))
- restyle episode and person cards + add focus style to interactive elements for
a11y
([a505a1d](https://code.castopod.org/adaures/castopod/commit/a505a1de56e8e3056379bd60d0595f432e294728))
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75)
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80)
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
- **rss:** add podcast:location tag
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
- **rss:** add rss feed route without the `.xml` extension
([94c0b7c](https://code.castopod.org/adaures/castopod/commit/94c0b7c15920dae9ade5cdc79c7996dbfe82ba05)),
closes [#247](https://code.castopod.org/adaures/castopod/issues/247)
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
- **rss:** add transcript and chapters support
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.castopod.org/adaures/castopod/issues/72)
[#82](https://code.castopod.org/adaures/castopod/issues/82)
- **rss:** generate rss feed from podcast entity
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
- **select:** enhance select input with choices.js
([910d457](https://code.castopod.org/adaures/castopod/commit/910d457cf843e0fc334b3505a4727d51633395ac))
- set app parameter forceGlobalSecureRequests = true forcing requests to go
through https
([d9dff1b](https://code.podlibre.org/podlibre/castopod-host/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- set podcast / episode description in the pages description meta tag
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
- **settings:** add general config for instance (site name, description and
icon)
([5c56f3e](https://code.castopod.org/adaures/castopod/commit/5c56f3e6f00a61af2ccf50811c155c325f2b10fa))
- **settings:** add theme settings to set an accent color for all public pages
([5c529a8](https://code.castopod.org/adaures/castopod/commit/5c529a83aa6d6147d94e5aee996e6b0ab02f0ce4))
- simplify podcast page's layout for better ux
([2c0efc6](https://code.castopod.org/adaures/castopod/commit/2c0efc6563604dd067be88cfc9ddd88a01745e64))
- **soundbites:** add soundbite list and creation forms with audio-clipper
component
([de19317](https://code.castopod.org/adaures/castopod/commit/de19317138a2106deb825c1eed7dda036ed7dac3))
- style file inputs using tailwind's file class
([8208ab6](https://code.castopod.org/adaures/castopod/commit/8208ab6785aae8c49f78eb9ac8cd53d77ec8e5e5))
- **themes:** add ViewThemes library to set views in root themes folder
([7a27676](https://code.castopod.org/adaures/castopod/commit/7a276764e6a1ee3619d9d3488f6163215db75338))
- **themes:** set different default banner per theme
([11c916f](https://code.castopod.org/adaures/castopod/commit/11c916fe433eb749ac32230c48e256057564cbb0))
- **themes:** set generic css variables for colors to enable instance themes
([a746a78](https://code.castopod.org/adaures/castopod/commit/a746a781b4bfc78209cf8302c6d7bb3cb452e446))
- toggle podcast sidebar on smaller screens
([f0205ec](https://code.castopod.org/adaures/castopod/commit/f0205ec274414e881cba40d6776126f05eaee583))
- **transcript:** parse srt subtitles into json file + add max file size info
below audio file input
([0098761](https://code.castopod.org/adaures/castopod/commit/00987610a068c8d6cdd4421ea16585fa037eb61a))
- **ui:** create ViewComponents library to enable building class and view files
components
([94872f2](https://code.castopod.org/adaures/castopod/commit/94872f2338e6025c2f3770be256160838dae9003))
- update analytics so to meet IABv2 requirements
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
- update pine colors + create charts components
([a50abc1](https://code.castopod.org/adaures/castopod/commit/a50abc138d4997b564e3065b37504cda5ce62da6))
- **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
- **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.podlibre.org/podlibre/castopod-host/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.podlibre.org/podlibre/castopod-host/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- **video-clip:** add video-clip page with video preview + logs
([42538dd](https://code.castopod.org/adaures/castopod/commit/42538dd7577be0ffe59b4fdfadbd76cc89e5ef30))
- **video-clip:** generate video clips in the bg using a cron job + add video
clip page + tidy up UI
([db0e427](https://code.castopod.org/adaures/castopod/commit/db0e4272bd6d307c562e1f961d2747cb62de0f35))
- **video-clips:** add dimensions for portrait and squared formats
([3af404d](https://code.castopod.org/adaures/castopod/commit/3af404da3dd1901c78cc7e1778fc225f6716207d))
- **video-clips:** add new themes + add castopod logo as a watermark
([1d1490b](https://code.castopod.org/adaures/castopod/commit/1d1490b06a1f5ecb10b3b98a72efc55d09c10944))
- **video-clips:** add route for scheduled video clips + list video clips with
status
([2065ebb](https://code.castopod.org/adaures/castopod/commit/2065ebbee5e3d0f890ac90b55ca984f1d62a184c))
- **video-clips:** allow episodeNumbering text to stand in the indent of
episodeTitle paragraph
([71a063d](https://code.castopod.org/adaures/castopod/commit/71a063dac311cb21639801fbae6af7c5106c2699))
- **video-clips:** generate a 16:9 video using ffmpeg
([35aa7ea](https://code.castopod.org/adaures/castopod/commit/35aa7ea5d9a339b3e6f745137282268d69fe2231))
- **video-clips:** generate subtitles clip using transcript json to have
subtitles accross video
([3ce07e4](https://code.castopod.org/adaures/castopod/commit/3ce07e455d171e29be30d8ad45055510eb8d363c))
- **video-clips:** replace hardcoded colors with config's theme colors
([e462abf](https://code.castopod.org/adaures/castopod/commit/e462abf6d660e41d2170c52caf45704008de58e9))
- **vite:** add vite config to decouple it from CI_ENVIRONMENT
([8721719](https://code.castopod.org/adaures/castopod/commit/8721719cd7cf32e94823541eafaba1e9309355a8))
- write id3v2 tags to episode's audio file
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.podlibre.org/podlibre/castopod-host/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
- defer javascript + lazy load images for faster page loads
([f0685e4](https://code.castopod.org/adaures/castopod/commit/f0685e44799dfb494592ff97841c0ae035381db8))
- **docker:** add redis caching service for development
([05ace8c](https://code.podlibre.org/podlibre/castopod-host/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
# [1.0.0-alpha.53](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.52...v1.0.0-alpha.53) (2021-04-16)
### Bug Fixes
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.podlibre.org/podlibre/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.podlibre.org/podlibre/castopod/issues/114)
# [1.0.0-alpha.52](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.51...v1.0.0-alpha.52) (2021-04-16)
### Bug Fixes
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.podlibre.org/podlibre/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.podlibre.org/podlibre/castopod/issues/111)
# [1.0.0-alpha.51](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.50...v1.0.0-alpha.51) (2021-04-15)
### Bug Fixes
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.podlibre.org/podlibre/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.podlibre.org/podlibre/castopod/issues/104)
# [1.0.0-alpha.50](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.49...v1.0.0-alpha.50) (2021-04-14)
### Bug Fixes
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.podlibre.org/podlibre/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
# [1.0.0-alpha.49](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.48...v1.0.0-alpha.49) (2021-04-12)
### Bug Fixes
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.podlibre.org/podlibre/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
# [1.0.0-alpha.48](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.47...v1.0.0-alpha.48) (2021-04-10)
### Bug Fixes
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.podlibre.org/podlibre/castopod/issues/103)
([35b5be0](https://code.podlibre.org/podlibre/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
# [1.0.0-alpha.47](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.46...v1.0.0-alpha.47) (2021-04-10)
### Bug Fixes
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.podlibre.org/podlibre/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
# [1.0.0-alpha.46](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.45...v1.0.0-alpha.46) (2021-04-09)
### Bug Fixes
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.podlibre.org/podlibre/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.podlibre.org/podlibre/castopod/issues/100)
# [1.0.0-alpha.45](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.44...v1.0.0-alpha.45) (2021-04-08)
### Bug Fixes
- add head request to analytics_hit route
([f0a2f0b](https://code.podlibre.org/podlibre/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
# [1.0.0-alpha.44](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.43...v1.0.0-alpha.44) (2021-04-08)
### Bug Fixes
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.podlibre.org/podlibre/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.podlibre.org/podlibre/castopod/issues/96)
# [1.0.0-alpha.43](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.42...v1.0.0-alpha.43) (2021-04-08)
### Bug Fixes
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.podlibre.org/podlibre/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.podlibre.org/podlibre/castopod/issues/5)
[#86](https://code.podlibre.org/podlibre/castopod/issues/86)
# [1.0.0-alpha.42](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.41...v1.0.0-alpha.42) (2021-04-02)
### Features
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.podlibre.org/podlibre/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.podlibre.org/podlibre/castopod/issues/69)
[#65](https://code.podlibre.org/podlibre/castopod/issues/65)
[#85](https://code.podlibre.org/podlibre/castopod/issues/85)
[#51](https://code.podlibre.org/podlibre/castopod/issues/51)
[#91](https://code.podlibre.org/podlibre/castopod/issues/91)
[#92](https://code.podlibre.org/podlibre/castopod/issues/92)
[#88](https://code.podlibre.org/podlibre/castopod/issues/88)
# [1.0.0-alpha.41](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.40...v1.0.0-alpha.41) (2021-03-30)
### Features
- **partner:** add link and image in episode description
([ad07bb9](https://code.podlibre.org/podlibre/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
# [1.0.0-alpha.40](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.39...v1.0.0-alpha.40) (2021-03-19)
### Features
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.podlibre.org/podlibre/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
# [1.0.0-alpha.39](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.38...v1.0.0-alpha.39) (2021-03-01)
### Bug Fixes
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.podlibre.org/podlibre/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
# [1.0.0-alpha.38](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.37...v1.0.0-alpha.38) (2021-02-27)
### Features
- **embeddable-player:** add embeddable player widget
([141788f](https://code.podlibre.org/podlibre/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
# [1.0.0-alpha.37](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.36...v1.0.0-alpha.37) (2021-02-17)
### Bug Fixes
- **import:** remove query string from files url
([109c4aa](https://code.podlibre.org/podlibre/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
# [1.0.0-alpha.36](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.35...v1.0.0-alpha.36) (2021-02-16)
### Features
- **platforms:** add pod.link
([3d7a232](https://code.podlibre.org/podlibre/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
# [1.0.0-alpha.35](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.34...v1.0.0-alpha.35) (2021-02-12)
### Bug Fixes
- **admin:** save block and lock switches
([b66c0af](https://code.podlibre.org/podlibre/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
# [1.0.0-alpha.34](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.33...v1.0.0-alpha.34) (2021-02-11)
### Bug Fixes
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.podlibre.org/podlibre/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
# [1.0.0-alpha.33](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.32...v1.0.0-alpha.33) (2021-02-10)
### Features
- **platforms:** add helloasso
([16cb993](https://code.podlibre.org/podlibre/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
# [1.0.0-alpha.32](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.31...v1.0.0-alpha.32) (2021-02-10)
### Features
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.podlibre.org/podlibre/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
# [1.0.0-alpha.31](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.30...v1.0.0-alpha.31) (2020-12-23)
### Features
- **rss:** add podcast:location tag
([c0a2282](https://code.podlibre.org/podlibre/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
# [1.0.0-alpha.30](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.29...v1.0.0-alpha.30) (2020-12-21)
### Features
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.podlibre.org/podlibre/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
# [1.0.0-alpha.29](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.28...v1.0.0-alpha.29) (2020-12-10)
### Bug Fixes
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.podlibre.org/podlibre/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.podlibre.org/podlibre/castopod/issues/70)
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
### Reverts
- **install:** redirect to install in homepage if no database was set
([73f094d](https://code.castopod.org/adaures/castopod/commit/73f094daf26a8cf75e39ebff1eeb7f9039276312))
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
- **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.podlibre.org/podlibre/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
# [1.0.0-alpha.28](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.27...v1.0.0-alpha.28) (2020-12-07)
### Features
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.podlibre.org/podlibre/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.podlibre.org/podlibre/castopod/issues/83)
# [1.0.0-alpha.27](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.26...v1.0.0-alpha.27) (2020-12-07)
### Features
- **platforms:** add AntennaPod
([53e9cfd](https://code.podlibre.org/podlibre/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
# [1.0.0-alpha.26](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.25...v1.0.0-alpha.26) (2020-11-30)
### Bug Fixes
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.podlibre.org/podlibre/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
# [1.0.0-alpha.25](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.24...v1.0.0-alpha.25) (2020-11-30)
### Features
- **platforms:** add podfriend
([9fdc8d3](https://code.podlibre.org/podlibre/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
# [1.0.0-alpha.24](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.23...v1.0.0-alpha.24) (2020-11-26)
### Features
- **monetization:** add Web Monetization support
([96a6026](https://code.podlibre.org/podlibre/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
# [1.0.0-alpha.23](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.22...v1.0.0-alpha.23) (2020-11-24)
### Bug Fixes
- define podcastNamespaceLink value
([0d744d2](https://code.podlibre.org/podlibre/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
# [1.0.0-alpha.22](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.21...v1.0.0-alpha.22) (2020-11-24)
### Features
- **rss:** add transcript and chapters support
([e769d83](https://code.podlibre.org/podlibre/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.podlibre.org/podlibre/castopod/issues/72)
[#82](https://code.podlibre.org/podlibre/castopod/issues/82)
# [1.0.0-alpha.21](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.20...v1.0.0-alpha.21) (2020-11-24)
### Features
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.podlibre.org/podlibre/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
# [1.0.0-alpha.20](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.19...v1.0.0-alpha.20) (2020-11-24)
### Bug Fixes
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.podlibre.org/podlibre/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
### Features
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.podlibre.org/podlibre/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.podlibre.org/podlibre/castopod/issues/73)
[#75](https://code.podlibre.org/podlibre/castopod/issues/75)
[#76](https://code.podlibre.org/podlibre/castopod/issues/76)
[#80](https://code.podlibre.org/podlibre/castopod/issues/80)
# [1.0.0-alpha.19](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.18...v1.0.0-alpha.19) (2020-11-13)
### Bug Fixes
- handle HEAD requests on podcast_feed route
([74b2640](https://code.podlibre.org/podlibre/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.podlibre.org/podlibre/castopod/issues/79)
# [1.0.0-alpha.18](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.17...v1.0.0-alpha.18) (2020-11-09)
### Features
- **platforms:** add Podcast Index
([ad52b1c](https://code.podlibre.org/podlibre/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
# [1.0.0-alpha.17](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.16...v1.0.0-alpha.17) (2020-11-05)
### Bug Fixes
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.podlibre.org/podlibre/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
# [1.0.0-alpha.16](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.15...v1.0.0-alpha.16) (2020-11-04)
### Features
- add Open Graph and Twitter meta tags
([af970b8](https://code.podlibre.org/podlibre/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.podlibre.org/podlibre/castopod/issues/41)
# [1.0.0-alpha.15](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.14...v1.0.0-alpha.15) (2020-11-03)
### Features
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.podlibre.org/podlibre/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
# [1.0.0-alpha.14](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.13...v1.0.0-alpha.14) (2020-11-02)
### Features
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.podlibre.org/podlibre/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
# [1.0.0-alpha.13](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2020-10-29)
### Bug Fixes
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.podlibre.org/podlibre/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
### Features
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.podlibre.org/podlibre/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.podlibre.org/podlibre/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
# [1.0.0-alpha.12](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.11...v1.0.0-alpha.12) (2020-10-26)
### Bug Fixes
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.podlibre.org/podlibre/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
# [1.0.0-alpha.11](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.10...v1.0.0-alpha.11) (2020-10-26)
### Features
- add CDN url
([972bcbf](https://code.podlibre.org/podlibre/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.podlibre.org/podlibre/castopod/issues/37)
# [1.0.0-alpha.10](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2020-10-26)
### Bug Fixes
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.podlibre.org/podlibre/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
# [1.0.0-alpha.9](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2020-10-26)
### Features
- display castopod version in admin footer
([9f2574e](https://code.podlibre.org/podlibre/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.podlibre.org/podlibre/castopod/issues/68)
# [1.0.0-alpha.8](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2020-10-22)
### Features
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.podlibre.org/podlibre/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.podlibre.org/podlibre/castopod/issues/47)
# [1.0.0-alpha.7](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2020-10-21)
### Features
- **analytics:** add service name from rss user-agent
([7202b98](https://code.podlibre.org/podlibre/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
- use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))
### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated
# [1.0.0-alpha.6](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2020-10-20)
# [1.0.0-beta.24](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2022-10-14)
### Bug Fixes
- **router:** trim URI slash to match same routes for URIs with and without
trailing slash
([9e9375f](https://code.castopod.org/adaures/castopod/commit/9e9375f9a2cd6102f827b36ec521f4c86a557c00))
### Features
- **episode:** add form to allow editing episode's publication date to a past
date
([d783d16](https://code.castopod.org/adaures/castopod/commit/d783d16eb73d3f896a3dea39a766b4e963e53abf)),
closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
- **rss:** add rss feed route without the `.xml` extension
([94c0b7c](https://code.castopod.org/adaures/castopod/commit/94c0b7c15920dae9ade5cdc79c7996dbfe82ba05)),
closes [#247](https://code.castopod.org/adaures/castopod/issues/247)
# [1.0.0-beta.23](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2022-09-29)
### Bug Fixes
- **premium-podcasts:** display unlock button in embed when premium episode
([ca109ba](https://code.castopod.org/adaures/castopod/commit/ca109ba3a8a08e661fd2484454b1983c3418f15d))
- **premium-podcasts:** remove cache in unlock form + redirect to podcast if
podcast is not premium
([242352c](https://code.castopod.org/adaures/castopod/commit/242352c4d9cd936de14e8e8a5d78ebf1287b1f95))
- **premium-podcasts:** return different cached page when podcast is unlocked
([b1303c5](https://code.castopod.org/adaures/castopod/commit/b1303c525517498b0edfb9885ff36e08c72628b5))
### Features
- add instructions on production error page to ease Castopod debugging process
([9eab54e](https://code.castopod.org/adaures/castopod/commit/9eab54e0853ccb8300d9f9b743cd84aefbf06549)),
closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
- add premium podcasts to manage subscriptions for premium episodes
([3234500](https://code.castopod.org/adaures/castopod/commit/3234500e2d967438ad140f65da801a543f43775d)),
closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
- **gdpr:** add purpose for granting access to premium content
([47d6d81](https://code.castopod.org/adaures/castopod/commit/47d6d81b798ec3ed467e0f4339c98c8a6b80cecd))
# [1.0.0-beta.22](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2022-09-23)
### Bug Fixes
- **fediverse:** set default castopod avatar url when actor avatar is not
present
([460f52f](https://code.castopod.org/adaures/castopod/commit/460f52f70e493d619c28632db6c698e88f0ebb5f))
- **import:** set default episode type if not set
([d7250ab](https://code.castopod.org/adaures/castopod/commit/d7250ab03f9b032830c575ad58b51c8d60b7a49a))
- **input-component:** unset required attribute to prevent rendering it when
false
([db9ac13](https://code.castopod.org/adaures/castopod/commit/db9ac13860bce58235a5da275910bea605a00626))
- **notifications:** notify actors after activities insert / update using model
callback methods
([e08555a](https://code.castopod.org/adaures/castopod/commit/e08555a4e9a6c15eeba18273c63403f82eddae35))
- overwrite getActorById to return app's Actor entity
([f2bc2f7](https://code.castopod.org/adaures/castopod/commit/f2bc2f7e01aa166faa627df6fe4d5ed4887c16e5))
- remove heavy image cover data from audio file metadata
([f74403b](https://code.castopod.org/adaures/castopod/commit/f74403bd7a5089b760603abe36264e7615be0e78))
- set storage limit as disk_total_space instead of free space
([7512e2e](https://code.castopod.org/adaures/castopod/commit/7512e2ed1ff5656cd63a4fc2524296dbb8b4164a))
- **ui:** remove empty tooltip when hovering on sponsor button
([40aa661](https://code.castopod.org/adaures/castopod/commit/40aa661289e1d1517fffcea5d257183bc9c458e4))
- **users:** remove required roles input when editing user + prevent owner's
roles from being edited
([1c8af75](https://code.castopod.org/adaures/castopod/commit/1c8af7550ba27d8c8473ae96acd21ad7731fd863)),
closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
- **ux:** have podcast dashboard card link to podcast dashboard if only one
podcast in instance
([7dabee5](https://code.castopod.org/adaures/castopod/commit/7dabee58a187abe92358d962da506a836e29cda3))
# [1.0.0-beta.21](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2022-09-06)
### Bug Fixes
- **email:** set the correct url in the activation and forgot emails
([10fc6f1](https://code.castopod.org/adaures/castopod/commit/10fc6f17c6838a58348f32ccfd0cf05f9d3e172c)),
closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
- **notifications:** add trigger after activities update + update insert trigger
([e5d16e8](https://code.castopod.org/adaures/castopod/commit/e5d16e87119021fa5a43470d67ddfe5128e57f74))
### Features
- **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca)
locales
([48d1443](https://code.castopod.org/adaures/castopod/commit/48d14434727c3310a391160c7af02c56b7e20425))
# [1.0.0-beta.20](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2022-08-12)
### Bug Fixes
- add underline and semibold font weight for prose links to have them stand out
([d4d8671](https://code.castopod.org/adaures/castopod/commit/d4d867121c50bded4176a53d7154cf1bb347e306))
- **router:** check if Accept header is set before getting value
([10a2ae0](https://code.castopod.org/adaures/castopod/commit/10a2ae02484672d6a0fbc6e7b943519c5ec16cb6)),
closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
- **search-episodes:** add fallback sql query using LIKE for search query with
less than 4 characters
([e66bf44](https://code.castopod.org/adaures/castopod/commit/e66bf44341175bc5a10fbf7dfa00b351e76136c2)),
closes [#236](https://code.castopod.org/adaures/castopod/issues/236)
- set interact_as_actor for user upon password reset
([ad8f5f5](https://code.castopod.org/adaures/castopod/commit/ad8f5f5a0fac7b0b9cc10a0b86200f014aca7553)),
closes [#178](https://code.castopod.org/adaures/castopod/issues/178)
### Features
- add label to sponsor button on podcast page
([c29c018](https://code.castopod.org/adaures/castopod/commit/c29c018c7a543fc9398b5d7d11f086123e2b33f2)),
closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
- add notifications inbox for actors
([999999e](https://code.castopod.org/adaures/castopod/commit/999999e3efab7b1aad7568e4fd114dc7bac04f38)),
closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
# [1.0.0-beta.19](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2022-07-21)
### Bug Fixes
- **episode-unpublish:** set consistent posts_counts' increments/decrements for
actors and episodes
([8acdafd](https://code.castopod.org/adaures/castopod/commit/8acdafd26044e50a4d6ee451bf24ad66003c5bb3)),
closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
- **get_browser_language:** return defaultLocale if browser doesn't send user
preferred language
([9cc2996](https://code.castopod.org/adaures/castopod/commit/9cc299626181048b85b629bbe7f5806a1f5d21ff))
### Features
- **episode-unpublish:** remove episode comments upon unpublish
([78acd7f](https://code.castopod.org/adaures/castopod/commit/78acd7f5c057c82507d801c424040296dbaba586))
# [1.0.0-beta.18](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2022-07-07)
### Bug Fixes
- **player-styling:** revert vite to 2.8 to reference the player css
([e07d3af](https://code.castopod.org/adaures/castopod/commit/e07d3afea9af85b8361227e000fb64b502781668))
### Features
- add legalNoticeURL to app config for setting an external url to legal notice
([711843a](https://code.castopod.org/adaures/castopod/commit/711843a0c81e1e2ec7a015431786df4ef32d5092))
# [1.0.0-beta.17](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2022-07-06)
### Bug Fixes
- explicitly cast seconds to int in iso8601_duration helper function
([779653f](https://code.castopod.org/adaures/castopod/commit/779653f75b140942f731cbb238bc0667cc461307))
- **housekeeping:** use EpisodeModel's builder to reset comments count
([65e9c0b](https://code.castopod.org/adaures/castopod/commit/65e9c0b05ea4992884149cb4a4b071bf31a20a1a))
- **rss:** round episode durations and soundbites
([c9fb987](https://code.castopod.org/adaures/castopod/commit/c9fb987fcfbe17069ec68fdbc823777079ce574b)),
closes [#214](https://code.castopod.org/adaures/castopod/issues/214)
- **xml-editor:** prettify xml even without root node
([ca55c24](https://code.castopod.org/adaures/castopod/commit/ca55c248d0562a8529071c1f10be12f40ef50dda))
### Features
- add publish feature for podcasts and set draft by default
([3d363f2](https://code.castopod.org/adaures/castopod/commit/3d363f2efe99836ac05c305a2fa683e342f06561)),
closes [#128](https://code.castopod.org/adaures/castopod/issues/128)
[#220](https://code.castopod.org/adaures/castopod/issues/220)
- **admin:** add instance wide dashboard with storage and bandwidth usage
([b1a6c02](https://code.castopod.org/adaures/castopod/commit/b1a6c02e56fdc01a7ff69fa7e7dd8ea71380b7ba)),
closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
- **datetime-picker:** set material_green theme to flatpickr
([3ce6541](https://code.castopod.org/adaures/castopod/commit/3ce6541003260677e722a916ad6bc83ef47c4371))
# [1.0.0-beta.16](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2022-06-24)
### Bug Fixes
- change image size requirement hints
([ea20206](https://code.castopod.org/adaures/castopod/commit/ea20206ee674eb54dd3ea188d2a2e2d41425df65))
### Features
- add update rss feed feature for podcasts to import their latest episodes
([5eb9dc1](https://code.castopod.org/adaures/castopod/commit/5eb9dc168eb9af04767829b76242c9120f55d46d)),
closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
- **admin:** add search form in podcast episodes list
([6be5d12](https://code.castopod.org/adaures/castopod/commit/6be5d12877342a7c56e25ea8dd15a975c6ce45ac)),
closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
- **api:** add rest api with podcasts read endpoints
([e64001d](https://code.castopod.org/adaures/castopod/commit/e64001d00604bcf587ec5e9a631282f212df450d)),
closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
# [1.0.0-beta.15](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-06-14)
### Bug Fixes
- replace deletedField with published_at for episodes
([14d7d07](https://code.castopod.org/adaures/castopod/commit/14d7d078225cdc8980759273a5dc4163d9f84b06))
### Features
- add default icons to Alert component
([0d98001](https://code.castopod.org/adaures/castopod/commit/0d9800123b135e4fa1a2acd14a5e039c12174333))
- add permanent delete feature for podcasts 🎉
([dbb4030](https://code.castopod.org/adaures/castopod/commit/dbb4030da49f9ea1f61759fb7c66d71fc29ea4a1)),
closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
- apply colour theme to embed player
([9548337](https://code.castopod.org/adaures/castopod/commit/9548337a7c49879e8b58c2dfece46e3cfc9517eb)),
closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
- **episodes:** replace soft delete with permanent delete
([eb9ff52](https://code.castopod.org/adaures/castopod/commit/eb9ff522c25af8ceb2ed08614b581757ee791d42))
# [1.0.0-beta.14](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-04-23)
### Bug Fixes
- **home:** remove hardcoded prefix in getAllPodcasts query
([92d5cc5](https://code.castopod.org/adaures/castopod/commit/92d5cc50a3e533875cd894dccc417918102d4b7f))
- overwrite common lang function to escape returned string
([4c490c1](https://code.castopod.org/adaures/castopod/commit/4c490c15bb6642ad0b2aaddf08d8af25de99b4b0)),
closes [#196](https://code.castopod.org/adaures/castopod/issues/196)
[#198](https://code.castopod.org/adaures/castopod/issues/198)
### Features
- **i18n:** add Spanish to supported locales
([e340b54](https://code.castopod.org/adaures/castopod/commit/e340b54a84d7dcdf9ba910fe7ff39c453fac0968))
# [1.0.0-beta.13](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-04-14)
### Bug Fixes
- **rss:** remove escaping for publisher and owner name
([e2046e4](https://code.castopod.org/adaures/castopod/commit/e2046e4b116ecddb5e6d68487f666b95fd7f493c))
- use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries
([853a6ba](https://code.castopod.org/adaures/castopod/commit/853a6ba9155b6687604304d59f03d0efb75a9f96))
### Features
- **i18n:** add Norwegian Nynorsk to supported locales
([744340d](https://code.castopod.org/adaures/castopod/commit/744340df615bee38a54c4abbbb7f03d51b61a39d))
# [1.0.0-beta.12](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-04-05)
### Bug Fixes
- update form_textarea to prevent escaping value
([78548b5](https://code.castopod.org/adaures/castopod/commit/78548b5cd75ea7d6688d1945ff5449ea4f6bec68))
### Features
- **i18n:** add support for German and Brazilian Portuguese languages
([19da003](https://code.castopod.org/adaures/castopod/commit/19da003fd396bff20b89ad330b787e9cdbe8d919))
# [1.0.0-beta.11](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-04-01)
### Bug Fixes
- change message upon cancellation of episode publication
([9859c74](https://code.castopod.org/adaures/castopod/commit/9859c7434c2a3478ce035f7a4de20f594d63f5b0))
- prefill description footer input when creating a new episode
([9ea5ca3](https://code.castopod.org/adaures/castopod/commit/9ea5ca31697c70d176294f8aea37bd57d471fcf7))
- remove value escaping for form inputs and textareas
([bc6dea2](https://code.castopod.org/adaures/castopod/commit/bc6dea2f8ad1cf0aee0eaa93151332fbac7fb771))
- restore default podcast icon on public website
([342778b](https://code.castopod.org/adaures/castopod/commit/342778bac3c684328d72633961df1a2ebdc1330e))
- **socialinteract:** move social interact uri into uri attribute + update
social data upon import
([12b2200](https://code.castopod.org/adaures/castopod/commit/12b22008a237185cb736fc29352fab22421dad16))
### Features
- **analytics-gdpr:** update cached personal data to expire at midnight
([0188b67](https://code.castopod.org/adaures/castopod/commit/0188b67354a756f0c926edd7b46623ab5b20c12b))
- **analytics:** add current date and secret salt to analytics hash for improved
privacy
([6f2e7c0](https://code.castopod.org/adaures/castopod/commit/6f2e7c009c24830d4f08633bfbde3b75f40bf215))
- **i18n:** add 7 new languages + update german translations
([d021abb](https://code.castopod.org/adaures/castopod/commit/d021abb52f5525d93810e25df2b453c918d7bc8b))
- **i18n:** add german language as supported locale + create Language files from
english source
([c220b31](https://code.castopod.org/adaures/castopod/commit/c220b310ed59cad188af044b1fed0c39efc7da5b))
- **icons:** add podnews icon to podcasting platforms
([5f42355](https://code.castopod.org/adaures/castopod/commit/5f423557c2b78fd7c38c5e0caab6c6c80d21e36e)),
closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
# [1.0.0-beta.10](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-03-15)
### Bug Fixes
- add explicit int conversion when formatting episode duration
([1253096](https://code.castopod.org/adaures/castopod/commit/1253096197a0d30692bdafa7152f250cd9a71acf))
- add href to castopod website on login page
([cc54257](https://code.castopod.org/adaures/castopod/commit/cc5425735184ad738aa0f38540f18e8971f8f56e))
- move html escaping on credits page
([fbffdbd](https://code.castopod.org/adaures/castopod/commit/fbffdbde78544c83138ee6234c62d43056f407b6))
- remove cache from remote follow form to display error messages
([90e4443](https://code.castopod.org/adaures/castopod/commit/90e44437bdf37d8024ef609b2f7336dbdfc3b974))
### Features
- add autofocus to input field "Email or username" on login page
([19caed4](https://code.castopod.org/adaures/castopod/commit/19caed4bce0daab9ccf6ab9645f44b60eb87de88))
- add WebSub module for pushing feed updates to open hubs
([10d3f73](https://code.castopod.org/adaures/castopod/commit/10d3f73786ba141e27a822b2585c4a244ee92c14))
- **GDPR:** add GDPR.yml file to public/.well-known/
([86bccc3](https://code.castopod.org/adaures/castopod/commit/86bccc3d5cc9562b89196f1766ac91cdc8ad786d))
# [1.0.0-beta.9](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-03-04)
### Bug Fixes
- **cache:** delete posts and comments pages cache when updating platform links
([f7c3e5b](https://code.castopod.org/adaures/castopod/commit/f7c3e5bf4ad43389bf8d58d2c4aaf16b81cbce00)),
closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
- escape characters for `min` in format_duration_symbol
([3b6722a](https://code.castopod.org/adaures/castopod/commit/3b6722a42b9e4330e5235d4ceed41c777159f4dc))
- **security:** add csrf filter + prevent xss attacks by escaping user input
([cd2e1e1](https://code.castopod.org/adaures/castopod/commit/cd2e1e1dc37c53d32d00971c451c4800b8fd6107))
- update ivoox podcasting icon
([f2b69a4](https://code.castopod.org/adaures/castopod/commit/f2b69a47339c887f57883ec612f3d200e512ac1c))
- **video-clips:** update condition to check if ffmpeg is installed
([b57f0b6](https://code.castopod.org/adaures/castopod/commit/b57f0b6eb65dccf22cb4d55f93d18ca36857d7fc)),
closes [#163](https://code.castopod.org/adaures/castopod/issues/163)
### Features
- **i18n:** add Polish translation
([2d83b44](https://code.castopod.org/adaures/castopod/commit/2d83b44add9e4e00766a1f326377ed892f48ad73))
- **icons:** add default icons for podcasting, social and funding platforms +
remove complex icons
([5bcdfeb](https://code.castopod.org/adaures/castopod/commit/5bcdfebe6489b5d6b90f3c828b014ec4e9a7e7e1)),
closes [#166](https://code.castopod.org/adaures/castopod/issues/166)
[#167](https://code.castopod.org/adaures/castopod/issues/167)
[#170](https://code.castopod.org/adaures/castopod/issues/170)
- make episode description more visible on episode pages
([90533be](https://code.castopod.org/adaures/castopod/commit/90533be0298249e5527870c01329fce5f94ec2dc)),
closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
- **podcasting 2.0:** update podcast:social tag to adhere to latest spec
([a597cf4](https://code.castopod.org/adaures/castopod/commit/a597cf4ecfa6807a3413177d99c816056a7e7c45))
# [1.0.0-beta.8](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-02-10)
### Features
- **podcast-form:** add new_feed_url field to set an url when changing domain or
host
([e7eec48](https://code.castopod.org/adaures/castopod/commit/e7eec48e7bc06a9aa907db01ed3e5b536e7dd8be))
# [1.0.0-beta.7](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-02-05)
### Bug Fixes
- **activitypub:** allow cors on get requests for routes exposing acitivitypub
objects
([2f24809](https://code.castopod.org/adaures/castopod/commit/2f2480998f9abb34f02ab186c65d462a74b4e640))
- **fediverse:** set model instances as non shared to prevent overlapping
([91128fa](https://code.castopod.org/adaures/castopod/commit/91128fad7a68e1f4e5acacba90b6899288699e61))
- **htaccess:** add ? after index.php in RewriteRule
([d9d139e](https://code.castopod.org/adaures/castopod/commit/d9d139eefa03c28d1a064b3b32c9036193497e57)),
closes [#152](https://code.castopod.org/adaures/castopod/issues/152)
### Features
- **home:** sort podcasts by recent activity + add dropdown menu to choose
between sorting options
([7b89da6](https://code.castopod.org/adaures/castopod/commit/7b89da6106c150708782d39ed2742fe416c41e89)),
closes [#164](https://code.castopod.org/adaures/castopod/issues/164)
# [1.0.0-beta.6](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-02-03)
### Bug Fixes
- **activitypub:** add conditions for possibly missing actor properties + add
user-agent to requests
([8fbf948](https://code.castopod.org/adaures/castopod/commit/8fbf948fbba22ffd33966a1b2ccd42e8f7c1f8a2))
- **activitypub:** add target actor id to like / announce activities to send
directly to note's actor
([962dd30](https://code.castopod.org/adaures/castopod/commit/962dd305f5d3f6eadc68f400e0e8f953827fe20d))
- **activitypub:** add target_actor_id for create activity to broadcast post
reply
([0128a21](https://code.castopod.org/adaures/castopod/commit/0128a21ec55dcc0a2fbf4081dadb4c4737735ba1))
- **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be
sent in any order
([b7f285e](https://code.castopod.org/adaures/castopod/commit/b7f285e4e24247fedb94f030356fa6f291f525cc))
- **install:** set message block on forms to show error messages
([3a0a20d](https://code.castopod.org/adaures/castopod/commit/3a0a20d59cdae7f166325efb750eaa6e9800ba6e)),
closes [#157](https://code.castopod.org/adaures/castopod/issues/157)
- **markdown-editor:** remove unnecessary buttons for podcast and episode
editors + add extensions
([9c4f60e](https://code.castopod.org/adaures/castopod/commit/9c4f60e00bcbd4f784f12d2a6fed357ad402ee2e))
- **podcast-activity:** check if transcript and chapters are set before
including them in audio
([5855a25](https://code.castopod.org/adaures/castopod/commit/5855a250936f91641efef77650890a18d8e9917f))
- **podcast:** use markdown description value for editor + set prose class to
about description
([f304d97](https://code.castopod.org/adaures/castopod/commit/f304d97b14e0ef383509cb3bba50beb55bf701ba)),
closes [#156](https://code.castopod.org/adaures/castopod/issues/156)
# [1.0.0-beta.5](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-01-31)
### Bug Fixes
- **analytics:** set initial value for duration and bandwidth
([ee50539](https://code.castopod.org/adaures/castopod/commit/ee5053959154b1a2e5fbe4b43162968425206a26))
# [1.0.0-beta.4](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-01-29)
### Bug Fixes
- **housekeeping:** replace the use of GLOB_BRACE with looping over file
extensions
([42d92d0](https://code.castopod.org/adaures/castopod/commit/42d92d0c8dfe0c567c28f5bfdda129890fa4c2ec)),
closes [#154](https://code.castopod.org/adaures/castopod/issues/154)
- **housekeeping:** set default sizes value + ignore illegal IFD size error to
proceed with script
([f21ca57](https://code.castopod.org/adaures/castopod/commit/f21ca57603cfa503699b7e09a155e18d876d65fe))
### Features
- **housekeeping:** add clear_cache option to flush redis or files cache
([99bfac0](https://code.castopod.org/adaures/castopod/commit/99bfac0b428a4bc6fe8bfd10a355dfd93f42ba5c))
# [1.0.0-beta.3](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-01-28)
### Bug Fixes
- revert to beta.1's codeigniter4 version
([e831411](https://code.castopod.org/adaures/castopod/commit/e83141127080ccde44987195db46ba97fd6cc2ca))
# [1.0.0-beta.2](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-01-28)
### Bug Fixes
- **migrations:** ignore invalid utf8 chars for media files metadata + update
transcript parser
([45e8f99](https://code.castopod.org/adaures/castopod/commit/45e8f99e753cc02ec105e6f4d7fe026a205724f8))
- **video-clips:** set audio codec to aac, fixing audio issue on twitter
([3c22c68](https://code.castopod.org/adaures/castopod/commit/3c22c68ee81f77bd7fcf7e2739ee6af016407843))
- **video-clips:** set longer podcast and episode lengths for squared format
([c030113](https://code.castopod.org/adaures/castopod/commit/c0301134c2048dc29eb2b995e4d5c22c49444100))
# 1.0.0-beta.1 (2022-01-23)
### Bug Fixes
- **a11y:** replace active tab color to contrast with background on podcast and
episode pages
([f3785e1](https://code.castopod.org/adaures/castopod/commit/f3785e140147d085a2fb6a62ded87cdfe360f442))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **activitypub:** set created_by to null for reblog if no user + update episode
oembed data
([209dfbd](https://code.castopod.org/adaures/castopod/commit/209dfbd134e1a2cc02e7c24c158d786fa4dda61d))
- add admin-audio-player to vite config to have admin player show up
([93cb9b2](https://code.castopod.org/adaures/castopod/commit/93cb9b24701c09b92820204a67c1fc1b3c044708))
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
- add category_label component to include parent category in about podcast page
([74e7d68](https://code.castopod.org/adaures/castopod/commit/74e7d68ac834885c4b89ee6e7d60db2157165799))
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
- add missing explicit badge for podcasts and episodes
([cdf9f9d](https://code.castopod.org/adaures/castopod/commit/cdf9f9d53f2597f19455cb65c51da4677bb99327))
- add open graph size for podcast images to replace the inadequate large format
([33aae1f](https://code.castopod.org/adaures/castopod/commit/33aae1f7934e4962116e94e477dbf48e24971f5f))
- add public/media folder to castopod bundle
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
- add translation key for audio-clipper trim labels
([db191ac](https://code.castopod.org/adaures/castopod/commit/db191ac31bd16bad2a72afdb8b25c685adf86a6e))
- add where condition to get episode count without deleted episodes
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
- **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
- **audio-clipper:** add mouse position offset when stretching clip to prevent
content from jumping
([602654b](https://code.castopod.org/adaures/castopod/commit/602654b99b33ee8c29da080058a0aaea976cd484))
- **audio-clipper:** show audio playing progress + put waveform behind audio
clipper
([01a09dc](https://code.castopod.org/adaures/castopod/commit/01a09dc447b81c5412ceb45d6706a867939fd4dd))
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
- **bundle:** include modules and themes when copying files with rsync
([cd5bb88](https://code.castopod.org/adaures/castopod/commit/cd5bb8835c6e259408a8c13a2196a347e161da83))
- **bundle:** update vite input files path + add `set -e` in bash scripts to
fail if command fails
([0ee53c7](https://code.castopod.org/adaures/castopod/commit/0ee53c71ffadb8a6ddb1febd9f912bc99f5f7a0b))
- **cache:** add locale for podcast and episode pages + clear some persisting
cache in models
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61)
- **cache:** return a non cached view when connected
([e2e7358](https://code.castopod.org/adaures/castopod/commit/e2e735815d805a48eed2ea3288d060d0ddb253a3))
- **cache:** suffix cache names with authenticated for credits, map and pages
([418a70b](https://code.castopod.org/adaures/castopod/commit/418a70b2a670d8ba0ab6c15fa5faa41f6be55e53))
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
- clear cache when deleting podcast banner
([99bb40b](https://code.castopod.org/adaures/castopod/commit/99bb40b8bc17b8ee2cd8468a82e46ea280c92cb6))
- comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- **comments:** add comment view partials for public pages
([fcecbe1](https://code.castopod.org/adaures/castopod/commit/fcecbe1c68b0d28d19454fba65caf3ab769fbc75))
- correct chart data
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
- define podcast_id and platform_slug as foreign keys in podcasts_plaforms table
([6e9451a](https://code.castopod.org/adaures/castopod/commit/6e9451a1103b43750fa70ad576de36af25ca29cb))
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
- **embed:** open embedded player's links in new tab
([4aa73d7](https://code.castopod.org/adaures/castopod/commit/4aa73d71e3b8c0a6c3f75f4d1d45c4d693aba64c))
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
- escape generated feed tag values and remove new lines from public pages meta
description
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46)
- expire default query cache upon scheduled episode publication
([b72e7c8](https://code.castopod.org/adaures/castopod/commit/b72e7c8691c887e41107baea0a4d50a39eaf8c8b)),
closes [#81](https://code.castopod.org/adaures/castopod/issues/81)
- fix layout bugs in admin and update translation files
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
- **images:** set default mimetype if none is specified when getting size info
([6e4acc6](https://code.castopod.org/adaures/castopod/commit/6e4acc64ad256178cee7905402b48bafcd49f84c))
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
- **import:** add validation for handle field to prevent
Router.invalidParameterType error
([5bf7200](https://code.castopod.org/adaures/castopod/commit/5bf7200fb390f2447b29f24b495f24483cf7b205)),
closes [#119](https://code.castopod.org/adaures/castopod/issues/119)
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
- **import:** save media files during podcast import + set missing media fields
([a9989d8](https://code.castopod.org/adaures/castopod/commit/a9989d841a634f8cf6c04df25f40bb1e7d4fcdcc))
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
- include missing variables on public ui's episode page and remote_actions
([193b373](https://code.castopod.org/adaures/castopod/commit/193b373bc94a5270acae99b637aa84b6cb2dedfe))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
- **install:** redirect to input baseUrl after instance config
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
- **json-ld:** add missing properties to PodcastSeries object
([e97266c](https://code.castopod.org/adaures/castopod/commit/e97266c5d4883a10f68b3685ecc0d1942f54d658))
- keep subtitle line breaks when parsing srt file to json
([cfb3da6](https://code.castopod.org/adaures/castopod/commit/cfb3da6592f2de23cb1a7ac420f19fc77fa338aa))
- **layouts:** replace holy-grail layout with tailwind config + widen public
podcast layout
([be5a287](https://code.castopod.org/adaures/castopod/commit/be5a28787fdb180b64d9bf570120eff7072ab9aa))
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
- **md-editor:** build new markdown editor with lit +
github/markdown-toolbar-element
([9ec1cb9](https://code.castopod.org/adaures/castopod/commit/9ec1cb93da6f41124c48b8cf14ee6942e865bede)),
closes [#93](https://code.castopod.org/adaures/castopod/issues/93)
[#94](https://code.castopod.org/adaures/castopod/issues/94)
[#120](https://code.castopod.org/adaures/castopod/issues/120)
- minor corrections
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
- **package.json:** update destination of postcss generation scripts
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- **pages:** add locale to page cache
([8f999ce](https://code.castopod.org/adaures/castopod/commit/8f999ce2f7ee1416c30cf58c84f67b3d11b3f142))
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
- pass timezone to relative time component to show the localized time in the UI
([b9db936](https://code.castopod.org/adaures/castopod/commit/b9db936461d4cb914958bb3256bb910bbd7ba815))
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
- **persons:** set person picture as optional for better ux
([7fdea63](https://code.castopod.org/adaures/castopod/commit/7fdea63de7e572810082c84fff3013af580df58b)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
- **podcast-import:** move guid attribute declaration for Episode entity to
include slug data
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328))
- **pwa:** add scope to webmanifests to allow installing an app per podcast
([74c683e](https://code.castopod.org/adaures/castopod/commit/74c683eb44398a84443ec17903c3e002bb5ea9b9))
- **pwa:** set app display as standalone in the webmanifests
([7aa37d2](https://code.castopod.org/adaures/castopod/commit/7aa37d24ac13a1ee160c01a56b43621d7efcfbbc))
- re-order graph values
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- redirect to non cached views when authenticated in public views
([482b47b](https://code.castopod.org/adaures/castopod/commit/482b47ba6bdab7f27fc5704a559567228e07cd14))
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- remove defer from js script declaration as it is a module
([18ae557](https://code.castopod.org/adaures/castopod/commit/18ae557e97f1cef775cd1e75fb1fedee7f1c0cc9))
- remove fixed size from podcast sidebar + rearrange account info + space out
import radio inputs
([776eec6](https://code.castopod.org/adaures/castopod/commit/776eec6f0d533d6c92ebec16f7a9dbfcde1f41f4))
- remove required for other_categories field and add podcast_id to latest
podcasts query
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- remove required property to persons picture
([c546be3](https://code.castopod.org/adaures/castopod/commit/c546be385b243014243ae93356006cd126d2f00d)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
- rename issue_templates labels
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
- rename podcast name to podcast handle to clarify field usage
([9dd4c77](https://code.castopod.org/adaures/castopod/commit/9dd4c7741eb1b7cb5fc214ff674697f3aa986df0)),
closes [#126](https://code.castopod.org/adaures/castopod/issues/126)
- reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
- replace hardcoded style links with vite service + set default value for remote
transcript url
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150)
- replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
- rewrite regenerate image function to use saveSizes method from Image entity
([3889912](https://code.castopod.org/adaures/castopod/commit/38899124ec27e94a8c798bc2db528f9f785eec20))
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
- **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
- **rss:** do not escape podcast and episode titles in the xml
([0dd3b7e](https://code.castopod.org/adaures/castopod/commit/0dd3b7e0bf00d5a9eb80c93cba1efcada59ec3c1)),
closes [#138](https://code.castopod.org/adaures/castopod/issues/138)
[#71](https://code.castopod.org/adaures/castopod/issues/71)
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set episode duration translation to hardcoded english
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
- set episode guid upon episode creation
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48)
- set episode numbers during import + remove all custom form_helpers + minor ui
issues
([99a3b8d](https://code.castopod.org/adaures/castopod/commit/99a3b8d33e00482da50dd62bdaa9215a351a56e4))
- set localized slug_field key as string in french language
([17fb29b](https://code.castopod.org/adaures/castopod/commit/17fb29b20993b7deee4e252e0e3a4a2459ee0d98))
- set location to null when getting empty string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- **settings:** add .jpg extension to site-icon file input to display all jpeg
images
([f611a16](https://code.castopod.org/adaures/castopod/commit/f611a16cd0c1a389e1c5a287eaec9d2a927a4bb6))
- sort episodic podcasts by season
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
- **themes:** update themes stylesheet route and remove css extension
([e4e7e00](https://code.castopod.org/adaures/castopod/commit/e4e7e0005e931967dd6162588f1c5913dbf4603e))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- unpublish episode before deleting it + add validation step before deletion
([f75bd76](https://code.castopod.org/adaures/castopod/commit/f75bd76458eeb01a2d37912695e33f77d03b7a69)),
closes [#112](https://code.castopod.org/adaures/castopod/issues/112)
[#55](https://code.castopod.org/adaures/castopod/issues/55)
- update .htaccess for shared hosting config
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
- update iso-369 language table seeder
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- update MarkdownEditor component + restyle Button and other components
([b05d177](https://code.castopod.org/adaures/castopod/commit/b05d177f1b7f44fef043ac5eb41f07133a2cf52d))
- update purgecss content path for php helper files
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
- update translations for settings' tasks to include what they should be used
for
([06b1a8b](https://code.castopod.org/adaures/castopod/commit/06b1a8b29b6ce5d81c5570d250bdac4e0c9ee5ca))
- use slash instead of backslash to call layout
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- **video-clips:** check if created video exists before recreating it and
failing
([dff1208](https://code.castopod.org/adaures/castopod/commit/dff12087251b2b89e195604202094b5ddd9a0936))
- **video-clips:** clear video clip cache after process has finished
([3ae6232](https://code.castopod.org/adaures/castopod/commit/3ae62325856f6ff331a5d9ed901b9fa097ca7055))
- **video-clips:** create unique temporary files for resources to be deleted
after generation
([7f7c878](https://code.castopod.org/adaures/castopod/commit/7f7c878cb6ecf7b4a967b2af87da82bc6593081e))
- **video-clips:** tweak portrait parameters to have subtitles display without
overflowing
([2385b1a](https://code.castopod.org/adaures/castopod/commit/2385b1a2926d1344569836e18cb30adb4c604664))
- **xml-editor:** escape xml editor's content + restyle form sections to prevent
overflowing
([588590b](https://code.castopod.org/adaures/castopod/commit/588590bd2c0346e2465ff8f1930580d76a3bf068))
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
- add alternate rss feed link tag to podcast page head
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
- add analytics and unknown useragents
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add audio-clipper toolbar + add video-clip-previewer
([0255753](https://code.castopod.org/adaures/castopod/commit/02557539e6eb48fc23ee2ee3b0c75aee3310965b))
- add audio-clipper webcomponent (wip)
([21d4251](https://code.castopod.org/adaures/castopod/commit/21d4251b9bcd5acb0f8a1761bc4edc34a3dbc228))
- add basic stats on podcast about page
([1670558](https://code.castopod.org/adaures/castopod/commit/1670558473dba47219d470ff21d6224db6ab42ba))
- add breadcrumb in admin area
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
- add codemirror to display xml editor for custom rss field
([f15f262](https://code.castopod.org/adaures/castopod/commit/f15f26240cd5311fa9d07779f364b6639a501dec))
- add cumulative listening time charts
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
- add DropdownMenu component + remove global audio player in admin
([abb7fba](https://code.castopod.org/adaures/castopod/commit/abb7fbac276d77b7d31a0aeba75d464f3ba3ad46))
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- add french translation
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add heading component + update ecs rules to fix views
([23bdc6f](https://code.castopod.org/adaures/castopod/commit/23bdc6f8e36b7e8dfbe32755a54dea59ad913432))
- add housekeeping task to run after migrations
([89dee41](https://code.castopod.org/adaures/castopod/commit/89dee41d583e57251ea9315402a757f03571d7ad))
- add install wizard form to bootstrap database and create the first superadmin
user
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
- add ISO 3166 country codes
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add js audio player on podcast, admin and embeddable player pages + fix admon
episodes ux
([0e14eb4](https://code.castopod.org/adaures/castopod/commit/0e14eb4d3f526b0fd256a6144f3fbfc3fe52a357)),
closes [#131](https://code.castopod.org/adaures/castopod/issues/131)
- add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
- add map analytics, add episodes analytics, clean analytics page layout,
translate countries
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add media entity and link documents, images and audio files to it
([6ecf286](https://code.castopod.org/adaures/castopod/commit/6ecf2866cfcde31a0840f15c3340808ce14b44cf))
- add Noto Sans Mono font to use for durations + button to access new video clip
form in list
([7609bb6](https://code.castopod.org/adaures/castopod/commit/7609bb60330539aa91bfdafbb35c2d585624218a))
- add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
- add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
- add platform models
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add podcast banner field for each podcast + refactor images configuration
([4a8147b](https://code.castopod.org/adaures/castopod/commit/4a8147bfbbd98d9badfc57a0f2a18bdd5812e802))
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
- add replied to post or comment to reply element
([d0f9c60](https://code.castopod.org/adaures/castopod/commit/d0f9c6018f1af527099f3e26b5d824710fa11caf))
- add schema.org json-ld objects to podcasts, episodes, posts and comments pages
([902f959](https://code.castopod.org/adaures/castopod/commit/902f959b30a10839684f093eb86edebc5d826a0b))
- add task to housekeeping setting for resetting all instance counts
([9303e51](https://code.castopod.org/adaures/castopod/commit/9303e51bc50d730a8026f58984e83b840360ee88))
- add unique listeners analytics
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add user permissions and basic groups to handle authorizations
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18)
- **admin:** make header stick on scroll and show title + action buttons using
css only
([d60498c](https://code.castopod.org/adaures/castopod/commit/d60498c1beb970a14eeb3bbe02d1b1d8116624b0))
- **admin:** update admin layout for better ux + update brand pine colors
([d86142e](https://code.castopod.org/adaures/castopod/commit/d86142ebe7cd7582835f180b79fbeaaaba703528))
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
- **analytics:** add charts and data export
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
- build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
- **cache:** add podcast and episode pages to cache + clear them after insert or
update
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **clips:** setup clip entities and model + save video clip to have it
generated in the background
([2f6fdf9](https://code.castopod.org/adaures/castopod/commit/2f6fdf9091d52ca49709fc82621ba1c6dd0e817d))
- **comments:** add comments to episodes + update naming of status to post
([bb4752c](https://code.castopod.org/adaures/castopod/commit/bb4752c35e086664f5fd75fdc0d56546a1e356f6))
- **comments:** add like / undo like to comment + add comment page
([0c187ef](https://code.castopod.org/adaures/castopod/commit/0c187ef7a9278a60bcc6e5ee4d69d948b51e5c54))
- **components:** add custom view renderer with ComponentRenderer adapted from
bonfire2
([a95de8b](https://code.castopod.org/adaures/castopod/commit/a95de8bab010f6b01c598da72191abe97e473687))
- create optimized & resized images upon upload
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6)
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
- **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
- display legal disclaimer and warning on podcast import page
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
- edit + delete podcast and episode
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
- enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9)
- enhance ui using javascript in admin area
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
- **fonts:** replace Montserrat with Inter for better readablity
([bfa11d0](https://code.castopod.org/adaures/castopod/commit/bfa11d007d04b8ac714c8cf3b8050a6aaf177a26))
- import podcast from an rss feed url
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
- integrate stylized form components and update podcast edit page
([6536729](https://code.castopod.org/adaures/castopod/commit/653672954606a23796e8a7bda3c34fd6b92f84e0))
- make displayed publication time as relative time using @github/time-elements
([230e139](https://code.castopod.org/adaures/castopod/commit/230e139e43324b9ebef06ca8f6e13b3d9a7bdc70))
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
- **media:** clean media api + create an entity per media type
([fafaa7e](https://code.castopod.org/adaures/castopod/commit/fafaa7e689b17f09a2b056081fa1f4fc53bf716b))
- **media:** save audio, images, transcripts and chapters to media for episode
and persons
([58e2a00](https://code.castopod.org/adaures/castopod/commit/58e2a00a87fa7d5b188e13cc521d94f0cfddba50))
- **meta-tags:** add activitypub alternate links to podcast, episode, comment
and post pages
([bd61752](https://code.castopod.org/adaures/castopod/commit/bd61752be2f574323b05d1d0aee0df55adf9a74e))
- minor corrections to some tables
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
- **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about
the castopod instance
([88fddc8](https://code.castopod.org/adaures/castopod/commit/88fddc81d730978f2a4d8a671936b54041e3fe45))
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
- **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **podcast:** create a podcast using form
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- prefill season and episode numbers + set episode number as mandatory for
serial podcasts
([07d740b](https://code.castopod.org/adaures/castopod/commit/07d740b79f9283e389e723954f680f909ce5de4a)),
closes [#134](https://code.castopod.org/adaures/castopod/issues/134)
[#136](https://code.castopod.org/adaures/castopod/issues/136)
- **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13)
- **pwa:** add service-worker + webmanifest for each podcasts to have them
install on devices
([fee2c1c](https://code.castopod.org/adaures/castopod/commit/fee2c1c0d0d03c4ff0a6a207b0a5e0c22bb7b13a))
- redesign public podcast and episode pages + remove any information clutter for
better ux
([9321400](https://code.castopod.org/adaures/castopod/commit/932140077c671f0486a2cd08ceb6126c7ecde87f))
- replace form helper functions with components in admin template
([e64548b](https://code.castopod.org/adaures/castopod/commit/e64548b982ba47ff35f2272e2e30dd85eeba950b))
- replace slug field with interactive permalink component
([578022b](https://code.castopod.org/adaures/castopod/commit/578022b8c5163ffaf8db5870ed5ec9d5d9536477))
- restyle episode and person cards + add focus style to interactive elements for
a11y
([a505a1d](https://code.castopod.org/adaures/castopod/commit/a505a1de56e8e3056379bd60d0595f432e294728))
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75)
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80)
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
- **rss:** add podcast:location tag
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
- **rss:** add transcript and chapters support
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.castopod.org/adaures/castopod/issues/72)
[#82](https://code.castopod.org/adaures/castopod/issues/82)
- **rss:** generate rss feed from podcast entity
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
- **select:** enhance select input with choices.js
([910d457](https://code.castopod.org/adaures/castopod/commit/910d457cf843e0fc334b3505a4727d51633395ac))
- set app parameter forceGlobalSecureRequests = true forcing requests to go
through https
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- set podcast / episode description in the pages description meta tag
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
- **settings:** add general config for instance (site name, description and
icon)
([5c56f3e](https://code.castopod.org/adaures/castopod/commit/5c56f3e6f00a61af2ccf50811c155c325f2b10fa))
- **settings:** add theme settings to set an accent color for all public pages
([5c529a8](https://code.castopod.org/adaures/castopod/commit/5c529a83aa6d6147d94e5aee996e6b0ab02f0ce4))
- simplify podcast page's layout for better ux
([2c0efc6](https://code.castopod.org/adaures/castopod/commit/2c0efc6563604dd067be88cfc9ddd88a01745e64))
- **soundbites:** add soundbite list and creation forms with audio-clipper
component
([de19317](https://code.castopod.org/adaures/castopod/commit/de19317138a2106deb825c1eed7dda036ed7dac3))
- style file inputs using tailwind's file class
([8208ab6](https://code.castopod.org/adaures/castopod/commit/8208ab6785aae8c49f78eb9ac8cd53d77ec8e5e5))
- **themes:** add ViewThemes library to set views in root themes folder
([7a27676](https://code.castopod.org/adaures/castopod/commit/7a276764e6a1ee3619d9d3488f6163215db75338))
- **themes:** set different default banner per theme
([11c916f](https://code.castopod.org/adaures/castopod/commit/11c916fe433eb749ac32230c48e256057564cbb0))
- **themes:** set generic css variables for colors to enable instance themes
([a746a78](https://code.castopod.org/adaures/castopod/commit/a746a781b4bfc78209cf8302c6d7bb3cb452e446))
- toggle podcast sidebar on smaller screens
([f0205ec](https://code.castopod.org/adaures/castopod/commit/f0205ec274414e881cba40d6776126f05eaee583))
- **transcript:** parse srt subtitles into json file + add max file size info
below audio file input
([0098761](https://code.castopod.org/adaures/castopod/commit/00987610a068c8d6cdd4421ea16585fa037eb61a))
- **ui:** create ViewComponents library to enable building class and view files
components
([94872f2](https://code.castopod.org/adaures/castopod/commit/94872f2338e6025c2f3770be256160838dae9003))
- update analytics so to meet IABv2 requirements
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
- update pine colors + create charts components
([a50abc1](https://code.castopod.org/adaures/castopod/commit/a50abc138d4997b564e3065b37504cda5ce62da6))
- **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
- **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- **video-clip:** add video-clip page with video preview + logs
([42538dd](https://code.castopod.org/adaures/castopod/commit/42538dd7577be0ffe59b4fdfadbd76cc89e5ef30))
- **video-clip:** generate video clips in the bg using a cron job + add video
clip page + tidy up UI
([db0e427](https://code.castopod.org/adaures/castopod/commit/db0e4272bd6d307c562e1f961d2747cb62de0f35))
- **video-clips:** add dimensions for portrait and squared formats
([3af404d](https://code.castopod.org/adaures/castopod/commit/3af404da3dd1901c78cc7e1778fc225f6716207d))
- **video-clips:** add new themes + add castopod logo as a watermark
([1d1490b](https://code.castopod.org/adaures/castopod/commit/1d1490b06a1f5ecb10b3b98a72efc55d09c10944))
- **video-clips:** add route for scheduled video clips + list video clips with
status
([2065ebb](https://code.castopod.org/adaures/castopod/commit/2065ebbee5e3d0f890ac90b55ca984f1d62a184c))
- **video-clips:** allow episodeNumbering text to stand in the indent of
episodeTitle paragraph
([71a063d](https://code.castopod.org/adaures/castopod/commit/71a063dac311cb21639801fbae6af7c5106c2699))
- **video-clips:** generate a 16:9 video using ffmpeg
([35aa7ea](https://code.castopod.org/adaures/castopod/commit/35aa7ea5d9a339b3e6f745137282268d69fe2231))
- **video-clips:** generate subtitles clip using transcript json to have
subtitles accross video
([3ce07e4](https://code.castopod.org/adaures/castopod/commit/3ce07e455d171e29be30d8ad45055510eb8d363c))
- **video-clips:** replace hardcoded colors with config's theme colors
([e462abf](https://code.castopod.org/adaures/castopod/commit/e462abf6d660e41d2170c52caf45704008de58e9))
- **vite:** add vite config to decouple it from CI_ENVIRONMENT
([8721719](https://code.castopod.org/adaures/castopod/commit/8721719cd7cf32e94823541eafaba1e9309355a8))
- write id3v2 tags to episode's audio file
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
- defer javascript + lazy load images for faster page loads
([f0685e4](https://code.castopod.org/adaures/castopod/commit/f0685e44799dfb494592ff97841c0ae035381db8))
- **docker:** add redis caching service for development
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
- **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
- use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))
### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated
# [1.0.0-alpha.80](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.79...v1.0.0-alpha.80) (2021-12-29)
### Bug Fixes
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
# [1.0.0-alpha.79](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.78...v1.0.0-alpha.79) (2021-12-20)
### Bug Fixes
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
# [1.0.0-alpha.78](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.77...v1.0.0-alpha.78) (2021-12-15)
### Bug Fixes
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
# [1.0.0-alpha.77](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.76...v1.0.0-alpha.77) (2021-11-23)
### Bug Fixes
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- **podcast-import:** move guid attribute declaration for Episode entity to
include slug data
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328))
# [1.0.0-alpha.76](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.75...v1.0.0-alpha.76) (2021-10-26)
### Bug Fixes
- replace hardcoded style links with vite service + set default value for remote
transcript url
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150)
# [1.0.0-alpha.75](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.74...v1.0.0-alpha.75) (2021-10-05)
### Bug Fixes
- **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
# [1.0.0-alpha.74](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2021-09-28)
### Features
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
# [1.0.0-alpha.73](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.72...v1.0.0-alpha.73) (2021-09-22)
### Bug Fixes
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
# [1.0.0-alpha.72](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.71...v1.0.0-alpha.72) (2021-09-20)
### Bug Fixes
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
# [1.0.0-alpha.71](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.70...v1.0.0-alpha.71) (2021-09-17)
### Features
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
# [1.0.0-alpha.70](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.69...v1.0.0-alpha.70) (2021-08-31)
### Bug Fixes
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
# [1.0.0-alpha.69](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.68...v1.0.0-alpha.69) (2021-08-23)
### Bug Fixes
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
# [1.0.0-alpha.68](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.67...v1.0.0-alpha.68) (2021-08-19)
### Bug Fixes
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
# [1.0.0-alpha.67](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.66...v1.0.0-alpha.67) (2021-07-24)
### Features
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
# [1.0.0-alpha.66](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.65...v1.0.0-alpha.66) (2021-07-24)
### Features
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
# [1.0.0-alpha.65](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2021-07-22)
### Bug Fixes
- update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
# [1.0.0-alpha.64](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2021-07-12)
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
# [1.0.0-alpha.63](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2021-07-12)
### Features
- build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
# [1.0.0-alpha.62](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.61...v1.0.0-alpha.62) (2021-07-02)
### Bug Fixes
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
# [1.0.0-alpha.61](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2021-06-23)
### Bug Fixes
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
# [1.0.0-alpha.60](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2021-06-21)
### Features
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
# [1.0.0-alpha.59](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.58...v1.0.0-alpha.59) (2021-06-15)
### Bug Fixes
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
# [1.0.0-alpha.58](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.57...v1.0.0-alpha.58) (2021-06-11)
### Bug Fixes
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set location to null when getting empty string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
### Performance Improvements
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
# [1.0.0-alpha.57](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.56...v1.0.0-alpha.57) (2021-05-12)
### Bug Fixes
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
# [1.0.0-alpha.56](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.55...v1.0.0-alpha.56) (2021-05-12)
### Bug Fixes
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
# [1.0.0-alpha.55](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.54...v1.0.0-alpha.55) (2021-05-03)
### Features
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
# [1.0.0-alpha.54](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.53...v1.0.0-alpha.54) (2021-05-03)
### Features
- set app parameter forceGlobalSecureRequests = true forcing requests to go
through https
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **docker:** add redis caching service for development
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
# [1.0.0-alpha.53](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.52...v1.0.0-alpha.53) (2021-04-16)
### Bug Fixes
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
# [1.0.0-alpha.52](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.51...v1.0.0-alpha.52) (2021-04-16)
### Bug Fixes
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
# [1.0.0-alpha.51](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.50...v1.0.0-alpha.51) (2021-04-15)
### Bug Fixes
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
# [1.0.0-alpha.50](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.49...v1.0.0-alpha.50) (2021-04-14)
### Bug Fixes
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
# [1.0.0-alpha.49](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.48...v1.0.0-alpha.49) (2021-04-12)
### Bug Fixes
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
# [1.0.0-alpha.48](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.47...v1.0.0-alpha.48) (2021-04-10)
### Bug Fixes
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
# [1.0.0-alpha.47](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.46...v1.0.0-alpha.47) (2021-04-10)
### Bug Fixes
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
# [1.0.0-alpha.46](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.45...v1.0.0-alpha.46) (2021-04-09)
### Bug Fixes
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
# [1.0.0-alpha.45](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.44...v1.0.0-alpha.45) (2021-04-08)
### Bug Fixes
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
# [1.0.0-alpha.44](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.43...v1.0.0-alpha.44) (2021-04-08)
### Bug Fixes
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
# [1.0.0-alpha.43](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.42...v1.0.0-alpha.43) (2021-04-08)
### Bug Fixes
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
# [1.0.0-alpha.42](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.41...v1.0.0-alpha.42) (2021-04-02)
### Features
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
# [1.0.0-alpha.41](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.40...v1.0.0-alpha.41) (2021-03-30)
### Features
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
# [1.0.0-alpha.40](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.39...v1.0.0-alpha.40) (2021-03-19)
### Features
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
# [1.0.0-alpha.39](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.38...v1.0.0-alpha.39) (2021-03-01)
### Bug Fixes
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
# [1.0.0-alpha.38](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.37...v1.0.0-alpha.38) (2021-02-27)
### Features
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
# [1.0.0-alpha.37](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.36...v1.0.0-alpha.37) (2021-02-17)
### Bug Fixes
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
# [1.0.0-alpha.36](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.35...v1.0.0-alpha.36) (2021-02-16)
### Features
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
# [1.0.0-alpha.35](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.34...v1.0.0-alpha.35) (2021-02-12)
### Bug Fixes
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
# [1.0.0-alpha.34](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.33...v1.0.0-alpha.34) (2021-02-11)
### Bug Fixes
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
# [1.0.0-alpha.33](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.32...v1.0.0-alpha.33) (2021-02-10)
### Features
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
# [1.0.0-alpha.32](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.31...v1.0.0-alpha.32) (2021-02-10)
### Features
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
# [1.0.0-alpha.31](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.30...v1.0.0-alpha.31) (2020-12-23)
### Features
- **rss:** add podcast:location tag
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
# [1.0.0-alpha.30](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.29...v1.0.0-alpha.30) (2020-12-21)
### Features
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
# [1.0.0-alpha.29](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.28...v1.0.0-alpha.29) (2020-12-10)
### Bug Fixes
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
### Reverts
- **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
# [1.0.0-alpha.28](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.27...v1.0.0-alpha.28) (2020-12-07)
### Features
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
# [1.0.0-alpha.27](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.26...v1.0.0-alpha.27) (2020-12-07)
### Features
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
# [1.0.0-alpha.26](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.25...v1.0.0-alpha.26) (2020-11-30)
### Bug Fixes
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
# [1.0.0-alpha.25](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.24...v1.0.0-alpha.25) (2020-11-30)
### Features
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
# [1.0.0-alpha.24](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.23...v1.0.0-alpha.24) (2020-11-26)
### Features
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
# [1.0.0-alpha.23](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.22...v1.0.0-alpha.23) (2020-11-24)
### Bug Fixes
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
# [1.0.0-alpha.22](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.21...v1.0.0-alpha.22) (2020-11-24)
### Features
- **rss:** add transcript and chapters support
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.castopod.org/adaures/castopod/issues/72)
[#82](https://code.castopod.org/adaures/castopod/issues/82)
# [1.0.0-alpha.21](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.20...v1.0.0-alpha.21) (2020-11-24)
### Features
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
# [1.0.0-alpha.20](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.19...v1.0.0-alpha.20) (2020-11-24)
### Bug Fixes
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
### Features
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75)
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80)
# [1.0.0-alpha.19](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.18...v1.0.0-alpha.19) (2020-11-13)
### Bug Fixes
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
# [1.0.0-alpha.18](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.17...v1.0.0-alpha.18) (2020-11-09)
### Features
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
# [1.0.0-alpha.17](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.16...v1.0.0-alpha.17) (2020-11-05)
### Bug Fixes
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
# [1.0.0-alpha.16](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.15...v1.0.0-alpha.16) (2020-11-04)
### Features
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
# [1.0.0-alpha.15](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.14...v1.0.0-alpha.15) (2020-11-03)
### Features
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
# [1.0.0-alpha.14](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.13...v1.0.0-alpha.14) (2020-11-02)
### Features
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
# [1.0.0-alpha.13](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2020-10-29)
### Bug Fixes
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
### Features
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
# [1.0.0-alpha.12](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.11...v1.0.0-alpha.12) (2020-10-26)
### Bug Fixes
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
# [1.0.0-alpha.11](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.10...v1.0.0-alpha.11) (2020-10-26)
### Features
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
# [1.0.0-alpha.10](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2020-10-26)
### Bug Fixes
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
# [1.0.0-alpha.9](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2020-10-26)
### Features
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
# [1.0.0-alpha.8](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2020-10-22)
### Features
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
# [1.0.0-alpha.7](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2020-10-21)
### Features
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated
# [1.0.0-alpha.6](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2020-10-20)
### Bug Fixes
- **cache:** add locale for podcast and episode pages + clear some persisting
cache in models
([9cec8a8](https://code.podlibre.org/podlibre/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.podlibre.org/podlibre/castopod/issues/42)
[#61](https://code.podlibre.org/podlibre/castopod/issues/61)
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61)
# [1.0.0-alpha.5](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2020-10-20)
# [1.0.0-alpha.5](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2020-10-20)
### Features
- add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import
([72b3012](https://code.podlibre.org/podlibre/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
# [1.0.0-alpha.4](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2020-10-20)
# [1.0.0-alpha.4](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2020-10-20)
### Features
- **analytics:** add charts and data export
([78625c4](https://code.podlibre.org/podlibre/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
# [1.0.0-alpha.3](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2020-10-19)
# [1.0.0-alpha.3](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2020-10-19)
### Bug Fixes
- **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.podlibre.org/podlibre/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
# [1.0.0-alpha.2](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2020-10-19)
# [1.0.0-alpha.2](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2020-10-19)
### Features
- add cumulative listening time charts
([588b4d2](https://code.podlibre.org/podlibre/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
# 1.0.0-alpha.1 (2020-10-16)
### Bug Fixes
- add public/media folder to castopod bundle
([8053d35](https://code.podlibre.org/podlibre/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.podlibre.org/podlibre/castopod/issues/52)
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
- add where condition to get episode count without deleted episodes
([7661734](https://code.podlibre.org/podlibre/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.podlibre.org/podlibre/castopod/issues/67)
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
- comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.podlibre.org/podlibre/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- correct chart data
([4d3e9c8](https://code.podlibre.org/podlibre/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue
([e53f819](https://code.podlibre.org/podlibre/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues
([1ef55d7](https://code.podlibre.org/podlibre/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug
([ed69b2f](https://code.podlibre.org/podlibre/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision
([31b7828](https://code.podlibre.org/podlibre/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.podlibre.org/podlibre/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.podlibre.org/podlibre/castopod/issues/23)
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
- escape generated feed tag values and remove new lines from public pages meta
description
([6238a43](https://code.podlibre.org/podlibre/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.podlibre.org/podlibre/castopod/issues/57)
[#46](https://code.podlibre.org/podlibre/castopod/issues/46)
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46)
- fix layout bugs in admin and update translation files
([a834171](https://code.podlibre.org/podlibre/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.podlibre.org/podlibre/castopod/issues/40)
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
- minor corrections
([13be386](https://code.podlibre.org/podlibre/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper
([d311917](https://code.podlibre.org/podlibre/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- re-order graph values
([35f633b](https://code.podlibre.org/podlibre/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- remove required for other_categories field and add podcast_id to latest
podcasts query
([5417be0](https://code.podlibre.org/podlibre/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- rename issue_templates labels
([9f00305](https://code.podlibre.org/podlibre/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file
([e109df3](https://code.podlibre.org/podlibre/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.podlibre.org/podlibre/castopod/issues/60)
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
- reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.podlibre.org/podlibre/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.podlibre.org/podlibre/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
- set episode duration translation to hardcoded english
([c39efc9](https://code.podlibre.org/podlibre/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.podlibre.org/podlibre/castopod/issues/64)
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
- set episode guid upon episode creation
([ad8b153](https://code.podlibre.org/podlibre/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.podlibre.org/podlibre/castopod/issues/48)
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48)
- update purgecss content path for php helper files
([eb70bb4](https://code.podlibre.org/podlibre/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.podlibre.org/podlibre/castopod/issues/59)
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
- **install:** redirect to input baseUrl after instance config
([2426af7](https://code.podlibre.org/podlibre/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.podlibre.org/podlibre/castopod/issues/53)
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
- **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.podlibre.org/podlibre/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.podlibre.org/podlibre/castopod/issues/39)
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
- sort episodic podcasts by season
([d7b6794](https://code.podlibre.org/podlibre/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
- update .htaccess for shared hosting config
([2379826](https://code.podlibre.org/podlibre/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
- update iso-369 language table seeder
([0c90db4](https://code.podlibre.org/podlibre/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- **package.json:** update destination of postcss generation scripts
([21413f8](https://code.podlibre.org/podlibre/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- use slash instead of backslash to call layout
([a80adb2](https://code.podlibre.org/podlibre/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
### Features
- add alternate rss feed link tag to podcast page head
([a973c09](https://code.podlibre.org/podlibre/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.podlibre.org/podlibre/castopod/issues/35)
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
- add analytics and unknown useragents
([ec92e65](https://code.podlibre.org/podlibre/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add breadcrumb in admin area
([7fb1de2](https://code.podlibre.org/podlibre/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.podlibre.org/podlibre/castopod/issues/17)
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
- add french translation
([196920d](https://code.podlibre.org/podlibre/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add install wizard form to bootstrap database and create the first superadmin
user
([cba871c](https://code.podlibre.org/podlibre/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.podlibre.org/podlibre/castopod/issues/2)
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
- add ISO 3166 country codes
([97cd94b](https://code.podlibre.org/podlibre/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add map analytics, add episodes analytics, clean analytics page layout,
translate countries
([07eae83](https://code.podlibre.org/podlibre/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.podlibre.org/podlibre/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.)
([9c224a8](https://code.podlibre.org/podlibre/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.podlibre.org/podlibre/castopod/issues/24)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
- add platform models
([a333d29](https://code.podlibre.org/podlibre/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings
([043f49c](https://code.podlibre.org/podlibre/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables
([ce59344](https://code.podlibre.org/podlibre/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add unique listeners analytics
([3a49258](https://code.podlibre.org/podlibre/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add user permissions and basic groups to handle authorizations
([d58e518](https://code.podlibre.org/podlibre/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.podlibre.org/podlibre/castopod/issues/3)
[#18](https://code.podlibre.org/podlibre/castopod/issues/18)
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18)
- create optimized & resized images upon upload
([02e4441](https://code.podlibre.org/podlibre/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.podlibre.org/podlibre/castopod/issues/6)
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6)
- display legal disclaimer and warning on podcast import page
([2f07992](https://code.podlibre.org/podlibre/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.podlibre.org/podlibre/castopod/issues/34)
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
- edit + delete podcast and episode
([ac5f0c7](https://code.podlibre.org/podlibre/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.podlibre.org/podlibre/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.podlibre.org/podlibre/castopod/issues/31)
[#9](https://code.podlibre.org/podlibre/castopod/issues/9)
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9)
- enhance ui using javascript in admin area
([c0e66d5](https://code.podlibre.org/podlibre/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- import podcast from an rss feed url
([9a5d5a1](https://code.podlibre.org/podlibre/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.podlibre.org/podlibre/castopod/issues/21)
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
- set podcast / episode description in the pages description meta tag
([1c4a504](https://code.podlibre.org/podlibre/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.podlibre.org/podlibre/castopod/issues/44)
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
- update analytics so to meet IABv2 requirements
([03e23a2](https://code.podlibre.org/podlibre/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.podlibre.org/podlibre/castopod/issues/10)
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
- **cache:** add podcast and episode pages to cache + clear them after insert or
update
([da0f047](https://code.podlibre.org/podlibre/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds
([f73b042](https://code.podlibre.org/podlibre/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.podlibre.org/podlibre/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.podlibre.org/podlibre/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.podlibre.org/podlibre/castopod/issues/1)
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.podlibre.org/podlibre/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **podcast:** create a podcast using form
([1202ba3](https://code.podlibre.org/podlibre/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.podlibre.org/podlibre/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.podlibre.org/podlibre/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.podlibre.org/podlibre/castopod/issues/30)
[#13](https://code.podlibre.org/podlibre/castopod/issues/13)
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13)
- **rss:** generate rss feed from podcast entity
([c815ecd](https://code.podlibre.org/podlibre/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
- **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login
([c63a077](https://code.podlibre.org/podlibre/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.podlibre.org/podlibre/castopod/issues/11)
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
- minor corrections to some tables
([3bf9420](https://code.podlibre.org/podlibre/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- write id3v2 tags to episode's audio file
([4651d01](https://code.podlibre.org/podlibre/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Reverts
- use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.podlibre.org/podlibre/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))

View file

@ -1,128 +1,162 @@
# Contributor Covenant Code of Conduct
# Contributor Covenant 3.0 Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
We pledge to make our community welcoming, safe, and equitable for all.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
We are committed to fostering an environment that respects and promotes the
dignity, rights, and contributions of all individuals, regardless of
characteristics including race, ethnicity, caste, color, age, physical
characteristics, neurodiversity, disability, sex or gender, gender identity or
expression, sexual orientation, language, philosophy or religion, national or
social origin, socio-economic position, level of education, or other status. The
same privileges of participation are extended to everyone who participates in
good faith and in accordance with this Covenant.
## Our Standards
## Encouraged Behaviors
Examples of behavior that contributes to a positive environment for our
community include:
While acknowledging differences in social norms, we all strive to meet our
community's expectations for positive behavior. We also understand that our
words and actions may be interpreted differently than we intend based on
culture, background, or native language.
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
With these considerations in mind, we agree to behave mindfully toward each
other and act in ways that center our shared values, including:
Examples of unacceptable behavior include:
1. Respecting the **purpose of our community**, our activities, and our ways of
gathering.
2. Engaging **kindly and honestly** with others.
3. Respecting **different viewpoints** and experiences.
4. **Taking responsibility** for our actions and contributions.
5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of our
community**.
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Restricted Behaviors
## Enforcement Responsibilities
We agree to restrict the following behaviors in our community. Instances,
threats, and promotion of these behaviors are violations of this Code of
Conduct.
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
1. **Harassment.** Violating explicitly expressed boundaries or engaging in
unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative comments
directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyones personality or
behavior on the basis of immutable identities or traits.
4. **Sexualization.** Behaving in a way that would generally be considered
inappropriately intimate in the context or purpose of the community.
5. **Violating confidentiality**. Sharing or acting on someone's personal or
private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm
toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our community.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason, or
pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of content
you contribute.
3. **Promotional materials**. Sharing marketing or other commercial content in a
way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present content which
includes, links or describes any other restricted behaviors.
## Reporting an Issue
Tensions can occur between community members even when they are trying their
best to collaborate. Not every conflict represents a code of conduct violation,
and this Code of Conduct reinforces encouraged behaviors and norms that can help
avoid conflicts and minimize harm.
When an incident does occur, it is important to report it promptly. To report a
possible violation, email us at [abuse@castopod.org](mailto:abuse@castopod.org).
Community Moderators take reports of violations seriously and will make every
effort to respond in a timely manner. They will investigate all reports of code
of conduct violations, reviewing messages, logs, and recordings, or interviewing
witnesses and other participants. Community Moderators will keep investigation
and enforcement actions as transparent as possible while prioritizing safety and
confidentiality. In order to honor these values, enforcement actions are carried
out in private with the involved parties, but communicating to the whole
community may be part of a mutually agreed upon resolution.
## Addressing and Repairing Harm
If an investigation by the Community Moderators finds that this Code of Conduct
has been violated, the following enforcement ladder may be used to determine how
best to repair harm, based on the incident's impact on the individuals involved
and the community as a whole. Depending on the severity of a violation, lower
rungs on the ladder may be skipped.
1. Warning
1. Event: A violation involving a single incident or series of incidents.
2. Consequence: A private, written warning from the Community Moderators.
3. Repair: Examples of repair include a private written apology,
acknowledgement of responsibility, and seeking clarification on
expectations.
2. Temporarily Limited Activities
1. Event: A repeated incidence of a violation that previously resulted in a
warning, or the first incidence of a more serious violation.
2. Consequence: A private, written warning with a time-limited cooldown
period designed to underscore the seriousness of the situation and give
the community members involved time to process the incident. The cooldown
period may be limited to particular communication channels or interactions
with particular community members.
3. Repair: Examples of repair may include making an apology, using the
cooldown period to reflect on actions and impact, and being thoughtful
about re-entering community spaces after the period is over.
3. Temporary Suspension
1. Event: A pattern of repeated violation which the Community Moderators have
tried to address with warnings, or a single serious violation.
2. Consequence: A private written warning with conditions for return from
suspension. In general, temporary suspensions give the person being
suspended time to reflect upon their behavior and possible corrective
actions.
3. Repair: Examples of repair include respecting the spirit of the
suspension, meeting the specified conditions for return, and being
thoughtful about how to reintegrate with the community when the suspension
is lifted.
4. Permanent Ban
1. Event: A pattern of repeated code of conduct violations that other steps
on the ladder have failed to resolve, or a violation so serious that the
Community Moderators determine there is no way to keep the community safe
with this person as a member.
2. Consequence: Access to all community spaces, tools, and communication
channels is removed. In general, permanent bans should be rarely used,
should have strong reasoning behind them, and should only be resorted to
if working through other remedies has failed to change the behavior.
3. Repair: There is no possible repair in cases of this severity.
This enforcement ladder is intended as a guideline. It does not limit the
ability of Community Managers to use their discretion and judgment, in keeping
with the best interests of our community.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
an individual is officially representing the community in public or other
spaces. Examples of representing our community include using an official email
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[abuse@podlibre.org](mailto:abuse@podlibre.org). All complaints will be reviewed
and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
This Code of Conduct is adapted from the Contributor Covenant, version 3.0,
permanently available at
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
Contributor Covenant is stewarded by the Organization for Ethical Source and
licensed under CC BY-SA 4.0. To view a copy of this license, visit
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
For answers to common questions about Contributor Covenant, see the FAQ at
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
Translations are provided at
[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
Additional enforcement and community guideline resources can be found at
[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources).
The enforcement ladder was inspired by the work of
[Mozillas code of conduct team](https://github.com/mozilla/inclusion).

437
CONTRIBUTING-DEV.md Normal file
View file

@ -0,0 +1,437 @@
# Setup your development environment
## Introduction
Castopod is a web app based on the `php` framework
[CodeIgniter 4](https://codeigniter.com).
We use [Docker](https://www.docker.com/) to quickly setup a dev environment. A
`docker-compose.yml` and `Dockerfile` are included in the project's root folder
to help you kickstart your contribution.
> You don't need any prior knowledge of Docker to follow the next steps.
> However, if you wish to use your own environment, feel free to do so!
## Setup instructions
### 1. Pre-requisites
0. Install [Docker](https://docs.docker.com/get-docker).
1. Clone the Castopod repository by running:
```bash
git clone https://code.castopod.org/adaures/castopod.git
```
2. Create a `.env` file with the minimum required config to connect the app to
the database and use redis as a cache handler:
```ini
CI_ENVIRONMENT="development"
# If set to development, you must run `pnpm run dev` to start the static assets server
vite.environment="development"
# By default, this is set to true in the app config.
# For development, this must be set to false as it is
# on a local environment
app.forceGlobalSecureRequests=false
app.baseURL="http://localhost:8080/"
admin.gateway="cp-admin"
auth.gateway="cp-auth"
database.default.hostname="mariadb"
database.default.database="castopod"
database.default.username="castopod"
database.default.password="castopod"
database.default.DBPrefix="dev_"
analytics.salt="DEV_ANALYTICS_SALT"
cache.handler="redis"
cache.redis.host="redis"
# You may not want to use redis as your cache handler
# Comment/remove the two lines above and uncomment
# the next line for file caching.
# -----------------------
#cache.handler="file"
######################################
# Media config
######################################
media.baseURL="http://localhost:8080/"
# S3
# Uncomment to store s3 objects using adobe/s3mock service
# -----------------------
#media.fileManager="s3"
#media.s3.bucket="castopod"
#media.s3.endpoint="http://172.31.0.6:9090/"
#media.s3.pathStyleEndpoint=true
```
> [!NOTE]
> You can tweak your environment by setting more environment variables in
> your custom `.env` file. See the `env` for examples or the
> [CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html)
> for more info.
3. (for Docker desktop) Add the repository you've cloned to Docker desktop's
`Settings` > `Resources` > `File Sharing`
### 2. (recommended) Develop inside the app container with VSCode
If you're working in VSCode, you can take advantage of the `.devcontainer/`
folder. It defines a development environment (dev container) with preinstalled
requirements and VSCode extensions so you don't have to worry about them. All
required services will be loaded automagically! 🪄
1. Install the VSCode extension
[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. `Ctrl/Cmd + Shift + P` > `Open in container`
> The VSCode window will reload inside the dev container. Expect several
> minutes during first load as it is building all necessary services.
**Note**: The dev container will start by running Castopod's PHP server.
During development, you will have to start [Vite](https://vitejs.dev)'s dev
server for compiling the typescript code and styles:
```bash
# run Vite dev server
pnpm run dev
```
If there is any issue with the PHP server not running, you can restart them
using the following commands:
```bash
# run Castopod server
php spark serve - 0.0.0.0
```
3. You're all set! 🎉
You're now **inside the dev container**, you may use the VSCode console
(`Terminal` > `New Terminal`) to run any command:
```bash
# PHP is installed
php -v
# Composer is installed
composer -V
# pnpm is installed
pnpm -v
# git is installed
git version
```
For more info, see
[Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers)
### 3. Start hacking
You're all set! Start working your magic by updating the project's files! Help
yourself to the
[CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html) for
more insights.
To see your changes, go to:
- `http://localhost:8080/` for the Castopod website
- `http://localhost:8080/cp-admin` for the Castopod admin:
- email: **admin@castopod.local**
- password: **castopod**
- `http://localhost:8888/` for the phpmyadmin interface:
- username: **castopod**
- password: **castopod**
### 2-alt. Develop outside the app container
You do not wish to use the VSCode devcontainer? No problem!
1. Start the Docker containers manually:
Go to the project's root folder and run:
```bash
# starts all services declared in docker-compose.yml file
# -d option starts the containers in the background
docker-compose up -d
# See all running processes (you should see 3 processes running)
docker-compose ps
# Alternatively, you can check all docker processes
docker ps -a
```
> The `docker-compose up -d` command will boot 5 containers in the
> background:
>
> - `castopod_app`: a php based container with Castopod requirements
> installed
> - `castopod_redis`: a [redis](https://redis.io/) database to handle queries
> and pages caching
> - `castopod_mariadb`: a [mariadb](https://mariadb.org/) server for
> persistent data
> - `castopod_phpmyadmin`: a phpmyadmin server to visualize the mariadb
> database.
> - `castopod_s3`: a mock s3 server to work on the s3 fileManager
2. Run any command inside the containers by prefixing them with
`docker-compose run --rm app`:
```bash
# use PHP
docker-compose run --rm app php -v
# use Composer
docker-compose run --rm app composer -V
# use pnpm
docker-compose run --rm app pnpm -v
# use git
docker-compose run --rm app git version
```
---
## Going Further
### Install Castopod's dependencies
1. Install php dependencies with [Composer](https://getcomposer.org/)
```bash
composer install
```
> [!NOTE]
> The php dependencies aren't included in the repository. Composer will check
> the `composer.json` and `composer.lock` files to download the packages with
> the right versions. The dependencies will live under the `vendor/` folder.
> For more info, check out the
> [Composer documentation](https://getcomposer.org/doc/).
2. Install JavaScript dependencies with [pnpm](https://pnpm.io/)
```bash
pnpm install
```
> [!NOTE]
> The JavaScript dependencies aren't included in the repository. Pnpm will
> check the `package.json` and `pnpm-lock.yaml` files to download the
> packages with the right versions. The dependencies will live under the
> `node_module` folder. For more info, check out the
> [PNPM documentation](https://pnpm.io/motivation).
3. Generate static assets:
```bash
# build all static assets at once
pnpm run build:static
# build specific assets
pnpm run build:icons
pnpm run build:svg
```
> [!NOTE]
> The static assets generated live under the `public/assets` folder, it
> includes JavaScript, styles, images, fonts, icons and svg files.
### Initialize and populate database
> [!TIP]
> You may skip this section if you go through the install wizard (go to
> `/cp-install`).
1. Build the database with the migrate command:
```bash
# loads the database schema during first migration
php spark migrate -all
```
You may need to undo the migration (rollback):
```bash
# rolls back database schema (deletes all tables and their content)
php spark migrate:rollback
```
2. Populate the database with the required data:
```bash
# Populates all required data
php spark db:seed DevSeeder
```
You may choose to add data separately:
```bash
# Populates all categories
php spark db:seed CategorySeeder
# Populates all Languages
php spark db:seed LanguageSeeder
# Adds a superadmin with [admin@castopod.local / castopod] credentials
php spark db:seed DevSuperadminSeeder
```
3. (optional) Populate the database with test data:
- Populate with fake podcast analytics:
```bash
php spark db:seed FakePodcastsAnalyticsSeeder
```
- Populate with fake website analytics:
```bash
php spark db:seed FakeWebsiteAnalyticsSeeder
```
### Useful docker / docker-compose commands
- Monitor the app container:
```bash
docker-compose logs --tail 50 --follow --timestamps app
```
- Interact with the Redis server using included redis-cli command:
```bash
docker exec -it castopod_redis redis-cli
```
- Monitor the Redis container:
```bash
docker-compose logs --tail 50 --follow --timestamps redis
```
- Monitor the mariadb container:
```bash
docker-compose logs --tail 50 --follow --timestamps mariadb
```
- Monitor the phpmyadmin container:
```bash
docker-compose logs --tail 50 --follow --timestamps phpmyadmin
```
- Restart docker containers:
```bash
docker-compose restart
```
- Destroy all containers, opposite of `up` command:
```bash
docker-compose down
```
- Rebuild app container:
```bash
docker-compose build app
```
Check [Docker](https://docs.docker.com/engine/reference/commandline/docker/) and
[docker-compose](https://docs.docker.com/compose/reference/) documentations for
more insights.
### Updating Documentation
Castopod's documentation is written in Markdown and uses the Astro Starlight
framework. To update Castopod's documentation, including the Getting Started
guide and User Guide:
1. Change directories to the `docs` directory and install the dependencies:
```bash
cd docs/
pnpm i
```
2. Start the documentation development server:
```bash
pnpm run dev --host
```
3. The documentation development server runs on port 4321. In your browser visit
`http://localhost:4321/docs`. If the page displays a 404 Not Found error,
click on the Castopod logo in the upper left hand corner of the page and the
documentation should load.
4. Edit the Markdown files with your documentation updates. The Astro Starlight
development server will automatically update each time you save a change.
## Known issues
### Allocation failed - JavaScript heap out of memory
This happens when running `pnpm install`.
👉 By default, docker might not have access to enough RAM. Allocate more memory
and run `pnpm install` again.
### (Linux) Files created inside container are attributed to root locally
You may use Linux user namespaces to fix this on your machine:
> [!NOTE]
> Replace "username" with your local username
1. Go to `/etc/docker/daemon.json` and add:
```json
{
"userns-remap": "username"
}
```
2. Configure the subordinate uid/guid:
```bash
# in /etc/subuid
username:1000:1
username:100000:65536
```
```bash
# in /etc/subgid
username:1000:1
username:100000:65536
```
3. Restart Docker:
```bash
sudo systemctl restart docker
```
4. That's it! Now, the root user in the container will be mapped to the user on
your local machine, no more permission issues! 🎉
You can check
[this great article](https://www.jujens.eu/posts/en/2017/Jul/02/docker-userns-remap/)
to know more about how it works.

View file

@ -1,8 +1,16 @@
# Contributing to Castopod Host
# Contributing to Castopod
Love Castopod Host and want to help? Thanks so much, there's something to do for
Love Castopod and want to help? Thanks so much, there's something to do for
everybody!
> [!NOTE]
> Castopod follows the [all contributors](https://allcontributors.org/)
> specification in an effort to **recognize any kind of contribution**, not just
> code!
> If you've made a contribution and do not appear in the
> [contributors](../index.md#contributors-✨) list, please
> [let us know](../index.md#contact) so we can correct our mistake! 🙂
Please take a moment to review this document in order to make the contribution
process easy and effective for everyone involved.
@ -11,17 +19,34 @@ developers managing and developing this open source project. In return, they
should reciprocate that respect in addressing your issue or assessing patches
and features.
⚠️ Note that **any** contribution made on a repository other than
[the original repository](https://code.podlibre.org/podlibre/castopod-host) will
not be accepted.
## Translating Castopod
We use [Crowdin](https://translate.castopod.org/) to manage translation files
for [Castopod](https://code.castopod.org/), the
[documentation](https://docs.castopod.org/) and the
[landing](https://castopod.org/) websites.
Whether you'd like to correct a translation error, validate new translations or
include your language to Castopod, head into the
[crowdin project](https://translate.castopod.org/) to get started.
> [!NOTE]
> To prevent degrading user experience, new languages are included to Castopod
> when they reach a certain threshold (~90%).
## Using the issue tracker
The [issue tracker](https://code.podlibre.org/podlibre/castopod-host/-/issues)
is the preferred channel for [bug reports](#bug-reports),
The [issue tracker](https://code.castopod.org/adaures/castopod/-/issues) is the
preferred channel for [bug reports](#bug-reports),
[features requests](#feature-requests) and
[submitting pull requests](#pull-requests).
## ⚠️ Security issues and vulnerabilities
If you encounter any security issue or vulnerability in the Castopod source,
please contact us directly by email at
[security@castopod.org](mailto:security@castopod.org)
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository.
@ -45,8 +70,9 @@ your environment? What steps will reproduce the issue? What browser(s) and OS
experience the problem? What would you expect to be the outcome? All these
details will help people to fix any potential bugs.
> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates)
> have been created for this project. You may use them to help you follow those
> [!NOTE]
> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates) have
> been created for this project. You may use them to help you follow those
> guidelines.
## Feature requests
@ -72,33 +98,33 @@ accurate comments, etc.) and any other requirements (such as test coverage).
Adhering to the following process is the best way to get your work included in
the project:
1. [Fork](https://docs.gitlab.com/ee/gitlab-basics/fork-project.html) the
project, clone your fork, and configure the remotes:
1. [Fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html)
the project, clone your fork, and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://code.podlibre.org/<your-username>/castopod-host.git
```bash
# Clone your fork of the repo into the current directory
git clone https://code.castopod.org/<your-username>/castopod.git
# Navigate to the newly cloned directory
cd castopod-host
# Navigate to the newly cloned directory
cd castopod
# Assign the original repo to a remote called "upstream"
git remote add upstream https://code.podlibre.org/podlibre/castopod-host.git
```
# Assign the original repo to a remote called "upstream"
git remote add upstream https://code.castopod.org/adaures/castopod.git
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout main
git pull upstream main
```
```bash
git checkout main
git pull upstream main
```
3. Create a new topic branch (off the `main` branch) to contain your feature,
change, or fix:
```bash
git checkout -b <topic-branch-name>
```
```bash
git checkout -b <topic-branch-name>
```
4. Commit your changes in logical chunks. Please adhere to these
[git commit message guidelines](https://conventionalcommits.org/) or your
@ -108,22 +134,23 @@ git checkout -b <topic-branch-name>
5. Locally merge (or rebase) the upstream dev branch into your topic branch:
```bash
git pull [--rebase] upstream main
```
```bash
git pull [--rebase] upstream main
```
6. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
```bash
git push origin <topic-branch-name>
```
7. [Open a Pull Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html#new-merge-request-from-a-fork)
with a clear title and description.
**IMPORTANT**: By submitting a patch, you agree to allow the project owners to
license your work under the terms of the
[GNU AGPLv3](https://code.podlibre.org/podlibre/castopod-host/-/blob/main/LICENSE).
> [!IMPORTANT]
> By submitting a patch, you agree to allow the project owners to license your
> work under the terms of the
> [GNU AGPLv3](https://code.castopod.org/adaures/castopod/-/blob/develop/LICENSE.md).
## Collaborating guidelines

View file

@ -1,66 +1,26 @@
# Castopod Host dependencies
# Castopod dependencies
Castopod Host uses the following components:
Castopod uses the following components:
PHP Dependencies:
## PHP Dependencies
- [CodeIgniter 4](https://codeigniter.com)
([MIT License](https://codeigniter.com/user_guide/license.html))
- [WhichBrowser/Parser-PHP](https://github.com/WhichBrowser/Parser-PHP)
([MIT License](https://github.com/WhichBrowser/Parser-PHP/blob/master/LICENSE))
- [GeoIP2 PHP API](https://github.com/maxmind/GeoIP2-php)
([Apache License 2.0](https://github.com/maxmind/GeoIP2-php/blob/master/LICENSE))
- [getID3](https://github.com/JamesHeinrich/getID3)
([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt))
- [myth-auth](https://github.com/lonnieezell/myth-auth)
([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md))
- [commonmark](https://commonmark.thephpleague.com/)
([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE))
- [phpdotenv](https://github.com/vlucas/phpdotenv)
([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE))
- [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown)
([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE))
- [opawg/user-agents-php](https://github.com/opawg/user-agents-php)
([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE))
- [podlibre/ipcat](https://github.com/podlibre/ipcat)
([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE))
- [podlibre/podcast-namespace](https://code.podlibre.org/podlibre/podcastnamespace)
([MIT License](https://code.podlibre.org/podlibre/podcastnamespace/-/blob/master/LICENSE))
- [phpseclib](https://phpseclib.com/)
([MIT License](https://github.com/phpseclib/phpseclib/blob/master/LICENSE))
- [codeigniter4-uuid](https://github.com/michalsn/codeigniter4-uuid)
([MIT License](https://github.com/michalsn/codeigniter4-uuid/blob/develop/LICENSE))
- [essence](https://github.com/essence/essence)
([The FreeBSD License](https://github.com/essence/essence/blob/master/LICENSE.txt))
PHP dependencies can be found in the [composer.json](./composer.json) file.
Javascript dependencies:
## Javascript dependencies
- [rollup](https://rollupjs.org/)
([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
- [tailwindcss](https://tailwindcss.com/)
([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
- [ProseMirror](https://prosemirror.net/)
([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE))
- [amCharts 4](https://github.com/amcharts/amcharts4)
([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE))
- [Choices.js](https://joshuajohnson.co.uk/Choices/)
([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
- [flatpickr](https://flatpickr.js.org/)
([MIT License](https://github.com/flatpickr/flatpickr/blob/master/LICENSE.md))
- [popperjs](https://popper.js.org/)
([MIT License](https://github.com/popperjs/popper-core/blob/master/LICENSE.md))
Javascript dependencies can be found in the [package.json](./package.json) file.
Other:
## Other dependencies
- [Kumbh Sans](https://fonts.google.com/specimen/Kumbh+Sans)
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
- [Montserrat](https://fonts.google.com/specimen/Montserrat)
- [Inter](https://fonts.google.com/specimen/Inter)
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
- [RemixIcon](https://remixicon.com/)
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
- [OPAWG/User agent list](https://github.com/opawg/user-agents)
- [OPAWG/User agent list](https://github.com/opawg/user-agents-v2)
([by Open Podcast Analytics Working Group](https://github.com/opawg))
([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
([MIT license](https://github.com/opawg/user-agents-v2/blob/master/LICENSE))
- [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents)
([by Open Podcast Analytics Working Group](https://github.com/opawg))
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))

View file

@ -1,62 +0,0 @@
####################################################
# Castopod Host development Docker file
####################################################
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
FROM php:8.0-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
COPY . /castopod-host
WORKDIR /castopod-host
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install server requirements
RUN apt-get update \
# gnupg to sign commits with gpg
&& apt-get install --yes --no-install-recommends gnupg \
# npm through the nodejs package
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get update \
&& apt-get install --yes --no-install-recommends nodejs \
# update npm
&& npm install --global npm@7 \
&& apt-get update \
&& apt-get install --yes --no-install-recommends \
git \
openssh-client \
vim \
# cron for scheduled tasks
cron \
# unzip used by composer
unzip \
# required libraries to install php extensions using
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
libicu-dev \
libpng-dev \
libjpeg-dev \
zlib1g-dev \
libzip-dev \
# intl for Internationalization
&& docker-php-ext-install intl \
&& docker-php-ext-install zip \
# gd for image processing
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install gd \
# redis extension for cache
&& pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& docker-php-ext-enable redis \
# mysqli for database access
&& docker-php-ext-install mysqli \
&& docker-php-ext-enable mysqli \
# configure php
&& echo "file_uploads = On\n" \
"memory_limit = 512M\n" \
"upload_max_filesize = 500M\n" \
"post_max_size = 512M\n" \
"max_execution_time = 300\n" \
> /usr/local/etc/php/conf.d/uploads.ini \

83
GDPR.txt Normal file
View file

@ -0,0 +1,83 @@
# This file lists processing purposes and the personal data gathered by
# Castopod.
# It is intended for hosting providers who want to provide a service
# based on Castopod, helping them to comply with GDPR requirements. Note
# that the services powered by Castopod may collect more data, HTTP logs
# in particular. As a hosting provider, you must inform your users of their
# rights and how their data are used and protected.
purpose:
Deduplicate number of audio file downloads made by the same listener
for analytics purposes
lawfulness: legitimate interest
data: (User IP address + Browser User Agent)
required: yes
visibility: none
description:
In order to produce analytics data comparable to the podcasting
ecosystem standards, the User IP address (REMOTE_ADDR) with the
browser User Agent (HTTP_USER_AGENT) are stored when an audio file
is downloaded.
mitigation:
The data (User IP address + Browser User Agent) is never stored in plain
format.
The data is concatenated with a cryptographic salt, the current date,
and the podcast or episode IDs.
The data is hashed (using sha1) after being concatenated and before
being stored.
The data is stored in a cache database (eg. Redis).
The data expires every day at midnight (server time).
purpose: Connect users to their accounts
lawfulness: legitimate interest
data: username
required: yes
visibility: authenticated users
description:
The username is used to identify users during the login process.
The username is only required for users accessing the admin area.
mitigation:
The username does not have to be a real or known identity.
data: user e-mail address
required: yes
visibility: administrators
description:
The e-mail address is used for administrative purposes, to identify users
during the login process and in case of forgotten password.
data: password
required: yes
visibility: private
description:
The password is used to check the identity of users during the login
process.
mitigation:
Only hashes (using the Argon2 key derivation function) of the passwords
are stored in the database (but they transit over the network).
purpose: Claim ownership of a podcast
lawfulness: legitimate interest
data: Podcast e-mail address
required: yes
visibility: public
description:
The podcast e-mail address is used to claim podcast ownership on other
platforms (such as Apple Podcasts).
mitigation:
The e-mail can be generic.
purpose: Grant access to premium content
lawfulness: legitimate interest
data: Subscriber's email address
required: yes
visibility: administrators
description:
The subscriber's e-mail address is used to provide credentials for
listening to premium content.
mitigation:
The e-mail can be generic.

View file

@ -1,123 +0,0 @@
# How to install Castopod Host <!-- omit in toc -->
_Castopod Host_ was thought-out to be easy to install. Whether using dedicated
or shared hosting, you can install it on most PHP-MySQL compatible web servers.
## Table of contents <!-- omit in toc -->
- [Install instructions](#install-instructions)
- [0. Pre-requisites](#0-pre-requisites)
- [(recommended) Install Wizard](#recommended-install-wizard)
- [(alternative) Manual configuration](#alternative-manual-configuration)
- [Web Server Requirements](#web-server-requirements)
- [PHP v8.0 or higher](#php-v80-or-higher)
- [MySQL compatible database](#mysql-compatible-database)
- [Privileges](#privileges)
- [(Optional) Other recommendations](#optional-other-recommendations)
- [Security concerns](#security-concerns)
## Install instructions
### 0. Pre-requisites
0. Get a Web Server with requirements installed
1. Create a MySQL database for Castopod Host with a user having access and
modification privileges (for more info, see
[Web Server Requirements](#web-server-requirements)).
2. Activate HTTPS on your domain with an _SSL certificate_.
3. Download and unzip the latest
[Castopod Host Package](https://code.podlibre.org/podlibre/castopod-host/-/releases)
onto the web server if you havent already.
- ⚠️ Set the web server document root to the `public/` sub-folder.
4. Add a cron task on your web server to run every minute (replace the paths
accordingly):
```php
* * * * * /path/to/php /path/to/castopod-host/public/index.php scheduled-activities
```
> ⚠️ Social features will not work properly if you do not set the task. It is
> used to broadcast social activities to the fediverse.
### (recommended) Install Wizard
1. Run the Castopod Host install script by going to the install wizard page
(`https://your_domain_name.com/cp-install`) in your favorite web browser.
2. Follow the instructions on your screen.
3. Start podcasting!
> **Note:**
>
> The install script writes a `.env` file in the package root. If you cannot go
> through the install wizard, you can
> [create and update the `.env` file manually](#alternative-manual-configuration).
### (alternative) Manual configuration
1. Rename the `.env.example` file to `.env` and update the default values with
your own.
2. Upload the `.env` file to the Castopod Host Package root on your server.
3. Go to `/cp-install` to finish the install process.
4. Start podcasting!
## Web Server Requirements
### PHP v8.0 or higher
PHP version 8.0 or higher is required, with the following extensions installed:
- [intl](https://php.net/manual/en/intl.requirements.php)
- [libcurl](https://php.net/manual/en/curl.requirements.php)
- [mbstring](https://php.net/manual/en/mbstring.installation.php)
- [gd](https://www.php.net/manual/en/image.installation.php)
Additionally, make sure that the following extensions are enabled in your PHP:
- json (enabled by default - don't turn it off)
- xml (enabled by default - don't turn it off)
- [mysqlnd](https://php.net/manual/en/mysqlnd.install.php)
### MySQL compatible database
> We recommend using [MariaDB](https://mariadb.org).
You will need the server hostname, database name, username and password to
complete the installation process. If you do not have these, please contact your
server administrator.
> NB. Castopod Host only works with supported MySQL compatible databases. It
> will break with MySQL v5.6 for example as its end of life was on February
> 5, 2021.
#### Privileges
User must have at least these privileges on the database for Castopod Host to
work: `ALTER`, `DELETE`, `EXECUTE`, `INDEX`, `INSERT`, `SELECT`, `UPDATE`.
### (Optional) Other recommendations
- Redis for better cache performances.
- CDN for static files caching and better performances.
- e-mail gateway for lost passwords.
## Security concerns
Castopod Host is built on top of Codeigniter, a PHP framework that encourages
[good security practices](https://codeigniter.com/user_guide/concepts/security.html).
To maximize your instance safety and prevent any malicious attack, we recommend
you update all your Castopod Host files permissions after installation (to avoid
any permission error):
- `writable/` folder must be **readable** and **writable**.
- `public/media/` folder must be **readable** and **writable**.
- any other file must be set to **readonly**.
For instance, if you are using Apache or NGINX with Ubuntu you may do the
following:
```bash
sudo chown -R root:root /path/to/castopod-host
sudo chown -R www-data:www-data /path/to/castopod-host/writable
sudo chown -R www-data:www-data /path/to/castopod-host/public/media
```

208
README.md
View file

@ -1,75 +1,191 @@
<h1 style="text-align: center">
<img src="https://podlibre.org/static/images/Castopod-Title.svg" alt="Castopod Host" />
</h1>
> ⚠️ **Castopod Host is in alpha version**. It is still under heavy development
> and may not be 100% stable as new features are being worked on.
_Castopod Host_ is a free and open-source podcast hosting solution made for
podcasters who want engage and interact with their audience.
Create, upload, publish, interact with your followers and get comprehensive
audience measurements that respect your listeners privacy.
Whether you choose to install it on your own server or have it hosted by a
professional, all your data and analytics belong to you and you only!
<div style="text-align: center">
<img src="https://podlibre.org/static/images/Castopod-Mascot-Server.svg" alt="Castopod Mascot" />
<div align="center">
<h1>
<a href="https://castopod.org/">
<img src="./docs/src/assets/castopod-logo-inline.svg" alt="Castopod" height="64px" />
</a>
</h1>
</div>
You may find Castopod Host's source code on the
[original repository](https://code.podlibre.org/podlibre/castopod-host) or,
alternatively, on the
[github repository (mirror)](https://github.com/podlibre/castopod-host).
<div align="center">
## Install / Update
[![release-badge]][release]&nbsp;[![license-badge]][license]&nbsp;[![crowdin-badge]][crowdin]&nbsp;[![contributions-badge]][contributions]&nbsp;[![semantic-release-badge]][semantic-release]&nbsp;[![discord-badge]][discord]&nbsp;[![stars-badge]][stars]
To install or update Castopod Host on your PHP/MySQL server:
</div>
- Download
[Castopod Host's latest Package (zip or tar.gz)](https://code.podlibre.org/podlibre/castopod-host/-/releases):
Castopod is a free and open-source podcast hosting solution made for podcasters
who want engage and interact with their audience.
- Follow one of the procedures on:
## Getting started
- [“How to **install** Castopod Host”](./INSTALL.md)
- or [“How to **update** Castopod Host”](./UPDATE.md)
Castopod comes pre-packaged with all the required static assets and
dependencies, you may download and install it by checking out the
[getting started page](https://castopod.org/getting-started/)!
## Documentation
## Security issues and vulnerabilities
You can check Castopod Host's documentation for
[setting up a development environment](./docs/setup-development.md).
If you encounter any security issue or vulnerability in the Castopod source,
please contact us directly by email at
[security@castopod.org](mailto:security@castopod.org)
## Contributing
Love Castopod Host and would like to help? Check out the
[contribution guidelines](./CONTRIBUTING.md) for this project, everything should
be there!
Contributions are always welcome!
⚠️ Note that **any** contribution made on a repository other than
[the original repository](https://code.podlibre.org/podlibre/castopod-host) will
not be accepted.
See the [contribution guidelines](./CONTRIBUTING.md) for ways to get started.
## Support
> [!Important]
> **Any** contribution made on a repository other than
> [the original repository](https://code.castopod.org/adaures/castopod) will not
> be accepted.
## Contributors ✨
Thanks goes to these wonderful people
([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://yassinedoghri.com"><img src="https://avatars.githubusercontent.com/u/11021441?v=4?s=100" width="100px;" alt="Yassine Doghri"/><br /><sub><b>Yassine Doghri</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=yassinedoghri" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=yassinedoghri" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-yassinedoghri" title="Maintenance">🚧</a> <a href="#content-yassinedoghri" title="Content">🖋</a> <a href="#design-yassinedoghri" title="Design">🎨</a> <a href="#a11y-yassinedoghri" title="Accessibility">️️️️♿️</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-yassinedoghri" title="Answering Questions">💬</a> <a href="#mentoring-yassinedoghri" title="Mentoring">🧑‍🏫</a> <a href="#infra-yassinedoghri" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-yassinedoghri" title="Ideas, Planning, & Feedback">🤔</a> <a href="#projectManagement-yassinedoghri" title="Project Management">📆</a> <a href="https://blog.castopod.org/author/yassinedoghri/" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/benjamin"><img src="https://code.castopod.org/uploads/-/system/user/avatar/2/avatar.png?s=100" width="100px;" alt="Benjamin Bellamy"/><br /><sub><b>Benjamin Bellamy</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=benjamin" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=benjamin" title="Reviewed Pull Requests">👀</a> <a href="#content-benjamin" title="Content">🖋</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-benjamin" title="Answering Questions">💬</a> <a href="#infra-benjamin" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-benjamin" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://blog.castopod.org/author/benjamin-bellamy/" title="Blogposts">📝</a> <a href="#projectManagement-benjamin" title="Project Management">📆</a> <a href="#talk-benjamin" title="Talks">📢</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ola-hn"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ola Hneini"/><br /><sub><b>Ola Hneini</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=ola" title="Reviewed Pull Requests">👀</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#maintenance-ola" title="Maintenance">🚧</a> <a href="#question-ola" title="Answering Questions">💬</a> <a href="#ideas-ola" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mamot.fr/@rdelaage"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Romain de Laage"/><br /><sub><b>Romain de Laage</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="#infra-rdelaage" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-rdelaage" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/lyonelbernard"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Lyonel Bernard"/><br /><sub><b>Lyonel Bernard</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Lyonel" title="Bug reports">🐛</a> <a href="#question-Lyonel" title="Answering Questions">💬</a> <a href="#audio-Lyonel" title="Audio">🔊</a> <a href="#ideas-Lyonel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.crypticchameleon.com/"><img src="https://secure.gravatar.com/avatar/7c2a721b52d0763673a600e8f01bd745?s=80&d=identicon?s=100" width="100px;" alt="Christopher Lagonick-Weitzel"/><br /><sub><b>Christopher Lagonick-Weitzel</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ctlw83" title="Bug reports">🐛</a> <a href="#question-ctlw83" title="Answering Questions">💬</a> <a href="#audio-ctlw83" title="Audio">🔊</a> <a href="#ideas-ctlw83" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ernestoacosta.me/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ernesto Acosta"/><br /><sub><b>Ernesto Acosta</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ernestoacostame" title="Bug reports">🐛</a> <a href="#audio-ernestoacostame" title="Audio">🔊</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-ernestoacostame" title="Answering Questions">💬</a> <a href="#ideas-ernestoacostame" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fedi.bzh/@ewen"><img src="https://mastodon.fedi.bzh/system/accounts/avatars/000/000/002/original/6f387690a504ae46.jpg?s=100" width="100px;" alt="Ewen"/><br /><sub><b>Ewen</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-3wen" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Behel"><img src="https://secure.gravatar.com/avatar/ad63ee8ef8e3db8253d21e5012d2724f?s=80&d=identicon?s=100" width="100px;" alt="Bastien Luneteau"/><br /><sub><b>Bastien Luneteau</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=Behel" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.cecillie.fr/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Cécile Ricordeau"/><br /><sub><b>Cécile Ricordeau</b></sub></a><br /><a href="#design-cecillie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/PatrykMis"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Patryk Miś"/><br /><sub><b>Patryk Miś</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/mspanc"><img src="https://secure.gravatar.com/avatar/eed8337939641eac5ad0b570bd6acf96?s=80&d=identicon?s=100" width="100px;" alt="Marcin Lewandowski"/><br /><sub><b>Marcin Lewandowski</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=mspanc" title="Bug reports">🐛</a> <a href="#ideas-mspanc" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/SJanik"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sebastian Janik"/><br /><sub><b>Sebastian Janik</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/patryk"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Patryk Karczmarczyk"/><br /><sub><b>Patryk Karczmarczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/ddenis"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="denis d"/><br /><sub><b>denis d</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ddenis" title="Bug reports">🐛</a> <a href="#ideas-ddenis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/douglaskastle"><img src="https://secure.gravatar.com/avatar/b7e652ba4b6bcd440afa069e7f7bc9e6?s=80&d=identicon?s=100" width="100px;" alt="Douglas Kastle"/><br /><sub><b>Douglas Kastle</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=douglaskastle" title="Bug reports">🐛</a> <a href="#ideas-douglaskastle" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/cExplorer"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="cExplorer"/><br /><sub><b>cExplorer</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=cExplorer" title="Bug reports">🐛</a> <a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/imacrea"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="ImaCrea"/><br /><sub><b>ImaCrea</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=imacrea" title="Bug reports">🐛</a> <a href="#ideas-imacrea" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/jonas"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Jonas S"/><br /><sub><b>Jonas S</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/yannL"><img src="https://secure.gravatar.com/avatar/9c46600ce566ec6d526370d8e104b1c8?s=80&d=identicon?s=100" width="100px;" alt="LEFEBVRE Yann"/><br /><sub><b>LEFEBVRE Yann</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=yannL" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/spaetz"><img src="https://secure.gravatar.com/avatar/278e1af65e82993efd0ba7bbbacf6435?s=80&d=identicon?s=100" width="100px;" alt="Sebastian Späth"/><br /><sub><b>Sebastian Späth</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=spaetz" title="Bug reports">🐛</a> <a href="#ideas-spaetz" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/rocky"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="rocky III"/><br /><sub><b>rocky III</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=rocky" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Regenpfeifer"><img src="https://code.castopod.org/uploads/-/system/user/avatar/103/avatar.png?s=100" width="100px;" alt="Hermann Josef Eckl"/><br /><sub><b>Hermann Josef Eckl</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Regenpfeifer" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/cyrilledel"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Delhaye Cyrille"/><br /><sub><b>Delhaye Cyrille</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=cyrilledel" title="Bug reports">🐛</a> <a href="#ideas-cyrilledel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/otetranome"><img src="https://code.castopod.org/uploads/-/system/user/avatar/113/avatar.png?s=100" width="100px;" alt="João Leandro"/><br /><sub><b>João Leandro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-otetranome" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://achouvardas.eu/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Angelos Chouvardas"/><br /><sub><b>Angelos Chouvardas</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fjerland.no/@eivind"><img src="https://mastodon.fjerland.no/system/accounts/avatars/107/769/768/295/192/222/original/e5c985fea6487dcb.jpg?s=100" width="100px;" alt="Eivind"/><br /><sub><b>Eivind</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/forght"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15073833/large/82d1e2e443a6df7edc43a7405dfeeb75_default.png?s=100" width="100px;" alt="forght"/><br /><sub><b>forght</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/glottis0q"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15209934/large/8b17ef6a7399f0b82a8198f87c224195.png?s=100" width="100px;" alt="glottis0q"/><br /><sub><b>glottis0q</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mstdn.fr/@ButterflyOfFire"><img src="https://static.mstdn.fr/static/accounts/avatars/000/065/901/original/5908e93ad5447f15.png?s=100" width="100px;" alt="ButterflyOfFire"/><br /><sub><b>ButterflyOfFire</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lil5"><img src="https://avatars.githubusercontent.com/u/17646836?v=4?s=100" width="100px;" alt="Lucian I. Last"/><br /><sub><b>Lucian I. Last</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/luuzviir"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13166188/large/d03ab0abc7ce354b210d836955cd3805_default.png?s=100" width="100px;" alt="LuuzViir"/><br /><sub><b>LuuzViir</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/cthtc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15211502/large/ed0651060cb8474a9519b5168bd377c1_default.png?s=100" width="100px;" alt="CTHTC"/><br /><sub><b>CTHTC</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/retrograde"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15021651/large/b10c4057f85bf4de49c7fdf01354ecde.jpeg?s=100" width="100px;" alt="Russian Retro"/><br /><sub><b>Russian Retro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/mareklach"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13572324/large/3eeba8d569c247ace33862bf4ef4748f.jpeg?s=100" width="100px;" alt="Marek L'ach"/><br /><sub><b>Marek L'ach</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gunchleoc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13043878/large/3223f7b606296a8b1c92c5de39c459a2_default.png?s=100" width="100px;" alt="GunChleoc"/><br /><sub><b>GunChleoc</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gabisnow"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15214858/large/5b083bdf9c9e9de67cc6ee72a6c8db18_default.png?s=100" width="100px;" alt="GabiSnow"/><br /><sub><b>GabiSnow</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/bendaha"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15331656/large/cd92450d2c20202299fb3a0075903e20_default.png?s=100" width="100px;" alt="bendaha"/><br /><sub><b>bendaha</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/samuelroland"><img src="https://crowdin-static.downloads.crowdin.com/avatar/14980053/large/3e154a37d03d6e98ae402ed3f930f4f5.png?s=100" width="100px;" alt="Samuel Roland"/><br /><sub><b>Samuel Roland</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dimitriregnier.net/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Dimitri Regnier"/><br /><sub><b>Dimitri Regnier</b></sub></a><br /><a href="#ideas-dimregnier" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://im.irithys.com/@thy"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15405614/large/3086461c47cce0a0c031925e5f943412.png?s=100" width="100px;" alt="irithys"/><br /><sub><b>irithys</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/caos30"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sergi"/><br /><sub><b>Sergi</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/basen1982"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Andreas Olsson"/><br /><sub><b>Andreas Olsson</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/leonfrom"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="leonfrom"/><br /><sub><b>leonfrom</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/agentcobra57"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="agentcobra"/><br /><sub><b>agentcobra</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/alephoto85"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15094649/large/530391f54157af52ae33058ec15b0f99.jpg?s=100" width="100px;" alt="Alessandro"/><br /><sub><b>Alessandro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/liimee"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="liimee"/><br /><sub><b>liimee</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahmedsabouni"><img src="https://avatars.githubusercontent.com/u/74497842?v=4?s=100" width="100px;" alt="Ahmed Sabouni"/><br /><sub><b>Ahmed Sabouni</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KrzysztofDomanczyk"><img src="https://avatars.githubusercontent.com/u/75178474?v=4?s=100" width="100px;" alt="KrzysztofDomanczyk"/><br /><sub><b>KrzysztofDomanczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Dwev"><img src="https://avatars.githubusercontent.com/u/46626050?v=4?s=100" width="100px;" alt="Guy Martin"/><br /><sub><b>Guy Martin</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Dwev" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prcutler"><img src="https://avatars.githubusercontent.com/u/67276?v=4?s=100" width="100px;" alt="Paul Cutler"/><br /><sub><b>Paul Cutler</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#question-prcutler" title="Answering Questions">💬</a> <a href="#ideas-prcutler" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nateritter"><img src="https://avatars.githubusercontent.com/u/198798?v=4?s=100" width="100px;" alt="Nate Ritter"/><br /><sub><b>Nate Ritter</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the
[all-contributors](https://github.com/all-contributors/all-contributors)
specification. Contributions of any kind welcome!
## Contact
You may reach us for help or ask any question you have on:
- [Discord](https://castopod.org/discord) (for direct interaction with
developers and the community)
- [Issue tracker](https://code.castopod.org/adaures/castopod/-/issues) (for
feature requests & bug reports)
Alternatively, you can follow us on social media platforms to get news about
Castopod:
- [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance)
- [Twitter](https://twitter.com/castopod)
- [Bluesky](https://bsky.app/profile/castopod.org)
- [LinkedIn](https://linkedin.com/company/castopod)
- [Facebook](https://www.facebook.com/castopod)
## Sponsors
[Castopod](https://nlnet.nl/project/Castopod/) was funded through the
[NGI0 Discovery](https://nlnet.nl/discovery/) Fund under grant agreement
Nº 825322.
The ongoing development of Castopod is made possible with the support of its
backers. If you'd like to help, please consider
[sponsoring Castopod's development](https://opencollective.com/castopod/contribute).
The fund was established by NLnet with financial support from the European
Commission's [Next Generation Internet](https://www.ngi.eu/) programme, under
the aegis of DG Communications Networks, Content and Technology.
<table>
<tbody>
<tr>
<td align="center">
<a href="https://docs.castopod.org/images/sponsors/adaures.svg" target="_blank" rel="noopener noreferrer"><img height="48" src="./docs/src/assets/images/sponsors/adaures.svg" alt="Ad Aures" /></a>
</td>
<td align="center">
<a href="https://nlnet.nl/project/Castopod/" target="_blank" rel="noopener noreferrer"><img src="./docs/src/assets/images/sponsors/nlnet.svg" alt="NLnet Logo" height="48" /></a>
</td>
</tr>
</tbody>
</table>
## License
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
Copyright © 2020-present, [Ad Aures](https://adaures.com/).
[release]: https://code.castopod.org/adaures/castopod/-/releases
[release-badge]:
https://img.shields.io/gitlab/v/release/2?color=brightgreen&gitlab_url=https%3A%2F%2Fcode.castopod.org%2F&include_prereleases&label=release
[license]: https://code.castopod.org/adaures/castopod/-/blob/beta/LICENSE.md
[license-badge]:
https://img.shields.io/github/license/ad-aures/castopod?color=blue
[contributions]: https://code.castopod.org/adaures/castopod/-/issues
[contributions-badge]:
https://img.shields.io/badge/contributions-welcome-brightgreen.svg
[semantic-release]: https://github.com/semantic-release/semantic-release
[semantic-release-badge]:
https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[discord]: https://castopod.org/discord
[discord-badge]: https://img.shields.io/badge/chat-on%20discord-7389D8
[stars]: https://github.com/ad-aures/castopod/stargazers
[stars-badge]:
https://img.shields.io/github/stars/ad-aures/castopod?style=social
[crowdin]: https://translate.castopod.org/project/castopod
[crowdin-badge]: https://badges.crowdin.net/castopod/localized.svg

View file

@ -1,95 +0,0 @@
# How to update Castopod Host <!-- omit in toc -->
After installing _Castopod Host_, you may want to update your instance to the
latest version in order to enjoy the latest features ✨, bug fixes 🐛 and
performance improvements ⚡.
## Table of contents <!-- omit in toc -->
- [Manual update instructions](#manual-update-instructions)
- [Automatic update instructions](#automatic-update-instructions)
- [Frequently asked questions (FAQ)](#frequently-asked-questions-faq)
- [Where can I find my _Castopod Host_ version?](#where-can-i-find-my-castopod-host-version)
- [I haven't updated my instance in a long time… What should I do?](#i-havent-updated-my-instance-in-a-long-time-what-should-i-do)
- [Should I make a backup before updating?](#should-i-make-a-backup-before-updating)
## Manual update instructions
1. Go to the
[releases page](https://code.podlibre.org/podlibre/castopod-host/-/releases)
and see if your instance is up to date with the latest _Castopod Host_
version
- cf.
[Where can I find my _Castopod Host_ version?](#where-can-i-find-my-castopod-host-version)
2. Download the latest release package named `Castopod Host Package`, you may
choose between the `zip` or `tar.gz` archives
- ⚠️ Make sure you download the Castopod Host Package and **NOT** the Source
Code
3. On your server:
- Remove all files except `.env` and `public/media`
- Copy the new files from the downloaded package into your server
- Note: you may need to reset files permissions as during the install
process. Check
[Security Concerns section in INSTALL.md](./INSTALL.md#security-concerns).
4. Alpha releases may come with additional update instructions (see
[releases page](https://code.podlibre.org/podlibre/castopod-host/-/releases)).
They are usually database migration scripts in `.sql` format to update your
database schema.
- 👉 Make sure you run the scripts on your phpmyadmin panel or using command
line to update the database along with the package files!
- cf.
[I haven't updated my instance in a long time… What should I do?](#i-havent-updated-my-instance-in-a-long-time-what-should-i-do)
5. If you are using redis, clear your cache.
6. ✨ Enjoy your fresh instance, you're all done!
## Automatic update instructions
> Coming soon... 👀
## Frequently asked questions (FAQ)
### Where can I find my _Castopod Host_ version?
Go to your _Castopod Host_ admin panel, the version is displayed on the bottom
right corner.
Alternatively, you can find the version in the `app > Config > Constants.php`
file.
### I haven't updated my instance in a long time… What should I do?
No problem! Just get the latest release as described above. Only, when going
through the release instructions (4), perform them sequentially, from the oldest
to the newest.
> You may want to backup your instance depending on how long you haven't updated
> _Castopod Host_.
For example, if you're on `v1.0.0-alpha.42` and would like to upgrade to
`v1.0.0-alpha.58`:
0. (recommended) Make a backup of your files and database.
1. Download the latest release, overwrite your files whilst keeping `.env` and
`public/media`.
2. Go through each release update instructions sequentially (from oldest to
newest) starting with `v1.0.0-alpha.43`, `v1.0.0-alpha.44`,
`v1.0.0-alpha.45`, …, `v1.0.0-alpha.58`.
3. ✨ Enjoy your fresh instance, you're all done!
### Should I make a backup before updating?
We advise you do, so you don't lose everything if anything goes wrong!
More generally, we advise you make regular backups of your Castopod Host files
and database to prevent you from losing it all…

View file

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

View file

@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\FlatAuthorization as MythAuthFlatAuthorization;
class FlatAuthorization extends MythAuthFlatAuthorization
{
/**
* The group model to use. Usually the class noted below (or an extension thereof) but can be any compatible
* CodeIgniter Model.
*
* @var PermissionModel
*/
protected $permissionModel;
/**
* Checks a group to see if they have the specified permission.
*/
public function groupHasPermission(int | string $permission, int $groupId): bool
{
// Get the Permission ID
$permissionId = $this->getPermissionID($permission);
if (! is_numeric($permissionId)) {
return false;
}
return $this->permissionModel->doesGroupHavePermission($groupId, $permissionId);
}
/**
* Makes user part of given groups.
*
* @param array<string, string> $groups Either collection of ID or names
*/
public function setUserGroups(int $userId, array $groups = []): bool
{
// remove user from all groups before resetting it in new groups
$this->groupModel->removeUserFromAllGroups($userId);
if ($groups === []) {
return true;
}
foreach ($groups as $group) {
$this->addUserToGroup($userId, $group);
}
return true;
}
}

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\GroupModel as MythAuthGroupModel;
class GroupModel extends MythAuthGroupModel
{
/**
* @return mixed[]
*/
public function getContributorRoles(): array
{
return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after')
->findAll();
}
/**
* @return mixed[]
*/
public function getUserRoles(): array
{
return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after')
->findAll();
}
}

View file

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\PermissionModel as MythAuthPermissionModel;
class PermissionModel extends MythAuthPermissionModel
{
/**
* Checks to see if a user, or one of their groups, has a specific permission.
*/
public function doesGroupHavePermission(int $groupId, int $permissionId): bool
{
// Check group permissions and take advantage of caching
$groupPerms = $this->getPermissionsForGroup($groupId);
return count($groupPerms) &&
array_key_exists($permissionId, $groupPerms);
}
/**
* Gets all permissions for a group in a way that can be easily used to check against:
*
* [ id => name, id => name ]
*
* @return array<int, string>
*/
public function getPermissionsForGroup(int $groupId): array
{
$cacheName = "group{$groupId}_permissions";
if (! ($found = cache($cacheName))) {
$groupPermissions = $this->db
->table('auth_groups_permissions')
->select('id, auth_permissions.name')
->join('auth_permissions', 'auth_permissions.id = permission_id', 'inner')
->where('group_id', $groupId)
->get()
->getResultObject();
$found = [];
foreach ($groupPermissions as $row) {
$found[$row->id] = strtolower($row->name);
}
cache()
->save($cacheName, $found, 300);
}
return $found;
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Commands;
use App\Models\EpisodeModel;
use CodeIgniter\CLI\BaseCommand;
class EpisodesComputeDownloads extends BaseCommand
{
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Episodes';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'episodes:compute-downloads';
/**
* The Command's Description
*
* @var string
*/
protected $description = "Calculates all episodes downloads and stores results in episodes' downloads_count field.";
/**
* Actually execute a command.
*/
public function run(array $params): void
{
$episodesModel = new EpisodeModel();
$query = $episodesModel->builder()
->select('episodes.id as id, IFNULL(SUM(ape.hits),0) as downloads_count')
->join('analytics_podcasts_by_episode ape', 'episodes.id=ape.episode_id', 'left')
->groupBy('episodes.id');
$episodeModel2 = new EpisodeModel();
$episodeModel2->builder()
->setQueryAsData($query)
->onConstraint('id')
->updateBatch();
}
}

View file

@ -2,13 +2,49 @@
declare(strict_types=1);
use ViewThemes\Theme;
/**
* The goal of this file is to allow developers a location where they can overwrite core procedural functions and
* replace them with their own. This file is loaded during the bootstrap process and is called during the frameworks
* replace them with their own. This file is loaded during the bootstrap process and is called during the framework's
* execution.
*
* This can be looked at as a `master helper` file that is loaded early on, and may also contain additional functions
* that you'd like to use throughout your entire application
*
* @link: https://codeigniter4.github.io/CodeIgniter4/
* @see: https://codeigniter.com/user_guide/extending/common.html
*/
if (! function_exists('view')) {
/**
* Grabs the current RendererInterface-compatible class and tells it to render the specified view. Simply provides a
* convenience method that can be used in Controllers, libraries, and routed closures.
*
* NOTE: Does not provide any escaping of the data, so that must all be handled manually by the developer.
*
* @param array<string, mixed> $data
* @param array<string, mixed> $options Unused - reserved for third-party extensions.
*/
function view(string $name, array $data = [], array $options = []): string
{
if (array_key_exists('theme', $options)) {
Theme::setTheme($options['theme']);
}
$path = Theme::path();
/** @var CodeIgniter\View\View $renderer */
$renderer = single_service('renderer', $path);
$saveData = config('View')
->saveData;
if (array_key_exists('saveData', $options)) {
$saveData = (bool) $options['saveData'];
unset($options['saveData']);
}
return $renderer->setData($data, 'raw')
->render($name, $options, $saveData);
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use ActivityPub\Config\ActivityPub as ActivityPubBase;
use App\Libraries\NoteObject;
class ActivityPub extends ActivityPubBase
{
/**
* --------------------------------------------------------------------
* ActivityPub Objects
* --------------------------------------------------------------------
*/
public string $noteObject = NoteObject::class;
/**
* --------------------------------------------------------------------
* Default avatar and cover images
* --------------------------------------------------------------------
*/
public string $defaultAvatarImagePath = 'assets/images/castopod-avatar-default.jpg';
public string $defaultAvatarImageMimetype = 'image/jpeg';
public string $defaultCoverImagePath = 'assets/images/castopod-cover-default.jpg';
public string $defaultCoverImageMimetype = 'image/jpeg';
}

View file

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use Analytics\Config\Analytics as AnalyticsBase;
class Analytics extends AnalyticsBase
{
/**
* --------------------------------------------------------------------
* Route filters options
* --------------------------------------------------------------------
*/
public array $routeFilters = [
'analytics-full-data' => 'permission:podcasts-view,podcast-view',
'analytics-data' => 'permission:podcasts-view,podcast-view',
'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
];
public function __construct()
{
parent::__construct();
// set the analytics gateway behind the admin gateway.
// Only logged in users should be able to view analytics
$this->gateway = config('App')
->adminGateway . '/analytics';
}
/**
* get the full audio file url
*
* @param string|string[] $audioFilePath
*/
public function getAudioFileUrl($audioFilePath): string
{
helper('media');
return media_base_url($audioFilePath);
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Handlers\FileHandler;
use Override;
class App extends BaseConfig
{
@ -14,38 +14,34 @@ class App extends BaseConfig
* Base Site URL
* --------------------------------------------------------------------------
*
* URL to your CodeIgniter root. Typically this will be your base URL,
* URL to your CodeIgniter root. Typically, this will be your base URL,
* WITH a trailing slash:
*
* http://example.com/
*
* If this is not set then CodeIgniter will try guess the protocol, domain
* and path to your installation. However, you should always configure this
* explicitly and never rely on auto-guessing, especially in production
* environments.
* E.g., http://example.com/
*/
public string $baseURL = 'http://localhost:8080/';
/**
* --------------------------------------------------------------------------
* Media Base URL
* --------------------------------------------------------------------------
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
* If you want to accept multiple Hostnames, set this.
*
* URL to your media root. Typically this will be your base URL,
* WITH a trailing slash:
* E.g.,
* When your site URL ($baseURL) is 'http://example.com/', and your site
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
*
* http://cdn.example.com/
* @var list<string>
*/
public string $mediaBaseURL = 'http://localhost:8080/';
public array $allowedHostnames = [];
/**
* --------------------------------------------------------------------------
* Index File
* --------------------------------------------------------------------------
*
* Typically this will be your index.php file, unless you've renamed it to
* something else. If you are using mod_rewrite to remove the page set this
* variable so that it is blank.
* Typically, this will be your `index.php` file, unless you've renamed it to
* something else. If you have configured your web server to remove this file
* from your site URIs, set this variable to an empty string.
*/
public string $indexPage = '';
@ -54,18 +50,42 @@ class App extends BaseConfig
* URI PROTOCOL
* --------------------------------------------------------------------------
*
* This item determines which getServer global should be used to retrieve the
* URI string. The default setting of 'REQUEST_URI' works for most servers.
* This item determines which server global should be used to retrieve the
* URI string. The default setting of 'REQUEST_URI' works for most servers.
* If your links do not seem to work, try one of the other delicious flavors:
*
* 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
* 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
* 'PATH_INFO' Uses $_SERVER['PATH_INFO']
* 'REQUEST_URI': Uses $_SERVER['REQUEST_URI']
* 'QUERY_STRING': Uses $_SERVER['QUERY_STRING']
* 'PATH_INFO': Uses $_SERVER['PATH_INFO']
*
* WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
*/
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
@ -99,9 +119,23 @@ class App extends BaseConfig
* by the application in descending order of priority. If no match is
* found, the first locale will be used.
*
* @var string[]
* IncomingRequest::setLocale() also uses this list.
*
* @var list<string>
*/
public array $supportedLocales = ['en', 'fr'];
public array $supportedLocales = [
'en',
'fr',
'pl',
'de',
'pt-br',
'nn-no',
'es',
'zh-hans',
'ca',
'br',
'sr-latn',
];
/**
* --------------------------------------------------------------------------
@ -110,6 +144,9 @@ class App extends BaseConfig
*
* The default timezone that will be used in your application to display
* dates with the date helper, and can be retrieved through app_timezone()
*
* @see https://www.php.net/manual/en/timezones.php for list of timezones
* supported by PHP.
*/
public string $appTimezone = 'UTC';
@ -133,170 +170,10 @@ class App extends BaseConfig
* If true, this will force every request made to this application to be
* made via a secure connection (HTTPS). If the incoming request is not
* secure, the user will be redirected to a secure version of the page
* and the HTTP Strict Transport Security header will be set.
* and the HTTP Strict Transport Security (HSTS) header will be set.
*/
public bool $forceGlobalSecureRequests = true;
/**
* --------------------------------------------------------------------------
* Session Driver
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*/
public string $sessionDriver = FileHandler::class;
/**
* --------------------------------------------------------------------------
* Session Cookie Name
* --------------------------------------------------------------------------
*
* The session cookie name, must contain only [0-9a-z_-] characters
*/
public string $sessionCookieName = 'ci_session';
/**
* --------------------------------------------------------------------------
* Session Expiration
* --------------------------------------------------------------------------
*
* The number of SECONDS you want the session to last.
* Setting to 0 (zero) means expire when the browser is closed.
*/
public int $sessionExpiration = 7200;
/**
* --------------------------------------------------------------------------
* Session Save Path
* --------------------------------------------------------------------------
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*/
public string $sessionSavePath = WRITEPATH . 'session';
/**
* --------------------------------------------------------------------------
* Session Match IP
* --------------------------------------------------------------------------
*
* Whether to match the user's IP address when reading the session data.
*
* WARNING: If you're using the database driver, don't forget to update
* your session table's PRIMARY KEY when changing this setting.
*/
public bool $sessionMatchIP = false;
/**
* --------------------------------------------------------------------------
* Session Time to Update
* --------------------------------------------------------------------------
*
* How many seconds between CI regenerating the session ID.
*/
public int $sessionTimeToUpdate = 300;
/**
* --------------------------------------------------------------------------
* Session Regenerate Destroy
* --------------------------------------------------------------------------
*
* Whether to destroy session data associated with the old session ID
* when auto-regenerating the session ID. When set to FALSE, the data
* will be later deleted by the garbage collector.
*/
public bool $sessionRegenerateDestroy = false;
/**
* --------------------------------------------------------------------------
* Cookie Prefix
* --------------------------------------------------------------------------
*
* Set a cookie name prefix if you need to avoid collisions.
*
* @deprecated use Config\Cookie::$prefix property instead.
*/
public string $cookiePrefix = '';
/**
* --------------------------------------------------------------------------
* Cookie Domain
* --------------------------------------------------------------------------
*
* Set to `.your-domain.com` for site-wide cookies.
*
* @deprecated use Config\Cookie::$domain property instead.
*/
public string $cookieDomain = '';
/**
* --------------------------------------------------------------------------
* Cookie Path
* --------------------------------------------------------------------------
*
* Typically will be a forward slash.
*
* @deprecated use Config\Cookie::$path property instead.
*/
public string $cookiePath = '/';
/**
* --------------------------------------------------------------------------
* Cookie Secure
* --------------------------------------------------------------------------
*
* Cookie will only be set if a secure HTTPS connection exists.
*
* @deprecated use Config\Cookie::$secure property instead.
*/
public bool $cookieSecure = false;
/**
* --------------------------------------------------------------------------
* Cookie HttpOnly
* --------------------------------------------------------------------------
*
* Cookie will only be accessible via HTTP(S) (no JavaScript).
*
* @deprecated use Config\Cookie::$httponly property instead.
*/
public bool $cookieHTTPOnly = true;
/**
* --------------------------------------------------------------------------
* Cookie SameSite
* --------------------------------------------------------------------------
*
* Configure cookie SameSite setting. Allowed values are:
* - None
* - Lax
* - Strict
* - ''
*
* Alternatively, you can use the constant names:
* - `Cookie::SAMESITE_NONE`
* - `Cookie::SAMESITE_LAX`
* - `Cookie::SAMESITE_STRICT`
*
* Defaults to `Lax` for compatibility with modern browsers. Setting `''`
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$cookieSecure` must also be set.
*
* @deprecated use Config\Cookie::$samesite property instead.
*/
public string $cookieSameSite = 'Lax';
/**
* --------------------------------------------------------------------------
* Reverse Proxy IPs
@ -304,103 +181,21 @@ class App extends BaseConfig
*
* If your server is behind a reverse proxy, you must whitelist the proxy
* IP addresses from which CodeIgniter should trust headers such as
* HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
* X-Forwarded-For or Client-IP in order to properly identify
* the visitor's IP address.
*
* You can use both an array or a comma-separated list of proxy addresses,
* as well as specifying whole subnets. Here are a few examples:
* You need to set a proxy IP address or IP address with subnets and
* the HTTP header for the client IP address.
*
* Comma-separated: '10.0.1.200,192.168.5.0/24'
* Array: ['10.0.1.200', '192.168.5.0/24']
* Here are some examples:
* [
* '10.0.1.200' => 'X-Forwarded-For',
* '192.168.5.0/24' => 'X-Real-IP',
* ]
*
* @var string|string[]
* @var array<string, string>|string
*/
public string | array $proxyIPs = '';
/**
* --------------------------------------------------------------------------
* CSRF Token Name
* --------------------------------------------------------------------------
*
* The token name.
*
* @deprecated Use `Config\Security` $tokenName property instead of using this property.
*/
public string $CSRFTokenName = 'csrf_test_name';
/**
* --------------------------------------------------------------------------
* CSRF Header Name
* --------------------------------------------------------------------------
*
* The header name.
*
* @deprecated Use `Config\Security` $headerName property instead of using this property.
*/
public string $CSRFHeaderName = 'X-CSRF-TOKEN';
/**
* --------------------------------------------------------------------------
* CSRF Cookie Name
* --------------------------------------------------------------------------
*
* The cookie name.
*
* @deprecated Use `Config\Security` $cookieName property instead of using this property.
*/
public string $CSRFCookieName = 'csrf_cookie_name';
/**
* --------------------------------------------------------------------------
* CSRF Expire
* --------------------------------------------------------------------------
*
* The number in seconds the token should expire.
*
* @deprecated Use `Config\Security` $expire property instead of using this property.
*/
public int $CSRFExpire = 7200;
/**
* --------------------------------------------------------------------------
* CSRF Regenerate
* --------------------------------------------------------------------------
*
* Regenerate token on every submission?
*
* @deprecated Use `Config\Security` $regenerate property instead of using this property.
*/
public bool $CSRFRegenerate = true;
/**
* --------------------------------------------------------------------------
* CSRF Redirect
* --------------------------------------------------------------------------
*
* Redirect to previous page with error on failure?
*
* @deprecated Use `Config\Security` $redirect property instead of using this property.
*/
public bool $CSRFRedirect = true;
/**
* --------------------------------------------------------------------------
* CSRF SameSite
* --------------------------------------------------------------------------
*
* Setting for CSRF SameSite cookie token. Allowed values are:
* - None
* - Lax
* - Strict
* - ''
*
* Defaults to `Lax` as recommended in this link:
*
* @see https://portswigger.net/web-security/csrf/samesite-cookies
*
* @deprecated Use `Config\Security` $samesite property instead of using this property.
*/
public string $CSRFSameSite = 'Lax';
public $proxyIPs = [];
/**
* --------------------------------------------------------------------------
@ -422,33 +217,70 @@ class App extends BaseConfig
/**
* --------------------------------------------------------------------------
* Media root folder
* Instance / Site Config
* --------------------------------------------------------------------------
* Defines the root folder for media files storage
*/
public string $mediaRoot = 'media';
public string $siteName = 'Castopod';
public string $siteTitleSeparator = ' | ';
public string $siteDescription = 'Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.';
/**
* --------------------------------------------------------------------------
* Admin gateway
* --------------------------------------------------------------------------
* Defines a base route for all admin pages
* @var array<int|string, string>
*/
public string $adminGateway = 'cp-admin';
public array $siteIcon = [
'ico' => '/favicon.ico',
'64' => '/icon-64.png',
'180' => '/icon-180.png',
'192' => '/icon-192.png',
'512' => '/icon-512.png',
];
public string $theme = 'pine';
/**
* --------------------------------------------------------------------------
* Auth gateway
* --------------------------------------------------------------------------
* Defines a base route for all authentication related pages
* Storage limit in Gigabytes
*/
public string $authGateway = 'cp-auth';
public ?int $storageLimit = null;
/**
* --------------------------------------------------------------------------
* Install gateway
* --------------------------------------------------------------------------
* Defines a base route for instance installation
* Bandwidth limit (per month) in Gigabytes
*/
public string $installGateway = 'cp-install';
public ?int $bandwidthLimit = null;
public ?string $legalNoticeURL = null;
/**
* AuthToken Config Constructor
*/
public function __construct()
{
parent::__construct();
if (is_string($this->proxyIPs)) {
$array = json_decode($this->proxyIPs, true);
if (is_array($array)) {
$this->proxyIPs = $array;
}
}
}
/**
* Override parent initEnvValue() to allow for direct setting to array properties values from ENV
*
* In order to set array properties via ENV vars we need to set the property to a string value first.
*
* @param mixed $property
*/
#[Override]
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix): void
{
// if attempting to set property from ENV, first set to empty string
if ($name === 'proxyIPs' && $this->getEnvValue($name, $prefix, $shortPrefix) !== null) {
$property = '';
}
parent::initEnvValue($property, $name, $prefix, $shortPrefix);
}
}

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use Myth\Auth\Config\Auth as MythAuthConfig;
class Auth extends MythAuthConfig
{
/**
* --------------------------------------------------------------------------
* Views used by Auth Controllers
* --------------------------------------------------------------------------
*
* @var array<string, string>
*/
public $views = [
'login' => 'auth/login',
'register' => 'auth/register',
'forgot' => 'auth/forgot',
'reset' => 'auth/reset',
'emailForgot' => 'auth/emails/forgot',
'emailActivation' => 'auth/emails/activation',
];
/**
* --------------------------------------------------------------------------
* Layout for the views to extend
* --------------------------------------------------------------------------
*
* @var string
*/
public $viewLayout = 'auth/_layout';
/**
* --------------------------------------------------------------------------
* Allow User Registration
* --------------------------------------------------------------------------
* When enabled (default) any unregistered user may apply for a new
* account. If you disable registration you may need to ensure your
* controllers and views know not to offer registration.
*
* @var bool
*/
public $allowRegistration = false;
/**
* --------------------------------------------------------------------------
* Require confirmation registration via email
* --------------------------------------------------------------------------
* When enabled, every registered user will receive an email message
* with a special link he have to confirm to activate his account.
*
* @var bool
*/
public $requireActivation = false;
}

View file

@ -27,30 +27,39 @@ class Autoload extends AutoloadConfig
* their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated.
*
* The '/app' and '/system' directories are already mapped for you.
* you may change the name of the 'App' namespace if you wish,
* The 'Config' (APPPATH . 'Config') and 'CodeIgniter' (SYSTEMPATH) are
* 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,
* else you will need to modify all of those classes for this to work.
*
* Prototype:
*
* $psr4 = [
* 'CodeIgniter' => SYSTEMPATH,
* 'App' => APPPATH
* ];
*
* @var array<string, string>
* @var array<string, list<string>|string>
*/
public $psr4 = [
APP_NAMESPACE => APPPATH,
'Config' => APPPATH . 'Config',
'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
'Analytics' => APPPATH . 'Libraries/Analytics',
APP_NAMESPACE => APPPATH,
'Modules' => ROOTPATH . 'modules/',
'Modules\Admin' => ROOTPATH . 'modules/Admin/',
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/',
'Modules\Api\Rest\V1' => ROOTPATH . 'modules/Api/Rest/V1',
'Modules\Auth' => ROOTPATH . 'modules/Auth/',
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
'Modules\Plugins' => ROOTPATH . 'modules/Plugins/',
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
'Modules\Update' => ROOTPATH . 'modules/Update/',
'Modules\WebSub' => ROOTPATH . 'modules/WebSub/',
'Themes' => ROOTPATH . 'themes',
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
];
/**
* -------------------------------------------------------------------
* Class Map
* -------------------------------------------------------------------
* The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have
@ -77,12 +86,25 @@ class Autoload extends AutoloadConfig
* or for loading functions.
*
* Prototype:
* ```
*
* $files = [
* '/path/to/my/file.php',
* ];
* ```
* @var array<int, string>
*
* @var list<string>
*/
public $files = [];
/**
* -------------------------------------------------------------------
* Helpers
* -------------------------------------------------------------------
* Prototype:
* $helpers = [
* 'form',
* ];
*
* @var list<string>
*/
public $helpers = ['auth', 'setting', 'plugins'];
}

View file

@ -9,8 +9,10 @@ declare(strict_types=1);
* In development, we want to show as many errors as possible to help
* make sure they don't make it to production. And save us hours of
* painful debugging.
*
* If you set 'display_errors' to '1', CI4's detailed error report will show.
*/
error_reporting(-1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
/**

View file

@ -8,9 +8,13 @@ declare(strict_types=1);
* --------------------------------------------------------------------------
* Don't show ANY in production environments. Instead, let the system catch
* it and display a generic error message.
*
* 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');
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
/**
* --------------------------------------------------------------------------

View file

@ -2,6 +2,12 @@
declare(strict_types=1);
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/**
* --------------------------------------------------------------------------
* ERROR DISPLAY
@ -10,7 +16,7 @@ declare(strict_types=1);
* make sure they don't make it to production. And save us hours of
* painful debugging.
*/
error_reporting(-1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
/**

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CURLRequest Share Connection Options
* --------------------------------------------------------------------------
*
* Share connection options between requests.
*
* @var list<int>
*
* @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
*/
public array $shareConnectionOptions = [CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_DNS];
/**
* --------------------------------------------------------------------------
* CURLRequest Share Options
* --------------------------------------------------------------------------
*
* Whether share options between requests or not.
*
* If true, all the options won't be reset between requests.
* It may cause an error request with unnecessary headers.
*/
public bool $shareOptions = false;
}

View file

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\ApcuHandler;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
@ -35,37 +37,6 @@ class Cache extends BaseConfig
*/
public string $backupHandler = 'dummy';
/**
* --------------------------------------------------------------------------
* Cache Directory Path
* --------------------------------------------------------------------------
*
* The path to where cache files should be stored, if using a file-based
* system.
*
* @deprecated Use the driver-specific variant under $file
*/
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.
* array('q') = Enabled, but only take into account the specified list
* of query parameters.
*
* @var boolean|string[]
*/
public bool | array $cacheQueryString = false;
/**
* --------------------------------------------------------------------------
* Key Prefix
@ -89,36 +60,51 @@ class Cache extends BaseConfig
*/
public int $ttl = 60;
/**
* --------------------------------------------------------------------------
* Reserved Characters
* --------------------------------------------------------------------------
*
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
*
* Note: The default set is required for PSR-6 compliance.
*/
public string $reservedCharacters = '{}()/\@:';
/**
* --------------------------------------------------------------------------
* File settings
* --------------------------------------------------------------------------
*
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
* @var array<string, string|int|null>
* @var array{storePath?: string, mode?: int}
*/
public array $file = [
'storePath' => WRITEPATH . 'cache/',
'mode' => 0640,
'mode' => 0640,
];
/**
* -------------------------------------------------------------------------
* Memcached settings
* -------------------------------------------------------------------------
*
* Your Memcached servers can be specified below, if you are using
* the Memcached drivers.
*
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
*
* @var array<string, string|int|boolean>
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
*/
public array $memcached = [
'host' => '127.0.0.1',
'port' => 11211,
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1,
'raw' => false,
'raw' => false,
];
/**
@ -128,14 +114,24 @@ class Cache extends BaseConfig
* Your Redis server can be specified below, if you are using
* the Redis or Predis drivers.
*
* @var array<string, string|int|null>
* @var array{
* host?: string,
* password?: string|null,
* port?: int,
* timeout?: int,
* async?: bool,
* persistent?: bool,
* database?: int
* }
*/
public array $redis = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'async' => false, // specific to Predis and ignored by the native Redis extension
'persistent' => false,
'database' => 0,
];
/**
@ -146,14 +142,58 @@ class Cache extends BaseConfig
* This is an array of cache engine alias' and class names. Only engines
* that are listed here are allowed to be used.
*
* @var array<string, string>
* @var array<string, class-string<CacheInterface>>
*/
public array $validHandlers = [
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'apcu' => ApcuHandler::class,
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
'predis' => PredisHandler::class,
'redis' => RedisHandler::class,
'wincache' => WincacheHandler::class,
'predis' => PredisHandler::class,
'redis' => RedisHandler::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;
/**
* --------------------------------------------------------------------------
* Web Page Caching: Cache Status Codes
* --------------------------------------------------------------------------
*
* HTTP status codes that are allowed to be cached. Only responses with
* these status codes will be cached by the PageCache filter.
*
* Default: [] - Cache all status codes (backward compatible)
*
* Recommended: [200] - Only cache successful responses
*
* You can also use status codes like:
* [200, 404, 410] - Cache successful responses and specific error codes
* [200, 201, 202, 203, 204] - All 2xx successful responses
*
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
* Consider restricting to [200] for production applications to avoid
* caching errors that should be temporary.
*
* @var list<int>
*/
public array $cacheStatusCodes = [];
}

150
app/Config/Colors.php Normal file
View file

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Colors extends BaseConfig
{
/**
* @var array<string, array<string, mixed>>
*/
public array $themes = [
/* Castopod's brand color */
'pine' => [
'accent-base' => [174, 100, 29],
'accent-hover' => [172, 100, 17],
'accent-muted' => [131, 100, 12],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [172, 100, 17],
'heading-background' => [111, 64, 94],
'background-elevated' => [0, 0, 100],
'background-base' => [173, 44, 96],
'background-navigation' => [172, 100, 17],
'background-header' => [172, 100, 17],
'background-highlight' => [111, 64, 94],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [111, 42, 86],
'border-contrast' => [0, 0, 0],
'border-navigation' => [131, 100, 12],
'text-base' => [158, 8, 3],
'text-muted' => [172, 8, 38],
],
/* Red / Rose color */
'crimson' => [
'accent-base' => [350, 87, 61],
'accent-hover' => [348, 75, 40],
'accent-muted' => [348, 73, 32],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [348, 73, 32],
'heading-background' => [344, 79, 96],
'background-elevated' => [0, 0, 100],
'background-base' => [350, 44, 96],
'background-header' => [348, 75, 40],
'background-highlight' => [344, 79, 96],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [348, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [340, 8, 3],
'text-muted' => [345, 8, 38],
],
/* Blue color */
'lake' => [
'accent-base' => [194, 100, 44],
'accent-hover' => [194, 100, 22],
'accent-muted' => [195, 100, 11],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [194, 100, 22],
'heading-background' => [195, 100, 92],
'background-elevated' => [0, 0, 100],
'background-base' => [196, 44, 96],
'background-header' => [194, 100, 22],
'background-highlight' => [195, 100, 92],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [195, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [194, 8, 3],
'text-muted' => [195, 8, 38],
],
/* Orange color */
'amber' => [
'accent-base' => [17, 100, 57],
'accent-hover' => [17, 100, 35],
'accent-muted' => [17, 100, 24],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [17, 100, 35],
'heading-background' => [17, 100, 89],
'background-elevated' => [0, 0, 100],
'background-base' => [15, 44, 96],
'background-header' => [17, 100, 35],
'background-highlight' => [17, 100, 89],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [17, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [15, 8, 3],
'text-muted' => [17, 8, 38],
],
/* Violet color */
'jacaranda' => [
'accent-base' => [254, 72, 52],
'accent-hover' => [254, 73, 30],
'accent-muted' => [254, 71, 19],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [254, 73, 30],
'heading-background' => [254, 73, 84],
'background-elevated' => [0, 0, 100],
'background-base' => [253, 44, 96],
'background-header' => [254, 73, 30],
'background-highlight' => [254, 88, 91],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [254, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [253, 8, 3],
'text-muted' => [254, 8, 38],
],
/* Black color */
'onyx' => [
'accent-base' => [240, 17, 2],
'accent-hover' => [240, 17, 17],
'accent-muted' => [240, 17, 17],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [240, 17, 17],
'heading-background' => [240, 17, 94],
'background-elevated' => [0, 0, 100],
'background-base' => [240, 17, 96],
'background-header' => [240, 12, 17],
'background-highlight' => [240, 17, 94],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [240, 17, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [240, 8, 3],
'text-muted' => [240, 8, 38],
],
];
}

View file

@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.80');
defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3');
/*
| --------------------------------------------------------------------
@ -24,10 +24,23 @@ defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.80');
| classes should use.
|
| NOTE: changing this will require manually modifying the
| existing namespaces of App\* namespaced-classes.
| existing namespaces of App* namespaced-classes.
*/
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
/*
| --------------------------------------------------------------------
| Plugins Path
| --------------------------------------------------------------------
|
| This defines the folder in which plugins will live.
*/
defined('PLUGINS_PATH') ||
define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR);
defined('PLUGINS_KEY_PATTERN') ||
define('PLUGINS_KEY_PATTERN', '[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*');
/*
| --------------------------------------------------------------------------
| Composer Path
@ -52,9 +65,9 @@ defined('MINUTE') || define('MINUTE', 60);
defined('HOUR') || define('HOUR', 3600);
defined('DAY') || define('DAY', 86400);
defined('WEEK') || define('WEEK', 604800);
defined('MONTH') || define('MONTH', 2592000);
defined('YEAR') || define('YEAR', 31536000);
defined('DECADE') || define('DECADE', 315360000);
defined('MONTH') || define('MONTH', 2_592_000);
defined('YEAR') || define('YEAR', 31_536_000);
defined('DECADE') || define('DECADE', 315_360_000);
/*
| --------------------------------------------------------------------------

View file

@ -26,37 +26,77 @@ class ContentSecurityPolicy extends BaseConfig
*/
public ?string $reportURI = null;
/**
* Specifies a reporting endpoint to which violation reports ought to be sent.
*/
public ?string $reportTo = null;
/**
* Instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large
* numbers of old URLs that need to be rewritten.
*/
public bool $upgradeInsecureRequests = false;
// -------------------------------------------------------------------------
// CSP DIRECTIVES SETTINGS
// NOTE: once you set a policy to 'none', it cannot be further restricted
// -------------------------------------------------------------------------
/**
* 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;
/**
* Lists allowed scripts' URLs.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $scriptSrc = 'self';
/**
* Specifies valid sources for JavaScript <script> elements.
*
* @var list<string>|string
*/
public array|string $scriptSrcElem = 'self';
/**
* Specifies valid sources for JavaScript inline event
* handlers and JavaScript URLs.
*
* @var list<string>|string
*/
public array|string $scriptSrcAttr = 'self';
/**
* Lists allowed stylesheets' URLs.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $styleSrc = 'self';
/**
* Specifies valid sources for stylesheets <link> elements.
*
* @var list<string>|string
*/
public array|string $styleSrcElem = 'self';
/**
* Specifies valid sources for stylesheets inline
* style attributes and `<style>` elements.
*
* @var list<string>|string
*/
public array|string $styleSrcAttr = 'self';
/**
* Defines the origins from which images can be loaded.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $imageSrc = 'self';
@ -65,35 +105,35 @@ class ContentSecurityPolicy extends BaseConfig
*
* Will default to self if not overridden
*
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $baseURI = null;
/**
* Lists the URLs for workers and embedded frame contents
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $childSrc = 'self';
/**
* 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';
/**
* Specifies the origins that can serve web fonts.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $fontSrc;
/**
* Lists valid endpoints for submission from `<form>` tags.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $formAction = 'self';
@ -102,47 +142,67 @@ class ContentSecurityPolicy extends BaseConfig
* `<embed>`, and `<applet>` tags. This directive can't be used in `<meta>` tags and applies only to non-HTML
* resources.
*
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $frameAncestors = null;
/**
* 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;
/**
* Restricts the origins allowed to deliver video and audio.
*
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $mediaSrc = null;
/**
* Allows control over Flash and other plugins.
*
* @var string|string[]
* @var list<string>|string
*/
public string | array $objectSrc = 'self';
/**
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $manifestSrc = null;
/**
* @var list<string>|string
*/
public array|string $workerSrc = [];
/**
* Limits the kinds of plugins a page may invoke.
*
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $pluginTypes = null;
/**
* List of actions allowed.
*
* @var string|string[]|null
* @var list<string>|string|null
*/
public string | array | null $sandbox = null;
/**
* Nonce placeholder for style tags.
*/
public string $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce placeholder for script tags.
*/
public string $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tag automatically?
*/
public bool $autoNonce = true;
}

View file

@ -84,6 +84,8 @@ class Cookie extends BaseConfig
* Defaults to `Lax` for compatibility with modern browsers. Setting `''`
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$secure` must also be set.
*
* @var ''|'Lax'|'None'|'Strict'
*/
public string $samesite = 'Lax';

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

@ -24,53 +24,67 @@ class Database extends Config
/**
* The default database connection.
*
* @var array<string, string|bool|int|array>
* @var array<string, mixed>
*/
public array $default = [
'DSN' => '',
'hostname' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'cp_',
'pConnect' => false,
'DBDebug' => ENVIRONMENT !== 'production',
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_unicode_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'DSN' => '',
'hostname' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'cp_',
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_unicode_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'numberNative' => false,
'foundRows' => false,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
];
/**
* This database connection is used when running PHPUnit database tests.
*
* @noRector StringClassNameToClassConstantRector
*
* @var array<string, string|bool|int|array>
* @var array<string, mixed>
*/
public array $tests = [
'DSN' => '',
'DSN' => '',
'hostname' => '127.0.0.1',
'username' => '',
'password' => '',
'database' => ':memory:',
'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_',
'pConnect' => false,
'DBDebug' => ENVIRONMENT !== 'production',
'charset' => 'utf8',
'DBCollat' => 'utf8_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
// Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
'pConnect' => false,
'DBDebug' => true,
'charset' => 'utf8',
'DBCollat' => '',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'synchronous' => null,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
];
//--------------------------------------------------------------------

View file

@ -12,42 +12,34 @@ class DocTypes
* @var array<string, string>
*/
public array $list = [
'xhtml11' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'xhtml1-strict' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'xhtml1-trans' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml1-frame' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'xhtml-basic11' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'html5' => '<!DOCTYPE html>',
'html4-strict' =>
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
'html4-trans' =>
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
'html4-frame' =>
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
'mathml1' =>
'<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
'mathml2' =>
'<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
'svg10' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
'svg11' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
'svg11-basic' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
'svg11-tiny' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
'xhtml-math-svg-xh' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-math-svg-sh' =>
'<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-rdfa-1' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
'xhtml-rdfa-2' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">',
'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'xhtml1-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'xhtml1-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml1-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'xhtml-basic11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'html5' => '<!DOCTYPE html>',
'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
'mathml1' => '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
'mathml2' => '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
'svg10' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
'svg11' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
'svg11-basic' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
'svg11-tiny' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
'xhtml-math-svg-xh' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-math-svg-sh' => '<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-rdfa-1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
'xhtml-rdfa-2' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">',
];
/**
* Whether to remove the solidus (`/`) character for void HTML elements (e.g. `<input>`)
* for HTML5 compatibility.
*
* Set to:
* `true` - to be HTML5 compatible
* `false` - to be XHTML compatible
*/
public bool $html5 = true;
}

View file

@ -8,21 +8,21 @@ use CodeIgniter\Config\BaseConfig;
class Email extends BaseConfig
{
public string $fromEmail;
public string $fromEmail = '';
public string $fromName;
public string $fromName = 'Castopod';
public string $recipients;
public string $recipients = '';
/**
* The "user agent"
*/
public string $userAgent = 'CodeIgniter';
public string $userAgent = 'Castopod/' . CP_VERSION;
/**
* The mail sending protocol: mail, sendmail, smtp
*/
public string $protocol = 'mail';
public string $protocol = 'smtp';
/**
* The server path to Sendmail.
@ -30,19 +30,24 @@ class Email extends BaseConfig
public string $mailPath = '/usr/sbin/sendmail';
/**
* SMTP Server Address
* SMTP Server Hostname
*/
public string $SMTPHost;
public string $SMTPHost = '';
/**
* Which SMTP authentication method to use: login, plain
*/
public string $SMTPAuthMethod = 'login';
/**
* SMTP Username
*/
public string $SMTPUser;
public string $SMTPUser = '';
/**
* SMTP Password
*/
public string $SMTPPass;
public string $SMTPPass = '';
/**
* SMTP Port
@ -60,7 +65,11 @@ class Email extends BaseConfig
public bool $SMTPKeepAlive = false;
/**
* SMTP Encryption. Either tls or ssl
* SMTP Encryption.
*
* @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command
* to the server. 'ssl' means implicit SSL. Connection on port
* 465 should set this to ''.
*/
public string $SMTPCrypto = 'tls';
@ -77,7 +86,7 @@ class Email extends BaseConfig
/**
* Type of mail, either 'text' or 'html'
*/
public string $mailType = 'text';
public string $mailType = 'html';
/**
* Character set (utf-8, iso-8859-1, etc.)
@ -118,4 +127,11 @@ class Email extends BaseConfig
* Enable notify message from server
*/
public bool $DSN = false;
public function __construct()
{
parent::__construct();
$this->userAgent = 'Castopod/' . CP_VERSION . '; +' . base_url('', 'https');
}
}

19
app/Config/Embed.php Normal file
View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Embed extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Embeddable player config
* --------------------------------------------------------------------------
*/
public int $width = 485;
public int $height = 112;
}

View file

@ -25,6 +25,23 @@ class Encryption extends BaseConfig
*/
public string $key = '';
/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
*
* When rotating encryption keys, add old keys here to maintain ability
* to decrypt data encrypted with previous keys. Encryption always uses
* the current $key. Decryption tries current key first, then falls back
* to previous keys if decryption fails.
*
* In .env file, use comma-separated string:
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
*
* @var list<string>|string
*/
public array|string $previousKeys = '';
/**
* --------------------------------------------------------------------------
* Encryption Driver to Use
@ -58,4 +75,37 @@ class Encryption extends BaseConfig
* HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
*/
public string $digest = 'SHA512';
/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
* This setting is only used by OpenSSLHandler.
*
* Set to false for CI3 Encryption compatibility.
*/
public bool $rawData = true;
/**
* Encryption key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'encryption' for CI3 Encryption compatibility.
*/
public string $encryptKeyInfo = '';
/**
* Authentication key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'authentication' for CI3 Encryption compatibility.
*/
public string $authKeyInfo = '';
/**
* Cipher to use.
* This setting is only used by OpenSSLHandler.
*
* Set to 'AES-128-CBC' to decrypt encrypted data that encrypted
* by CI3 Encryption default configuration.
*/
public string $cipher = 'AES-256-CTR';
}

View file

@ -5,10 +5,12 @@ declare(strict_types=1);
namespace Config;
use App\Entities\Actor;
use App\Entities\Status;
use App\Entities\User;
use App\Entities\Post;
use App\Models\EpisodeModel;
use CodeIgniter\Debug\Toolbar\Collectors\Database;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
use CodeIgniter\HotReloader\HotReloader;
/*
* --------------------------------------------------------------------
@ -27,8 +29,7 @@ use CodeIgniter\Exceptions\FrameworkException;
* Events::on('create', [$myInstance, 'myMethod']);
*/
Events::on('pre_system', function () {
// @phpstan-ignore-next-line
Events::on('pre_system', static function (): void {
if (ENVIRONMENT !== 'testing') {
if (ini_get('zlib.output_compression')) {
throw FrameworkException::forEnabledZlibOutputCompression();
@ -38,9 +39,7 @@ Events::on('pre_system', function () {
ob_end_flush();
}
ob_start(function ($buffer) {
return $buffer;
});
ob_start(static fn ($buffer) => $buffer);
}
/*
@ -48,42 +47,32 @@ Events::on('pre_system', function () {
* Debug Toolbar Listeners.
* --------------------------------------------------------------------
* If you delete, they will no longer be collected.
*
* @phpstan-ignore-next-line
*/
if (CI_DEBUG && ! is_cli()) {
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
Services::toolbar()->respond();
Events::on('DBQuery', Database::class . '::collect');
service('toolbar')
->respond();
// Hot Reload route - for framework use on the hot reloader.
if (ENVIRONMENT === 'development') {
service('routes')->get('__hot-reload', static function (): void {
new HotReloader()
->run();
});
}
}
});
Events::on('login', function (User $user): void {
helper('auth');
// set interact_as_actor_id value
$userPodcasts = $user->podcasts;
if ($userPodcasts = $user->podcasts) {
set_interact_as_actor($userPodcasts[0]->actor_id);
}
});
Events::on('logout', function (User $user): void {
helper('auth');
// remove user's interact_as_actor session
remove_interact_as_actor();
});
/*
* --------------------------------------------------------------------
* ActivityPub events
* Fediverse events
* --------------------------------------------------------------------
*/
/**
* @param Actor $actor
* @param Actor $targetActor
*/
Events::on('on_follow', function ($actor, $targetActor): void {
Events::on('on_follow', static function ($actor, $targetActor): void {
if ($actor->is_podcast) {
cache()
->deleteMatching("podcast#{$actor->podcast->id}*");
@ -103,7 +92,7 @@ Events::on('on_follow', function ($actor, $targetActor): void {
* @param Actor $actor
* @param Actor $targetActor
*/
Events::on('on_undo_follow', function ($actor, $targetActor): void {
Events::on('on_undo_follow', static function ($actor, $targetActor): void {
if ($actor->is_podcast) {
cache()
->deleteMatching("podcast#{$actor->podcast->id}*");
@ -120,82 +109,53 @@ Events::on('on_undo_follow', function ($actor, $targetActor): void {
});
/**
* @param Status $status
* @param Post $post
*/
Events::on('on_status_add', function ($status): void {
if ($status->in_reply_to_id !== null) {
$status = $status->reply_to_status;
}
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_add', static function ($post): void {
model(EpisodeModel::class, false)->builder()
->where('id', $post->episode_id)
->increment('posts_count');
if ($post->actor->is_podcast) {
// Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs
// same for other events below
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
});
/**
* @param Status $status
* @param Post $post
*/
Events::on('on_status_remove', function ($status): void {
if ($status->in_reply_to_id !== null) {
Events::trigger('on_status_remove', $status->reply_to_status);
Events::on('on_post_remove', static function ($post): void {
if ($episodeId = $post->episode_id) {
model(EpisodeModel::class, false)->builder()
->where('id', $episodeId)
->decrement('posts_count');
}
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('statuses_total', 1 + $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total', $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('favourites_total', $status->favourites_count);
}
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
});
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_reblog', function ($actor, $status): void {
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->increment('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_reblog', static function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
if ($actor->is_podcast) {
@ -205,111 +165,106 @@ Events::on('on_status_reblog', function ($actor, $status): void {
}
cache()
->deleteMatching("page_status#{$status->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
->deleteMatching("page_post#{$post->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
}
});
/**
* @param Status $reblogStatus
* @param Post $reblogPost
*/
Events::on('on_status_undo_reblog', function ($reblogStatus): void {
$status = $reblogStatus->reblog_of_status;
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->decrement('statuses_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_undo_reblog', static function ($reblogPost): void {
$post = $reblogPost->reblog_of_post;
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
cache()
->deleteMatching("page_status#{$reblogStatus->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
->deleteMatching("page_post#{$reblogPost->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
}
if ($reblogStatus->actor->is_podcast) {
if ($reblogPost->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*");
->deleteMatching("podcast#{$reblogPost->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$reblogStatus->actor->podcast->id}*");
->deleteMatching("page_podcast#{$reblogPost->actor->podcast->id}*");
}
});
/**
* @param Status $reply
* @param Post $reply
*/
Events::on('on_status_reply', function ($reply): void {
$status = $reply->reply_to_status;
Events::on('on_post_reply', static function ($reply): void {
$post = $reply->reply_to_post;
if ($post->in_reply_to_id === null) {
model(EpisodeModel::class, false)->builder()
->where('id', $post->episode_id)
->increment('comments_count');
}
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast-{$post->actor->podcast->handle}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
});
/**
* @param Status $reply
* @param Post $reply
*/
Events::on('on_reply_remove', function ($reply): void {
$status = $reply->reply_to_status;
Events::on('on_reply_remove', static function ($reply): void {
$post = $reply->reply_to_post;
if ($post->in_reply_to_id === null) {
model(EpisodeModel::class, false)->builder()
->where('id', $post->episode_id)
->decrement('comments_count');
}
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast-{$post->actor->podcast->handle}*");
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
cache()
->deleteMatching("page_status#{$reply->id}*");
->deleteMatching("page_post#{$reply->id}*");
});
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('favourites_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_favourite', static function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
->deleteMatching("page_post#{$post->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
}
if ($actor->is_podcast) {
@ -321,27 +276,20 @@ Events::on('on_status_favourite', function ($actor, $status): void {
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_undo_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->decrement('favourites_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_undo_favourite', static function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
->deleteMatching("page_post#{$post->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
}
if ($actor->is_podcast) {
@ -351,34 +299,34 @@ Events::on('on_status_undo_favourite', function ($actor, $status): void {
}
});
Events::on('on_block_actor', function (int $actorId): void {
Events::on('on_block_actor', static function (int $actorId): void {
cache()->deleteMatching('page_podcast*');
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_unblock_actor', function (int $actorId): void {
Events::on('on_unblock_actor', static function (int $actorId): void {
cache()->deleteMatching('page_podcast*');
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_block_domain', function (string $domainName): void {
Events::on('on_block_domain', static function (string $domainName): void {
cache()->deleteMatching('page_podcast*');
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_unblock_domain', function (string $domainName): void {
Events::on('on_unblock_domain', static function (string $domainName): void {
cache()->deleteMatching('page_podcast*');
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});

View file

@ -5,6 +5,10 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\ExceptionHandler;
use CodeIgniter\Debug\ExceptionHandlerInterface;
use Psr\Log\LogLevel;
use Throwable;
/**
* Setup how the exception handler works.
@ -29,7 +33,7 @@ class Exceptions extends BaseConfig
* Any status codes here will NOT be logged if logging is turned on.
* By default, only 404 (Page Not Found) exceptions are ignored.
*
* @var int[]
* @var list<int>
*/
public array $ignoreCodes = [404];
@ -52,7 +56,53 @@ class Exceptions extends BaseConfig
* In order to specify 2 levels, use "/" to separate.
* ex. ['server', 'setup/password', 'secret_token']
*
* @var string[]
* @var list<string>
*/
public array $sensitiveDataInTrace = [];
/**
* --------------------------------------------------------------------------
* WHETHER TO THROW AN EXCEPTION ON DEPRECATED ERRORS
* --------------------------------------------------------------------------
* If set to `true`, DEPRECATED errors are only logged and no exceptions are
* thrown. This option also works for user deprecations.
*/
public bool $logDeprecations = true;
/**
* --------------------------------------------------------------------------
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
* --------------------------------------------------------------------------
* If `$logDeprecations` is set to `true`, this sets the log level
* to which the deprecation will be logged. This should be one of the log
* levels recognized by PSR-3.
*
* The related `Config\Logger::$threshold` should be adjusted, if needed,
* to capture logging the deprecations.
*/
public string $deprecationLogLevel = LogLevel::WARNING;
/*
* DEFINE THE HANDLERS USED
* --------------------------------------------------------------------------
* Given the HTTP status code, returns exception handler that
* should be used to deal with this error. By default, it will run CodeIgniter's
* default handler and display the error information in the expected format
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
* response format.
*
* Custom handlers can be returned if you want to handle one or more specific
* error codes yourself like:
*
* if (in_array($statusCode, [400, 404, 500])) {
* return new \App\Libraries\MyExceptionHandler();
* }
* if ($exception instanceOf PageNotFoundException) {
* return new \App\Libraries\MyExceptionHandler();
* }
*/
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
{
return new ExceptionHandler($this);
}
}

39
app/Config/Feature.php Normal file
View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Enable/disable backward compatibility breaking features.
*/
class Feature extends BaseConfig
{
/**
* Use improved new auto routing instead of the legacy version.
*/
public bool $autoRoutesImproved = true;
/**
* 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;
/**
* Use strict location negotiation.
*
* By default, the locale is selected based on a loose comparison of the language code (ISO 639-1)
* Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2).
*/
public bool $strictLocaleNegotiation = false;
}

58
app/Config/Fediverse.php Normal file
View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Config;
use App\Libraries\NoteObject;
use Exception;
use Modules\Fediverse\Config\Fediverse as FediverseBaseConfig;
class Fediverse extends FediverseBaseConfig
{
/**
* --------------------------------------------------------------------
* ActivityPub Objects
* --------------------------------------------------------------------
*/
public string $noteObject = NoteObject::class;
public string $defaultAvatarImagePath = 'castopod-avatar_thumbnail.webp';
public string $defaultAvatarImageMimetype = 'image/webp';
public function __construct()
{
parent::__construct();
try {
$appTheme = service('settings')
->get('App.theme');
$defaultBanner = config('Images')
->podcastBannerDefaultPaths[$appTheme] ?? config('Images')->podcastBannerDefaultPaths['default'];
} catch (Exception) {
$defaultBanner = config('Images')
->podcastBannerDefaultPaths['default'];
}
['dirname' => $dirname, 'extension' => $extension, 'filename' => $filename] = pathinfo(
$defaultBanner['path'],
);
$defaultBannerPath = $filename;
if ($dirname !== '.') {
$defaultBannerPathList = [$dirname, $filename];
$defaultBannerPath = implode('/', $defaultBannerPathList);
}
helper('media');
$this->defaultCoverImagePath = $defaultBannerPath . '_federation.' . $extension;
$this->defaultCoverImageMimetype = $defaultBanner['mimetype'];
}
}

View file

@ -4,54 +4,101 @@ declare(strict_types=1);
namespace Config;
use ActivityPub\Filters\ActivityPubFilter;
use App\Filters\PermissionFilter;
use App\Filters\AllowCorsFilter;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\ForceHTTPS;
use CodeIgniter\Filters\Honeypot;
use Myth\Auth\Filters\LoginFilter;
use Myth\Auth\Filters\RoleFilter;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\SecureHeaders;
use Modules\Auth\Filters\PermissionFilter;
class Filters extends BaseConfig
{
/**
* Configures aliases for Filter classes to make reading things nicer and simpler.
*
* @var array<string, string>
* @var array<string, class-string|list<class-string>>
*
* [filter_name => classname]
* or [filter_name => [classname1, classname2, ...]]
*/
public array $aliases = [
'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class,
'login' => LoginFilter::class,
'role' => RoleFilter::class,
'permission' => PermissionFilter::class,
'activity-pub' => ActivityPubFilter::class,
'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class,
'invalidchars' => InvalidChars::class,
'secureheaders' => SecureHeaders::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.
*
* @var array<string, string[]>
* @var array{
* before: array<string, array{except: list<string>|string}>|list<string>,
* after: array<string, array{except: list<string>|string}>|list<string>
* }
*/
public array $globals = [
'before' => [
// 'honeypot',
// 'csrf',
'csrf' => [
'except' => [
'@[a-zA-Z0-9\_]{1,32}/inbox',
'api/rest/v1/episodes',
'api/rest/v1/episodes/[0-9]+/publish',
],
],
// 'invalidchars',
],
'after' => [
'toolbar',
// 'honeypot',
// 'secureheaders',
],
];
/**
* List of filter aliases that works on a particular HTTP method (GET, POST, etc.).
*
* Example: 'post' => ['csrf', 'throttle']
* Example: 'POST' => ['foo', 'bar']
*
* @var array<string, string[]>
* 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.
*
* @var array<string, list<string>>
*/
public array $methods = [];
@ -60,7 +107,7 @@ class Filters extends BaseConfig
*
* Example: 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*
* @var array<string, array<string, string[]>>
* @var array<string, array<string, list<string>>>
*/
public array $filters = [];
@ -69,9 +116,14 @@ class Filters extends BaseConfig
parent::__construct();
$this->filters = [
'login' => [
'before' => [config('App')->adminGateway . '*'],
'session' => [
'before' => [config('Admin')->gateway . '*', config('Analytics')->gateway . '*'],
],
'podcast-unlock' => [
'before' => ['*@*/episodes/*'],
],
];
$this->aliases['permission'] = PermissionFilter::class;
}
}

View file

@ -6,6 +6,9 @@ namespace Config;
use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters;
/**
* @immutable
*/
class ForeignCharacters extends BaseForeignCharacters
{
}

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\FormatterInterface;
use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter;
@ -24,7 +23,7 @@ class Format extends BaseConfig
* These formats are only checked when the data passed to the respond()
* method is an array.
*
* @var string[]
* @var list<string>
*/
public array $supportedResponseFormats = [
'application/json',
@ -45,8 +44,8 @@ class Format extends BaseConfig
*/
public array $formatters = [
'application/json' => JSONFormatter::class,
'application/xml' => XMLFormatter::class,
'text/xml' => XMLFormatter::class,
'application/xml' => XMLFormatter::class,
'text/xml' => XMLFormatter::class,
];
/**
@ -61,19 +60,16 @@ class Format extends BaseConfig
*/
public array $formatterOptions = [
'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
'application/xml' => 0,
'text/xml' => 0,
'application/xml' => 0,
'text/xml' => 0,
];
//--------------------------------------------------------------------
/**
* A Factory method to return the appropriate formatter for the given mime type.
* --------------------------------------------------------------------------
* Maximum depth for JSON encoding.
* --------------------------------------------------------------------------
*
* @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
* This value determines how deep the JSON encoder will traverse nested structures.
*/
public function getFormatter(string $mime): FormatterInterface
{
return Services::format()->getFormatter($mime);
}
public int $jsonEncodeDepth = 512;
}

View file

@ -25,22 +25,22 @@ class Generators extends BaseConfig
*
* YOU HAVE BEEN WARNED!
*
* @var array<string, string>
* @var array<string, string|array<string,string>>
*/
public array $views = [
'make:command' =>
'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'make:controller' =>
'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
'make:migration' =>
'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
'make:validation' =>
'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
'session:migration' =>
'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:cell' => [
'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:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
'make:validation' => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
'session:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
];
}

View file

@ -30,6 +30,15 @@ class Honeypot extends BaseConfig
/**
* Honeypot container
*
* If you enabled CSP, you can remove `style="display:none"`.
*/
public string $container = '<div style="display:none">{template}</div>';
/**
* The id attribute for Honeypot container tag
*
* Used when CSP is enabled.
*/
public string $containerId = 'hpc';
}

42
app/Config/Hostnames.php Normal file
View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Config;
class Hostnames
{
// List of known two-part TLDs for subdomain extraction
public const TWO_PART_TLDS = [
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
];
}

View file

@ -17,6 +17,8 @@ class Images extends BaseConfig
/**
* The path to the image library. Required for ImageMagick, GraphicsMagick, or NetPBM.
*
* @deprecated 4.7.0 No longer used.
*/
public string $libraryPath = '/usr/local/bin/convert';
@ -26,48 +28,181 @@ class Images extends BaseConfig
* @var array<string, string>
*/
public array $handlers = [
'gd' => GDHandler::class,
'gd' => GDHandler::class,
'imagick' => ImageMagickHandler::class,
];
/*
|--------------------------------------------------------------------------
| Uploaded images resizing sizes (in px)
| Uploaded images sizes (in px)
|--------------------------------------------------------------------------
| The sizes listed below determine the resizing of images when uploaded.
| All uploaded images are of 1:1 ratio (width and height are the same).
*/
public int $thumbnailSize = 150;
public int $mediumSize = 320;
public int $largeSize = 1024;
/**
* Podcast cover image sizes
*
* Uploaded podcast covers are of 1:1 ratio (width and height are the same).
*
* Size of images linked in the rss feed (should be between 1400 and 3000). Size for ID3 tag cover art (should be
* between 300 and 800)
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, array<string, int|string>>
*/
public array $podcastCoverSizes = [
'tiny' => [
'width' => 40,
'height' => 40,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'large' => [
'width' => 1024,
'height' => 1024,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'feed' => [
'width' => 1400,
'height' => 1400,
],
'id3' => [
'width' => 500,
'height' => 500,
],
'og' => [
'width' => 1200,
'height' => 1200,
],
'federation' => [
'width' => 400,
'height' => 400,
],
'webmanifest192' => [
'width' => 192,
'height' => 192,
'mimetype' => 'image/png',
'extension' => 'png',
],
'webmanifest512' => [
'width' => 512,
'height' => 512,
'mimetype' => 'image/png',
'extension' => 'png',
],
];
/**
* Size of images linked in the rss feed (should be between 1400 and 3000)
* Podcast header cover image
*
* Uploaded podcast header covers are of 3:1 ratio
*
* @var array<string, array<string, int|string>>
*/
public int $feedSize = 1400;
public array $podcastBannerSizes = [
'small' => [
'width' => 320,
'height' => 128,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 960,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'federation' => [
'width' => 1500,
'height' => 500,
],
];
public string $avatarDefaultPath = 'assets/images/castopod-avatar.jpg';
public string $avatarDefaultMimeType = 'image/jpg';
/**
* Size for ID3 tag cover art (should be between 300 and 800)
* @var array<string, array<string, string>>
*/
public int $id3Size = 500;
public array $podcastBannerDefaultPaths = [
'default' => [
'path' => 'assets/images/castopod-banner-pine.jpg',
'mimetype' => 'image/jpeg',
],
'pine' => [
'path' => 'assets/images/castopod-banner-pine.jpg',
'mimetype' => 'image/jpeg',
],
'crimson' => [
'path' => 'assets/images/castopod-banner-crimson.jpg',
'mimetype' => 'image/jpeg',
],
'amber' => [
'path' => 'assets/images/castopod-banner-amber.jpg',
'mimetype' => 'image/jpeg',
],
'lake' => [
'path' => 'assets/images/castopod-banner-lake.jpg',
'mimetype' => 'image/jpeg',
],
'jacaranda' => [
'path' => 'assets/images/castopod-banner-jacaranda.jpg',
'mimetype' => 'image/jpeg',
],
'onyx' => [
'path' => 'assets/images/castopod-banner-onyx.jpg',
'mimetype' => 'image/jpeg',
],
];
/*
|--------------------------------------------------------------------------
| Uploaded images naming extensions
|--------------------------------------------------------------------------
| The properties listed below set the name extensions for the resized images
*/
public string $podcastBannerDefaultMimeType = 'image/jpeg';
public string $thumbnailSuffix = '_thumbnail';
public string $mediumSuffix = '_medium';
public string $largeSuffix = '_large';
public string $feedSuffix = '_feed';
public string $id3Suffix = '_id3';
/**
* Person image
*
* Uploaded person images are of 1:1 ratio (width and height are the same).
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, array<string, int|string>>
*/
public array $personAvatarSizes = [
'federation' => [
'width' => 400,
'height' => 400,
],
'tiny' => [
'width' => 40,
'height' => 40,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
];
}

View file

@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use Kint\Renderer\Renderer;
use Kint\Parser\ConstructablePluginInterface;
use Kint\Renderer\Rich\TabPluginInterface;
use Kint\Renderer\Rich\ValuePluginInterface;
/**
* --------------------------------------------------------------------------
* Kint
* --------------------------------------------------------------------------
*
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
@ -17,7 +17,7 @@ use Kint\Renderer\Renderer;
*
* @see https://kint-php.github.io/kint/ for details on these settings.
*/
class Kint extends BaseConfig
class Kint
{
/*
|--------------------------------------------------------------------------
@ -26,9 +26,9 @@ class Kint extends BaseConfig
*/
/**
* @var string[]
* @var list<class-string<ConstructablePluginInterface>|ConstructablePluginInterface>|null
*/
public array $plugins = [];
public ?array $plugins = [];
public int $maxDepth = 6;
@ -46,17 +46,15 @@ class Kint extends BaseConfig
public bool $richFolder = false;
public int $richSort = Renderer::SORT_FULL;
/**
* @var array<string, class-string<ValuePluginInterface>>|null
*/
public ?array $richObjectPlugins = [];
/**
* @var string[]
* @var array<string, class-string<TabPluginInterface>>|null
*/
public array $richObjectPlugins = [];
/**
* @var string[]
*/
public array $richTabPlugins = [];
public ?array $richTabPlugins = [];
/*
|--------------------------------------------------------------------------

View file

@ -6,6 +6,7 @@ namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Log\Handlers\FileHandler;
use CodeIgniter\Log\Handlers\HandlerInterface;
class Logger extends BaseConfig
{
@ -38,9 +39,9 @@ class Logger extends BaseConfig
* For a live site you'll usually enable Critical or higher (3) to be logged otherwise
* your log files will fill up very fast.
*
* @var int|int[]
* @var int|list<int>
*/
public int | array $threshold = 4;
public int | array $threshold = (ENVIRONMENT === 'production') ? 4 : 9;
/**
* --------------------------------------------------------------------------
@ -60,7 +61,7 @@ class Logger extends BaseConfig
* The logging system supports multiple actions to be taken when something
* is logged. This is done by allowing for multiple Handlers, special classes
* designed to write the log to their chosen destinations, whether that is
* a file on the getServer, a cloud-based service, or even taking actions such
* a file on the server, a cloud-based service, or even taking actions such
* as emailing the dev team.
*
* Each handler is defined by the class name used for that handler, and it
@ -75,7 +76,7 @@ class Logger extends BaseConfig
* Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down.
*
* @var array<string, mixed>
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
*/
public array $handlers = [
/*
@ -114,5 +115,32 @@ class Logger extends BaseConfig
*/
'path' => '',
],
/*
* The ChromeLoggerHandler requires the use of the Chrome web browser
* and the ChromeLogger extension. Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
// /*
// * The log levels that this handler will handle.
// */
// 'handles' => ['critical', 'alert', 'emergency', 'debug',
// 'error', 'info', 'notice', 'warning'],
// ],
/*
* The ErrorlogHandler writes the logs to PHP's native `error_log()` function.
* Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ErrorlogHandler' => [
// /* The log levels this handler can handle. */
// 'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
//
// /*
// * The message type where the error should go. Can be 0 or 4, or use the
// * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4)
// */
// 'messageType' => 0,
// ],
];
}

View file

@ -27,9 +27,7 @@ class Migrations extends BaseConfig
*
* This is the name of the table that will store the current migrations state.
* When migrations runs it will store in a database table which migration
* level the system is at. It then compares the migration level in this
* table to the $config['migration_version'] if they are not the same it
* will migrate up. This must be set.
* files have already been run.
*/
public string $table = 'migrations';
@ -48,4 +46,19 @@ class Migrations extends BaseConfig
* - Y_m_d_His_
*/
public string $timestampFormat = 'Y-m-d-His_';
/**
* --------------------------------------------------------------------------
* Enable/Disable Migration Lock
* --------------------------------------------------------------------------
*
* Locking is disabled by default.
*
* When enabled, it will prevent multiple migration processes
* from running at the same time by using a lock mechanism.
*
* This is useful in production environments to avoid conflicts
* or race conditions during concurrent deployments.
*/
public bool $lock = false;
}

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Config;
/**
* Mimes
*
* This file contains an array of mime types. It is used by the Upload class to help identify allowed file types.
*
* When more than one variation for an extension exist (like jpg, jpeg, etc) the most common one should be first in the
@ -15,13 +13,12 @@ namespace Config;
* When working with mime types, please make sure you have the ´fileinfo´ extension enabled to reliably detect the
* media types.
*/
class Mimes
{
/**
* Map of extensions to mime types.
*
* @var array<string, string|string[]>
* @var array<string, list<string>|string>
*/
public static $mimes = [
'hqx' => [
@ -53,21 +50,24 @@ class Mimes
'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'exe' => ['application/octet-stream', 'application/x-msdownload'],
'exe' => ['application/octet-stream',
'application/vnd.microsoft.portable-executable',
'application/x-dosexec',
'application/x-msdownload'],
'class' => 'application/octet-stream',
'psd' => ['application/x-photoshop', 'image/vnd.adobe.photoshop'],
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => ['application/pdf', 'application/force-download', 'application/x-download'],
'ai' => ['application/pdf', 'application/postscript'],
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => [
'psd' => ['application/x-photoshop', 'image/vnd.adobe.photoshop'],
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => ['application/pdf', 'application/force-download', 'application/x-download'],
'ai' => ['application/pdf', 'application/postscript'],
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => [
'application/vnd.ms-excel',
'application/msexcel',
'application/x-msexcel',
@ -87,21 +87,17 @@ class Mimes
'application/vnd.ms-office',
'application/msword',
],
'pptx' => [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/x-zip',
'application/zip',
],
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
'wbxml' => 'application/wbxml',
'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip',
'php' => [
'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip',
'php' => [
'application/x-php',
'application/x-httpd-php',
'application/php',
@ -109,41 +105,41 @@ class Mimes
'text/x-php',
'application/x-httpd-php-source',
],
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'js' => ['application/x-javascript', 'text/plain'],
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
'z' => 'application/x-compress',
'phps' => 'application/x-httpd-php-source',
'js' => ['application/x-javascript', 'text/plain'],
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
'z' => 'application/x-compress',
'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml',
'zip' => [
'xht' => 'application/xhtml+xml',
'zip' => [
'application/x-zip',
'application/zip',
'application/x-zip-compressed',
'application/s-compressed',
'multipart/x-zip',
],
'rar' => ['application/vnd.rar', 'application/x-rar', 'application/rar', 'application/x-rar-compressed'],
'mid' => 'audio/midi',
'rar' => ['application/vnd.rar', 'application/x-rar', 'application/rar', 'application/x-rar-compressed'],
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3', 'application/octet-stream'],
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3', 'application/octet-stream'],
'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg',
'aif' => ['audio/x-aiff', 'audio/aiff'],
'mp2' => 'audio/mpeg',
'aif' => ['audio/x-aiff', 'audio/aiff'],
'aiff' => ['audio/x-aiff', 'audio/aiff'],
'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => ['audio/x-wav', 'audio/wave', 'audio/wav'],
'bmp' => [
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => ['audio/x-wav', 'audio/wave', 'audio/wav'],
'bmp' => [
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
@ -156,47 +152,48 @@ class Mimes
'application/x-bmp',
'application/x-win-bitmap',
],
'gif' => 'image/gif',
'jpg' => ['image/jpeg', 'image/pjpeg'],
'jpeg' => ['image/jpeg', 'image/pjpeg'],
'jpe' => ['image/jpeg', 'image/pjpeg'],
'jp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'j2k' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpf' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpg2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpx' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpm' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'png' => ['image/png', 'image/x-png'],
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'css' => ['text/css', 'text/plain'],
'html' => ['text/html', 'text/plain'],
'htm' => ['text/html', 'text/plain'],
'gif' => 'image/gif',
'jpg' => ['image/jpeg', 'image/pjpeg'],
'jpeg' => ['image/jpeg', 'image/pjpeg'],
'jpe' => ['image/jpeg', 'image/pjpeg'],
'jp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'j2k' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpf' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpg2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpx' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpm' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'png' => ['image/png', 'image/x-png'],
'webp' => 'image/webp',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'css' => ['text/css', 'text/plain'],
'html' => ['text/html', 'text/plain'],
'htm' => ['text/html', 'text/plain'],
'shtml' => ['text/html', 'text/plain'],
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => ['text/plain', 'text/x-log'],
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => ['application/xml', 'text/xml', 'text/plain'],
'xsl' => ['application/xml', 'text/xsl', 'text/xml'],
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => ['video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'],
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => ['text/plain', 'text/x-log'],
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => ['application/xml', 'text/xml', 'text/plain'],
'xsl' => ['application/xml', 'text/xsl', 'text/xml'],
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => ['video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'],
'movie' => 'video/x-sgi-movie',
'doc' => ['application/msword', 'application/vnd.ms-office'],
'docx' => [
'doc' => ['application/msword', 'application/vnd.ms-office'],
'docx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
'application/msword',
'application/x-zip',
],
'dot' => ['application/msword', 'application/vnd.ms-office'],
'dot' => ['application/msword', 'application/vnd.ms-office'],
'dotx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip',
@ -212,49 +209,49 @@ class Mimes
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'word' => ['application/msword', 'application/octet-stream'],
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => ['application/json', 'text/json'],
'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'],
'p10' => ['application/x-pkcs10', 'application/pkcs10'],
'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature',
'p7c' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7m' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'crt' => ['application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'],
'crl' => ['application/pkix-crl', 'application/pkcs-crl'],
'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7',
'cer' => ['application/pkix-cert', 'application/x-x509-ca-cert'],
'3g2' => 'video/3gpp2',
'3gp' => ['video/3gp', 'video/3gpp'],
'mp4' => 'video/mp4',
'm4a' => ['audio/m4a', 'audio/x-m4a', 'application/octet-stream'],
'f4v' => ['video/mp4', 'video/x-f4v'],
'flv' => 'video/x-flv',
'xl' => 'application/excel',
'eml' => 'message/rfc822',
'json' => ['application/json', 'text/json', 'text/plain'],
'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'],
'p10' => ['application/x-pkcs10', 'application/pkcs10'],
'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature',
'p7c' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7m' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'crt' => ['application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'],
'crl' => ['application/pkix-crl', 'application/pkcs-crl'],
'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7',
'cer' => ['application/pkix-cert', 'application/x-x509-ca-cert'],
'3g2' => 'video/3gpp2',
'3gp' => ['video/3gp', 'video/3gpp'],
'mp4' => 'video/mp4',
'm4a' => ['audio/m4a', 'audio/x-m4a', 'application/octet-stream'],
'f4v' => ['video/mp4', 'video/x-f4v'],
'flv' => 'video/x-flv',
'webm' => 'video/webm',
'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain',
'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain',
'xspf' => 'application/xspf+xml',
'vlc' => 'application/videolan',
'wmv' => ['video/x-ms-wmv', 'video/x-ms-asf'],
'au' => 'audio/x-au',
'ac3' => 'audio/ac3',
'vlc' => 'application/videolan',
'wmv' => ['video/x-ms-wmv', 'video/x-ms-asf'],
'au' => 'audio/x-au',
'ac3' => 'audio/ac3',
'flac' => 'audio/x-flac',
'ogg' => ['audio/ogg', 'video/ogg', 'application/ogg'],
'kmz' => ['application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'],
'kml' => ['application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'],
'ics' => 'text/calendar',
'ogg' => ['audio/ogg', 'video/ogg', 'application/ogg'],
'kmz' => ['application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'],
'kml' => ['application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'],
'ics' => 'text/calendar',
'ical' => 'text/calendar',
'zsh' => 'text/x-scriptzsh',
'zsh' => 'text/x-scriptzsh',
'7zip' => [
'application/x-compressed',
'application/x-zip-compressed',
@ -279,10 +276,11 @@ class Mimes
],
'svg' => ['image/svg+xml', 'image/svg', 'application/xml', 'text/xml'],
'vcf' => 'text/x-vcard',
'srt' => ['text/srt', 'text/plain', 'application/octet-stream'],
'srt' => ['application/x-subrip', 'text/srt', 'text/plain', 'application/octet-stream'],
'vtt' => ['text/vtt', 'text/plain'],
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle'],
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle', 'model/stl',
'application/octet-stream', ],
];
/**
@ -306,35 +304,28 @@ class Mimes
/**
* Attempts to determine the best file extension for a given mime type.
*
* @param string $proposedExtension - default extension (in case there is more than one with the same mime type)
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
* @return string|null The extension determined, or null if unable to match.
*/
public static function guessExtensionFromType(string $type, string $proposedExtension = ''): ?string
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null): ?string
{
$type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension));
$proposedExtension = trim(strtolower($proposedExtension ?? ''));
if ($proposedExtension !== '') {
if (array_key_exists($proposedExtension, static::$mimes) && in_array(
$type,
is_string(static::$mimes[$proposedExtension]) ? [
static::$mimes[$proposedExtension],
] : static::$mimes[$proposedExtension],
true
)) {
// The detected mime type matches with the proposed extension.
return $proposedExtension;
}
// An extension was proposed, but the media type does not match the mime type list.
return null;
if (
$proposedExtension !== ''
&& array_key_exists($proposedExtension, static::$mimes)
&& in_array($type, (array) static::$mimes[$proposedExtension], true)
) {
// The detected mime type matches with the proposed extension.
return $proposedExtension;
}
// Reverse check the mime type list if no extension was proposed.
// This search is order sensitive!
foreach (static::$mimes as $ext => $types) {
if ((is_string($types) && $types === $type) || (is_array($types) && in_array($type, $types, true))) {
if (in_array($type, (array) $types, true)) {
return $ext;
}
}

View file

@ -6,6 +6,12 @@ namespace Config;
use CodeIgniter\Modules\Modules as BaseModules;
/**
* Modules Configuration.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/
class Modules extends BaseModules
{
/**
@ -33,6 +39,29 @@ class Modules extends BaseModules
*/
public $discoverInComposer = true;
/**
* The Composer package list for Auto-Discovery
* This setting is optional.
*
* E.g.:
* [
* 'only' => [
* // List up all packages to auto-discover
* 'codeigniter4/shield',
* ],
* ]
* or
* [
* 'exclude' => [
* // List up packages to exclude.
* 'pestphp/pest',
* ],
* ]
*
* @var array{only?: list<string>, exclude?: list<string>}
*/
public $composerPackages = [];
/**
* --------------------------------------------------------------------------
* Auto-Discovery Rules
@ -43,7 +72,7 @@ class Modules extends BaseModules
*
* If it is not listed, only the base application elements will be used.
*
* @var string[]
* @var list<string>
*/
public $aliases = ['events', 'filters', 'registrars', 'routes', 'services'];
}

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.
*
* WARNING: Do not use these options when running the app in the Worker Mode.
*/
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

@ -21,13 +21,11 @@ class Pager extends BaseConfig
* and the desired group as $pagerGroup;
*
* @var array<string, string>
*
* @noRector Rector\Php55\Rector\String_\StringClassNameToClassConstantRector
*/
public $templates = [
'default_full' => 'App\Views\pager\default_full',
public array $templates = [
'default_full' => 'App\Views\pager\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
'default_head' => 'CodeIgniter\Pager\Views\default_head',
'default_head' => 'CodeIgniter\Pager\Views\default_head',
];
/**

View file

@ -5,16 +5,16 @@ declare(strict_types=1);
namespace Config;
/**
* Paths
*
* Holds the paths that are used by the system to locate the main directories, app, system, etc.
*
* Modifying these allows you to restructure your application, share a system folder between multiple applications, and
* more.
*
* All paths are relative to the project's root folder.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/
class Paths
{
/**
@ -26,7 +26,7 @@ class Paths
* the path if the folder is not in the same directory as this file.
*/
public string $systemDirectory =
__DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
__DIR__ . '/../../vendor/codeigniter4/framework/system';
/**
* ---------------------------------------------------------------
@ -35,8 +35,8 @@ class Paths
*
* If you want this front controller to use a different "app"
* folder than the default one you can set its name here. The folder
* can also be renamed or relocated anywhere on your getServer. If
* you do, use a full getServer path.
* can also be renamed or relocated anywhere on your server. If
* you do, use a full server path.
*
* @see http://codeigniter.com/user_guide/general/managing_apps.html
*/
@ -72,7 +72,7 @@ class Paths
* This variable must contain the name of the directory that
* contains the view files used by your application. By
* default this is in `app/Views`. This value
* is used when no value is provided to `Services::renderer()`.
* is used when no value is provided to `service('renderer')`.
*/
public string $viewDirectory = __DIR__ . '/../Views';
}

28
app/Config/Publisher.php Normal file
View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\Publisher as BasePublisher;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class to prevent abuse by injecting malicious files into a
* project.
*/
class Publisher extends BasePublisher
{
/**
* A list of allowed destinations with a (pseudo-)regex of allowed files for each destination. Attempts to publish
* to directories not in this list will result in a PublisherException. Files that do no fit the pattern will cause
* copy/merge to fail.
*
* @var array<string, string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
}

View file

@ -2,41 +2,19 @@
declare(strict_types=1);
namespace Config;
// Create a new instance of our RouteCollection class.
$routes = Services::routes();
// Load the system's routing file first, so that the app and ENVIRONMENT
// can override as needed.
if (file_exists(SYSTEMPATH . 'Config/Routes.php')) {
require SYSTEMPATH . 'Config/Routes.php';
}
/**
* --------------------------------------------------------------------
* Router Setup
* --------------------------------------------------------------------
*/
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Home');
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
$routes->setAutoRoute(false);
use CodeIgniter\Router\RouteCollection;
/**
* --------------------------------------------------------------------
* Placeholder definitions
* --------------------------------------------------------------------
*/
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
/** @var RouteCollection $routes */
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder(
'uuid',
'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}',
@ -48,700 +26,111 @@ $routes->addPlaceholder(
* --------------------------------------------------------------------
*/
$routes->get('manifest.webmanifest', 'WebmanifestController', [
'as' => 'webmanifest',
]);
$routes->get('themes/colors', 'ColorsController', [
'as' => 'themes-colors-css',
]);
// health check
$routes->get('/health', 'HomeController::health', [
'as' => 'health',
]);
// We get a performance increase by specifying the default
// route since we don't have to scan directories.
$routes->get('/', 'HomeController::index', [
$routes->get('/', 'HomeController', [
'as' => 'home',
]);
// Install Wizard route
$routes->group(config('App')->installGateway, function ($routes): void {
$routes->get('/', 'InstallController', [
'as' => 'install',
]);
$routes->post('instance-config', 'InstallController::attemptInstanceConfig', [
'as' => 'instance-config',
]);
$routes->post('database-config', 'InstallController::attemptDatabaseConfig', [
'as' => 'database-config',
]);
$routes->post('cache-config', 'InstallController::attemptCacheConfig', [
'as' => 'cache-config',
]);
$routes->post(
'create-superadmin',
'InstallController::attemptCreateSuperAdmin',
[
'as' => 'create-superadmin',
],
);
});
$routes->get('.well-known/platforms', 'Platform');
// Admin area
$routes->group(
config('App')
->adminGateway,
[
'namespace' => 'App\Controllers\Admin',
],
function ($routes): void {
$routes->get('/', 'HomeController', [
'as' => 'admin',
]);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PersonController', [
'as' => 'person-list',
'filter' => 'permission:person-list',
]);
$routes->get('new', 'PersonController::create', [
'as' => 'person-create',
'filter' => 'permission:person-create',
]);
$routes->post('new', 'PersonController::attemptCreate', [
'filter' => 'permission:person-create',
]);
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PersonController::view/$1', [
'as' => 'person-view',
'filter' => 'permission:person-view',
]);
$routes->get('edit', 'PersonController::edit/$1', [
'as' => 'person-edit',
'filter' => 'permission:person-edit',
]);
$routes->post('edit', 'PersonController::attemptEdit/$1', [
'filter' => 'permission:person-edit',
]);
$routes->add('delete', 'PersonController::delete/$1', [
'as' => 'person-delete',
'filter' => 'permission:person-delete',
]);
});
});
// Podcasts
$routes->group('podcasts', function ($routes): void {
$routes->get('/', 'PodcastController::list', [
'as' => 'podcast-list',
]);
$routes->get('new', 'PodcastController::create', [
'as' => 'podcast-create',
'filter' => 'permission:podcasts-create',
]);
$routes->post('new', 'PodcastController::attemptCreate', [
'filter' => 'permission:podcasts-create',
]);
$routes->get('import', 'PodcastImportController', [
'as' => 'podcast-import',
'filter' => 'permission:podcasts-import',
]);
$routes->post('import', 'PodcastImportController::attemptImport', [
'filter' => 'permission:podcasts-import',
]);
// Podcast
// Use ids in admin area to help permission and group lookups
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PodcastController::view/$1', [
'as' => 'podcast-view',
'filter' => 'permission:podcasts-view,podcast-view',
]);
$routes->get('edit', 'PodcastController::edit/$1', [
'as' => 'podcast-edit',
'filter' => 'permission:podcast-edit',
]);
$routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit',
]);
$routes->get('delete', 'PodcastController::delete/$1', [
'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete',
]);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PodcastPersonController/$1', [
'as' => 'podcast-person-manage',
'filter' => 'permission:podcast-edit',
]);
$routes->post(
'/',
'PodcastPersonController::attemptAdd/$1',
[
'filter' => 'permission:podcast-edit',
],
);
$routes->get(
'(:num)/remove',
'PodcastPersonController::remove/$1/$2',
[
'as' => 'podcast-person-remove',
'filter' => 'permission:podcast-edit',
],
);
});
$routes->group('analytics', function ($routes): void {
$routes->get('/', 'PodcastController::viewAnalytics/$1', [
'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view',
]);
$routes->get(
'webpages',
'PodcastController::viewAnalyticsWebpages/$1',
[
'as' => 'podcast-analytics-webpages',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'locations',
'PodcastController::viewAnalyticsLocations/$1',
[
'as' => 'podcast-analytics-locations',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'unique-listeners',
'PodcastController::viewAnalyticsUniqueListeners/$1',
[
'as' => 'podcast-analytics-unique-listeners',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'listening-time',
'PodcastController::viewAnalyticsListeningTime/$1',
[
'as' => 'podcast-analytics-listening-time',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'time-periods',
'PodcastController::viewAnalyticsTimePeriods/$1',
[
'as' => 'podcast-analytics-time-periods',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'players',
'PodcastController::viewAnalyticsPlayers/$1',
[
'as' => 'podcast-analytics-players',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
});
// Podcast episodes
$routes->group('episodes', function ($routes): void {
$routes->get('/', 'EpisodeController::list/$1', [
'as' => 'episode-list',
'filter' =>
'permission:episodes-list,podcast_episodes-list',
]);
$routes->get('new', 'EpisodeController::create/$1', [
'as' => 'episode-create',
'filter' => 'permission:podcast_episodes-create',
]);
$routes->post(
'new',
'EpisodeController::attemptCreate/$1',
[
'filter' => 'permission:podcast_episodes-create',
],
);
// Episode
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'EpisodeController::view/$1/$2', [
'as' => 'episode-view',
'filter' =>
'permission:episodes-view,podcast_episodes-view',
]);
$routes->get('edit', 'EpisodeController::edit/$1/$2', [
'as' => 'episode-edit',
'filter' => 'permission:podcast_episodes-edit',
]);
$routes->post(
'edit',
'EpisodeController::attemptEdit/$1/$2',
[
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'publish',
'EpisodeController::publish/$1/$2',
[
'as' => 'episode-publish',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish',
'EpisodeController::attemptPublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-edit',
'EpisodeController::publishEdit/$1/$2',
[
'as' => 'episode-publish_edit',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish-edit',
'EpisodeController::attemptPublishEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-cancel',
'EpisodeController::publishCancel/$1/$2',
[
'as' => 'episode-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'unpublish',
'EpisodeController::unpublish/$1/$2',
[
'as' => 'episode-unpublish',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'unpublish',
'EpisodeController::attemptUnpublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'delete',
'EpisodeController::delete/$1/$2',
[
'as' => 'episode-delete',
'filter' =>
'permission:podcast_episodes-delete',
],
);
$routes->get(
'transcript-delete',
'EpisodeController::transcriptDelete/$1/$2',
[
'as' => 'transcript-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'chapters-delete',
'EpisodeController::chaptersDelete/$1/$2',
[
'as' => 'chapters-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'soundbites',
'EpisodeController::soundbitesEdit/$1/$2',
[
'as' => 'soundbites-edit',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->post(
'soundbites',
'EpisodeController::soundbitesAttemptEdit/$1/$2',
[
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'soundbites/(:num)/delete',
'EpisodeController::soundbiteDelete/$1/$2/$3',
[
'as' => 'soundbite-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'embeddable-player',
'EpisodeController::embeddablePlayer/$1/$2',
[
'as' => 'embeddable-player-add',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'EpisodePersonController/$1/$2', [
'as' => 'episode-person-manage',
'filter' => 'permission:podcast_episodes-edit',
]);
$routes->post(
'/',
'EpisodePersonController::attemptAdd/$1/$2',
[
'filter' =>
'permission:podcast_episodes-edit',
],
);
$routes->get(
'(:num)/remove',
'EpisodePersonController::remove/$1/$2/$3',
[
'as' => 'episode-person-remove',
'filter' =>
'permission:podcast_episodes-edit',
],
);
});
});
});
// Podcast contributors
$routes->group('contributors', function ($routes): void {
$routes->get('/', 'ContributorController::list/$1', [
'as' => 'contributor-list',
'filter' =>
'permission:podcasts-view,podcast-manage_contributors',
]);
$routes->get('add', 'ContributorController::add/$1', [
'as' => 'contributor-add',
'filter' => 'permission:podcast-manage_contributors',
]);
$routes->post(
'add',
'ContributorController::attemptAdd/$1',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
// Contributor
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'ContributorController::view/$1/$2', [
'as' => 'contributor-view',
'filter' =>
'permission:podcast-manage_contributors',
]);
$routes->get(
'edit',
'ContributorController::edit/$1/$2',
[
'as' => 'contributor-edit',
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->post(
'edit',
'ContributorController::attemptEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->get(
'remove',
'ContributorController::remove/$1/$2',
[
'as' => 'contributor-remove',
'filter' =>
'permission:podcast-manage_contributors',
],
);
});
});
$routes->group('platforms', function ($routes): void {
$routes->get(
'/',
'PodcastPlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'social',
'PodcastPlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'funding',
'PodcastPlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->post(
'save/(:platformType)',
'PodcastPlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'(:slug)/podcast-platform-remove',
'PodcastPlatformController::removePodcastPlatform/$1/$2',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast-manage_platforms',
],
);
});
});
});
// Instance wide Fediverse config
$routes->group('fediverse', function ($routes): void {
$routes->get('/', 'FediverseController::dashboard', [
'as' => 'fediverse-dashboard',
]);
$routes->get(
'blocked-actors',
'FediverseController::blockedActors',
[
'as' => 'fediverse-blocked-actors',
'filter' => 'permission:fediverse-block_actors',
],
);
$routes->get(
'blocked-domains',
'FediverseController::blockedDomains',
[
'as' => 'fediverse-blocked-domains',
'filter' => 'permission:fediverse-block_domains',
],
);
});
// Pages
$routes->group('pages', function ($routes): void {
$routes->get('/', 'PageController::list', [
'as' => 'page-list',
]);
$routes->get('new', 'PageController::create', [
'as' => 'page-create',
'filter' => 'permission:pages-manage',
]);
$routes->post('new', 'PageController::attemptCreate', [
'filter' => 'permission:pages-manage',
]);
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PageController::view/$1', [
'as' => 'page-view',
]);
$routes->get('edit', 'PageController::edit/$1', [
'as' => 'page-edit',
'filter' => 'permission:pages-manage',
]);
$routes->post('edit', 'PageController::attemptEdit/$1', [
'filter' => 'permission:pages-manage',
]);
$routes->get('delete', 'PageController::delete/$1', [
'as' => 'page-delete',
'filter' => 'permission:pages-manage',
]);
});
});
// Users
$routes->group('users', function ($routes): void {
$routes->get('/', 'UserController::list', [
'as' => 'user-list',
'filter' => 'permission:users-list',
]);
$routes->get('new', 'UserController::create', [
'as' => 'user-create',
'filter' => 'permission:users-create',
]);
$routes->post('new', 'UserController::attemptCreate', [
'filter' => 'permission:users-create',
]);
// User
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'UserController::view/$1', [
'as' => 'user-view',
'filter' => 'permission:users-view',
]);
$routes->get('edit', 'UserController::edit/$1', [
'as' => 'user-edit',
'filter' => 'permission:users-manage_authorizations',
]);
$routes->post('edit', 'UserController::attemptEdit/$1', [
'filter' => 'permission:users-manage_authorizations',
]);
$routes->get('ban', 'UserController::ban/$1', [
'as' => 'user-ban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get('unban', 'UserController::unBan/$1', [
'as' => 'user-unban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get(
'force-pass-reset',
'UserController::forcePassReset/$1',
[
'as' => 'user-force_pass_reset',
'filter' => 'permission:users-force_pass_reset',
],
);
$routes->get('delete', 'UserController::delete/$1', [
'as' => 'user-delete',
'filter' => 'permission:users-delete',
]);
});
});
// My account
$routes->group('my-account', function ($routes): void {
$routes->get('/', 'MyAccountController', [
'as' => 'my-account',
]);
$routes->get(
'change-password',
'MyAccountController::changePassword/$1',
[
'as' => 'change-password',
],
);
$routes->post('change-password', 'MyAccountController::attemptChange/$1');
});
},
);
/**
* Overwriting Myth:auth routes file
*/
$routes->group(config('App')->authGateway, function ($routes): void {
// Login/out
$routes->get('login', 'AuthController::login', [
'as' => 'login',
]);
$routes->post('login', 'AuthController::attemptLogin');
$routes->get('logout', 'AuthController::logout', [
'as' => 'logout',
]);
// Registration
$routes->get('register', 'AuthController::register', [
'as' => 'register',
]);
$routes->post('register', 'AuthController::attemptRegister');
// Activation
$routes->get('activate-account', 'AuthController::activateAccount', [
'as' => 'activate-account',
]);
$routes->get(
'resend-activate-account',
'AuthController::resendActivateAccount',
[
'as' => 'resend-activate-account',
],
);
// Forgot/Resets
$routes->get('forgot', 'AuthController::forgotPassword', [
'as' => 'forgot',
]);
$routes->post('forgot', 'AuthController::attemptForgot');
$routes->get('reset-password', 'AuthController::resetPassword', [
'as' => 'reset-password',
]);
$routes->post('reset-password', 'AuthController::attemptReset');
});
service('auth')
->routes($routes);
// Podcast's Public routes
$routes->group('@(:podcastName)', function ($routes): void {
$routes->get('/', 'PodcastController::activity/$1', [
'as' => 'podcast-activity',
]);
// override default ActivityPub Library's actor route
$routes->group('@(:podcastHandle)', static function ($routes): void {
// override default Fediverse Library's actor route
$routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'PodcastController::activity/$1', [
'as' => 'actor',
'as' => 'podcast-activity',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'ActorController/$1',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'ActorController::index/$1',
],
'application/podcast-activity+json' => [
'namespace' => 'App\Controllers',
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::podcastActor/$1',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'ActorController/$1',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'ActorController::index/$1',
],
],
'filter' => 'allow-cors',
]);
$routes->get('manifest.webmanifest', 'WebmanifestController::podcastManifest/$1', [
'as' => 'podcast-webmanifest',
]);
$routes->get('links', 'PodcastController::links/$1', [
'as' => 'podcast-links',
]);
$routes->get('about', 'PodcastController::about/$1', [
'as' => 'podcast-about',
]);
$routes->options('episodes', 'ActivityPubController::preflight');
$routes->get('episodes', 'PodcastController::episodes/$1', [
'as' => 'podcast-episodes',
'as' => 'podcast-episodes',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1',
],
'application/podcast-activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1',
],
],
'filter' => 'allow-cors',
]);
$routes->group('episodes/(:slug)', function ($routes): void {
$routes->group('episodes/(:slug)', static function ($routes): void {
$routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'EpisodeController/$1/$2', [
'as' => 'episode',
$routes->get('/', 'EpisodeController::index/$1/$2', [
'as' => 'episode',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
],
'application/podcast-activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2',
],
],
'filter' => 'allow-cors',
]);
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
'as' => 'episode-activity',
]);
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
'as' => 'episode-chapters',
]);
$routes->get('transcript', 'EpisodeController::transcript/$1/$2', [
'as' => 'episode-transcript',
]);
$routes->options('comments', 'ActivityPubController::preflight');
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
'as' => 'episode-comments',
'as' => 'episode-comments',
'application/activity+json' => [
'controller-method' => 'EpisodeController::comments/$1/$2',
],
@ -751,6 +140,27 @@ $routes->group('@(:podcastName)', function ($routes): void {
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'controller-method' => 'EpisodeController::comments/$1/$2',
],
'filter' => 'allow-cors',
]);
$routes->options('comments/(:uuid)', 'ActivityPubController::preflight');
$routes->get('comments/(:uuid)', 'EpisodeCommentController::view/$1/$2/$3', [
'as' => 'episode-comment',
'application/activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/podcast-activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'filter' => 'allow-cors',
]);
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
'as' => 'episode-comment-replies',
]);
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::likeAction/$1/$2/$3', [
'as' => 'episode-comment-attempt-like',
]);
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json',
@ -758,144 +168,139 @@ $routes->group('@(:podcastName)', function ($routes): void {
$routes->get('oembed.xml', 'EpisodeController::oembedXML/$1/$2', [
'as' => 'episode-oembed-xml',
]);
$routes->group('embeddable-player', function ($routes): void {
$routes->get('/', 'EpisodeController::embeddablePlayer/$1/$2', [
'as' => 'embeddable-player',
$routes->group('embed', static function ($routes): void {
$routes->get('/', 'EpisodeController::embed/$1/$2', [
'as' => 'embed',
]);
$routes->get(
'(:embeddablePlayerTheme)',
'EpisodeController::embeddablePlayer/$1/$2/$3',
[
'as' => 'embeddable-player-theme',
],
);
$routes->get('(:embedTheme)', 'EpisodeController::embed/$1/$2/$3', [
'as' => 'embed-theme',
],);
});
});
$routes->head('feed.xml', 'FeedController/$1', [
'as' => 'podcast_feed',
$routes->head('feed.xml', 'FeedController::index/$1', [
'as' => 'podcast-rss-feed',
]);
$routes->get('feed.xml', 'FeedController/$1', [
'as' => 'podcast_feed',
$routes->get('feed.xml', 'FeedController::index/$1', [
'as' => 'podcast-rss-feed',
]);
$routes->head('feed', 'FeedController::index/$1');
$routes->get('feed', 'FeedController::index/$1');
});
// audio routes
$routes->head('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioController::index/$1/$2', [
'as' => 'episode-audio',
], );
$routes->get('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioController::index/$1/$2', [
'as' => 'episode-audio',
], );
// episode preview link
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
'as' => 'episode-preview',
]);
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
'as' => 'episode-preview-activity',
]);
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
'as' => 'episode-preview-chapters',
]);
$routes->get('/p/(:uuid)/transcript', 'EpisodePreviewController::transcript/$1', [
'as' => 'episode-preview-transcript',
]);
// Other pages
$routes->get('/credits', 'CreditsController', [
'as' => 'credits',
]);
$routes->get('/map', 'MapMarkerController', [
$routes->get('/map', 'MapController', [
'as' => 'map',
]);
$routes->get('/episodes-markers', 'MapMarkerController::getEpisodesMarkers', [
$routes->get('/episodes-markers', 'MapController::getEpisodesMarkers', [
'as' => 'episodes-markers',
]);
$routes->get('/pages/(:slug)', 'PageController/$1', [
$routes->get('/pages/(:slug)', 'PageController::index/$1', [
'as' => 'page',
]);
// interacting as an actor
$routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
'as' => 'interact-as-actor',
]);
/**
* Overwriting ActivityPub routes file
* Overwriting Fediverse routes file
*/
$routes->group('@(:podcastName)', function ($routes): void {
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
'as' => 'status-attempt-create',
'filter' => 'permission:podcast-manage_publications',
$routes->group('@(:podcastHandle)', static function ($routes): void {
$routes->post('posts/new', 'PostController::createAction/$1', [
'as' => 'post-attempt-create',
'filter' => 'permission:podcast$1.manage-publications',
]);
// Status
$routes->group('statuses/(:uuid)', function ($routes): void {
// Post
$routes->group('posts/(:uuid)', static function ($routes): void {
$routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'StatusController::view/$1/$2', [
'as' => 'status',
$routes->get('/', 'PostController::view/$1/$2', [
'as' => 'post',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'PostController::index/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'PostController::index/$2',
],
],
'filter' => 'allow-cors',
]);
$routes->options('replies', 'ActivityPubController::preflight');
$routes->get('replies', 'StatusController/$1/$2', [
'as' => 'status-replies',
$routes->get('replies', 'PostController::index/$1/$2', [
'as' => 'post-replies',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'PostController::replies/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2',
'namespace' => 'Modules\Fediverse\Controllers',
'controller-method' => 'PostController::replies/$2',
],
],
'filter' => 'allow-cors',
]);
// Actions
$routes->post('action', 'StatusController::attemptAction/$1/$2', [
'as' => 'status-attempt-action',
'filter' => 'permission:podcast-interact_as',
$routes->post('action', 'PostController::action/$1/$2', [
'as' => 'post-attempt-action',
'filter' => 'permission:podcast$1.interact-as',
]);
$routes->post(
'block-actor',
'StatusController::attemptBlockActor/$1/$2',
'PostController::blockActorAction/$1/$2',
[
'as' => 'status-attempt-block-actor',
'filter' => 'permission:fediverse-block_actors',
'as' => 'post-attempt-block-actor',
'filter' => 'permission:fediverse.manage-blocks',
],
);
$routes->post(
'block-domain',
'StatusController::attemptBlockDomain/$1/$2',
'PostController::blockDomainAction/$1/$2',
[
'as' => 'status-attempt-block-domain',
'filter' => 'permission:fediverse-block_domains',
'as' => 'post-attempt-block-domain',
'filter' => 'permission:fediverse.manage-blocks',
],
);
$routes->post('delete', 'StatusController::attemptDelete/$1/$2', [
'as' => 'status-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
$routes->post('delete', 'PostController::deleteAction/$1/$2', [
'as' => 'post-attempt-delete',
'filter' => 'permission:podcast$1.manage-publications',
]);
$routes->get(
'remote/(:statusAction)',
'StatusController::remoteAction/$1/$2/$3',
'remote/(:postAction)',
'PostController::remoteActionAction/$1/$2/$3',
[
'as' => 'status-remote-action',
'as' => 'post-remote-action',
],
);
});
$routes->get('follow', 'ActorController::follow/$1', [
$routes->get('follow', 'ActorController::followView/$1', [
'as' => 'follow',
]);
$routes->get('outbox', 'ActorController::outbox/$1', [
'as' => 'outbox',
'filter' => 'activity-pub:verify-activitystream',
'as' => 'outbox',
'filter' => 'fediverse:verify-activitystream',
]);
});
/*
* --------------------------------------------------------------------
* Additional Routing
* --------------------------------------------------------------------
*
* There will often be times that you need additional routing and you
* need it to be able to override any defaults in this file. Environment
* based routes is one such time. require() additional route files here
* to make that happen.
*
* You will have access to the $routes object within that file without
* needing to reload it.
*/
if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) {
require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php';
}

151
app/Config/Routing.php Normal file
View file

@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\Routing as BaseRouting;
/**
* Routing configuration
*/
class Routing extends BaseRouting
{
/**
* For Defined Routes.
* An array of files that contain route definitions.
* Route files are read in order, with the first match
* found taking precedence.
*
* Default: APPPATH . 'Config/Routes.php'
*
* @var list<string>
*/
public array $routeFiles = [
APPPATH . 'Config/Routes.php',
ROOTPATH . 'modules/Admin/Config/Routes.php',
ROOTPATH . 'modules/Analytics/Config/Routes.php',
ROOTPATH . 'modules/Api/Rest/V1/Config/Routes.php',
ROOTPATH . 'modules/Auth/Config/Routes.php',
ROOTPATH . 'modules/Fediverse/Config/Routes.php',
ROOTPATH . 'modules/Install/Config/Routes.php',
ROOTPATH . 'modules/Platforms/Config/Routes.php',
ROOTPATH . 'modules/PodcastImport/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
* namespace has been specified.
*
* Default: 'App\Controllers'
*/
public string $defaultNamespace = 'App\Controllers';
/**
* For Auto Routing.
* The default controller to use when no other controller has been
* specified.
*
* Default: 'Home'
*/
public string $defaultController = 'HomeController';
/**
* For Defined Routes and Auto Routing.
* The default method to call on the controller when no other
* method has been set in the route.
*
* Default: 'index'
*/
public string $defaultMethod = 'index';
/**
* For Auto Routing.
* Whether to translate dashes in URIs for controller/method to underscores.
* Primarily useful when using the auto-routing.
*
* Default: false
*/
public bool $translateURIDashes = false;
/**
* Sets the class/method that should be called if routing doesn't
* find a match. It can be the controller/method name like: Users::index
*
* This setting is passed to the Router class and handled there.
*
* If you want to use a closure, you will have to set it in the
* routes file by calling:
*
* $routes->set404Override(function() {
* // Do something here
* });
*
* Example:
* public $override404 = 'App\Errors::show404';
*/
public ?string $override404 = null;
/**
* If TRUE, the system will attempt to match the URI against
* Controllers by matching each segment against folders/files
* in APPPATH/Controllers, when a match wasn't found against
* defined routes.
*
* If FALSE, will stop searching and do NO automatic routing.
*/
public bool $autoRoute = false;
/**
* If TRUE, the system will look for attributes on controller
* class and methods that can run before and after the
* controller/method.
*
* If FALSE, will ignore any attributes.
*/
public bool $useControllerAttributes = true;
/**
* For Defined Routes.
* If TRUE, will enable the use of the 'prioritize' option
* when defining routes.
*
* Default: false
*/
public bool $prioritize = false;
/**
* 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.
* E.g.,
* [
* 'blog' => 'Acme\Blog\Controllers',
* ]
*
* @var array<string, string> [ uri_segment => namespace ]
*/
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 = true;
}

View file

@ -8,12 +8,32 @@ use CodeIgniter\Config\BaseConfig;
class Security extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CSRF Protection Method
* --------------------------------------------------------------------------
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var 'cookie'|'session'
*/
public string $csrfProtection = 'session';
/**
* --------------------------------------------------------------------------
* CSRF Token Randomization
* --------------------------------------------------------------------------
*
* Randomize the CSRF Token for added security.
*/
public bool $tokenRandomize = true;
/**
* --------------------------------------------------------------------------
* CSRF Token Name
* --------------------------------------------------------------------------
*
* Token name for Cross Site Request Forgery protection cookie.
* Token name for Cross Site Request Forgery protection.
*/
public string $tokenName = 'csrf_test_name';
@ -22,7 +42,7 @@ class Security extends BaseConfig
* CSRF Header Name
* --------------------------------------------------------------------------
*
* Token name for Cross Site Request Forgery protection cookie.
* Header name for Cross Site Request Forgery protection.
*/
public string $headerName = 'X-CSRF-TOKEN';
@ -31,7 +51,7 @@ class Security extends BaseConfig
* CSRF Cookie Name
* --------------------------------------------------------------------------
*
* Cookie name for Cross Site Request Forgery protection cookie.
* Cookie name for Cross Site Request Forgery protection.
*/
public string $cookieName = 'csrf_cookie_name';
@ -51,7 +71,7 @@ class Security extends BaseConfig
* CSRF Regenerate
* --------------------------------------------------------------------------
*
* Regenerate CSRF Token on every request.
* Regenerate CSRF Token on every submission.
*/
public bool $regenerate = true;
@ -60,7 +80,7 @@ class Security extends BaseConfig
* 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 = true;
public bool $redirect = (ENVIRONMENT === 'production');
}

View file

@ -4,20 +4,16 @@ declare(strict_types=1);
namespace Config;
use App\Authorization\FlatAuthorization;
use App\Authorization\GroupModel;
use App\Authorization\PermissionModel;
use App\Libraries\Breadcrumb;
use App\Libraries\HtmlHead;
use App\Libraries\Negotiate;
use App\Libraries\Router;
use App\Libraries\Vite;
use App\Models\UserModel;
use CodeIgniter\Config\BaseService;
use CodeIgniter\HTTP\Negotiate as CodeIgniterHTTPNegotiate;
use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\Model;
use CodeIgniter\Router\RouteCollectionInterface;
use Myth\Auth\Models\LoginModel;
use CodeIgniter\Router\Router as CodeIgniterRouter;
/**
* Services Configuration file.
@ -34,20 +30,18 @@ class Services extends BaseService
/**
* The Router class uses a RouteCollection's array of routes, and determines the correct Controller and Method to
* execute.
*
* @noRector PHPStan\Reflection\MissingMethodFromReflectionException
*/
public static function router(
?RouteCollectionInterface $routes = null,
?Request $request = null,
bool $getShared = true
): Router {
bool $getShared = true,
): CodeIgniterRouter {
if ($getShared) {
return static::getSharedInstance('router', $routes, $request);
}
$routes = $routes ?? static::routes();
$request = $request ?? static::request();
$routes ??= static::routes();
$request ??= static::request();
return new Router($routes, $request);
}
@ -55,82 +49,20 @@ class Services extends BaseService
/**
* The Negotiate class provides the content negotiation features for working the request to determine correct
* language, encoding, charset, and more.
*
* @noRector PHPStan\Reflection\MissingMethodFromReflectionException
*/
public static function negotiator(?RequestInterface $request = null, bool $getShared = true): Negotiate
{
public static function negotiator(
?RequestInterface $request = null,
bool $getShared = true,
): CodeIgniterHTTPNegotiate {
if ($getShared) {
return static::getSharedInstance('negotiator', $request);
}
$request = $request ?? static::request();
$request ??= static::request();
return new Negotiate($request);
}
/**
* @return mixed
*/
public static function authentication(
string $lib = 'local',
Model $userModel = null,
Model $loginModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance('authentication', $lib, $userModel, $loginModel);
}
// config() checks first in app/Config
$config = config('Auth');
$class = $config->authenticationLibs[$lib];
$instance = new $class($config);
if ($userModel === null) {
$userModel = new UserModel();
}
if ($loginModel === null) {
$loginModel = new LoginModel();
}
return $instance->setUserModel($userModel)
->setLoginModel($loginModel);
}
/**
* @return mixed
*/
public static function authorization(
Model $groupModel = null,
Model $permissionModel = null,
Model $userModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance('authorization', $groupModel, $permissionModel, $userModel);
}
if ($groupModel === null) {
$groupModel = new GroupModel();
}
if ($permissionModel === null) {
$permissionModel = new PermissionModel();
}
$instance = new FlatAuthorization($groupModel, $permissionModel);
if ($userModel === null) {
$userModel = new UserModel();
}
return $instance->setUserModel($userModel);
}
public static function breadcrumb(bool $getShared = true): Breadcrumb
{
if ($getShared) {
@ -140,12 +72,12 @@ class Services extends BaseService
return new Breadcrumb();
}
public static function vite(bool $getShared = true): Vite
public static function html_head(bool $getShared = true): HtmlHead
{
if ($getShared) {
return self::getSharedInstance('vite');
return self::getSharedInstance('html_head');
}
return new Vite();
return new HtmlHead();
}
}

130
app/Config/Session.php Normal file
View file

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
class Session extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Session Driver
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\ArrayHandler` (for testing)
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*
* @var class-string<BaseHandler>
*/
public string $driver = FileHandler::class;
/**
* --------------------------------------------------------------------------
* Session Cookie Name
* --------------------------------------------------------------------------
*
* The session cookie name, must contain only [0-9a-z_-] characters
*/
public string $cookieName = 'ci_session';
/**
* --------------------------------------------------------------------------
* Session Expiration
* --------------------------------------------------------------------------
*
* The number of SECONDS you want the session to last.
* Setting to 0 (zero) means expire when the browser is closed.
*/
public int $expiration = 7200;
/**
* --------------------------------------------------------------------------
* Session Save Path
* --------------------------------------------------------------------------
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*/
public string $savePath = WRITEPATH . 'session';
/**
* --------------------------------------------------------------------------
* Session Match IP
* --------------------------------------------------------------------------
*
* Whether to match the user's IP address when reading the session data.
*
* WARNING: If you're using the database driver, don't forget to update
* your session table's PRIMARY KEY when changing this setting.
*/
public bool $matchIP = false;
/**
* --------------------------------------------------------------------------
* Session Time to Update
* --------------------------------------------------------------------------
*
* How many seconds between CI regenerating the session ID.
*/
public int $timeToUpdate = 300;
/**
* --------------------------------------------------------------------------
* Session Regenerate Destroy
* --------------------------------------------------------------------------
*
* Whether to destroy session data associated with the old session ID
* when auto-regenerating the session ID. When set to FALSE, the data
* will be later deleted by the garbage collector.
*/
public bool $regenerateDestroy = false;
/**
* --------------------------------------------------------------------------
* Session Database Group
* --------------------------------------------------------------------------
*
* DB Group for the database session.
*/
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;
}

59
app/Config/Tasks.php Normal file
View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Tasks\Scheduler;
class Tasks extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Should performance metrics be logged
* --------------------------------------------------------------------------
*
* If true, will log the time it takes for each task to run.
* Requires the settings table to have been created previously.
*/
public bool $logPerformance = false;
/**
* --------------------------------------------------------------------------
* Maximum performance logs
* --------------------------------------------------------------------------
*
* The maximum number of logs that should be saved per Task.
* Lower numbers reduced the amount of database required to
* store the logs.
*/
public int $maxLogsPerTask = 10;
/**
* Register any tasks within this method for the application.
* Called by the TaskRunner.
*/
public function init(Scheduler $schedule): void
{
$schedule->command('fediverse:broadcast')
->everyMinute()
->named('fediverse-broadcast');
$schedule->command('websub:publish')
->everyMinute()
->named('websub-publish');
$schedule->command('video-clips:generate')
->everyMinute()
->named('video-clips-generate');
$schedule->command('podcast:import')
->everyMinute()
->named('podcast-import');
$schedule->command('episodes:compute-downloads')
->everyHour()
->named('episodes:compute-downloads');
}
}

View file

@ -33,7 +33,7 @@ class Toolbar extends BaseConfig
* List of toolbar collectors that will be called when Debug Toolbar
* fires up and collects data from.
*
* @var string[]
* @var list<class-string>
*/
public array $collectors = [
Timers::class,
@ -46,6 +46,16 @@ class Toolbar extends BaseConfig
Events::class,
];
/**
* --------------------------------------------------------------------------
* Collect Var Data
* --------------------------------------------------------------------------
*
* 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.
*/
public bool $collectVarData = true;
/**
* --------------------------------------------------------------------------
* Max History
@ -80,4 +90,56 @@ class Toolbar extends BaseConfig
* `$maxQueries` defines the maximum amount of queries that will be stored.
*/
public int $maxQueries = 100;
/**
* --------------------------------------------------------------------------
* Watched Directories
* --------------------------------------------------------------------------
*
* Contains an array of directories that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
* We restrict the values to keep performance as high as possible.
*
* NOTE: The ROOTPATH will be prepended to all values.
*
* @var list<string>
*/
public array $watchedDirectories = ['app', 'modules', 'themes'];
/**
* --------------------------------------------------------------------------
* Watched File Extensions
* --------------------------------------------------------------------------
*
* 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.
*
* @var list<string>
*/
public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env'];
/**
* --------------------------------------------------------------------------
* Ignored HTTP Headers
* --------------------------------------------------------------------------
*
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
* HTML response. This is correct for full page loads, but it breaks requests
* that expect only a clean HTML fragment.
*
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
* manage navigation on the client side. Injecting the Debug Toolbar into their
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
* (such as infinite loops or "Maximum call stack size exceeded").
*
* Any request containing one of the following headers is treated as a
* client-managed or partial request, and the Debug Toolbar injection is skipped.
*
* @var array<string, string|null>
*/
public array $disableOnHeaders = [
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
'HX-Request' => 'true', // HTMX requests
'X-Up-Version' => null, // Unpoly partial requests
];
}

View file

@ -27,47 +27,47 @@ class UserAgents extends BaseConfig
*/
public array $platforms = [
'windows nt 10.0' => 'Windows 10',
'windows nt 6.3' => 'Windows 8.1',
'windows nt 6.2' => 'Windows 8',
'windows nt 6.1' => 'Windows 7',
'windows nt 6.0' => 'Windows Vista',
'windows nt 5.2' => 'Windows 2003',
'windows nt 5.1' => 'Windows XP',
'windows nt 5.0' => 'Windows 2000',
'windows nt 4.0' => 'Windows NT 4.0',
'winnt4.0' => 'Windows NT 4.0',
'winnt 4.0' => 'Windows NT',
'winnt' => 'Windows NT',
'windows 98' => 'Windows 98',
'win98' => 'Windows 98',
'windows 95' => 'Windows 95',
'win95' => 'Windows 95',
'windows phone' => 'Windows Phone',
'windows' => 'Unknown Windows OS',
'android' => 'Android',
'blackberry' => 'BlackBerry',
'iphone' => 'iOS',
'ipad' => 'iOS',
'ipod' => 'iOS',
'os x' => 'Mac OS X',
'ppc mac' => 'Power PC Mac',
'freebsd' => 'FreeBSD',
'ppc' => 'Macintosh',
'linux' => 'Linux',
'debian' => 'Debian',
'sunos' => 'Sun Solaris',
'beos' => 'BeOS',
'apachebench' => 'ApacheBench',
'aix' => 'AIX',
'irix' => 'Irix',
'osf' => 'DEC OSF',
'hp-ux' => 'HP-UX',
'netbsd' => 'NetBSD',
'bsdi' => 'BSDi',
'openbsd' => 'OpenBSD',
'gnu' => 'GNU/Linux',
'unix' => 'Unknown Unix OS',
'symbian' => 'Symbian OS',
'windows nt 6.3' => 'Windows 8.1',
'windows nt 6.2' => 'Windows 8',
'windows nt 6.1' => 'Windows 7',
'windows nt 6.0' => 'Windows Vista',
'windows nt 5.2' => 'Windows 2003',
'windows nt 5.1' => 'Windows XP',
'windows nt 5.0' => 'Windows 2000',
'windows nt 4.0' => 'Windows NT 4.0',
'winnt4.0' => 'Windows NT 4.0',
'winnt 4.0' => 'Windows NT',
'winnt' => 'Windows NT',
'windows 98' => 'Windows 98',
'win98' => 'Windows 98',
'windows 95' => 'Windows 95',
'win95' => 'Windows 95',
'windows phone' => 'Windows Phone',
'windows' => 'Unknown Windows OS',
'android' => 'Android',
'blackberry' => 'BlackBerry',
'iphone' => 'iOS',
'ipad' => 'iOS',
'ipod' => 'iOS',
'os x' => 'Mac OS X',
'ppc mac' => 'Power PC Mac',
'freebsd' => 'FreeBSD',
'ppc' => 'Macintosh',
'linux' => 'Linux',
'debian' => 'Debian',
'sunos' => 'Sun Solaris',
'beos' => 'BeOS',
'apachebench' => 'ApacheBench',
'aix' => 'AIX',
'irix' => 'Irix',
'osf' => 'DEC OSF',
'hp-ux' => 'HP-UX',
'netbsd' => 'NetBSD',
'bsdi' => 'BSDi',
'openbsd' => 'OpenBSD',
'gnu' => 'GNU/Linux',
'unix' => 'Unknown Unix OS',
'symbian' => 'Symbian OS',
];
/**
@ -81,37 +81,37 @@ class UserAgents extends BaseConfig
* @var array<string, string>
*/
public array $browsers = [
'OPR' => 'Opera',
'Flock' => 'Flock',
'Edge' => 'Spartan',
'Edg' => 'Edge',
'OPR' => 'Opera',
'Flock' => 'Flock',
'Edge' => 'Spartan',
'Edg' => 'Edge',
'Chrome' => 'Chrome',
// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
'Opera.*?Version' => 'Opera',
'Opera' => 'Opera',
'MSIE' => 'Internet Explorer',
'Opera.*?Version' => 'Opera',
'Opera' => 'Opera',
'MSIE' => 'Internet Explorer',
'Internet Explorer' => 'Internet Explorer',
'Trident.* rv' => 'Internet Explorer',
'Shiira' => 'Shiira',
'Firefox' => 'Firefox',
'Chimera' => 'Chimera',
'Phoenix' => 'Phoenix',
'Firebird' => 'Firebird',
'Camino' => 'Camino',
'Netscape' => 'Netscape',
'OmniWeb' => 'OmniWeb',
'Safari' => 'Safari',
'Mozilla' => 'Mozilla',
'Konqueror' => 'Konqueror',
'icab' => 'iCab',
'Lynx' => 'Lynx',
'Links' => 'Links',
'hotjava' => 'HotJava',
'amaya' => 'Amaya',
'IBrowse' => 'IBrowse',
'Maxthon' => 'Maxthon',
'Ubuntu' => 'Ubuntu Web Browser',
'Vivaldi' => 'Vivaldi',
'Trident.* rv' => 'Internet Explorer',
'Shiira' => 'Shiira',
'Firefox' => 'Firefox',
'Chimera' => 'Chimera',
'Phoenix' => 'Phoenix',
'Firebird' => 'Firebird',
'Camino' => 'Camino',
'Netscape' => 'Netscape',
'OmniWeb' => 'OmniWeb',
'Safari' => 'Safari',
'Mozilla' => 'Mozilla',
'Konqueror' => 'Konqueror',
'icab' => 'iCab',
'Lynx' => 'Lynx',
'Links' => 'Links',
'hotjava' => 'HotJava',
'amaya' => 'Amaya',
'IBrowse' => 'IBrowse',
'Maxthon' => 'Maxthon',
'Ubuntu' => 'Ubuntu Web Browser',
'Vivaldi' => 'Vivaldi',
];
/**
@ -139,86 +139,86 @@ class UserAgents extends BaseConfig
// 'motorola' => 'Motorola'
// Phones and Manufacturers
'motorola' => 'Motorola',
'nokia' => 'Nokia',
'palm' => 'Palm',
'iphone' => 'Apple iPhone',
'ipad' => 'iPad',
'ipod' => 'Apple iPod Touch',
'sony' => 'Sony Ericsson',
'ericsson' => 'Sony Ericsson',
'blackberry' => 'BlackBerry',
'cocoon' => 'O2 Cocoon',
'blazer' => 'Treo',
'lg' => 'LG',
'amoi' => 'Amoi',
'xda' => 'XDA',
'mda' => 'MDA',
'vario' => 'Vario',
'htc' => 'HTC',
'samsung' => 'Samsung',
'sharp' => 'Sharp',
'sie-' => 'Siemens',
'alcatel' => 'Alcatel',
'benq' => 'BenQ',
'ipaq' => 'HP iPaq',
'mot-' => 'Motorola',
'motorola' => 'Motorola',
'nokia' => 'Nokia',
'palm' => 'Palm',
'iphone' => 'Apple iPhone',
'ipad' => 'iPad',
'ipod' => 'Apple iPod Touch',
'sony' => 'Sony Ericsson',
'ericsson' => 'Sony Ericsson',
'blackberry' => 'BlackBerry',
'cocoon' => 'O2 Cocoon',
'blazer' => 'Treo',
'lg' => 'LG',
'amoi' => 'Amoi',
'xda' => 'XDA',
'mda' => 'MDA',
'vario' => 'Vario',
'htc' => 'HTC',
'samsung' => 'Samsung',
'sharp' => 'Sharp',
'sie-' => 'Siemens',
'alcatel' => 'Alcatel',
'benq' => 'BenQ',
'ipaq' => 'HP iPaq',
'mot-' => 'Motorola',
'playstation portable' => 'PlayStation Portable',
'playstation 3' => 'PlayStation 3',
'playstation vita' => 'PlayStation Vita',
'hiptop' => 'Danger Hiptop',
'nec-' => 'NEC',
'panasonic' => 'Panasonic',
'philips' => 'Philips',
'sagem' => 'Sagem',
'sanyo' => 'Sanyo',
'spv' => 'SPV',
'zte' => 'ZTE',
'sendo' => 'Sendo',
'nintendo dsi' => 'Nintendo DSi',
'nintendo ds' => 'Nintendo DS',
'nintendo 3ds' => 'Nintendo 3DS',
'wii' => 'Nintendo Wii',
'open web' => 'Open Web',
'openweb' => 'OpenWeb',
'playstation 3' => 'PlayStation 3',
'playstation vita' => 'PlayStation Vita',
'hiptop' => 'Danger Hiptop',
'nec-' => 'NEC',
'panasonic' => 'Panasonic',
'philips' => 'Philips',
'sagem' => 'Sagem',
'sanyo' => 'Sanyo',
'spv' => 'SPV',
'zte' => 'ZTE',
'sendo' => 'Sendo',
'nintendo dsi' => 'Nintendo DSi',
'nintendo ds' => 'Nintendo DS',
'nintendo 3ds' => 'Nintendo 3DS',
'wii' => 'Nintendo Wii',
'open web' => 'Open Web',
'openweb' => 'OpenWeb',
// Operating Systems
'android' => 'Android',
'symbian' => 'Symbian',
'SymbianOS' => 'SymbianOS',
'elaine' => 'Palm',
'series60' => 'Symbian S60',
'android' => 'Android',
'symbian' => 'Symbian',
'SymbianOS' => 'SymbianOS',
'elaine' => 'Palm',
'series60' => 'Symbian S60',
'windows ce' => 'Windows CE',
// Browsers
'obigo' => 'Obigo',
'netfront' => 'Netfront Browser',
'openwave' => 'Openwave Browser',
'obigo' => 'Obigo',
'netfront' => 'Netfront Browser',
'openwave' => 'Openwave Browser',
'mobilexplorer' => 'Mobile Explorer',
'operamini' => 'Opera Mini',
'opera mini' => 'Opera Mini',
'opera mobi' => 'Opera Mobile',
'fennec' => 'Firefox Mobile',
'operamini' => 'Opera Mini',
'opera mini' => 'Opera Mini',
'opera mobi' => 'Opera Mobile',
'fennec' => 'Firefox Mobile',
// Other
'digital paths' => 'Digital Paths',
'avantgo' => 'AvantGo',
'xiino' => 'Xiino',
'novarra' => 'Novarra Transcoder',
'vodafone' => 'Vodafone',
'docomo' => 'NTT DoCoMo',
'o2' => 'O2',
'avantgo' => 'AvantGo',
'xiino' => 'Xiino',
'novarra' => 'Novarra Transcoder',
'vodafone' => 'Vodafone',
'docomo' => 'NTT DoCoMo',
'o2' => 'O2',
// Fallback
'mobile' => 'Generic Mobile',
'wireless' => 'Generic Mobile',
'j2me' => 'Generic Mobile',
'midp' => 'Generic Mobile',
'cldc' => 'Generic Mobile',
'up.link' => 'Generic Mobile',
'mobile' => 'Generic Mobile',
'wireless' => 'Generic Mobile',
'j2me' => 'Generic Mobile',
'midp' => 'Generic Mobile',
'cldc' => 'Generic Mobile',
'up.link' => 'Generic Mobile',
'up.browser' => 'Generic Mobile',
'smartphone' => 'Generic Mobile',
'cellphone' => 'Generic Mobile',
'cellphone' => 'Generic Mobile',
];
/**
@ -231,24 +231,34 @@ class UserAgents extends BaseConfig
* @var array<string, string>
*/
public array $robots = [
'googlebot' => 'Googlebot',
'msnbot' => 'MSNBot',
'baiduspider' => 'Baiduspider',
'bingbot' => 'Bing',
'slurp' => 'Inktomi Slurp',
'yahoo' => 'Yahoo',
'ask jeeves' => 'Ask Jeeves',
'fastcrawler' => 'FastCrawler',
'infoseek' => 'InfoSeek Robot 1.0',
'lycos' => 'Lycos',
'yandex' => 'YandexBot',
'googlebot' => 'Googlebot',
'google-pagerenderer' => 'Google Page Renderer',
'google-read-aloud' => 'Google Read Aloud',
'google-safety' => 'Google Safety Bot',
'msnbot' => 'MSNBot',
'baiduspider' => 'Baiduspider',
'bingbot' => 'Bing',
'bingpreview' => 'BingPreview',
'slurp' => 'Inktomi Slurp',
'yahoo' => 'Yahoo',
'ask jeeves' => 'Ask Jeeves',
'fastcrawler' => 'FastCrawler',
'infoseek' => 'InfoSeek Robot 1.0',
'lycos' => 'Lycos',
'yandex' => 'YandexBot',
'mediapartners-google' => 'MediaPartners Google',
'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
'adsbot-google' => 'AdsBot Google',
'feedfetcher-google' => 'Feedfetcher Google',
'curious george' => 'Curious George',
'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot',
'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
'adsbot-google' => 'AdsBot Google',
'feedfetcher-google' => 'Feedfetcher Google',
'curious george' => 'Curious George',
'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot',
'duckduckbot' => 'DuckDuckBot',
'sogou' => 'Sogou Spider',
'exabot' => 'Exabot',
'bot' => 'Generic Bot',
'crawler' => 'Generic Crawler',
'spider' => 'Generic Spider',
];
}

View file

@ -5,28 +5,27 @@ declare(strict_types=1);
namespace Config;
use App\Validation\FileRules as AppFileRules;
use App\Validation\Rules as AppRules;
use CodeIgniter\Validation\CreditCardRules;
use CodeIgniter\Validation\FileRules;
use CodeIgniter\Validation\FormatRules;
use CodeIgniter\Validation\Rules;
use Myth\Auth\Authentication\Passwords\ValidationRules as PasswordRules;
use App\Validation\OtherRules;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Validation\StrictRules\CreditCardRules;
use CodeIgniter\Validation\StrictRules\FileRules;
use CodeIgniter\Validation\StrictRules\FormatRules;
use CodeIgniter\Validation\StrictRules\Rules;
class Validation
class Validation extends BaseConfig
{
/**
* Stores the classes that contain the rules that are available.
*
* @var string[]
* @var list<string>
*/
public array $ruleSets = [
Rules::class,
FormatRules::class,
FileRules::class,
CreditCardRules::class,
AppRules::class,
AppFileRules::class,
PasswordRules::class,
OtherRules::class,
];
/**
@ -35,7 +34,7 @@ class Validation
* @var array<string, string>
*/
public array $templates = [
'list' => 'CodeIgniter\Validation\Views\list',
'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single',
];
}

View file

@ -5,7 +5,13 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\View as BaseView;
use CodeIgniter\View\ViewDecoratorInterface;
use ViewComponents\Decorator;
/**
* @phpstan-type parser_callable (callable(mixed): mixed)
* @phpstan-type parser_callable_string (callable(mixed): mixed)&string
*/
class View extends BaseView
{
/**
@ -25,7 +31,8 @@ class View extends BaseView
*
* Examples: { title|esc(js) } { created_on|date(Y-m-d)|esc(attr) }
*
* @var string[]
* @var array<string, string>
* @phpstan-var array<string, parser_callable_string>
*/
public $filters = [];
@ -33,7 +40,35 @@ class View extends BaseView
* 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.
*
* @var string[]
* @var array<string, callable|list<string>|string>
* @phpstan-var array<string, list<parser_callable_string>|parser_callable_string|parser_callable>
*/
public $plugins = [];
/**
* View Decorators are class methods that will be run in sequence to have a chance to alter the generated output
* just prior to caching the results.
*
* All classes must implement CodeIgniter\View\ViewDecoratorInterface
*
* @var list<class-string<ViewDecoratorInterface>>
*/
public array $decorators = [Decorator::class];
/**
* Subdirectory within app/Views for namespaced view overrides.
*
* Namespaced views will be searched in:
*
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
*
* This allows application-level overrides for package or module views
* without modifying vendor source files.
*
* Examples:
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
*/
public string $appOverridesFolder = 'overrides';
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Config;
use ViewComponents\Config\ViewComponents as ViewComponentsConfig;
class ViewComponents extends ViewComponentsConfig
{
/**
* @var string[]
*/
public array $lookupPaths = [
ROOTPATH . 'themes/cp_app/',
ROOTPATH . 'themes/cp_admin/',
ROOTPATH . 'themes/cp_auth/',
ROOTPATH . 'themes/cp_install/',
];
}

42
app/Config/Vite.php Normal file
View file

@ -0,0 +1,42 @@
<?php
// app/Config/Vite.php
declare(strict_types=1);
namespace Config;
use CodeIgniterVite\Config\Vite as ViteConfig;
class Vite extends ViteConfig
{
public function __construct()
{
parent::__construct();
$adminGateway = config('Admin')
->gateway;
$installGateway = config('Install')
->gateway;
$this->routesAssets = [
[
'routes' => ['*'],
'exclude' => [$adminGateway . '*', $installGateway . '*'],
'assets' => ['styles/site.css', 'js/app.ts', 'js/podcast.ts', 'js/audio-player.ts'],
],
[
'routes' => ['/map'],
'assets' => ['js/map.ts'],
],
[
'routes' => ['/' . $adminGateway . '*'],
'assets' => ['styles/admin.css', 'js/admin.ts', 'js/admin-audio-player.ts'],
],
[
'routes' => [$installGateway . '*'],
'assets' => ['styles/install.css'],
],
];
}
}

52
app/Config/WorkerMode.php Normal file
View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Config;
/**
* This configuration controls how CodeIgniter behaves when running
* in worker mode (with FrankenPHP).
*/
class WorkerMode
{
/**
* Persistent Services
*
* List of service names that should persist across requests.
* These services will NOT be reset between requests.
*
* Services not in this list will be reset for each request to prevent
* state leakage.
*
* Recommended persistent services:
* - `autoloader`: PSR-4 autoloading configuration
* - `locator`: File locator
* - `exceptions`: Exception handler
* - `commands`: CLI commands registry
* - `codeigniter`: Main application instance
* - `superglobals`: Superglobals wrapper
* - `routes`: Router configuration
* - `cache`: Cache instance
*
* @var list<string>
*/
public array $persistentServices = [
'autoloader',
'locator',
'exceptions',
'commands',
'codeigniter',
'superglobals',
'routes',
'cache',
];
/**
* Force Garbage Collection
*
* Whether to force garbage collection after each request.
* Helps prevent memory leaks at a small performance cost.
*/
public bool $forceGarbageCollection = true;
}

View file

@ -3,28 +3,15 @@
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\Response;
use Modules\Fediverse\Controllers\ActivityPubController as FediverseActivityPubController;
class ActivityPubController extends Controller
class ActivityPubController extends FediverseActivityPubController
{
/**
* @noRector ReturnTypeDeclarationRector
*/
public function preflight(): Response
{
return $this->response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
->setHeader('Access-Control-Max-Age', '86400')
->setHeader('Cache-Control', 'public, max-age=86400')
->setStatusCode(200);
}
}

View file

@ -3,46 +3,37 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Controllers\ActorController as ActivityPubActorController;
use Analytics\AnalyticsTrait;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\ActorController as FediverseActorController;
class ActorController extends ActivityPubActorController
class ActorController extends FediverseActorController
{
use AnalyticsTrait;
/**
* @var string[]
* @var list<string>
*/
protected $helpers = ['auth', 'svg', 'components', 'misc'];
protected $helpers = ['svg', 'components', 'misc', 'seo'];
public function follow(): string
public function followView(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
// @phpstan-ignore-next-line
$this->registerPodcastWebpageHit($this->actor->podcast->id);
}
// @phpstan-ignore-next-line
$this->registerPodcastWebpageHit($this->actor->podcast->id);
$cacheName = "page_podcast-{$this->actor->username}_follow";
if (! ($cachedView = cache($cacheName))) {
helper(['form', 'components', 'svg']);
$data = [
'actor' => $this->actor,
];
helper(['form', 'components', 'svg']);
// @phpstan-ignore-next-line
set_follow_metatags($this->actor);
$data = [
'actor' => $this->actor,
];
return view('podcast/follow', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
return view('podcast/follow', $data);
}
}

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Class BaseController
*
* 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
*
* For security be sure to declare any new methods as protected or private.
*/
class BaseController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend BaseController.
*
* @var string[]
*/
protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc'];
/**
* Constructor.
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
}
}

View file

@ -1,199 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Authorization\GroupModel;
use App\Entities\Podcast;
use App\Entities\User;
use App\Models\PodcastModel;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Exception;
class ContributorController extends BaseController
{
protected Podcast $podcast;
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound();
}
if (($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) <= 1) {
return $this->{$method}();
}
if (($this->user = (new UserModel())->getPodcastContributor((int) $params[1], (int) $params[0])) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/contributor/list', $data);
}
public function view(): string
{
$data = [
'contributor' => (new UserModel())->getPodcastContributor($this->user->id, $this->podcast->id),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('admin/contributor/view', $data);
}
public function add(): string
{
helper('form');
$users = (new UserModel())->findAll();
$userOptions = array_reduce(
$users,
function ($result, $user) {
$result[$user->id] = $user->username;
return $result;
},
[],
);
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'userOptions' => $userOptions,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/contributor/add', $data);
}
public function attemptAdd(): RedirectResponse
{
try {
(new PodcastModel())->addPodcastContributor(
(int) $this->request->getPost('user'),
$this->podcast->id,
(int) $this->request->getPost('role'),
);
} catch (Exception) {
return redirect()
->back()
->withInput()
->with('errors', [lang('Contributor.messages.alreadyAddedError')]);
}
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'user' => $this->user,
'contributorGroupId' => (new PodcastModel())->getContributorGroupId(
$this->user->id,
$this->podcast->id,
),
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('admin/contributor/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
(new PodcastModel())->updatePodcastContributor(
$this->user->id,
$this->podcast->id,
(int) $this->request->getPost('role'),
);
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function remove(): RedirectResponse
{
if ($this->podcast->created_by === $this->user->id) {
return redirect()
->back()
->with('errors', [lang('Contributor.messages.removeOwnerContributorError')]);
}
$podcastModel = new PodcastModel();
if (
! $podcastModel->removePodcastContributor($this->user->id, $this->podcast->id)
) {
return redirect()
->back()
->with('errors', $podcastModel->errors());
}
return redirect()
->back()
->with(
'message',
lang('Contributor.messages.removeContributorSuccess', [
'username' => $this->user->username,
'podcastTitle' => $this->podcast->title,
]),
);
}
}

View file

@ -1,785 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Status;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
class EpisodeController extends BaseController
{
protected Podcast $podcast;
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (
($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) > 1) {
if (
! ($episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
}
return $this->{$method}(...$params);
}
public function list(): string
{
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->orderBy('created_at', 'desc');
$data = [
'podcast' => $this->podcast,
'episodes' => $episodes->paginate(10),
'pager' => $episodes->pager,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/episode/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/view', $data);
}
public function create(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/episode/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'audio_file' => 'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'transcript_file' =>
'ext_in[transcript,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters,json]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$image = null;
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$image = new Image($imageFile);
}
$newEpisode = new Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'guid' => null,
'audio_file' => $this->request->getFile('audio_file'),
'description_markdown' => $this->request->getPost('description'),
'image' => $image,
'location' => $this->request->getPost('location_name') === '' ? null : new Location($this->request->getPost(
'location_name'
)),
'transcript' => $this->request->getFile('transcript'),
'chapters' => $this->request->getFile('chapters'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'number' => $this->request->getPost('episode_number')
? $this->request->getPost('episode_number')
: null,
'season_number' => $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null,
'type' => $this->request->getPost('type'),
'is_blocked' => $this->request->getPost('block') === 'yes',
'custom_rss_string' => $this->request->getPost('custom_rss'),
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => null,
]);
$transcriptChoice = $this->request->getPost('transcript-choice');
if (
$transcriptChoice === 'upload-file'
&& ($transcriptFile = $this->request->getFile('transcript_file'))
&& $transcriptFile->isValid()
) {
$newEpisode->transcript_file = $transcriptFile;
} elseif ($transcriptChoice === 'remote-url') {
$newEpisode->transcript_file_remote_url = $this->request->getPost(
'transcript_file_remote_url'
) === '' ? null : $this->request->getPost('transcript_file_remote_url');
}
$chaptersChoice = $this->request->getPost('chapters-choice');
if (
$chaptersChoice === 'upload-file'
&& ($chaptersFile = $this->request->getFile('chapters_file'))
&& $chaptersFile->isValid()
) {
$newEpisode->chapters_file = $chaptersFile;
} elseif ($chaptersChoice === 'remote-url') {
$newEpisode->chapters_file_remote_url = $this->request->getPost(
'chapters_file_remote_url'
) === '' ? null : $this->request->getPost('chapters_file_remote_url');
}
$episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
return redirect()->route('episode-view', [$this->podcast->id, $newEpisodeId]);
}
public function edit(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'audio_file' =>
'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]|permit_empty',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'transcript_file' =>
'ext_in[transcript_file,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters_file,json]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->episode->title = $this->request->getPost('title');
$this->episode->slug = $this->request->getPost('slug');
$this->episode->description_markdown = $this->request->getPost('description');
$this->episode->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
);
$this->episode->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->episode->number = $this->request->getPost('episode_number')
? $this->request->getPost('episode_number')
: null;
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null;
$this->episode->type = $this->request->getPost('type');
$this->episode->is_blocked = $this->request->getPost('block') === 'yes';
$this->episode->custom_rss_string = $this->request->getPost('custom_rss');
$this->episode->updated_by = (int) user_id();
$audioFile = $this->request->getFile('audio_file');
if ($audioFile !== null && $audioFile->isValid()) {
$this->episode->audio_file = $audioFile;
}
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->episode->image = new Image($imageFile);
}
$transcriptChoice = $this->request->getPost('transcript-choice');
if ($transcriptChoice === 'upload-file') {
$transcriptFile = $this->request->getFile('transcript_file');
if ($transcriptFile !== null && $transcriptFile->isValid()) {
$this->episode->transcript_file = $transcriptFile;
$this->episode->transcript_file_remote_url = null;
}
} elseif ($transcriptChoice === 'remote-url') {
if (
($transcriptFileRemoteUrl = $this->request->getPost('transcript_file_remote_url')) &&
(($transcriptFile = $this->episode->transcript_file) !== null)
) {
unlink((string) $transcriptFile);
$this->episode->transcript_file_path = null;
}
$this->episode->transcript_file_remote_url = $transcriptFileRemoteUrl === '' ? null : $transcriptFileRemoteUrl;
}
$chaptersChoice = $this->request->getPost('chapters-choice');
if ($chaptersChoice === 'upload-file') {
$chaptersFile = $this->request->getFile('chapters_file');
if ($chaptersFile !== null && $chaptersFile->isValid()) {
$this->episode->chapters_file = $chaptersFile;
$this->episode->chapters_file_remote_url = null;
}
} elseif ($chaptersChoice === 'remote-url') {
if (
($chaptersFileRemoteUrl = $this->request->getPost('chapters_file_remote_url')) &&
(($chaptersFile = $this->episode->chapters_file) !== null)
) {
unlink((string) $chaptersFile);
$this->episode->chapters_file_path = null;
}
$this->episode->chapters_file_remote_url = $chaptersFileRemoteUrl === '' ? null : $chaptersFileRemoteUrl;
}
$db = db_connect();
$db->transStart();
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function transcriptDelete(): RedirectResponse
{
unlink((string) $this->episode->transcript_file);
$this->episode->transcript_file_path = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->back();
}
public function chaptersDelete(): RedirectResponse
{
unlink((string) $this->episode->chapters_file);
$this->episode->chapters_file_path = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->back();
}
public function publish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'not_published') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/publish', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_error')
);
}
public function attemptPublish(): RedirectResponse
{
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$newStatus = new Status([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
'message' => $this->request->getPost('message'),
'created_by' => user_id(),
]);
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone('UTC');
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
}
} else {
$this->episode->published_at = Time::now();
}
$newStatus->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->addStatus($newStatus)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function publishEdit(): string | RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'status' => (new StatusModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/publish_edit', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_edit_error')
);
}
public function attemptPublishEdit(): RedirectResponse
{
$rules = [
'status_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone('UTC');
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
}
} else {
$this->episode->published_at = Time::now();
}
$status = (new StatusModel())->getStatusById($this->request->getPost('status_id'));
if ($status !== null) {
$status->message = $this->request->getPost('message');
$status->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->editStatus($status)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
}
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function publishCancel(): RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
$db = db_connect();
$db->transStart();
$statusModel = new StatusModel();
$status = $statusModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first();
$statusModel->removeStatus($status);
$this->episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_cancel_error')
);
}
public function unpublish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'published') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/unpublish', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.unpublish_error')
);
}
public function attemptUnpublish(): RedirectResponse
{
$rules = [
'understand' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$allStatusesLinkedToEpisode = (new StatusModel())
->where([
'episode_id' => $this->episode->id,
])
->findAll();
foreach ($allStatusesLinkedToEpisode as $status) {
(new StatusModel())->removeStatus($status);
}
// set episode published_at to null to unpublish
$this->episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function delete(): RedirectResponse
{
(new EpisodeModel())->delete($this->episode->id);
return redirect()->route('episode-list', [$this->podcast->id]);
}
public function soundbitesEdit(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/soundbites', $data);
}
public function soundbitesAttemptEdit(): RedirectResponse
{
$soundbites = $this->request->getPost('soundbites');
$rules = [
'soundbites.0.start_time' =>
'permit_empty|required_with[soundbites.0.duration]|decimal|greater_than_equal_to[0]',
'soundbites.0.duration' =>
'permit_empty|required_with[soundbites.0.start_time]|decimal|greater_than_equal_to[0]',
];
foreach (array_keys($soundbites) as $soundbite_id) {
$rules += [
"soundbites.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
"soundbites.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]',
];
}
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
foreach ($soundbites as $soundbite_id => $soundbite) {
if ((int) $soundbite['start_time'] < (int) $soundbite['duration']) {
$data = [
'podcast_id' => $this->podcast->id,
'episode_id' => $this->episode->id,
'start_time' => (float) $soundbite['start_time'],
'duration' => (float) $soundbite['duration'],
'label' => $soundbite['label'],
'updated_by' => user_id(),
];
if ($soundbite_id === 0) {
$data += [
'created_by' => user_id(),
];
} else {
$data += [
'id' => $soundbite_id,
];
}
$soundbiteModel = new SoundbiteModel();
if (! $soundbiteModel->save($data)) {
return redirect()
->back()
->withInput()
->with('errors', $soundbiteModel->errors());
}
}
}
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
}
public function soundbiteDelete(string $soundbiteId): RedirectResponse
{
(new SoundbiteModel())->deleteSoundbite($this->podcast->id, $this->episode->id, (int) $soundbiteId);
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
}
public function embeddablePlayer(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'themes' => EpisodeModel::$themes,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/embeddable_player', $data);
}
}

View file

@ -1,97 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class EpisodePersonController extends BaseController
{
protected Podcast $podcast;
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) &&
($this->episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper('form');
$data = [
'episode' => $this->episode,
'podcast' => $this->podcast,
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/persons', $data);
}
public function attemptAdd(): RedirectResponse
{
$rules = [
'persons' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new PersonModel())->addEpisodePersons(
$this->podcast->id,
$this->episode->id,
$this->request->getPost('persons'),
$this->request->getPost('roles') ?? [],
);
return redirect()->back();
}
public function remove(string $personId): RedirectResponse
{
(new PersonModel())->removePersonFromEpisode($this->podcast->id, $this->episode->id, (int) $personId);
return redirect()->back();
}
}

View file

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
class FediverseController extends BaseController
{
public function dashboard(): string
{
return view('admin/fediverse/dashboard');
}
public function blockedActors(): string
{
helper(['form']);
$blockedActors = model('ActorModel')
->getBlockedActors();
return view('admin/fediverse/blocked_actors', [
'blockedActors' => $blockedActors,
]);
}
public function blockedDomains(): string
{
helper(['form']);
$blockedDomains = model('BlockedDomainModel')
->getBlockedDomains();
return view('admin/fediverse/blocked_domains', [
'blockedDomains' => $blockedDomains,
]);
}
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use CodeIgniter\HTTP\RedirectResponse;
class HomeController extends BaseController
{
public function index(): RedirectResponse
{
session()->keepFlashdata('message');
return redirect()->route('podcast-list');
}
}

View file

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\UserModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class MyAccountController extends BaseController
{
public function index(): string
{
return view('admin/my_account/view');
}
public function changePassword(): string
{
helper('form');
return view('admin/my_account/change_password');
}
public function attemptChange(): RedirectResponse
{
$auth = Services::authentication();
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'password' => 'required',
'new_password' => 'required|strong_password|differs[password]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
$credentials = [
'email' => user()
->email,
'password' => $this->request->getPost('password'),
];
if (! $auth->validate($credentials)) {
return redirect()
->back()
->withInput()
->with('error', lang('MyAccount.messages.wrongPasswordError'));
}
user()
->password = $this->request->getPost('new_password');
if (! $userModel->update(user_id(), user())) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->back()
->with('message', lang('MyAccount.messages.passwordChangeSuccess'));
}
}

View file

@ -1,118 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Page;
use App\Models\PageModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PageController extends BaseController
{
protected ?Page $page;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if ($this->page = (new PageModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'pages' => (new PageModel())->findAll(),
];
return view('admin/page/list', $data);
}
public function view(): string
{
return view('admin/page/view', [
'page' => $this->page,
]);
}
public function create(): string
{
helper('form');
return view('admin/page/create');
}
public function attemptCreate(): RedirectResponse
{
$page = new Page([
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'content_markdown' => $this->request->getPost('content'),
]);
$pageModel = new PageModel();
if (! $pageModel->insert($page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()
->route('page-list')
->with('message', lang('Page.messages.createSuccess', [
'pageTitle' => $page->title,
]));
}
public function edit(): string
{
helper('form');
replace_breadcrumb_params([
0 => $this->page->title,
]);
return view('admin/page/edit', [
'page' => $this->page,
]);
}
public function attemptEdit(): RedirectResponse
{
$this->page->title = $this->request->getPost('title');
$this->page->slug = $this->request->getPost('slug');
$this->page->content_markdown = $this->request->getPost('content');
$pageModel = new PageModel();
if (! $pageModel->update($this->page->id, $this->page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()->route('page-list');
}
public function delete(): RedirectResponse
{
(new PageModel())->delete($this->page->id);
return redirect()->route('page-list');
}
}

View file

@ -1,156 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Image;
use App\Entities\Person;
use App\Models\PersonModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PersonController extends BaseController
{
protected ?Person $person;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($this->person = (new PersonModel())->getPersonById((int) $params[0])) !== null
) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
$data = [
'persons' => (new PersonModel())->findAll(),
];
return view('admin/person/list', $data);
}
public function view(): string
{
$data = [
'person' => $this->person,
];
replace_breadcrumb_params([
0 => $this->person->full_name,
]);
return view('admin/person/view', $data);
}
public function create(): string
{
helper(['form']);
return view('admin/person/create');
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$person = new Person([
'full_name' => $this->request->getPost('full_name'),
'unique_name' => $this->request->getPost('unique_name'),
'information_url' => $this->request->getPost('information_url'),
'image' => new Image($this->request->getFile('image')),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
$personModel = new PersonModel();
if (! $personModel->insert($person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-list');
}
public function edit(): string
{
helper('form');
$data = [
'person' => $this->person,
];
replace_breadcrumb_params([
0 => $this->person->full_name,
]);
return view('admin/person/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->person->full_name = $this->request->getPost('full_name');
$this->person->unique_name = $this->request->getPost('unique_name');
$this->person->information_url = $this->request->getPost('information_url');
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->person->image = new Image($imageFile);
}
$this->person->updated_by = user_id();
$personModel = new PersonModel();
if (! $personModel->update($this->person->id, $this->person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-view', [$this->person->id]);
}
public function delete(): RedirectResponse
{
(new PersonModel())->delete($this->person->id);
return redirect()->route('person-list');
}
}

View file

@ -1,378 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class PodcastController extends BaseController
{
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
$this->podcast = $podcast;
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
if (! has_permission('podcasts-list')) {
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts((int) user_id()),
];
} else {
$data = [
'podcasts' => (new PodcastModel())->findAll(),
];
}
return view('admin/podcast/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/view', $data);
}
public function viewAnalytics(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/index', $data);
}
public function viewAnalyticsWebpages(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/webpages', $data);
}
public function viewAnalyticsLocations(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/locations', $data);
}
public function viewAnalyticsUniqueListeners(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/unique_listeners', $data);
}
public function viewAnalyticsListeningTime(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/listening_time', $data);
}
public function viewAnalyticsTimePeriods(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/time_periods', $data);
}
public function viewAnalyticsPlayers(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/players', $data);
}
public function create(): string
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
];
return view('admin/podcast/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
if (
($partnerId = $this->request->getPost('partner_id')) === '' ||
($partnerLinkUrl = $this->request->getPost('partner_link_url')) === '' ||
($partnerImageUrl = $this->request->getPost('partner_image_url')) === '') {
$partnerId = null;
$partnerLinkUrl = null;
$partnerImageUrl = null;
}
$podcast = new Podcast([
'guid' => podcast_uuid(url_to('podcast_feed', $this->request->getPost('name'))),
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description_markdown' => $this->request->getPost('description'),
'image' => new Image($this->request->getFile('image')),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name') === '' ? null : new Location($this->request->getPost(
'location_name'
)),
'payment_pointer' => $this->request->getPost(
'payment_pointer'
) === '' ? null : $this->request->getPost('payment_pointer'),
'custom_rss_string' => $this->request->getPost('custom_rss'),
'partner_id' => $partnerId,
'partner_link_url' => $partnerLinkUrl,
'partner_image_url' => $partnerImageUrl,
'is_blocked' => $this->request->getPost('block') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes',
'is_locked' => $this->request->getPost('lock') === 'yes',
'created_by' => user_id(),
'updated_by' => user_id(),
]);
$podcastModel = new PodcastModel();
$db = db_connect();
$db->transStart();
if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
(int) $newPodcastId,
$this->request->getPost('other_categories') ?? [],
);
// set interact as the newly created podcast actor
$createdPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($createdPodcast->actor_id);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function edit(): string
{
helper('form');
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'podcast' => $this->podcast,
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
if (
($partnerId = $this->request->getPost('partner_id')) === '' ||
($partnerLinkUrl = $this->request->getPost('partner_link_url')) === '' ||
($partnerImageUrl = $this->request->getPost('partner_image_url')) === '') {
$partnerId = null;
$partnerLinkUrl = null;
$partnerImageUrl = null;
}
$this->podcast->title = $this->request->getPost('title');
$this->podcast->description_markdown = $this->request->getPost('description');
$image = $this->request->getFile('image');
if ($image !== null && $image->isValid()) {
$this->podcast->image = new Image($image);
}
$this->podcast->language_code = $this->request->getPost('language');
$this->podcast->category_id = $this->request->getPost('category');
$this->podcast->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
);
$this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer'
) === '' ? null : $this->request->getPost('payment_pointer');
$this->podcast->custom_rss_string = $this->request->getPost('custom_rss');
$this->podcast->partner_id = $partnerId;
$this->podcast->partner_link_url = $partnerLinkUrl;
$this->podcast->partner_image_url = $partnerImageUrl;
$this->podcast->is_blocked = $this->request->getPost('block') === 'yes';
$this->podcast->is_completed =
$this->request->getPost('complete') === 'yes';
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
$this->podcast->updated_by = (int) user_id();
$db = db_connect();
$db->transStart();
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$this->podcast->id,
$this->request->getPost('other_categories') ?? [],
);
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function latestEpisodes(int $limit, int $podcast_id): string
{
$episodes = (new EpisodeModel())
->where('podcast_id', $podcast_id)
->orderBy('created_at', 'desc')
->findAll($limit);
return view('admin/podcast/latest_episodes', [
'episodes' => $episodes,
]);
}
public function delete(): RedirectResponse
{
(new PodcastModel())->delete($this->podcast->id);
return redirect()->route('podcast-list');
}
}

View file

@ -1,453 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Person;
use App\Entities\Podcast;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use ErrorException;
use League\HTMLToMarkdown\HtmlConverter;
use Podlibre\PodcastNamespace\ReversedTaxonomy;
class PodcastImportController extends BaseController
{
protected ?Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
];
return view('admin/podcast/import', $data);
}
public function attemptImport(): RedirectResponse
{
helper(['media', 'misc']);
$rules = [
'imported_feed_url' => 'required|validate_url',
'season_number' => 'is_natural_no_zero|permit_empty',
'max_episodes' => 'is_natural_no_zero|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
try {
ini_set('user_agent', 'Castopod/' . CP_VERSION);
$feed = simplexml_load_file($this->request->getPost('imported_feed_url'));
} catch (ErrorException $errorException) {
return redirect()
->back()
->withInput()
->with('errors', [
$errorException->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$nsItunes = $feed->channel[0]->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
$nsPodcast = $feed->channel[0]->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
);
$nsContent = $feed->channel[0]->children('http://purl.org/rss/1.0/modules/content/');
if ((string) $nsPodcast->locked === 'yes') {
return redirect()
->back()
->withInput()
->with('errors', [lang('PodcastImport.lock_import')]);
}
$converter = new HtmlConverter();
$channelDescriptionHtml = (string) $feed->channel[0]->description;
try {
if (
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$imageFile = download_file((string) $nsItunes->image->attributes()['href']);
} else {
$imageFile = download_file((string) $feed->channel[0]->image->url);
}
$location = null;
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
$location = new Location(
(string) $nsPodcast->location,
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
);
}
if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
$guid = (string) $nsPodcast->guid;
} else {
$guid = podcast_uuid(url_to('podcast_feed', $this->request->getPost('name')));
}
$podcast = new Podcast([
'guid' => $guid,
'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost('imported_feed_url'),
'new_feed_url' => url_to('podcast_feed', $this->request->getPost('name')),
'title' => (string) $feed->channel[0]->title,
'description_markdown' => $converter->convert($channelDescriptionHtml),
'description_html' => $channelDescriptionHtml,
'image' => new Image($imageFile),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
? 'explicit'
: (in_array((string) $nsItunes->explicit, ['no', 'false'], true)
? 'clean'
: null))
: null,
'owner_name' => (string) $nsItunes->owner->name,
'owner_email' => (string) $nsItunes->owner->email,
'publisher' => (string) $nsItunes->author,
'type' => property_exists(
$nsItunes,
'type'
) && $nsItunes->type !== null ? (string) $nsItunes->type : 'episodic',
'copyright' => (string) $feed->channel[0]->copyright,
'is_blocked' =>
property_exists($nsItunes, 'block') && $nsItunes->block !== null && (string) $nsItunes->block === 'yes',
'is_completed' =>
property_exists(
$nsItunes,
'complete'
) && $nsItunes->complete !== null && (string) $nsItunes->complete === 'yes',
'location' => $location,
'created_by' => user_id(),
'updated_by' => user_id(),
]);
} catch (ErrorException $ex) {
return redirect()
->back()
->withInput()
->with('errors', [
$ex->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$podcastModel = new PodcastModel();
$db = db_connect();
$db->transStart();
if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
$podcastsPlatformsData = [];
$platformTypes = [
[
'name' => 'podcasting',
'elements' => $nsPodcast->id,
],
[
'name' => 'social',
'elements' => $nsPodcast->social,
],
[
'name' => 'funding',
'elements' => $nsPodcast->funding,
],
];
$platformModel = new PlatformModel();
foreach ($platformTypes as $platformType) {
foreach ($platformType['elements'] as $platform) {
$platformLabel = $platform->attributes()['platform'];
$platformSlug = slugify((string) $platformLabel);
if ($platformModel->getPlatform($platformSlug) !== null) {
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $newPodcastId,
'link_url' => $platform->attributes()['url'],
'link_content' => $platform->attributes()['id'],
'is_visible' => false,
];
}
}
}
if (count($podcastsPlatformsData) > 1) {
$platformModel->createPodcastPlatforms($newPodcastId, $podcastsPlatformsData);
}
foreach ($nsPodcast->person as $podcastPerson) {
$fullName = (string) $podcastPerson;
$personModel = new PersonModel();
$newPersonId = null;
if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id;
} else {
$newPodcastPerson = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $podcastPerson->attributes()['href'],
'image' => new Image(download_file((string) $podcastPerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
if (! $newPersonId = $personModel->insert($newPodcastPerson)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
// TODO: these checks should be in the taxonomy as default values
$podcastPersonGroup = $podcastPerson->attributes()['group'] ?? 'Cast';
$podcastPersonRole = $podcastPerson->attributes()['role'] ?? 'Host';
$personGroup = ReversedTaxonomy::$taxonomy[(string) $podcastPersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $podcastPersonRole]['slug'];
$podcastPersonModel = new PersonModel();
if (! $podcastPersonModel->addPodcastPerson(
$newPodcastId,
$newPersonId,
$personGroupSlug,
$personRoleSlug
)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastPersonModel->errors());
}
}
$numberItems = $feed->channel[0]->item->count();
$lastItem =
$this->request->getPost('max_episodes') !== '' &&
$this->request->getPost('max_episodes') < $numberItems
? (int) $this->request->getPost('max_episodes')
: $numberItems;
$slugs = [];
for ($itemNumber = 1; $itemNumber <= $lastItem; ++$itemNumber) {
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
$nsItunes = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
$nsPodcast = $item->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
);
$nsContent = $item->children('http://purl.org/rss/1.0/modules/content/');
$textToSlugify = $this->request->getPost('slug_field') === 'title'
? (string) $item->title
: basename((string) $item->link);
$slug = slugify($textToSlugify, 185);
if (in_array($slug, $slugs, true)) {
$slugNumber = 2;
while (in_array($slug . '-' . $slugNumber, $slugs, true)) {
++$slugNumber;
}
$slug = $slug . '-' . $slugNumber;
}
$slugs[] = $slug;
$itemDescriptionHtml = match ($this->request->getPost('description_field')) {
'content' => (string) $nsContent->encoded,
'summary' => (string) $nsItunes->summary,
'subtitle_summary' => $nsItunes->subtitle . '<br/>' . $nsItunes->summary,
default => (string) $item->description,
};
if (
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$episodeImage = new Image(download_file((string) $nsItunes->image->attributes()['href']));
} else {
$episodeImage = null;
}
$location = null;
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
$location = new Location(
(string) $nsPodcast->location,
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
);
}
$newEpisode = new Episode([
'podcast_id' => $newPodcastId,
'title' => $item->title,
'slug' => $slug,
'guid' => $item->guid ?? null,
'audio_file' => download_file(
(string) $item->enclosure->attributes()['url'],
(string) $item->enclosure->attributes()['type']
),
'description_markdown' => $converter->convert($itemDescriptionHtml),
'description_html' => $itemDescriptionHtml,
'image' => $episodeImage,
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
? 'explicit'
: (in_array((string) $nsItunes->explicit, ['no', 'false'], true)
? 'clean'
: null))
: null,
'number' =>
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
: ((string) $nsItunes->episode === '' ? null : (int) $nsItunes->episode),
'season_number' =>
$this->request->getPost('season_number') === ''
? ((string) $nsItunes->season === '' ? null : (int) $nsItunes->season)
: (int) $this->request->getPost('season_number'),
'type' => property_exists($nsItunes, 'episodeType') && $nsItunes->episodeType !== null
? (string) $nsItunes->episodeType
: 'full',
'is_blocked' => property_exists(
$nsItunes,
'block'
) && $nsItunes->block !== null && (string) $nsItunes->block === 'yes',
'location' => $location,
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => strtotime((string) $item->pubDate),
]);
$episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
foreach ($nsPodcast->person as $episodePerson) {
$fullName = (string) $episodePerson;
$personModel = new PersonModel();
$newPersonId = null;
if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id;
} else {
$newPerson = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $episodePerson->attributes()['href'],
'image' => new Image(download_file((string) $episodePerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
if (! ($newPersonId = $personModel->insert($newPerson))) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
// TODO: these checks should be in the taxonomy as default values
$episodePersonGroup = $episodePerson->attributes()['group'] ?? 'Cast';
$episodePersonRole = $episodePerson->attributes()['role'] ?? 'Host';
$personGroup = ReversedTaxonomy::$taxonomy[(string) $episodePersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $episodePersonRole]['slug'];
$episodePersonModel = new PersonModel();
if (! $episodePersonModel->addEpisodePerson(
$newPodcastId,
$newEpisodeId,
$newPersonId,
$personGroupSlug,
$personRoleSlug
)) {
return redirect()
->back()
->withInput()
->with('errors', $episodePersonModel->errors());
}
}
}
// set interact as the newly imported podcast actor
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($importedPodcast->actor_id);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
}

View file

@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PodcastPersonController extends BaseController
{
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
'podcastPersons' => (new PersonModel())->getPodcastPersons($this->podcast->id),
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/persons', $data);
}
public function attemptAdd(): RedirectResponse
{
$rules = [
'persons' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new PersonModel())->addPodcastPersons(
$this->podcast->id,
$this->request->getPost('persons'),
$this->request->getPost('roles') ?? [],
);
return redirect()->back();
}
public function remove(string $personId): RedirectResponse
{
(new PersonModel())->removePersonFromPodcast($this->podcast->id, (int) $personId);
return redirect()->back();
}
}

View file

@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class PodcastPlatformController extends BaseController
{
protected ?Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
return view('admin/podcast/platforms/dashboard');
}
public function platforms(string $platformType): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks($this->podcast->id, $platformType),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/platforms', $data);
}
public function attemptPlatformsUpdate(string $platformType): RedirectResponse
{
$platformModel = new PlatformModel();
$validation = Services::validation();
$podcastsPlatformsData = [];
foreach (
$this->request->getPost('platforms')
as $platformSlug => $podcastPlatform
) {
$podcastPlatformUrl = $podcastPlatform['url'];
if ($podcastPlatformUrl === null) {
continue;
}
if (! $validation->check($podcastPlatformUrl, 'validate_url')) {
continue;
}
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'link_content' => $podcastPlatform['content'],
'is_visible' =>
array_key_exists('visible', $podcastPlatform) &&
$podcastPlatform['visible'] === 'yes',
'is_on_embeddable_player' =>
array_key_exists(
'on_embeddable_player',
$podcastPlatform,
) && $podcastPlatform['on_embeddable_player'] === 'yes',
];
}
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType, $podcastsPlatformsData);
return redirect()
->back()
->with('message', lang('Platforms.messages.updateSuccess'));
}
public function removePodcastPlatform(string $platformSlug): RedirectResponse
{
(new PlatformModel())->removePodcastPlatform($this->podcast->id, $platformSlug);
return redirect()
->back()
->with('message', lang('Platforms.messages.removeLinkSuccess'));
}
}

View file

@ -1,247 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Authorization\GroupModel;
use App\Entities\User;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class UserController extends BaseController
{
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if ($this->user = (new UserModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'users' => (new UserModel())->findAll(),
];
return view('admin/user/list', $data);
}
public function view(): string
{
$data = [
'user' => $this->user,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('admin/user/view', $data);
}
public function create(): string
{
helper('form');
$data = [
'roles' => (new GroupModel())->getUserRoles(),
];
return view('admin/user/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules([
'only' => ['username'],
]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
],
);
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new User($this->request->getPost());
// Activate user
$user->activate();
// Force user to reset his password on first connection
$user->forcePasswordReset();
if (! $userModel->insert($user)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.createSuccess', [
'username' => $user->username,
]));
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getUserRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->name] = lang('User.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'user' => $this->user,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('admin/user/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$authorize = Services::authorization();
$roles = $this->request->getPost('roles');
$authorize->setUserGroups($this->user->id, $roles ?? []);
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.rolesEditSuccess', [
'username' => $this->user->username,
]));
}
public function forcePassReset(): RedirectResponse
{
$userModel = new UserModel();
$this->user->forcePasswordReset();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.forcePassResetSuccess', [
'username' => $this->user->username,
]),
);
}
public function ban(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.banSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
$userModel = new UserModel();
// TODO: add ban reason?
$this->user->ban('');
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.banSuccess', [
'username' => $this->user->username,
]));
}
public function unBan(): RedirectResponse
{
$userModel = new UserModel();
$this->user->unBan();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.unbanSuccess', [
'username' => $this->user->username,
]));
}
public function delete(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
(new UserModel())->delete($this->user->id);
return redirect()
->back()
->with('message', lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
]));
}
}

View file

@ -1,188 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\User;
use CodeIgniter\HTTP\RedirectResponse;
use Myth\Auth\Controllers\AuthController as MythAuthController;
class AuthController extends MythAuthController
{
/**
* An array of helpers to be automatically loaded upon class instantiation.
*
* @var string[]
*/
protected $helpers = ['components'];
/**
* Attempt to register a new user.
*/
public function attemptRegister(): RedirectResponse
{
// Check if registration is allowed
if (! $this->config->allowRegistration) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.registerDisabled'));
}
$users = model('UserModel');
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'username' =>
'required|alpha_numeric_space|min_length[3]|is_unique[users.username]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
// Save the user
$allowedPostFields = array_merge(['password'], $this->config->validFields, $this->config->personalFields);
$user = new User($this->request->getPost($allowedPostFields));
$this->config->requireActivation === null
? $user->activate()
: $user->generateActivateHash();
// Ensure default group gets assigned if set
if ($this->config->defaultUserGroup !== null) {
$users = $users->withGroup($this->config->defaultUserGroup);
}
if (! $users->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
if ($this->config->requireActivation !== null) {
$activator = service('activator');
$sent = $activator->send($user);
if (! $sent) {
return redirect()
->back()
->withInput()
->with('error', $activator->error() ?? lang('Auth.unknownError'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.activationSuccess'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.registerSuccess'));
}
/**
* Verifies the code with the email and saves the new password, if they all pass validation.
*/
public function attemptReset(): RedirectResponse
{
if ($this->config->activeResetter === null) {
return redirect()
->route('login')
->with('error', lang('Auth.forgotDisabled'));
}
$users = model('UserModel');
// First things first - log the reset attempt.
$users->logResetAttempt(
$this->request->getPost('email'),
$this->request->getPost('token'),
$this->request->getIPAddress(),
(string) $this->request->getUserAgent(),
);
$rules = [
'token' => 'required',
'email' => 'required|valid_email',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
$user = $users
->where('email', $this->request->getPost('email'))
->where('reset_hash', $this->request->getPost('token'))
->first();
if ($user === null) {
return redirect()
->back()
->with('error', lang('Auth.forgotNoUser'));
}
// Reset token still valid?
if (
$user->reset_expires !== null &&
time() > $user->reset_expires->getTimestamp()
) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.resetTokenExpired'));
}
// Success! Save the new password, and cleanup the reset hash.
$user->password = $this->request->getPost('password');
$user->reset_hash = null;
$user->reset_at = date('Y-m-d H:i:s');
$user->reset_expires = null;
$user->force_pass_reset = false;
$users->save($user);
return redirect()
->route('login')
->with('message', lang('Auth.resetSuccess'));
}
public function attemptInteractAsActor(): RedirectResponse
{
$rules = [
'actor_id' => 'required|numeric',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
helper('auth');
set_interact_as_actor((int) $this->request->getPost('actor_id'));
return redirect()->back();
}
}

View file

@ -7,35 +7,51 @@ namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Override;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Class BaseController
*
* 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.
*
* For security be sure to declare any new methods as protected or private.
* Extend this class in any new controllers:
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/
class BaseController extends Controller
abstract class BaseController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend BaseController.
* An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var string[]
* @var list<string>
*/
protected $helpers = ['auth', 'svg', 'components', 'misc'];
protected $helpers = [];
/**
* Constructor.
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
#[Override]
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
LoggerInterface $logger,
): void {
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
$this->helpers = [...$this->helpers, 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
// Do Not Edit This Line
parent::initController($request, $response, $logger);
Theme::setTheme('app');
}
}

View file

@ -0,0 +1,45 @@
<?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 CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
class ColorsController extends Controller
{
public function index(): ResponseInterface
{
$cacheName = 'colors.css';
if (
! ($colorsCssBody = cache($cacheName))
) {
$colorThemes = config('Colors')
->themes;
$colorsCssBody = '';
foreach ($colorThemes as $name => $color) {
$colorsCssBody .= ".theme-{$name} {";
foreach ($color as $variable => $value) {
$colorsCssBody .= "--color-{$variable}: {$value[0]} {$value[1]}% {$value[2]}%;";
}
$colorsCssBody .= '}';
}
cache()
->save($cacheName, $colorsCssBody, DECADE);
}
return $this->response->setHeader('Content-Type', 'text/css')
->setHeader('charset', 'UTF-8')
->setBody($colorsCssBody);
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -20,17 +20,23 @@ class CreditsController extends BaseController
{
$locale = service('request')
->getLocale();
$allPodcasts = (new PodcastModel())->findAll();
$cacheName = "page_credits_{$locale}";
$cacheName = implode(
'_',
array_filter(['page', 'credits', $locale, auth()->loggedIn() ? 'authenticated' : null]),
);
if (! ($found = cache($cacheName))) {
$page = new Page([
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'content_markdown' => '',
]);
$allCredits = (new CreditModel())->findAll();
$allPodcasts = new PodcastModel()
->findAll();
$allCredits = new CreditModel()
->findAll();
// Unlike the carpenter, we make a tree from a table:
$personGroup = null;
@ -44,27 +50,24 @@ class CreditsController extends BaseController
$personRole = $credit->person_role;
$credits[$personGroup] = [
'group_label' => $credit->group_label,
'persons' => [
'persons' => [
$personId => [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' =>
$credit->person->information_url,
'roles' => [
'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
'information_url' => $credit->person->information_url,
'roles' => [
$personRole => [
'role_label' => $credit->role_label,
'is_in' => [
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} "
? esc($credit->podcast->title) . ' '
: '') .
$credit->episode
->title .
esc($credit->episode->title) .
episode_numbering(
$credit->episode
->number,
@ -73,7 +76,7 @@ class CreditsController extends BaseController
'text-xs ml-2',
true,
)
: $credit->podcast->title,
: esc($credit->podcast->title),
],
],
],
@ -85,23 +88,22 @@ class CreditsController extends BaseController
$personId = $credit->person_id;
$personRole = $credit->person_role;
$credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
'information_url' => $credit->person->information_url,
'roles' => [
'roles' => [
$personRole => [
'role_label' => $credit->role_label,
'is_in' => [
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} "
? esc($credit->podcast->title) . ' '
: '') .
$credit->episode->title .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode
@ -109,7 +111,7 @@ class CreditsController extends BaseController
'text-xs ml-2',
true,
)
: $credit->podcast->title,
: esc($credit->podcast->title),
],
],
],
@ -121,23 +123,23 @@ class CreditsController extends BaseController
$personRole
] = [
'role_label' => $credit->role_label,
'is_in' => [
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} "
? esc($credit->podcast->title) . ' '
: '') .
$credit->episode->title .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
: esc($credit->podcast->title),
],
],
];
@ -150,26 +152,27 @@ class CreditsController extends BaseController
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} "
? esc($credit->podcast->title) . ' '
: '') .
$credit->episode->title .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
: esc($credit->podcast->title),
];
}
}
set_page_metatags($page);
$data = [
'page' => $page,
'page' => $page,
'credits' => $credits,
];
$found = view('credits', $data);
$found = view('pages/credits', $data);
cache()
->save($cacheName, $found, DECADE);

View file

@ -0,0 +1,174 @@
<?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\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use Modules\Analytics\Config\Analytics;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Override;
use Psr\Log\LoggerInterface;
class EpisodeAudioController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend Analytics.
*
* @var list<string>
*/
protected $helpers = ['analytics'];
protected Podcast $podcast;
protected Episode $episode;
protected Analytics $analyticsConfig;
#[Override]
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger,
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
set_user_session_deny_list_ip();
set_user_session_location();
set_user_session_player();
$this->analyticsConfig = config('Analytics');
}
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): RedirectResponse | ResponseInterface
{
// check if episode is premium?
$subscription = null;
// check if podcast is already unlocked before any token validation
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
$this->episode->podcast->handle,
)) instanceof Subscription) {
// look for token as GET parameter
if (($token = $this->request->getGet('token')) === null) {
return $this->response->setStatusCode(401)
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Episode is premium, you must provide a token to unlock it.',
],
]);
}
// check if there's a valid subscription for the provided token
if (! ($subscription = new SubscriptionModel()->validateSubscription(
$this->episode->podcast->handle,
$token,
)) instanceof Subscription) {
return $this->response->setStatusCode(401, 'Invalid token!')
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Invalid token!',
],
]);
}
}
$session = service('session');
$serviceName = '';
if ($this->request->getGet('_from')) {
$serviceName = $this->request->getGet('_from');
} elseif ($session->get('embed_domain') !== null) {
$serviceName = $session->get('embed_domain');
} elseif ($session->get('referer') !== null && $session->get('referer') !== '- Direct -') {
$serviceName = parse_url((string) $session->get('referer'), PHP_URL_HOST);
}
$audioFileSize = $this->episode->audio->file_size;
$audioFileHeaderSize = $this->episode->audio->header_size;
$audioDuration = $this->episode->audio->duration;
// bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
// - if audio is less than or equal to 60s, then take the audio file_size
// - if audio is more than 60s, then take the audio file_header_size + 60s
$bytesThreshold = $audioDuration <= 60
? $audioFileSize
: $audioFileHeaderSize +
(int) floor((($audioFileSize - $audioFileHeaderSize) / $audioDuration) * 60);
podcast_hit(
$this->episode->podcast_id,
$this->episode->id,
$bytesThreshold,
$audioFileSize,
$audioDuration,
$this->episode->published_at->getTimestamp(),
$serviceName,
$subscription instanceof Subscription ? $subscription->id : null,
);
$audioFileURI = new URI(service('file_manager')->getUrl($this->episode->audio->file_key));
$queryParams = [];
foreach ($this->request->getGet() as $key => $value) {
// do not include token in query params
if ($key !== 'token') {
$queryParams[$key] = $value;
}
}
$audioFileURI->setQueryArray($queryParams);
return redirect()->to((string) $audioFileURI);
}
}

View file

@ -0,0 +1,184 @@
<?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\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class EpisodeCommentController extends BaseController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
protected Episode $episode;
protected EpisodeComment $comment;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 3) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $podcast->actor;
if (
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if (
! ($comment = new EpisodeCommentModel()->getCommentById($params[2])) instanceof EpisodeComment
) {
throw PageNotFoundException::forPageNotFound();
}
$this->comment = $comment;
unset($params[2]);
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"episode#{$this->episode->id}",
"comment#{$this->comment->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_comment_metatags($this->comment);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'episode' => $this->episode,
'comment' => $this->comment,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('episode/comment', $data);
}
return view('episode/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function commentObject(): ResponseInterface
{
$commentObject = new CommentObject($this->comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function replies(): ResponseInterface
{
/**
* get comment replies
*/
$commentReplies = model(EpisodeCommentModel::class, false)
->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function likeAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
public function replyAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
}

View file

@ -3,16 +3,13 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\NoteObject;
@ -21,9 +18,12 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\Embed;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
use Modules\Media\FileManagers\FileManagerInterface;
use SimpleXMLElement;
class EpisodeController extends BaseController
@ -41,7 +41,7 @@ class EpisodeController extends BaseController
}
if (
($podcast = (new PodcastModel())->getPodcastByName($params[0])) === null
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
@ -49,7 +49,7 @@ class EpisodeController extends BaseController
$this->podcast = $podcast;
if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
@ -64,36 +64,41 @@ class EpisodeController extends BaseController
public function index(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$locale = service('request')
->getLocale();
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
(can_user_interact() ? '_authenticated' : '');
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/episode_authenticated', $data);
return view('episode/comments', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -101,44 +106,208 @@ class EpisodeController extends BaseController
return $cachedView;
}
public function embeddablePlayer(string $theme = 'light-transparent'): string
public function activity(): string
{
header('Content-Security-Policy: frame-ancestors https://* http://*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$session = Services::session();
$session->start();
if (isset($_SERVER['HTTP_REFERER'])) {
$session->set('embeddable_player_domain', parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST));
}
$locale = service('request')
->getLocale();
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'activity',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$theme = EpisodeModel::$themes[$theme];
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
if (auth()->loggedIn()) {
helper('form');
return view('episode/activity', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embeddable_player', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function chapters(): string
{
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'chapters',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get chapters from json file
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
if (auth()->loggedIn()) {
helper('form');
return view('episode/chapters', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function transcript(): string
{
$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))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get transcript from json file
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,
]);
}
return $cachedView;
}
public function embed(string $theme = 'light-transparent'): string
{
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$session = service('session');
if (service('superglobals')->server('HTTP_REFERER') !== null) {
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'embed',
$theme,
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$themeData = EpisodeModel::$themes[$theme];
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
'themeData' => $themeData,
];
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -149,24 +318,25 @@ class EpisodeController extends BaseController
public function oembedJSON(): ResponseInterface
{
return $this->response->setJSON([
'type' => 'rich',
'version' => '1.0',
'title' => $this->episode->title,
'type' => 'rich',
'version' => '1.0',
'title' => $this->episode->title,
'provider_name' => $this->podcast->title,
'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link,
'html' =>
'<iframe src="' .
$this->episode->embeddable_player_url .
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
'width' => 600,
'height' => 200,
'thumbnail_url' => $this->episode->image->large_url,
'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link,
'html' => '<iframe src="' .
$this->episode->embed_url .
'" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
'width' => config('Embed')
->width,
'height' => config('Embed')
->height,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_width' => config('Images')
->largeSize,
->podcastCoverSizes['og']['width'],
'thumbnail_height' => config('Images')
->largeSize,
->podcastCoverSizes['og']['height'],
]);
}
@ -181,27 +351,27 @@ class EpisodeController extends BaseController
$oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->image->large_url);
$oembed->addChild('thumbnail_width', config('Images')->largeSize);
$oembed->addChild('thumbnail_height', config('Images')->largeSize);
$oembed->addChild('thumbnail', $this->episode->cover->og_url);
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['og']['width']);
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['og']['height']);
$oembed->addChild(
'html',
htmlentities(
htmlspecialchars(
'<iframe src="' .
$this->episode->embeddable_player_url .
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
$this->episode->embed_url .
'" width="100%" height="' . config(
Embed::class,
)->height . '" frameborder="0" scrolling="no"></iframe>',
),
);
$oembed->addChild('width', '600');
$oembed->addChild('height', '200');
$oembed->addChild('width', (string) config('Embed')->width);
$oembed->addChild('height', (string) config('Embed')->height);
return $this->response->setXML((string) $oembed);
// @phpstan-ignore-next-line
return $this->response->setXML($oembed);
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeObject(): Response
public function episodeObject(): ResponseInterface
{
$podcastObject = new PodcastEpisode($this->episode);
@ -210,21 +380,16 @@ class EpisodeController extends BaseController
->setBody($podcastObject->toJSON());
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function comments(): Response
public function comments(): ResponseInterface
{
/**
* get comments: aggregated replies from posts referring to the episode
*/
$episodeComments = model('StatusModel')
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
return $builder->select('id')
->from('activitypub_statuses')
->where('episode_id', $this->episode->id);
})
->where('`published_at` <= NOW()', null, false)
$episodeComments = model('PostModel')
->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
->from('fediverse_posts')
->where('episode_id', $this->episode->id))
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
@ -238,10 +403,8 @@ class EpisodeController extends BaseController
$pager = $episodeComments->pager;
$orderedItems = [];
if ($paginatedComments !== null) {
foreach ($paginatedComments as $comment) {
$orderedItems[] = (new NoteObject($comment))->toArray();
}
foreach ($paginatedComments as $comment) {
$orderedItems[] = new NoteObject($comment)->toArray();
}
// @phpstan-ignore-next-line

View file

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use Modules\Media\FileManagers\FileManagerInterface;
class EpisodePreviewController extends BaseController
{
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 1) {
throw PageNotFoundException::forPageNotFound();
}
// find episode by previewUUID
$episode = new EpisodeModel()
->getEpisodeByPreviewId($params[0]);
if (! $episode instanceof Episode) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if ($episode->publication_status === 'published') {
// redirect to episode page
return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
}
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): string
{
helper('form');
return view('episode/preview-comments', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function activity(): string
{
helper('form');
return view('episode/preview-activity', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function chapters(): string
{
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
helper('form');
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

@ -3,36 +3,59 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use Exception;
use Opawg\UserAgentsPhp\UserAgentsRSS;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Opawg\UserAgentsV2Php\UserAgentsRSS;
class FeedController extends Controller
{
public function index(string $podcastName): ResponseInterface
{
helper('rss');
/**
* Instance of the main Request object.
*
* @var IncomingRequest
*/
protected $request;
$podcast = (new PodcastModel())->where('name', $podcastName)
public function index(string $podcastHandle): ResponseInterface
{
$podcast = new PodcastModel()
->where('handle', $podcastHandle)
->first();
if (! $podcast) {
if (! $podcast instanceof Podcast) {
throw PageNotFoundException::forPageNotFound();
}
// 301 redirect to new feed?
$redirectToNewFeed = service('settings')
->get('Podcast.redirect_to_new_feed', 'podcast:' . $podcast->id);
if ($redirectToNewFeed && $podcast->new_feed_url !== null && filter_var(
$podcast->new_feed_url,
FILTER_VALIDATE_URL,
) && $podcast->new_feed_url !== current_url()) {
return redirect()->to($podcast->new_feed_url, 301);
}
helper(['rss', 'premium_podcasts', 'misc']);
$service = null;
try {
$service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']);
$service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT'));
} catch (Exception $exception) {
// If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $exception->getMessage());
@ -43,25 +66,32 @@ class FeedController extends Controller
$serviceSlug = $service['slug'];
}
$cacheName =
"podcast#{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
$subscription = null;
$token = $this->request->getGet('token');
if ($token) {
$subscription = new SubscriptionModel()
->validateSubscription($podcastHandle, $token);
}
$cacheName = implode(
'_',
array_filter([
"podcast#{$podcast->id}",
'feed',
$service ? $serviceSlug : null,
$subscription instanceof Subscription ? "subscription#{$subscription->id}" : null,
]),
);
if (! ($found = cache($cacheName))) {
$found = get_rss_feed($podcast, $serviceSlug);
$found = get_rss_feed($podcast, $serviceSlug, $subscription, $token);
// The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$podcast->id,
);
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($podcast->id);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $this->response->setXML($found);

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -11,33 +11,74 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Models\PodcastModel;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Media\FileManagers\FileManagerInterface;
class HomeController extends BaseController
{
public function index(): RedirectResponse | string
{
$db = db_connect();
if ($db->getDatabase() === '' || ! $db->tableExists('podcasts')) {
// Database connection has not been set or could not find the podcasts table
// Redirecting to install page because it is likely that Castopod Host has not been installed yet.
// NB: as base_url wouldn't have been defined here, redirect to install wizard manually
$route = Services::routes()->reverseRoute('install');
return redirect()->to(rtrim(host_url(), '/') . $route);
}
$sortOptions = ['activity', 'created_desc', 'created_asc'];
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
'sort',
) : 'activity';
$allPodcasts = (new PodcastModel())->findAll();
$allPodcasts = new PodcastModel()
->getAllPodcasts($sortBy);
// check if there's only one podcast to redirect user to it
if (count($allPodcasts) === 1) {
return redirect()->route('podcast-activity', [$allPodcasts[0]->name]);
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
}
set_home_metatags();
// default behavior: list all podcasts on home page
$data = [
'podcasts' => $allPodcasts,
'sortBy' => $sortBy,
];
return view('home', $data);
}
public function health(): ResponseInterface
{
$errors = [];
try {
db_connect();
} catch (DatabaseException) {
$errors[] = 'Unable to connect to the database.';
}
// --- Can Castopod connect to the cache handler
if (config('Cache')->handler !== 'dummy' && cache()->getCacheInfo() === null) {
$errors[] = 'Unable connect to the cache handler.';
}
// --- Can Castopod write to storage?
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager', false);
if (! $fileManager->isHealthy()) {
$errors[] = 'Problem with file manager.';
}
if ($errors !== []) {
return $this->response->setStatusCode(503)
->setJSON([
'code' => 503,
'errors' => $errors,
]);
}
return $this->response->setStatusCode(200)
->setJSON([
'code' => 200,
'message' => '✨ All good!',
]);
}
}

View file

@ -1,364 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\User;
use App\Models\UserModel;
use CodeIgniter\Controller;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Database;
use Config\Services;
use Dotenv\Dotenv;
use Dotenv\Exception\ValidationException;
use Psr\Log\LoggerInterface;
use Throwable;
class InstallController extends Controller
{
/**
* @var string[]
*/
protected $helpers = ['form', 'components', 'svg'];
/**
* Constructor.
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
}
/**
* Every operation goes through this method to handle the install logic.
*
* If all required actions have already been performed, the install route will show a 404 page.
*/
public function index(): string
{
if (! file_exists(ROOTPATH . '.env')) {
// create empty .env file
try {
$envFile = fopen(ROOTPATH . '.env', 'w');
fclose($envFile);
} catch (Throwable) {
// Could not create the .env file, redirect to a view with instructions on how to add it manually
return view('install/manual_config');
}
}
// Check if .env has all required fields
$dotenv = Dotenv::createUnsafeImmutable(ROOTPATH);
$dotenv->load();
// Check if the created .env file is writable to continue install process
if (is_really_writable(ROOTPATH . '.env')) {
try {
$dotenv->required(['app.baseURL', 'app.adminGateway', 'app.authGateway']);
} catch (ValidationException) {
// form to input instance configuration
return $this->instanceConfig();
}
try {
$dotenv->required([
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
]);
} catch (ValidationException) {
return $this->databaseConfig();
}
try {
$dotenv->required('cache.handler');
} catch (ValidationException) {
return $this->cacheConfig();
}
} else {
try {
$dotenv->required([
'app.baseURL',
'app.adminGateway',
'app.authGateway',
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
'cache.handler',
]);
} catch (ValidationException) {
return view('install/manual_config');
}
}
try {
$db = db_connect();
// Check if superadmin has been created, meaning migrations and seeds have passed
if (
$db->tableExists('users') &&
(new UserModel())->countAll() > 0
) {
// if so, show a 404 page
throw PageNotFoundException::forPageNotFound();
}
} catch (DatabaseException) {
// Could not connect to the database
// show database config view to fix value
session()
->setFlashdata('error', lang('Install.messages.databaseConnectError'));
return view('install/database_config');
}
// migrate if no user has been created
$this->migrate();
// Check if all seeds have succeeded
$this->seed();
return $this->createSuperAdmin();
}
public function instanceConfig(): string
{
return view('install/instance_config');
}
public function attemptInstanceConfig(): RedirectResponse
{
$rules = [
'hostname' => 'required|validate_url',
'media_base_url' => 'permit_empty|validate_url',
'admin_gateway' => 'required',
'auth_gateway' => 'required|differs[admin_gateway]',
];
if (! $this->validate($rules)) {
return redirect()
->to((host_url() === null ? config('App') ->baseURL : host_url()) . config('App')->installGateway)
->withInput()
->with('errors', $this->validator->getErrors());
}
$baseUrl = $this->request->getPost('hostname');
$mediaBaseUrl = $this->request->getPost('media_base_url');
self::writeEnv([
'app.baseURL' => $baseUrl,
'app.mediaBaseURL' =>
$mediaBaseUrl === '' ? $baseUrl : $mediaBaseUrl,
'app.adminGateway' => $this->request->getPost('admin_gateway'),
'app.authGateway' => $this->request->getPost('auth_gateway'),
]);
helper('text');
// redirect to full install url with new baseUrl input
return redirect()->to(reduce_double_slashes($baseUrl . '/' . config('App')->installGateway));
}
public function databaseConfig(): string
{
return view('install/database_config');
}
public function attemptDatabaseConfig(): RedirectResponse
{
$rules = [
'db_hostname' => 'required',
'db_name' => 'required',
'db_username' => 'required',
'db_password' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'database.default.hostname' => $this->request->getPost('db_hostname'),
'database.default.database' => $this->request->getPost('db_name'),
'database.default.username' => $this->request->getPost('db_username'),
'database.default.password' => $this->request->getPost('db_password'),
'database.default.DBPrefix' => $this->request->getPost('db_prefix'),
]);
return redirect()->back();
}
public function cacheConfig(): string
{
return view('install/cache_config');
}
public function attemptCacheConfig(): RedirectResponse
{
$rules = [
'cache_handler' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'cache.handler' => $this->request->getPost('cache_handler'),
]);
return redirect()->back();
}
/**
* Runs all database migrations required for instance.
*/
public function migrate(): void
{
$migrations = Services::migrations();
$migrations->setNamespace('Myth\Auth')
->latest();
$migrations->setNamespace('ActivityPub')
->latest();
$migrations->setNamespace('Analytics')
->latest();
$migrations->setNamespace(APP_NAMESPACE)
->latest();
}
/**
* Runs all database seeds required for instance.
*/
public function seed(): void
{
$seeder = Database::seeder();
// Seed database
$seeder->call('AppSeeder');
}
/**
* Returns the form to create a the first superadmin user for the instance.
*/
public function createSuperAdmin(): string
{
return view('install/create_superadmin');
}
/**
* Creates the first superadmin user or redirects back to form if any error.
*
* After creation, user is redirected to login page to input its credentials.
*/
public function attemptCreateSuperAdmin(): RedirectResponse
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules([
'only' => ['username'],
]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
],
);
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new User($this->request->getPost());
// Activate user
$user->activate();
$db = db_connect();
$db->transStart();
if (! ($userId = $userModel->insert($user, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// add newly created user to superadmin group
$authorization = Services::authorization();
$authorization->addUserToGroup($userId, 'superadmin');
$db->transComplete();
// Success!
// set redirect_url session as admin area to go to after login
session()
->set('redirect_url', route_to('admin'));
return redirect()
->route('login')
->with('message', lang('Install.messages.createSuperAdminSuccess'));
}
/**
* writes config values in .env file overwrites any existing key and appends new ones
*
* @param array<string, string> $configData key/value config pairs
*/
public static function writeEnv(array $configData): void
{
$envData = file(ROOTPATH . '.env'); // reads an array of lines
foreach ($configData as $key => $value) {
$replaced = false;
$keyVal = $key . '="' . $value . '"' . PHP_EOL;
$envData = array_map(
function ($line) use ($key, $keyVal, &$replaced) {
if (str_starts_with($line, $key)) {
$replaced = true;
return $keyVal;
}
return $line;
},
$envData
);
if (! $replaced) {
$envData[] = $keyVal;
}
}
file_put_contents(ROOTPATH . '.env', implode('', $envData));
}
}

View file

@ -0,0 +1,72 @@
<?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\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface;
class MapController extends BaseController
{
public function index(): string
{
$cacheName = implode(
'_',
array_filter([
'page',
'map',
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
return view('pages/map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
public function getEpisodesMarkers(): ResponseInterface
{
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = new EpisodeModel()
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->where('location_geo is not')
->findAll();
$found = [];
foreach ($episodes as $episode) {
$found[] = [
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'location_name' => esc($episode->location->name),
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'cover_url' => $episode->cover->thumbnail_url,
'podcast_title' => esc($episode->podcast->title),
'episode_title' => esc($episode->title),
];
}
// The page cache is set to a decade so it is deleted manually upon episode update
cache()
->save($cacheName, $found, DECADE);
}
return $this->response->setJSON($found);
}
}

View file

@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface;
class MapMarkerController extends BaseController
{
public function index(): string
{
$locale = service('request')
->getLocale();
$cacheName = "page_map_{$locale}";
if (! ($found = cache($cacheName))) {
$found = view('map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
public function getEpisodesMarkers(): ResponseInterface
{
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = (new EpisodeModel())
->where('`published_at` <= NOW()', null, false)
->where('location_geo is not', null)
->findAll();
$found = [];
foreach ($episodes as $episode) {
$found[] = [
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'location_name' => $episode->location->name,
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'image_path' => $episode->image->thumbnail_url,
'podcast_title' => $episode->podcast->title,
'episode_title' => $episode->title,
];
}
// The page cache is set to a decade so it is deleted manually upon episode update
cache()
->save($cacheName, $found, DECADE);
}
return $this->response->setJSON($found);
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -20,13 +20,13 @@ class PageController extends BaseController
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
if (
($page = (new PageModel())->where('slug', $params[0])->first()) === null
) {
$page = new PageModel()
->where('slug', $params[0])->first();
if (! $page instanceof Page) {
throw PageNotFoundException::forPageNotFound();
}
@ -37,13 +37,25 @@ class PageController extends BaseController
public function index(): string
{
$cacheName = "page-{$this->page->slug}";
$cacheName = implode(
'_',
array_filter([
'page',
$this->page->slug,
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
set_page_metatags($this->page);
$data = [
'page' => $this->page,
];
$found = view('page', $data);
$found = view('pages/page', $data);
// The page cache is set to a decade so it is deleted manually upon page update
cache()

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @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

@ -3,25 +3,24 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Podcast;
use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class PodcastController extends BaseController
{
@ -31,12 +30,12 @@ class PodcastController extends BaseController
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
if (
($podcast = (new PodcastModel())->getPodcastByName($params[0])) === null
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
@ -48,7 +47,7 @@ class PodcastController extends BaseController
return $this->{$method}(...$params);
}
public function podcastActor(): RedirectResponse
public function podcastActor(): ResponseInterface
{
$podcastActor = new PodcastActor($this->podcast);
@ -59,10 +58,7 @@ class PodcastController extends BaseController
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
@ -72,30 +68,79 @@ class PodcastController extends BaseController
'activity',
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_podcast_metatags($this->podcast, 'activity');
$data = [
'podcast' => $this->podcast,
'statuses' => (new StatusModel())->getActorPublishedStatuses($this->podcast->actor_id),
'posts' => new PostModel()
->getActorPublishedPosts($this->podcast->actor_id),
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/activity_authenticated', $data);
return view('podcast/activity', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function about(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'about',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$stats = new EpisodeModel()
->getPodcastStats($this->podcast->id);
set_podcast_metatags($this->podcast, 'about');
$data = [
'podcast' => $this->podcast,
'stats' => $stats,
];
// // if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('podcast/about', $data);
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -105,16 +150,14 @@ class PodcastController extends BaseController
public function episodes(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season');
if (! $yearQuery && ! $seasonQuery) {
$defaultQuery = (new PodcastModel())->getDefaultQuery($this->podcast->id);
$defaultQuery = new PodcastModel()
->getDefaultQuery($this->podcast->id);
if ($defaultQuery) {
if ($defaultQuery['type'] === 'season') {
$seasonQuery = $defaultQuery['data']['season_number'];
@ -134,7 +177,9 @@ class PodcastController extends BaseController
$seasonQuery ? 'season' . $seasonQuery : null,
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -150,18 +195,17 @@ class PodcastController extends BaseController
$isActive = $yearQuery === $year['year'];
if ($isActive) {
$activeQuery = [
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
];
}
$episodesNavigation[] = [
'label' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?year=' .
$year['year'],
'is_active' => $isActive,
@ -172,7 +216,7 @@ class PodcastController extends BaseController
$isActive = $seasonQuery === $season['season_number'];
if ($isActive) {
$activeQuery = [
'type' => 'season',
'type' => 'season',
'value' => $season['season_number'],
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
@ -186,38 +230,30 @@ class PodcastController extends BaseController
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?season=' .
$season['season_number'],
'is_active' => $isActive,
];
}
set_podcast_metatags($this->podcast, 'episodes');
$data = [
'podcast' => $this->podcast,
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id,
$this->podcast->type,
$yearQuery,
$seasonQuery,
),
'episodes' => new EpisodeModel()
->getPodcastEpisodes($this->podcast->id, $this->podcast->type, $yearQuery, $seasonQuery),
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
// if user is logged in then send to the authenticated episodes view
if (can_user_interact()) {
return view('podcast/episodes_authenticated', $data);
if (auth()->loggedIn()) {
return view('podcast/episodes', $data);
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -225,19 +261,16 @@ class PodcastController extends BaseController
return $cachedView;
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeCollection(): Response
public function episodeCollection(): ResponseInterface
{
if ($this->podcast->type === 'serial') {
// podcast is serial
$episodes = model('EpisodeModel')
->where('`published_at` <= NOW()', null, false)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('season_number DESC, number ASC');
} else {
$episodes = model('EpisodeModel')
->where('`published_at` <= NOW()', null, false)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'DESC');
}
@ -252,10 +285,8 @@ class PodcastController extends BaseController
$pager = $episodes->pager;
$orderedItems = [];
if ($paginatedEpisodes !== null) {
foreach ($paginatedEpisodes as $episode) {
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
}
foreach ($paginatedEpisodes as $episode) {
$orderedItems[] = new PodcastEpisode($episode)->toArray();
}
// @phpstan-ignore-next-line
@ -266,4 +297,12 @@ class PodcastController extends BaseController
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function links(): string
{
set_podcast_metatags($this->podcast, 'links');
return view('podcast/links', [
'podcast' => $this->podcast,
]);
}
}

View file

@ -0,0 +1,275 @@
<?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\Entities\Actor;
use App\Entities\Podcast;
use App\Entities\Post as CastopodPost;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Override;
class PostController extends FediversePostController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
/**
* @var CastopodPost
*/
protected $post;
/**
* @var list<string>
*/
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
#[Override]
public function _remap(string $method, string ...$params): mixed
{
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (count($params) <= 1) {
unset($params[0]);
return $this->{$method}(...$params);
}
if (
! ($post = new PostModel()->getPostById($params[1])) instanceof CastopodPost
) {
throw PageNotFoundException::forPageNotFound();
}
$this->post = $post;
// show 404 if post is private
if ($this->post->is_private && ! can_user_interact()) {
throw PageNotFoundException::forPageNotFound();
}
unset($params[0]);
unset($params[1]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"post#{$this->post->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_post_metatags($this->post);
$data = [
'post' => $this->post,
'podcast' => $this->podcast,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('post/post', $data);
}
return view('post/post', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
#[Override]
public function createAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url_strict|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$message = $validData['message'];
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $validData['episode_url'];
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = new EpisodeModel()->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
) {
$newPost->episode_id = $episode->id;
}
$newPost->message = $message;
$postModel = new PostModel();
if (
! $postModel
->addPost($newPost, ! (bool) $newPost->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Post has been successfully created
return redirect()->back();
}
#[Override]
public function replyAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $validData['message'],
'is_private' => $this->post->is_private,
'published_at' => Time::now(),
'created_by' => user_id(),
]);
if ($this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
$postModel = new PostModel();
if (! $postModel->addReply($newPost)) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Reply post without preview card has been successfully created
return redirect()->back();
}
#[Override]
public function favouriteAction(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back();
}
#[Override]
public function reblogAction(): RedirectResponse
{
new PostModel()
->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back();
}
public function action(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$action = $validData['action'];
return match ($action) {
'favourite' => $this->favouriteAction(),
'reblog' => $this->reblogAction(),
'reply' => $this->replyAction(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteActionView(string $action): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
set_remote_actions_metatags($this->post, $action);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
helper('form');
// NO VIEW CACHING: form has a CSRF token which should change on each request
return view('post/remote_action', $data);
}
}

View file

@ -1,254 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Controllers\StatusController as ActivityPubStatusController;
use ActivityPub\Entities\Status as ActivityPubStatus;
use Analytics\AnalyticsTrait;
use App\Entities\Actor;
use App\Entities\Podcast;
use App\Entities\Status as CastopodStatus;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
class StatusController extends ActivityPubStatusController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
/**
* @var string[]
*/
protected $helpers = ['auth', 'activitypub', 'svg', 'components', 'misc'];
public function _remap(string $method, string ...$params): mixed
{
if (
($podcast = (new PodcastModel())->getPodcastByName($params[0],)) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (
count($params) > 1 &&
($status = (new StatusModel())->getStatusById($params[1])) !== null
) {
$this->status = $status;
unset($params[0]);
unset($params[1]);
}
return $this->{$method}(...$params);
}
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"status#{$this->status->id}",
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'status' => $this->status,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/status_authenticated', $data);
}
return view('podcast/status', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$message = $this->request->getPost('message');
$newStatus = new CastopodStatus([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $this->request->getPost('episode_url');
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastName'], $params['episodeSlug']))
) {
$newStatus->episode_id = $episode->id;
}
$newStatus->message = $message;
$statusModel = new StatusModel();
if (
! $statusModel
->addStatus($newStatus, ! (bool) $newStatus->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
// Status has been successfully created
return redirect()->back();
}
public function attemptReply(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$newStatus = new ActivityPubStatus([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->status->id,
'message' => $this->request->getPost('message'),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
$statusModel = new StatusModel();
if (! $statusModel->addReply($newStatus)) {
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
// Reply status without preview card has been successfully created
return redirect()->back();
}
public function attemptFavourite(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->status);
return redirect()->back();
}
public function attemptReblog(): RedirectResponse
{
(new StatusModel())->toggleReblog(interact_as_actor(), $this->status);
return redirect()->back();
}
public function attemptAction(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$action = $this->request->getPost('action');
return match ($action) {
'favourite' => $this->attemptFavourite(),
'reblog' => $this->attemptReblog(),
'reply' => $this->attemptReply(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteAction(string $action): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter(['page', "status#{$this->status->id}", "remote_{$action}", service('request') ->getLocale()]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'status' => $this->status,
'action' => $action,
];
helper('form');
return view('podcast/status_remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return (string) $cachedView;
}
}

View file

@ -0,0 +1,117 @@
<?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\Entities\Podcast;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
class WebmanifestController extends Controller
{
/**
* @var array<string, array<string, string>>
*/
final public const array THEME_COLORS = [
'pine' => [
'theme' => '#009486',
'background' => '#F0F9F8',
],
'lake' => [
'theme' => '#00ACE0',
'background' => '#F0F7F9',
],
'jacaranda' => [
'theme' => '#562CDD',
'background' => '#F2F0F9',
],
'crimson' => [
'theme' => '#F24562',
'background' => '#F9F0F2',
],
'amber' => [
'theme' => '#FF6224',
'background' => '#F9F3F0',
],
'onyx' => [
'theme' => '#040406',
'background' => '#F3F3F7',
],
];
public function index(): ResponseInterface
{
helper('misc');
$webmanifest = [
'name' => esc(service('settings') ->get('App.siteName')),
'description' => esc(service('settings') ->get('App.siteDescription')),
'lang' => service('request')
->getLocale(),
'start_url' => base_url(),
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => get_site_icon_url('192'),
'type' => 'image/png',
'sizes' => '192x192',
],
[
'src' => get_site_icon_url('512'),
'type' => 'image/png',
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
public function podcastManifest(string $podcastHandle): ResponseInterface
{
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($podcastHandle)) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$webmanifest = [
'name' => esc($podcast->title),
'short_name' => $podcast->at_handle,
'description' => $podcast->description,
'lang' => $podcast->language_code,
'start_url' => $podcast->link,
'scope' => '/' . $podcast->at_handle,
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'sizes' => '192x192',
],
[
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
}

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddAddPodcastsPlatforms Creates podcasts_platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'link_content' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embeddable_player' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->createTable('podcasts_platforms');
}
public function down(): void
{
$this->forge->dropTable('podcasts_platforms');
}
}

View file

@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCategories Creates categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCategories extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'code' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('code');
$this->forge->addForeignKey('parent_id', 'categories', 'id');
$this->forge->createTable('categories');
}
public function down(): void
{
$this->forge->dropTable('categories');
}
}

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddLanguages Creates languages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddLanguages extends Migration
{
public function up(): void
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
$this->forge->addPrimaryKey('code');
$this->forge->createTable('languages');
}
public function down(): void
{
$this->forge->dropTable('languages');
}
}

View file

@ -1,211 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcasts Creates podcasts table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcasts extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'guid' => [
'type' => 'CHAR',
'constraint' => 36,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('name');
$this->forge->addUniqueKey('guid');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('podcasts');
}
public function down(): void
{
$this->forge->dropTable('podcasts');
}
}

View file

@ -1,200 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodes Creates episodes table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodes extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
],
'audio_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_duration' => [
// exact value for duration with max 99999,999 ~ 27.7 hours
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
'comment' => 'Playtime in seconds',
],
'audio_file_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'File size in bytes',
],
'audio_file_header_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'Header size in bytes',
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
],
'transcript_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'transcript_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'chapters_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'chapters_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'favourites_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'reblogs_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'statuses_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
}
public function down(): void
{
$this->forge->dropTable('episodes');
}
}

View file

@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddSoundbites Creates soundbites table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddSoundbites extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'start_time' => [
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
],
'duration' => [
// soundbite duration cannot be higher than 9999,999 seconds ~ 2.77 hours
'type' => 'DECIMAL(7,3)',
'unsigned' => true,
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('soundbites');
}
public function down(): void
{
$this->forge->dropTable('soundbites');
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPlatforms Creates platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPlatforms extends Migration
{
public function up(): void
{
$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,
'default' => null,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField('`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()');
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
public function down(): void
{
$this->forge->dropTable('platforms');
}
}

View file

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastUsers Creates podcast_users table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsUsers extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'user_id' => [
'type' => 'INT',
'unsigned' => true,
],
'group_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id', '', 'CASCADE');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('group_id', 'auth_groups', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_users');
}
public function down(): void
{
$this->forge->dropTable('podcasts_users');
}
}

View file

@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPages Creates pages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPages extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
'unique' => true,
],
'content_markdown' => [
'type' => 'TEXT',
],
'content_html' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
public function down(): void
{
$this->forge->dropTable('pages');
}
}

View file

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsCategories Creates podcasts_categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsCategories extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_categories');
}
public function down(): void
{
$this->forge->dropTable('podcasts_categories');
}
}

View file

@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class Persons Creates persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'full_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the full name or alias of the person.',
],
'unique_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the slug name or alias of the person.',
'unique' => true,
],
'information_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
}
public function down(): void
{
$this->forge->dropTable('persons');
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsPersons Creates podcasts_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['podcast_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_persons');
}
public function down(): void
{
$this->forge->dropTable('podcasts_persons');
}
}

View file

@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodesPersons Creates episodes_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodesPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'episode_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('episodes_persons');
}
public function down(): void
{
$this->forge->dropTable('episodes_persons');
}
}

View file

@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCreditView Creates Credit View in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreditView extends Migration
{
public function up(): void
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personsTable = $this->db->prefixTable('persons');
$podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<CODE_SAMPLE
CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
INNER JOIN `{$episodesTable}`
ON (`episode_id`=`{$episodesTable}`.`id`)
WHERE `{$episodesTable}`.published_at <= NOW()
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `{$viewName}`");
}
}

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeIdToStatuses Adds episode_id field to activitypub_statuses table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodeIdToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_episode_id_foreign');
$this->forge->dropColumn('activitypub_statuses', 'episode_id');
}
}

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToStatuses Adds created_by field to activitypub_statuses table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreatedByToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_created_by_foreign');
$this->forge->dropColumn('activitypub_statuses', 'created_by');
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* Class AddCategories Creates categories table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddCategories extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'code' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('code');
$this->forge->addForeignKey('parent_id', 'categories', 'id');
$this->forge->createTable('categories');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('categories');
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* Class AddLanguages Creates languages table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddLanguages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
$this->forge->addPrimaryKey('code');
$this->forge->createTable('languages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('languages');
}
}

View file

@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcasts Creates podcasts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcasts extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'guid' => [
'type' => 'CHAR',
'constraint' => 36,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'handle' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
],
'banner_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'is_premium_by_default' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('handle');
$this->forge->addUniqueKey('guid');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('cover_id', 'media', 'id');
$this->forge->addForeignKey('banner_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('podcasts');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts');
}
}

View file

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodes Creates episodes table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'audio_id' => [
'type' => 'INT',
'unsigned' => true,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'transcript_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'transcript_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'chapters_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'chapters_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
],
'number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'posts_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'comments_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'is_premium' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('audio_id', 'media', 'id');
$this->forge->addForeignKey('cover_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('transcript_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('chapters_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
// Add Full-Text Search index on title and description_markdown
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
ADD FULLTEXT title (title, description_markdown);
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episodes');
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* Class AddPlatforms Creates platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$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');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('platforms');
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* Class AddAddPodcastsPlatforms Creates podcasts_platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$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');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_platforms');
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeComments creates episode_comments table in database
*
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodeComments extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'BINARY',
'constraint' => 16,
],
'uri' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'in_reply_to_id' => [
'type' => 'BINARY',
'constraint' => 16,
'null' => true,
],
'message' => [
'type' => 'VARCHAR',
'constraint' => 5000,
],
'message_html' => [
'type' => 'VARCHAR',
'constraint' => 6000,
],
'likes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->createTable('episode_comments');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episode_comments');
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* Class AddLikes Creates likes table in database
*
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddLikes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'comment_id' => [
'type' => 'BINARY',
'constraint' => 16,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()');
$this->forge->addPrimaryKey(['actor_id', 'comment_id']);
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('comment_id', 'episode_comments', 'id', '', 'CASCADE');
$this->forge->createTable('likes');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('likes');
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* Class AddPages Creates pages table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 128,
'unique' => true,
],
'content_markdown' => [
'type' => 'TEXT',
],
'content_html' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('pages');
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsCategories Creates podcasts_categories table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsCategories extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_categories');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_categories');
}
}

View file

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddClips extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'start_time' => [
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
],
'duration' => [
// clip duration cannot be higher than 9999,999 seconds ~ 2.77 hours
'type' => 'DECIMAL(7,3)',
'unsigned' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['audio', 'video'],
],
'media_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'metadata' => [
'type' => 'JSON',
'null' => true,
],
'status' => [
'type' => 'ENUM',
'constraint' => ['queued', 'pending', 'running', 'passed', 'failed'],
],
'logs' => [
'type' => 'TEXT',
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'job_started_at' => [
'type' => 'DATETIME',
'null' => true,
],
'job_ended_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('media_id', 'media', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('clips');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('clips');
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* Class Persons Creates persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'full_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the full name or alias of the person.',
],
'unique_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the slug name or alias of the person.',
'unique' => true,
],
'information_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'avatar_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('avatar_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('persons');
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsPersons Creates podcasts_persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['podcast_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_persons');
}
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodesPersons Creates episodes_persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodesPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'episode_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('episodes_persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episodes_persons');
}
}

View file

@ -0,0 +1,49 @@
<?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\Database\Migrations;
use Override;
class AddCreditsView extends BaseMigration
{
#[Override]
public function up(): void
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personsTable = $this->db->prefixTable('persons');
$podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<SQL
CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
INNER JOIN `{$episodesTable}`
ON (`episode_id`=`{$episodesTable}`.`id`)
WHERE `{$episodesTable}`.published_at <= UTC_TIMESTAMP()
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `{$viewName}`");
}
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeIdToPosts Adds episode_id field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodeIdToPosts extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$this->forge->addColumn('fediverse_posts', [
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'replies_count',
],
]);
$this->forge->addForeignKey(
'episode_id',
'episodes',
'id',
'',
'CASCADE',
$prefix . 'fediverse_posts_episode_id_foreign',
);
$this->forge->processIndexes('fediverse_posts');
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropForeignKey('fediverse_posts', $prefix . 'fediverse_posts_episode_id_foreign');
$this->forge->dropColumn('fediverse_posts', 'episode_id');
}
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToPosts Adds created_by field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddCreatedByToPosts extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$this->forge->addColumn('fediverse_posts', [
'created_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'episode_id',
],
]);
$this->forge->addForeignKey(
'created_by',
'users',
'id',
'',
'CASCADE',
$prefix . 'fediverse_posts_created_by_foreign',
);
$this->forge->processIndexes('fediverse_posts');
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropForeignKey('fediverse_posts', $prefix . 'fediverse_posts_created_by_foreign');
$this->forge->dropColumn('fediverse_posts', 'created_by');
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use Override;
class AddFullTextSearchIndexes extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes DROP INDEX title;
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
ADD FULLTEXT episodes_search (title, description_markdown, slug, location_name);
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}podcasts
ADD FULLTEXT podcasts_search (title, description_markdown, handle, location_name);
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
DROP INDEX episodes_search;
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}podcasts
DROP INDEX podcasts_search;
SQL;
$this->db->query($createQuery);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use Override;
class AddEpisodePreviewId extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'preview_id' => [
'type' => 'BINARY',
'constraint' => 16,
'after' => 'podcast_id',
],
];
$this->forge->addColumn('episodes', $fields);
// set preview_id as unique key
$prefix = $this->db->getPrefix();
$uniquePreviewId = <<<CODE_SAMPLE
ALTER TABLE `{$prefix}episodes`
ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`);
CODE_SAMPLE;
$this->db->query($uniquePreviewId);
}
#[Override]
public function down(): void
{
$fields = ['preview_id'];
$this->forge->dropColumn('episodes', $fields);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsOwnerEmailRemovedFromFeed adds is_owner_email_removed_from_feed field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'is_owner_email_removed_from_feed' => [
'type' => 'BOOLEAN',
'null' => false,
'default' => 0,
'after' => 'owner_email',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$fields = ['is_owner_email_removed_from_feed'];
$this->forge->dropColumn('podcasts', $fields);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsMediumField extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'medium' => [
'type' => "ENUM('podcast','music','audiobook')",
'null' => false,
'default' => 'podcast',
'after' => 'type',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$fields = ['medium'];
$this->forge->dropColumn('podcasts', $fields);
}
}

View file

@ -0,0 +1,38 @@
<?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;
use Override;
class AddPodcastsVerifyTxtField extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$this->forge->dropColumn('podcasts', 'verify_txt');
}
}

View file

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

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodeDownloadsCount extends Migration
{
public function up(): void
{
$fields = [
'downloads_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
'after' => 'is_published_on_hubs',
],
];
$this->forge->addColumn('episodes', $fields);
}
public function down(): void
{
$this->forge->dropColumn('episodes', 'downloads_count');
}
}

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedPodcastsFields extends BaseMigration
{
#[Override]
public function up(): void
{
// TODO: migrate data
$this->forge->dropColumn(
'podcasts',
'episode_description_footer_markdown,episode_description_footer_html,is_owner_email_removed_from_feed,medium,payment_pointer,verify_txt,custom_rss,partner_id,partner_link_url,partner_image_url',
);
}
#[Override]
public function down(): void
{
$fields = [
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_owner_email_removed_from_feed' => [
'type' => 'BOOLEAN',
'null' => false,
'default' => 0,
'after' => 'owner_email',
],
'medium' => [
'type' => "ENUM('podcast','music','audiobook')",
'null' => false,
'default' => 'podcast',
'after' => 'type',
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
];
$this->forge->addColumn('podcasts', $fields);
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedEpisodesFields extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->dropColumn('episodes', 'custom_rss');
}
#[Override]
public function down(): void
{
$fields = [
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
];
$this->forge->addColumn('episodes', $fields);
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToPosts Adds created_by field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Migration;
use Override;
class BaseMigration extends Migration
{
/**
* Database Connection instance
*
* @var BaseConnection
*/
protected $db;
#[Override]
public function up(): void
{
}
#[Override]
public function down(): void
{
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/**
* Class AppSeeder Calls all required seeders for castopod to work properly
*
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -13,14 +13,14 @@ declare(strict_types=1);
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Override;
class AppSeeder extends Seeder
{
#[Override]
public function run(): void
{
$this->call('AuthSeeder');
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('PlatformSeeder');
}
}

View file

@ -1,314 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PermissionSeeder Inserts permissions
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class AuthSeeder extends Seeder
{
/**
* @var array<string, string>[]
*/
protected array $groups = [
[
'name' => 'superadmin',
'description' =>
'Somebody who has access to all the castopod instance features',
],
[
'name' => 'podcast_admin',
'description' =>
'Somebody who has access to all the features within a given podcast',
],
];
/**
* Build permissions array as a list of:
*
* ``` context => [ [action, description], [action, description], ... ] ```
*
* @var array<string, array<string, string|array>[]>
*/
protected array $permissions = [
'users' => [
[
'name' => 'create',
'description' => 'Create a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all users',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any user info',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_authorizations',
'description' => 'Add or remove roles/permissions to a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_bans',
'description' => 'Ban / unban a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'force_pass_reset',
'description' =>
'Force a user to update his password upon next login',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete user without removing him from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of a user from the database',
'has_permission' => ['superadmin'],
],
],
'pages' => [
[
'name' => 'manage',
'description' => 'List / create / edit / delete pages',
'has_permission' => ['superadmin'],
],
],
'podcasts' => [
[
'name' => 'create',
'description' => 'Add a new podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'import',
'description' => 'Import a new podcast from an external feed',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all podcasts and their episodes',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any podcast and their contributors list',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete a podcast without removing it from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' => 'Delete any podcast from the database',
'has_permission' => ['superadmin'],
],
],
'episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of any podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any episode of any podcast',
'has_permission' => ['superadmin'],
],
],
'podcast' => [
[
'name' => 'view',
'description' => 'View a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_contributors',
'description' =>
'Add / remove contributors to a podcast and edit their roles',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_platforms',
'description' => 'Set / remove platform links of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_publications',
'description' =>
'Publish / unpublish episodes & statuses of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'description' =>
'Interact as the podcast to favourite / share or reply to statuses.',
'has_permission' => ['podcast_admin'],
],
],
'podcast_episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'view',
'description' => 'View any episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'create',
'description' => 'Add new episodes for a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit an episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete',
'description' =>
'Delete an episode of a podcast without removing it from the database',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of an episode of a podcast from the database',
'has_permission' => ['podcast_admin'],
],
],
'person' => [
[
'name' => 'create',
'description' => 'Add a new person',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all persons',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any person',
'has_permission' => ['superadmin'],
],
[
'name' => 'edit',
'description' => 'Edit a person',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete permanently any person from the database',
'has_permission' => ['superadmin'],
],
],
'fediverse' => [
[
'name' => 'block_actors',
'description' =>
'Block an activitypub actors from interacting with the instance.',
'has_permission' => ['superadmin'],
],
[
'name' => 'block_domains',
'description' =>
'Block an activitypub domains from interacting with the instance.',
'has_permission' => ['superadmin'],
],
],
];
public function run(): void
{
$groupId = 0;
$dataGroups = [];
foreach ($this->groups as $group) {
$dataGroups[] = [
'id' => ++$groupId,
'name' => $group['name'],
'description' => $group['description'],
];
}
// Map permissions to a format the `auth_permissions` table expects
$dataPermissions = [];
$dataGroupsPermissions = [];
$permissionId = 0;
foreach ($this->permissions as $context => $actions) {
foreach ($actions as $action) {
$dataPermissions[] = [
'id' => ++$permissionId,
'name' => $context . '-' . $action['name'],
'description' => $action['description'],
];
foreach ($action['has_permission'] as $role) {
// link permission to specified groups
$dataGroupsPermissions[] = [
'group_id' => $this->getGroupIdByName($role, $dataGroups),
'permission_id' => $permissionId,
];
}
}
}
$this->db
->table('auth_permissions')
->ignore(true)
->insertBatch($dataPermissions);
$this->db
->table('auth_groups')
->ignore(true)
->insertBatch($dataGroups);
$this->db
->table('auth_groups_permissions')
->ignore(true)
->insertBatch($dataGroupsPermissions);
}
/**
* @param array<string, string|int>[] $dataGroups
*/
public static function getGroupIdByName(string $name, array $dataGroups): ?int
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/**
* Class CategorySeeder Inserts values in categories table in database
*
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -13,787 +13,791 @@ declare(strict_types=1);
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Override;
class CategorySeeder extends Seeder
{
#[Override]
public function run(): void
{
$data = [
[
'id' => 1,
'parent_id' => null,
'code' => 'arts',
'apple_category' => 'Arts',
'id' => 1,
'parent_id' => null,
'code' => 'arts',
'apple_category' => 'Arts',
'google_category' => 'Arts',
],
[
'id' => 2,
'parent_id' => null,
'code' => 'business',
'apple_category' => 'Business',
'id' => 2,
'parent_id' => null,
'code' => 'business',
'apple_category' => 'Business',
'google_category' => 'Business',
],
[
'id' => 3,
'parent_id' => null,
'code' => 'comedy',
'apple_category' => 'Comedy',
'id' => 3,
'parent_id' => null,
'code' => 'comedy',
'apple_category' => 'Comedy',
'google_category' => 'Comedy',
],
[
'id' => 4,
'parent_id' => null,
'code' => 'education',
'apple_category' => 'Education',
'id' => 4,
'parent_id' => null,
'code' => 'education',
'apple_category' => 'Education',
'google_category' => 'Education',
],
[
'id' => 5,
'parent_id' => null,
'code' => 'fiction',
'apple_category' => 'Fiction',
'id' => 5,
'parent_id' => null,
'code' => 'fiction',
'apple_category' => 'Fiction',
'google_category' => '',
],
[
'id' => 6,
'parent_id' => null,
'code' => 'government',
'apple_category' => 'Government',
'id' => 6,
'parent_id' => null,
'code' => 'government',
'apple_category' => 'Government',
'google_category' => 'Government & Organizations',
],
[
'id' => 7,
'parent_id' => null,
'code' => 'health_and_fitness',
'apple_category' => 'Health & Fitness',
'id' => 7,
'parent_id' => null,
'code' => 'health_and_fitness',
'apple_category' => 'Health & Fitness',
'google_category' => 'Health',
],
[
'id' => 8,
'parent_id' => null,
'code' => 'history',
'apple_category' => 'History',
'id' => 8,
'parent_id' => null,
'code' => 'history',
'apple_category' => 'History',
'google_category' => '',
],
[
'id' => 9,
'parent_id' => null,
'code' => 'kids_and_family',
'apple_category' => 'Kids & Family',
'id' => 9,
'parent_id' => null,
'code' => 'kids_and_family',
'apple_category' => 'Kids & Family',
'google_category' => 'Kids & Family',
],
[
'id' => 10,
'parent_id' => null,
'code' => 'leisure',
'apple_category' => 'Leisure',
'id' => 10,
'parent_id' => null,
'code' => 'leisure',
'apple_category' => 'Leisure',
'google_category' => 'Games & Hobbies',
],
[
'id' => 11,
'parent_id' => null,
'code' => 'music',
'apple_category' => 'Music',
'id' => 11,
'parent_id' => null,
'code' => 'music',
'apple_category' => 'Music',
'google_category' => 'Music',
],
[
'id' => 12,
'parent_id' => null,
'code' => 'news',
'apple_category' => 'News',
'id' => 12,
'parent_id' => null,
'code' => 'news',
'apple_category' => 'News',
'google_category' => 'News & Politics',
],
[
'id' => 13,
'parent_id' => null,
'code' => 'religion_and_spirituality',
'apple_category' => 'Religion & Spirituality',
'id' => 13,
'parent_id' => null,
'code' => 'religion_and_spirituality',
'apple_category' => 'Religion & Spirituality',
'google_category' => 'Religion & Spirituality',
],
[
'id' => 14,
'parent_id' => null,
'code' => 'science',
'apple_category' => 'Science',
'id' => 14,
'parent_id' => null,
'code' => 'science',
'apple_category' => 'Science',
'google_category' => 'Science & Medicine',
],
[
'id' => 15,
'parent_id' => null,
'code' => 'society_and_culture',
'apple_category' => 'Society & Culture',
'id' => 15,
'parent_id' => null,
'code' => 'society_and_culture',
'apple_category' => 'Society & Culture',
'google_category' => 'Society & Culture',
],
[
'id' => 16,
'parent_id' => null,
'code' => 'sports',
'apple_category' => 'Sports',
'id' => 16,
'parent_id' => null,
'code' => 'sports',
'apple_category' => 'Sports',
'google_category' => 'Sports & Recreation',
],
[
'id' => 17,
'parent_id' => null,
'code' => 'technology',
'apple_category' => 'Technology',
'id' => 17,
'parent_id' => null,
'code' => 'technology',
'apple_category' => 'Technology',
'google_category' => 'Technology',
],
[
'id' => 18,
'parent_id' => null,
'code' => 'true_crime',
'apple_category' => 'True Crime',
'id' => 18,
'parent_id' => null,
'code' => 'true_crime',
'apple_category' => 'True Crime',
'google_category' => '',
],
[
'id' => 19,
'parent_id' => null,
'code' => 'tv_and_film',
'apple_category' => 'TV & Film',
'id' => 19,
'parent_id' => null,
'code' => 'tv_and_film',
'apple_category' => 'TV & Film',
'google_category' => 'TV & Film',
],
[
'id' => 20,
'parent_id' => 1,
'code' => 'books',
'apple_category' => 'Books',
'id' => 20,
'parent_id' => 1,
'code' => 'books',
'apple_category' => 'Books',
'google_category' => '',
],
[
'id' => 21,
'parent_id' => 1,
'code' => 'design',
'apple_category' => 'Design',
'id' => 21,
'parent_id' => 1,
'code' => 'design',
'apple_category' => 'Design',
'google_category' => '',
],
[
'id' => 22,
'parent_id' => 1,
'code' => 'fashion_and_beauty',
'apple_category' => 'Fashion & Beauty',
'id' => 22,
'parent_id' => 1,
'code' => 'fashion_and_beauty',
'apple_category' => 'Fashion & Beauty',
'google_category' => '',
],
[
'id' => 23,
'parent_id' => 1,
'code' => 'food',
'apple_category' => 'Food',
'id' => 23,
'parent_id' => 1,
'code' => 'food',
'apple_category' => 'Food',
'google_category' => '',
],
[
'id' => 24,
'parent_id' => 1,
'code' => 'performing_arts',
'apple_category' => 'Performing Arts',
'id' => 24,
'parent_id' => 1,
'code' => 'performing_arts',
'apple_category' => 'Performing Arts',
'google_category' => '',
],
[
'id' => 25,
'parent_id' => 1,
'code' => 'visual_arts',
'apple_category' => 'Visual Arts',
'id' => 25,
'parent_id' => 1,
'code' => 'visual_arts',
'apple_category' => 'Visual Arts',
'google_category' => '',
],
[
'id' => 26,
'parent_id' => 2,
'code' => 'careers',
'apple_category' => 'Careers',
'id' => 26,
'parent_id' => 2,
'code' => 'careers',
'apple_category' => 'Careers',
'google_category' => '',
],
[
'id' => 27,
'parent_id' => 2,
'code' => 'entrepreneurship',
'apple_category' => 'Entrepreneurship',
'id' => 27,
'parent_id' => 2,
'code' => 'entrepreneurship',
'apple_category' => 'Entrepreneurship',
'google_category' => '',
],
[
'id' => 28,
'parent_id' => 2,
'code' => 'investing',
'apple_category' => 'Investing',
'id' => 28,
'parent_id' => 2,
'code' => 'investing',
'apple_category' => 'Investing',
'google_category' => '',
],
[
'id' => 29,
'parent_id' => 2,
'code' => 'management',
'apple_category' => 'Management',
'id' => 29,
'parent_id' => 2,
'code' => 'management',
'apple_category' => 'Management',
'google_category' => '',
],
[
'id' => 30,
'parent_id' => 2,
'code' => 'marketing',
'apple_category' => 'Marketing',
'id' => 30,
'parent_id' => 2,
'code' => 'marketing',
'apple_category' => 'Marketing',
'google_category' => '',
],
[
'id' => 31,
'parent_id' => 2,
'code' => 'non_profit',
'apple_category' => 'Non-Profit',
'id' => 31,
'parent_id' => 2,
'code' => 'non_profit',
'apple_category' => 'Non-Profit',
'google_category' => '',
],
[
'id' => 32,
'parent_id' => 3,
'code' => 'comedy_interviews',
'apple_category' => 'Comedy Interviews',
'id' => 32,
'parent_id' => 3,
'code' => 'comedy_interviews',
'apple_category' => 'Comedy Interviews',
'google_category' => '',
],
[
'id' => 33,
'parent_id' => 3,
'code' => 'improv',
'apple_category' => 'Improv',
'id' => 33,
'parent_id' => 3,
'code' => 'improv',
'apple_category' => 'Improv',
'google_category' => '',
],
[
'id' => 34,
'parent_id' => 3,
'code' => 'stand_up',
'apple_category' => 'Stand-Up',
'id' => 34,
'parent_id' => 3,
'code' => 'stand_up',
'apple_category' => 'Stand-Up',
'google_category' => '',
],
[
'id' => 35,
'parent_id' => 4,
'code' => 'courses',
'apple_category' => 'Courses',
'id' => 35,
'parent_id' => 4,
'code' => 'courses',
'apple_category' => 'Courses',
'google_category' => '',
],
[
'id' => 36,
'parent_id' => 4,
'code' => 'how_to',
'apple_category' => 'How To',
'id' => 36,
'parent_id' => 4,
'code' => 'how_to',
'apple_category' => 'How To',
'google_category' => '',
],
[
'id' => 37,
'parent_id' => 4,
'code' => 'language_learning',
'apple_category' => 'Language Learning',
'id' => 37,
'parent_id' => 4,
'code' => 'language_learning',
'apple_category' => 'Language Learning',
'google_category' => '',
],
[
'id' => 38,
'parent_id' => 4,
'code' => 'self_improvement',
'apple_category' => 'Self-Improvement',
'id' => 38,
'parent_id' => 4,
'code' => 'self_improvement',
'apple_category' => 'Self-Improvement',
'google_category' => '',
],
[
'id' => 39,
'parent_id' => 5,
'code' => 'comedy_fiction',
'apple_category' => 'Comedy Fiction',
'id' => 39,
'parent_id' => 5,
'code' => 'comedy_fiction',
'apple_category' => 'Comedy Fiction',
'google_category' => '',
],
[
'id' => 40,
'parent_id' => 5,
'code' => 'drama',
'apple_category' => 'Drama',
'id' => 40,
'parent_id' => 5,
'code' => 'drama',
'apple_category' => 'Drama',
'google_category' => '',
],
[
'id' => 41,
'parent_id' => 5,
'code' => 'science_fiction',
'apple_category' => 'Science Fiction',
'id' => 41,
'parent_id' => 5,
'code' => 'science_fiction',
'apple_category' => 'Science Fiction',
'google_category' => '',
],
[
'id' => 42,
'parent_id' => 7,
'code' => 'alternative_health',
'apple_category' => 'Alternative Health',
'id' => 42,
'parent_id' => 7,
'code' => 'alternative_health',
'apple_category' => 'Alternative Health',
'google_category' => '',
],
[
'id' => 43,
'parent_id' => 7,
'code' => 'fitness',
'apple_category' => 'Fitness',
'id' => 43,
'parent_id' => 7,
'code' => 'fitness',
'apple_category' => 'Fitness',
'google_category' => '',
],
[
'id' => 44,
'parent_id' => 7,
'code' => 'medicine',
'apple_category' => 'Medicine',
'id' => 44,
'parent_id' => 7,
'code' => 'medicine',
'apple_category' => 'Medicine',
'google_category' => '',
],
[
'id' => 45,
'parent_id' => 7,
'code' => 'mental_health',
'apple_category' => 'Mental Health',
'id' => 45,
'parent_id' => 7,
'code' => 'mental_health',
'apple_category' => 'Mental Health',
'google_category' => '',
],
[
'id' => 46,
'parent_id' => 7,
'code' => 'nutrition',
'apple_category' => 'Nutrition',
'id' => 46,
'parent_id' => 7,
'code' => 'nutrition',
'apple_category' => 'Nutrition',
'google_category' => '',
],
[
'id' => 47,
'parent_id' => 7,
'code' => 'sexuality',
'apple_category' => 'Sexuality',
'id' => 47,
'parent_id' => 7,
'code' => 'sexuality',
'apple_category' => 'Sexuality',
'google_category' => '',
],
[
'id' => 48,
'parent_id' => 9,
'code' => 'education_for_kids',
'apple_category' => 'Education for Kids',
'id' => 48,
'parent_id' => 9,
'code' => 'education_for_kids',
'apple_category' => 'Education for Kids',
'google_category' => '',
],
[
'id' => 49,
'parent_id' => 9,
'code' => 'parenting',
'apple_category' => 'Parenting',
'id' => 49,
'parent_id' => 9,
'code' => 'parenting',
'apple_category' => 'Parenting',
'google_category' => '',
],
[
'id' => 50,
'parent_id' => 9,
'code' => 'pets_and_animals',
'apple_category' => 'Pets & Animals',
'id' => 50,
'parent_id' => 9,
'code' => 'pets_and_animals',
'apple_category' => 'Pets & Animals',
'google_category' => '',
],
[
'id' => 51,
'parent_id' => 9,
'code' => 'stories_for_kids',
'apple_category' => 'Stories for Kids',
'id' => 51,
'parent_id' => 9,
'code' => 'stories_for_kids',
'apple_category' => 'Stories for Kids',
'google_category' => '',
],
[
'id' => 52,
'parent_id' => 10,
'code' => 'animation_and_manga',
'apple_category' => 'Animation & Manga',
'id' => 52,
'parent_id' => 10,
'code' => 'animation_and_manga',
'apple_category' => 'Animation & Manga',
'google_category' => '',
],
[
'id' => 53,
'parent_id' => 10,
'code' => 'automotive',
'apple_category' => 'Automotive',
'id' => 53,
'parent_id' => 10,
'code' => 'automotive',
'apple_category' => 'Automotive',
'google_category' => '',
],
[
'id' => 54,
'parent_id' => 10,
'code' => 'aviation',
'apple_category' => 'Aviation',
'id' => 54,
'parent_id' => 10,
'code' => 'aviation',
'apple_category' => 'Aviation',
'google_category' => '',
],
[
'id' => 55,
'parent_id' => 10,
'code' => 'crafts',
'apple_category' => 'Crafts',
'id' => 55,
'parent_id' => 10,
'code' => 'crafts',
'apple_category' => 'Crafts',
'google_category' => '',
],
[
'id' => 56,
'parent_id' => 10,
'code' => 'games',
'apple_category' => 'Games',
'id' => 56,
'parent_id' => 10,
'code' => 'games',
'apple_category' => 'Games',
'google_category' => '',
],
[
'id' => 57,
'parent_id' => 10,
'code' => 'hobbies',
'apple_category' => 'Hobbies',
'id' => 57,
'parent_id' => 10,
'code' => 'hobbies',
'apple_category' => 'Hobbies',
'google_category' => '',
],
[
'id' => 58,
'parent_id' => 10,
'code' => 'home_and_garden',
'apple_category' => 'Home & Garden',
'id' => 58,
'parent_id' => 10,
'code' => 'home_and_garden',
'apple_category' => 'Home & Garden',
'google_category' => '',
],
[
'id' => 59,
'parent_id' => 10,
'code' => 'video_games',
'apple_category' => 'Video Games',
'id' => 59,
'parent_id' => 10,
'code' => 'video_games',
'apple_category' => 'Video Games',
'google_category' => '',
],
[
'id' => 60,
'parent_id' => 11,
'code' => 'music_commentary',
'apple_category' => 'Music Commentary',
'id' => 60,
'parent_id' => 11,
'code' => 'music_commentary',
'apple_category' => 'Music Commentary',
'google_category' => '',
],
[
'id' => 61,
'parent_id' => 11,
'code' => 'music_history',
'apple_category' => 'Music History',
'id' => 61,
'parent_id' => 11,
'code' => 'music_history',
'apple_category' => 'Music History',
'google_category' => '',
],
[
'id' => 62,
'parent_id' => 11,
'code' => 'music_interviews',
'apple_category' => 'Music Interviews',
'id' => 62,
'parent_id' => 11,
'code' => 'music_interviews',
'apple_category' => 'Music Interviews',
'google_category' => '',
],
[
'id' => 63,
'parent_id' => 12,
'code' => 'business_news',
'apple_category' => 'Business News',
'id' => 63,
'parent_id' => 12,
'code' => 'business_news',
'apple_category' => 'Business News',
'google_category' => '',
],
[
'id' => 64,
'parent_id' => 12,
'code' => 'daily_news',
'apple_category' => 'Daily News',
'id' => 64,
'parent_id' => 12,
'code' => 'daily_news',
'apple_category' => 'Daily News',
'google_category' => '',
],
[
'id' => 65,
'parent_id' => 12,
'code' => 'entertainment_news',
'apple_category' => 'Entertainment News',
'id' => 65,
'parent_id' => 12,
'code' => 'entertainment_news',
'apple_category' => 'Entertainment News',
'google_category' => '',
],
[
'id' => 66,
'parent_id' => 12,
'code' => 'news_commentary',
'apple_category' => 'News Commentary',
'id' => 66,
'parent_id' => 12,
'code' => 'news_commentary',
'apple_category' => 'News Commentary',
'google_category' => '',
],
[
'id' => 67,
'parent_id' => 12,
'code' => 'politics',
'apple_category' => 'Politics',
'id' => 67,
'parent_id' => 12,
'code' => 'politics',
'apple_category' => 'Politics',
'google_category' => '',
],
[
'id' => 68,
'parent_id' => 12,
'code' => 'sports_news',
'apple_category' => 'Sports News',
'id' => 68,
'parent_id' => 12,
'code' => 'sports_news',
'apple_category' => 'Sports News',
'google_category' => '',
],
[
'id' => 69,
'parent_id' => 12,
'code' => 'tech_news',
'apple_category' => 'Tech News',
'id' => 69,
'parent_id' => 12,
'code' => 'tech_news',
'apple_category' => 'Tech News',
'google_category' => '',
],
[
'id' => 70,
'parent_id' => 13,
'code' => 'buddhism',
'apple_category' => 'Buddhism',
'id' => 70,
'parent_id' => 13,
'code' => 'buddhism',
'apple_category' => 'Buddhism',
'google_category' => '',
],
[
'id' => 71,
'parent_id' => 13,
'code' => 'christianity',
'apple_category' => 'Christianity',
'id' => 71,
'parent_id' => 13,
'code' => 'christianity',
'apple_category' => 'Christianity',
'google_category' => '',
],
[
'id' => 72,
'parent_id' => 13,
'code' => 'hinduism',
'apple_category' => 'Hinduism',
'id' => 72,
'parent_id' => 13,
'code' => 'hinduism',
'apple_category' => 'Hinduism',
'google_category' => '',
],
[
'id' => 73,
'parent_id' => 13,
'code' => 'islam',
'apple_category' => 'Islam',
'id' => 73,
'parent_id' => 13,
'code' => 'islam',
'apple_category' => 'Islam',
'google_category' => '',
],
[
'id' => 74,
'parent_id' => 13,
'code' => 'judaism',
'apple_category' => 'Judaism',
'id' => 74,
'parent_id' => 13,
'code' => 'judaism',
'apple_category' => 'Judaism',
'google_category' => '',
],
[
'id' => 75,
'parent_id' => 13,
'code' => 'religion',
'apple_category' => 'Religion',
'id' => 75,
'parent_id' => 13,
'code' => 'religion',
'apple_category' => 'Religion',
'google_category' => '',
],
[
'id' => 76,
'parent_id' => 13,
'code' => 'spirituality',
'apple_category' => 'Spirituality',
'id' => 76,
'parent_id' => 13,
'code' => 'spirituality',
'apple_category' => 'Spirituality',
'google_category' => '',
],
[
'id' => 77,
'parent_id' => 14,
'code' => 'astronomy',
'apple_category' => 'Astronomy',
'id' => 77,
'parent_id' => 14,
'code' => 'astronomy',
'apple_category' => 'Astronomy',
'google_category' => '',
],
[
'id' => 78,
'parent_id' => 14,
'code' => 'chemistry',
'apple_category' => 'Chemistry',
'id' => 78,
'parent_id' => 14,
'code' => 'chemistry',
'apple_category' => 'Chemistry',
'google_category' => '',
],
[
'id' => 79,
'parent_id' => 14,
'code' => 'earth_sciences',
'apple_category' => 'Earth Sciences',
'id' => 79,
'parent_id' => 14,
'code' => 'earth_sciences',
'apple_category' => 'Earth Sciences',
'google_category' => '',
],
[
'id' => 80,
'parent_id' => 14,
'code' => 'life_sciences',
'apple_category' => 'Life Sciences',
'id' => 80,
'parent_id' => 14,
'code' => 'life_sciences',
'apple_category' => 'Life Sciences',
'google_category' => '',
],
[
'id' => 81,
'parent_id' => 14,
'code' => 'mathematics',
'apple_category' => 'Mathematics',
'id' => 81,
'parent_id' => 14,
'code' => 'mathematics',
'apple_category' => 'Mathematics',
'google_category' => '',
],
[
'id' => 82,
'parent_id' => 14,
'code' => 'natural_sciences',
'apple_category' => 'Natural Sciences',
'id' => 82,
'parent_id' => 14,
'code' => 'natural_sciences',
'apple_category' => 'Natural Sciences',
'google_category' => '',
],
[
'id' => 83,
'parent_id' => 14,
'code' => 'nature',
'apple_category' => 'Nature',
'id' => 83,
'parent_id' => 14,
'code' => 'nature',
'apple_category' => 'Nature',
'google_category' => '',
],
[
'id' => 84,
'parent_id' => 14,
'code' => 'physics',
'apple_category' => 'Physics',
'id' => 84,
'parent_id' => 14,
'code' => 'physics',
'apple_category' => 'Physics',
'google_category' => '',
],
[
'id' => 85,
'parent_id' => 14,
'code' => 'social_sciences',
'apple_category' => 'Social Sciences',
'id' => 85,
'parent_id' => 14,
'code' => 'social_sciences',
'apple_category' => 'Social Sciences',
'google_category' => '',
],
[
'id' => 86,
'parent_id' => 15,
'code' => 'documentary',
'apple_category' => 'Documentary',
'id' => 86,
'parent_id' => 15,
'code' => 'documentary',
'apple_category' => 'Documentary',
'google_category' => '',
],
[
'id' => 87,
'parent_id' => 15,
'code' => 'personal_journals',
'apple_category' => 'Personal Journals',
'id' => 87,
'parent_id' => 15,
'code' => 'personal_journals',
'apple_category' => 'Personal Journals',
'google_category' => '',
],
[
'id' => 88,
'parent_id' => 15,
'code' => 'philosophy',
'apple_category' => 'Philosophy',
'id' => 88,
'parent_id' => 15,
'code' => 'philosophy',
'apple_category' => 'Philosophy',
'google_category' => '',
],
[
'id' => 89,
'parent_id' => 15,
'code' => 'places_and_travel',
'apple_category' => 'Places & Travel',
'id' => 89,
'parent_id' => 15,
'code' => 'places_and_travel',
'apple_category' => 'Places & Travel',
'google_category' => '',
],
[
'id' => 90,
'parent_id' => 15,
'code' => 'relationships',
'apple_category' => 'Relationships',
'id' => 90,
'parent_id' => 15,
'code' => 'relationships',
'apple_category' => 'Relationships',
'google_category' => '',
],
[
'id' => 91,
'parent_id' => 16,
'code' => 'baseball',
'apple_category' => 'Baseball',
'id' => 91,
'parent_id' => 16,
'code' => 'baseball',
'apple_category' => 'Baseball',
'google_category' => '',
],
[
'id' => 92,
'parent_id' => 16,
'code' => 'basketball',
'apple_category' => 'Basketball',
'id' => 92,
'parent_id' => 16,
'code' => 'basketball',
'apple_category' => 'Basketball',
'google_category' => '',
],
[
'id' => 93,
'parent_id' => 16,
'code' => 'cricket',
'apple_category' => 'Cricket',
'id' => 93,
'parent_id' => 16,
'code' => 'cricket',
'apple_category' => 'Cricket',
'google_category' => '',
],
[
'id' => 94,
'parent_id' => 16,
'code' => 'fantasy_sports',
'apple_category' => 'Fantasy Sports',
'id' => 94,
'parent_id' => 16,
'code' => 'fantasy_sports',
'apple_category' => 'Fantasy Sports',
'google_category' => '',
],
[
'id' => 95,
'parent_id' => 16,
'code' => 'football',
'apple_category' => 'Football',
'id' => 95,
'parent_id' => 16,
'code' => 'football',
'apple_category' => 'Football',
'google_category' => '',
],
[
'id' => 96,
'parent_id' => 16,
'code' => 'golf',
'apple_category' => 'Golf',
'id' => 96,
'parent_id' => 16,
'code' => 'golf',
'apple_category' => 'Golf',
'google_category' => '',
],
[
'id' => 97,
'parent_id' => 16,
'code' => 'hockey',
'apple_category' => 'Hockey',
'id' => 97,
'parent_id' => 16,
'code' => 'hockey',
'apple_category' => 'Hockey',
'google_category' => '',
],
[
'id' => 98,
'parent_id' => 16,
'code' => 'rugby',
'apple_category' => 'Rugby',
'id' => 98,
'parent_id' => 16,
'code' => 'rugby',
'apple_category' => 'Rugby',
'google_category' => '',
],
[
'id' => 99,
'parent_id' => 16,
'code' => 'running',
'apple_category' => 'Running',
'id' => 99,
'parent_id' => 16,
'code' => 'running',
'apple_category' => 'Running',
'google_category' => '',
],
[
'id' => 100,
'parent_id' => 16,
'code' => 'soccer',
'apple_category' => 'Soccer',
'id' => 100,
'parent_id' => 16,
'code' => 'soccer',
'apple_category' => 'Soccer',
'google_category' => '',
],
[
'id' => 101,
'parent_id' => 16,
'code' => 'swimming',
'apple_category' => 'Swimming',
'id' => 101,
'parent_id' => 16,
'code' => 'swimming',
'apple_category' => 'Swimming',
'google_category' => '',
],
[
'id' => 102,
'parent_id' => 16,
'code' => 'tennis',
'apple_category' => 'Tennis',
'id' => 102,
'parent_id' => 16,
'code' => 'tennis',
'apple_category' => 'Tennis',
'google_category' => '',
],
[
'id' => 103,
'parent_id' => 16,
'code' => 'volleyball',
'apple_category' => 'Volleyball',
'id' => 103,
'parent_id' => 16,
'code' => 'volleyball',
'apple_category' => 'Volleyball',
'google_category' => '',
],
[
'id' => 104,
'parent_id' => 16,
'code' => 'wilderness',
'apple_category' => 'Wilderness',
'id' => 104,
'parent_id' => 16,
'code' => 'wilderness',
'apple_category' => 'Wilderness',
'google_category' => '',
],
[
'id' => 105,
'parent_id' => 16,
'code' => 'wrestling',
'apple_category' => 'Wrestling',
'id' => 105,
'parent_id' => 16,
'code' => 'wrestling',
'apple_category' => 'Wrestling',
'google_category' => '',
],
[
'id' => 106,
'parent_id' => 19,
'code' => 'after_shows',
'apple_category' => 'After Shows',
'id' => 106,
'parent_id' => 19,
'code' => 'after_shows',
'apple_category' => 'After Shows',
'google_category' => '',
],
[
'id' => 107,
'parent_id' => 19,
'code' => 'film_history',
'apple_category' => 'Film History',
'id' => 107,
'parent_id' => 19,
'code' => 'film_history',
'apple_category' => 'Film History',
'google_category' => '',
],
[
'id' => 108,
'parent_id' => 19,
'code' => 'film_interviews',
'apple_category' => 'Film Interviews',
'id' => 108,
'parent_id' => 19,
'code' => 'film_interviews',
'apple_category' => 'Film Interviews',
'google_category' => '',
],
[
'id' => 109,
'parent_id' => 19,
'code' => 'film_reviews',
'apple_category' => 'Film Reviews',
'id' => 109,
'parent_id' => 19,
'code' => 'film_reviews',
'apple_category' => 'Film Reviews',
'google_category' => '',
],
[
'id' => 110,
'parent_id' => 19,
'code' => 'tv_reviews',
'apple_category' => 'TV Reviews',
'id' => 110,
'parent_id' => 19,
'code' => 'tv_reviews',
'apple_category' => 'TV Reviews',
'google_category' => '',
],
];
$this->db
->table('categories')
->ignore(true)
->insertBatch($data);
foreach ($data as $categoryLine) {
$this->db
->table('categories')
->ignore(true)
->insert($categoryLine);
}
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* Class AppSeeder Calls all required seeders for castopod to work properly
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Override;
class DevSeeder extends Seeder
{
#[Override]
public function run(): void
{
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('DevSuperadminSeeder');
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* Class TestSeeder Inserts a superadmin user in the database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Override;
class DevSuperadminSeeder extends Seeder
{
#[Override]
public function run(): void
{
if (new UserModel()->where('is_owner', true)->first() instanceof User) {
return;
}
/**
* Inserts an owner with the following credentials: admin: `admin@example.com` password: `castopod`
*/
// Get the User Provider (UserModel by default)
$users = auth()
->getProvider();
$user = new User([
'username' => 'admin',
'email' => 'admin@castopod.local',
'password' => 'castopod',
'is_owner' => true,
]);
$users->save($user);
// To get the complete user object with ID, we need to get from the database
$user = $users->findById($users->getInsertID());
$user->addGroup(setting('AuthGroups.mostPowerfulGroup'));
}
}

View file

@ -5,22 +5,26 @@ declare(strict_types=1);
/**
* Class FakePodcastsAnalyticsSeeder Inserts Fake Analytics in the database
*
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder;
use Exception;
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
use Override;
class FakePodcastsAnalyticsSeeder extends Seeder
{
#[Override]
public function run(): void
{
$jsonUserAgents = json_decode(
@ -39,163 +43,165 @@ class FakePodcastsAnalyticsSeeder extends Seeder
JSON_THROW_ON_ERROR,
);
$podcast = (new PodcastModel())->first();
$podcast = new PodcastModel()
->first();
if ($podcast !== null) {
$firstEpisode = (new EpisodeModel())
->selectMin('published_at')
->first();
if (! $podcast instanceof Podcast) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
}
for (
$date = strtotime((string) $firstEpisode->published_at);
$date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$analyticsPodcasts = [];
$analyticsPodcastsByHour = [];
$analyticsPodcastsByCountry = [];
$analyticsPodcastsByEpisode = [];
$analyticsPodcastsByPlayer = [];
$analyticsPodcastsByRegion = [];
$firstEpisode = new EpisodeModel()
->selectMin('published_at')
->first();
$episodes = (new EpisodeModel())
->where('podcast_id', $podcast->id)
->where('`published_at` <= NOW()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = floor(exp(3 - $age / 40)) + 1;
if (! $firstEpisode instanceof Episode) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate an episode first.");
}
for (
$lineNumber = 0;
$lineNumber < rand(1, (int) $probability1);
++$lineNumber
) {
$probability2 = floor(exp(6 - $age / 20)) + 10;
for (
$date = strtotime((string) $firstEpisode->published_at);
$date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$analyticsPodcasts = [];
$analyticsPodcastsByHour = [];
$analyticsPodcastsByCountry = [];
$analyticsPodcastsByEpisode = [];
$analyticsPodcastsByPlayer = [];
$analyticsPodcastsByRegion = [];
$player =
$jsonUserAgents[
rand(1, count($jsonUserAgents) - 1)
];
$service =
$jsonRSSUserAgents[
rand(1, count($jsonRSSUserAgents) - 1)
]['slug'];
$app = isset($player['app']) ? $player['app'] : '';
$device = isset($player['device'])
? $player['device']
: '';
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
$episodes = new EpisodeModel()
->where('podcast_id', $podcast->id)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = floor(exp(3 - $age / 40)) + 1;
$fakeIp =
rand(0, 255) .
'.' .
rand(0, 255) .
'.' .
rand(0, 255) .
'.' .
rand(0, 255);
for (
$lineNumber = 0;
$lineNumber < random_int(1, (int) $probability1);
++$lineNumber
) {
$probability2 = floor(exp(6 - $age / 20)) + 10;
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
$countryCode = 'N/A';
$regionCode = 'N/A';
$latitude = null;
$longitude = null;
try {
$city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null
? 'N/A'
: $city->country->isoCode;
$regionCode = $city->subdivisions === []
? 'N/A'
: $city->subdivisions[0]->isoCode;
$latitude = round((float) $city->location->latitude, 3);
$longitude = round((float) $city->location->longitude, 3);
} catch (AddressNotFoundException) {
//Bad luck, bad IP, nothing to do.
}
$hits = rand(0, (int) $probability2);
$analyticsPodcasts[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600),
'bandwidth' => rand(1000000, 10000000),
'hits' => $hits,
'unique_listeners' => $hits,
];
$analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'hour' => rand(0, 23),
'hits' => $hits,
];
$analyticsPodcastsByCountry[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'hits' => $hits,
];
$analyticsPodcastsByEpisode[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'episode_id' => $episode->id,
'age' => $age,
'hits' => $hits,
];
$analyticsPodcastsByPlayer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'service' => $service,
'app' => $app,
'device' => $device,
'os' => $os,
'is_bot' => $isBot,
'hits' => $hits,
];
$analyticsPodcastsByRegion[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'region_code' => $regionCode,
'latitude' => $latitude,
'longitude' => $longitude,
'hits' => $hits,
$player =
$jsonUserAgents[
random_int(1, count($jsonUserAgents) - 1)
];
$service =
$jsonRSSUserAgents[
random_int(1, count($jsonRSSUserAgents) - 1)
]['slug'];
$app = $player['app'] ?? '';
$device = $player['device'] ?? '';
$os = $player['os'] ?? '';
$isBot = $player['bot'] ?? 0;
$fakeIp =
random_int(0, 255) .
'.' .
random_int(0, 255) .
'.' .
random_int(0, 255) .
'.' .
random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
$countryCode = 'N/A';
$regionCode = 'N/A';
$latitude = null;
$longitude = null;
try {
$city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode ?? 'N/A';
$regionCode = $city->subdivisions === []
? 'N/A'
: $city->subdivisions[0]->isoCode;
$latitude = round((float) $city->location->latitude, 3);
$longitude = round((float) $city->location->longitude, 3);
} catch (AddressNotFoundException) {
//Bad luck, bad IP, nothing to do.
}
$hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'duration' => random_int(60, 3600),
'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits,
'unique_listeners' => $hits,
];
$analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'hour' => random_int(0, 23),
'hits' => $hits,
];
$analyticsPodcastsByCountry[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'hits' => $hits,
];
$analyticsPodcastsByEpisode[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'episode_id' => $episode->id,
'age' => $age,
'hits' => $hits,
];
$analyticsPodcastsByPlayer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'service' => $service,
'app' => $app,
'device' => $device,
'os' => $os,
'is_bot' => $isBot,
'hits' => $hits,
];
$analyticsPodcastsByRegion[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'region_code' => $regionCode,
'latitude' => $latitude,
'longitude' => $longitude,
'hits' => $hits,
];
}
$this->db
->table('analytics_podcasts')
->ignore(true)
->insertBatch($analyticsPodcasts);
$this->db
->table('analytics_podcasts_by_hour')
->ignore(true)
->insertBatch($analyticsPodcastsByHour);
$this->db
->table('analytics_podcasts_by_country')
->ignore(true)
->insertBatch($analyticsPodcastsByCountry);
$this->db
->table('analytics_podcasts_by_episode')
->ignore(true)
->insertBatch($analyticsPodcastsByEpisode);
$this->db
->table('analytics_podcasts_by_player')
->ignore(true)
->insertBatch($analyticsPodcastsByPlayer);
$this->db
->table('analytics_podcasts_by_region')
->ignore(true)
->insertBatch($analyticsPodcastsByRegion);
}
} else {
echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
$this->db
->table('analytics_podcasts')
->ignore(true)
->insertBatch($analyticsPodcasts);
$this->db
->table('analytics_podcasts_by_hour')
->ignore(true)
->insertBatch($analyticsPodcastsByHour);
$this->db
->table('analytics_podcasts_by_country')
->ignore(true)
->insertBatch($analyticsPodcastsByCountry);
$this->db
->table('analytics_podcasts_by_episode')
->ignore(true)
->insertBatch($analyticsPodcastsByEpisode);
$this->db
->table('analytics_podcasts_by_player')
->ignore(true)
->insertBatch($analyticsPodcastsByPlayer);
$this->db
->table('analytics_podcasts_by_region')
->ignore(true)
->insertBatch($analyticsPodcastsByRegion);
}
}
}

View file

@ -5,17 +5,20 @@ declare(strict_types=1);
/**
* Class FakeWebsiteAnalyticsSeeder Inserts Fake Analytics in the database
*
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder;
use Exception;
use Override;
class FakeWebsiteAnalyticsSeeder extends Seeder
{
@ -179,90 +182,96 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'WOSBrowser',
];
#[Override]
public function run(): void
{
$podcast = (new PodcastModel())->first();
$podcast = new PodcastModel()
->first();
if ($podcast) {
$firstEpisode = (new EpisodeModel())
->selectMin('published_at')
->first();
if (! $podcast instanceof Podcast) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n");
}
for (
$date = strtotime((string) $firstEpisode->published_at);
$date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$websiteByBrowser = [];
$websiteByEntryPage = [];
$websiteByReferer = [];
$firstEpisode = new EpisodeModel()
->selectMin('published_at')
->first();
$episodes = (new EpisodeModel())
->where('podcast_id', $podcast->id)
->where('`published_at` <= NOW()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = (int) floor(exp(3 - $age / 40)) + 1;
if (! $firstEpisode instanceof Episode) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate an episode first.");
}
for (
$lineNumber = 0;
$lineNumber < rand(1, $probability1);
++$lineNumber
) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10;
for (
$date = strtotime((string) $firstEpisode->published_at);
$date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$websiteByBrowser = [];
$websiteByEntryPage = [];
$websiteByReferer = [];
$domain =
$this->domains[rand(0, count($this->domains) - 1)];
$keyword =
$this->keywords[
rand(0, count($this->keywords) - 1)
];
$browser =
$this->browsers[
rand(0, count($this->browsers) - 1)
];
$episodes = new EpisodeModel()
->where('podcast_id', $podcast->id)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = (int) floor(exp(3 - $age / 40)) + 1;
$hits = rand(0, $probability2);
for (
$lineNumber = 0;
$lineNumber < random_int(1, $probability1);
++$lineNumber
) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$websiteByBrowser[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'browser' => $browser,
'hits' => $hits,
$domain =
$this->domains[random_int(0, count($this->domains) - 1)];
$keyword =
$this->keywords[
random_int(0, count($this->keywords) - 1)
];
$websiteByEntryPage[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'entry_page_url' => $episode->link,
'hits' => $hits,
$browser =
$this->browsers[
random_int(0, count($this->browsers) - 1)
];
$websiteByReferer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'referer_url' =>
'http://' . $domain . '/?q=' . $keyword,
'domain' => $domain,
'keywords' => $keyword,
'hits' => $hits,
];
}
$hits = random_int(0, $probability2);
$websiteByBrowser[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'browser' => $browser,
'hits' => $hits,
];
$websiteByEntryPage[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'entry_page_url' => $episode->link,
'hits' => $hits,
];
$websiteByReferer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'referer_url' => 'http://' . $domain . '/?q=' . $keyword,
'domain' => $domain,
'keywords' => $keyword,
'hits' => $hits,
];
}
$this->db
->table('analytics_website_by_browser')
->ignore(true)
->insertBatch($websiteByBrowser);
$this->db
->table('analytics_website_by_entry_page')
->ignore(true)
->insertBatch($websiteByEntryPage);
$this->db
->table('analytics_website_by_referer')
->ignore(true)
->insertBatch($websiteByReferer);
}
} else {
echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
$this->db
->table('analytics_website_by_browser')
->ignore(true)
->insertBatch($websiteByBrowser);
$this->db
->table('analytics_website_by_entry_page')
->ignore(true)
->insertBatch($websiteByEntryPage);
$this->db
->table('analytics_website_by_referer')
->ignore(true)
->insertBatch($websiteByReferer);
}
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/**
* Class LanguageSeeder Inserts values in languages table in database
*
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -18,754 +18,757 @@ declare(strict_types=1);
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Override;
class LanguageSeeder extends Seeder
{
#[Override]
public function run(): void
{
$data = [
[
'code' => 'aa',
'code' => 'aa',
'native_name' => 'Afaraf',
],
[
'code' => 'ab',
'code' => 'ab',
'native_name' => 'аҧсуа бызшәа, аҧсшәа',
],
[
'code' => 'ae',
'native_name' => 'avesta',
'code' => 'ae',
'native_name' => 'Avesta',
],
[
'code' => 'af',
'code' => 'af',
'native_name' => 'Afrikaans',
],
[
'code' => 'ak',
'code' => 'ak',
'native_name' => 'Akan',
],
[
'code' => 'am',
'code' => 'am',
'native_name' => 'አማርኛ',
],
[
'code' => 'an',
'native_name' => 'aragonés',
'code' => 'an',
'native_name' => 'Aragonés',
],
[
'code' => 'ar',
'code' => 'ar',
'native_name' => 'العربية',
],
[
'code' => 'as',
'code' => 'as',
'native_name' => 'অসমীয়া',
],
[
'code' => 'av',
'code' => 'av',
'native_name' => 'авар мацӀ, магӀарул мацӀ',
],
[
'code' => 'ay',
'native_name' => 'aymar aru',
'code' => 'ay',
'native_name' => 'Aymar aru',
],
[
'code' => 'az',
'code' => 'az',
'native_name' => 'azərbaycan dili',
],
[
'code' => 'ba',
'code' => 'ba',
'native_name' => 'башҡорт теле',
],
[
'code' => 'be',
'code' => 'be',
'native_name' => 'беларуская мова',
],
[
'code' => 'bg',
'code' => 'bg',
'native_name' => 'български език',
],
[
'code' => 'bh',
'code' => 'bh',
'native_name' => 'भोजपुरी',
],
[
'code' => 'bi',
'code' => 'bi',
'native_name' => 'Bislama',
],
[
'code' => 'bm',
'native_name' => 'bamanankan',
'code' => 'bm',
'native_name' => 'Bamanankan',
],
[
'code' => 'bn',
'code' => 'bn',
'native_name' => 'বাংলা',
],
[
'code' => 'bo',
'code' => 'bo',
'native_name' => 'བོད་ཡིག',
],
[
'code' => 'br',
'native_name' => 'brezhoneg',
'code' => 'br',
'native_name' => 'Brezhoneg',
],
[
'code' => 'bs',
'native_name' => 'bosanski jezik',
'code' => 'bs',
'native_name' => 'Bosanski jezik',
],
[
'code' => 'ca',
'native_name' => 'català, valencià',
'code' => 'ca',
'native_name' => 'Català, valencià',
],
[
'code' => 'ce',
'code' => 'ce',
'native_name' => 'нохчийн мотт',
],
[
'code' => 'ch',
'code' => 'ch',
'native_name' => 'Chamoru',
],
[
'code' => 'co',
'native_name' => 'corsu, lingua corsa',
'code' => 'co',
'native_name' => 'Corsu, lingua corsa',
],
[
'code' => 'cr',
'code' => 'cr',
'native_name' => 'ᓀᐦᐃᔭᐍᐏᐣ',
],
[
'code' => 'cs',
'code' => 'cs',
'native_name' => 'čeština, český jazyk',
],
[
'code' => 'cu',
'code' => 'cu',
'native_name' => 'ѩзыкъ словѣньскъ',
],
[
'code' => 'cv',
'code' => 'cv',
'native_name' => 'чӑваш чӗлхи',
],
[
'code' => 'cy',
'code' => 'cy',
'native_name' => 'Cymraeg',
],
[
'code' => 'da',
'native_name' => 'dansk',
'code' => 'da',
'native_name' => 'Dansk',
],
[
'code' => 'de',
'code' => 'de',
'native_name' => 'Deutsch',
],
[
'code' => 'dv',
'code' => 'dv',
'native_name' => 'ދިވެހި',
],
[
'code' => 'dz',
'code' => 'dz',
'native_name' => 'རྫོང་ཁ',
],
[
'code' => 'ee',
'code' => 'ee',
'native_name' => 'Eʋegbe',
],
[
'code' => 'el',
'code' => 'el',
'native_name' => 'ελληνικά',
],
[
'code' => 'en',
'code' => 'en',
'native_name' => 'English',
],
[
'code' => 'eo',
'code' => 'eo',
'native_name' => 'Esperanto',
],
[
'code' => 'es',
'code' => 'es',
'native_name' => 'Español',
],
[
'code' => 'et',
'code' => 'et',
'native_name' => 'eesti, eesti keel',
],
[
'code' => 'eu',
'native_name' => 'euskara, euskera',
'code' => 'eu',
'native_name' => 'Euskara, euskera',
],
[
'code' => 'fa',
'code' => 'fa',
'native_name' => 'فارسی',
],
[
'code' => 'ff',
'code' => 'ff',
'native_name' => 'Fulfulde, Pulaar, Pular',
],
[
'code' => 'fi',
'native_name' => 'suomi, suomen kieli',
'code' => 'fi',
'native_name' => 'Suomi, suomen kieli',
],
[
'code' => 'fj',
'native_name' => 'vosa Vakaviti',
'code' => 'fj',
'native_name' => 'Vosa Vakaviti',
],
[
'code' => 'fo',
'native_name' => 'føroyskt',
'code' => 'fo',
'native_name' => 'Føroyskt',
],
[
'code' => 'fr',
'native_name' => 'français, langue française',
'code' => 'fr',
'native_name' => 'Français, langue française',
],
[
'code' => 'fy',
'code' => 'fy',
'native_name' => 'Frysk',
],
[
'code' => 'ga',
'code' => 'ga',
'native_name' => 'Gaeilge',
],
[
'code' => 'gd',
'code' => 'gd',
'native_name' => 'Gàidhlig',
],
[
'code' => 'gl',
'code' => 'gl',
'native_name' => 'Galego',
],
[
'code' => 'gn',
'code' => 'gn',
'native_name' => "Avañe'ẽ",
],
[
'code' => 'gu',
'code' => 'gu',
'native_name' => 'ગુજરાતી',
],
[
'code' => 'gv',
'code' => 'gv',
'native_name' => 'Gaelg, Gailck',
],
[
'code' => 'ha',
'code' => 'ha',
'native_name' => '(Hausa) هَوُسَ',
],
[
'code' => 'he',
'code' => 'he',
'native_name' => 'עברית',
],
[
'code' => 'hi',
'code' => 'hi',
'native_name' => 'हिन्दी, हिंदी',
],
[
'code' => 'ho',
'code' => 'ho',
'native_name' => 'Hiri Motu',
],
[
'code' => 'hr',
'native_name' => 'hrvatski jezik',
'code' => 'hr',
'native_name' => 'Hrvatski jezik',
],
[
'code' => 'ht',
'code' => 'ht',
'native_name' => 'Kreyòl ayisyen',
],
[
'code' => 'hu',
'native_name' => 'magyar',
'code' => 'hu',
'native_name' => 'Magyar',
],
[
'code' => 'hy',
'code' => 'hy',
'native_name' => 'Հայերեն',
],
[
'code' => 'hz',
'code' => 'hz',
'native_name' => 'Otjiherero',
],
[
'code' => 'ia',
'code' => 'ia',
'native_name' => 'Interlingua',
],
[
'code' => 'id',
'code' => 'id',
'native_name' => 'Bahasa Indonesia',
],
[
'code' => 'ie',
'native_name' =>
'(originally:) Occidental, (after WWII:) Interlingue',
'code' => 'ie',
'native_name' => 'Interlingue, formerly Occidental',
],
[
'code' => 'ig',
'code' => 'ig',
'native_name' => 'Asụsụ Igbo',
],
[
'code' => 'ii',
'code' => 'ii',
'native_name' => 'ꆈꌠ꒿ Nuosuhxop',
],
[
'code' => 'ik',
'code' => 'ik',
'native_name' => 'Iñupiaq, Iñupiatun',
],
[
'code' => 'io',
'code' => 'io',
'native_name' => 'Ido',
],
[
'code' => 'is',
'code' => 'is',
'native_name' => 'Íslenska',
],
[
'code' => 'it',
'code' => 'it',
'native_name' => 'Italiano',
],
[
'code' => 'iu',
'code' => 'iu',
'native_name' => 'ᐃᓄᒃᑎᑐᑦ',
],
[
'code' => 'ja',
'code' => 'ja',
'native_name' => '日本語 (にほんご)',
],
[
'code' => 'jv',
'code' => 'jv',
'native_name' => 'ꦧꦱꦗꦮ, Basa Jawa',
],
[
'code' => 'ka',
'code' => 'ka',
'native_name' => 'ქართული',
],
[
'code' => 'kg',
'code' => 'kg',
'native_name' => 'Kikongo',
],
[
'code' => 'ki',
'code' => 'ki',
'native_name' => 'Gĩkũyũ',
],
[
'code' => 'kj',
'code' => 'kj',
'native_name' => 'Kuanyama',
],
[
'code' => 'kk',
'code' => 'kk',
'native_name' => 'қазақ тілі',
],
[
'code' => 'kl',
'native_name' => 'kalaallisut, kalaallit oqaasii',
'code' => 'kl',
'native_name' => 'Kalaallisut, kalaallit oqaasii',
],
[
'code' => 'km',
'code' => 'km',
'native_name' => 'ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ',
],
[
'code' => 'kn',
'code' => 'kn',
'native_name' => 'ಕನ್ನಡ',
],
[
'code' => 'ko',
'code' => 'ko',
'native_name' => '한국어',
],
[
'code' => 'kr',
'code' => 'kr',
'native_name' => 'Kanuri',
],
[
'code' => 'ks',
'code' => 'ks',
'native_name' => 'कश्मीरी, كشميري‎',
],
[
'code' => 'ku',
'code' => 'ku',
'native_name' => 'Kurdî, کوردی‎',
],
[
'code' => 'kv',
'code' => 'kv',
'native_name' => 'коми кыв',
],
[
'code' => 'kw',
'code' => 'kw',
'native_name' => 'Kernewek',
],
[
'code' => 'ky',
'code' => 'ky',
'native_name' => 'Кыргызча, Кыргыз тили',
],
[
'code' => 'la',
'native_name' => 'latine, lingua latina',
'code' => 'la',
'native_name' => 'Latine, lingua latina',
],
[
'code' => 'lb',
'code' => 'lb',
'native_name' => 'Lëtzebuergesch',
],
[
'code' => 'lg',
'code' => 'lg',
'native_name' => 'Luganda',
],
[
'code' => 'li',
'code' => 'li',
'native_name' => 'Limburgs',
],
[
'code' => 'ln',
'code' => 'ln',
'native_name' => 'Lingála',
],
[
'code' => 'lo',
'code' => 'lo',
'native_name' => 'ພາສາລາວ',
],
[
'code' => 'lt',
'native_name' => 'lietuvių kalba',
'code' => 'lt',
'native_name' => 'Lietuvių kalba',
],
[
'code' => 'lu',
'code' => 'lu',
'native_name' => 'Kiluba',
],
[
'code' => 'lv',
'native_name' => 'latviešu valoda',
'code' => 'lv',
'native_name' => 'Latviešu valoda',
],
[
'code' => 'mg',
'native_name' => 'fiteny malagasy',
'code' => 'mg',
'native_name' => 'Fiteny malagasy',
],
[
'code' => 'mh',
'code' => 'mh',
'native_name' => 'Kajin M̧ajeļ',
],
[
'code' => 'mi',
'native_name' => 'te reo Māori',
'code' => 'mi',
'native_name' => 'Te reo Māori',
],
[
'code' => 'mk',
'code' => 'mk',
'native_name' => 'македонски јазик',
],
[
'code' => 'ml',
'code' => 'ml',
'native_name' => 'മലയാളം',
],
[
'code' => 'mn',
'code' => 'mn',
'native_name' => 'Монгол хэл',
],
[
'code' => 'mr',
'code' => 'mr',
'native_name' => 'मराठी',
],
[
'code' => 'ms',
'code' => 'ms',
'native_name' => 'Bahasa Melayu, بهاس ملايو‎',
],
[
'code' => 'mt',
'code' => 'mt',
'native_name' => 'Malti',
],
[
'code' => 'my',
'code' => 'my',
'native_name' => 'ဗမာစာ',
],
[
'code' => 'na',
'code' => 'na',
'native_name' => 'Dorerin Naoero',
],
[
'code' => 'nb',
'code' => 'nb',
'native_name' => 'Norsk Bokmål',
],
[
'code' => 'nd',
'code' => 'nd',
'native_name' => 'isiNdebele',
],
[
'code' => 'ne',
'code' => 'ne',
'native_name' => 'नेपाली',
],
[
'code' => 'ng',
'code' => 'ng',
'native_name' => 'Owambo',
],
[
'code' => 'nl',
'code' => 'nl',
'native_name' => 'Nederlands, Vlaams',
],
[
'code' => 'nn',
'code' => 'nn',
'native_name' => 'Norsk Nynorsk',
],
[
'code' => 'no',
'code' => 'no',
'native_name' => 'Norsk',
],
[
'code' => 'nr',
'code' => 'nr',
'native_name' => 'isiNdebele',
],
[
'code' => 'nv',
'code' => 'nv',
'native_name' => 'Diné bizaad',
],
[
'code' => 'ny',
'native_name' => 'chiCheŵa, chinyanja',
'code' => 'ny',
'native_name' => 'Chicheŵa, chinyanja',
],
[
'code' => 'oc',
'native_name' => 'occitan, lenga dòc',
'code' => 'oc',
'native_name' => 'Occitan, lenga dòc',
],
[
'code' => 'oj',
'code' => 'oj',
'native_name' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ',
],
[
'code' => 'om',
'code' => 'om',
'native_name' => 'Afaan Oromoo',
],
[
'code' => 'or',
'code' => 'or',
'native_name' => 'ଓଡ଼ିଆ',
],
[
'code' => 'os',
'code' => 'os',
'native_name' => 'ирон æвзаг',
],
[
'code' => 'pa',
'code' => 'pa',
'native_name' => 'ਪੰਜਾਬੀ, پنجابی‎',
],
[
'code' => 'pi',
'code' => 'pi',
'native_name' => 'पालि, पाळि',
],
[
'code' => 'pl',
'code' => 'pl',
'native_name' => 'język polski, polszczyzna',
],
[
'code' => 'ps',
'code' => 'ps',
'native_name' => 'پښتو',
],
[
'code' => 'pt',
'code' => 'pt',
'native_name' => 'Português',
],
[
'code' => 'qu',
'code' => 'qu',
'native_name' => 'Runa Simi, Kichwa',
],
[
'code' => 'rm',
'code' => 'rm',
'native_name' => 'Rumantsch Grischun',
],
[
'code' => 'rn',
'code' => 'rn',
'native_name' => 'Ikirundi',
],
[
'code' => 'ro',
'code' => 'ro',
'native_name' => 'Română',
],
[
'code' => 'ru',
'native_name' => 'русский',
'code' => 'ru',
'native_name' => 'Pусский',
],
[
'code' => 'rw',
'code' => 'rw',
'native_name' => 'Ikinyarwanda',
],
[
'code' => 'sa',
'code' => 'sa',
'native_name' => 'संस्कृतम्',
],
[
'code' => 'sc',
'native_name' => 'sardu',
'code' => 'sc',
'native_name' => 'Sardu',
],
[
'code' => 'sd',
'code' => 'sd',
'native_name' => 'सिन्धी, سنڌي، سندھی‎',
],
[
'code' => 'se',
'code' => 'se',
'native_name' => 'Davvisámegiella',
],
[
'code' => 'sg',
'native_name' => 'yângâ tî sängö',
'code' => 'sg',
'native_name' => 'Yângâ tî sängö',
],
[
'code' => 'si',
'code' => 'si',
'native_name' => 'සිංහල',
],
[
'code' => 'sk',
'code' => 'sk',
'native_name' => 'Slovenčina, Slovenský Jazyk',
],
[
'code' => 'sl',
'code' => 'sl',
'native_name' => 'Slovenski Jezik, Slovenščina',
],
[
'code' => 'sm',
'native_name' => "gagana fa'a Samoa",
'code' => 'sm',
'native_name' => "Gagana fa'a Samoa",
],
[
'code' => 'sn',
'code' => 'sn',
'native_name' => 'chiShona',
],
[
'code' => 'so',
'code' => 'so',
'native_name' => 'Soomaaliga, af Soomaali',
],
[
'code' => 'sq',
'code' => 'sq',
'native_name' => 'Shqip',
],
[
'code' => 'sr',
'code' => 'sr',
'native_name' => 'српски језик',
],
[
'code' => 'ss',
'code' => 'ss',
'native_name' => 'SiSwati',
],
[
'code' => 'st',
'code' => 'st',
'native_name' => 'Sesotho',
],
[
'code' => 'su',
'code' => 'su',
'native_name' => 'Basa Sunda',
],
[
'code' => 'sv',
'code' => 'sv',
'native_name' => 'Svenska',
],
[
'code' => 'sw',
'code' => 'sw',
'native_name' => 'Kiswahili',
],
[
'code' => 'ta',
'code' => 'ta',
'native_name' => 'தமிழ்',
],
[
'code' => 'te',
'code' => 'te',
'native_name' => 'తెలుగు',
],
[
'code' => 'tg',
'code' => 'tg',
'native_name' => 'тоҷикӣ, toçikī, تاجیکی‎',
],
[
'code' => 'th',
'code' => 'th',
'native_name' => 'ไทย',
],
[
'code' => 'ti',
'code' => 'ti',
'native_name' => 'ትግርኛ',
],
[
'code' => 'tk',
'code' => 'tk',
'native_name' => 'Türkmen, Түркмен',
],
[
'code' => 'tl',
'code' => 'tl',
'native_name' => 'Wikang Tagalog',
],
[
'code' => 'tn',
'code' => 'tn',
'native_name' => 'Setswana',
],
[
'code' => 'to',
'code' => 'to',
'native_name' => 'Faka Tonga',
],
[
'code' => 'tr',
'code' => 'tr',
'native_name' => 'Türkçe',
],
[
'code' => 'ts',
'code' => 'ts',
'native_name' => 'Xitsonga',
],
[
'code' => 'tt',
'code' => 'tt',
'native_name' => 'татар теле, tatar tele',
],
[
'code' => 'tw',
'code' => 'tw',
'native_name' => 'Twi',
],
[
'code' => 'ty',
'code' => 'ty',
'native_name' => 'Reo Tahiti',
],
[
'code' => 'ug',
'code' => 'ug',
'native_name' => 'ئۇيغۇرچە‎, Uyghurche',
],
[
'code' => 'uk',
'code' => 'uk',
'native_name' => 'Українська',
],
[
'code' => 'ur',
'code' => 'ur',
'native_name' => 'اردو',
],
[
'code' => 'uz',
'code' => 'uz',
'native_name' => 'Oʻzbek, Ўзбек, أۇزبېك‎',
],
[
'code' => 've',
'code' => 've',
'native_name' => 'Tshivenḓa',
],
[
'code' => 'vi',
'code' => 'vi',
'native_name' => 'Tiếng Việt',
],
[
'code' => 'vo',
'code' => 'vo',
'native_name' => 'Volapük',
],
[
'code' => 'wa',
'code' => 'wa',
'native_name' => 'Walon',
],
[
'code' => 'wo',
'code' => 'wo',
'native_name' => 'Wollof',
],
[
'code' => 'xh',
'code' => 'xh',
'native_name' => 'isiXhosa',
],
[
'code' => 'yi',
'code' => 'yi',
'native_name' => 'ייִדיש',
],
[
'code' => 'yo',
'code' => 'yo',
'native_name' => 'Yorùbá',
],
[
'code' => 'za',
'code' => 'za',
'native_name' => 'Saɯ cueŋƅ, Saw cuengh',
],
[
'code' => 'zh',
'code' => 'zh',
'native_name' => '中文 (Zhōngwén), 汉语, 漢語',
],
[
'code' => 'zu',
'code' => 'zu',
'native_name' => 'isiZulu',
],
];
$this->db
->table('languages')
->ignore(true)
->insertBatch($data);
foreach ($data as $languageLine) {
$this->db
->table('languages')
->ignore(true)
->insert($languageLine);
}
}
}

View file

@ -1,591 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PlatformsSeeder Inserts values in platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class PlatformSeeder extends Seeder
{
public function run(): void
{
$data = [
[
'slug' => 'amazon',
'type' => 'podcasting',
'label' => 'Amazon Music and Audible',
'home_url' => 'https://music.amazon.com/podcasts',
'submit_url' => 'http://amazon.com/podcasters',
],
[
'slug' => 'antennapod',
'type' => 'podcasting',
'label' => 'AntennaPod',
'home_url' => 'https://antennapod.org/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'apple',
'type' => 'podcasting',
'label' => 'Apple Podcasts',
'home_url' => 'https://www.apple.com/itunes/podcasts/',
'submit_url' =>
'https://podcastsconnect.apple.com/my-podcasts/new-feed',
],
[
'slug' => 'blubrry',
'type' => 'podcasting',
'label' => 'Blubrry',
'home_url' => 'https://www.blubrry.com/',
'submit_url' => 'https://www.blubrry.com/addpodcast.php',
],
[
'slug' => 'breaker',
'type' => 'podcasting',
'label' => 'Breaker',
'home_url' => 'https://www.breaker.audio/',
'submit_url' => 'https://podcasters.breaker.audio/',
],
[
'slug' => 'castbox',
'type' => 'podcasting',
'label' => 'Castbox',
'home_url' => 'https://castbox.fm/',
'submit_url' =>
'https://helpcenter.castbox.fm/portal/kb/articles/submit-my-podcast',
],
[
'slug' => 'castopod',
'type' => 'podcasting',
'label' => 'Castopod',
'home_url' => 'https://castopod.org/',
'submit_url' => 'https://castopod.org/instances',
],
[
'slug' => 'castro',
'type' => 'podcasting',
'label' => 'Castro',
'home_url' => 'http://castro.fm/',
'submit_url' =>
'https://castro.fm/support/link-to-your-podcast-in-castro',
],
[
'slug' => 'chartable',
'type' => 'podcasting',
'label' => 'Chartable',
'home_url' => 'https://chartable.com/',
'submit_url' => 'https://chartable.com/podcasts/submit',
],
[
'slug' => 'deezer',
'type' => 'podcasting',
'label' => 'Deezer',
'home_url' => 'https://www.deezer.com/',
'submit_url' => 'https://podcasters.deezer.com/submission',
],
[
'slug' => 'fyyd',
'type' => 'podcasting',
'label' => 'fyyd',
'home_url' => 'https://fyyd.de/',
'submit_url' => 'https://fyyd.de/add-feed',
],
[
'slug' => 'google',
'type' => 'podcasting',
'label' => 'Google Podcasts',
'home_url' => 'https://podcasts.google.com/about',
'submit_url' =>
'https://search.google.com/search-console/about',
],
[
'slug' => 'ivoox',
'type' => 'podcasting',
'label' => 'Ivoox',
'home_url' => 'https://www.ivoox.com/',
'submit_url' => 'http://www.ivoox.com/upload-podcast_u.html',
],
[
'slug' => 'listennotes',
'type' => 'podcasting',
'label' => 'ListenNotes',
'home_url' => 'https://www.listennotes.com/',
'submit_url' => 'https://www.listennotes.com/submit/',
],
[
'slug' => 'overcast',
'type' => 'podcasting',
'label' => 'Overcast',
'home_url' => 'https://overcast.fm/',
'submit_url' => 'https://overcast.fm/podcasterinfo',
],
[
'slug' => 'playerfm',
'type' => 'podcasting',
'label' => 'Player.Fm',
'home_url' => 'https://player.fm/',
'submit_url' => 'https://player.fm/importer/feed',
],
[
'slug' => 'pocketcasts',
'type' => 'podcasting',
'label' => 'Pocketcasts',
'home_url' => 'https://www.pocketcasts.com/',
'submit_url' => 'https://www.pocketcasts.com/submit/',
],
[
'slug' => 'podbean',
'type' => 'podcasting',
'label' => 'Podbean',
'home_url' => 'https://www.podbean.com/',
'submit_url' => 'https://www.podbean.com/site/submitPodcast',
],
[
'slug' => 'podcastaddict',
'type' => 'podcasting',
'label' => 'Podcast Addict',
'home_url' => 'https://podcastaddict.com/',
'submit_url' => 'https://podcastaddict.com/submit',
],
[
'slug' => 'podcastindex',
'type' => 'podcasting',
'label' => 'Podcast Index',
'home_url' => 'https://podcastindex.org/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'podchaser',
'type' => 'podcasting',
'label' => 'Podchaser',
'home_url' => 'https://www.podchaser.com/',
'submit_url' => 'https://www.podchaser.com/creators/edit',
],
[
'slug' => 'podcloud',
'type' => 'podcasting',
'label' => 'podCloud',
'home_url' => 'https://podcloud.fr/',
'submit_url' => 'https://podcloud.fr/studio/podcasts/new',
],
[
'slug' => 'podinstall',
'type' => 'podcasting',
'label' => 'Podinstall',
'home_url' => 'https://www.podinstall.com/',
'submit_url' => 'https://www.podinstall.com/claim.html',
],
[
'slug' => 'podlink',
'type' => 'podcasting',
'label' => 'pod.link',
'home_url' => 'https://pod.link/',
'submit_url' => 'https://pod.link',
],
[
'slug' => 'podtail',
'type' => 'podcasting',
'label' => 'Podtail',
'home_url' => 'https://podtail.com/',
'submit_url' => 'https://podtail.com/about/faq/',
],
[
'slug' => 'podfriend',
'type' => 'podcasting',
'label' => 'Podfriend',
'home_url' => 'https://www.podfriend.com/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'podverse',
'type' => 'podcasting',
'label' => 'Podverse',
'home_url' => 'https://podverse.fm/',
'submit_url' =>
'https://docs.google.com/forms/d/e/1FAIpQLSdewKP-YrE8zGjDPrkmoJEwCxPl_gizEkmzAlTYsiWAuAk1Ng/viewform',
],
[
'slug' => 'radiopublic',
'type' => 'podcasting',
'label' => 'RadioPublic',
'home_url' => 'https://radiopublic.com/',
'submit_url' => 'https://podcasters.radiopublic.com/signup',
],
[
'slug' => 'spotify',
'type' => 'podcasting',
'label' => 'Spotify',
'home_url' => 'https://www.spotify.com/',
'submit_url' => 'https://podcasters.spotify.com/submit',
],
[
'slug' => 'spreaker',
'type' => 'podcasting',
'label' => 'Spreaker',
'home_url' => 'https://www.spreaker.com/',
'submit_url' => 'https://www.spreaker.com/cms/shows/rss-import',
],
[
'slug' => 'stitcher',
'type' => 'podcasting',
'label' => 'Stitcher',
'home_url' => 'https://www.stitcher.com/',
'submit_url' => 'https://partners.stitcher.com/join',
],
[
'slug' => 'tunein',
'type' => 'podcasting',
'label' => 'TuneIn',
'home_url' => 'https://tunein.com/',
'submit_url' =>
'https://help.tunein.com/contact/add-podcast-S19TR3Sdf',
],
[
'slug' => 'anytime',
'type' => 'podcasting',
'label' => 'Anytime Podcast Player',
'home_url' => 'https://anytimeplayer.app/',
'submit_url' => '',
],
[
'slug' => 'breez',
'type' => 'podcasting',
'label' => 'Breez',
'home_url' => 'https://breez.technology/',
'submit_url' => '',
],
[
'slug' => 'castamatic',
'type' => 'podcasting',
'label' => 'Castamatic',
'home_url' => 'https://castamatic.com/',
'submit_url' => '',
],
[
'slug' => 'castcoverage',
'type' => 'podcasting',
'label' => 'CastCoverage',
'home_url' => 'http://castcoverage.com/',
'submit_url' => '',
],
[
'slug' => 'curiocaster',
'type' => 'podcasting',
'label' => 'CurioCaster',
'home_url' => 'https://curiocaster.com/',
'submit_url' => '',
],
[
'slug' => 'escapepod',
'type' => 'podcasting',
'label' => 'Escapepod',
'home_url' => 'http://y20k.org/escapepod/',
'submit_url' => '',
],
[
'slug' => 'fountain',
'type' => 'podcasting',
'label' => 'Fountain',
'home_url' => 'https://www.fountain.fm/',
'submit_url' => '',
],
[
'slug' => 'gpodder',
'type' => 'podcasting',
'label' => 'gPodder',
'home_url' => 'https://gpodder.org/',
'submit_url' => '',
],
[
'slug' => 'hypercatcher',
'type' => 'podcasting',
'label' => 'HyperCatcher',
'home_url' => 'https://hypercatcher.com/',
'submit_url' => '',
],
[
'slug' => 'ivyfm',
'type' => 'podcasting',
'label' => 'Ivy Podcast Discovery',
'home_url' => 'https://ivy.fm/',
'submit_url' => '',
],
[
'slug' => 'jumplink',
'type' => 'podcasting',
'label' => 'JumpLink',
'home_url' => 'https://jump.link/',
'submit_url' => 'https://jump.link/a/accounts/signup/',
],
[
'slug' => 'kasts',
'type' => 'podcasting',
'label' => 'Kasts',
'home_url' => 'https://apps.kde.org/kasts/',
'submit_url' => '',
],
[
'slug' => 'playapod',
'type' => 'podcasting',
'label' => 'Playapod',
'home_url' => 'https://playapod.com/',
'submit_url' => '',
],
[
'slug' => 'plink',
'type' => 'podcasting',
'label' => 'Plink',
'home_url' => 'https://plinkhq.com/',
'submit_url' => '',
],
[
'slug' => 'podcastchapters',
'type' => 'podcasting',
'label' => 'Podcast Chapters',
'home_url' => 'https://chaptersapp.com/',
'submit_url' => '',
],
[
'slug' => 'podcastguru',
'type' => 'podcasting',
'label' => 'Podcast Guru',
'home_url' => 'https://podcastguru.io/',
'submit_url' => 'https://podcastguru.io/promote-your-podcast/',
],
[
'slug' => 'podlp',
'type' => 'podcasting',
'label' => 'PodLP',
'home_url' => 'https://podlp.com/',
'submit_url' => 'https://podlp.com/submit.html',
],
[
'slug' => 'podnews',
'type' => 'podcasting',
'label' => 'podnews',
'home_url' => 'https://podnews.net/podcast/subscribe-pages',
'submit_url' => '',
],
[
'slug' => 'podstation',
'type' => 'podcasting',
'label' => 'podStation',
'home_url' => 'https://podstation.github.io/',
'submit_url' => '',
],
[
'slug' => 'sphinxchat',
'type' => 'podcasting',
'label' => 'Sphinx',
'home_url' => 'https://sphinx.chat/',
'submit_url' => '',
],
[
'slug' => 'tsacdop',
'type' => 'podcasting',
'label' => 'Tsacdop',
'home_url' => 'https://www.tsacdop.app/',
'submit_url' => '',
],
[
'slug' => 'zion',
'type' => 'podcasting',
'label' => 'Zion',
'home_url' => 'https://getzion.com/',
'submit_url' => 'https://shop.n2n2.chat/',
],
[
'slug' => 'paypal',
'type' => 'funding',
'label' => 'Paypal',
'home_url' => 'https://www.paypal.com/',
'submit_url' => 'https://www.paypal.com/paypalme/my/grab',
],
[
'slug' => 'gofundme',
'type' => 'funding',
'label' => 'GoFundMe',
'home_url' => 'https://www.gofundme.com/',
'submit_url' => 'https://www.gofundme.com/sign-up',
],
[
'slug' => 'helloasso',
'type' => 'funding',
'label' => 'helloasso',
'home_url' => 'https://www.helloasso.com/',
'submit_url' => 'https://auth.helloasso.com/inscription',
],
[
'slug' => 'indiegogo',
'type' => 'funding',
'label' => 'Indiegogo',
'home_url' => 'https://www.indiegogo.com/',
'submit_url' => 'https://www.indiegogo.com/start-a-campaign#/',
],
[
'slug' => 'kickstarter',
'type' => 'funding',
'label' => 'Kickstarter',
'home_url' => 'https://www.kickstarter.com/',
'submit_url' => 'https://www.kickstarter.com/learn',
],
[
'slug' => 'kisskissbankbank',
'type' => 'funding',
'label' => 'KissKissBankBank',
'home_url' => 'https://www.kisskissbankbank.com/',
'submit_url' =>
'https://www.kisskissbankbank.com/en/financer-mon-projet',
],
[
'slug' => 'liberapay',
'type' => 'funding',
'label' => 'Liberapay',
'home_url' => 'https://liberapay.com/',
'submit_url' => 'https://liberapay.com/sign-up',
],
[
'slug' => 'patreon',
'type' => 'funding',
'label' => 'Patreon',
'home_url' => 'https://www.patreon.com/',
'submit_url' => 'https://www.patreon.com/create',
],
[
'slug' => 'tipeee',
'type' => 'funding',
'label' => 'Tipeee',
'home_url' => 'https://tipeee.com/',
'submit_url' => 'https://tipeee.com/register/',
],
[
'slug' => 'ulule',
'type' => 'funding',
'label' => 'Ulule',
'home_url' => 'https://www.ulule.com/',
'submit_url' => 'https://www.ulule.com/projects/create/#/',
],
[
'slug' => 'discord',
'type' => 'social',
'label' => 'Discord',
'home_url' => 'https://discord.com/',
'submit_url' => 'https://discord.com/register',
],
[
'slug' => 'facebook',
'type' => 'social',
'label' => 'Facebook',
'home_url' => 'https://www.facebook.com/',
'submit_url' =>
'https://www.facebook.com/pages/creation/?ref_type=comet_home',
],
[
'slug' => 'funkwhale',
'type' => 'social',
'label' => 'Funkwhale',
'home_url' => 'https://funkwhale.audio/',
'submit_url' => 'https://network.funkwhale.audio/dashboards/',
],
[
'slug' => 'instagram',
'type' => 'social',
'label' => 'Instagram',
'home_url' => 'https://www.instagram.com/',
'submit_url' =>
'https://www.instagram.com/accounts/emailsignup/',
],
[
'slug' => 'linkedin',
'type' => 'social',
'label' => 'LinkedIn',
'home_url' => 'https://www.linkedin.com/',
'submit_url' => 'https://www.linkedin.com/company/setup/new/',
],
[
'slug' => 'mastodon',
'type' => 'social',
'label' => 'Mastodon',
'home_url' => 'https://joinmastodon.org/',
'submit_url' => 'https://joinmastodon.org/communities',
],
[
'slug' => 'mobilizon',
'type' => 'social',
'label' => 'Mobilizon',
'home_url' => 'https://joinmobilizon.org/',
'submit_url' => 'https://instances.joinmobilizon.org/instances',
],
[
'slug' => 'peertube',
'type' => 'social',
'label' => 'PeerTube',
'home_url' => 'https://joinpeertube.org/',
'submit_url' => 'https://joinpeertube.org/instances',
],
[
'slug' => 'pixelfed',
'type' => 'social',
'label' => 'Pixelfed',
'home_url' => 'https://pixelfed.org/',
'submit_url' => 'https://beta.joinpixelfed.org/',
],
[
'slug' => 'plume',
'type' => 'social',
'label' => 'Plume',
'home_url' => 'https://joinplu.me/',
'submit_url' => 'https://joinplu.me/#instances',
],
[
'slug' => 'slack',
'type' => 'social',
'label' => 'Slack',
'home_url' => 'https://slack.com/',
'submit_url' => 'https://slack.com/get-started#/create',
],
[
'slug' => 'twitch',
'type' => 'social',
'label' => 'Twitch',
'home_url' => 'https://www.twitch.tv/',
'submit_url' => 'https://www.twitch.tv/signup',
],
[
'slug' => 'twitter',
'type' => 'social',
'label' => 'Twitter',
'home_url' => 'https://twitter.com/',
'submit_url' => 'https://twitter.com/i/flow/signup',
],
[
'slug' => 'writefreely',
'type' => 'social',
'label' => 'WriteFreely',
'home_url' => 'https://writefreely.org/',
'submit_url' => 'https://writefreely.org/instances',
],
[
'slug' => 'youtube',
'type' => 'social',
'label' => 'Youtube',
'home_url' => 'https://www.youtube.com/',
'submit_url' => 'https://creatoracademy.youtube.com/page/home',
],
];
$this->db
->table('platforms')
->ignore(true)
->insertBatch($data);
}
}

View file

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class TestSeeder Inserts a superadmin user in the database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class TestSeeder extends Seeder
{
public function run(): void
{
/**
* Inserts an active user with the following credentials: username: admin password: AGUehL3P
*/
$this->db->table('users')
->insert([
'id' => 1,
'username' => 'admin',
'email' => 'admin@example.com',
'password_hash' =>
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
]);
$this->db
->table('auth_groups_users')
->insert([
'group_id' => 1,
'user_id' => 1,
]);
}
}

View file

@ -3,22 +3,22 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use ActivityPub\Entities\Actor as ActivityPubActor;
use App\Models\PodcastModel;
use RuntimeException;
use Modules\Fediverse\Entities\Actor as FediverseActor;
use Override;
/**
* @property Podcast|null $podcast
* @property boolean $is_podcast
*/
class Actor extends ActivityPubActor
class Actor extends FediverseActor
{
protected ?Podcast $podcast = null;
@ -26,19 +26,36 @@ class Actor extends ActivityPubActor
public function getIsPodcast(): bool
{
return $this->getPodcast() !== null;
return $this->getPodcast() instanceof Podcast;
}
public function getPodcast(): ?Podcast
{
if ($this->id === null) {
throw new RuntimeException('Podcast id must be set before getting associated podcast.');
}
if ($this->podcast === null) {
$this->podcast = (new PodcastModel())->getPodcastByActorId($this->id);
if (! $this->podcast instanceof Podcast) {
$this->podcast = new PodcastModel()
->getPodcastByActorId($this->id);
}
return $this->podcast;
}
#[Override]
public function getAvatarImageUrl(): string
{
if ($this->podcast instanceof Podcast) {
return $this->podcast->cover->thumbnail_url;
}
return parent::getAvatarImageUrl();
}
#[Override]
public function getAvatarImageMimetype(): string
{
if ($this->podcast instanceof Podcast) {
return $this->podcast->cover->thumbnail_mimetype;
}
return parent::getAvatarImageMimetype();
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -15,7 +15,7 @@ use CodeIgniter\Entity\Entity;
/**
* @property int $id
* @property int $parent_id
* @property ?int $parent_id
* @property Category|null $parent
* @property string $code
* @property string $apple_category
@ -29,22 +29,20 @@ class Category extends Entity
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'parent_id' => '?integer',
'code' => 'string',
'apple_category' => 'string',
'id' => 'integer',
'parent_id' => '?integer',
'code' => 'string',
'apple_category' => 'string',
'google_category' => 'string',
];
/**
* @noRector ReturnTypeDeclarationRector
*/
public function getParent(): ?self
{
if ($this->parent_id === null) {
return null;
}
return (new CategoryModel())->getCategoryById($this->parent_id);
return new CategoryModel()
->getCategoryById($this->parent_id);
}
}

View file

@ -0,0 +1,156 @@
<?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\Entities\Clip;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
/**
* @property int $id
* @property int $podcast_id
* @property Podcast $podcast
* @property int $episode_id
* @property Episode $episode
* @property string $title
* @property double $start_time
* @property ?double $end_time
* @property double $duration
* @property string $type
* @property int|null $media_id
* @property Video|Audio|null $media
* @property array<mixed>|null $metadata
* @property string $status
* @property string $logs
* @property User $user
* @property int $created_by
* @property int $updated_by
* @property Time|null $job_started_at
* @property Time|null $job_ended_at
*/
class BaseClip extends Entity
{
/**
* @var Video|Audio|null
*/
protected $media;
protected ?int $job_duration = null;
protected ?float $end_time = null;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['created_at', 'updated_at', 'job_started_at', 'job_ended_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'episode_id' => 'integer',
'title' => 'string',
'start_time' => 'double',
'duration' => 'double',
'type' => 'string',
'media_id' => '?integer',
'metadata' => '?json-array',
'status' => 'string',
'logs' => 'string',
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function getJobDuration(): ?int
{
if ($this->job_duration === null && $this->job_started_at && $this->job_ended_at) {
$this->job_duration = ($this->job_started_at->difference($this->job_ended_at))
->getSeconds();
}
return $this->job_duration;
}
public function getEndTime(): float
{
if ($this->end_time === null) {
$this->end_time = $this->start_time + $this->duration;
}
return $this->end_time;
}
public function getPodcast(): ?Podcast
{
return new PodcastModel()
->getPodcastById($this->podcast_id);
}
public function getEpisode(): ?Episode
{
return new EpisodeModel()
->getEpisodeById($this->episode_id);
}
public function getUser(): ?User
{
/** @var ?User */
return new UserModel()
->find($this->created_by);
}
public function setMedia(File $file, string $fileKey): static
{
if ($this->media_id !== null) {
$this->getMedia()
->setFile($file);
$this->getMedia()
->updated_by = $this->attributes['updated_by'];
new MediaModel('audio')
->updateMedia($this->getMedia());
} else {
$media = new Audio([
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$media->setFile($file);
$this->attributes['media_id'] = new MediaModel()->saveMedia($media);
}
return $this;
}
public function getMedia(): Audio | Video | null
{
if ($this->media_id !== null && $this->media === null) {
$this->media = new MediaModel($this->type)
->getMediaById($this->media_id);
}
return $this->media;
}
}

View file

@ -0,0 +1,16 @@
<?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\Entities\Clip;
class Soundbite extends BaseClip
{
protected string $type = 'audio';
}

View file

@ -0,0 +1,88 @@
<?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\Entities\Clip;
use CodeIgniter\Files\File;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
use Override;
/**
* @property array{name:string,preview:string} $theme
* @property string $format
*/
class VideoClip extends BaseClip
{
protected string $type = 'video';
/**
* @param array<string, mixed>|null $data
*/
public function __construct(?array $data = null)
{
parent::__construct($data);
if ($this->metadata !== null && $this->metadata !== []) {
$this->theme = $this->metadata['theme'];
$this->format = $this->metadata['format'];
}
}
/**
* @param array{name:string,preview:string} $theme
*/
public function setTheme(array $theme): self
{
// TODO: change?
$this->attributes['metadata'] = json_decode($this->attributes['metadata'] ?? '[]', true);
$this->attributes['theme'] = $theme;
$this->attributes['metadata']['theme'] = $theme;
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
return $this;
}
public function setFormat(string $format): self
{
$this->attributes['metadata'] = json_decode((string) $this->attributes['metadata'], true);
$this->attributes['format'] = $format;
$this->attributes['metadata']['format'] = $format;
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
return $this;
}
#[Override]
public function setMedia(File $file, string $fileKey): static
{
if ($this->attributes['media_id'] !== null) {
// media is already set, do nothing
return $this;
}
$video = new Video([
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'],
]);
$video->setFile($file);
$this->attributes['media_id'] = new MediaModel('video')->saveMedia($video);
return $this;
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -45,22 +45,19 @@ class Credit extends Entity
* @var array<string, string>
*/
protected $casts = [
'podcast_id' => 'integer',
'episode_id' => '?integer',
'person_id' => 'integer',
'full_name' => 'string',
'podcast_id' => 'integer',
'episode_id' => '?integer',
'person_id' => 'integer',
'full_name' => 'string',
'person_group' => 'string',
'person_role' => 'string',
'person_role' => 'string',
];
public function getPerson(): ?Person
{
if ($this->person_id === null) {
throw new RuntimeException('Credit must have person_id before getting person.');
}
if ($this->person === null) {
$this->person = (new PersonModel())->getPersonById($this->person_id);
if (! $this->person instanceof Person) {
$this->person = new PersonModel()
->getPersonById($this->person_id);
}
return $this->person;
@ -68,12 +65,9 @@ class Credit extends Entity
public function getPodcast(): ?Podcast
{
if ($this->podcast_id === null) {
throw new RuntimeException('Credit must have podcast_id before getting podcast.');
}
if ($this->podcast === null) {
$this->podcast = (new PodcastModel())->getPodcastById($this->podcast_id);
if (! $this->podcast instanceof Podcast) {
$this->podcast = new PodcastModel()
->getPodcastById($this->podcast_id);
}
return $this->podcast;
@ -85,8 +79,9 @@ class Credit extends Entity
throw new RuntimeException('Credit must have episode_id before getting episode.');
}
if ($this->episode === null) {
$this->episode = (new EpisodeModel())->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
if (! $this->episode instanceof Episode) {
$this->episode = new EpisodeModel()
->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
}
return $this->episode;
@ -94,10 +89,11 @@ class Credit extends Entity
public function getGroupLabel(): string
{
if ($this->person_group === null) {
if ($this->person_group === '') {
return '';
}
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
}
@ -111,6 +107,7 @@ class Credit extends Entity
return '';
}
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label");
}
}

View file

@ -3,108 +3,113 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Libraries\SimpleRSSElement;
use App\Entities\Clip\Soundbite;
use App\Models\ClipModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use App\Models\PostModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
use RuntimeException;
use Exception;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel;
use Override;
/**
* @property int $id
* @property int $podcast_id
* @property Podcast $podcast
* @property ?string $preview_id
* @property string $preview_link
* @property string $link
* @property string $guid
* @property string $slug
* @property string $title
* @property File $audio_file
* @property string $audio_file_url
* @property string $audio_file_analytics_url
* @property string $audio_file_web_url
* @property string $audio_file_opengraph_url
* @property string $audio_file_path
* @property double $audio_file_duration
* @property string $audio_file_mimetype
* @property int $audio_file_size
* @property int $audio_file_header_size
* @property int $audio_id
* @property ?Audio $audio
* @property string $audio_url
* @property string $audio_web_url
* @property string $audio_opengraph_url
* @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown
* @property string $description_html
* @property Image $image
* @property string|null $image_path
* @property string|null $image_mimetype
* @property File|null $transcript_file
* @property string|null $transcript_file_url
* @property string|null $transcript_file_path
* @property string|null $transcript_file_remote_url
* @property File|null $chapters_file
* @property string|null $chapters_file_url
* @property string|null $chapters_file_path
* @property string|null $chapters_file_remote_url
* @property ?int $cover_id
* @property ?Image $cover
* @property int|null $transcript_id
* @property Transcript|null $transcript
* @property string|null $transcript_remote_url
* @property int|null $chapters_id
* @property Chapters|null $chapters
* @property string|null $chapters_remote_url
* @property string|null $parental_advisory
* @property int $number
* @property int $season_number
* @property ?int $number
* @property ?int $season_number
* @property string $type
* @property bool $is_blocked
* @property Location|null $location
* @property string|null $location_name
* @property string|null $location_geo
* @property string|null $location_osm
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property int $favourites_total
* @property int $reblogs_total
* @property int $statuses_total
* @property bool $is_published_on_hubs
* @property int $downloads_count
* @property int $posts_count
* @property int $comments_count
* @property EpisodeComment[]|null $comments
* @property bool $is_premium
* @property int $created_by
* @property int $updated_by
* @property string $publication_status;
* @property Time|null $published_at;
* @property Time $created_at;
* @property Time $updated_at;
* @property Time|null $deleted_at;
* @property string $publication_status
* @property Time|null $published_at
* @property Time $created_at
* @property Time $updated_at
*
* @property Person[] $persons;
* @property Soundbite[] $soundbites;
* @property string $embeddable_player_url;
* @property Person[] $persons
* @property Soundbite[] $soundbites
* @property string $embed_url
*/
class Episode extends Entity
{
public string $link = '';
public string $audio_url = '';
public string $audio_web_url = '';
public string $audio_opengraph_url = '';
protected Podcast $podcast;
protected string $link;
protected ?Audio $audio = null;
protected File $audio_file;
protected string $embed_url = '';
protected string $audio_file_url;
protected string $audio_file_analytics_url;
protected string $audio_file_web_url;
protected string $audio_file_opengraph_url;
protected string $embeddable_player_url;
protected Image $image;
protected ?Image $cover = null;
protected ?string $description = null;
protected File $transcript_file;
protected ?Transcript $transcript = null;
protected File $chapters_file;
protected ?Chapters $chapters = null;
/**
* @var Person[]|null
@ -117,237 +122,272 @@ class Episode extends Entity
protected ?array $soundbites = null;
/**
* @var Status[]|null
* @var Post[]|null
*/
protected ?array $statuses = null;
protected ?array $posts = null;
/**
* @var Status[]|null
* @var EpisodeComment[]|null
*/
protected ?array $comments = null;
protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
* @var string[]
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at'];
protected $dates = ['published_at', 'created_at', 'updated_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'guid' => 'string',
'slug' => 'string',
'title' => 'string',
'audio_file_path' => 'string',
'audio_file_duration' => 'double',
'audio_file_mimetype' => 'string',
'audio_file_size' => 'integer',
'audio_file_header_size' => 'integer',
'description_markdown' => 'string',
'description_html' => 'string',
'image_path' => '?string',
'image_mimetype' => '?string',
'transcript_file_path' => '?string',
'transcript_file_remote_url' => '?string',
'chapters_file_path' => '?string',
'chapters_file_remote_url' => '?string',
'parental_advisory' => '?string',
'number' => '?integer',
'season_number' => '?integer',
'type' => 'string',
'is_blocked' => 'boolean',
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'custom_rss' => '?json-array',
'favourites_total' => 'integer',
'reblogs_total' => 'integer',
'statuses_total' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
'id' => 'integer',
'podcast_id' => 'integer',
'preview_id' => '?string',
'guid' => 'string',
'slug' => 'string',
'title' => 'string',
'audio_id' => 'integer',
'description_markdown' => 'string',
'description_html' => 'string',
'cover_id' => '?integer',
'transcript_id' => '?integer',
'transcript_remote_url' => '?string',
'chapters_id' => '?integer',
'chapters_remote_url' => '?string',
'parental_advisory' => '?string',
'number' => '?integer',
'season_number' => '?integer',
'type' => 'string',
'is_blocked' => 'boolean',
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'is_published_on_hubs' => 'boolean',
'downloads_count' => 'integer',
'posts_count' => 'integer',
'comments_count' => 'integer',
'is_premium' => 'boolean',
'created_by' => 'integer',
'updated_by' => 'integer',
];
/**
* Saves an episode image
* @param array<string, mixed> $data
*/
public function setImage(?Image $image = null): static
#[Override]
public function injectRawData(array $data): static
{
if ($image === null) {
parent::injectRawData($data);
$this->link = url_to('episode', esc($this->getPodcast()->handle, 'url'), esc($this->attributes['slug'], 'url'));
$this->audio_url = url_to(
'episode-audio',
$this->getPodcast()
->handle,
$this->slug,
$this->getAudio()
->file_extension,
);
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
$this->audio_web_url = $this->audio_url . '?_from=-+Website+-';
return $this;
}
public function setCover(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
// Save image
$image->saveImage('podcasts/' . $this->getPodcast()->name, $this->attributes['slug']);
if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
$this->getCover()
->setFile($file);
$this->getCover()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$cover->setFile($file);
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
}
return $this;
}
public function getImage(): Image
public function getCover(): Image
{
if ($imagePath = $this->attributes['image_path']) {
return new Image(null, $imagePath, $this->attributes['image_mimetype']);
if ($this->cover instanceof Image) {
return $this->cover;
}
return $this->getPodcast()
->image;
if ($this->cover_id === null) {
$this->cover = $this->getPodcast()
->getCover();
return $this->cover;
}
$this->cover = new MediaModel('image')
->getMediaById($this->cover_id);
return $this->cover;
}
/**
* Saves an audio file
*/
public function setAudioFile(UploadedFile | File $audioFile): static
public function setAudio(UploadedFile | File|null $file = null): self
{
helper(['media', 'id3']);
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
$audioMetadata = get_file_tags($audioFile);
if ($this->audio_id !== 0) {
$this->getAudio()
->setFile($file);
$this->getAudio()
->updated_by = $this->attributes['updated_by'];
new MediaModel('audio')
->updateMedia($this->getAudio());
} else {
$audio = new Audio([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$audio->setFile($file);
$this->attributes['audio_file_path'] = save_media(
$audioFile,
'podcasts/' . $this->getPodcast()->name,
$this->attributes['slug'],
);
$this->attributes['audio_file_duration'] =
$audioMetadata['playtime_seconds'];
$this->attributes['audio_file_mimetype'] = $audioMetadata['mime_type'];
$this->attributes['audio_file_size'] = $audioMetadata['filesize'];
$this->attributes['audio_file_header_size'] =
$audioMetadata['avdataoffset'];
$this->attributes['audio_id'] = new MediaModel()->saveMedia($audio);
}
return $this;
}
/**
* Saves an episode transcript file
*/
public function setTranscriptFile(UploadedFile | File $transcriptFile): static
public function getAudio(): Audio
{
helper('media');
if (! $this->audio instanceof Audio) {
$this->audio = new MediaModel('audio')
->getMediaById($this->audio_id);
}
$this->attributes['transcript_file_path'] = save_media(
$transcriptFile,
'podcasts/' . $this->getPodcast()
->name,
$this->attributes['slug'] . '-transcript',
);
return $this->audio;
}
public function setTranscript(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if ($this->getTranscript() instanceof Transcript) {
$this->getTranscript()
->setFile($file);
$this->getTranscript()
->updated_by = $this->attributes['updated_by'];
new MediaModel('transcript')
->updateMedia($this->getTranscript());
} else {
$transcript = new Transcript([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = new MediaModel('transcript')->saveMedia($transcript);
}
return $this;
}
/**
* Saves an episode chapters file
*/
public function setChaptersFile(UploadedFile | File $chaptersFile): static
public function getTranscript(): ?Transcript
{
helper('media');
if ($this->transcript_id !== null && ! $this->transcript instanceof Transcript) {
$this->transcript = new MediaModel('transcript')
->getMediaById($this->transcript_id);
}
$this->attributes['chapters_file_path'] = save_media(
$chaptersFile,
'podcasts/' . $this->getPodcast()
->name,
$this->attributes['slug'] . '-chapters',
);
return $this->transcript;
}
public function setChapters(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if ($this->getChapters() instanceof Chapters) {
$this->getChapters()
->setFile($file);
$this->getChapters()
->updated_by = $this->attributes['updated_by'];
new MediaModel('chapters')
->updateMedia($this->getChapters());
} else {
$chapters = new Chapters([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$chapters->setFile($file);
$this->attributes['chapters_id'] = new MediaModel('chapters')->saveMedia($chapters);
}
return $this;
}
public function getAudioFile(): File
public function getChapters(): ?Chapters
{
helper('media');
return new File(media_path($this->audio_file_path));
}
public function getTranscriptFile(): ?File
{
if ($this->attributes['transcript_file_path']) {
helper('media');
return new File(media_path($this->attributes['transcript_file_path']));
if ($this->chapters_id !== null && ! $this->chapters instanceof Chapters) {
$this->chapters = new MediaModel('chapters')
->getMediaById($this->chapters_id);
}
return null;
}
public function getChaptersFile(): ?File
{
if ($this->attributes['chapters_file_path']) {
helper('media');
return new File(media_path($this->attributes['chapters_file_path']));
}
return null;
}
public function getAudioFileUrl(): string
{
helper('media');
return media_base_url($this->audio_file_path);
}
public function getAudioFileAnalyticsUrl(): string
{
helper('analytics');
// remove 'podcasts/' from audio file path
$strippedAudioFilePath = substr($this->audio_file_path, 9);
return generate_episode_analytics_url(
$this->podcast_id,
$this->id,
$strippedAudioFilePath,
$this->audio_file_duration,
$this->audio_file_size,
$this->audio_file_header_size,
$this->published_at,
);
}
public function getAudioFileWebUrl(): string
{
return $this->getAudioFileAnalyticsUrl() . '?_from=-+Website+-';
}
public function getAudioFileOpengraphUrl(): string
{
return $this->getAudioFileAnalyticsUrl() . '?_from=-+Open+Graph+-';
return $this->chapters;
}
/**
* Gets transcript url from transcript file uri if it exists or returns the transcript_file_remote_url which can be
* null.
* Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null.
*/
public function getTranscriptFileUrl(): ?string
public function getTranscriptUrl(): ?string
{
if ($this->attributes['transcript_file_path']) {
return media_base_url($this->attributes['transcript_file_path']);
if ($this->transcript instanceof Transcript) {
return $this->transcript->file_url;
}
return $this->attributes['transcript_file_remote_url'];
return $this->transcript_remote_url;
}
/**
* Gets chapters file url from chapters file uri if it exists or returns the chapters_file_remote_url which can be
* null.
* Gets chapters file url from chapters file uri if it exists or returns the chapters_remote_url which can be null.
*/
public function getChaptersFileUrl(): ?string
{
if ($this->chapters_file_path) {
return media_base_url($this->chapters_file_path);
if ($this->chapters instanceof Chapters) {
return $this->chapters->file_url;
}
return $this->chapters_file_remote_url;
return $this->chapters_remote_url;
}
/**
@ -357,145 +397,101 @@ class Episode extends Entity
*/
public function getPersons(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting persons.');
}
if ($this->persons === null) {
$this->persons = (new PersonModel())->getEpisodePersons($this->podcast_id, $this->id);
$this->persons = new PersonModel()
->getEpisodePersons($this->podcast_id, $this->id);
}
return $this->persons;
}
/**
* Returns the episodes soundbites
* Returns the episodes clips
*
* @return Soundbite[]
*/
public function getSoundbites(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting soundbites.');
}
if ($this->soundbites === null) {
$this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites($this->getPodcast() ->id, $this->id);
$this->soundbites = new ClipModel()
->getEpisodeSoundbites($this->getPodcast()->id, $this->id);
}
return $this->soundbites;
}
/**
* @return Status[]
* @return Post[]
*/
public function getStatuses(): array
public function getPosts(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting statuses.');
if ($this->posts === null) {
$this->posts = new PostModel()
->getEpisodePosts($this->id);
}
if ($this->statuses === null) {
$this->statuses = (new StatusModel())->getEpisodeStatuses($this->id);
}
return $this->statuses;
return $this->posts;
}
/**
* @return Status[]
* @return EpisodeComment[]
*/
public function getComments(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting comments.');
}
if ($this->comments === null) {
$this->comments = (new StatusModel())->getEpisodeComments($this->id);
$this->comments = new EpisodeCommentModel()
->getEpisodeComments($this->id);
}
return $this->comments;
}
public function getLink(): string
public function getEmbedUrl(?string $theme = null): string
{
return url_to('episode', $this->getPodcast()->name, $this->attributes['slug']);
}
public function getEmbeddablePlayerUrl(string $theme = null): string
{
return base_url(
$theme
? route_to(
'embeddable-player-theme',
$this->getPodcast()
->name,
$this->attributes['slug'],
$theme,
)
: route_to('embeddable-player', $this->getPodcast() ->name, $this->attributes['slug']),
);
return $theme
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme)
: url_to('embed', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
}
public function setGuid(?string $guid = null): static
{
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid;
$this->attributes['guid'] = $guid ?? $this->link;
return $this;
}
public function getPodcast(): ?Podcast
{
return (new PodcastModel())->getPodcastById($this->podcast_id);
return new PodcastModel()
->getPodcastById($this->podcast_id);
}
public function setDescriptionMarkdown(string $descriptionMarkdown): static
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
$config = [
'html_input' => 'escape',
'allow_unsafe_links' => false,
]);
];
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['description_markdown'] = $descriptionMarkdown;
$this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
$this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
return $this;
}
public function getDescriptionHtml(?string $serviceSlug = null): string
{
$descriptionHtml = '';
if (
$this->getPodcast()
->partner_id !== null &&
$this->getPodcast()
->partner_link_url !== null &&
$this->getPodcast()
->partner_image_url !== null
) {
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
$serviceSlug,
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
$serviceSlug,
)}\" alt=\"Partner image\" /></a></div>";
}
$descriptionHtml .= $this->attributes['description_html'];
if ($this->getPodcast()->episode_description_footer_html) {
$descriptionHtml .= "<footer>{$this->getPodcast()
->episode_description_footer_html}</footer>";
}
return $descriptionHtml;
}
public function getDescription(): string
{
if ($this->description === null) {
$this->description = trim(
preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
(string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])),
);
}
@ -505,8 +501,10 @@ class Episode extends Entity
public function getPublicationStatus(): string
{
if ($this->publication_status === null) {
if ($this->published_at === null) {
if (! $this->published_at instanceof Time) {
$this->publication_status = 'not_published';
} elseif ($this->getPodcast()->publication_status !== 'published') {
$this->publication_status = 'with_podcast';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
@ -522,7 +520,7 @@ class Episode extends Entity
*/
public function setLocation(?Location $location = null): static
{
if ($location === null) {
if (! $location instanceof Location) {
$this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null;
@ -550,88 +548,33 @@ class Episode extends Entity
return null;
}
if ($this->location === null) {
if (! $this->location instanceof Location) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
}
return $this->location;
}
/**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
public function getPreviewLink(): string
{
if ($this->custom_rss === null) {
return '';
if ($this->preview_id === null) {
// generate preview id
if (! $previewUUID = new EpisodeModel()->setEpisodePreviewId($this->id)) {
throw new Exception('Could not set episode preview id');
}
$this->preview_id = $previewUUID;
}
helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))
->addChild('channel')
->addChild('item');
array_to_rss([
'elements' => $this->custom_rss,
], $xmlNode);
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
return url_to('episode-preview', (string) $this->preview_id);
}
/**
* Saves custom rss tag into json
* Returns the episode's clip count
*/
public function setCustomRssString(?string $customRssString = null): static
public function getClipCount(): int|string
{
if ($customRssString === null) {
return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
$customRssString .
'</item></channel></rss>',
),
)['elements'][0]['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
}
public function getPartnerLink(?string $serviceSlug = null): string
{
$partnerLink =
rtrim($this->getPodcast()->partner_link_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']);
if ($serviceSlug !== null) {
$partnerLink .= '&_from=' . $serviceSlug;
}
return $partnerLink;
}
public function getPartnerImageUrl(string $serviceSlug = null): string
{
return rtrim($this->getPodcast()->partner_image_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
return new ClipModel()
->getClipCount($this->podcast_id, $this->id);
}
}

View file

@ -0,0 +1,142 @@
<?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\Entities;
use App\Models\ActorModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidEntity;
use RuntimeException;
/**
* @property string $id
* @property string $uri
* @property int $episode_id
* @property Episode|null $episode
* @property int $actor_id
* @property Actor|null $actor
* @property ?string $in_reply_to_id
* @property EpisodeComment|null $reply_to_comment
* @property string $message
* @property string $message_html
* @property int $likes_count
* @property int $replies_count
* @property Time $created_at
* @property int $created_by
*
* @property EpisodeComment[] $replies
*/
class EpisodeComment extends UuidEntity
{
protected ?Episode $episode = null;
protected ?Actor $actor = null;
protected ?EpisodeComment $reply_to_comment = null;
/**
* @var EpisodeComment[]|null
*/
protected ?array $replies = null;
protected bool $has_replies = false;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['created_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'string',
'uri' => 'string',
'episode_id' => 'integer',
'actor_id' => 'integer',
'in_reply_to_id' => '?string',
'message' => 'string',
'message_html' => 'string',
'likes_count' => 'integer',
'replies_count' => 'integer',
'created_by' => 'integer',
'is_from_post' => 'boolean',
];
public function getEpisode(): ?Episode
{
if (! $this->episode instanceof Episode) {
$this->episode = new EpisodeModel()
->getEpisodeById($this->episode_id);
}
return $this->episode;
}
/**
* Returns the comment's actor
*/
public function getActor(): ?Actor
{
if (! $this->actor instanceof Actor) {
$this->actor = model(ActorModel::class, false)
->getActorById($this->actor_id);
}
return $this->actor;
}
/**
* @return EpisodeComment[]
*/
public function getReplies(): array
{
if ($this->replies === null) {
$this->replies = new EpisodeCommentModel()
->getCommentReplies($this->id);
}
return $this->replies;
}
public function getHasReplies(): bool
{
return $this->getReplies() !== [];
}
public function getReplyToComment(): ?self
{
if ($this->in_reply_to_id === null) {
throw new RuntimeException('Comment is not a reply.');
}
if (! $this->reply_to_comment instanceof self) {
$this->reply_to_comment = model(EpisodeCommentModel::class, false)
->getCommentById($this->in_reply_to_id);
}
return $this->reply_to_comment;
}
public function setMessage(string $message): static
{
helper('fediverse');
$messageWithoutTags = strip_tags($message);
$this->attributes['message'] = $messageWithoutTags;
$this->attributes['message_html'] = str_replace("\n", '<br />', linkify($messageWithoutTags));
return $this;
}
}

View file

@ -1,232 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use Config\Images;
use Config\Services;
use RuntimeException;
/**
* @property File|null $file
* @property string $dirname
* @property string $filename
* @property string $extension
* @property string $mimetype
* @property string $path
* @property string $url
* @property string $thumbnail_path
* @property string $thumbnail_url
* @property string $medium_path
* @property string $medium_url
* @property string $large_path
* @property string $large_url
* @property string $feed_path
* @property string $feed_url
* @property string $id3_path
* @property string $id3_url
*/
class Image extends Entity
{
protected Images $config;
protected ?File $file = null;
protected string $dirname;
protected string $filename;
protected string $extension;
public function __construct(?File $file, string $path = '', string $mimetype = '')
{
if ($file === null && $path === '') {
throw new RuntimeException('File or path must be set to create an Image.');
}
$this->config = config('Images');
$dirname = '';
$filename = '';
$extension = '';
if ($file !== null) {
$dirname = $file->getPath();
$filename = $file->getBasename();
$extension = $file->getExtension();
$mimetype = $file->getMimeType();
}
if ($path !== '') {
[
'filename' => $filename,
'dirname' => $dirname,
'extension' => $extension,
] = pathinfo($path);
}
$this->file = $file;
$this->dirname = $dirname;
$this->filename = $filename;
$this->extension = $extension;
$this->mimetype = $mimetype;
}
public function getFile(): File
{
if ($this->file === null) {
$this->file = new File($this->path);
}
return $this->file;
}
public function getPath(): string
{
return $this->dirname . '/' . $this->filename . '.' . $this->extension;
}
public function getUrl(): string
{
helper('media');
return media_base_url($this->path);
}
public function getThumbnailPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->thumbnailSuffix .
'.' .
$this->extension;
}
public function getThumbnailUrl(): string
{
helper('media');
return media_base_url($this->thumbnail_path);
}
public function getMediumPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->mediumSuffix .
'.' .
$this->extension;
}
public function getMediumUrl(): string
{
helper('media');
return media_base_url($this->medium_path);
}
public function getLargePath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->largeSuffix .
'.' .
$this->extension;
}
public function getLargeUrl(): string
{
helper('media');
return media_base_url($this->large_path);
}
public function getFeedPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->feedSuffix .
'.' .
$this->extension;
}
public function getFeedUrl(): string
{
helper('media');
return media_base_url($this->feed_path);
}
public function getId3Path(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->id3Suffix .
'.' .
$this->extension;
}
public function getId3Url(): string
{
helper('media');
return media_base_url($this->id3_path);
}
public function saveImage(string $dirname, string $filename): void
{
helper('media');
$this->dirname = $dirname;
$this->filename = $filename;
save_media($this->file, $this->dirname, $this->filename);
$imageService = Services::image();
$thumbnailSize = $this->config->thumbnailSize;
$mediumSize = $this->config->mediumSize;
$largeSize = $this->config->largeSize;
$feedSize = $this->config->feedSize;
$id3Size = $this->config->id3Size;
$imageService
->withFile(media_path($this->path))
->resize($thumbnailSize, $thumbnailSize)
->save(media_path($this->thumbnail_path));
$imageService
->withFile(media_path($this->path))
->resize($mediumSize, $mediumSize)
->save(media_path($this->medium_path));
$imageService
->withFile(media_path($this->path))
->resize($largeSize, $largeSize)
->save(media_path($this->large_path));
$imageService
->withFile(media_path($this->path))
->resize($feedSize, $feedSize)
->save(media_path($this->feed_path));
$imageService
->withFile(media_path($this->path))
->resize($id3Size, $id3Size)
->save(media_path($this->id3_path));
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -22,7 +22,7 @@ class Language extends Entity
* @var array<string, string>
*/
protected $casts = [
'code' => 'string',
'code' => 'string',
'native_name' => 'string',
];
}

33
app/Entities/Like.php Normal file
View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use Michalsn\Uuid\UuidEntity;
/**
* @property int $actor_id
* @property string $comment_id
*/
class Like extends UuidEntity
{
/**
* @var string[]
*/
protected $uuids = ['comment_id'];
/**
* @var array<string, string>
*/
protected $casts = [
'actor_id' => 'integer',
'comment_id' => 'string',
];
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\Entities;
use CodeIgniter\Entity\Entity;
use Config\Services;
/**
* @property string $url
@ -23,15 +22,9 @@ use Config\Services;
*/
class Location extends Entity
{
/**
* @var string
*/
private const OSM_URL = 'https://www.openstreetmap.org/';
private const string OSM_URL = 'https://www.openstreetmap.org/';
/**
* @var string
*/
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
public function __construct(
protected string $name,
@ -42,14 +35,18 @@ class Location extends Entity
$longitude = null;
if ($geo !== null) {
$geoArray = explode(',', substr($geo, 4));
$latitude = floatval($geoArray[0]);
$longitude = floatval($geoArray[1]);
if (count($geoArray) === 2) {
$latitude = (float) $geoArray[0];
$longitude = (float) $geoArray[1];
}
}
parent::__construct([
'name' => $name,
'geo' => $geo,
'osm' => $osm,
'latitude' => $latitude,
'name' => $name,
'geo' => $geo,
'osm' => $osm,
'latitude' => $latitude,
'longitude' => $longitude,
]);
}
@ -81,7 +78,7 @@ class Location extends Entity
*/
public function fetchOsmLocation(): static
{
$client = Services::curlrequest();
$client = service('curlrequest');
$response = $client->request(
'GET',
@ -92,12 +89,12 @@ class Location extends Entity
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
'Accept' => 'application/json',
],
],
);
$places = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
$places = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR);
if ($places === []) {
return $this;
@ -105,16 +102,16 @@ class Location extends Entity
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
$places[0],
'lon'
'lon',
) && $places[0]->lon !== null)) {
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
}
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
$places[0],
'osm_id'
'osm_id',
) && $places[0]->osm_id !== null)) {
$this->attributes['osm'] = strtoupper(substr($places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
$this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
}
return $this;

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -12,7 +12,12 @@ namespace App\Entities;
use CodeIgniter\Entity\Entity;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
/**
* @property int $id
@ -35,11 +40,11 @@ class Page extends Entity
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'title' => 'string',
'slug' => 'string',
'id' => 'integer',
'title' => 'string',
'slug' => 'string',
'content_markdown' => 'string',
'content_html' => 'string',
'content_html' => 'string',
];
public function getLink(): string
@ -49,13 +54,20 @@ class Page extends Entity
public function setContentMarkdown(string $contentMarkdown): static
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
$config = [
'allow_unsafe_links' => false,
]);
];
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['content_markdown'] = $contentMarkdown;
$this->attributes['content_html'] = $converter->convertToHtml($contentMarkdown);
$this->attributes['content_html'] = $converter->convert($contentMarkdown);
return $this;
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
@ -12,6 +12,10 @@ namespace App\Entities;
use App\Models\PersonModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use RuntimeException;
/**
@ -19,20 +23,15 @@ use RuntimeException;
* @property string $full_name
* @property string $unique_name
* @property string|null $information_url
* @property Image $image
* @property string $image_path
* @property string $image_mimetype
* @property ?int $avatar_id
* @property ?Image $avatar
* @property int $created_by
* @property int $updated_by
* @property object[]|null $roles
*/
class Person extends Entity
{
protected Image $image;
protected ?int $podcast_id = null;
protected ?int $episode_id = null;
protected ?Image $avatar = null;
/**
* @var object[]|null
@ -43,37 +42,61 @@ class Person extends Entity
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'full_name' => 'string',
'unique_name' => 'string',
'id' => 'integer',
'full_name' => 'string',
'unique_name' => 'string',
'information_url' => '?string',
'image_path' => 'string',
'image_mimetype' => 'string',
'podcast_id' => '?integer',
'episode_id' => '?integer',
'created_by' => 'integer',
'updated_by' => 'integer',
'avatar_id' => '?int',
'podcast_id' => '?integer',
'episode_id' => '?integer',
'created_by' => 'integer',
'updated_by' => 'integer',
];
/**
* Saves a picture in `public/media/persons/`
* Saves the person avatar in `public/media/persons/`
*/
public function setImage(Image $image): static
public function setAvatar(UploadedFile | File|null $file = null): static
{
helper('media');
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
// Save image
$image->saveImage('persons', $this->attributes['unique_name']);
if (array_key_exists('avatar_id', $this->attributes) && $this->attributes['avatar_id'] !== null) {
$this->getAvatar()
->setFile($file);
$this->getAvatar()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getAvatar());
} else {
$avatar = new Image([
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->personAvatarSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$avatar->setFile($file);
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
$this->attributes['avatar_id'] = new MediaModel('image')->saveMedia($avatar);
}
return $this;
}
public function getImage(): Image
public function getAvatar(): ?Image
{
return new Image(null, $this->attributes['image_path'], $this->attributes['image_mimetype']);
if ($this->avatar_id === null) {
return null;
}
if (! $this->avatar instanceof Image) {
$this->avatar = new MediaModel('image')
->getMediaById($this->avatar_id);
}
return $this->avatar;
}
/**
@ -86,11 +109,12 @@ class Person extends Entity
}
if ($this->roles === null) {
$this->roles = (new PersonModel())->getPersonRoles(
$this->id,
(int) $this->attributes['podcast_id'],
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null
);
$this->roles = new PersonModel()
->getPersonRoles(
$this->id,
(int) $this->attributes['podcast_id'],
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null,
);
}
return $this->roles;

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity\Entity;
/**
* @property string $slug
* @property string $type
* @property string $label
* @property string $home_url
* @property string|null $submit_url
* @property string|null $link_url
* @property string|null $link_content
* @property bool|null $is_visible
* @property bool|null $is_on_embeddable_player
*/
class Platform extends Entity
{
/**
* @var array<string, string>
*/
protected $casts = [
'slug' => 'string',
'type' => 'string',
'label' => 'string',
'home_url' => 'string',
'submit_url' => '?string',
'link_url' => '?string',
'link_content' => '?string',
'is_visible' => '?boolean',
'is_on_embeddable_player' => '?boolean',
];
}

View file

@ -3,22 +3,36 @@
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Libraries\SimpleRSSElement;
use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\UserModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
use CodeIgniter\Shield\Entities\User;
use Exception;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException;
/**
@ -26,16 +40,18 @@ use RuntimeException;
* @property string $guid
* @property int $actor_id
* @property Actor|null $actor
* @property string $name
* @property string $handle
* @property string $at_handle
* @property string $link
* @property string $feed_url
* @property string $title
* @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown
* @property string $description_html
* @property Image $image
* @property string $image_path
* @property string $image_mimetype
* @property int $cover_id
* @property ?Image $cover
* @property int|null $banner_id
* @property ?Image $banner
* @property string $language_code
* @property int $category_id
* @property Category|null $category
@ -47,8 +63,6 @@ use RuntimeException;
* @property string $owner_email
* @property string $type
* @property string|null $copyright
* @property string|null $episode_description_footer_markdown
* @property string|null $episode_description_footer_html
* @property bool $is_blocked
* @property bool $is_completed
* @property bool $is_locked
@ -58,21 +72,20 @@ use RuntimeException;
* @property string|null $location_name
* @property string|null $location_geo
* @property string|null $location_osm
* @property string|null $payment_pointer
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property string|null $partner_id
* @property string|null $partner_link_url
* @property string|null $partner_image_url
* @property bool $is_published_on_hubs
* @property int $created_by
* @property int $updated_by
* @property Time $created_at;
* @property Time $updated_at;
* @property Time|null $deleted_at;
* @property string $publication_status
* @property bool $is_premium_by_default
* @property bool $is_premium
* @property Time|null $published_at
* @property Time $created_at
* @property Time $updated_at
*
* @property Episode[] $episodes
* @property Person[] $persons
* @property User[] $contributors
* @property Subscription[] $subscriptions
* @property Platform[] $podcasting_platforms
* @property Platform[] $social_platforms
* @property Platform[] $funding_platforms
@ -81,9 +94,13 @@ class Podcast extends Entity
{
protected string $link;
protected string $at_handle;
protected ?Actor $actor = null;
protected Image $image;
protected ?Image $cover = null;
protected ?Image $banner = null;
protected ?string $description = null;
@ -95,9 +112,9 @@ class Podcast extends Entity
protected ?array $other_categories = null;
/**
* @var string[]|null
* @var int[]
*/
protected ?array $other_categories_ids = null;
protected array $other_categories_ids = [];
/**
* @var Episode[]|null
@ -114,6 +131,11 @@ class Podcast extends Entity
*/
protected ?array $contributors = null;
/**
* @var Subscription[]|null
*/
protected ?array $subscriptions = null;
/**
* @var Platform[]|null
*/
@ -131,89 +153,164 @@ class Podcast extends Entity
protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['published_at', 'created_at', 'updated_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'guid' => 'string',
'actor_id' => 'integer',
'name' => 'string',
'title' => 'string',
'description_markdown' => 'string',
'description_html' => 'string',
'image_path' => 'string',
'image_mimetype' => 'string',
'language_code' => 'string',
'category_id' => 'integer',
'parental_advisory' => '?string',
'publisher' => '?string',
'owner_name' => 'string',
'owner_email' => 'string',
'type' => 'string',
'copyright' => '?string',
'episode_description_footer_markdown' => '?string',
'episode_description_footer_html' => '?string',
'is_blocked' => 'boolean',
'is_completed' => 'boolean',
'is_locked' => 'boolean',
'imported_feed_url' => '?string',
'new_feed_url' => '?string',
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'payment_pointer' => '?string',
'custom_rss' => '?json-array',
'partner_id' => '?string',
'partner_link_url' => '?string',
'partner_image_url' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
'id' => 'integer',
'guid' => 'string',
'actor_id' => 'integer',
'handle' => 'string',
'title' => 'string',
'description_markdown' => 'string',
'description_html' => 'string',
'cover_id' => 'int',
'banner_id' => '?int',
'language_code' => 'string',
'category_id' => 'integer',
'parental_advisory' => '?string',
'publisher' => '?string',
'owner_name' => 'string',
'owner_email' => 'string',
'type' => 'string',
'copyright' => '?string',
'is_blocked' => 'boolean',
'is_completed' => 'boolean',
'is_locked' => 'boolean',
'is_premium_by_default' => 'boolean',
'imported_feed_url' => '?string',
'new_feed_url' => '?string',
'location_name' => '?string',
'location_geo' => '?string',
'location_osm' => '?string',
'is_published_on_hubs' => 'boolean',
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function getActor(): Actor
public function getAtHandle(): string
{
return '@' . $this->handle;
}
public function getActor(): ?Actor
{
if ($this->actor_id === 0) {
throw new RuntimeException('Podcast must have an actor_id before getting actor.');
}
if ($this->actor === null) {
$this->actor = model('ActorModel')
if (! $this->actor instanceof Actor) {
$this->actor = model(ActorModel::class, false)
->getActorById($this->actor_id);
}
return $this->actor;
}
/**
* Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
*/
public function setImage(Image $image): static
public function setCover(UploadedFile | File|null $file = null): self
{
// Save image
$image->saveImage('podcasts/' . $this->attributes['name'], 'cover');
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
$this->getCover()
->setFile($file);
$this->getCover()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$cover->setFile($file);
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
}
return $this;
}
public function getImage(): Image
public function getCover(): Image
{
return new Image(null, $this->image_path, $this->image_mimetype);
if (! $this->cover instanceof Image) {
$cover = new MediaModel('image')
->getMediaById($this->cover_id);
if (! $cover instanceof Image) {
throw new Exception('Could not retrieve podcast cover.');
}
$this->cover = $cover;
}
return $this->cover;
}
public function setBanner(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if (array_key_exists('banner_id', $this->attributes) && $this->attributes['banner_id'] !== null) {
$this->getBanner()
->setFile($file);
$this->getBanner()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getBanner());
} else {
$banner = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
'sizes' => config('Images')
->podcastBannerSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$banner->setFile($file);
$this->attributes['banner_id'] = new MediaModel('image')->saveMedia($banner);
}
return $this;
}
public function getBanner(): ?Image
{
if ($this->banner_id === null) {
return null;
}
if (! $this->banner instanceof Image) {
$this->banner = new MediaModel('image')
->getMediaById($this->banner_id);
}
return $this->banner;
}
public function getLink(): string
{
return url_to('podcast-activity', $this->attributes['name']);
return url_to('podcast-activity', $this->attributes['handle']);
}
public function getFeedUrl(): string
{
return url_to('podcast_feed', $this->attributes['name']);
return url_to('podcast-rss-feed', $this->attributes['handle']);
}
/**
@ -223,17 +320,23 @@ class Podcast extends Entity
*/
public function getEpisodes(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting episodes.');
}
if ($this->episodes === null) {
$this->episodes = (new EpisodeModel())->getPodcastEpisodes($this->id, $this->type);
$this->episodes = new EpisodeModel()
->getPodcastEpisodes($this->id, $this->type);
}
return $this->episodes;
}
/**
* Returns the podcast's episodes count
*/
public function getEpisodesCount(): int|string
{
return new EpisodeModel()
->getPodcastEpisodesCount($this->id);
}
/**
* Returns the podcast's persons
*
@ -241,12 +344,9 @@ class Podcast extends Entity
*/
public function getPersons(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting persons.');
}
if ($this->persons === null) {
$this->persons = (new PersonModel())->getPodcastPersons($this->id);
$this->persons = new PersonModel()
->getPodcastPersons($this->id);
}
return $this->persons;
@ -257,17 +357,29 @@ class Podcast extends Entity
*/
public function getCategory(): ?Category
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting category.');
}
if ($this->category === null) {
$this->category = (new CategoryModel())->getCategoryById($this->category_id);
if (! $this->category instanceof Category) {
$this->category = new CategoryModel()
->getCategoryById($this->category_id);
}
return $this->category;
}
/**
* Returns all podcast subscriptions
*
* @return Subscription[]
*/
public function getSubscriptions(): array
{
if ($this->subscriptions === null) {
$this->subscriptions = new SubscriptionModel()
->getPodcastSubscriptions($this->id);
}
return $this->subscriptions;
}
/**
* Returns all podcast contributors
*
@ -275,12 +387,9 @@ class Podcast extends Entity
*/
public function getContributors(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcasts must be created before getting contributors.');
}
if ($this->contributors === null) {
$this->contributors = (new UserModel())->getPodcastContributors($this->id);
$this->contributors = new UserModel()
->getPodcastContributors($this->id);
}
return $this->contributors;
@ -288,41 +397,21 @@ class Podcast extends Entity
public function setDescriptionMarkdown(string $descriptionMarkdown): static
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
$config = [
'html_input' => 'escape',
'allow_unsafe_links' => false,
]);
];
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['description_markdown'] = $descriptionMarkdown;
$this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
return $this;
}
public function setEpisodeDescriptionFooterMarkdown(?string $episodeDescriptionFooterMarkdown = null): static
{
if ($episodeDescriptionFooterMarkdown === null || $episodeDescriptionFooterMarkdown === '') {
$this->attributes[
'episode_description_footer_markdown'
] = null;
$this->attributes[
'episode_description_footer_html'
] = null;
return $this;
}
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$this->attributes[
'episode_description_footer_markdown'
] = $episodeDescriptionFooterMarkdown;
$this->attributes[
'episode_description_footer_html'
] = $converter->convertToHtml($episodeDescriptionFooterMarkdown);
$this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
return $this;
}
@ -331,13 +420,28 @@ class Podcast extends Entity
{
if ($this->description === null) {
$this->description = trim(
(string) preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
(string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])),
);
}
return $this->description;
}
public function getPublicationStatus(): string
{
if ($this->publication_status === null) {
if (! $this->published_at instanceof Time) {
$this->publication_status = 'not_published';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
$this->publication_status = 'scheduled';
}
}
return $this->publication_status;
}
/**
* Returns the podcast's podcasting platform links
*
@ -345,12 +449,9 @@ class Podcast extends Entity
*/
public function getPodcastingPlatforms(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting podcasting platform links.');
}
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;
@ -363,12 +464,9 @@ class Podcast extends Entity
*/
public function getSocialPlatforms(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting social platform links.');
}
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;
@ -381,12 +479,9 @@ class Podcast extends Entity
*/
public function getFundingPlatforms(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting funding platform links.');
}
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;
@ -397,12 +492,9 @@ class Podcast extends Entity
*/
public function getOtherCategories(): array
{
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting other categories.');
}
if ($this->other_categories === null) {
$this->other_categories = (new CategoryModel())->getPodcastCategories($this->id);
$this->other_categories = new CategoryModel()
->getPodcastCategories($this->id);
}
return $this->other_categories;
@ -413,7 +505,7 @@ class Podcast extends Entity
*/
public function getOtherCategoriesIds(): array
{
if ($this->other_categories_ids === null) {
if ($this->other_categories_ids === []) {
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
}
@ -425,7 +517,7 @@ class Podcast extends Entity
*/
public function setLocation(?Location $location = null): static
{
if ($location === null) {
if (! $location instanceof Location) {
$this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null;
@ -453,58 +545,17 @@ class Podcast extends Entity
return null;
}
if ($this->location === null) {
if (! $this->location instanceof Location) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
}
return $this->location;
}
/**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
public function getIsPremium(): bool
{
if ($this->attributes['custom_rss'] === null) {
return '';
}
helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))->addChild('channel');
array_to_rss([
'elements' => $this->custom_rss,
], $xmlNode);
return str_replace(['<channel>', '</channel>'], '', $xmlNode->asXML());
}
/**
* Saves custom rss tag into json
*/
public function setCustomRssString(string $customRssString): static
{
if ($customRssString === '') {
return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel>' .
$customRssString .
'</channel></rss>',
),
)['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
// podcast is premium if at least one of its episodes is set as premium
return new EpisodeModel()
->doesPodcastHavePremiumEpisodes($this->id);
}
}

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