Merge branch 'podcast' into dev

This commit is contained in:
Marco Cavalli 2025-07-18 11:36:27 +02:00
commit c14c0149ec
18 changed files with 525 additions and 140 deletions

View file

@ -0,0 +1,89 @@
<?php
namespace App\Console\Commands;
use App\Models\Podcast\CeleryTask;
use App\Models\Podcast\PodcastEpisode;
use App\Models\Podcast\ThirdPartyTrackReference;
use Celery\Celery;
use DateTime;
use DateTimeZone;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class CheckPodcastDownload extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:check-podcast-download';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for messages in celeryresults queue to update celery_tasks table for finished tasks';
/**
* Execute the console command.
*/
public function handle()
{
$tasks = CeleryTask::where('status','=','PENDING')->with('aThirdPartyTrackReferences')->get();
if ($tasks->count() > 0) {
foreach ($tasks as $task) {
Log::info($task->task_id);
$queue = 'celeryresults.'.$task['task_id'];
$c = new Celery(
host: config('rabbitmq.host'),
login: config('rabbitmq.user'),
password: config('rabbitmq.password'),
vhost: config('rabbitmq.vhost'),
exchange: 'celeryresults',
binding: $queue,
result_expire: 900000
);
try {
$message = $c->getAsyncResultMessage($task->name, $task->task_id);
if ($message) {
try {
DB::beginTransaction();
$body = json_decode($message['body'], true);
$result = json_decode($body['result'], true);
$podcastEpisode = PodcastEpisode::where('id', '=',$result['episodeid'])->first();
$podcastEpisode->fill([
'file_id' => $result['fileid'],
])->save();
$timezone = new DateTimeZone(getenv('APP_TIMEZONE'));
$thirdPartyTrackReference = ThirdPartyTrackReference::where('foreign_id','=',$podcastEpisode->id)->first();
$thirdPartyTrackReference->fill([
'file_id' => $result['fileid'],
'upload_time' => new DateTime('now', $timezone)
])->save();
$celeryTask = CeleryTask::where('task_id','=',$task->task_id)->where('track_reference','=',$thirdPartyTrackReference->id)->first();
$celeryTask->fill([
'status' => 'SUCCESS'
])->save();
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
}
}
} catch (\Exception $e) {
Log::error('test '.$e);
}
}
}
}
}

View file

@ -15,6 +15,9 @@ class Kernel extends ConsoleKernel
// $schedule->command('inspire')->hourly(); // $schedule->command('inspire')->hourly();
$schedule->command('telescope:prune --hours=48')->daily(); $schedule->command('telescope:prune --hours=48')->daily();
$schedule->command('app:create-show-schedule')->everyMinute(); $schedule->command('app:create-show-schedule')->everyMinute();
$schedule->command('app:check-podcast-download')->everyFiveSeconds();
} }
/** /**

View file

@ -7,8 +7,10 @@ use App\Helpers\LengthFormatter;
use App\Lib\RabbitMQSender; use App\Lib\RabbitMQSender;
use App\Models\File; use App\Models\File;
use App\Models\TrackType; use App\Models\TrackType;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class FileController extends Controller class FileController extends Controller
@ -54,9 +56,19 @@ class FileController extends Controller
$user = Auth::user(); $user = Auth::user();
$apiKey = $request->header('php-auth-user');
//Accept request only from logged-in users //Accept request only from logged-in users
if (!$user) { if (!$user) {
throw new \Exception("You must be logged in"); if ($apiKey != 'some_secret_api_key') {
throw new \Exception("You must be logged in");
}
//ToDo: check how to work in Legacy, getting user in this way is quite horrible
try {
$user = User::where('type','=','P')->orderBy('id','ASC')->first();
} catch (\Exception $e) {
Log::error($e->getMessage());
}
} }
//Mime type list: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types //Mime type list: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
@ -138,6 +150,11 @@ class FileController extends Controller
$fields['track_type_id'] = TrackType::where('code','MUSICA')->first()->id; $fields['track_type_id'] = TrackType::where('code','MUSICA')->first()->id;
} }
//Force BPM to int
if (isset($fields['bpm'])) {
$fields['bpm'] = intval($fields['bpm']);
}
//return json_encode(['req' => $fields,'id' => $id]); //return json_encode(['req' => $fields,'id' => $id]);
$file->fill($fields)->save(); $file->fill($fields)->save();

View file

@ -7,6 +7,7 @@ use App\Models\Podcast\Podcast;
use App\Models\Podcast\PodcastEpisode; use App\Models\Podcast\PodcastEpisode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class PodcastController extends Controller class PodcastController extends Controller
{ {
@ -53,12 +54,32 @@ class PodcastController extends Controller
return $xml->channel; return $xml->channel;
} }
public function getEpisodes(Request $request) public function loadPodcastDataFromXml(Request $request) {
{
$xml = simplexml_load_file($request->url, null, LIBXML_NOCDATA); try {
$xmlArray = (array) $xml->channel; $xml = simplexml_load_file($request->url, null, LIBXML_NOCDATA);
//episodes are stored in `item` array $xmlArray = (array) $xml->channel;
return $xmlArray['item'];
$newArray = [];
foreach ($xmlArray['item'] as $key => $item) {
$item = (array) $item;
$newArray[$key] = $item;
$downloading = PodcastEpisode::where('episode_guid','=',$item['guid'])->first();
if (!is_null($downloading)) {
$newArray[$key]['imported'] = $downloading->third_party_track_reference->celery_task->status;
} else {
$newArray[$key]['imported'] = 0;
}
}
} catch (\Exception $e) {
$xmlArray = $newArray = false;
}
return json_encode([
'podcast' => $xmlArray,
'episodes' => $newArray
]);
} }
protected function save(Request $request) protected function save(Request $request)
@ -67,27 +88,40 @@ class PodcastController extends Controller
$xml = simplexml_load_file($request->url); $xml = simplexml_load_file($request->url);
$itunes = $xml->channel->children('itunes', TRUE); $itunes = $xml->channel->children('itunes', TRUE);
$dbPodcast = Podcast::firstOrNew(['id' => $request->id]); try {
$dbPodcast->fill([ $dbPodcast = Podcast::firstOrNew(['id' => $request->id]);
'url' => $request->url, DB::beginTransaction();
'title' => $xml->channel->title, $dbPodcast->fill([
'creator' => $itunes->author, 'url' => $request->url,
'description' => $itunes->subtitle, 'title' => $request->title,
'language' => $xml->channel->language, 'creator' => $itunes->author,
'copyright' => $xml->channel->copyright, 'description' => $itunes->subtitle,
'link' => $xml->channel->link, 'language' => $xml->channel->language,
'itunes_author'=> $itunes->author, 'copyright' => $xml->channel->copyright,
'itunes_keywords' => '', 'link' => $xml->channel->link,
'itunes_summary' => $itunes->summary, 'itunes_author'=> $itunes->author,
'itunes_subtitle' => $itunes->subtitle, 'itunes_keywords' => '',
'itunes_category' => '', 'itunes_summary' => $itunes->summary,
'itunes_explicit' => $itunes->explicit, 'itunes_subtitle' => $itunes->subtitle,
'owner' => $user->id, 'itunes_category' => '',
])->save(); 'itunes_explicit' => $itunes->explicit,
'owner' => $user->id,
])->save();
return response()->json([ $dbImportedPodcast = ImportedPodcast::firstOrNew(['podcast_id' => $dbPodcast->id]);;
'podcast' => $dbPodcast, $dbImportedPodcast->fill([
'episodes' => $xml->channel->children('item', TRUE) 'auto_ingest' => !isset($request->auto_ingest) ? true : false,
]); 'podcast_id' => $dbPodcast->id
])->save();
DB::commit();
return response()->json([
'podcast' => $dbPodcast,
'episodes' => $xml->channel->children('item', TRUE)
]);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
} }
} }

View file

@ -2,12 +2,16 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Podcast\CeleryTask;
use App\Models\Podcast\Podcast; use App\Models\Podcast\Podcast;
use App\Models\Podcast\PodcastEpisode; use App\Models\Podcast\PodcastEpisode;
use App\Models\Podcast\ThirdPartyTrackReference;
use Celery\Celery;
use DateTime;
use DateTimeZone;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Celery\Celery;
class PodcastEpisodeController extends Controller class PodcastEpisodeController extends Controller
{ {
@ -32,22 +36,6 @@ class PodcastEpisodeController extends Controller
if (!$user->id) { if (!$user->id) {
throw new \Exception('You must be logged in'); 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',
'episode_url' => 'required',
'episode_title' => 'required',
]);
try {
$podcast = Podcast::findOrFail($request->podcast_id);
$podcastEpisode = new PodcastEpisode(); $podcastEpisode = new PodcastEpisode();
$podcastEpisode->fill([ $podcastEpisode->fill([
@ -59,6 +47,64 @@ class PodcastEpisodeController extends Controller
'episode_description' => htmlentities($request->episode['description']), 'episode_description' => htmlentities($request->episode['description']),
])->save(); ])->save();
$brokerTask = $this->downloadPodcastEpisode($request, $podcastEpisode);
$ref = new ThirdPartyTrackReference();
$ref->fill([
'service' => 'podcast',
'foreign_id' => $podcastEpisode->id,
'file_id' => null
])->save();
$task = new CeleryTask();
$timezone = new DateTimeZone(getenv('APP_TIMEZONE'));
$task->fill([
'task_id' => $brokerTask->task_id,
'track_reference' => $ref->id,
'name' => 'podcast-download',
'dispatch_time' => new DateTime('now', $timezone),
'status' => 'PENDING'
])->save();
return $podcastEpisode->toJson();
} catch (\Exception $exception) {
return response($exception->getMessage(), 500);
}
}
public function checkPodcastEpisodeDownload($id) {
$episode = PodcastEpisode::where('id',$id)->firstOrFail();
return $episode->third_party_track_reference->celery_task->status;
}
public function savePodcastEpisode(Request $request) {
if(isset($request->file)) {
try {
$file = (new FileController())->store($request);
} catch (\Exception $e) {
return response($e->getMessage(), 500);
}
if ($file) {
return $file;
}
return json_encode([
'id' => 0
]);
}
}
private function downloadPodcastEpisode(Request $request, $podcastEpisode) {
$request->validate([
'podcast_id' => 'required',
'episode_url' => 'required',
'episode_title' => 'required',
]);
try {
$podcast = Podcast::findOrFail($request->podcast_id);
$conn = new Celery( $conn = new Celery(
config('rabbitmq.host'), config('rabbitmq.host'),
config('rabbitmq.user'), config('rabbitmq.user'),
@ -79,24 +125,9 @@ class PodcastEpisodeController extends Controller
'override_album' => 'false' //ToDo connect $album_override from imported_podcast, 'override_album' => 'false' //ToDo connect $album_override from imported_podcast,
]; ];
$result = $conn->PostTask('podcast-download', $data, true, 'podcast'); $taskId = $conn->PostTask('podcast-download', $data, true, 'podcast');
return $taskId;
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) { } catch (\Exception $exception) {
Log::error($exception->getMessage()); Log::error($exception->getMessage());
die($exception->getMessage()); die($exception->getMessage());

View file

@ -0,0 +1,27 @@
<?php
namespace App\Models\Podcast;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CeleryTask extends Model
{
use HasFactory;
protected $table = 'celery_tasks';
public $timestamps = false;
protected $fillable = [
'task_id',
'track_reference',
'name',
'dispatch_time',
'status'
];
public function aThirdPartyTrackReferences() {
return $this->hasOne(ThirdPartyTrackReference::class, 'id', 'track_reference');
}
}

View file

@ -29,4 +29,8 @@ class PodcastEpisode extends Model
public function podcast() { public function podcast() {
return $this->belongsTo(Podcast::class); return $this->belongsTo(Podcast::class);
} }
public function third_party_track_reference() {
return $this->hasOne(ThirdPartyTrackReference::class, 'foreign_id', 'id');
}
} }

View file

@ -0,0 +1,36 @@
<?php
namespace App\Models\Podcast;
use App\Models\File;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ThirdPartyTrackReference extends Model
{
use HasFactory;
protected $table = 'third_party_track_references';
public $timestamps = false;
protected $fillable = [
'service',
'foreign_id',
'file_id',
'upload_time',
'status'
];
public function file() {
return $this->hasOne(File::class);
}
public function podcast_episode() {
return $this->belongsTo(PodcastEpisode::class, 'foreign_id');
}
public function celery_task() {
return $this->hasOne(CeleryTask::class, 'track_reference');
}
}

View file

@ -18,8 +18,15 @@
"mxl/laravel-job": "^1.6", "mxl/laravel-job": "^1.6",
"php-amqplib/php-amqplib": "^3.7", "php-amqplib/php-amqplib": "^3.7",
"spatie/laravel-permission": "^6.13", "spatie/laravel-permission": "^6.13",
"xenos/musicbrainz": "^1.0" "xenos/musicbrainz": "^1.0",
"libretime/celery-php": "dev-main"
}, },
"repositories": [
{
"type": "vcs",
"url": "https://github.com/libretime/celery-php.git"
}
],
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/breeze": "*", "laravel/breeze": "*",

View file

@ -14,6 +14,7 @@ podcastStore.loadPodcast(basePodcast());
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = podcast_page(); const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = podcast_page();
const url = ref(''); const url = ref('');
const itemEdited = ref(basePodcast()); const itemEdited = ref(basePodcast());
const episodes = ref([]);
const bulk = ref(false); const bulk = ref(false);
const dialog = reactive({ const dialog = reactive({
open: false, open: false,
@ -21,6 +22,7 @@ const dialog = reactive({
title: '', title: '',
text: '' text: ''
}); });
const dialogLoading = ref(false);
const openDialog = (type, title = '', text = '', bulk = false) => { const openDialog = (type, title = '', text = '', bulk = false) => {
dialog.open = true dialog.open = true
@ -47,12 +49,29 @@ const confirm = (confirm, bulk) => {
} }
} }
const confirmAdd = (confirm) => { const confirmAdd = async (confirm) => {
if (confirm) { if (confirm) {
podcastStore.updateField({key: 'url', value: url}); dialogLoading.value = true;
console.log(podcastStore); await axios.get('/rss_podcast_load', {
params: {
url: url.value,
}
}).then(res => {
if (res.data.podcast) {
podcastStore.updateField({key: 'title', value: res.data.podcast.title});
podcastStore.updateField({key: 'url', value: url});
podcastStore.currentPodcastEpisodes = res.data.episodes;
}
closeDialog();
dialogLoading.value = false;
//episodes.value = res.data.episodes;
openDialog(
'info',
'Attenzione',
'L\'URL inserito non è un feed RSS valido'
);
})
} }
closeDialog()
} }
const edit = (item) => { const edit = (item) => {
@ -60,42 +79,46 @@ const edit = (item) => {
} }
const cancel = (item) => { const cancel = (item) => {
bulk.value = Array.isArray(item) bulk.value = Array.isArray(item);
itemEdited.value = item itemEdited.value = item;
openDialog( openDialog(
'delete', 'delete',
'Cancella', 'Cancella',
bulk.value ? 'Vuoi cancellare i podcast selezionati?' : 'Vuoi cancellare il podcast selezionato?' bulk.value ? 'Vuoi cancellare i podcast selezionati?' : 'Vuoi cancellare il podcast selezionato?'
) );
} }
const confirmDelete = (confirm, bulk) => { const confirmDelete = (confirm, bulk) => {
if (confirm) { if (confirm) {
if (!bulk) { if (!bulk) {
deleteItem(itemEdited.value.id) deleteItem(itemEdited.value.id);
} else { } else {
itemEdited.value.forEach(el => { itemEdited.value.forEach(el => {
deleteItem(el.id) deleteItem(el.id);
}) })
} }
} }
closeDialog() closeDialog();
} }
const closeDialog = () => { const closeDialog = () => {
dialog.open = false dialog.open = false;
itemEdited.value = basePodcast(); itemEdited.value = basePodcast();
} }
const updateSearch = (text) => { const updateSearch = (text) => {
search.value = text search.value = text;
}
const resetItemEdited = () => {
podcastStore.currentPodcast = basePodcast();
} }
</script> </script>
<template> <template>
<PodcastEditor <PodcastEditor
v-if="podcastStore.currentPodcast.url != '' && podcastStore.currentPodcast.url != null" v-if="podcastStore.currentPodcast.url != '' && podcastStore.currentPodcast.url != null"
@go-back="podcastStore.currentPodcast = basePodcast()" @go-back="resetItemEdited"
/> />
<Table <Table
v-else v-else
@ -126,6 +149,8 @@ const updateSearch = (text) => {
:bulk="bulk" :bulk="bulk"
@confirm="confirm" @confirm="confirm"
@after-leave="closeDialog" @after-leave="closeDialog"
:loading="dialogLoading"
:hide_confirm="dialog.type === 'info' ? true : false"
> >
<VTextField <VTextField
label="Feed RSS" label="Feed RSS"

View file

@ -3,57 +3,111 @@ import {useAuthStore} from "@/stores/auth.store.ts";
import {usePodcastStore} from "@/stores/podcast.store.ts"; import {usePodcastStore} from "@/stores/podcast.store.ts";
import {podcast} from "@models/podcast/podcast.ts"; import {podcast} from "@models/podcast/podcast.ts";
import {podcast_episode_page} from "@/composables/content/podcastEpisode_page.ts"; import {podcast_episode_page} from "@/composables/content/podcastEpisode_page.ts";
import {ref, watch} from "vue"; import {onBeforeMount, reactive, ref, watch} from "vue";
import axios from "axios"; import axios from "axios";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import {podcast_page} from "@/composables/content/podcast_page.ts";
const auth = useAuthStore(); const auth = useAuthStore();
const emit = defineEmits([ const emit = defineEmits([
'saveItem' 'saveItem',
'goBack'
]) ])
const podcastStore = usePodcastStore(); const podcastStore = usePodcastStore();
const item = podcastStore.currentPodcast; const item = podcastStore.currentPodcast;
const { items, headers, loading, downloadEpisode, getItems } = podcast_episode_page(item.url);
const podcast_id = ref(item.id);
console.log(item) console.log(item)
const podcastFields = podcast(item); const podcastFields = podcast(item);
const { items, headers, loading, downloadingEpisode, downloadEpisode } = podcast_episode_page(item.id, item.url); console.log(podcastFields())
const { editItem } = podcast_page();
const episodes = ref([]); const episodes = ref([]);
const disabledSaveButton = ref(false)
const dialog = reactive({
open: false,
type: '',
title: '',
text: '',
item: null
})
const openDialog = (type, title = '', text = '', item = null) => {
dialog.open = true
dialog.type = type
dialog.title = title
dialog.text = text
dialog.item = item
}
const closeDialog = () => {
dialog.open = false
}
const checkError = (field, model) => { const checkError = (field, model) => {
if (field.required) { if (field.required) {
// const error = field.required && (model === '' || model === null) const error = field.required && (model === '' || model === null)
// // disabledSaveButton.value = error disabledSaveButton.value = error
// return error return error
} }
return false return false
} }
watch(items, (newItems, oldItems) => { const checkDownload = (item) => {
console.log(newItems, oldItems) if (podcast_id.value > 0) {
if (item.id > 0) { // console.log(item);
axios.get('/podcast_episode', { downloadEpisode(podcast_id.value, item);
params: { } else {
podcast_id: item.id openDialog(
} 'save',
}).then((response) => { 'Salvataggio necessario',
newItems.forEach((element) => { 'Per procedere con il download dell\'episodio è necessario salvare il podcast. Confermi?',
const episode = response.data.filter(imp => { item
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) }
const confirmSave = (confirm, bulk, row) => {
console.log(confirm, row)
if (confirm) {
save(row);
}
closeDialog();
}
const save = (row) => {
console.log(row)
// const errors
editItem(item).then(res => {
podcastStore.loadPodcast(res.podcast);
podcast_id.value = res.podcast.id;
console.log(podcast_id.value);
//Check if row is effectively a podcast episode object using his `title` property
if (row.title) {
downloadEpisode(podcast_id.value, row);
}
});
}
watch(items, (newItems, oldItems) => {
episodes.value = newItems; episodes.value = newItems;
}, {deep: true}); }, {deep: true});
setInterval(() => {
getItems(false, item.id);
}, 5000)
onBeforeMount(() => {
getItems().then(title => podcastStore.updateField({'title': title}));
});
</script> </script>
<template> <template>
@ -66,7 +120,6 @@ watch(items, (newItems, oldItems) => {
:value="field.value ? field.value : field.type == 'checkbox' ? true : null" :value="field.value ? field.value : field.type == 'checkbox' ? true : null"
:disabled="field.disabled" :disabled="field.disabled"
@update:modelValue="checkError(field, item[key])" @update:modelValue="checkError(field, item[key])"
@update-property="updateProperty"
:error="checkError(field, item[key])" :error="checkError(field, item[key])"
rows="2" rows="2"
:items="field.items" :items="field.items"
@ -100,26 +153,25 @@ watch(items, (newItems, oldItems) => {
{{ item.short_description }} {{ item.short_description }}
</template> </template>
<template v-slot:item.imported="{ item }"> <template v-slot:item.imported="{ item }">
{{item.imported}}
<v-icon <v-icon
class="me-2 spinning" class="me-2 spinning"
size="small" size="small"
v-if="item.imported === null" v-if="item.imported === 'PENDING'"
> >
mdi-loading mdi-loading
</v-icon> </v-icon>
<v-icon <v-icon
class="me-2" class="me-2"
size="small" size="small"
v-else-if="item.imported > 0" v-else-if="item.imported === 'SUCCESS'"
> >
mdi-check-outline mdi-check-outline
</v-icon> </v-icon>
<v-icon <v-icon
class="me-2 text-center" class="me-2 text-center"
size="small" size="small"
v-else v-else-if="item.imported === 0"
@click="downloadEpisode(item)" @click="checkDownload(item)"
> >
mdi-download-box mdi-download-box
</v-icon> </v-icon>
@ -127,6 +179,17 @@ watch(items, (newItems, oldItems) => {
</VDataTable> </VDataTable>
</v-col> </v-col>
</v-row> </v-row>
<v-dialog v-model="dialog.open">
<ConfirmDelete
v-if="dialog.type === 'save'"
:title="dialog.title"
:text="dialog.text"
:bulk="false"
:item="dialog.item"
@confirm="confirmSave"
@after-leave="closeDialog"
/>
</v-dialog>
</template> </template>
<style scoped> <style scoped>

View file

@ -2,10 +2,30 @@
const props = defineProps({ const props = defineProps({
title: String, title: String,
text: String, text: String,
bulk: Boolean confirm_text: {
type: String,
required: false
},
cancel_text: {
type: String,
required: false
},
item: {
type: Object,
required: false
},
bulk: Boolean,
loading: {
type: Boolean,
required: false
},
hide_confirm: {
type: Boolean,
required: false
}
}) })
console.log(props.bulk) console.log(props.loading)
</script> </script>
<template> <template>
@ -20,7 +40,9 @@ console.log(props.bulk)
<v-card-actions> <v-card-actions>
<v-btn <v-btn
color="primary" color="primary"
@click="$emit('confirm',true, props.bulk)" @click="$emit('confirm',true, props.bulk, item)"
:loading="loading"
v-if="hide_confirm != true"
>Conferma</v-btn> >Conferma</v-btn>
<v-btn <v-btn
color="accent" color="accent"

View file

@ -70,7 +70,7 @@ export const deletePodcast = async (podcastIds: Number[]) => {
export function podcast(item) { export function podcast(item) {
const visibleFields = { const visibleFields = {
name: { title: {
title: 'Nome del podcast', title: 'Nome del podcast',
required: true, required: true,
disabled: false disabled: false

View file

@ -2,8 +2,9 @@ import {reactive, ref} from "vue";
import axios, {type AxiosResponse} from "axios"; import axios, {type AxiosResponse} from "axios";
import {type PodcastEpisode, PodcastEpisodeTableHeader} from "@models/podcast/podcastEpisode.ts"; import {type PodcastEpisode, PodcastEpisodeTableHeader} from "@models/podcast/podcastEpisode.ts";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {usePodcastStore} from "@/stores/podcast.store.ts";
export function podcast_episode_page(podcast_id: Number, url: String) { export function podcast_episode_page(url: String) {
const items = ref([]); const items = ref([]);
const loading = ref(false); const loading = ref(false);
const listData = reactive({ const listData = reactive({
@ -35,40 +36,44 @@ export function podcast_episode_page(podcast_id: Number, url: String) {
}); });
} }
const getItems = async () => { const getItems = async (load, podcast_id = 0) => {
loading.value = true; if (load) {
console.log(url) loading.value = true;
return await axios.get(`/rss_podcast_episodes`, { }
return await axios.get(`/rss_podcast_load`, {
params: { params: {
podcast_id: podcast_id,
url: url url: url
} }
}).then( async (podcastEpisodesList: AxiosResponse) => { }).then( async (podcastEpisodesList: AxiosResponse) => {
// console.log(podcastEpisodesList, podcast_id); const episodes = podcastEpisodesList.data.episodes;
const episodes = podcastEpisodesList.data; items.value = episodes.map(element => {
items.value = await episodes.map(element => { //element.imported = -1;
element.imported = -1;
element.short_description = ''; element.short_description = '';
const arr = element.description.split(' '); if (typeof element.description === "string") {
for (let j = 0; j < 20; j++) { const arr = element.description.split(' ');
element.short_description += arr[j]; const limit = arr.length >= 20 ? 20 : arr.length;
element.short_description += (j === 19) ? '...' : ' '; for (let j = 0; j < limit; 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]);
});
} }
element.short_description = (element.short_description + '').replace(/&#\d+;/gm, function(s) {
return String.fromCharCode(s.match(/\d+/gm)[0]);
});
return element; return element;
}); });
loading.value = false;
if (load) {
loading.value = false;
}
return podcastEpisodesList.data.podcast.title;
}).catch((error: Error) => { }).catch((error: Error) => {
console.log("Error: " + error); console.log("Error: " + error);
}); });
} }
const downloadEpisode = async (item) => { const downloadEpisode = async (podcast_id, item) => {
console.log(item.enclosure["@attributes"].url)
return await axios.post('/podcast_episode', { return await axios.post('/podcast_episode', {
podcast_id: podcast_id, podcast_id: podcast_id,
episode: item, episode: item,
@ -80,7 +85,21 @@ export function podcast_episode_page(podcast_id: Number, url: String) {
console.log("Error: "+error); console.log("Error: "+error);
}); });
} }
getItems();
return { items, listData, headers, loading, downloadingEpisode, downloadEpisode } const checkDownloadEpisode = async (episode_id) => {
return await axios.get(`/check_podcast_episode_download/${episode_id}`, {
params: {
}
}).then( (response) => {
if (response.data === 'SUCCESS') {
return true;
}
checkDownloadEpisode(episode_id);
})
}
getItems(true);
return { items, listData, headers, loading, downloadingEpisode, getItems, downloadEpisode }
} }

View file

@ -45,9 +45,9 @@ watch(currentPage, (newVal) => {
<template> <template>
<v-col> <v-col>
<!-- <keep-alive>--> <!-- <keep-alive>-->
<Component :is="tabs[currentPage]" /> <Component :is="tabs[currentPage]" />
<!-- </keep-alive>--> <!-- </keep-alive>-->
</v-col> </v-col>
</template> </template>

View file

@ -5,7 +5,7 @@ import type {PodcastEpisode} from "@models/podcast/podcastEpisode.ts";
export const usePodcastStore = defineStore('podcast', { export const usePodcastStore = defineStore('podcast', {
state: () => ({ state: () => ({
currentPodcast: {} as Podcast, currentPodcast: {} as Podcast,
currentPodcastEpisodes: {} as PodcastEpisode, currentPodcastEpisodes: {} as PodcastEpisode[],
}), }),
actions: { actions: {
loadPodcast(podcastData: Podcast) { loadPodcast(podcastData: Podcast) {

View file

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\FileController; use App\Http\Controllers\FileController;
use App\Http\Controllers\PodcastEpisodeController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\TestControllerXdebug; use App\Http\Controllers\TestControllerXdebug;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -16,4 +17,5 @@ use Illuminate\Support\Facades\Route;
| |
*/ */
Route::middleware('parsejson')->put('/media/{id}', [FileController::class, 'update'])->name('media.update'); Route::middleware('parsejson')->put('/media/{id}', [FileController::class, 'update'])->name('media.update');
Route::post('/media', [PodcastEpisodeController::class, 'savePodcastEpisode'])->name('media.podcast');

View file

@ -57,6 +57,12 @@ Route::get('/file/check_upload_status/{id}', [FileController::class, 'checkUploa
*/ */
Route::post('/smartblock/{id}/tracks', [SmartBlockController::class, 'getTrackList']); Route::post('/smartblock/{id}/tracks', [SmartBlockController::class, 'getTrackList']);
/**
* Podcast and podcast episodes custom routes
*/
Route::get('/rss_podcast_load', [PodcastController::class, 'loadPodcastDataFromXml']);
Route::get('/check_podcast_episode_download/{id}', [PodcastEpisodeController::class, 'checkPodcastEpisodeDownload']);
/** /**
* Preferences custom routes * Preferences custom routes
*/ */