feat(podcast): created ui, working on logics

This commit is contained in:
Marco Cavalli 2025-06-27 16:03:25 +02:00
parent baeb70dd46
commit f042bf2140
18 changed files with 774 additions and 31 deletions

View file

@ -0,0 +1,38 @@
<?php
namespace App\Filters;
use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\LikeFilter;
class PodcastFilter
{
protected $filters = [
'title' => LikeFilter::class,
'all' => AllFilter::class,
];
public function apply($query, $filters)
{
foreach ($this->receivedFilters($filters) as $name => $value) {
switch ($name) {
case 'all':
$name = array_diff(array_keys($this->filters), ['all']);
$filterInstance = new $this->filters['all'];
break;
default:
$filterInstance = new $this->filters[$name];
break;
}
$query = $filterInstance($query, $name, $value);
}
return $query;
}
public function receivedFilters($filters)
{
return $filters->only(array_keys($this->filters));
}
}

View file

@ -2,7 +2,9 @@
namespace App\Http\Controllers;
use App\Models\Podcast\ImportedPodcast;
use App\Models\Podcast\Podcast;
use App\Models\Podcast\PodcastEpisode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -14,9 +16,9 @@ class PodcastController extends Controller
} else {
$pagination = $request->per_page;
}
return Podcast::searchFilter($request)
->with(['episodes', 'imported'])
->where('id', '!=', 1)
->with(['episodes', 'imported', 'owner'])
->paginate($pagination)
->toJson();
}
@ -35,13 +37,28 @@ class PodcastController extends Controller
return $this->save($request);
}
/**
* Delete a smart block, cleaning also cc_blockcontent and cc_blockcriteria
* @param $id
* @return int
*/
public function delete($id) {
return Podcast::with(['content', 'criteria'])::destroy($id);
public function destroy($id) {
try {
PodcastEpisode::where('podcast_id', $id)->delete();
ImportedPodcast::where('podcast_id', $id)->delete();
Podcast::destroy($id);
return true;
} catch(\Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function show(Request $request, $id) {
$xml = simplexml_load_file($request->url);
return $xml->channel;
}
public function getEpisodes(Request $request)
{
$xml = simplexml_load_file($request->url, null, LIBXML_NOCDATA);
$xmlArray = (array) $xml->channel;
//episodes are stored in `item` array
return $xmlArray['item'];
}
protected function save(Request $request)
@ -68,6 +85,9 @@ class PodcastController extends Controller
'owner' => $user->id,
])->save();
return $dbPodcast->toJson();
return response()->json([
'podcast' => $dbPodcast,
'episodes' => $xml->channel->children('item', TRUE)
]);
}
}

View file

@ -5,32 +5,58 @@ namespace App\Http\Controllers;
use App\Models\Podcast\Podcast;
use App\Models\Podcast\PodcastEpisode;
use Illuminate\Http\Request;
use Celery\Celery;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Celery\Celery;
class PodcastEpisodeController extends Controller
{
private static $_CELERY_MESSAGE_TIMEOUT = 900000; // 15 minutes
public function downloadPodcastEpisode(Request $request) {
public function index(Request $request) {
$user = Auth::user();
try {
if (!$user->id) {
throw new \Exception('You must be logged in');
}
return PodcastEpisode::where('podcast_id','=',$request->podcast_id)->get()->toJson();
} catch (\Exception $exception) {
return response($exception->getMessage(), 500);
}
}
public function store(Request $request) {
$user = Auth::user();
try {
if (!$user->id) {
throw new \Exception('You must be logged in');
}
return $this->downloadPodcastEpisode($request);
} catch (\Exception $exception) {
return response($exception->getMessage(), 500);
}
}
private function downloadPodcastEpisode(Request $request) {
$request->validate([
'podcast_id' => 'required',
'download_url' => 'required',
'episode_url' => 'required',
'episode_title' => 'required',
]);
try {
$podcast = Podcast::find($request->podcast_id);
$podcast = Podcast::findOrFail($request->podcast_id);
$podcastEpisode = new PodcastEpisode();
$podcastEpisode->fill([
'podcast_id' => $request->podcast_id,
'publication_date' =>$request->publication_date,
'download_url' => $request->download_url,
'episode_guid' => $request->episode_guid,
'episode_title' => $request->episode_title,
'episode_description' => $request->episode_description,
'publication_date' =>$request->episode['pubDate'],
'download_url' => $request->episode['link'],
'episode_guid' => $request->episode['guid'],
'episode_title' => $request->episode['title'],
'episode_description' => htmlentities($request->episode['description']),
])->save();
$conn = new Celery(
@ -47,25 +73,33 @@ class PodcastEpisodeController extends Controller
$data = [
'episode_id' => $podcastEpisode->id,
'episode_url' => $podcastEpisode->url,
'episode_url' => $request->episode_url,
'episode_title' => $podcastEpisode->episode_title,
'podcast_name' => $podcast->title,
'override_album' => 'false' //ToDo connect $album_override from imported_podcast,
];
$result = $conn->PostTask('tasks.download', $data);
$result = $conn->PostTask('podcast-download', $data, true, 'podcast');
return $result->getId();
while (!$result->isReady()) {
sleep(1);
}
if (!$result->isSuccess()) {
//Todo: throw exception
throw new \Exception('podcast episode id:'.$podcastEpisode->id.' download failed');
}
//Todo: return ok
return $result;
// $podcastEpisode->fill([
// ''
// ]);
} catch (\Exception $exception) {
Log::error($exception->getMessage());
die($exception->getMessage());
}
}

View file

@ -2,11 +2,10 @@
namespace App\Models\Podcast;
use App\Filters\PlaylistFilter;
use App\Filters\PodcastFilter;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Podcast extends Model
{
@ -47,7 +46,7 @@ class Podcast extends Model
}
public function scopeSearchFilter($query, $request) {
$filters = new PlaylistFilter();
$filters = new PodcastFilter();
return $filters->apply($query, $request);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models\Podcast;
use App\Models\File;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -22,6 +23,9 @@ class PodcastEpisode extends Model
'episode_description',
];
public function file() {
return $this->hasOne(File::class);
}
public function podcast() {
return $this->belongsTo(Podcast::class);
}

View file

@ -0,0 +1,143 @@
<script setup lang="ts">
import Table from "@partials/Table.vue";
import {usePodcastStore} from "@/stores/podcast.store.ts";
import {podcast_page} from "@/composables/content/podcast_page.ts";
import {basePodcast} from "@models/podcast/podcast.ts";
import {reactive, ref} from "vue";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import PodcastEditor from "@partials/PodcastEditor.vue";
import SmartBlockEditor from "@partials/SmartBlockEditor.vue";
const podcastStore = usePodcastStore();
podcastStore.loadPodcast(basePodcast());
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = podcast_page();
const url = ref('');
const itemEdited = ref(basePodcast());
const bulk = ref(false);
const dialog = reactive({
open: false,
type: '',
title: '',
text: ''
});
const openDialog = (type, title = '', text = '', bulk = false) => {
dialog.open = true
dialog.type = type
dialog.title = title
dialog.text = text
}
const add = () => {
openDialog(
'add',
'Aggiungi podcast',
'Inserisci l\'url del feed RSS del podcast che vuoi aggiungere.'
);
}
const confirm = (confirm, bulk) => {
switch (dialog.type) {
case 'delete':
confirmDelete(confirm, bulk);
break;
case 'add':
confirmAdd(confirm);
}
}
const confirmAdd = (confirm) => {
if (confirm) {
podcastStore.updateField({key: 'url', value: url});
console.log(podcastStore);
}
closeDialog()
}
const edit = (item) => {
podcastStore.loadPodcast(item);
}
const cancel = (item) => {
bulk.value = Array.isArray(item)
itemEdited.value = item
openDialog(
'delete',
'Cancella',
bulk.value ? 'Vuoi cancellare i podcast selezionati?' : 'Vuoi cancellare il podcast selezionato?'
)
}
const confirmDelete = (confirm, bulk) => {
if (confirm) {
if (!bulk) {
deleteItem(itemEdited.value.id)
} else {
itemEdited.value.forEach(el => {
deleteItem(el.id)
})
}
}
closeDialog()
}
const closeDialog = () => {
dialog.open = false
itemEdited.value = basePodcast();
}
const updateSearch = (text) => {
search.value = text
}
</script>
<template>
<PodcastEditor
v-if="podcastStore.currentPodcast.url != '' && podcastStore.currentPodcast.url != null"
@go-back="podcastStore.currentPodcast = basePodcast()"
/>
<Table
v-else
:headers="headers"
v-model:selected="selected"
v-model:search="search"
:list-data="listData"
:items="items"
:loading="loading"
:get-items="getItems"
:actions="true"
:show-select="true"
@update-table="getItems"
@update-search="updateSearch"
@delete-item="cancel"
@edit-item="edit"
>
<template v-slot:header-buttons>
<v-btn color="primary" @click="add">
Aggiungi podcast
</v-btn>
</template>
<template v-slot:dialog>
<v-dialog v-model="dialog.open">
<ConfirmDelete
:title="dialog.title"
:text="dialog.text"
:bulk="bulk"
@confirm="confirm"
@after-leave="closeDialog"
>
<VTextField
label="Feed RSS"
v-if="dialog.type === 'add'"
v-model="url"
/>
</ConfirmDelete>
</v-dialog>
</template>
</Table>
</template>
<style scoped>
</style>

View file

@ -0,0 +1,153 @@
<script setup lang="ts">
import {useAuthStore} from "@/stores/auth.store.ts";
import {usePodcastStore} from "@/stores/podcast.store.ts";
import {podcast} from "@models/podcast/podcast.ts";
import {podcast_episode_page} from "@/composables/content/podcastEpisode_page.ts";
import {ref, watch} from "vue";
import axios from "axios";
const auth = useAuthStore();
const emit = defineEmits([
'saveItem'
])
const podcastStore = usePodcastStore();
const item = podcastStore.currentPodcast;
console.log(item)
const podcastFields = podcast(item);
const { items, headers, loading, downloadingEpisode, downloadEpisode } = podcast_episode_page(item.id, item.url);
const episodes = ref([]);
const checkError = (field, model) => {
if (field.required) {
// const error = field.required && (model === '' || model === null)
// // disabledSaveButton.value = error
// return error
}
return false
}
watch(items, (newItems, oldItems) => {
console.log(newItems, oldItems)
if (item.id > 0) {
axios.get('/podcast_episode', {
params: {
podcast_id: item.id
}
}).then((response) => {
newItems.forEach((element) => {
const episode = response.data.filter(imp => {
if (imp.episode_guid === element.guid) {
return true;
}
});
if (episode.length > 0) {
element.imported = (episode[0].file_id === null) ? null : episode[0].file_id;
}
});
});
}
console.log(newItems)
episodes.value = newItems;
}, {deep: true});
</script>
<template>
<v-row no-gutters>
<v-col>
<Component
v-for="(field, key) in podcastFields()"
:is="field.component"
:label="field.label"
:value="field.value ? field.value : field.type == 'checkbox' ? true : null"
:disabled="field.disabled"
@update:modelValue="checkError(field, item[key])"
@update-property="updateProperty"
:error="checkError(field, item[key])"
rows="2"
:items="field.items"
v-model="item[key]"
item-title="title"
item-value="value"
density="compact"
hide-details="auto"
class="mb-2"
clearable
:active="true"
/>
<v-btn
color="accent"
@click="$emit('goBack')"
>Torna indietro</v-btn>
<v-btn
color="primary"
@click="save"
:disabled="disabledSaveButton"
>Salva</v-btn>
</v-col>
<v-col>
<!-- Tracks-->
<VDataTable
:headers="headers"
:items="episodes"
:loading="loading"
>
<template v-slot:item.short_description="{ item }">
{{ item.short_description }}
</template>
<template v-slot:item.imported="{ item }">
{{item.imported}}
<v-icon
class="me-2 spinning"
size="small"
v-if="item.imported === null"
>
mdi-loading
</v-icon>
<v-icon
class="me-2"
size="small"
v-else-if="item.imported > 0"
>
mdi-check-outline
</v-icon>
<v-icon
class="me-2 text-center"
size="small"
v-else
@click="downloadEpisode(item)"
>
mdi-download-box
</v-icon>
</template>
</VDataTable>
</v-col>
</v-row>
</template>
<style scoped>
.tables > .v-col {
width: 50%;
overflow: hidden;
}
.v-list {
max-height: 77vh;
margin: 4px 0 0 4px;
border-radius: 4px;
}
.spinning {
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View file

@ -15,6 +15,7 @@ console.log(props.bulk)
</v-card-title>
<v-card-text>
<p>{{ props.text }}</p>
<slot></slot>
</v-card-text>
<v-card-actions>
<v-btn
@ -24,7 +25,7 @@ console.log(props.bulk)
<v-btn
color="accent"
@click="$emit('confirm',false)"
>Cancella</v-btn>
>Annulla</v-btn>
</v-card-actions>
</v-card>
</template>

View file

@ -0,0 +1,109 @@
// Interfaccia Podcast
import {cleanOptions} from "@/helpers/AxiosHelper.ts";
import axios, {type AxiosResponse} from "axios";
import {VCheckbox, VTextField} from "vuetify/components";
import type {PodcastEpisode} from "@models/podcast/podcastEpisode.ts";
export interface Podcast {
id: number;
url: string;
title: string;
creator?: string;
description: string;
language: string;
copyright?: string;
link: string;
itunes_author?: string;
itunes_keywords?: string;
itunes_summary?: string;
itunes_subtitle?: string;
itunes_category?: string;
itunes_explicit?: string;
owner: number; // ID dell'owner
episodes?: PodcastEpisode[];
}
// Costante basePodcast
export const basePodcast = (): Podcast => {
return {
id: 0,
url: '',
title: '',
description: '',
language: '',
link: '',
itunes_explicit: 'false',
owner: 0,
}
};
export const PodcastTableHeader = [
{title: 'Nome', value: 'title'},
{title: 'Creato da', value: 'owner.login'},
{title: 'Data di importazione', value: 'imported.auto_ingest_timestamp'},
{title: 'Azioni', value: 'actions'}
];
export const getPodcast = async (options: {
id?: number | null;
page?: Number | null;
per_page?: Number | null;
all?: string | null;
}): Promise<Podcast[]> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/podcast`, {params: filteredParams})
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
})
}
export const deletePodcast = async (podcastIds: Number[]) => {
return axios.delete(`podcast`, {
data: {
_method: 'DELETE',
'podcastIds': podcastIds
}
})
}
export function podcast(item) {
const visibleFields = {
name: {
title: 'Nome del podcast',
required: true,
disabled: false
},
url: {
title: 'URL del podcast',
required: true,
disabled: true
},
auto_ingest: {
title: 'Scarica l\'ultimo episodio in automatico',
required: false,
disabled: false
}
}
return () => {
const fields = {}
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key].title,
value: item !== null ? item[key] : '',
required: visibleFields[key].required,
disabled: (visibleFields[key].disabled !== undefined) ? visibleFields[key].disabled : false,
component: VTextField
}
// console.log(fields)
switch (key) {
case 'auto_ingest':
fields[key].component = VCheckbox
break
}
})
return fields
}
}

View file

@ -0,0 +1,48 @@
import type {Podcast} from "@models/podcast/podcast.ts";
import {cleanOptions} from "@/helpers/AxiosHelper.ts";
import axios, {type AxiosResponse} from "axios";
export interface PodcastEpisode {
id: number;
file_id?: number;
podcast_id: number;
publication_date: Date;
download_url: string;
episode_guid: string;
episode_title: string;
episode_description: string;
}
export const PodcastEpisodeTableHeader = [
{title: 'Importazione', value: 'imported'},
{title: 'Titolo', value: 'title'},
{title: 'Descrizione', value: 'short_description'},
{title: 'Autore', value: 'author'},
{title: 'Data di pubblicazione', value: 'pubDate'}
];
export const getPodcastEpisodes = async (options: {
podcast_id: Number;
url: String;
}): Promise<Array[]> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/rss_podcast_episodes`, {params: filteredParams})
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
})
}
export const downloadPodcastEpisode = async (options: {
podcast_id: Number;
podcast: Object;
}): Promise => { //must add <Type>
const filteredParams = cleanOptions(options);
return await axios.post(`/podcast_episode`, {params: filteredParams})
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
})
}

View file

@ -0,0 +1,86 @@
import {reactive, ref} from "vue";
import axios, {type AxiosResponse} from "axios";
import {type PodcastEpisode, PodcastEpisodeTableHeader} from "@models/podcast/podcastEpisode.ts";
import {DateTime} from "luxon";
export function podcast_episode_page(podcast_id: Number, url: String) {
const items = ref([]);
const loading = ref(false);
const listData = reactive({
'itemsPerPage': 5,
'first_page': null,
'last_page': null,
'total_items': 0,
'page': 1,
});
const headers = PodcastEpisodeTableHeader;
const downloadingEpisode = reactive({});
const importedPodcastEpisodes = async (podcast_id, element) => {
return await axios.get('/podcast_episode', {
params: {
podcast_id: podcast_id
}
}).then( (response) => {
// console.log(response.data);
const episode = response.data.filter(imp => {
if (imp.episode_guid === element.guid) {
return true;
}
});
if (episode.length > 0) {
return (episode[0].file_id === null) ? -1 : episode[0].file_id;
}
return null;
});
}
const getItems = async () => {
loading.value = true;
console.log(url)
return await axios.get(`/rss_podcast_episodes`, {
params: {
podcast_id: podcast_id,
url: url
}
}).then( async (podcastEpisodesList: AxiosResponse) => {
// console.log(podcastEpisodesList, podcast_id);
const episodes = podcastEpisodesList.data;
items.value = await episodes.map(element => {
element.imported = -1;
element.short_description = '';
const arr = element.description.split(' ');
for (let j = 0; j < 20; j++) {
element.short_description += arr[j];
element.short_description += (j === 19) ? '...' : ' ';
}
element.short_description = (element.short_description + '').replace(/&#\d+;/gm, function(s) {
return String.fromCharCode(s.match(/\d+/gm)[0]);
});
return element;
});
loading.value = false;
}).catch((error: Error) => {
console.log("Error: " + error);
});
}
const downloadEpisode = async (item) => {
console.log(item.enclosure["@attributes"].url)
return await axios.post('/podcast_episode', {
podcast_id: podcast_id,
episode: item,
episode_url: item.enclosure["@attributes"].url,
episode_title: item.title,
}).then((response) => {
console.log(response);
}).catch((error) => {
console.log("Error: "+error);
});
}
getItems();
return { items, listData, headers, loading, downloadingEpisode, downloadEpisode }
}

View file

@ -0,0 +1,80 @@
import {reactive, ref} from "vue";
import axios from "axios";
import {timeFormatter} from "@/helpers/TimeFormatter.ts";
import {deleteSmartBlock, getSmartBlock, SmartBlockTableHeader} from "@models/smartblock/smartblock.ts";
import {showTableHeader} from "@models/show/show.ts";
import {useAuthStore} from "@/stores/auth.store.ts";
import {getPodcast, PodcastTableHeader} from "@models/podcast/podcast.ts";
export function podcast_page() {
const items = ref([])
const selected = ref([])
const loading = ref(false)
const search = ref('')
const listData = reactive({
'itemsPerPage': 5,
'first_page': null,
'last_page': null,
'total_items': 0,
'page': 1,
})
const auth = useAuthStore();
const timezone = auth.userData.timezone;
const headers = PodcastTableHeader;
const getItems = async (options) => {
loading.value = true;
return getPodcast({
page: options.page,
per_page: options.itemsPerPage,
all: search.value
}).then((podcastList) => {
console.log(podcastList)
listData.itemsPerPage = podcastList.per_page;
listData.first_page = podcastList.from;
listData.last_page = podcastList.last_page;
listData.page = podcastList.current_page;
listData.total_items = podcastList.total;
items.value = podcastList.data
loading.value = false;
}).catch((error) => {
console.log("Error: "+error);
})
}
const editItem = async (item) => {
loading.value = true;
let url = '/podcast'
if (item.id > 0) {
item['_method'] = 'PUT'
url += `/${item.id}/`
}
return await axios.post(
url,
item
).then((response) => {
console.log(response)
loading.value = false
return response.data
}).catch((error) => {
console.log("Error: "+error);
})
}
const deleteItem = (id) => {
return axios.post(`/podcast/${id}`, {
_method: 'DELETE'
}).then((response) => {
getItems(listData)
// items.value = response.status === 200 ? items.value.filter(obj => obj.id !== id) : items
})
}
return { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem }
}

View file

@ -16,14 +16,15 @@ const tabs = {
archive: defineAsyncComponent(() => import('@components/content/Archive.vue')),
playlist: defineAsyncComponent(() => import('@components/content/Playlist.vue')),
blocks: defineAsyncComponent(() => import('@components/content/SmartBlock.vue')),
podcast: defineAsyncComponent(() => import('@components/content/Podcast.vue')),
}
</script>
<template>
<v-col>
<keep-alive>
<!-- <keep-alive>-->
<Component :is="tabs[currentPage]" />
</keep-alive>
<!-- </keep-alive>-->
</v-col>
</template>

View file

@ -24,7 +24,12 @@ const pages = [
{
id: 'blocks',
name: 'Blocchi dinamici',
component: '@components/content/Blocks.vue',
component: '@components/content/SmartBlock.vue',
},
{
id: 'podcast',
name: 'Podcast',
component: '@components/content/Podcast.vue',
}
];
</script>

View file

@ -0,0 +1,19 @@
import {defineStore} from "pinia";
import {basePodcast, type Podcast} from "@models/podcast/podcast.ts";
import type {PodcastEpisode} from "@models/podcast/podcastEpisode.ts";
export const usePodcastStore = defineStore('podcast', {
state: () => ({
currentPodcast: {} as Podcast,
currentPodcastEpisodes: {} as PodcastEpisode,
}),
actions: {
loadPodcast(podcastData: Podcast) {
this.currentPodcast = { ...podcastData };
this.currentPodcastEpisodes = podcastData.episodes;
},
updateField(payload: { key: string; value: any }) {
this.currentPodcast[payload.key] = payload.value;
},
}
})

View file

@ -10,9 +10,8 @@ export const useSmartBlockStore = defineStore('smartblock', {
actions: {
async loadSmartBlock(smartblockData: SmartBlock) {
this.currentSmartBlock = { ...smartblockData }
this.currentSmartBlockCriteria = smartblockData.criteria
},
resetShowDays() {
resetSmartBlock() {
this.currentSmartBlock.showDays = { ...this.baseShowDays };
},
saveSmartBlock() {

View file

@ -1,8 +1,9 @@
<?php
use App\Helpers\Preferences;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\FileController;
use App\Http\Controllers\PodcastController;
use App\Http\Controllers\PodcastEpisodeController;
use App\Http\Controllers\Show\ShowController;
use App\Http\Controllers\Show\ShowDaysController;
use App\Http\Controllers\ShowInstance\ShowInstancesController;
@ -33,6 +34,8 @@ Route::resources([
'file' => FileController::class,
'track_type' => TrackTypeController::class,
'playlist' => PlaylistController::class,
'podcast' => PodcastController::class,
'podcast_episode' => PodcastEpisodeController::class,
'smartblock' => SmartBlockController::class,
'show' => ShowController::class,
'showDays' => ShowDaysController::class,
@ -62,6 +65,7 @@ Route::get('/timezone', [Preferences::class, 'getTimezone']);
*/
Route::get('/test', [FileController::class, 'test']);
Route::get('/testSchedule', [ShowInstancesController::class, 'testSchedule']);
Route::get('/rss_podcast_episodes', [PodcastController::class, 'getEpisodes']);
/**
* CDDB Routes
*/