From 1a04f9791f8a2c98a3ec11e32e570a8573337fcb Mon Sep 17 00:00:00 2001 From: Vaalyn Date: Tue, 25 May 2021 06:29:07 +0200 Subject: [PATCH] Implement Podcasting Support Co-authored-by: Buster "Silver Eagle" Neece Co-authored-by: Mitch --- CHANGELOG.md | 5 + azuracast.dev.env | 3 + composer.json | 2 + composer.lock | 233 ++++++++++ config/assets.php | 12 + config/menus/station.php | 6 + config/messagequeue.php | 3 + config/routes/api.php | 91 ++++ config/routes/public.php | 12 + config/routes/stations.php | 4 + frontend/vue/Admin/StorageLocations.vue | 4 + frontend/vue/Common/AlbumArt.vue | 24 + frontend/vue/Common/DataTable.vue | 13 +- frontend/vue/Stations/Media.vue | 17 +- frontend/vue/Stations/Podcasts.vue | 30 ++ .../vue/Stations/Podcasts/Common/Artwork.vue | 54 +++ .../Stations/Podcasts/EpisodeEditModal.vue | 236 ++++++++++ .../Podcasts/EpisodeForm/BasicInfo.vue | 86 ++++ .../Stations/Podcasts/EpisodeForm/Media.vue | 56 +++ .../vue/Stations/Podcasts/EpisodesView.vue | 158 +++++++ frontend/vue/Stations/Podcasts/ListView.vue | 154 +++++++ .../Stations/Podcasts/PodcastEditModal.vue | 220 +++++++++ .../Podcasts/PodcastForm/BasicInfo.vue | 84 ++++ .../vue/Stations/Profile/PublicPagesPanel.vue | 7 + frontend/webpack.config.js | 1 + src/Acl.php | 2 + .../Stations/PodcastEpisodesController.php | 386 ++++++++++++++++ .../Stations/Podcasts/Art/DeleteArtAction.php | 47 ++ .../Stations/Podcasts/Art/GetArtAction.php | 42 ++ .../Podcasts/Episodes/Art/DeleteArtAction.php | 36 ++ .../Podcasts/Episodes/Art/GetArtAction.php | 49 ++ .../Podcasts/Episodes/DownloadAction.php | 51 +++ .../Api/Stations/PodcastsController.php | 345 ++++++++++++++ .../PublicPages/PodcastEpisodeController.php | 78 ++++ .../PublicPages/PodcastEpisodesController.php | 87 ++++ .../PublicPages/PodcastFeedController.php | 358 +++++++++++++++ .../PublicPages/PodcastsController.php | 35 ++ src/Controller/Stations/PodcastsAction.php | 35 ++ src/Entity/Api/Podcast.php | 69 +++ src/Entity/Api/PodcastEpisode.php | 64 +++ src/Entity/Api/PodcastMedia.php | 36 ++ src/Entity/Fixture/Podcast.php | 44 ++ src/Entity/Fixture/PodcastEpisode.php | 92 ++++ src/Entity/Fixture/Station.php | 3 + src/Entity/Metadata.php | 12 + .../Migration/Version20210512225946.php | 104 +++++ src/Entity/Podcast.php | 198 ++++++++ src/Entity/PodcastCategory.php | 233 ++++++++++ src/Entity/PodcastEpisode.php | 231 ++++++++++ src/Entity/PodcastMedia.php | 242 ++++++++++ .../Repository/PodcastEpisodeRepository.php | 133 ++++++ .../Repository/PodcastMediaRepository.php | 96 ++++ src/Entity/Repository/PodcastRepository.php | 134 ++++++ .../Repository/StorageLocationRepository.php | 5 + src/Entity/Station.php | 40 ++ src/Entity/StationMedia.php | 1 - src/Entity/StorageLocation.php | 1 + .../InvalidPodcastMediaFileException.php | 21 + .../PodcastEpisodeNotFoundException.php | 21 + .../PodcastMediaNotFoundException.php | 21 + .../PodcastMediaProcessingException.php | 21 + src/Exception/PodcastNotFoundException.php | 21 + src/Flysystem/StationFilesystems.php | 17 + src/Form/StationCloneForm.php | 1 + src/Http/ServerRequest.php | 12 +- .../MetadataService/GetId3MetadataService.php | 2 + src/Message/AddNewPodcastMediaMessage.php | 21 + src/Message/ReprocessPodcastMediaMessage.php | 26 ++ src/MessageQueue/QueueManager.php | 2 + src/Middleware/HandleMultipartJson.php | 45 ++ ...quirePublishedPodcastEpisodeMiddleware.php | 105 +++++ src/Radio/Configuration.php | 1 + .../frontend/public/podcast-episode.js.phtml | 3 + .../frontend/public/podcast-episode.phtml | 119 +++++ .../frontend/public/podcast-episodes.js.phtml | 3 + .../frontend/public/podcast-episodes.phtml | 109 +++++ templates/frontend/public/podcasts.phtml | 106 +++++ templates/stations/podcasts/index.phtml | 20 + templates/stations/profile/index.phtml | 6 + util/openapi.php | 1 + web/static/api/openapi.yml | 431 +++++++++++++++++- web/static/img/generic_song.jpg | Bin 198358 -> 918175 bytes 82 files changed, 5894 insertions(+), 45 deletions(-) create mode 100644 frontend/vue/Common/AlbumArt.vue create mode 100644 frontend/vue/Stations/Podcasts.vue create mode 100644 frontend/vue/Stations/Podcasts/Common/Artwork.vue create mode 100644 frontend/vue/Stations/Podcasts/EpisodeEditModal.vue create mode 100644 frontend/vue/Stations/Podcasts/EpisodeForm/BasicInfo.vue create mode 100644 frontend/vue/Stations/Podcasts/EpisodeForm/Media.vue create mode 100644 frontend/vue/Stations/Podcasts/EpisodesView.vue create mode 100644 frontend/vue/Stations/Podcasts/ListView.vue create mode 100644 frontend/vue/Stations/Podcasts/PodcastEditModal.vue create mode 100644 frontend/vue/Stations/Podcasts/PodcastForm/BasicInfo.vue create mode 100644 src/Controller/Api/Stations/PodcastEpisodesController.php create mode 100644 src/Controller/Api/Stations/Podcasts/Art/DeleteArtAction.php create mode 100644 src/Controller/Api/Stations/Podcasts/Art/GetArtAction.php create mode 100644 src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php create mode 100644 src/Controller/Api/Stations/Podcasts/Episodes/Art/GetArtAction.php create mode 100644 src/Controller/Api/Stations/Podcasts/Episodes/DownloadAction.php create mode 100644 src/Controller/Api/Stations/PodcastsController.php create mode 100644 src/Controller/Frontend/PublicPages/PodcastEpisodeController.php create mode 100644 src/Controller/Frontend/PublicPages/PodcastEpisodesController.php create mode 100644 src/Controller/Frontend/PublicPages/PodcastFeedController.php create mode 100644 src/Controller/Frontend/PublicPages/PodcastsController.php create mode 100644 src/Controller/Stations/PodcastsAction.php create mode 100644 src/Entity/Api/Podcast.php create mode 100644 src/Entity/Api/PodcastEpisode.php create mode 100644 src/Entity/Api/PodcastMedia.php create mode 100644 src/Entity/Fixture/Podcast.php create mode 100644 src/Entity/Fixture/PodcastEpisode.php create mode 100644 src/Entity/Migration/Version20210512225946.php create mode 100644 src/Entity/Podcast.php create mode 100644 src/Entity/PodcastCategory.php create mode 100644 src/Entity/PodcastEpisode.php create mode 100644 src/Entity/PodcastMedia.php create mode 100644 src/Entity/Repository/PodcastEpisodeRepository.php create mode 100644 src/Entity/Repository/PodcastMediaRepository.php create mode 100644 src/Entity/Repository/PodcastRepository.php create mode 100644 src/Exception/InvalidPodcastMediaFileException.php create mode 100644 src/Exception/PodcastEpisodeNotFoundException.php create mode 100644 src/Exception/PodcastMediaNotFoundException.php create mode 100644 src/Exception/PodcastMediaProcessingException.php create mode 100644 src/Exception/PodcastNotFoundException.php create mode 100644 src/Message/AddNewPodcastMediaMessage.php create mode 100644 src/Message/ReprocessPodcastMediaMessage.php create mode 100644 src/Middleware/HandleMultipartJson.php create mode 100644 src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php create mode 100644 templates/frontend/public/podcast-episode.js.phtml create mode 100644 templates/frontend/public/podcast-episode.phtml create mode 100644 templates/frontend/public/podcast-episodes.js.phtml create mode 100644 templates/frontend/public/podcast-episodes.phtml create mode 100644 templates/frontend/public/podcasts.phtml create mode 100644 templates/stations/podcasts/index.phtml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fbaaaa16..410a50c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ release channel, you can take advantage of these new features and fixes. ## New Features/Changes +- **Podcast Management (Beta):** You can now upload and manage podcasts directly via the AzuraCast web interface. Via + this interface, you can create and manage individual podcast episodes and associate them with uploaded media (which + can be managed in an interface similar to the Media Manager). Podcasts have their own automatically generated public + pages and RSS feeds that are compatible with many major podcast aggregation services. + - **Automatic Theme Selection:** If you haven't set a default theme for either your user account or the AzuraCast public pages, the theme will automatically be determined by the user's browser based on their OS's theme preference (dark or light). You can override this by selecting a default theme in the "Branding" settings, or reset to using browser diff --git a/azuracast.dev.env b/azuracast.dev.env index 26231dce4..c76f7c0ea 100644 --- a/azuracast.dev.env +++ b/azuracast.dev.env @@ -15,7 +15,10 @@ INIT_DEMO_API_KEY= INIT_ADMIN_EMAIL= INIT_ADMIN_PASSWORD= INIT_ADMIN_API_KEY= + INIT_MUSIC_PATH=/var/azuracast/www/util/fixtures/init_music +INIT_PODCASTS_PATH=/var/azuracast/www/util/fixtures/init_podcasts + INIT_GEOLITE_LICENSE_KEY= # diff --git a/composer.json b/composer.json index 8c4e51837..564bab139 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "league/mime-type-detection": "^1.7", "league/plates": "^3.1", "lstrojny/fxmlrpc": "dev-master", + "marcw/rss-writer": "^0.4.0", "matomo/device-detector": "^4.0", "mezzio/mezzio-session": "^1.3", "mezzio/mezzio-session-cache": "^1.4", @@ -66,6 +67,7 @@ "symfony/console": "^5", "symfony/event-dispatcher": "^5", "symfony/finder": "^5", + "symfony/intl": "^5.2", "symfony/lock": "^5.1", "symfony/mailer": "^5.2", "symfony/messenger": "^5", diff --git a/composer.lock b/composer.lock index 08b6823e4..6addd867b 100644 --- a/composer.lock +++ b/composer.lock @@ -3572,6 +3572,64 @@ }, "time": "2021-05-21T15:11:33+00:00" }, + { + "name": "marcw/rss-writer", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/marcw/rss-writer.git", + "reference": "4bbd63aea62246fe43bec589a1e8bdda2f4ef219" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marcw/rss-writer/zipball/4bbd63aea62246fe43bec589a1e8bdda2f4ef219", + "reference": "4bbd63aea62246fe43bec589a1e8bdda2f4ef219", + "shasum": "" + }, + "require": { + "ext-xmlwriter": "*" + }, + "require-dev": { + "phpunit/phpunit": "^5.4", + "symfony/debug": "^3.1", + "symfony/http-foundation": "^3.1", + "symfony/validator": "^3.1", + "symfony/var-dumper": "^3.1" + }, + "suggest": { + "symfony/http-foundation": "Enable streaming RSS response", + "symfony/validator": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "MarcW\\RssWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marc Weistroff", + "email": "marc@weistroff.net" + } + ], + "description": "A simple yet powerful RSS2 feed writer with RSS extensions support (like iTunes podcast tags)", + "keywords": [ + "feed", + "podcast", + "podcasting", + "rss", + "rss2" + ], + "support": { + "issues": "https://github.com/marcw/rss-writer/issues", + "source": "https://github.com/marcw/rss-writer/tree/master" + }, + "time": "2017-04-01T11:53:47+00:00" + }, { "name": "matomo/device-detector", "version": "4.2.2", @@ -6901,6 +6959,94 @@ ], "time": "2021-02-15T18:55:04+00:00" }, + { + "name": "symfony/intl", + "version": "v5.2.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "6d40be5e4331041aa14add5633986d95667ae624" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/6d40be5e4331041aa14add5633986d95667ae624", + "reference": "6d40be5e4331041aa14add5633986d95667ae624", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "require-dev": { + "symfony/filesystem": "^4.4|^5.0" + }, + "suggest": { + "ext-intl": "to use the component with locales other than \"en\"" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a PHP replacement layer for the C intl extension that includes additional data from the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "support": { + "source": "https://github.com/symfony/intl/tree/v5.2.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-04-24T14:39:13+00:00" + }, { "name": "symfony/lock", "version": "v5.2.6", @@ -7312,6 +7458,93 @@ ], "time": "2021-01-07T16:49:33+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "af1842919c7e7364aaaa2798b29839e3ba168588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/af1842919c7e7364aaaa2798b29839e3ba168588", + "reference": "af1842919c7e7364aaaa2798b29839e3ba168588", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.22.1", diff --git a/config/assets.php b/config/assets.php index 4c4139046..3e7cd9dcb 100644 --- a/config/assets.php +++ b/config/assets.php @@ -530,6 +530,18 @@ return [ // Auto-managed by Assets ], + 'Vue_StationsPodcasts' => [ + 'order' => 10, + 'require' => ['vue-component-common', 'bootstrap-vue', 'fancybox', 'moment_base', 'moment_timezone'], + // Auto-managed by Assets + ], + + 'Vue_StationsPodcastEpisodes' => [ + 'order' => 10, + 'require' => ['vue-component-common', 'bootstrap-vue', 'fancybox', 'moment_base', 'moment_timezone'], + // Auto-managed by Assets + ], + 'Vue_StationsProfile' => [ 'order' => 10, 'require' => ['vue-component-common', 'bootstrap-vue', 'moment', 'fancybox'], diff --git a/config/menus/station.php b/config/menus/station.php index 18e68ed16..91f212727 100644 --- a/config/menus/station.php +++ b/config/menus/station.php @@ -70,6 +70,12 @@ return function (App\Event\BuildStationMenu $e) { 'visible' => $backend->supportsMedia(), 'permission' => Acl::STATION_MEDIA, ], + 'podcasts' => [ + 'label' => __('Podcasts (Beta)'), + 'icon' => 'cast', + 'url' => $router->fromHere('stations:podcasts:index'), + 'permission' => Acl::STATION_PODCASTS, + ], 'streamers' => [ 'label' => __('Streamer/DJ Accounts'), 'icon' => 'mic', diff --git a/config/messagequeue.php b/config/messagequeue.php index 5a1141f67..7eaa35eeb 100644 --- a/config/messagequeue.php +++ b/config/messagequeue.php @@ -9,6 +9,9 @@ return [ Message\AddNewMediaMessage::class => Task\CheckMediaTask::class, Message\ReprocessMediaMessage::class => Task\CheckMediaTask::class, + Message\AddNewPodcastMediaMessage::class => Task\CheckPodcastMediaTask::class, + Message\ReprocessPodcastMediaMessage::class => Task\CheckPodcastMediaTask::class, + Message\WritePlaylistFileMessage::class => Liquidsoap\ConfigWriter::class, Message\UpdateNowPlayingMessage::class => Task\NowPlayingTask::class, diff --git a/config/routes/api.php b/config/routes/api.php index 1e45e67fa..3303cc9c5 100644 --- a/config/routes/api.php +++ b/config/routes/api.php @@ -267,6 +267,97 @@ return function (App $app) { $group->delete('/art/{media_id:[a-zA-Z0-9]+}', Controller\Api\Stations\Art\DeleteArtAction::class) ->add(new Middleware\Permissions(Acl::STATION_MEDIA, true)); + // Public and private podcast pages + $group->group( + '/podcast/{podcast_id}', + function (RouteCollectorProxy $group) { + $group->get('', Controller\Api\Stations\PodcastsController::class . ':getAction') + ->setName('api:stations:podcast'); + + $group->get( + '/art', + Controller\Api\Stations\Podcasts\Art\GetArtAction::class + )->setName('api:stations:podcast:art'); + + $group->get( + '/episodes', + Controller\Api\Stations\PodcastEpisodesController::class . ':listAction' + )->setName('api:stations:podcast:episodes'); + + $group->group( + '/episode/{episode_id}', + function (RouteCollectorProxy $group) { + $group->get( + '', + Controller\Api\Stations\PodcastEpisodesController::class . ':getAction' + )->setName('api:stations:podcast:episode'); + + $group->get( + '/art', + Controller\Api\Stations\Podcasts\Episodes\Art\GetArtAction::class + )->setName('api:stations:podcast:episode:art'); + + $group->get( + '/download', + Controller\Api\Stations\Podcasts\Episodes\DownloadAction::class + )->setName('api:stations:podcast:episode:download'); + } + ); + } + )->add(Middleware\RequirePublishedPodcastEpisodeMiddleware::class); + + // Private-only podcast pages + $group->group( + '/podcasts', + function (RouteCollectorProxy $group) { + $group->get('', Controller\Api\Stations\PodcastsController::class . ':listAction') + ->setName('api:stations:podcasts'); + + $group->post('', Controller\Api\Stations\PodcastsController::class . ':createAction') + ->add(new Middleware\HandleMultipartJson()); + } + )->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true)); + + $group->group( + '/podcast/{podcast_id}', + function (RouteCollectorProxy $group) { + $group->post('', Controller\Api\Stations\PodcastsController::class . ':editAction') + ->add(new Middleware\HandleMultipartJson()); + + $group->delete('', Controller\Api\Stations\PodcastsController::class . ':deleteAction'); + + $group->delete( + '/art', + Controller\Api\Stations\Podcasts\Art\DeleteArtAction::class + )->setName('api:stations:podcast:art-internal'); + + $group->post( + '/episodes', + Controller\Api\Stations\PodcastEpisodesController::class . ':createAction' + )->add(new Middleware\HandleMultipartJson()); + + $group->group( + '/episode/{episode_id}', + function (RouteCollectorProxy $group) { + $group->post( + '', + Controller\Api\Stations\PodcastEpisodesController::class . ':editAction' + )->add(new Middleware\HandleMultipartJson()); + + $group->delete( + '', + Controller\Api\Stations\PodcastEpisodesController::class . ':deleteAction' + ); + + $group->delete( + '/art', + Controller\Api\Stations\Podcasts\Episodes\Art\DeleteArtAction::class + )->setName('api:stations:podcast:episode:art-internal'); + } + ); + } + )->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true)); + $station_api_endpoints = [ ['file', 'files', Controller\Api\Stations\FilesController::class, Acl::STATION_MEDIA], ['mount', 'mounts', Controller\Api\Stations\MountsController::class, Acl::STATION_MOUNTS], diff --git a/config/routes/public.php b/config/routes/public.php index c3dd1117f..e48caa91d 100644 --- a/config/routes/public.php +++ b/config/routes/public.php @@ -26,6 +26,18 @@ return function (App $app) { $group->get('/ondemand[/{embed:embed}]', Controller\Frontend\PublicPages\OnDemandAction::class) ->setName('public:ondemand'); + + $group->get('/podcasts', Controller\Frontend\PublicPages\PodcastsController::class) + ->setName('public:podcasts'); + + $group->get('/podcast/{podcast_id}/episodes', Controller\Frontend\PublicPages\PodcastEpisodesController::class) + ->setName('public:podcast:episodes'); + + $group->get('/podcast/{podcast_id}/episode/{episode_id}', Controller\Frontend\PublicPages\PodcastEpisodeController::class) + ->setName('public:podcast:episode'); + + $group->get('/podcast/{podcast_id}/feed', Controller\Frontend\PublicPages\PodcastFeedController::class) + ->setName('public:podcast:feed'); } ) ->add(Middleware\GetStation::class) diff --git a/config/routes/stations.php b/config/routes/stations.php index 4b32eeb0d..c943681dd 100644 --- a/config/routes/stations.php +++ b/config/routes/stations.php @@ -55,6 +55,10 @@ return function (App $app) { ->setName('stations:playlists:index') ->add(new Middleware\Permissions(Acl::STATION_MEDIA, true)); + $group->get('/podcasts', Controller\Stations\PodcastsAction::class) + ->setName('stations:podcasts:index') + ->add(new Middleware\Permissions(Acl::STATION_PODCASTS, true)); + $group->group( '/mounts', function (RouteCollectorProxy $group) { diff --git a/frontend/vue/Admin/StorageLocations.vue b/frontend/vue/Admin/StorageLocations.vue index 7ee874bd5..da65229b3 100644 --- a/frontend/vue/Admin/StorageLocations.vue +++ b/frontend/vue/Admin/StorageLocations.vue @@ -7,6 +7,7 @@ + @@ -72,6 +73,9 @@ export default { langStationRecordingsTab () { return this.$gettext('Station Recordings'); }, + langStationPodcastsTab () { + return this.$gettext('Station Podcasts'); + }, langBackupsTab () { return this.$gettext('Backups'); }, diff --git a/frontend/vue/Common/AlbumArt.vue b/frontend/vue/Common/AlbumArt.vue new file mode 100644 index 000000000..df09e07e3 --- /dev/null +++ b/frontend/vue/Common/AlbumArt.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/frontend/vue/Common/DataTable.vue b/frontend/vue/Common/DataTable.vue index eb77ca1e7..b0e639334 100644 --- a/frontend/vue/Common/DataTable.vue +++ b/frontend/vue/Common/DataTable.vue @@ -60,11 +60,11 @@ :no-provider-filtering="handleClientSide" tbody-tr-class="align-middle" thead-tr-class="align-middle" selected-variant="" :filter="filter" @filtered="onFiltered" @refreshed="onRefreshed"> -