Clean up Podcast API endpoints, add "is_published" to podcast and episodes, make Podcast pages in the UI properly routed with VueRouter so browser history works.
This commit is contained in:
parent
a614af165f
commit
a4d45b6260
|
@ -23,6 +23,7 @@ return static function (RouteCollectorProxy $app) {
|
|||
'stations:logs' => '/logs',
|
||||
'stations:playlists:index' => '/playlists',
|
||||
'stations:podcasts:index' => '/podcasts',
|
||||
'stations:podcast:episodes' => '/podcast/{podcast_id}',
|
||||
'stations:mounts:index' => '/mounts',
|
||||
'stations:profile:index' => '/profile',
|
||||
'stations:profile:edit' => '/profile/edit',
|
||||
|
|
|
@ -30,16 +30,14 @@
|
|||
</div>
|
||||
|
||||
<div class="card-body buttons">
|
||||
<button
|
||||
type="button"
|
||||
<router-link
|
||||
class="btn btn-secondary"
|
||||
@click="doClearPodcast()"
|
||||
:to="{name: 'stations:podcasts:index'}"
|
||||
>
|
||||
<icon :icon="IconChevronLeft" />
|
||||
<span>
|
||||
{{ $gettext('All Podcasts') }}
|
||||
</span>
|
||||
</button>
|
||||
{{ $gettext('All Podcasts') }}
|
||||
</router-link>
|
||||
|
||||
<add-button
|
||||
:text="$gettext('Add Episode')"
|
||||
@click="doCreate"
|
||||
|
@ -53,45 +51,55 @@
|
|||
:fields="fields"
|
||||
:api-url="podcast.links.episodes"
|
||||
>
|
||||
<template #cell(art)="row">
|
||||
<album-art :src="row.item.art" />
|
||||
<template #cell(art)="{item}">
|
||||
<album-art :src="item.art" />
|
||||
</template>
|
||||
<template #cell(title)="row">
|
||||
<template #cell(title)="{item}">
|
||||
<h5 class="m-0">
|
||||
{{ row.item.title }}
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
<a
|
||||
:href="row.item.links.public"
|
||||
target="_blank"
|
||||
>{{ $gettext('Public Page') }}</a>
|
||||
<div v-if="item.is_published">
|
||||
<a
|
||||
:href="item.links.public"
|
||||
target="_blank"
|
||||
>{{ $gettext('Public Page') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="badges"
|
||||
>
|
||||
<span class="badge text-bg-info">
|
||||
{{ $gettext('Unpublished') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(podcast_media)="row">
|
||||
<template v-if="row.item.media">
|
||||
<span>{{ row.item.media.original_name }}</span>
|
||||
<template #cell(podcast_media)="{item}">
|
||||
<template v-if="item.media">
|
||||
<span>{{ item.media.original_name }}</span>
|
||||
<br>
|
||||
<small>{{ row.item.media.length_text }}</small>
|
||||
<small>{{ item.media.length_text }}</small>
|
||||
</template>
|
||||
</template>
|
||||
<template #cell(explicit)="row">
|
||||
<template #cell(explicit)="{item}">
|
||||
<span
|
||||
v-if="row.item.explicit"
|
||||
v-if="item.explicit"
|
||||
class="badge text-bg-danger"
|
||||
>{{ $gettext('Explicit') }}</span>
|
||||
<span v-else> </span>
|
||||
</template>
|
||||
<template #cell(actions)="row">
|
||||
<template #cell(actions)="{item}">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="doEdit(row.item.links.self)"
|
||||
@click="doEdit(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Edit') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="doDelete(row.item.links.self)"
|
||||
@click="doDelete(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Delete') }}
|
||||
</button>
|
||||
|
@ -111,32 +119,25 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataTable, { DataTableField } from '~/components/Common/DataTable.vue';
|
||||
import EditModal from './EpisodeEditModal.vue';
|
||||
import DataTable, {DataTableField} from '~/components/Common/DataTable.vue';
|
||||
import EditModal from './Podcasts/EpisodeEditModal.vue';
|
||||
import Icon from '~/components/Common/Icon.vue';
|
||||
import AlbumArt from '~/components/Common/AlbumArt.vue';
|
||||
import StationsCommonQuota from "~/components/Stations/Common/Quota.vue";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {ref} from "vue";
|
||||
import {useSweetAlert} from "~/vendor/sweetalert";
|
||||
import {useNotify} from "~/functions/useNotify";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
import AddButton from "~/components/Common/AddButton.vue";
|
||||
import {IconChevronLeft} from "~/components/Common/icons";
|
||||
import {DataTableTemplateRef} from "~/functions/useHasDatatable.ts";
|
||||
import {getStationApiUrl} from "~/router.ts";
|
||||
import useConfirmAndDelete from "~/functions/useConfirmAndDelete.ts";
|
||||
import {ApiPodcast} from "~/entities/ApiInterfaces.ts";
|
||||
|
||||
const props = defineProps({
|
||||
quotaUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
podcast: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
podcast: ApiPodcast
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['clear-podcast']);
|
||||
const quotaUrl = getStationApiUrl('/quota/station_podcasts');
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
|
@ -166,24 +167,8 @@ const doEdit = (url) => {
|
|||
$editEpisodeModal.value?.edit(url);
|
||||
};
|
||||
|
||||
const doClearPodcast = () => {
|
||||
emit('clear-podcast');
|
||||
};
|
||||
|
||||
const {confirmDelete} = useSweetAlert();
|
||||
const {notifySuccess} = useNotify();
|
||||
const {axios} = useAxios();
|
||||
|
||||
const doDelete = (url) => {
|
||||
confirmDelete({
|
||||
title: $gettext('Delete Episode?'),
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
notifySuccess(resp.data.message);
|
||||
relist();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const {doDelete} = useConfirmAndDelete(
|
||||
$gettext('Delete Episode?'),
|
||||
() => relist()
|
||||
);
|
||||
</script>
|
|
@ -1,39 +1,158 @@
|
|||
<template>
|
||||
<episodes-view
|
||||
v-if="activePodcast"
|
||||
:podcast="activePodcast"
|
||||
:quota-url="quotaUrl"
|
||||
@clear-podcast="onClearPodcast"
|
||||
/>
|
||||
<list-view
|
||||
v-else
|
||||
v-bind="pickProps(props, listViewProps)"
|
||||
:quota-url="quotaUrl"
|
||||
@select-podcast="onSelectPodcast"
|
||||
<card-page>
|
||||
<template #header>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h2 class="card-title">
|
||||
{{ $gettext('Podcasts') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-5 text-end">
|
||||
<stations-common-quota
|
||||
ref="$quota"
|
||||
:quota-url="quotaUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<add-button
|
||||
:text="$gettext('Add Podcast')"
|
||||
@click="doCreate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<data-table
|
||||
id="station_podcasts"
|
||||
ref="$datatable"
|
||||
paginated
|
||||
:fields="fields"
|
||||
:api-url="listUrl"
|
||||
>
|
||||
<template #cell(art)="{item}">
|
||||
<album-art :src="item.art" />
|
||||
</template>
|
||||
<template #cell(title)="{item}">
|
||||
<h5 class="m-0">
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
<div v-if="item.is_published">
|
||||
<a
|
||||
:href="item.links.public_episodes"
|
||||
target="_blank"
|
||||
>{{ $gettext('Public Page') }}</a> •
|
||||
<a
|
||||
:href="item.links.public_feed"
|
||||
target="_blank"
|
||||
>{{ $gettext('RSS Feed') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="badges"
|
||||
>
|
||||
<span class="badge text-bg-info">
|
||||
{{ $gettext('Unpublished') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(actions)="{item}">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="doEdit(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Edit') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="doDelete(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Delete') }}
|
||||
</button>
|
||||
<router-link
|
||||
class="btn btn-secondary"
|
||||
:to="{name: 'stations:podcast:episodes', params: {podcast_id: item.id}}"
|
||||
>
|
||||
{{ $gettext('Episodes') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</data-table>
|
||||
</card-page>
|
||||
|
||||
<edit-modal
|
||||
ref="$editPodcastModal"
|
||||
:create-url="listUrl"
|
||||
:new-art-url="newArtUrl"
|
||||
:language-options="languageOptions"
|
||||
:categories-options="categoriesOptions"
|
||||
@relist="relist"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EpisodesView from './Podcasts/EpisodesView.vue';
|
||||
import ListView from './Podcasts/ListView.vue';
|
||||
import DataTable, {DataTableField} from '~/components/Common/DataTable.vue';
|
||||
import EditModal from './Podcasts/PodcastEditModal.vue';
|
||||
import AlbumArt from '~/components/Common/AlbumArt.vue';
|
||||
import StationsCommonQuota from "~/components/Stations/Common/Quota.vue";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {ref} from "vue";
|
||||
import listViewProps from "./Podcasts/listViewProps";
|
||||
import {pickProps} from "~/functions/pickProps";
|
||||
import {getStationApiUrl} from "~/router";
|
||||
import AddButton from "~/components/Common/AddButton.vue";
|
||||
import {DataTableTemplateRef} from "~/functions/useHasDatatable.ts";
|
||||
import CardPage from "~/components/Common/CardPage.vue";
|
||||
import useConfirmAndDelete from "~/functions/useConfirmAndDelete.ts";
|
||||
|
||||
const props = defineProps({
|
||||
...listViewProps
|
||||
languageOptions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
categoriesOptions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
});
|
||||
|
||||
const quotaUrl = getStationApiUrl('/quota/station_podcasts');
|
||||
const listUrl = getStationApiUrl('/podcasts');
|
||||
const newArtUrl = getStationApiUrl('/podcasts/art');
|
||||
|
||||
const activePodcast = ref(null);
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
const onSelectPodcast = (podcast) => {
|
||||
activePodcast.value = podcast;
|
||||
const fields: DataTableField[] = [
|
||||
{key: 'art', label: $gettext('Art'), sortable: false, class: 'shrink pe-0'},
|
||||
{key: 'title', label: $gettext('Podcast'), sortable: false},
|
||||
{
|
||||
key: 'episodes',
|
||||
label: $gettext('# Episodes'),
|
||||
sortable: false,
|
||||
},
|
||||
{key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
];
|
||||
|
||||
const $quota = ref<InstanceType<typeof StationsCommonQuota> | null>(null);
|
||||
const $datatable = ref<DataTableTemplateRef>(null);
|
||||
|
||||
const relist = () => {
|
||||
$quota.value?.update();
|
||||
$datatable.value?.refresh();
|
||||
};
|
||||
|
||||
const onClearPodcast = () => {
|
||||
activePodcast.value = null;
|
||||
}
|
||||
const $editPodcastModal = ref<InstanceType<typeof EditModal> | null>(null);
|
||||
|
||||
const doCreate = () => {
|
||||
$editPodcastModal.value?.create();
|
||||
};
|
||||
|
||||
const doEdit = (url) => {
|
||||
$editPodcastModal.value?.edit(url);
|
||||
};
|
||||
|
||||
const {doDelete} = useConfirmAndDelete(
|
||||
$gettext('Delete Podcast?'),
|
||||
() => relist()
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
<template>
|
||||
<section
|
||||
class="card"
|
||||
role="region"
|
||||
>
|
||||
<div class="card-header text-bg-primary">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h2 class="card-title">
|
||||
{{ $gettext('Podcasts') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-5 text-end">
|
||||
<stations-common-quota
|
||||
ref="$quota"
|
||||
:quota-url="quotaUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body buttons">
|
||||
<add-button
|
||||
:text="$gettext('Add Podcast')"
|
||||
@click="doCreate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<data-table
|
||||
id="station_podcasts"
|
||||
ref="$datatable"
|
||||
paginated
|
||||
:fields="fields"
|
||||
:api-url="listUrl"
|
||||
>
|
||||
<template #cell(art)="{item}">
|
||||
<album-art :src="item.art" />
|
||||
</template>
|
||||
<template #cell(title)="{item}">
|
||||
<h5 class="m-0">
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
<a
|
||||
:href="item.links.public_episodes"
|
||||
target="_blank"
|
||||
>{{ $gettext('Public Page') }}</a> •
|
||||
<a
|
||||
:href="item.links.public_feed"
|
||||
target="_blank"
|
||||
>{{ $gettext('RSS Feed') }}</a>
|
||||
</template>
|
||||
<template #cell(actions)="{item}">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="doEdit(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Edit') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="doDelete(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Delete') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="doSelectPodcast(item)"
|
||||
>
|
||||
{{ $gettext('Episodes') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</data-table>
|
||||
</section>
|
||||
|
||||
<edit-modal
|
||||
ref="$editPodcastModal"
|
||||
:create-url="listUrl"
|
||||
:new-art-url="newArtUrl"
|
||||
:language-options="languageOptions"
|
||||
:categories-options="categoriesOptions"
|
||||
@relist="relist"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataTable, {DataTableField} from '~/components/Common/DataTable.vue';
|
||||
import EditModal from './PodcastEditModal.vue';
|
||||
import AlbumArt from '~/components/Common/AlbumArt.vue';
|
||||
import StationsCommonQuota from "~/components/Stations/Common/Quota.vue";
|
||||
import listViewProps from "./listViewProps";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {ref} from "vue";
|
||||
import {useSweetAlert} from "~/vendor/sweetalert";
|
||||
import {useNotify} from "~/functions/useNotify";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
import {getStationApiUrl} from "~/router";
|
||||
import AddButton from "~/components/Common/AddButton.vue";
|
||||
import {DataTableTemplateRef} from "~/functions/useHasDatatable.ts";
|
||||
|
||||
const props = defineProps({
|
||||
...listViewProps,
|
||||
quotaUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const listUrl = getStationApiUrl('/podcasts');
|
||||
const newArtUrl = getStationApiUrl('/podcasts/art');
|
||||
|
||||
const emit = defineEmits(['select-podcast']);
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
const fields: DataTableField[] = [
|
||||
{key: 'art', label: $gettext('Art'), sortable: false, class: 'shrink pe-0'},
|
||||
{key: 'title', label: $gettext('Podcast'), sortable: false},
|
||||
{
|
||||
key: 'episodes',
|
||||
label: $gettext('# Episodes'),
|
||||
sortable: false,
|
||||
formatter: (val) => {
|
||||
return val?.length ?? 0;
|
||||
}
|
||||
},
|
||||
{key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
];
|
||||
|
||||
const $quota = ref<InstanceType<typeof StationsCommonQuota> | null>(null);
|
||||
const $datatable = ref<DataTableTemplateRef>(null);
|
||||
|
||||
const relist = () => {
|
||||
$quota.value?.update();
|
||||
$datatable.value?.refresh();
|
||||
};
|
||||
|
||||
const $editPodcastModal = ref<InstanceType<typeof EditModal> | null>(null);
|
||||
|
||||
const doCreate = () => {
|
||||
$editPodcastModal.value?.create();
|
||||
};
|
||||
|
||||
const doEdit = (url) => {
|
||||
$editPodcastModal.value?.edit(url);
|
||||
};
|
||||
|
||||
const doSelectPodcast = (podcast) => {
|
||||
emit('select-podcast', podcast);
|
||||
};
|
||||
|
||||
const {confirmDelete} = useSweetAlert();
|
||||
const {notifySuccess} = useNotify();
|
||||
const {axios} = useAxios();
|
||||
|
||||
const doDelete = (url) => {
|
||||
confirmDelete({
|
||||
title: $gettext('Delete Podcast?'),
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
notifySuccess(resp.data.message);
|
||||
relist();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
|
@ -1,10 +0,0 @@
|
|||
export default {
|
||||
languageOptions: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
categoriesOptions: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {getStationApiUrl} from "~/router.ts";
|
||||
import populateComponentRemotely from "~/functions/populateComponentRemotely.ts";
|
||||
import {RouteRecordRaw} from "vue-router";
|
||||
import {useAxios} from "~/vendor/axios.ts";
|
||||
|
||||
export default function useStationsRoutes(): RouteRecordRaw[] {
|
||||
return [
|
||||
|
@ -62,6 +63,22 @@ export default function useStationsRoutes(): RouteRecordRaw[] {
|
|||
name: 'stations:podcasts:index',
|
||||
...populateComponentRemotely(getStationApiUrl('/vue/podcasts'))
|
||||
},
|
||||
{
|
||||
path: '/podcast/:podcast_id',
|
||||
component: () => import('~/components/Stations/PodcastEpisodes.vue'),
|
||||
name: 'stations:podcast:episodes',
|
||||
beforeEnter: async (to, _, next) => {
|
||||
const apiUrl = getStationApiUrl(`/podcast/${to.params.podcast_id}`);
|
||||
const {axios} = useAxios();
|
||||
to.meta.state = {
|
||||
podcast: await axios.get(apiUrl.value).then(r => r.data)
|
||||
};
|
||||
next();
|
||||
},
|
||||
props: (to) => {
|
||||
return to.meta.state;
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'stations:profile:index',
|
||||
|
|
|
@ -597,26 +597,39 @@ export interface ApiNowPlayingStationRemote {
|
|||
}
|
||||
|
||||
export type ApiPodcast = HasLinks & {
|
||||
id?: string | null;
|
||||
storage_location_id?: number | null;
|
||||
title?: string | null;
|
||||
id?: string;
|
||||
storage_location_id?: number;
|
||||
title?: string;
|
||||
link?: string | null;
|
||||
description?: string | null;
|
||||
language?: string | null;
|
||||
author?: string | null;
|
||||
email?: string | null;
|
||||
description?: string;
|
||||
description_short?: string;
|
||||
language?: string;
|
||||
language_name?: string;
|
||||
author?: string;
|
||||
email?: string;
|
||||
has_custom_art?: boolean;
|
||||
art?: string | null;
|
||||
art?: string;
|
||||
art_updated_at?: number;
|
||||
categories?: string[];
|
||||
episodes?: string[];
|
||||
is_published?: boolean;
|
||||
episodes?: number;
|
||||
categories?: ApiPodcastCategory[];
|
||||
};
|
||||
|
||||
export interface ApiPodcastCategory {
|
||||
category?: string;
|
||||
text?: string;
|
||||
title?: string;
|
||||
subtitle?: string | null;
|
||||
}
|
||||
|
||||
export type ApiPodcastEpisode = HasLinks & {
|
||||
id?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
description_short?: string;
|
||||
explicit?: boolean;
|
||||
created_at?: number;
|
||||
is_published?: boolean;
|
||||
publish_at?: number | null;
|
||||
has_media?: boolean;
|
||||
media?: ApiPodcastMedia;
|
||||
|
@ -649,32 +662,32 @@ export interface ApiSong {
|
|||
* The song artist.
|
||||
* @example "Chet Porter"
|
||||
*/
|
||||
artist?: string;
|
||||
artist?: string | null;
|
||||
/**
|
||||
* The song title.
|
||||
* @example "Aluko River"
|
||||
*/
|
||||
title?: string;
|
||||
title?: string | null;
|
||||
/**
|
||||
* The song album.
|
||||
* @example "Moving Castle"
|
||||
*/
|
||||
album?: string;
|
||||
album?: string | null;
|
||||
/**
|
||||
* The song genre.
|
||||
* @example "Rock"
|
||||
*/
|
||||
genre?: string;
|
||||
genre?: string | null;
|
||||
/**
|
||||
* The International Standard Recording Code (ISRC) of the file.
|
||||
* @example "US28E1600021"
|
||||
*/
|
||||
isrc?: string;
|
||||
isrc?: string | null;
|
||||
/**
|
||||
* Lyrics to the song.
|
||||
* @example ""
|
||||
*/
|
||||
lyrics?: string;
|
||||
lyrics?: string | null;
|
||||
/**
|
||||
* URL to the album artwork (if available).
|
||||
* @example "https://picsum.photos/1200/1200"
|
||||
|
|
|
@ -54,6 +54,12 @@ final class Podcast
|
|||
#[OA\Property]
|
||||
public int $art_updated_at = 0;
|
||||
|
||||
#[OA\Property]
|
||||
public bool $is_published = true;
|
||||
|
||||
#[OA\Property]
|
||||
public int $episodes = 0;
|
||||
|
||||
/**
|
||||
* @var PodcastCategory[]
|
||||
*/
|
||||
|
|
|
@ -33,6 +33,9 @@ final class PodcastEpisode
|
|||
#[OA\Property]
|
||||
public int $created_at;
|
||||
|
||||
#[OA\Property]
|
||||
public bool $is_published = true;
|
||||
|
||||
#[OA\Property]
|
||||
public ?int $publish_at = null;
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ namespace App\Entity\ApiGenerator;
|
|||
use App\Entity\Api\Podcast as ApiPodcast;
|
||||
use App\Entity\Api\PodcastCategory as ApiPodcastCategory;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Entity\Station;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Strings;
|
||||
use Symfony\Component\Intl\Exception\MissingResourceException;
|
||||
|
@ -14,6 +16,16 @@ use Symfony\Component\Intl\Languages;
|
|||
|
||||
final class PodcastApiGenerator
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string>>
|
||||
*/
|
||||
private array $publishedPodcasts = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly PodcastRepository $podcastRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
Podcast $record,
|
||||
ServerRequest $request
|
||||
|
@ -60,9 +72,13 @@ final class PodcastApiGenerator
|
|||
}
|
||||
$return->categories = $categories;
|
||||
|
||||
$return->is_published = $this->isPublished($record, $station);
|
||||
|
||||
$return->art_updated_at = $record->getArtUpdatedAt();
|
||||
$return->has_custom_art = (0 !== $record->getArtUpdatedAt());
|
||||
|
||||
$return->episodes = $record->getEpisodes()->count();
|
||||
|
||||
$baseRouteParams = [
|
||||
'station_id' => $station->getIdRequired(),
|
||||
'podcast_id' => $record->getIdRequired(),
|
||||
|
@ -104,4 +120,21 @@ final class PodcastApiGenerator
|
|||
|
||||
return $return;
|
||||
}
|
||||
|
||||
private function isPublished(
|
||||
Podcast $podcast,
|
||||
Station $station
|
||||
): bool {
|
||||
if (!isset($this->publishedPodcasts[$station->getShortName()])) {
|
||||
$this->publishedPodcasts[$station->getShortName()] = $this->podcastRepo->getPodcastIdsWithPublishedEpisodes(
|
||||
$station
|
||||
);
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$podcast->getIdRequired(),
|
||||
$this->publishedPodcasts[$station->getShortName()] ?? [],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ final class PodcastEpisodeApiGenerator
|
|||
$return->media = new ApiPodcastMedia();
|
||||
}
|
||||
|
||||
$return->is_published = $record->isPublished();
|
||||
|
||||
$return->art_updated_at = $record->getArtUpdatedAt();
|
||||
$return->has_custom_art = (0 !== $return->art_updated_at);
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class Podcast implements Interfaces\IdentifiableEntityInterface
|
|||
protected Collection $categories;
|
||||
|
||||
/** @var Collection<int, PodcastEpisode> */
|
||||
#[ORM\OneToMany(mappedBy: 'podcast', targetEntity: PodcastEpisode::class)]
|
||||
#[ORM\OneToMany(mappedBy: 'podcast', targetEntity: PodcastEpisode::class, fetch: 'EXTRA_LAZY')]
|
||||
protected Collection $episodes;
|
||||
|
||||
public function __construct(StorageLocation $storageLocation)
|
||||
|
|
|
@ -3938,44 +3938,52 @@ components:
|
|||
properties:
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
storage_location_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
title:
|
||||
type: string
|
||||
nullable: true
|
||||
link:
|
||||
type: string
|
||||
nullable: true
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
description_short:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
nullable: true
|
||||
language_name:
|
||||
type: string
|
||||
author:
|
||||
type: string
|
||||
nullable: true
|
||||
email:
|
||||
type: string
|
||||
nullable: true
|
||||
has_custom_art:
|
||||
type: boolean
|
||||
art:
|
||||
type: string
|
||||
nullable: true
|
||||
art_updated_at:
|
||||
type: integer
|
||||
is_published:
|
||||
type: boolean
|
||||
episodes:
|
||||
type: integer
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
episodes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/components/schemas/Api_PodcastCategory'
|
||||
type: object
|
||||
Api_PodcastCategory:
|
||||
properties:
|
||||
category:
|
||||
type: string
|
||||
text:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
subtitle:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
Api_PodcastEpisode:
|
||||
type: object
|
||||
allOf:
|
||||
|
@ -3985,15 +3993,18 @@ components:
|
|||
properties:
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
title:
|
||||
type: string
|
||||
nullable: true
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
description_short:
|
||||
type: string
|
||||
explicit:
|
||||
type: boolean
|
||||
created_at:
|
||||
type: integer
|
||||
is_published:
|
||||
type: boolean
|
||||
publish_at:
|
||||
type: integer
|
||||
nullable: true
|
||||
|
@ -4041,26 +4052,32 @@ components:
|
|||
description: 'The song artist.'
|
||||
type: string
|
||||
example: 'Chet Porter'
|
||||
nullable: true
|
||||
title:
|
||||
description: 'The song title.'
|
||||
type: string
|
||||
example: 'Aluko River'
|
||||
nullable: true
|
||||
album:
|
||||
description: 'The song album.'
|
||||
type: string
|
||||
example: 'Moving Castle'
|
||||
nullable: true
|
||||
genre:
|
||||
description: 'The song genre.'
|
||||
type: string
|
||||
example: Rock
|
||||
nullable: true
|
||||
isrc:
|
||||
description: 'The International Standard Recording Code (ISRC) of the file.'
|
||||
type: string
|
||||
example: US28E1600021
|
||||
nullable: true
|
||||
lyrics:
|
||||
description: 'Lyrics to the song.'
|
||||
type: string
|
||||
example: ''
|
||||
nullable: true
|
||||
art:
|
||||
description: 'URL to the album artwork (if available).'
|
||||
example: 'https://picsum.photos/1200/1200'
|
||||
|
|
Loading…
Reference in New Issue