Compare commits

...
Sign in to create a new pull request.

33 commits
main ... dev

Author SHA1 Message Date
e2ebd8fbfa Delete composer.lock 2025-07-18 15:00:19 +02:00
38a0e27d56 feat: composer lock 2025-07-18 14:44:59 +02:00
d451b42c29 Merge branch 'userProfile' into dev
# Conflicts:
#	resources/js/layouts/partials/Content.vue
2025-07-18 14:11:47 +02:00
13a3de9709 feat: userProfile 2025-07-18 14:10:37 +02:00
c14c0149ec Merge branch 'podcast' into dev 2025-07-18 11:36:27 +02:00
4818828817 feat (fe podcast): finishing podcast implementation 2025-07-18 11:14:11 +02:00
8f24453eff feat: userProfile 2025-07-16 11:36:10 +02:00
4e8664ee31 Merge branch 'spot' into dev 2025-07-15 11:29:43 +02:00
0b80530aef feat: spots 2025-07-15 11:18:32 +02:00
1abc294930 feat (fe file): added track type support on upload 2025-07-15 10:26:00 +02:00
a4e1a3328f feat: spots 2025-07-11 15:03:59 +02:00
222b1d2d7b fix (be podcast): trying to retreive file and podcast data to update third party track references and celery tasks tables 2025-07-10 18:06:52 +02:00
08010fbb61 feat(BE): get show spot 2025-07-04 11:40:13 +02:00
2cac6b6a90 feat(FE): get show spot 2025-07-04 11:40:07 +02:00
df5bc78f2e feat(tsconfig): store path 2025-07-04 00:16:25 +02:00
ba56bccc35 feat(BE): spot 2025-07-04 00:16:09 +02:00
5af0b32634 feat(FE): spot 2025-07-04 00:15:58 +02:00
7eadbe16f5 feat(DB): spots migrations 2025-07-04 00:15:18 +02:00
bcf9b5c5a7 feat (be podcast): added third_party_track_references and celery_tasks table support 2025-07-03 17:47:58 +02:00
2dae6e07e7 feat (be podcast): fe connected to be, trying to download podcast episode, save it in cc_files and updating podcast_episodes 2025-07-03 15:39:56 +02:00
0a8cc600fc feat(routes): webstream 2025-07-02 13:14:49 +02:00
bfa0e365f1 feat(BE): webstream tests 2025-07-02 13:14:40 +02:00
ebe6b72efe feat(BE): webstream 2025-07-02 13:14:27 +02:00
42dcde7fc9 feat(FE): webstreams and menu 2025-07-02 13:13:49 +02:00
438220a664 feat(FE): webstreams and menu 2025-07-02 13:13:43 +02:00
c6b07fa803 fix(routes): !tmp! status updates 2025-07-02 13:13:14 +02:00
7f3b48cf89 fix(DB): Migrations check for existing tables 2025-07-02 13:12:51 +02:00
2fcde13ef5 feat (be podcast): added libretime celery php library to composer.json 2025-06-30 16:30:45 +02:00
f1b467e4f3 fix (fe playlist): fixed tracks error on new playlist 2025-06-30 14:57:31 +02:00
8e3103b9db feat(podcast): created ui, working on logics 2025-06-30 14:55:26 +02:00
15d256303f feat(podcast): created Models and Controllers 2025-06-30 14:55:26 +02:00
f042bf2140 feat(podcast): created ui, working on logics 2025-06-27 16:03:25 +02:00
baeb70dd46 feat(podcast): created Models and Controllers 2025-06-17 14:53:55 +02:00
118 changed files with 4439 additions and 970 deletions

View file

@ -23,7 +23,6 @@ class LoginUser
try {
$user = User::where('login', $userInfo['username'])->first();
if ($user) {
$user['role'] = ($user->getRoleNames())->first();
$password = $user->getAuthPassword();
if (strlen($password) === 32 && ctype_xdigit($password)) {
if (hash_equals($password, md5($userInfo['password']))) {

View file

@ -26,7 +26,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
'pass' => Hash::make($input['password']),
])->save();
}
}

View file

@ -13,43 +13,48 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
* @param array<string, mixed> $input
*/
public function update(User $user, array $input): void
{
// Use PHP's built-in list of timezones for robust validation
$timezones = timezone_identifiers_list();
$rules = [
'login' => ['required', 'string', 'max:255', Rule::unique('cc_subjs')->ignore($user->id)],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('cc_subjs')->ignore($user->id),
],
'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('cc_subjs')->ignore($user->id)],
'first_name' => ['nullable', 'string', 'max:255'],
'last_name' => ['nullable', 'string', 'max:255'],
'cell_phone' => ['nullable', 'string', 'max:25'],
'timezone' => ['nullable', 'string', Rule::in($timezones)],
];
// Only add the 'type' validation rule if the user has the permission to change roles
if (auth()->user()->hasPermissionTo('user.changeRole')) {
if (isset($rules['type']) && auth()->user()->hasPermissionTo('users.changeRole')) {
$rules['type'] = ['required', 'string', 'max:6', Rule::in(['admin', 'editor', 'dj'])];
}
Validator::make($input, $rules)->validateWithBag('updateProfileInformation');
$data = [
'login' => $input['login'],
'email' => $input['email'],
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'cell_phone' => $input['cell_phone'],
];
if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$data = [
'login' => $input['login'],
'email' => $input['email'],
];
// Only update 'type' if the user has the permission
if (auth()->user()->hasPermissionTo('user.changeRole')) {
if (isset($rules['type']) && auth()->user()->hasPermissionTo('users.changeRole')) {
$data['type'] = $input['type'];
}
$user->forceFill($data)->save();
}
// The timezone is handled by the mutator in the User model
$user->timezone = $input['timezone'];
}
/**
@ -62,16 +67,22 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
$data = [
'login' => $input['login'],
'email' => $input['email'],
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'cell_phone' => $input['cell_phone'],
'email_verified_at' => null,
];
// Only update 'type' if the user has the permission
if (auth()->user()->hasPermissionTo('user.changeRole')) {
// Corrected permission name to be consistent
if (auth()->user()->hasPermissionTo('users.changeRole')) {
$data['type'] = $input['type'];
}
$user->forceFill($data)->save();
// The timezone is handled by the mutator in the User model
$user->timezone = $input['timezone'];
$user->sendEmailVerificationNotification();
}
}
}

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('telescope:prune --hours=48')->daily();
$schedule->command('app:create-show-schedule')->everyMinute();
$schedule->command('app:check-podcast-download')->everyFiveSeconds();
}
/**

View file

@ -5,7 +5,7 @@ namespace App\Enums;
enum PlaylistContentType: int
{
case audioclip = 0;
case stream = 1;
case webstream = 1;
case block = 2;
public static function fromName(string $name){

View file

@ -3,6 +3,7 @@
namespace App\Filters;
use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\IsFilter;
use App\Filters\FiltersType\LikeFilter;
class FileFilters

View file

@ -0,0 +1,12 @@
<?php
namespace App\Filters\FiltersType;
class GreaterEqualFilter
{
function __invoke($query, $whereToSearch, $textToSearch) {
return $query->where(function($query) use ($whereToSearch, $textToSearch) {
$query->where($whereToSearch, '>=', $textToSearch);
});
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace App\Filters\FiltersType;
class SmallerEqualFiler
{
function __invoke($query, $whereToSearch, $textToSearch) {
return $query->where(function($query) use ($whereToSearch, $textToSearch) {
$query->where($whereToSearch, '<=', $textToSearch);
});
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Filters\FiltersType;
use Log;
class SpotFilter
{
function __invoke($query, $spotTableName, $idColumnName) {
try {
return $query->whereIn('id', $spotTableName::select($idColumnName));
} catch (\Exception $e) {
Log::error($e);
}
}
}

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

@ -0,0 +1,42 @@
<?php
namespace App\Filters;
use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\GreaterEqualFilter;
use App\Filters\FiltersType\IsFilter;
use App\Filters\FiltersType\LikeFilter;
use App\Filters\FiltersType\SmallerEqualFiler;
use Illuminate\Http\Request;
class ScheduleFilters
{
protected $filters = [
'ends' => SmallerEqualFiler::class,
'starts' => GreaterEqualFilter::class,
];
public function apply($query, Request $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(Request $filters)
{
return $filters->only(array_keys($this->filters));
}
}

View file

@ -2,44 +2,38 @@
namespace App\Filters\Show;
use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\LikeFilter;
use App\Models\Spot\SpotPlaylist;
class ShowFilters
{
protected $filters = [
'name' => LikeFilter::class,
'dj' => LikeFilter::class,
'genre' => LikeFilter::class,
'description' => LikeFilter::class,
'color' => LikeFilter::class,
'background_color' => LikeFilter::class,
'live_stream_using_airtime_auth' => LikeFilter::class,
'live_stream_using_custom_auth' => LikeFilter::class,
'live_stream_user' => LikeFilter::class,
'live_stream_pass' => LikeFilter::class,
'linked' => LikeFilter::class,
'is_linkable' => LikeFilter::class,
'image_path' => LikeFilter::class,
'has_autoplaylist' => LikeFilter::class,
'autoplaylist_id' => LikeFilter::class,
'autoplaylist_repeat' => LikeFilter::class,
'all' => AllFilter::class,
];
public function apply($query)
public function apply($query, $filters)
{
foreach ($this->receivedFilters() as $name => $value) {
if ($name != 'per_page') {
$filterInstance = new $this->filters[$name];
$query = $filterInstance($query, $name, $value);
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()
public function receivedFilters($filters)
{
return request()->only(array_keys($this->filters));
return $filters->only(array_keys($this->filters));
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Filters\Webstream;
use App\Filters\FiltersType\LikeFilter;
class WebstreamFilters
{
protected $filters = [
'name' => LikeFilter::class,
'description' => LikeFilter::class,
'url' => LikeFilter::class,
'mime' => LikeFilter::class,
'per_page' => null,
];
public function apply($query)
{
foreach ($this->receivedFilters() as $name => $value) {
if ($name != 'per_page') {
$filterInstance = new $this->filters[$name];
$query = $filterInstance($query, $name, $value);
}
}
return $query;
}
public function receivedFilters()
{
return request()->only(array_keys($this->filters));
}
}

View file

@ -7,8 +7,10 @@ use App\Helpers\LengthFormatter;
use App\Lib\RabbitMQSender;
use App\Models\File;
use App\Models\TrackType;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class FileController extends Controller
@ -27,11 +29,17 @@ class FileController extends Controller
$pagination = $request->per_page;
}
return File::searchFilter($request)
->where('import_status', '=', 0)
->with('track_type')
->with('owner')
->orderBy('artist_name')
$files = File::searchFilter($request)
->where('import_status', '=', 0)
->with('track_type')
->with('owner');
if($request->track_type == 'spot'){
$trackTypes = TrackType::where('code', '=', 'SPOT')->orWhere('parent_id', '=', '3')->pluck('id');
$files = $files->whereIn('track_type_id', $trackTypes->toArray());
}
return $files->orderBy('artist_name')
->paginate($pagination)
->toJson();
}
@ -48,9 +56,19 @@ class FileController extends Controller
$user = Auth::user();
$apiKey = $request->header('php-auth-user');
//Accept request only from logged-in users
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
@ -132,6 +150,11 @@ class FileController extends Controller
$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]);
$file->fill($fields)->save();

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ImportedPodcastController extends Controller
{
//
}

View file

@ -8,10 +8,13 @@ use App\Models\File;
use App\Models\Playlist;
use App\Models\PlaylistContent;
use App\Models\SmartBlock;
use App\Models\Spot\SpotPlaylist;
use App\Models\Webstream;
use DateInterval;
use DateTime;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PlaylistController extends Controller
{
@ -21,17 +24,37 @@ class PlaylistController extends Controller
* @return string
*/
public function index(Request $request) {
if (!isset($request->per_page) || is_null($request)) {
$pagination = 5;
} else {
$pagination = $request->per_page;
}
try {
if ( ! isset($request->per_page) || is_null($request)) {
$pagination = 5;
} else {
$pagination = $request->per_page;
}
return Playlist::searchFilter($request)
->with(['creator', 'tracks.file', 'tracks.block', 'tracks.block.criteria', 'tracks.block.creator'])
->orderBy('name')
->paginate($pagination)
->toJson();
$playlists = Playlist::searchFilter($request)
->with(
[
'creator',
'tracks.file',
'tracks.block',
'tracks.block.criteria',
'tracks.block.creator'
]
);
if($request->has('playlistType')) {
$playlistType = $request->get('playlistType');
$playlists = ($playlistType == 'show')
? $playlists->doesntHave('spotPlaylist')
: $playlists->has('spotPlaylist');
}
return $playlists->orderBy('name')
->paginate($pagination)
->toJson();
} catch (\Exception $e) {
Log::error($e);
}
}
/**
@ -41,60 +64,67 @@ class PlaylistController extends Controller
* @throws \DateMalformedIntervalStringException
*/
public function store(Request $request) {
$user = Auth::user();
$request->validate([
'name' => 'required',
'tracks' => 'required|array'
]);
$length = 0;
$dbPlaylist = Playlist::create([
'name' => $request->get('name'),
'creator_id' => $user->id,
'description' => $request->description,
]);
// dd($request->tracks);
foreach($request->tracks as $key => $file) {
if (!isset($file['id'])) {
$file = $file['db_element'];
}
switch ($file['type']) {
case 'audioclip':
$model = File::whereId($file['id'])->first();
break;
case 'block':
$model = SmartBlock::whereId($file['id'])->first();
break;
case 'stream':
//Todo: $model = Stream::whereId($file['id'])->first();
break;
}
$modelTime = new LengthFormatter($model->length);
$modelCuein = new LengthFormatter($model->cuein);
$modelCueout = new LengthFormatter($model->cueout);
$length = $length + $modelTime->toSeconds();
PlaylistContent::create([
'playlist_id' => $dbPlaylist->id,
'file_id' => ($file['type'] === 'audioclip') ? $file['id'] : null,
'block_id' => ($file['type'] === 'block') ? $file['id'] : null,
'stream_id' => ($file['type'] === 'stream') ? $file['id'] : null,
'type' => PlaylistContentType::fromName($file['type']),
'position' => $key,
'trackoffset' => 0, //ToDo: understand this field
'cliplength' => $modelTime->toSeconds(),
'cuein' => $modelCuein->toSeconds() ?? 0,
'cueout' => $modelCueout->toSeconds() ?? $modelTime->toSeconds(),
'fadein' => '00:00:00',
'fadeout' => '00:00:00',
try{
$user = Auth::user();
$request->validate([
'name' => 'required',
'tracks' => 'required|array'
]);
$length = 0;
$dbPlaylist = Playlist::create([
'name' => $request->get('name'),
'creator_id' => $user->id,
'description' => $request->description,
]);
if($request['playlist_type'] === 'spot') {
$dbPlaylist->spotPlaylist()->create();
}
// dd($request->tracks);
foreach($request->tracks as $key => $file) {
if (!isset($file['id'])) {
$file = $file['db_element'];
}
switch ($file['type']) {
case 'audioclip':
$model = File::whereId($file['id'])->first();
break;
case 'block':
$model = SmartBlock::whereId($file['id'])->first();
break;
case 'webstream':
$model = Webstream::whereId($file['id'])->first();
break;
}
$modelTime = new LengthFormatter($model->length);
$modelCuein = new LengthFormatter($model->cuein);
$modelCueout = new LengthFormatter($model->cueout);
$length = $length + $modelTime->toSeconds();
PlaylistContent::create([
'playlist_id' => $dbPlaylist->id,
'file_id' => ($file['type'] === 'audioclip') ? $file['id'] : null,
'block_id' => ($file['type'] === 'block') ? $file['id'] : null,
'stream_id' => ($file['type'] === 'webstream') ? $file['id'] : null,
'type' => PlaylistContentType::fromName($file['type']),
'position' => $key,
'trackoffset' => 0, //ToDo: understand this field
'cliplength' => $modelTime->toSeconds(),
'cuein' => $modelCuein->toSeconds() ?? 0,
'cueout' => $modelCueout->toSeconds() ?? $modelTime->toSeconds(),
'fadein' => '00:00:00',
'fadeout' => '00:00:00',
]);
}
$dbPlaylist->update(['length' => $length]);
return $dbPlaylist->with('tracks')->get()->toJson();
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
$dbPlaylist->update(['length' => $length]);
return $dbPlaylist->with('tracks')->get()->toJson();
}
/**
@ -141,7 +171,7 @@ class PlaylistController extends Controller
];
break;
case 'stream':
//Todo: $model = Stream::whereId($file['id'])->first();
$model = Webstream::whereId($file['id'])->first();
$data = [
'playlist_id' => $playlist->id,
'stream_id' => $file['id'],

View file

@ -0,0 +1,127 @@
<?php
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;
use Illuminate\Support\Facades\DB;
class PodcastController extends Controller
{
public function index(Request $request) {
if (!isset($request->per_page) || is_null($request)) {
$pagination = 5;
} else {
$pagination = $request->per_page;
}
return Podcast::searchFilter($request)
->where('id', '!=', 1)
->with(['episodes', 'imported', 'owner'])
->paginate($pagination)
->toJson();
}
public function store(Request $request) {
return $this->save($request);
}
/**
* Method used to update a smart block
* @param Request $request
* @param $id
* @return mixed
*/
public function update(Request $request) {
return $this->save($request);
}
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 loadPodcastDataFromXml(Request $request) {
try {
$xml = simplexml_load_file($request->url, null, LIBXML_NOCDATA);
$xmlArray = (array) $xml->channel;
$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)
{
$user = Auth::user();
$xml = simplexml_load_file($request->url);
$itunes = $xml->channel->children('itunes', TRUE);
try {
$dbPodcast = Podcast::firstOrNew(['id' => $request->id]);
DB::beginTransaction();
$dbPodcast->fill([
'url' => $request->url,
'title' => $request->title,
'creator' => $itunes->author,
'description' => $itunes->subtitle,
'language' => $xml->channel->language,
'copyright' => $xml->channel->copyright,
'link' => $xml->channel->link,
'itunes_author'=> $itunes->author,
'itunes_keywords' => '',
'itunes_summary' => $itunes->summary,
'itunes_subtitle' => $itunes->subtitle,
'itunes_category' => '',
'itunes_explicit' => $itunes->explicit,
'owner' => $user->id,
])->save();
$dbImportedPodcast = ImportedPodcast::firstOrNew(['podcast_id' => $dbPodcast->id]);;
$dbImportedPodcast->fill([
'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

@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers;
use App\Models\Podcast\CeleryTask;
use App\Models\Podcast\Podcast;
use App\Models\Podcast\PodcastEpisode;
use App\Models\Podcast\ThirdPartyTrackReference;
use Celery\Celery;
use DateTime;
use DateTimeZone;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PodcastEpisodeController extends Controller
{
private static $_CELERY_MESSAGE_TIMEOUT = 900000; // 15 minutes
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');
}
$podcastEpisode = new PodcastEpisode();
$podcastEpisode->fill([
'podcast_id' => $request->podcast_id,
'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();
$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(
config('rabbitmq.host'),
config('rabbitmq.user'),
config('rabbitmq.password'),
config('rabbitmq.vhost'),
'podcast',
'podcast',
config('rabbitmq.port'),
false,
self::$_CELERY_MESSAGE_TIMEOUT
);
$data = [
'episode_id' => $podcastEpisode->id,
'episode_url' => $request->episode_url,
'episode_title' => $podcastEpisode->episode_title,
'podcast_name' => $podcast->title,
'override_album' => 'false' //ToDo connect $album_override from imported_podcast,
];
$taskId = $conn->PostTask('podcast-download', $data, true, 'podcast');
return $taskId;
} catch (\Exception $exception) {
Log::error($exception->getMessage());
die($exception->getMessage());
}
}
}

View file

@ -5,12 +5,15 @@ namespace App\Http\Controllers;
use App\Http\Requests\ScheduleRequest;
use App\Http\Resources\ScheduleResource;
use App\Models\Schedule;
use Illuminate\Http\Request;
class ScheduleController extends Controller
{
public function index()
public function index(Request $request)
{
return ScheduleResource::collection(Schedule::all());
$schedule = Schedule::searchFilter($request)->get();
return $schedule->toJson();
}
public function store(ScheduleRequest $request)

View file

@ -25,15 +25,25 @@ class ShowController extends Controller
use ShowInstancesTrait;
use ShowDjTrait;
public function index(ShowFilters $filters)
public function index(Request $request)
{
if ( ! isset($filters->per_page) || is_null($filters)) {
$pagination = 20;
} else {
$pagination = $filters->per_page;
$pagination = 20;
if(isset($request->per_page) || is_null($request)) {
$pagination = $request->per_page;
}
$shows = Show::searchFilter($request);
if($request->has('showType')) {
$showType = $request->get('showType');
$shows = ($showType == 'show')
? $shows->doesntHave('spotShow')
: $shows->has('spotShow');
}
return Show::searchFilter($filters)->cursorPaginate($pagination)->toJson();
return $shows->orderBy('name')
->paginate($pagination)
->toJson();
}
/**

View file

@ -14,6 +14,7 @@ use http\Exception\BadMethodCallException;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class SmartBlockController extends Controller
{
@ -29,9 +30,17 @@ class SmartBlockController extends Controller
} else {
$pagination = $request->per_page;
}
$smartblocks = SmartBlock::searchFilter($request)
->with(['creator', 'tracks', 'tracks.file', 'criteria']);
return SmartBlock::searchFilter($request)
->with(['creator', 'tracks', 'tracks.file', 'criteria'])
if($request->has('smartblockType')) {
$smartblockType = $request->get('smartblockType');
$smartblocks = ($smartblockType == 'show')
? $smartblocks->doesntHave('spotSmartblock')
: $smartblocks->has('spotSmartblock');
}
return $smartblocks
->paginate($pagination)
->toJson();
}
@ -71,37 +80,37 @@ class SmartBlockController extends Controller
* @return mixed string
*/
public function save(Request $request) {
$user = Auth::user();
//dd($user);
$request->validate([
'name' => 'required|string',
'type' => 'required|string',
'criteria' => 'required|array'
]);
try {
$user = Auth::user();//dd($user);
$request->validate([
'name' => 'required|string',
'type' => 'required|string',
'criteria' => 'required|array'
]);
$criteria = $this->createCriteria($request);
$length = 0;
$dbSmartBlock = SmartBlock::firstOrNew(['id' => $request->id]);
$dbSmartBlock->fill([
'name' => $request->name,
'creator_id' => $user->id,
'description' => $request->description,
'length' => $request->length,
])->save();
if ($request['smart_block_type'] === 'spot') {
$dbSmartBlock->spotSmartBlock()->create();
}
$this->saveCriteria($dbSmartBlock, $criteria);//ToDo: save content
if (is_array($request->tracks) && count($request->tracks) > 0) {
SmartBlockContent::where('block_id', '=', $dbSmartBlock->id)->delete();
foreach ($request->tracks as $key => $track) {
$this->saveContent($track['file'], $dbSmartBlock->id, $key);
}
}
$criteria = $this->createCriteria($request);
$length = 0;
$dbSmartBlock = SmartBlock::firstOrNew(['id' => $request->id]);
$dbSmartBlock->fill([
'name' => $request->name,
'creator_id' => $user->id,
'description' => $request->description,
'length' => $request->length,
])->save();
$this->saveCriteria($dbSmartBlock, $criteria);
//ToDo: save content
if (is_array($request->tracks) && count($request->tracks) > 0) {
SmartBlockContent::where('block_id','=',$dbSmartBlock->id)->delete();
foreach ($request->tracks as $key => $track) {
$this->saveContent($track['file'], $dbSmartBlock->id, $key);
}
}
return $dbSmartBlock->toJson();
return $dbSmartBlock->toJson();
} catch (\Exception $e) {
Log::error($e);
}
}
/**

View file

@ -2,30 +2,25 @@
namespace App\Http\Controllers;
use App\Filters\Show\ShowFilters;
use App\Http\Controllers\Controller;
use App\Http\Requests\ShowRequest;
use App\Http\Resources\ShowResource;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\Show\Show;
use App\Models\User;
use App\Traits\ScheduleTrait;
use App\Traits\Show\ShowDaysTrait;
use App\Traits\Show\ShowDjTrait;
use App\Traits\Show\ShowInstancesTrait;
use App\Traits\Show\ShowTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Exception;
use Log;
# TODO Expose the show instance generation for user interaction and pypo queue
# When pypo requests the schedule up to a certain date, generate the shows up to that date
class UserController extends Controller
{
public function index(Request $request)
{
$queryParams = collect($request->except('withShow'));
$userFilter = (new User())->searchFilter($queryParams);
if($request->withShow) $userFilter = $userFilter->with('showDjs');
$userFilter = (new User())->searchFilter($queryParams);
if ($request->withShow) {
$userFilter = $userFilter->with('showDjs');
}
return response()->json($userFilter->get());
}
@ -41,22 +36,56 @@ class UserController extends Controller
$show = Show::firstOrCreate($showInfos);
$this->manageShowDays($show, $showDaysRules);
$this->manageShowDjs($showDjs, $show);
}catch(Exception $e){
} catch (Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
return response()->json(['message' => 'Show created successfully']);
}
public function show(ShowResource $show)
public function show(User $user)
{
return new ShowResource($show);
$allowedRoles = ['admin', 'editor'];
$authenticatedUser = auth()->user();
if ( ! $authenticatedUser && ! in_array($authenticatedUser->type, $allowedRoles)) {
return response()->json(['message' => 'Forbidden'], 403);
}
return response()->json($user);
}
public function update(ShowRequest $request, Show $show)
public function userProfile()
{
$show->update($request->validated());
$user =auth()->user();
$user->role = $user->roles()->value('name');
return new ShowResource($show);
return response()->json($user);
}
public function update(Request $request, User $user, UpdateUserProfileInformation $updater)
{
$authenticatedUser = auth()->user();
if ($authenticatedUser->id !== $user->id && !$authenticatedUser->hasPermissionTo('user.manageAll')) {
return response()->json(['message' => 'You do not have permission to edit other users.'], 403);
}
if ($authenticatedUser->id === $user->id && !$authenticatedUser->hasPermissionTo('users.manageOwn')) {
return response()->json(['message' => 'You do not have permission to edit your own profile.'], 403);
}
try {
$updater->update($user, $request->all());
$user->load('preferences');
return response()->json($user);
} catch (\Throwable $e) {
Log::error($e->getMessage());
if ($e instanceof \Illuminate\Validation\ValidationException) {
return response()->json(['message' => $e->getMessage(), 'errors' => $e->errors()], 422);
}
return response()->json(['message' => 'Failed to update user'], 500);
}
}
public function destroy(Request $request)
@ -71,10 +100,4 @@ class UserController extends Controller
return response()->json(['message' => $responseMessage]);
}
public function testSchedule(int $showId)
{
$show = Show::find($showId);
$this->manageShowSchedule($show);
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers;
use App\Filters\Webstream\WebstreamFilters;
use App\Models\Webstream;
use App\Services\WebstreamService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
use Throwable;
class WebstreamController extends Controller
{
public function __construct(WebstreamService $webstreamService)
{
$this->webstreamService = $webstreamService;
}
public function index(WebstreamFilters $filters)
{
try {
if ( ! isset($filters->per_page) || is_null($filters)) {
$pagination = 20;
} else {
$pagination = $filters->per_page;
}
return Webstream::searchFilter($filters)->cursorPaginate($pagination)->toJson();
} catch (Exception $e) {
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function show(Request $request, $webstreamId)
{
try {
$webstream = Webstream::findOrFail($webstreamId);
return response()->json($webstream);
} catch (Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => 'Webstream not found: ' . $e->getMessage()], 404);
}
}
public function store(Request $request)
{
try {
# Todo length field needs to be validated against a type
$data = $request->validate([
'name' => ['required'],
'description' => ['required'],
'url' => ['required', 'url'],
'length' => ['required'],
]);
$isAudioStream = $this->webstreamService->isAudioStream($data['url']);
# TODO Permissions
$data['mtime'] = now();
$data['utime'] = now();
$data['creator_id'] = auth()->id();
DB::beginTransaction();
$webstream = Webstream::create($data);
DB::commit();
return response()->json([
'status' => 'success',
'message' => 'Webstream saved successfully!',
'data' => $webstream,
'isAudioStream' => $isAudioStream
]);
} catch (Exception $e) {
Log::error($e->getMessage());
DB::rollBack();
return response()->json(['message' => $e->getMessage()], 500);
} catch (Throwable $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function update(Request $request, Webstream $webstream)
{
try {
$data = $request->validate([
'name' => ['required'],
'description' => ['required'],
'url' => ['required', 'url'],
]);
$isAudioStream = $this->webstreamService->isAudioStream($data['url']);
// Update utime to current time
$data['utime'] = now();
DB::beginTransaction();
$webstream->update($data);
DB::commit();
return response()->json([
'status' => 'success',
'message' => 'Webstream updated successfully!',
'data' => $webstream,
'isAudioStream' => $isAudioStream
]);
} catch (Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
} catch (Throwable $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function destroy(Webstream $webstream)
{
try {
DB::beginTransaction();
$webstream->delete();
DB::commit();
return response()->json([
'status' => 'success',
'message' => 'Webstream deleted successfully!'
]);
} catch (Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
} catch (Throwable $e) {
DB::rollBack();
Log::error($e->getMessage());
return response()->json(['message' => $e->getMessage()], 500);
}
}
}

View file

@ -12,6 +12,6 @@ class Authenticate extends Middleware
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
return $request->expectsJson() ? null : '/login';
}
}

View file

@ -12,6 +12,18 @@ class LoginResponse implements LoginResponseContract
public function toResponse($request)
{
$user = $request->user();
$user->load(['roles', 'preferences' => function ($query) {
$query->where('keystr', 'user_timezone');
}]);
$timezonePreference = $user->preferences->first();
$user->timezone = $timezonePreference ? $timezonePreference->value : null;
unset($user->preferences);
$user->role = $user->roles->first() ? $user->roles->first()->name : null;
unset($user->roles);
return response()->json($user);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
class LogoutResponse implements LogoutResponseContract
{
/**
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
return $request->wantsJson()
? new JsonResponse('', 204)
: redirect('/login');
}
}

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Filters\PlaylistFilter;
use App\Models\Spot\SpotPlaylist;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use MusicBrainz\Value\Track;
@ -30,6 +31,10 @@ class Playlist extends Model
return $this->hasMany(PlaylistContent::class);
}
public function spotPlaylist(){
return $this->hasOne(SpotPlaylist::class, 'playlist_id');
}
public function scopeSearchFilter($query, $request) {
$filters = new PlaylistFilter();
return $filters->apply($query, $request);

View file

@ -40,7 +40,7 @@ class PlaylistContent extends Model
return $this->belongsTo(SmartBlock::class, 'block_id');
}
public function stream() {
//ToDo create belongsTo relationship after create stream model
public function webstream() {
return $this->belongsTo(Webstream::class, 'stream_id');
}
}

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

@ -0,0 +1,25 @@
<?php
namespace App\Models\Podcast;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ImportedPodcast extends Model
{
use HasFactory;
protected $table = 'imported_podcast';
public $timestamps = false;
protected $fillable = [
'auto_ingest',
'auto_ingest_timestamp',
'album_override',
'podcast_id'
];
public function podcast() {
return $this->belongsTo(Podcast::class);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Models\Podcast;
use App\Filters\PodcastFilter;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Podcast extends Model
{
use HasFactory;
protected $table = 'podcast';
public $timestamps = false;
protected $fillable = [
'url',
'title',
'creator',
'description',
'language',
'copyright',
'link',
'itunes_author',
'itunes_keywords',
'itunes_summary',
'itunes_subtitle',
'itunes_category',
'itunes_explicit',
'owner',
];
public function owner() {
return $this->belongsTo(User::class, 'owner');
}
public function episodes() {
return $this->hasMany(PodcastEpisode::class, 'podcast_id');
}
public function imported()
{
return $this->hasOne(ImportedPodcast::class, 'podcast_id');
}
public function scopeSearchFilter($query, $request) {
$filters = new PodcastFilter();
return $filters->apply($query, $request);
}
}

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 PodcastEpisode extends Model
{
use HasFactory;
protected $table = 'podcast_episodes';
public $timestamps = false;
protected $fillable = [
'file_id',
'podcast_id',
'publication_date',
'download_url',
'episode_guid',
'episode_title',
'episode_description',
];
public function file() {
return $this->hasOne(File::class);
}
public function podcast() {
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

@ -2,6 +2,7 @@
namespace App\Models;
use App\Filters\ScheduleFilters;
use App\Models\ShowInstances\ShowInstances;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,6 +26,7 @@ class Schedule extends Model
'broadcasted',
'position',
'file_id',
'stream_id',
'instance_id'
];
@ -34,13 +36,23 @@ class Schedule extends Model
'media_item_played' => 'boolean',
];
public function ccFile(): BelongsTo
public function file(): BelongsTo
{
return $this->belongsTo(File::class, 'cc_file_id');
return $this->belongsTo(File::class, 'file_id');
}
public function ccShowInstance(): BelongsTo
public function webstream(): BelongsTo
{
return $this->belongsTo(ShowInstances::class, 'cc_show_instance_id');
return $this->belongsTo(Webstream::class, 'stream_id');
}
public function showInstance(): BelongsTo
{
return $this->belongsTo(ShowInstances::class, 'show_instance_id');
}
public function scopeSearchFilter($query, $request)
{
$filters = new ScheduleFilters();
return $filters->apply($query, $request);
}
}

View file

@ -2,14 +2,14 @@
namespace App\Models\Show;
use App\Filters\Show\ShowFilters;
use App\Models\Playlist;
use App\Models\ShowInstances\ShowInstances;
use App\Models\SmartBlock;
use App\Models\Spot\SpotShow;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Show extends Model
{
@ -66,13 +66,18 @@ class Show extends Model
return $this->hasMany(ShowInstances::class, 'show_id');
}
public function scopeSearchFilter($query, $filters)
public function scopeSearchFilter($query, $request)
{
return $filters->apply($query);
$filters = new ShowFilters();
return $filters->apply($query, $request);
}
public function playlist()
{
return $this->belongsTo(Playlist::class, 'autoplaylist_id', 'id');
}
public function spotShow(){
return $this->hasOne(SpotShow::class, 'show_id');
}
}

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Filters\PlaylistFilter;
use App\Models\Spot\SpotSmartBlock;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -41,4 +42,8 @@ class SmartBlock extends Model
$filters = new PlaylistFilter();
return $filters->apply($query, $request);
}
public function spotSmartBlock(){
return $this->hasOne(SpotSmartBlock::class, 'block_id');
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Models\Spot;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SpotPlaylist extends Model
{
protected $table = 'wa_spot_playlist';
protected $fillable = [
'playlist_id',
];
/**
* Get the playlist associated with this spot.
*/
public function playlist(): BelongsTo
{
return $this->belongsTo(Playlist::class, 'playlist_id');
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Models\Spot;
use App\Models\Show\Show;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SpotShow extends Model
{
protected $table = 'wa_spot_show';
protected $fillable = [
'show_id',
];
/**
* Get the show associated with this spot.
*/
public function show(): BelongsTo
{
return $this->belongsTo(Show::class, 'show_id');
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Models\Spot;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use PhpParser\Node\Stmt\Block;
class SpotSmartBlock extends Model
{
protected $table = 'wa_spot_block';
protected $fillable = [
'block_id',
];
/**
* Get the block associated with this spot.
*/
public function block(): BelongsTo
{
return $this->belongsTo(Block::class, 'block_id');
}
}

View file

@ -4,6 +4,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Filters\UserFilters;
use App\Helpers\Preferences;
use App\Models\Show\ShowHosts;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
@ -26,10 +27,16 @@ class User extends Authenticatable
protected $fillable = [
'login',
'email',
'first_name',
'last_name',
'cell_phone',
'pass',
'type'
'type',
'timezone',
];
protected $appends = ['timezone'];
/**
* The attributes that should be hidden for serialization.
*
@ -87,6 +94,24 @@ class User extends Authenticatable
parent::setAttribute($key, $value);
}
public function getTimezoneAttribute(): string
{
// Find the timezone preference or return a default value.
return $this->preferences()->where('keystr', 'user_timezone')->first()->valstr ?? Preferences::getDefaultTimeZone();
}
public function setTimezoneAttribute(?string $value): void
{
if ($value) {
$this->preferences()->updateOrCreate(
['keystr' => 'user_timezone'],
['valstr' => $value]
);
} else {
$this->preferences()->where('keystr', 'user_timezone')->delete();
}
}
/**
* Specify the password column for authentication.
*/
@ -113,4 +138,9 @@ class User extends Authenticatable
{
return $this->hasMany(ShowHosts::class, 'subjs_id');
}
public function preferences()
{
return $this->hasMany(Preference::class, 'subjid');
}
}

35
app/Models/Webstream.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Webstream extends Model
{
public $timestamps = false;
protected $table = 'cc_webstream';
protected $fillable = [
'name',
'description',
'url',
'length',
'creator_id',
'mtime',
'utime',
'lptime',
'mime',
];
protected $casts = [
'mtime' => 'timestamp',
'utime' => 'timestamp',
'lptime' => 'timestamp',
];
public function scopeSearchFilter($query, $filters)
{
return $filters->apply($query);
}
}

View file

@ -15,6 +15,7 @@ use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\LogoutResponse;
class FortifyServiceProvider extends ServiceProvider
{
@ -32,11 +33,20 @@ class FortifyServiceProvider extends ServiceProvider
public function boot(): void
{
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(LogoutResponse::class, LogoutResponse::class);
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::authenticateUsing([LoginUser::class, 'login']);
$this->app->instance(LogoutResponse::class, new class implements LogoutResponse {
public function toResponse($request)
{
return redirect('/login');
}
});
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());

View file

@ -50,15 +50,14 @@ class ScheduleService
->where('ends', '<=', $showInstance->ends)
->get()
->keyBy(function ($item) {
return $item->starts->timestamp . '_' . $item->ends->timestamp . '_' . $item->file_id;
return $item->starts->timestamp . '_' . $item->ends->timestamp;
});
// Compare and insert only new entries
$newItems = collect($this->prepareScheduleData($scheduleItems))
->filter(function ($item) use ($existingSchedule) {
$key = Carbon::parse($item['starts'])->timestamp . '_' .
Carbon::parse($item['ends'])->timestamp . '_' .
$item['file_id'];
Carbon::parse($item['ends'])->timestamp;
return !$existingSchedule->has($key);
})
->toArray();
@ -117,7 +116,7 @@ class ScheduleService
$nextContent = $playlistContent[$playlistIndex];
$contentType = $this->resolveContentType($nextContent->type);
$content = $nextContent->$contentType()->first();
$nextLength = LengthFormatter::contentLengthToSeconds($content->length);
//$nextLength = LengthFormatter::contentLengthToSeconds($content->length);
$result = $this->managePlaylistContentEntry(
$playlistContent[$playlistIndex],
@ -153,19 +152,29 @@ class ScheduleService
$contentLength = LengthFormatter::contentLengthToSeconds($content->length);
$contentFinalLength = $this->calculateFinalLength($currentTime, $contentLength, $endTime);
$fileId = null;
$webstreamId = null;
if ($contentType === 'file') {
$content->update(['is_scheduled' => true]);
$fileId = $content->id;
}
if ($contentType === 'webstream') {
$webstreamId = $content->id;
}
$schedule = $this->createScheduleItem(
$currentTime,
$contentLength,
$contentFinalLength,
$playlistContentEntry,
$content->id,
$fileId,
$webstreamId,
$showInstanceId
);
if ($contentType === 'file') {
$content->update(['is_scheduled' => true]);
}
return [
'currentTime' => $currentTime->addSeconds($contentLength),
'schedule' => $schedule
@ -176,7 +185,7 @@ class ScheduleService
{
return match (PlaylistContentType::from($type)) {
PlaylistContentType::audioclip => 'file',
PlaylistContentType::stream => 'stream',
PlaylistContentType::webstream => 'webstream',
PlaylistContentType::block => 'block',
default => throw new InvalidArgumentException('Invalid content type'),
};
@ -194,7 +203,8 @@ class ScheduleService
int $contentLength,
int $contentDuration,
PlaylistContent $playlistContentEntry,
int $contentId,
int | null $fileId,
int | null $webstreamId,
int $showInstanceId
): Schedule {
return new Schedule([
@ -209,7 +219,8 @@ class ScheduleService
'playout_status' => 0,
'broadcasted' => self::BROADCAST_STATUS_PENDING,
'position' => $playlistContentEntry->position,
'file_id' => $contentId,
'file_id' => $fileId,
'stream_id' => $webstreamId,
'instance_id' => $showInstanceId
]);
}
@ -219,7 +230,7 @@ class ScheduleService
return collect($scheduleItems)->map(fn (Schedule $schedule) => $schedule->only([
'starts', 'ends', 'clip_length', 'fade_in', 'fade_out',
'cue_in', 'cue_out', 'media_item_played', 'playout_status',
'broadcasted', 'position', 'instance_id', 'file_id'
'broadcasted', 'position', 'instance_id', 'file_id', 'stream_id'
]))->toArray();
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Services;
use Exception;
class WebstreamService
{
public function __construct()
{
}
/**
* @param $headers
* @param $headerAttribute
* @param $acceptedValues
*
* @return bool
*/
private function checkHttpHeaders($headers, $headerAttribute, $acceptedValues): bool
{
if (isset($headers[$headerAttribute])) {
$contentType = is_array($headers[$headerAttribute])
? $headers[$headerAttribute][0]
: $headers[$headerAttribute];
$contentType = strtolower($contentType);
foreach ($acceptedValues as $acceptedValue) {
if (str_contains($contentType, $acceptedValue)) {
return true;
}
}
}
return false;
}
/**
* Check if URL is a valid audio stream
*
* @param string $url
*
* @return array{server: bool, audio: bool, streamingServer: bool}
*/
public function isAudioStream(string $url): array
{
$acceptedContentTypes = ['mpeg', 'mp3', 'aac', 'ogg'];
$acceptedServers = ['icecast', 'shoutcast', 'rocket', 'rsas', 'azura', 'ampache'];
$audioCheckResults = ['server' => false, 'audio' => false, 'streamingServer' => false];
try {
$headers = get_headers($url, 1);
if ( ! $headers) {
return $audioCheckResults;
}
$audioCheckResults['server'] = true;
$audioCheckResults['audio'] = $this->checkHttpHeaders($headers,'Content-Type',$acceptedContentTypes);
$audioCheckResults['streamingServer'] = $this->checkHttpHeaders($headers,'Server',$acceptedServers);
return $audioCheckResults;
} catch (Exception $e) {
return $audioCheckResults;
}
}
}

View file

@ -77,8 +77,11 @@ trait ShowInstancesTrait
public function manageShowInstances(Show $show)
{
try {
$generationLimitDate = Preference::where('keystr', 'shows_populated_until')->value('valstr');
$generationLimitDate = Carbon::createFromFormat('Y-m-d H:i:s', $generationLimitDate);
$generationLimitDateSetting = Preference::where('keystr', 'shows_populated_until')->value('valstr');
if (empty($generationLimitDateSetting)) {
$generationLimitDateSetting = Carbon::now()->addYears(3)->format('Y-m-d H:i:s');
}
$generationLimitDate = Carbon::createFromFormat('Y-m-d H:i:s', $generationLimitDateSetting);
$showInstances = [];
foreach ($show->showDays as $showDay) {
try {

View file

@ -6,6 +6,7 @@ use App\Http\Resources\ShowResource;
use App\Lib\RabbitMQSender;
use App\Models\Show\Show;
use App\Models\Show\ShowHosts;
use App\Models\Spot\SpotShow;
use Exception;
use Illuminate\Support\Facades\DB;
use Throwable;
@ -31,6 +32,10 @@ trait ShowTrait
}
$this->manageShowInstances($show);
$show->save();
## TODO Add show to table of spots
if($showData['show_type'] === 'spot') {
$show->spotShow()->create();
}
DB::commit();
} catch (Exception $e) {
DB::rollBack();

View file

@ -18,8 +18,15 @@
"mxl/laravel-job": "^1.6",
"php-amqplib/php-amqplib": "^3.7",
"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": {
"fakerphp/faker": "^1.9.1",
"laravel/breeze": "*",

View file

@ -29,6 +29,6 @@ return [
'max_age' => 0,
'supports_credentials' => false,
'supports_credentials' => true,
];

View file

@ -6,23 +6,36 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('cc_subjs', function (Blueprint $table) {
$table->rememberToken();
$table->timestamps();
$table->string('email_verified_at')->nullable();
if (!Schema::hasColumn('cc_subjs', 'remember_token')) {
$table->rememberToken();
}
if (!Schema::hasColumn('cc_subjs', 'created_at') && !Schema::hasColumn('cc_subjs', 'updated_at')) {
$table->timestamps();
}
if (!Schema::hasColumn('cc_subjs', 'email_verified_at')) {
$table->string('email_verified_at')->nullable();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cc_subjs');
Schema::table('cc_subjs', function (Blueprint $table) {
if (Schema::hasColumn('cc_subjs', 'remember_token')) {
$table->dropColumn('remember_token');
}
if (Schema::hasColumn('cc_subjs', 'created_at')) {
$table->dropColumn('created_at');
}
if (Schema::hasColumn('cc_subjs', 'updated_at')) {
$table->dropColumn('updated_at');
}
if (Schema::hasColumn('cc_subjs', 'email_verified_at')) {
$table->dropColumn('email_verified_at');
}
});
}
};
};

View file

@ -11,11 +11,14 @@ return new class extends Migration
*/
public function up(): void
{
Schema::create('wa_password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
if ( ! Schema::hasTable('wa_password_reset_tokens')) {
Schema::create('wa_password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
}
/**

View file

@ -21,39 +21,45 @@ return new class extends Migration
{
$schema = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
if (!$schema->hasTable('telescope_entries')) {
$schema->create('telescope_entries', function (Blueprint $table) {
$table->bigIncrements('sequence');
$table->uuid('uuid');
$table->uuid('batch_id');
$table->string('family_hash')->nullable();
$table->boolean('should_display_on_index')->default(true);
$table->string('type', 20);
$table->longText('content');
$table->dateTime('created_at')->nullable();
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
$table->unique('uuid');
$table->index('batch_id');
$table->index('family_hash');
$table->index('created_at');
$table->index(['type', 'should_display_on_index']);
});
}
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
if (!$schema->hasTable('telescope_entries_tags')) {
$schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->uuid('entry_uuid');
$table->string('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->primary(['entry_uuid', 'tag']);
$table->index('tag');
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
$table->foreign('entry_uuid')
->references('uuid')
->on('telescope_entries')
->onDelete('cascade');
});
}
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
if (!$schema->hasTable('telescope_monitoring')) {
$schema->create('telescope_monitoring', function (Blueprint $table) {
$table->string('tag')->primary();
});
}
}
/**

View file

@ -11,15 +11,17 @@ return new class extends Migration
*/
public function up(): void
{
Schema::create('wa_failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
if (!Schema::hasTable('wa_failed_jobs')) {
Schema::create('wa_failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
}
/**

View file

@ -1,33 +1,136 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('wa_personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if (!Schema::hasTable($tableNames['permissions'])) {
Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
}
if (!Schema::hasTable($tableNames['roles'])) {
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
$table->bigIncrements('id');
if ($teams || config('permission.testing')) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
}
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
}
});
}
if (!Schema::hasTable($tableNames['model_has_roles'])) {
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
}
});
}
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
}
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('wa_personal_access_tokens');
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};

View file

@ -4,21 +4,22 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('laravelsessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
if ( ! Schema::hasTable('laravelsessions')) {
Schema::create('laravelsessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity');
});
}
}
/**

View file

@ -11,9 +11,15 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('cc_track_types', function (Blueprint $table) {
$table->foreignId('parent_id')->after('id')->nullable()->references('id')->on('cc_track_types');
});
if (!Schema::hasColumn('cc_track_types', 'parent_id')) {
Schema::table('cc_track_types', function (Blueprint $table) {
$table->foreignId('parent_id')
->after('id')
->nullable()
->constrained('cc_track_types')
->onDelete('cascade'); // optional, add if you want cascade delete
});
}
}
/**
@ -21,8 +27,11 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('cc_track_types', function (Blueprint $table) {
$table->dropColumn('parent_id');
});
if (Schema::hasColumn('cc_track_types', 'parent_id')) {
Schema::table('cc_track_types', function (Blueprint $table) {
$table->dropForeign(['parent_id']); // drop foreign key first
$table->dropColumn('parent_id');
});
}
}
};

View file

@ -4,137 +4,171 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
return new class extends Migration {
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw new \Exception(
'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'
);
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw new \Exception(
'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'
);
}
Schema::create($tableNames['permissions'], function (Blueprint $table) {
//$table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ( ! Schema::hasTable($tableNames['permissions'])) {
Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
//$table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
});
}
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
if ( ! Schema::hasTable($tableNames['roles'])) {
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
$table->bigIncrements('id');
if ($teams || config('permission.testing')) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
}
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
if ( ! Schema::hasTable($tableNames['model_has_permissions'])) {
Schema::create(
$tableNames['model_has_permissions'],
function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_model_id_model_type_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
});
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->primary(
[
$columnNames['team_foreign_key'],
$pivotPermission,
$columnNames['model_morph_key'],
'model_type'
],
'model_has_permissions_permission_model_type_primary'
);
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
}
);
}
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
if ( ! Schema::hasTable($tableNames['model_has_roles'])) {
Schema::create(
$tableNames['model_has_roles'],
function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'],
'model_has_roles_model_id_model_type_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->primary(
[
$columnNames['team_foreign_key'],
$pivotRole,
$columnNames['model_morph_key'],
'model_type'
],
'model_has_roles_role_model_type_primary'
);
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
}
);
}
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ( ! Schema::hasTable($tableNames['role_has_permissions'])) {
Schema::create(
$tableNames['role_has_permissions'],
function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole],
'role_has_permissions_permission_id_role_id_primary');
}
);
}
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
throw new \Exception(
'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'
);
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};

View file

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWaSpotPlaylistTable extends Migration
{
public function up()
{
if (!Schema::hasTable('wa_spot_playlist')) {
Schema::create('wa_spot_playlist', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('playlist_id');
$table->timestamps();
$table->foreign('playlist_id')->references('id')->on('cc_playlist')->onDelete('cascade');
});
}
}
public function down()
{
Schema::dropIfExists('wa_spot_playlist');
}
}

View file

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWaSpotShowTable extends Migration
{
public function up()
{
if (!Schema::hasTable('wa_spot_show')) {
Schema::create('wa_spot_show', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('show_id');
$table->timestamps();
$table->foreign('show_id')->references('id')->on('cc_show')->onDelete('cascade');
});
}}
public function down()
{
Schema::dropIfExists('wa_spot_show');
}
}

View file

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWaSpotBlockTable extends Migration
{
public function up()
{
if (!Schema::hasTable('wa_spot_block')) {
Schema::create('wa_spot_block', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('block_id');
$table->timestamps();
$table->foreign('block_id')->references('id')->on('cc_block')->onDelete('cascade');
});
}
}
public function down()
{
Schema::dropIfExists('wa_spot_block');
}
}

View file

@ -8,6 +8,8 @@ import { createPinia } from "pinia";
import { createI18n } from "vue-i18n";
import App from "@/layouts/App.vue";
import { useAuthStore } from '@/stores/auth.store';
const pinia = createPinia();
const i18n = createI18n(vueI18n);
const app = createApp(App);
@ -16,4 +18,8 @@ app.use(pinia)
.use(i18n)
.use(router)
.use(vuetify)
.mount("#app");
const auth = useAuthStore();
auth.fetchUser().finally(() => {
app.mount("#app");
});

View file

@ -6,7 +6,7 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.withCredentials = true
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import {computed, onActivated, onDeactivated, ref} from "vue";
import {computed, onActivated, onDeactivated, onMounted, ref} from "vue";
import {useAuthStore} from "@/stores/auth.store.ts";
import {deleteShow} from "@models/show/show.ts";
import {baseShowInstance, deleteShowInstance, getShowInstances} from "@models/show/showInstance.ts";
@ -10,7 +10,7 @@ import CalendarShowEvent from "@partials/show/CalendarShowEvent.vue"
// Store
const auth = useAuthStore();
const userRole = auth.userData.user.role;
const userRole = auth.userData.role;
// Data
const editMode = ref(false);
@ -76,7 +76,7 @@ const goBack = async () => {
// so reducing the network calls sent
// That requires the handling of the context menu
// and the show/instance id in a different way though
onActivated(async () => {
onMounted(async () => {
await triggerFetchShowInstances(new Date());
intervalId = setInterval(async () => {
if (!isRunning.value) {
@ -91,29 +91,31 @@ onDeactivated(() => {
</script>
<template>
<template v-if="showCreateEditMode || showInstanceCreateEditMode">
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/>
<ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance"
@toggle-menu-edit-instance="goBack"/>
</template>
<template v-else>
<v-row class="ma-4 justify-space-around">
<template v-if="userRole && (userRole == 'admin' || userRole == 'editor')">
<v-btn color="primary" @click="createShow">Crea show</v-btn>
<v-btn color="secondary" @click="toggleEditMode">
{{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }}
</v-btn>
</template>
</v-row>
<CalendarShowEvent
:edit-mode="editMode"
:shows="shows"
@contextMenuEditInstance="contextMenuEditInstance"
@contextMenuEditShow="contextMenuEditShow"
@contextMenuDeleteInstance="contextMenuDeleteInstance"
@contextMenuDeleteShow="contextMenuDeleteShow"
/>
</template>
<div>
<template v-if="showCreateEditMode || showInstanceCreateEditMode">
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/>
<ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance"
@toggle-menu-edit-instance="goBack"/>
</template>
<template v-else>
<v-row class="ma-4 justify-space-around">
<template v-if="userRole && (userRole == 'admin' || userRole == 'editor')">
<v-btn color="primary" @click="createShow">Crea show</v-btn>
<v-btn color="secondary" @click="toggleEditMode">
{{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }}
</v-btn>
</template>
</v-row>
<CalendarShowEvent
:edit-mode="editMode"
:shows="shows"
@contextMenuEditInstance="contextMenuEditInstance"
@contextMenuEditShow="contextMenuEditShow"
@contextMenuDeleteInstance="contextMenuDeleteInstance"
@contextMenuDeleteShow="contextMenuDeleteShow"
/>
</template>
</div>
</template>
<style scoped>

View file

@ -3,19 +3,20 @@ import {playlist_page} from "@/composables/content/playlist_page.ts";
import Table from "@/components/content/partials/Table.vue";
import PlaylistEditor from "@/components/content/partials/PlaylistEditor.vue";
import ConfirmDelete from "@/components/content/partials/dialogs/ConfirmDelete.vue";
import {reactive, ref, watch} from "vue";
import {onBeforeMount, onMounted, type PropType, reactive, ref, watch} from "vue";
import {usePlaylistStore} from "@/stores/playlist.store.ts";
import {baseSmartBlock} from "@models/smartblock/smartblock.ts";
import {basePlaylist} from "@models/playlist/playlist.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
const playlistStore = usePlaylistStore();
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = playlist_page();
// Props, data, stores
const itemEdited = ref({
id: null
});
const bulk = ref(false)
const dialog = reactive({
open: false,
@ -23,7 +24,9 @@ const dialog = reactive({
title: '',
text: ''
})
const showTypeStore = useShowTypeStore();
// Funcs
const openDialog = (type, title = '', text = '', bulk = false) => {
dialog.open = true
dialog.type = type
@ -37,10 +40,11 @@ const edit = (item) => {
item = basePlaylist();
}
playlistStore.loadPlaylist(item);
playlistStore.currentPlaylist.playlist_type = showTypeStore.currentType;
itemEdited.value = item;
}
const save = (item) => {
const save = (item) => {5
if (item.name === '') {
//Check required fields
console.log('error!')
@ -91,7 +95,6 @@ const resetItemEdited = () => {
watch(search, (newValue, oldValue) => {
getItems(listData)
})
</script>
<template>

View file

@ -0,0 +1,168 @@
<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 episodes = ref([]);
const bulk = ref(false);
const dialog = reactive({
open: false,
type: '',
title: '',
text: ''
});
const dialogLoading = ref(false);
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 = async (confirm) => {
if (confirm) {
dialogLoading.value = true;
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'
);
})
}
}
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;
}
const resetItemEdited = () => {
podcastStore.currentPodcast = basePodcast();
}
</script>
<template>
<PodcastEditor
v-if="podcastStore.currentPodcast.url != '' && podcastStore.currentPodcast.url != null"
@go-back="resetItemEdited"
/>
<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"
:loading="dialogLoading"
:hide_confirm="dialog.type === 'info' ? true : false"
>
<VTextField
label="Feed RSS"
v-if="dialog.type === 'add'"
v-model="url"
/>
</ConfirmDelete>
</v-dialog>
</template>
</Table>
</template>
<style scoped>
</style>

View file

@ -1,13 +1,15 @@
<script setup lang="ts">
import {reactive, ref, watch} from "vue";
import {onActivated, onBeforeMount, onMounted, type PropType, reactive, ref, watch} from "vue";
import Table from "@partials/Table.vue";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import {show_page} from "@/composables/content/show/show_page.ts";
import ShowForm from "@partials/show/ShowForm.vue";
import {baseShow, type Show} from "@models/show/show";
import {useShowTypeStore} from "@stores/showType.store.ts";
const {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem} = show_page()
const showTypeStore = useShowTypeStore();
const showCreateEditMode = ref(false);
let showSelected = ref<Number | null>(null);
@ -23,6 +25,7 @@ const dialog = reactive({
text: ''
})
// Funcs
const openDialog = (type, title: string = '', text: string = '') => {
dialog.open = true
dialog.type = type
@ -46,8 +49,8 @@ const saveItem = (item) => {
}
const cancel = (item) => {
let deleteMessage = 'Vuoi cancellare lo show selezionato?'
if(bulk.value.state) deleteMessage = 'Vuoi cancellare gli show selezionati?'
let deleteMessage = `Vuoi cancellare lo ${showTypeStore.currentType} selezionato?`
if (bulk.value.state) deleteMessage = `Vuoi cancellare gli ${showTypeStore.currentType} selezionati?`
bulk.value.items = item
showSelected.value = item?.id
openDialog(
@ -59,7 +62,7 @@ const cancel = (item) => {
const confirmDelete = (confirm) => {
if (confirm) {
const showId = showSelected.value == 0 ? null : showSelected.value ;
const showId = showSelected.value == 0 ? null : showSelected.value;
deleteItem(showId)
}
closeDialog()
@ -78,19 +81,19 @@ const resetItemEdited = () => {
showSelected.value = null
}
watch(search, (newValue, oldValue) => {
const options = {...listData};
getItems(options)
})
const goBack = () => {
showCreateEditMode.value = false
showSelected.value = null
}
watch(search, (newValue, oldValue) => {
const options = {...listData};
getItems(options)
})
</script>
<template>
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/>
<ShowForm v-if="showCreateEditMode" :showId="showSelected" :showType="showTypeStore.currentType" @go-back="goBack"/>
<Table
v-else
:headers="headers"
@ -109,7 +112,7 @@ const goBack = () => {
>
<template v-slot:header-buttons>
<v-btn color="primary" @click="create">
Crea una nuova trasmissione
<span>Crea un nuovo {{ showTypeStore.currentType }} </span>
</v-btn>
</template>
<template v-slot:dialog>

View file

@ -1,12 +1,16 @@
<script setup lang="ts">
import Table from "@/components/content/partials/Table.vue";
import ConfirmDelete from "@/components/content/partials/dialogs/ConfirmDelete.vue";
import {onBeforeMount, reactive, ref, watch} from "vue";
import {onBeforeMount, type PropType, reactive, ref, watch} from "vue";
import {smartblock_page} from "@/composables/content/smartblock_page.ts";
import SmartBlockEditor from "@partials/SmartBlockEditor.vue";
import {useSmartBlockStore} from "@/stores/smartblock.store.ts";
import {baseSmartBlock} from "@models/smartblock/smartblock.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = smartblock_page()
// Props, data and stores
const props = defineProps({
hideColumns: {
type: Array,
@ -15,14 +19,9 @@ const props = defineProps({
isDraggable: {
type: Boolean,
required: false
}
},
});
const smartBlockStore = useSmartBlockStore()
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = smartblock_page()
const itemEdited = ref({
id: null
})
@ -37,6 +36,11 @@ const dialog = reactive({
const visibleHeaders = ref(headers)
const showTypeStore = useShowTypeStore();
const smartBlockStore = useSmartBlockStore()
// Funcs
const openDialog = (type, title = '', text = '') => {
dialog.open = true
dialog.type = type
@ -49,6 +53,7 @@ const edit = (item) => {
item = baseSmartBlock();
}
smartBlockStore.loadSmartBlock(item);
smartBlockStore.currentSmartBlock.smart_block_type = showTypeStore.currentType;
itemEdited.value = item;
console.log(smartBlockStore)
}
@ -72,7 +77,7 @@ const cancel = (item) => {
const confirmDelete = (confirm) => {
if (confirm) {
if (!bulk) {
if (!bulk.value) {
deleteItem(itemEdited.value.id)
} else {
itemEdited.value.forEach(el => {

View file

@ -0,0 +1,233 @@
<script setup lang="ts">
import {useAuthStore} from '@/stores/auth.store.ts';
import {storeToRefs} from 'pinia';
import {onBeforeMount, ref, reactive} from 'vue';
import {useRouter} from "vue-router";
import axios from "axios";
const router = useRouter();
const authStore = useAuthStore();
const {userData} = storeToRefs(authStore);
const localUserData = reactive({...userData.value});
const emit = defineEmits([
'userProfilePage'
]);
let timezones = ref<string[]>([]);
let roleList = ref<string[]>([]);
const form = ref<HTMLFormElement | null>(null);
const passwordForm = ref<HTMLFormElement | null>(null);
const dialog = ref(false);
const passwordData = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const formRules = {
'emailRules': [(v: string) => !v || /.+@.+\..+/.test(v) || 'E-mail must be a valid format'],
'cellphoneRules': [(v: string) => !v || /^[0-9-()]*$/.test(v) || 'Cellphone must be a valid number'],
'passwordConfirmationRule': [
(v: string) => !!v || 'Password confirmation is required',
(v: string) => v === passwordData.newPassword || 'Passwords do not match'
],
'requiredRule': [(v: string) => !!v || 'This field is required'],
}
const saveUser = async () => {
if (!form.value) return
const {valid} = await form.value.validate();
if (!valid) return
authStore.userData = {...localUserData};
await authStore.updateUser();
};
const goBack = () => {
router.go(-1);
}
const openPasswordDialog = () => {
dialog.value = true;
};
const closePasswordDialog = () => {
dialog.value = false;
passwordForm.value?.reset();
passwordForm.value?.resetValidation();
};
const resetPassword = async () => {
console.log('aaaa')
try {
await axios.put('/api/user/password', passwordData)
console.log('Password changed');
closePasswordDialog()
return
} catch (e) {
const errorMessage = e.response?.data?.error || 'An unexpected error occurred.';
console.error('Error changing password:' + errorMessage);
return
}
};
onBeforeMount(async () => {
await axios.get('/timezoneList').then(response => {
timezones.value = response?.data
})
if (userData.value.role === 'admin') {
await axios.get('/roleList').then(response => {
roleList.value = response?.data
})
}
})
</script>
<template>
<div>
<v-form ref="form">
<v-container>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="localUserData.login"
label="Login"
required
hint="Your public username."
persistent-hint
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="localUserData.email"
:rules="formRules.emailRules"
label="Email Address"
hint="Used for notifications and account recovery."
persistent-hint
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="localUserData.firstName"
label="First Name"
required
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="localUserData.lastName"
label="Last Name"
required
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="localUserData.cellPhone"
:rules="formRules.cellphoneRules"
label="Cell Phone"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-autocomplete
v-model="localUserData.timezone"
:items="timezones"
label="Timezone"
hint="Sets the time for all events and schedules."
persistent-hint
></v-autocomplete>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="localUserData.role"
:items="roleList"
label="Ruolo"
:disabled="userData.role !== 'admin'"
></v-select>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-end">
<v-btn color="secondary" @click="goBack" class="mr-4">
Back
</v-btn>
<v-btn color="primary" @click="saveUser">
Save Changes
</v-btn>
<v-btn color="error" @click="openPasswordDialog">
Reset Password
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
<v-dialog v-model="dialog" persistent max-width="600px">
<v-card>
<v-card-title>
<span class="text-h5">Reset Your Password</span>
</v-card-title>
<v-card-text>
<p class="text-subtitle-1 mb-4">
Please be sure about the new password you are choosing. Password recovery via email is not implemented yet,
so
a forgotten password cannot be recovered.
</p>
</v-card-text>
<v-form ref="passwordForm" @submit.prevent="resetPassword">
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
v-model="passwordData.oldPassword"
label="Old Password"
type="password"
:rules="formRules.requiredRule"
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passwordData.newPassword"
label="New Password"
type="password"
:rules="formRules.requiredRule"
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passwordData.confirmPassword"
label="Confirm New Password"
type="password"
:rules="formRules.passwordConfirmationRule"
required
></v-text-field>
</v-col>
</v-row>
</v-container>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="closePasswordDialog">
Cancel
</v-btn>
<v-btn type="submit" color="blue-darken-1" variant="tonal">
Confirm
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template>

View file

@ -0,0 +1,150 @@
<script setup lang="ts">
import {onBeforeMount, reactive, ref, watch} from "vue";
import Table from "@partials/Table.vue";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import {webstream_page} from "@/composables/content/webstream_page.ts";
import {baseWebstream, type Webstream} from "@models/webstream.ts";
import WebstreamForm from "@partials/WebstreamForm.vue";
const {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem} = webstream_page()
const props = defineProps({
hideColumns: {
type: Array,
required: false
},
isDraggable: {
type: Boolean,
required: false
}
});
const webstreamCreateEditMode = ref(false);
let webstreamSelected = ref<Number | null>(null);
const visibleHeaders = ref(headers)
const bulk = ref({
state: false,
items: [] as Webstream[],
})
const dialog = reactive({
open: false,
type: '',
title: '',
text: ''
})
const openDialog = (type, title: string = '', text: string = '') => {
dialog.open = true
dialog.type = type
dialog.title = title
dialog.text = text
}
const edit = (webstreamSelectedFromUser) => {
webstreamSelected.value = webstreamSelectedFromUser.id
webstreamCreateEditMode.value = true
}
const create = () => {
webstreamSelected.value = null
webstreamCreateEditMode.value = true
}
const saveItem = (item) => {
const saved = editItem(item)
closeDialog()
}
const cancel = (item) => {
let deleteMessage = 'Vuoi cancellare il webstream selezionato?'
if(bulk.value.state) deleteMessage = 'Vuoi cancellare i webstream selezionati?'
bulk.value.items = item
webstreamSelected.value = item?.id
openDialog(
'delete',
'Cancella',
deleteMessage
)
}
const confirmDelete = (confirm) => {
if (confirm) {
const webstreamId = webstreamSelected.value == 0 ? null : webstreamSelected.value ;
deleteItem(webstreamId)
}
closeDialog()
}
const closeDialog = () => {
dialog.open = false
resetItemEdited()
}
const updateSearch = (text) => {
search.value = text
}
onBeforeMount(() => {
if(props.hideColumns != undefined) {
visibleHeaders.value = headers.filter(el => {
return !props.hideColumns.includes(el.value)
});
}
})
const resetItemEdited = () => {
webstreamSelected.value = null
}
watch(search, (newValue, oldValue) => {
const options = {...listData};
getItems(options)
})
const goBack = () => {
webstreamCreateEditMode.value = false
webstreamSelected.value = null
}
</script>
<template>
<WebstreamForm v-if="webstreamCreateEditMode" :webstreamId="webstreamSelected" @go-back="goBack"/>
<Table
v-else
:headers="visibleHeaders"
v-model:selected="selected"
v-model:search="search"
:list-data="listData"
:items="items"
:loading="loading"
:get-items="getItems"
:actions="true"
:show-select="true"
:is-draggable="isDraggable"
@update-table="getItems"
@update-search="updateSearch"
@delete-item="cancel"
@edit-item="edit"
>
<template v-slot:header-buttons>
<v-btn color="primary" @click="create">
Crea un nuovo webstream
</v-btn>
</template>
<template v-slot:dialog>
<v-dialog v-model="dialog.open">
<ConfirmDelete
v-if="dialog.type === 'delete'"
:title="dialog.title"
:text="dialog.text"
:bulk="bulk.state"
@confirm="confirmDelete"
@after-leave="closeDialog"
/>
</v-dialog>
</template>
</Table>
</template>

View file

@ -0,0 +1,216 @@
<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 {onBeforeMount, reactive, ref, watch} from "vue";
import axios from "axios";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import {podcast_page} from "@/composables/content/podcast_page.ts";
const auth = useAuthStore();
const emit = defineEmits([
'saveItem',
'goBack'
])
const podcastStore = usePodcastStore();
const item = podcastStore.currentPodcast;
const { items, headers, loading, downloadEpisode, getItems } = podcast_episode_page(item.url);
const podcast_id = ref(item.id);
console.log(item)
const podcastFields = podcast(item);
console.log(podcastFields())
const { editItem } = podcast_page();
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) => {
if (field.required) {
const error = field.required && (model === '' || model === null)
disabledSaveButton.value = error
return error
}
return false
}
const checkDownload = (item) => {
if (podcast_id.value > 0) {
// console.log(item);
downloadEpisode(podcast_id.value, item);
} else {
openDialog(
'save',
'Salvataggio necessario',
'Per procedere con il download dell\'episodio è necessario salvare il podcast. Confermi?',
item
)
}
}
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;
}, {deep: true});
setInterval(() => {
getItems(false, item.id);
}, 5000)
onBeforeMount(() => {
getItems().then(title => podcastStore.updateField({'title': title}));
});
</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])"
: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 }">
<v-icon
class="me-2 spinning"
size="small"
v-if="item.imported === 'PENDING'"
>
mdi-loading
</v-icon>
<v-icon
class="me-2"
size="small"
v-else-if="item.imported === 'SUCCESS'"
>
mdi-check-outline
</v-icon>
<v-icon
class="me-2 text-center"
size="small"
v-else-if="item.imported === 0"
@click="checkDownload(item)"
>
mdi-download-box
</v-icon>
</template>
</VDataTable>
</v-col>
</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>
<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

@ -6,26 +6,24 @@ import {smartblock} from "@models/smartblock/smartblock.ts";
import {smartblock_page} from "@/composables/content/smartblock_page.ts";
import {formatFromSeconds} from "@/helpers/TimeFormatter.ts";
import {useSmartBlockStore} from "@/stores/smartblock.store.ts";
const auth = useAuthStore();
import {baseSmartBlockCriteria} from "@models/smartblock/smartblockCriteria.ts";
const { loading, getTracklist } = smartblock_page()
const emit = defineEmits([
'saveItem'
])
//
const auth = useAuthStore();
const smartBlockStore = useSmartBlockStore();
const item = smartBlockStore.currentSmartBlock;
console.log(smartBlockStore.currentSmartBlock)
const length = ref([]);
const smartblockFields = smartblock(item)
const length = ref([]);
//Is true if there is a required field empty or while saving
const disabledSaveButton = ref(true)
// Funcs
const update = (list) => {
item.tracks = list
}
@ -61,8 +59,25 @@ const showPreview = async () => {
})
}
function hiddenSmartBlockCriteria(){
if(!smartBlockStore.currentSmartBlock.smart_block_type) return
let showSmartBlockCriteria = baseSmartBlockCriteria()
showSmartBlockCriteria.criteria = 'track_type_id'
showSmartBlockCriteria.modifier = 'is not'
showSmartBlockCriteria.value = '3'
if (smartBlockStore.currentSmartBlock.smart_block_type == 'spot') {
showSmartBlockCriteria.modifier = 'is'
}
smartBlockStore.currentSmartBlock.criteria.push(showSmartBlockCriteria)
}
// Hook and watch
onMounted(() => {
smartBlockStore.updateField({key: 'creator_id', value: auth.userData.user.id})
smartBlockStore.updateField({key: 'creator_id', value: auth.userData.id})
hiddenSmartBlockCriteria()
})
watch(loading.value, (newVal, oldVal) => {

View file

@ -1,7 +1,9 @@
<script setup lang="ts">
import {ref} from "vue";
import {onBeforeMount, ref} from "vue";
import Archive from "@/components/content/Archive.vue";
import Blocks from "@components/content/SmartBlock.vue";
import Webstream from "@components/content/Webstream.vue";
import {useShowTypeStore} from "@stores/showType.store.ts";
const tab = ref(null)
const tabs = [
@ -13,7 +15,20 @@ const tabs = [
id: 'blocks',
title: 'Blocchi dinamici',
},
{
id: 'webstream',
title: 'Webstream',
},
]
onBeforeMount(() => {
const showTypeStore = useShowTypeStore()
if(showTypeStore.currentType == 'spot') {
const webstreamIndex = tabs.findIndex(tab => tab.id == 'webstream')
tabs.splice(webstreamIndex, 1)
}
})
</script>
<template>
@ -42,6 +57,11 @@ const tabs = [
:is-draggable="true"
:hideColumns="['mtime', 'utime', 'actions']"
/>
<Webstream
v-if="tab.id === 'webstream'"
:show-select="false"
:is-draggable="true"
/>
</v-tabs-window-item>
</v-tabs-window>
</template>

View file

@ -65,12 +65,30 @@ const rehydratateTracks = () => {
track_info.block = track
track_info.id = track.id
}
if (Object.hasOwn(track, 'webstream') && track.webstream !== null) {
track_info.type = 'webstream'
track_info.title = track.webstream.name
track_info.subtitle = track.webstream.description
track_info.db_element = track.webstream
track_info.webstream = track.webstream
track_info.id = track.stream_id
}
if (!Object.hasOwn(track, 'webstream') && track.type === 'webstream') {
track_info.type = 'webstream'
track_info.title = track.name
track_info.subtitle = track.description
track_info.db_element = track
track_info.webstream = track
track_info.id = track.id
}
return track_info
})
}
onMounted(() => {
rehydratateTracks()
if (typeof props.tracks === "object") {
rehydratateTracks();
}
})
const checkMove = (e) => {
@ -100,6 +118,15 @@ const change = (event) => {
trackList.value[event.added.newIndex].title = trackList.value[event.added.newIndex].name;
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].creator.login;
}
// webstream
if (
Object.hasOwn(trackList.value[event.added.newIndex], 'url') &&
!Object.hasOwn(trackList.value[event.added.newIndex], 'track_title')
) {
trackList.value[event.added.newIndex].type = 'webstream';
trackList.value[event.added.newIndex].title = trackList.value[event.added.newIndex].name;
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].description;
}
}
emit('updateTracks', trackList.value)
}

View file

@ -0,0 +1,141 @@
<script setup lang="ts">
import {ref, onMounted, type PropType, reactive} from "vue";
import {baseWebstream, createWebstream, getWebstream, updateWebstream, type Webstream} from "@models/webstream.ts";
// Props and emits
const props = defineProps({
webstreamId: {
type: Number as PropType<number | null>,
required: true,
},
});
const emits = defineEmits(['go-back']);
// Data
const loading = ref(false);
const isFormValid = ref(false);
const currentWebstream = ref<Webstream>(baseWebstream());
// Funcs
onMounted(async () => {
loading.value = true;
if (props.webstreamId === null) {
currentWebstream.value = baseWebstream();
} else {
currentWebstream.value = await getWebstream(props.webstreamId);
}
loading.value = false;
});
const goBack = () => {
emits('go-back');
};
const saveWebstream = async () => {
loading.value = true;
try {
if (currentWebstream.value.id) {
await updateWebstream(currentWebstream.value);
goBack();
return
}
await createWebstream(currentWebstream.value);
goBack()
return
} catch (error) {
console.error("Error saving webstream:", error);
} finally {
loading.value = false;
}
};
</script>
<template>
<v-card
:disabled="loading"
:loading="loading"
>
<template v-slot:loader="{ isActive }">
<v-progress-linear
:active="isActive"
color="deep-purple"
height="4"
indeterminate
></v-progress-linear>
</template>
<v-card-title>
<h3>Webstream</h3>
</v-card-title>
<v-form ref="form" v-model="isFormValid">
<v-card-text>
<v-row no-gutters>
<!-- Name Field -->
<v-col cols="12" md="6">
<v-card>
<v-text-field
v-model="currentWebstream.name"
label="Nome"
density="compact"
:rules="[v => !!v || 'Nome è obbligatorio']"
required="true"
/>
</v-card>
</v-col>
<!-- URL Field -->
<v-col cols="12" md="6">7
<v-card>
<v-text-field
v-model="currentWebstream.url"
label="URL"
density="compact"
:rules="[v => !!v || 'URL è obbligatorio', v => /^http?:\/\//.test(v) || 'URL deve iniziare con http://']"
required="true"
/>
</v-card>
</v-col>
<!-- Description Field -->
<v-col cols="12">
<v-card>
<v-textarea
v-model="currentWebstream.description"
label="Descrizione"
density="compact"
rows="2"
:rules="[v => !!v || 'Descrizione è obbligatoria']"
required="true"
/>
</v-card>
</v-col>
<!-- Length Field -->
<v-col cols="12">
<v-card>
<v-textarea
v-model="currentWebstream.length"
label="Durata"
density="compact"
rows="2"
:rules="[v => !!v || 'Durata è obbligatoria']"
required="true"
/>
</v-card>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="accent" @click="goBack">Torna indietro</v-btn>
<v-btn color="primary" @click="saveWebstream" :disabled="!isFormValid">
{{ currentWebstream.id ? 'Aggiorna' : 'Crea' }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</template>
<style scoped></style>

View file

@ -2,10 +2,30 @@
const props = defineProps({
title: 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>
<template>
@ -15,16 +35,19 @@ console.log(props.bulk)
</v-card-title>
<v-card-text>
<p>{{ props.text }}</p>
<slot></slot>
</v-card-text>
<v-card-actions>
<v-btn
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>
<v-btn
color="accent"
@click="$emit('confirm',false)"
>Cancella</v-btn>
>Annulla</v-btn>
</v-card-actions>
</v-card>
</template>

View file

@ -32,6 +32,7 @@ const uploadFiles = async () => {
const formData = new FormData();
formData.append('file', fileToUpload.file);
formData.append('track_type_id', selectedTrackType.value.id);
const response = await uploadItem(formData);
fileToUpload.id = response?.id;
@ -107,7 +108,6 @@ const checkUploadStatus = async (file) => {
{{ track.file.name }}
</v-list-item>
</v-list>
{{ selectedTrackType }}
</v-card-text>
<v-card-actions>
<v-btn

View file

@ -1,11 +1,13 @@
<script setup lang="ts">
import {ref, onMounted, type PropType} from "vue";;
import {ref, onMounted, type PropType} from "vue";
;
import ShowScheduleForm from "@partials/show/ShowScheduleForm.vue";
import {useShowStore} from "@/stores/show.store.ts";
import {useShowStore} from "@stores/show/show.store.ts";
import {getUser} from "@models/User.ts";
import {getPlaylist} from "@models/playlist.ts";
import ColorPickerButton from "@partials/fields/misc/ColorPickerButton.vue";
import {useShowDaysStore} from "@/stores/showDays.store.ts";
import {useShowDaysStore} from "@stores/show/showDays.store.ts";
// Props and emits
const props = defineProps({
@ -13,6 +15,11 @@ const props = defineProps({
type: Number as PropType<number | null>,
required: true,
},
showType: {
type: String as PropType<'show' | 'spot'>,
required: true,
validator: (value: string) => ['show', 'spot'].includes(value),
},
});
const emits = defineEmits(['go-back']);
@ -31,15 +38,27 @@ const showDaysStore = useShowDaysStore()
// Funcs
onMounted(async () => {
loading.value = true
if (props.showId === null ) {
// Prepare show store
if (props.showId === null) {
showStore.resetShow()
showStore.currentShow.showType = props.showType
} else {
const selectedShow = await showStore.getShow(props.showId, {withDjs: true})
const withDjs = props.showType === 'show';
const selectedShow = await showStore.getShow(props.showId, {showType: props.showType, withDjs: withDjs})
showStore.loadShow(selectedShow)
showStore.currentShow.showType = props.showType
}
usersDJs.value = await getUser({role: 'dj'});
playlists.value = await getPlaylist({});
loading.value = false
// fill store
let playlistOptions: { playlistType: 'show' | 'spot' } = { playlistType: 'spot' };
if (props.showType === 'show') {
usersDJs.value = await getUser({role: 'dj'});
playlistOptions.playlistType = 'show';
}
playlists.value = await getPlaylist(playlistOptions);
loading.value = false;
})
const toggleShowScheduleForm = () => {
@ -105,7 +124,7 @@ const createShow = () => {
</v-col>
<!-- URL Field -->
<v-col cols="12" md="6" lg="4">
<v-col v-if="props.showType == 'show'" cols="12" md="6" lg="4">
<v-card>
<v-text-field
v-model="showStore.currentShow.url"
@ -197,7 +216,7 @@ const createShow = () => {
<!-- TODO Instead of the dj name, obj obj is shown -->
<!-- DJs Select -->
<v-col cols="12" md="6" lg="4">
<v-col v-if="props.showType == 'show'" cols="12" md="6" lg="4">
<v-card>
<v-select
v-model="showStore.currentShow.showDjs"
@ -216,8 +235,11 @@ const createShow = () => {
<v-card-actions>
<v-btn color="accent" @click="goBack">Torna indietro</v-btn>
<v-btn v-if="showStore.currentShow.id" color="accent" @click="showStore.updateShow()" :disabled="!isFormValid" >Salva</v-btn>
<v-btn color="accent" @click="toggleShowScheduleForm" :disabled="!isFormValid" >Regole di programmazione</v-btn>
<v-btn v-if="showStore.currentShow.id" color="accent" @click="showStore.updateShow()"
:disabled="!isFormValid">Salva
</v-btn>
<v-btn color="accent" @click="toggleShowScheduleForm" :disabled="!isFormValid">Regole di programmazione
</v-btn>
</v-card-actions>
</v-form>
</template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import ShowStartEndTime from "@partials/fields/show/ShowStartEndTime.vue";
import {useShowInstanceStore} from "@/stores/showInstance.store.ts";
import {useShowInstanceStore} from "@stores/show/showInstance.store.ts";
import {onMounted, ref, watch, type PropType} from "vue";
import {baseShowInstance, type ShowInstance} from "@models/show/showInstance.ts";
import {extractTime} from "@/helpers/DateFormatter.ts";
@ -8,7 +8,7 @@ import {getPlaylistContent} from "@models/playlist.ts";
import Sources from "@partials/Sources.vue";
import TrackList from "@partials/TrackList.vue";
import {DateTime} from "luxon";
import {useShowStore} from "@/stores/show.store.ts";
import {useShowStore} from "@stores/show/show.store.ts";
const emits = defineEmits(['toggle-menu-edit-instance']);
// Props

View file

@ -3,7 +3,7 @@ import {onMounted, type PropType, ref, watch} from 'vue';
import {showRepetitionData} from "@models/show/ShowRepetition.ts";
import DaysCheckbox from "@partials/fields/show/DaysCheckbox.vue";
import ShowStartEndTime from "@partials/fields/show/ShowStartEndTime.vue";
import {useShowDaysStore} from "@/stores/showDays.store.ts";
import {useShowDaysStore} from "@stores/show/showDays.store.ts";
// Emits and props
const emits = defineEmits(['toggle-show-schedule-form', 'trigger-show-creation'])

View file

@ -11,19 +11,17 @@ const color = ref("secondary");
const isOnAir = async (): void => {
const now = DateTime.now().setZone(import.meta.env.VITE_APP_TIMEZONE).toISO();
return await axios.get(`/api/v2/schedule`, {
auth: {
username: auth.userData.user.login,
password: auth.userData.password
},
return await axios.get(`/schedule`, {
params: {
ends_after: now,
starts_before: now,
ends: now,
start: now,
}
}).then((response: AxiosResponse) => {
if (typeof response.data === Array && response.data.length > 0) {
color.value = 'error';
}
}).catch((error) => {
console.error(error)
})
}

View file

@ -1,18 +1,16 @@
<script setup lang="ts">
import {useRouter} from "vue-router";
import {useAuthStore} from "@/stores/auth.store.ts";
import axios from "axios";
const router = useRouter();
const auth = useAuthStore();
const userInfo = auth.userData;
if (!userInfo) {
router.push('/login');
}
const userName = auth.userData.login;
const logout = () => {
auth.logout();
router.push('/login');
}
const logout = async () => {
await auth.logout()
router.push({ path: 'login' });
};
</script>
@ -20,16 +18,17 @@ const logout = () => {
<v-sheet
:width="150"
>
<v-btn color="info">{{ userName }} {{ $t('header.userinfo.info') }}</v-btn>
<v-btn color="info" :to="{ path: 'user-profile'}">{{ userName }} {{ $t('header.userinfo.info') }}</v-btn>
<v-btn color="" @click="logout">{{ $t('header.userinfo.logout') }}</v-btn>
</v-sheet>
</template>
<style scoped>
div button {
width: 100%;
}
div button:not(:first-child) {
margin-top: 2px;
}
div button {
width: 100%;
}
div button:not(:first-child) {
margin-top: 2px;
}
</style>

View file

@ -1,5 +1,6 @@
import axios from "axios";
import {ref, reactive, computed} from "vue";
import {useShowTypeStore} from "@stores/showType.store.ts";
export function archive_page() {
const items = ref([])
@ -13,6 +14,7 @@ export function archive_page() {
'total_items': 0,
'page': 1,
})
const showTypeStore = useShowTypeStore();
const headers = [
// {title: '', key: 'artwork'},
@ -30,12 +32,14 @@ export function archive_page() {
*/
const getItems = async (page_info) => {
loading.value = true;
let options = {
page: page_info.page,
per_page: page_info.itemsPerPage,
all: search.value
}
options['track_type'] = showTypeStore.currentType
return await axios.get(`/file`, {
params: {
page: page_info.page,
per_page: page_info.itemsPerPage,
all: search.value
}
params: options
}).then((response) => {
//console.log(response)
listData.itemsPerPage = response.data.per_page;

View file

@ -9,6 +9,7 @@ export interface User {
lastName: string;
email?: boolean;
cellPhone?: boolean;
timezone?: string;
show?: Show[];
}

View file

@ -40,6 +40,7 @@ export function playlist(item) {
}
// TODO playlist interface
// TODO Add filter if playlist is spot
export const getPlaylist = async (options: {
id?: number | null;
scheduled?: number | null;
@ -48,6 +49,7 @@ export const getPlaylist = async (options: {
page?: Number | null;
per_page?: Number | null;
all?: string | null;
playlistType?: 'show' | 'spot' | null;
}): Promise<any> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/playlist`, {params: filteredParams})

View file

@ -11,6 +11,7 @@ export interface Playlist {
description?: string; // Opzionale
length: number; // Durata della playlist
contents: PlaylistContent[];
playlist_type: 'show' | 'spot' | null
}
export const basePlaylist = (): Playlist => {
@ -21,6 +22,7 @@ export const basePlaylist = (): Playlist => {
description: '',
length: 0,
contents: [],
playlist_type: null
}
}

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 = {
title: {
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

@ -2,7 +2,7 @@ import type {ShowInstance} from "@models/show/showInstance.ts";
import type {ShowDays} from "@models/show/showDays";
import type {ShowDJs} from "@models/show/showDJs.ts";
import axios, {type AxiosResponse} from "axios";
import {cleanOptions} from "@/helpers/AxiosHelper.ts";
import {camelToSnake, cleanOptions} from "@/helpers/AxiosHelper.ts";
export interface Show {
id?: number;
@ -27,6 +27,9 @@ export interface Show {
showDjs?: ShowDJs[];
showInstances?: ShowInstance[];
playlist?: any;
// Extra
showType: 'show' | "spot" | null // Either show or spot
}
export const baseShow = (): Show => {
@ -46,6 +49,7 @@ export const baseShow = (): Show => {
autoplaylistRepeat: false,
showDjs: null,
showDays: null,
showType: null
}
}
@ -66,6 +70,7 @@ export const getShows = async (options: {
page?: Number | null;
per_page?: Number | null;
all?: string | null;
showType?: 'show' | 'spot' | null;
}): Promise<Show[]> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/show`, {params: filteredParams})

View file

@ -17,6 +17,7 @@ export interface SmartBlock {
contents?: SmartBlockContent[]; // Contenuti associati (opzionale)
criteria?: SmartBlockCriteria[]; // Criteri associati (opzionale)
tracks?: SmartBlockContent[];
smart_block_type: 'show' | 'spot' | null;
}
export const baseSmartBlock = (): SmartBlock => {
@ -30,6 +31,7 @@ export const baseSmartBlock = (): SmartBlock => {
contents: null,
criteria: [],
tracks: [],
smart_block_type: null,
}
}
@ -46,6 +48,7 @@ export const getSmartBlock = async (options: {
page?: Number | null;
per_page?: Number | null;
all?: string | null;
smartblockType?: 'show' | 'spot' | null;
}): Promise<SmartBlock[]> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/smartblock`, {params: filteredParams})

View file

@ -0,0 +1,96 @@
import axios, {type AxiosResponse} from "axios";
import {cleanOptions} from "@/helpers/AxiosHelper.ts";
export interface Webstream {
id?: number;
name: string;
description: string;
url: string;
length?: string;
creator_id?: number;
mtime?: Date;
utime?: Date;
lptime?: Date;
mime?: string;
}
export const baseWebstream = (): Webstream => {
return {
id: null,
name: 'test',
description: '',
url: '',
length: '00:00:00',
creator_id: null,
mtime: null,
utime: null,
lptime: null,
mime: null
}
}
export const webstreamTableHeader = [
{title: 'Nome', value: 'name'},
{title: 'Descrizione', value: 'description'},
{title: 'URL', value: 'url'},
{title: 'Durata', value: 'length'},
{title: 'Azioni', value: 'actions'}
]
export const getWebstreams = async (options: {
name?: string | null;
description?: string | null;
url?: string | null;
mime?: string | null;
page?: Number | null;
per_page?: Number | null;
}): Promise<Webstream[]> => {
const filteredParams = cleanOptions(options);
return await axios.get(`/webstream`, {params: filteredParams})
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
})
}
export const getWebstream = async (id: number): Promise<Webstream> => {
return await axios.get(`/webstream/${id}`)
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
throw error;
})
}
export const createWebstream = async (webstream: Webstream): Promise<Webstream> => {
return await axios.post('/webstream', webstream)
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
throw error;
})
}
export const updateWebstream = async (webstream: Webstream): Promise<Webstream> => {
return await axios.put(`/webstream/${webstream.id}`, webstream)
.then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
throw error;
})
}
export const deleteWebstream = async (webstreamIds: Number[]) => {
try {
for (const webstreamId of webstreamIds) {
await axios.delete(`/webstream/${webstreamId}`)
}
} catch (error) {
console.error('Error deleting webstream:', error);
throw error;
}
}

View file

@ -1,6 +1,7 @@
import {reactive, ref} from "vue";
import axios from "axios";
import {timeFormatter} from "@/helpers/TimeFormatter.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
export function playlist_page() {
const items = ref([])
@ -14,6 +15,7 @@ export function playlist_page() {
'total_items': 0,
'page': 1,
})
const showTypeStore = useShowTypeStore();
const headers = [
// {title: '', key: 'artwork'},
@ -30,7 +32,8 @@ export function playlist_page() {
params: {
page: page_info.page,
per_page: page_info.itemsPerPage,
all: search.value
all: search.value,
playlistType: showTypeStore.currentType,
}
}).then((response) => {
console.log(response)
@ -65,6 +68,8 @@ export function playlist_page() {
item
).then((response) => {
console.log(response)
}).catch((error) => {
console.error("Error: "+error);
})
}

View file

@ -0,0 +1,105 @@
import {reactive, ref} from "vue";
import axios, {type AxiosResponse} from "axios";
import {type PodcastEpisode, PodcastEpisodeTableHeader} from "@models/podcast/podcastEpisode.ts";
import {DateTime} from "luxon";
import {usePodcastStore} from "@/stores/podcast.store.ts";
export function podcast_episode_page(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 (load, podcast_id = 0) => {
if (load) {
loading.value = true;
}
return await axios.get(`/rss_podcast_load`, {
params: {
url: url
}
}).then( async (podcastEpisodesList: AxiosResponse) => {
const episodes = podcastEpisodesList.data.episodes;
items.value = episodes.map(element => {
//element.imported = -1;
element.short_description = '';
if (typeof element.description === "string") {
const arr = element.description.split(' ');
const limit = arr.length >= 20 ? 20 : arr.length;
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]);
});
}
return element;
});
if (load) {
loading.value = false;
}
return podcastEpisodesList.data.podcast.title;
}).catch((error: Error) => {
console.log("Error: " + error);
});
}
const downloadEpisode = async (podcast_id, item) => {
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);
});
}
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

@ -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

@ -1,6 +1,7 @@
import {reactive, ref} from "vue";
import axios, {type AxiosResponse} from "axios";
import {deleteShow, getShows, type Show, showTableHeader} from "@models/show/show.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
export function show_page() {
const items = ref([])
@ -15,10 +16,18 @@ export function show_page() {
'page': 1,
})
const showTypeStore = useShowTypeStore();
const headers = showTableHeader
const getItems = async (options) => {
return getShows(options).then(showList => {
const showSearchOptions = {
page: options?.page,
per_page: options?.itemsPerPage,
all: search.value,
showType: showTypeStore.currentType,
}
return getShows(showSearchOptions).then(showList => {
listData.itemsPerPage = showList.per_page;
listData.first_page = showList.from;
listData.last_page = showList.last_page;

View file

@ -4,6 +4,7 @@ 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 {useShowTypeStore} from "@stores/showType.store.ts";
export function smartblock_page() {
const items = ref([])
@ -17,6 +18,7 @@ export function smartblock_page() {
'total_items': 0,
'page': 1,
})
const showTypeStore = useShowTypeStore();
const auth = useAuthStore();
const timezone = auth.userData.timezone;
@ -28,7 +30,8 @@ export function smartblock_page() {
return getSmartBlock({
page: options.page,
per_page: options.itemsPerPage,
all: search.value
all: search.value,
smartblockType: showTypeStore.currentType,
}).then((smartblockList) => {
console.log(smartblockList)
listData.itemsPerPage = smartblockList.per_page;

View file

@ -0,0 +1,52 @@
import {reactive, ref} from "vue";
import {deleteWebstream, getWebstreams, updateWebstream, type Webstream, webstreamTableHeader} from "@models/webstream.ts";
export function webstream_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 headers = webstreamTableHeader
const getItems = async (options) => {
loading.value = true;
return getWebstreams(options).then(webstreamList => {
items.value = webstreamList.data
loading.value = false;
}).catch(error => {
console.log("Error: " + error);
loading.value = false;
})
};
const editItem = (item: Webstream) => {
return updateWebstream(item)
.then((response) => {
console.log(response);
return response;
})
.catch(error => {
console.error("Error updating webstream:", error);
throw error;
});
}
const deleteItem = async (itemId: number | null = null) => {
const webstreamId = itemId ? [itemId] : selected.value.map(item => item.id)
await deleteWebstream(webstreamId).then(async () => {
await getItems(listData)
}).catch(error => {
console.error("Error deleting webstream:", error);
})
};
return {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem}
}

View file

@ -1,11 +1,74 @@
import { createRouter, createWebHistory } from "vue-router";
import { useAuthStore } from "@/stores/auth.store.ts";
import {createRouter, createWebHashHistory, createWebHistory, type RouteRecordRaw} from "vue-router";
import {useAuthStore} from "@/stores/auth.store.ts";
import {useShowTypeStore} from '@/stores/showType.store';
const routes = [
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Dashboard',
name: 'Backoffice',
component: () => import('../pages/Backoffice.vue'),
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/components/content/Dashboard.vue')
},
{
path: 'show',
name: 'Trasmissioni',
component: () => import('@/components/content/Show.vue'),
meta: {showType: 'shows'}
},
{
path: 'archive',
name: 'Archivio',
component: () => import('@/components/content/Archive.vue')},
{
path: 'playlist',
name: 'Playlist',
component: () => import('@/components/content/Playlist.vue'),
meta: {showType: 'shows'}
},
{
path: 'blocks',
name: 'Blocchi dinamici',
component: () => import('@/components/content/SmartBlock.vue'),
meta: {showType: 'shows'}
},
{
path: 'podcast',
name: 'Podcast',
component: () => import('@/components/content/Podcast.vue')
},
{
path: 'webstream',
name: 'Webstream',
component: () => import('@/components/content/Webstream.vue')
},
{
path: 'spot',
name: 'Spot',
component: () => import('@/components/content/Show.vue'),
meta: {showType: 'spots'}
},
{
path: 'spot-playlist',
name: 'Spot playlist',
component: () => import('@/components/content/Playlist.vue'),
meta: {showType: 'spots'}
},
{
path: 'spot-blocks',
name: 'Spot Blocchi dinamici',
component: () => import('@/components/content/SmartBlock.vue'),
meta: {showType: 'spots'}
},
{
path: 'user-profile',
name: 'UserProfile',
component: () => import('@/components/content/UserProfile.vue')
},
]
},
{
path: '/login',
@ -15,23 +78,35 @@ const routes = [
];
const router = createRouter({
history: createWebHistory(),
history: createWebHashHistory(),
routes
});
export default router;
/**
* Redirect to login page if unauthenticated
* Navigation Guards
*/
router.beforeEach(async (to) => {
// redirect to login page if not logged in and trying to access a restricted page
router.beforeEach(async (to, from, next) => {
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
const auth = useAuthStore();
if (authRequired && !auth.userData) {
auth.returnUrl = to.fullPath;
return '/login';
if (authRequired && !auth.userData.login) {
return next('/login');
}
const showTypeStore = useShowTypeStore();
switch (to.meta.showType) {
case 'shows':
showTypeStore.setAsShows();
break;
case 'spots':
showTypeStore.setAsSpots();
break;
default:
showTypeStore.clearType();
}
next();
});
export default router;

View file

@ -1,12 +1,15 @@
<script setup lang="ts">
import {computed, defineAsyncComponent} from 'vue';
import {computed, defineAsyncComponent, ref, watch} from 'vue';
import { useShowTypeStore } from '@/stores/showType.store';
// Props and data
const props = defineProps({
page: Object,
});
const currentPage = computed(() => props.page.id)
const showTypeStore = useShowTypeStore();
/**
* ToDo:
*/
@ -16,14 +19,36 @@ const tabs = {
archive: defineAsyncComponent(() => import('@components/content/Archive.vue')),
playlist: defineAsyncComponent(() => import('@components/content/Playlist.vue')),
blocks: defineAsyncComponent(() => import('@components/content/SmartBlock.vue')),
webstream: defineAsyncComponent(() => import('@components/content/Webstream.vue')),
podcast: defineAsyncComponent(() => import('@components/content/Podcast.vue')),
spot: defineAsyncComponent(() => import('@components/content/Show.vue')),
'spot-playlist': defineAsyncComponent(() => import('@components/content/Playlist.vue')),
'spot-blocks': defineAsyncComponent(() => import('@components/content/SmartBlock.vue')),
}
watch(currentPage, (newVal) => {
const showTypes = ['show', 'playlist', 'blocks']
const spotTypes = ['spot', 'spot-playlist', 'spot-blocks']
switch (true) {
case showTypes.includes(newVal):
showTypeStore.setAsShows()
break
case spotTypes.includes(newVal):
showTypeStore.setAsSpots()
break
default:
showTypeStore.clearType()
break
}
}, {immediate: true})
</script>
<template>
<v-col>
<keep-alive>
<Component :is="tabs[currentPage]" />
</keep-alive>
<router-view></router-view>
<!-- <keep-alive>-->
<Component :is="tabs[currentPage]" />
<!-- </keep-alive>-->
</v-col>
</template>

View file

@ -3,6 +3,7 @@ import OnAir from "@/components/header/OnAir.vue";
import Clock from "@/components/header/Clock.vue";
import Timer from "@/components/header/Timer.vue";
import UserInfo from "@/components/header/UserInfo.vue";
import {useRouter} from "vue-router";
</script>
<template>

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