Compare commits

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

39 commits
main ... dev

Author SHA1 Message Date
95a50531da Fix (fe): fixing issue #23 and #26, added close button that when clicked reload the table items 2025-07-22 17:01:55 +02:00
ae049b7966 Fix (fe): fixing issue #18, adding h1 title to pages 2025-07-22 15:50:08 +02:00
3fe6ec2901 Fix (fe): fixing issue #15, changing application font to Atkinson Hyperlegible 2025-07-21 16:08:09 +02:00
be77593531 Fix (fe): fixing issue #13, sidebar button gap 2025-07-21 15:42:18 +02:00
9657b843fe Fix (fe): fixing issue #11, sidebar typography 2025-07-21 15:34:43 +02:00
51526c9889 Feat (fe): added Sintonia logo to login page and changed colors in the app 2025-07-21 15:24:12 +02:00
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
126 changed files with 4691 additions and 1140 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@
namespace App\Filters; namespace App\Filters;
use App\Filters\FiltersType\AllFilter; use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\IsFilter;
use App\Filters\FiltersType\LikeFilter; use App\Filters\FiltersType\LikeFilter;
class FileFilters 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; namespace App\Filters\Show;
use App\Filters\FiltersType\AllFilter;
use App\Filters\FiltersType\LikeFilter; use App\Filters\FiltersType\LikeFilter;
use App\Models\Spot\SpotPlaylist;
class ShowFilters class ShowFilters
{ {
protected $filters = [ protected $filters = [
'name' => LikeFilter::class, 'name' => LikeFilter::class,
'dj' => LikeFilter::class, 'all' => AllFilter::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,
]; ];
public function apply($query) public function apply($query, $filters)
{ {
foreach ($this->receivedFilters() as $name => $value) { foreach ($this->receivedFilters($filters) as $name => $value) {
if ($name != 'per_page') { switch ($name) {
$filterInstance = new $this->filters[$name]; case 'all':
$query = $filterInstance($query, $name, $value); $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; 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\Lib\RabbitMQSender;
use App\Models\File; use App\Models\File;
use App\Models\TrackType; use App\Models\TrackType;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class FileController extends Controller class FileController extends Controller
@ -27,11 +29,17 @@ class FileController extends Controller
$pagination = $request->per_page; $pagination = $request->per_page;
} }
return File::searchFilter($request) $files = File::searchFilter($request)
->where('import_status', '=', 0) ->where('import_status', '=', 0)
->with('track_type') ->with('track_type')
->with('owner') ->with('owner');
->orderBy('artist_name')
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) ->paginate($pagination)
->toJson(); ->toJson();
} }
@ -48,9 +56,19 @@ class FileController extends Controller
$user = Auth::user(); $user = Auth::user();
$apiKey = $request->header('php-auth-user');
//Accept request only from logged-in users //Accept request only from logged-in users
if (!$user) { if (!$user) {
throw new \Exception("You must be logged in"); if ($apiKey != 'some_secret_api_key') {
throw new \Exception("You must be logged in");
}
//ToDo: check how to work in Legacy, getting user in this way is quite horrible
try {
$user = User::where('type','=','P')->orderBy('id','ASC')->first();
} catch (\Exception $e) {
Log::error($e->getMessage());
}
} }
//Mime type list: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types //Mime type list: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
@ -132,6 +150,11 @@ class FileController extends Controller
$fields['track_type_id'] = TrackType::where('code','MUSICA')->first()->id; $fields['track_type_id'] = TrackType::where('code','MUSICA')->first()->id;
} }
//Force BPM to int
if (isset($fields['bpm'])) {
$fields['bpm'] = intval($fields['bpm']);
}
//return json_encode(['req' => $fields,'id' => $id]); //return json_encode(['req' => $fields,'id' => $id]);
$file->fill($fields)->save(); $file->fill($fields)->save();

View file

@ -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\Playlist;
use App\Models\PlaylistContent; use App\Models\PlaylistContent;
use App\Models\SmartBlock; use App\Models\SmartBlock;
use App\Models\Spot\SpotPlaylist;
use App\Models\Webstream;
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PlaylistController extends Controller class PlaylistController extends Controller
{ {
@ -21,17 +24,37 @@ class PlaylistController extends Controller
* @return string * @return string
*/ */
public function index(Request $request) { public function index(Request $request) {
if (!isset($request->per_page) || is_null($request)) { try {
$pagination = 5; if ( ! isset($request->per_page) || is_null($request)) {
} else { $pagination = 5;
$pagination = $request->per_page; } else {
} $pagination = $request->per_page;
}
return Playlist::searchFilter($request) $playlists = Playlist::searchFilter($request)
->with(['creator', 'tracks.file', 'tracks.block', 'tracks.block.criteria', 'tracks.block.creator']) ->with(
->orderBy('name') [
->paginate($pagination) 'creator',
->toJson(); '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 * @throws \DateMalformedIntervalStringException
*/ */
public function store(Request $request) { public function store(Request $request) {
$user = Auth::user(); try{
$request->validate([ $user = Auth::user();
'name' => 'required', $request->validate([
'tracks' => 'required|array' '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',
]); ]);
$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; break;
case 'stream': case 'stream':
//Todo: $model = Stream::whereId($file['id'])->first(); $model = Webstream::whereId($file['id'])->first();
$data = [ $data = [
'playlist_id' => $playlist->id, 'playlist_id' => $playlist->id,
'stream_id' => $file['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\Requests\ScheduleRequest;
use App\Http\Resources\ScheduleResource; use App\Http\Resources\ScheduleResource;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Http\Request;
class ScheduleController extends Controller 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) public function store(ScheduleRequest $request)

View file

@ -25,15 +25,25 @@ class ShowController extends Controller
use ShowInstancesTrait; use ShowInstancesTrait;
use ShowDjTrait; use ShowDjTrait;
public function index(ShowFilters $filters) public function index(Request $request)
{ {
if ( ! isset($filters->per_page) || is_null($filters)) { $pagination = 20;
$pagination = 20;
} else { if(isset($request->per_page) || is_null($request)) {
$pagination = $filters->per_page; $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\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class SmartBlockController extends Controller class SmartBlockController extends Controller
{ {
@ -29,9 +30,17 @@ class SmartBlockController extends Controller
} else { } else {
$pagination = $request->per_page; $pagination = $request->per_page;
} }
$smartblocks = SmartBlock::searchFilter($request)
->with(['creator', 'tracks', 'tracks.file', 'criteria']);
return SmartBlock::searchFilter($request) if($request->has('smartblockType')) {
->with(['creator', 'tracks', 'tracks.file', 'criteria']) $smartblockType = $request->get('smartblockType');
$smartblocks = ($smartblockType == 'show')
? $smartblocks->doesntHave('spotSmartblock')
: $smartblocks->has('spotSmartblock');
}
return $smartblocks
->paginate($pagination) ->paginate($pagination)
->toJson(); ->toJson();
} }
@ -71,37 +80,37 @@ class SmartBlockController extends Controller
* @return mixed string * @return mixed string
*/ */
public function save(Request $request) { public function save(Request $request) {
$user = Auth::user(); try {
//dd($user); $user = Auth::user();//dd($user);
$request->validate([ $request->validate([
'name' => 'required|string', 'name' => 'required|string',
'type' => 'required|string', 'type' => 'required|string',
'criteria' => 'required|array' '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); return $dbSmartBlock->toJson();
$length = 0; } catch (\Exception $e) {
Log::error($e);
$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();
} }
/** /**

View file

@ -2,30 +2,25 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Filters\Show\ShowFilters; use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Http\Controllers\Controller;
use App\Http\Requests\ShowRequest;
use App\Http\Resources\ShowResource;
use App\Models\Show\Show; use App\Models\Show\Show;
use App\Models\User; use App\Models\User;
use App\Traits\ScheduleTrait; use Illuminate\Http\JsonResponse;
use App\Traits\Show\ShowDaysTrait;
use App\Traits\Show\ShowDjTrait;
use App\Traits\Show\ShowInstancesTrait;
use App\Traits\Show\ShowTrait;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Exception; 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 class UserController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$queryParams = collect($request->except('withShow')); $queryParams = collect($request->except('withShow'));
$userFilter = (new User())->searchFilter($queryParams); $userFilter = (new User())->searchFilter($queryParams);
if($request->withShow) $userFilter = $userFilter->with('showDjs'); if ($request->withShow) {
$userFilter = $userFilter->with('showDjs');
}
return response()->json($userFilter->get()); return response()->json($userFilter->get());
} }
@ -41,22 +36,56 @@ class UserController extends Controller
$show = Show::firstOrCreate($showInfos); $show = Show::firstOrCreate($showInfos);
$this->manageShowDays($show, $showDaysRules); $this->manageShowDays($show, $showDaysRules);
$this->manageShowDjs($showDjs, $show); $this->manageShowDjs($showDjs, $show);
}catch(Exception $e){ } catch (Exception $e) {
return response()->json(['message' => $e->getMessage()], 500); return response()->json(['message' => $e->getMessage()], 500);
} }
return response()->json(['message' => 'Show created successfully']); 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) public function destroy(Request $request)
@ -71,10 +100,4 @@ class UserController extends Controller
return response()->json(['message' => $responseMessage]); 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 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) public function toResponse($request)
{ {
$user = $request->user(); $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); 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; namespace App\Models;
use App\Filters\PlaylistFilter; use App\Filters\PlaylistFilter;
use App\Models\Spot\SpotPlaylist;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use MusicBrainz\Value\Track; use MusicBrainz\Value\Track;
@ -30,6 +31,10 @@ class Playlist extends Model
return $this->hasMany(PlaylistContent::class); return $this->hasMany(PlaylistContent::class);
} }
public function spotPlaylist(){
return $this->hasOne(SpotPlaylist::class, 'playlist_id');
}
public function scopeSearchFilter($query, $request) { public function scopeSearchFilter($query, $request) {
$filters = new PlaylistFilter(); $filters = new PlaylistFilter();
return $filters->apply($query, $request); return $filters->apply($query, $request);

View file

@ -40,7 +40,7 @@ class PlaylistContent extends Model
return $this->belongsTo(SmartBlock::class, 'block_id'); return $this->belongsTo(SmartBlock::class, 'block_id');
} }
public function stream() { public function webstream() {
//ToDo create belongsTo relationship after create stream model 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; namespace App\Models;
use App\Filters\ScheduleFilters;
use App\Models\ShowInstances\ShowInstances; use App\Models\ShowInstances\ShowInstances;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,6 +26,7 @@ class Schedule extends Model
'broadcasted', 'broadcasted',
'position', 'position',
'file_id', 'file_id',
'stream_id',
'instance_id' 'instance_id'
]; ];
@ -34,13 +36,23 @@ class Schedule extends Model
'media_item_played' => 'boolean', '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; namespace App\Models\Show;
use App\Filters\Show\ShowFilters;
use App\Models\Playlist; use App\Models\Playlist;
use App\Models\ShowInstances\ShowInstances; use App\Models\ShowInstances\ShowInstances;
use App\Models\SmartBlock; use App\Models\SmartBlock;
use App\Models\Spot\SpotShow;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Show extends Model class Show extends Model
{ {
@ -66,13 +66,18 @@ class Show extends Model
return $this->hasMany(ShowInstances::class, 'show_id'); 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() public function playlist()
{ {
return $this->belongsTo(Playlist::class, 'autoplaylist_id', 'id'); 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; namespace App\Models;
use App\Filters\PlaylistFilter; use App\Filters\PlaylistFilter;
use App\Models\Spot\SpotSmartBlock;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -41,4 +42,8 @@ class SmartBlock extends Model
$filters = new PlaylistFilter(); $filters = new PlaylistFilter();
return $filters->apply($query, $request); 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 Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Filters\UserFilters; use App\Filters\UserFilters;
use App\Helpers\Preferences;
use App\Models\Show\ShowHosts; use App\Models\Show\ShowHosts;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@ -26,10 +27,16 @@ class User extends Authenticatable
protected $fillable = [ protected $fillable = [
'login', 'login',
'email', 'email',
'first_name',
'last_name',
'cell_phone',
'pass', 'pass',
'type' 'type',
'timezone',
]; ];
protected $appends = ['timezone'];
/** /**
* The attributes that should be hidden for serialization. * The attributes that should be hidden for serialization.
* *
@ -87,6 +94,24 @@ class User extends Authenticatable
parent::setAttribute($key, $value); 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. * Specify the password column for authentication.
*/ */
@ -113,4 +138,9 @@ class User extends Authenticatable
{ {
return $this->hasMany(ShowHosts::class, 'subjs_id'); 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 Illuminate\Support\Str;
use Laravel\Fortify\Fortify; use Laravel\Fortify\Fortify;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract; use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\LogoutResponse;
class FortifyServiceProvider extends ServiceProvider class FortifyServiceProvider extends ServiceProvider
{ {
@ -32,11 +33,20 @@ class FortifyServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$this->app->singleton(LoginResponseContract::class, LoginResponse::class); $this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(LogoutResponse::class, LogoutResponse::class);
Fortify::createUsersUsing(CreateNewUser::class); Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class); Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::authenticateUsing([LoginUser::class, 'login']); 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) { RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip()); $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());

View file

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

View file

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

View file

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

View file

@ -29,6 +29,6 @@ return [
'max_age' => 0, '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 return new class extends Migration
{ {
/**
* Run the migrations.
*/
public function up(): void public function up(): void
{ {
Schema::table('cc_subjs', function (Blueprint $table) { Schema::table('cc_subjs', function (Blueprint $table) {
$table->rememberToken(); if (!Schema::hasColumn('cc_subjs', 'remember_token')) {
$table->timestamps(); $table->rememberToken();
$table->string('email_verified_at')->nullable(); }
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 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 public function up(): void
{ {
Schema::create('wa_password_reset_tokens', function (Blueprint $table) { if ( ! Schema::hasTable('wa_password_reset_tokens')) {
$table->string('email')->primary(); Schema::create('wa_password_reset_tokens', function (Blueprint $table) {
$table->string('token'); $table->string('email')->primary();
$table->timestamp('created_at')->nullable(); $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 = Schema::connection($this->getConnection());
$schema->create('telescope_entries', function (Blueprint $table) { if (!$schema->hasTable('telescope_entries')) {
$table->bigIncrements('sequence'); $schema->create('telescope_entries', function (Blueprint $table) {
$table->uuid('uuid'); $table->bigIncrements('sequence');
$table->uuid('batch_id'); $table->uuid('uuid');
$table->string('family_hash')->nullable(); $table->uuid('batch_id');
$table->boolean('should_display_on_index')->default(true); $table->string('family_hash')->nullable();
$table->string('type', 20); $table->boolean('should_display_on_index')->default(true);
$table->longText('content'); $table->string('type', 20);
$table->dateTime('created_at')->nullable(); $table->longText('content');
$table->dateTime('created_at')->nullable();
$table->unique('uuid'); $table->unique('uuid');
$table->index('batch_id'); $table->index('batch_id');
$table->index('family_hash'); $table->index('family_hash');
$table->index('created_at'); $table->index('created_at');
$table->index(['type', 'should_display_on_index']); $table->index(['type', 'should_display_on_index']);
}); });
}
$schema->create('telescope_entries_tags', function (Blueprint $table) { if (!$schema->hasTable('telescope_entries_tags')) {
$table->uuid('entry_uuid'); $schema->create('telescope_entries_tags', function (Blueprint $table) {
$table->string('tag'); $table->uuid('entry_uuid');
$table->string('tag');
$table->primary(['entry_uuid', 'tag']); $table->primary(['entry_uuid', 'tag']);
$table->index('tag'); $table->index('tag');
$table->foreign('entry_uuid') $table->foreign('entry_uuid')
->references('uuid') ->references('uuid')
->on('telescope_entries') ->on('telescope_entries')
->onDelete('cascade'); ->onDelete('cascade');
}); });
}
$schema->create('telescope_monitoring', function (Blueprint $table) { if (!$schema->hasTable('telescope_monitoring')) {
$table->string('tag')->primary(); $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 public function up(): void
{ {
Schema::create('wa_failed_jobs', function (Blueprint $table) { if (!Schema::hasTable('wa_failed_jobs')) {
$table->id(); Schema::create('wa_failed_jobs', function (Blueprint $table) {
$table->string('uuid')->unique(); $table->id();
$table->text('connection'); $table->string('uuid')->unique();
$table->text('queue'); $table->text('connection');
$table->longText('payload'); $table->text('queue');
$table->longText('exception'); $table->longText('payload');
$table->timestamp('failed_at')->useCurrent(); $table->longText('exception');
}); $table->timestamp('failed_at')->useCurrent();
});
}
} }
/** /**

View file

@ -1,33 +1,136 @@
<?php <?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration return new class extends Migration
{ {
/**
* Run the migrations.
*/
public function up(): void public function up(): void
{ {
Schema::create('wa_personal_access_tokens', function (Blueprint $table) { $teams = config('permission.teams');
$table->id(); $tableNames = config('permission.table_names');
$table->morphs('tokenable'); $columnNames = config('permission.column_names');
$table->string('name'); $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$table->string('token', 64)->unique(); $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable(); if (empty($tableNames)) {
$table->timestamp('expires_at')->nullable(); throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
$table->timestamps(); }
}); 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 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\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration {
{
/** /**
* Run the migrations. * Run the migrations.
*/ */
public function up(): void public function up(): void
{ {
Schema::create('laravelsessions', function (Blueprint $table) { if ( ! Schema::hasTable('laravelsessions')) {
$table->string('id')->primary(); Schema::create('laravelsessions', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->index(); $table->string('id')->primary();
$table->string('ip_address', 45)->nullable(); $table->unsignedBigInteger('user_id')->nullable();
$table->text('user_agent')->nullable(); $table->string('ip_address', 45)->nullable();
$table->longText('payload'); $table->text('user_agent')->nullable();
$table->integer('last_activity')->index(); $table->text('payload');
}); $table->integer('last_activity');
});
}
} }
/** /**

View file

@ -11,9 +11,15 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::table('cc_track_types', function (Blueprint $table) { if (!Schema::hasColumn('cc_track_types', 'parent_id')) {
$table->foreignId('parent_id')->after('id')->nullable()->references('id')->on('cc_track_types'); 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 public function down(): void
{ {
Schema::table('cc_track_types', function (Blueprint $table) { if (Schema::hasColumn('cc_track_types', 'parent_id')) {
$table->dropColumn('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\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
return new class extends Migration return new class extends Migration {
{
/**
* Run the migrations.
*/
public function up(): void public function up(): void
{ {
$teams = config('permission.teams'); $teams = config('permission.teams');
$tableNames = config('permission.table_names'); $tableNames = config('permission.table_names');
$columnNames = config('permission.column_names'); $columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) { 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)) { 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) { if ( ! Schema::hasTable($tableNames['permissions'])) {
//$table->engine('InnoDB'); Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id'); // permission id $table->bigIncrements('id');
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) $table->string('name');
$table->string('guard_name'); // For MyISAM use string('guard_name', 25); $table->string('guard_name');
$table->timestamps(); $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']); $table->unique(['name', 'guard_name']);
} });
}); }
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { if ( ! Schema::hasTable($tableNames['roles'])) {
$table->unsignedBigInteger($pivotPermission); 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'); if ( ! Schema::hasTable($tableNames['model_has_permissions'])) {
$table->unsignedBigInteger($columnNames['model_morph_key']); Schema::create(
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); $tableNames['model_has_permissions'],
function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->foreign($pivotPermission) $table->string('model_type');
->references('id') // permission id $table->unsignedBigInteger($columnNames['model_morph_key']);
->on($tableNames['permissions']) $table->index([$columnNames['model_morph_key'], 'model_type'],
->onDelete('cascade'); 'model_has_permissions_model_id_model_type_index');
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'], $table->foreign($pivotPermission)
'model_has_permissions_permission_model_type_primary'); ->references('id')
} else { ->on($tableNames['permissions'])
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], ->onDelete('cascade');
'model_has_permissions_permission_model_type_primary');
}
}); 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->primary(
$table->unsignedBigInteger($pivotRole); [
$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'); if ( ! Schema::hasTable($tableNames['model_has_roles'])) {
$table->unsignedBigInteger($columnNames['model_morph_key']); Schema::create(
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); $tableNames['model_has_roles'],
function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotRole) $table->string('model_type');
->references('id') // role id $table->unsignedBigInteger($columnNames['model_morph_key']);
->on($tableNames['roles']) $table->index([$columnNames['model_morph_key'], 'model_type'],
->onDelete('cascade'); 'model_has_roles_model_id_model_type_index');
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'], $table->foreign($pivotRole)
'model_has_roles_role_model_type_primary'); ->references('id')
} else { ->on($tableNames['roles'])
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], ->onDelete('cascade');
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { if ($teams) {
$table->unsignedBigInteger($pivotPermission); $table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->unsignedBigInteger($pivotRole); $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->foreign($pivotPermission) $table->primary(
->references('id') // permission id [
->on($tableNames['permissions']) $columnNames['team_foreign_key'],
->onDelete('cascade'); $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) if ( ! Schema::hasTable($tableNames['role_has_permissions'])) {
->references('id') // role id Schema::create(
->on($tableNames['roles']) $tableNames['role_has_permissions'],
->onDelete('cascade'); 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') app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key')); ->forget(config('permission.cache.key'));
} }
/**
* Reverse the migrations.
*/
public function down(): void public function down(): void
{ {
$tableNames = config('permission.table_names'); $tableNames = config('permission.table_names');
if (empty($tableNames)) { 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::dropIfExists($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']); Schema::dropIfExists($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']); Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']); Schema::dropIfExists($tableNames['roles']);
Schema::drop($tableNames['permissions']); 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');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -1,3 +1,26 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@font-face {
font-family: 'Atkinson Hyperlegible';
src: url('../fonts/AtkinsonHyperlegible-Regular.ttf');
font-weight: 1 600;
}
@font-face {
font-family: 'Atkinson Hyperlegible';
src: url('../fonts/AtkinsonHyperlegible-Bold.ttf');
font-weight: 601 900;
}
body {
background-color: #F4F4EC;
color: #141414;
font-family: "Atkinson Hyperlegible", sans-serif;
}
h1 {
font-size:2.5rem;
font-weight: bold;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

@ -68,6 +68,7 @@ const confirmDelete = (confirm, bulk) => {
}) })
} }
} }
getItems(listData)
closeDialog() closeDialog()
} }
@ -92,49 +93,53 @@ watch(search, (newValue, oldValue) => {
</script> </script>
<template> <template>
<Table <div>
:headers="visibleHeaders" <h1>Archivio</h1>
v-model:selected="selected" <Table
v-model:search="search" :headers="visibleHeaders"
:list-data="listData" v-model:selected="selected"
:items="items" v-model:search="search"
:loading="loading" :list-data="listData"
:get-items="getItems" :items="items"
:is-draggable="isDraggable" :loading="loading"
:actions="true" :get-items="getItems"
:item-value="'track_title'" :is-draggable="isDraggable"
:show-select="true" :actions="true"
@update-table="getItems" :item-value="'track_title'"
@update-search="updateSearch" :show-select="true"
@delete-item="cancel" @update-table="getItems"
@edit-item="edit" @update-search="updateSearch"
> @delete-item="cancel"
<template v-slot:header-buttons> @edit-item="edit"
<v-btn color="primary" @click="openDialog('upload')"> >
Upload <template v-slot:header-buttons>
</v-btn> <v-btn color="primary" @click="openDialog('upload')">
</template> Upload
<template v-slot:dialog> </v-btn>
<v-dialog v-model="dialog.open"> </template>
<FileUpload <template v-slot:dialog>
v-if="dialog.type === 'upload'" <v-dialog v-model="dialog.open">
@close-dialog="closeDialog" <FileUpload
/> v-if="dialog.type === 'upload'"
<FileEdit @close-dialog="closeDialog"
v-else-if="dialog.type === 'edit'" @confirm="confirmDelete"
:item="itemEdited" />
@edit-item="saveItem" <FileEdit
/> v-else-if="dialog.type === 'edit'"
<ConfirmDelete :item="itemEdited"
v-else-if="dialog.type === 'delete'" @edit-item="saveItem"
:title="dialog.title" />
:text="dialog.text" <ConfirmDelete
:bulk="bulk" v-else-if="dialog.type === 'delete'"
@confirm="confirmDelete" :title="dialog.title"
/> :text="dialog.text"
</v-dialog> :bulk="bulk"
</template> @confirm="confirmDelete"
</Table> />
</v-dialog>
</template>
</Table>
</div>
</template> </template>
<style scoped> <style scoped>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <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 {useAuthStore} from "@/stores/auth.store.ts";
import {deleteShow} from "@models/show/show.ts"; import {deleteShow} from "@models/show/show.ts";
import {baseShowInstance, deleteShowInstance, getShowInstances} from "@models/show/showInstance.ts"; import {baseShowInstance, deleteShowInstance, getShowInstances} from "@models/show/showInstance.ts";
@ -10,7 +10,7 @@ import CalendarShowEvent from "@partials/show/CalendarShowEvent.vue"
// Store // Store
const auth = useAuthStore(); const auth = useAuthStore();
const userRole = auth.userData.user.role; const userRole = auth.userData.role;
// Data // Data
const editMode = ref(false); const editMode = ref(false);
@ -76,7 +76,7 @@ const goBack = async () => {
// so reducing the network calls sent // so reducing the network calls sent
// That requires the handling of the context menu // That requires the handling of the context menu
// and the show/instance id in a different way though // and the show/instance id in a different way though
onActivated(async () => { onMounted(async () => {
await triggerFetchShowInstances(new Date()); await triggerFetchShowInstances(new Date());
intervalId = setInterval(async () => { intervalId = setInterval(async () => {
if (!isRunning.value) { if (!isRunning.value) {
@ -91,29 +91,32 @@ onDeactivated(() => {
</script> </script>
<template> <template>
<template v-if="showCreateEditMode || showInstanceCreateEditMode"> <div>
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/> <h1>Dashboard {{ (showCreateEditMode) ? ' - Crea/Modifica trasmissione' : (showInstanceCreateEditMode) ? ' - Modifica puntata' : ''}}</h1>
<ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance" <template v-if="showCreateEditMode || showInstanceCreateEditMode">
@toggle-menu-edit-instance="goBack"/> <ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/>
</template> <ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance"
<template v-else> @toggle-menu-edit-instance="goBack"/>
<v-row class="ma-4 justify-space-around"> </template>
<template v-if="userRole && (userRole == 'admin' || userRole == 'editor')"> <template v-else>
<v-btn color="primary" @click="createShow">Crea show</v-btn> <v-row class="ma-4 justify-space-around">
<v-btn color="secondary" @click="toggleEditMode"> <template v-if="userRole && (userRole == 'admin' || userRole == 'editor')">
{{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }} <v-btn color="primary" @click="createShow">Crea show</v-btn>
</v-btn> <v-btn color="secondary" @click="toggleEditMode">
</template> {{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }}
</v-row> </v-btn>
<CalendarShowEvent </template>
:edit-mode="editMode" </v-row>
:shows="shows" <CalendarShowEvent
@contextMenuEditInstance="contextMenuEditInstance" :edit-mode="editMode"
@contextMenuEditShow="contextMenuEditShow" :shows="shows"
@contextMenuDeleteInstance="contextMenuDeleteInstance" @contextMenuEditInstance="contextMenuEditInstance"
@contextMenuDeleteShow="contextMenuDeleteShow" @contextMenuEditShow="contextMenuEditShow"
/> @contextMenuDeleteInstance="contextMenuDeleteInstance"
</template> @contextMenuDeleteShow="contextMenuDeleteShow"
/>
</template>
</div>
</template> </template>
<style scoped> <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 Table from "@/components/content/partials/Table.vue";
import PlaylistEditor from "@/components/content/partials/PlaylistEditor.vue"; import PlaylistEditor from "@/components/content/partials/PlaylistEditor.vue";
import ConfirmDelete from "@/components/content/partials/dialogs/ConfirmDelete.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 {usePlaylistStore} from "@/stores/playlist.store.ts";
import {baseSmartBlock} from "@models/smartblock/smartblock.ts"; import {baseSmartBlock} from "@models/smartblock/smartblock.ts";
import {basePlaylist} from "@models/playlist/playlist.ts"; import {basePlaylist} from "@models/playlist/playlist.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
const playlistStore = usePlaylistStore(); const playlistStore = usePlaylistStore();
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = playlist_page(); const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = playlist_page();
// Props, data, stores
const itemEdited = ref({ const itemEdited = ref({
id: null id: null
}); });
const bulk = ref(false) const bulk = ref(false)
const dialog = reactive({ const dialog = reactive({
open: false, open: false,
@ -23,7 +24,9 @@ const dialog = reactive({
title: '', title: '',
text: '' text: ''
}) })
const showTypeStore = useShowTypeStore();
// Funcs
const openDialog = (type, title = '', text = '', bulk = false) => { const openDialog = (type, title = '', text = '', bulk = false) => {
dialog.open = true dialog.open = true
dialog.type = type dialog.type = type
@ -37,10 +40,11 @@ const edit = (item) => {
item = basePlaylist(); item = basePlaylist();
} }
playlistStore.loadPlaylist(item); playlistStore.loadPlaylist(item);
playlistStore.currentPlaylist.playlist_type = showTypeStore.currentType;
itemEdited.value = item; itemEdited.value = item;
} }
const save = (item) => { const save = (item) => {5
if (item.name === '') { if (item.name === '') {
//Check required fields //Check required fields
console.log('error!') console.log('error!')
@ -91,50 +95,52 @@ const resetItemEdited = () => {
watch(search, (newValue, oldValue) => { watch(search, (newValue, oldValue) => {
getItems(listData) getItems(listData)
}) })
</script> </script>
<template> <template>
<PlaylistEditor <div>
v-if="itemEdited.id !== null && !dialog.open" <h1>Playlist {{ (showTypeStore.type === 'spot') ? 'pubblicitarie' : '' }}</h1>
:item="itemEdited" <PlaylistEditor
@go-back="resetItemEdited" v-if="itemEdited.id !== null && !dialog.open"
@save-item="save" :item="itemEdited"
/> @go-back="resetItemEdited"
<Table @save-item="save"
v-else />
:headers="headers" <Table
v-model:selected="selected" v-else
v-model:search="search" :headers="headers"
:list-data="listData" v-model:selected="selected"
:items="items" v-model:search="search"
:loading="loading" :list-data="listData"
:get-items="getItems" :items="items"
:actions="true" :loading="loading"
:show-select="true" :get-items="getItems"
@update-table="getItems" :actions="true"
@update-search="updateSearch" :show-select="true"
@delete-item="cancel" @update-table="getItems"
@edit-item="edit" @update-search="updateSearch"
> @delete-item="cancel"
<template v-slot:header-buttons> @edit-item="edit"
<v-btn color="primary" @click="edit(0)"> >
Crea una nuova Playlist <template v-slot:header-buttons>
</v-btn> <v-btn color="primary" @click="edit(0)">
</template> Crea una nuova Playlist
<template v-slot:dialog> </v-btn>
<v-dialog v-model="dialog.open"> </template>
<ConfirmDelete <template v-slot:dialog>
v-if="dialog.type === 'delete'" <v-dialog v-model="dialog.open">
:title="dialog.title" <ConfirmDelete
:text="dialog.text" v-if="dialog.type === 'delete'"
:bulk="bulk" :title="dialog.title"
@confirm="confirmDelete" :text="dialog.text"
@after-leave="closeDialog" :bulk="bulk"
/> @confirm="confirmDelete"
</v-dialog> @after-leave="closeDialog"
</template> />
</Table> </v-dialog>
</template>
</Table>
</div>
</template> </template>
<style scoped> <style scoped>

View file

@ -0,0 +1,171 @@
<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>
<div>
<h1>Podcast</h1>
<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>
</div>
</template>
<style scoped>
</style>

View file

@ -1,13 +1,15 @@
<script setup lang="ts"> <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 Table from "@partials/Table.vue";
import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue"; import ConfirmDelete from "@partials/dialogs/ConfirmDelete.vue";
import {show_page} from "@/composables/content/show/show_page.ts"; import {show_page} from "@/composables/content/show/show_page.ts";
import ShowForm from "@partials/show/ShowForm.vue"; import ShowForm from "@partials/show/ShowForm.vue";
import {baseShow, type Show} from "@models/show/show"; 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 {items, listData, headers, selected, loading, search, getItems, editItem, deleteItem} = show_page()
const showTypeStore = useShowTypeStore();
const showCreateEditMode = ref(false); const showCreateEditMode = ref(false);
let showSelected = ref<Number | null>(null); let showSelected = ref<Number | null>(null);
@ -23,6 +25,7 @@ const dialog = reactive({
text: '' text: ''
}) })
// Funcs
const openDialog = (type, title: string = '', text: string = '') => { const openDialog = (type, title: string = '', text: string = '') => {
dialog.open = true dialog.open = true
dialog.type = type dialog.type = type
@ -46,8 +49,8 @@ const saveItem = (item) => {
} }
const cancel = (item) => { const cancel = (item) => {
let deleteMessage = 'Vuoi cancellare lo show selezionato?' let deleteMessage = `Vuoi cancellare lo ${showTypeStore.currentType} selezionato?`
if(bulk.value.state) deleteMessage = 'Vuoi cancellare gli show selezionati?' if (bulk.value.state) deleteMessage = `Vuoi cancellare gli ${showTypeStore.currentType} selezionati?`
bulk.value.items = item bulk.value.items = item
showSelected.value = item?.id showSelected.value = item?.id
openDialog( openDialog(
@ -59,7 +62,7 @@ const cancel = (item) => {
const confirmDelete = (confirm) => { const confirmDelete = (confirm) => {
if (confirm) { if (confirm) {
const showId = showSelected.value == 0 ? null : showSelected.value ; const showId = showSelected.value == 0 ? null : showSelected.value;
deleteItem(showId) deleteItem(showId)
} }
closeDialog() closeDialog()
@ -78,51 +81,56 @@ const resetItemEdited = () => {
showSelected.value = null showSelected.value = null
} }
watch(search, (newValue, oldValue) => {
const options = {...listData};
getItems(options)
})
const goBack = () => { const goBack = () => {
showCreateEditMode.value = false showCreateEditMode.value = false
showSelected.value = null showSelected.value = null
} }
watch(search, (newValue, oldValue) => {
const options = {...listData};
getItems(options)
})
</script> </script>
<template> <template>
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/> <div>
<Table <h1>
v-else {{ (showTypeStore.currentType === 'show') ? 'Trasmissioni' : 'Blocchi pubblicitari' }}
:headers="headers" </h1>
v-model:selected="selected" <ShowForm v-if="showCreateEditMode" :showId="showSelected" :showType="showTypeStore.currentType" @go-back="goBack"/>
v-model:search="search" <Table
:list-data="listData" v-else
:items="items" :headers="headers"
:loading="loading" v-model:selected="selected"
:get-items="getItems" v-model:search="search"
:actions="true" :list-data="listData"
:show-select="true" :items="items"
@update-table="getItems" :loading="loading"
@update-search="updateSearch" :get-items="getItems"
@delete-item="cancel" :actions="true"
@edit-item="edit" :show-select="true"
> @update-table="getItems"
<template v-slot:header-buttons> @update-search="updateSearch"
<v-btn color="primary" @click="create"> @delete-item="cancel"
Crea una nuova trasmissione @edit-item="edit"
</v-btn> >
</template> <template v-slot:header-buttons>
<template v-slot:dialog> <v-btn color="primary" @click="create">
<v-dialog v-model="dialog.open"> <span> {{ (showTypeStore.currentType === 'show') ? 'Crea una nuova trasmissione' : 'Crea un nuovo blocco pubblicitario' }} </span>
<ConfirmDelete </v-btn>
v-if="dialog.type === 'delete'" </template>
:title="dialog.title" <template v-slot:dialog>
:text="dialog.text" <v-dialog v-model="dialog.open">
:bulk="bulk.state" <ConfirmDelete
@confirm="confirmDelete" v-if="dialog.type === 'delete'"
@after-leave="closeDialog" :title="dialog.title"
/> :text="dialog.text"
</v-dialog> :bulk="bulk.state"
</template> @confirm="confirmDelete"
</Table> @after-leave="closeDialog"
/>
</v-dialog>
</template>
</Table>
</div>
</template> </template>

View file

@ -1,12 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import Table from "@/components/content/partials/Table.vue"; import Table from "@/components/content/partials/Table.vue";
import ConfirmDelete from "@/components/content/partials/dialogs/ConfirmDelete.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 {smartblock_page} from "@/composables/content/smartblock_page.ts";
import SmartBlockEditor from "@partials/SmartBlockEditor.vue"; import SmartBlockEditor from "@partials/SmartBlockEditor.vue";
import {useSmartBlockStore} from "@/stores/smartblock.store.ts"; import {useSmartBlockStore} from "@/stores/smartblock.store.ts";
import {baseSmartBlock} from "@models/smartblock/smartblock.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({ const props = defineProps({
hideColumns: { hideColumns: {
type: Array, type: Array,
@ -15,14 +19,9 @@ const props = defineProps({
isDraggable: { isDraggable: {
type: Boolean, type: Boolean,
required: false required: false
} },
}); });
const smartBlockStore = useSmartBlockStore()
const { items, listData, headers, selected, loading, search, getItems, editItem, deleteItem } = smartblock_page()
const itemEdited = ref({ const itemEdited = ref({
id: null id: null
}) })
@ -37,6 +36,11 @@ const dialog = reactive({
const visibleHeaders = ref(headers) const visibleHeaders = ref(headers)
const showTypeStore = useShowTypeStore();
const smartBlockStore = useSmartBlockStore()
// Funcs
const openDialog = (type, title = '', text = '') => { const openDialog = (type, title = '', text = '') => {
dialog.open = true dialog.open = true
dialog.type = type dialog.type = type
@ -49,6 +53,7 @@ const edit = (item) => {
item = baseSmartBlock(); item = baseSmartBlock();
} }
smartBlockStore.loadSmartBlock(item); smartBlockStore.loadSmartBlock(item);
smartBlockStore.currentSmartBlock.smart_block_type = showTypeStore.currentType;
itemEdited.value = item; itemEdited.value = item;
console.log(smartBlockStore) console.log(smartBlockStore)
} }
@ -72,7 +77,7 @@ const cancel = (item) => {
const confirmDelete = (confirm) => { const confirmDelete = (confirm) => {
if (confirm) { if (confirm) {
if (!bulk) { if (!bulk.value) {
deleteItem(itemEdited.value.id) deleteItem(itemEdited.value.id)
} else { } else {
itemEdited.value.forEach(el => { itemEdited.value.forEach(el => {
@ -116,47 +121,50 @@ watch(search, (newValue, oldValue) => {
</script> </script>
<template> <template>
<SmartBlockEditor <div>
v-if="itemEdited.id !== null && !dialog.open" <h1>Blocchi dinamici {{ (showTypeStore.type === 'spot') ? 'pubblicitari' : '' }}</h1>
:item="smartBlockStore.currentSmartBlock" <SmartBlockEditor
@go-back="resetItemEdited" v-if="itemEdited.id !== null && !dialog.open"
@save-item="saveItem" :item="smartBlockStore.currentSmartBlock"
/> @go-back="resetItemEdited"
<Table @save-item="saveItem"
v-else />
:headers="visibleHeaders" <Table
v-model:selected="selected" v-else
v-model:search="search" :headers="visibleHeaders"
:list-data="listData" v-model:selected="selected"
:items="items" v-model:search="search"
:loading="loading" :list-data="listData"
:get-items="getItems" :items="items"
:actions="true" :loading="loading"
:show-select="true" :get-items="getItems"
:is-draggable="isDraggable" :actions="true"
@update-table="getItems" :show-select="true"
@update-search="updateSearch" :is-draggable="isDraggable"
@delete-item="cancel" @update-table="getItems"
@edit-item="edit" @update-search="updateSearch"
> @delete-item="cancel"
<template v-slot:header-buttons> @edit-item="edit"
<v-btn color="primary" @click="edit(0)"> >
Crea un nuovo blocco dinamico <template v-slot:header-buttons>
</v-btn> <v-btn color="primary" @click="edit(0)">
</template> Crea un nuovo blocco dinamico
<template v-slot:dialog> </v-btn>
<v-dialog v-model="dialog.open"> </template>
<ConfirmDelete <template v-slot:dialog>
v-if="dialog.type === 'delete'" <v-dialog v-model="dialog.open">
:title="dialog.title" <ConfirmDelete
:text="dialog.text" v-if="dialog.type === 'delete'"
:bulk="bulk" :title="dialog.title"
@confirm="confirmDelete" :text="dialog.text"
@after-leave="closeDialog" :bulk="bulk"
/> @confirm="confirmDelete"
</v-dialog> @after-leave="closeDialog"
</template> />
</Table> </v-dialog>
</template>
</Table>
</div>
</template> </template>
<style scoped> <style scoped>

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,153 @@
<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>
<div>
<h1>Webstream</h1>
<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>
</div>
</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 {smartblock_page} from "@/composables/content/smartblock_page.ts";
import {formatFromSeconds} from "@/helpers/TimeFormatter.ts"; import {formatFromSeconds} from "@/helpers/TimeFormatter.ts";
import {useSmartBlockStore} from "@/stores/smartblock.store.ts"; import {useSmartBlockStore} from "@/stores/smartblock.store.ts";
import {baseSmartBlockCriteria} from "@models/smartblock/smartblockCriteria.ts";
const auth = useAuthStore();
const { loading, getTracklist } = smartblock_page() const { loading, getTracklist } = smartblock_page()
const emit = defineEmits([ const emit = defineEmits([
'saveItem' 'saveItem'
]) ])
//
const auth = useAuthStore();
const smartBlockStore = useSmartBlockStore(); const smartBlockStore = useSmartBlockStore();
const item = smartBlockStore.currentSmartBlock; const item = smartBlockStore.currentSmartBlock;
console.log(smartBlockStore.currentSmartBlock)
const length = ref([]);
const smartblockFields = smartblock(item) const smartblockFields = smartblock(item)
const length = ref([]);
//Is true if there is a required field empty or while saving //Is true if there is a required field empty or while saving
const disabledSaveButton = ref(true) const disabledSaveButton = ref(true)
// Funcs
const update = (list) => { const update = (list) => {
item.tracks = 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(() => { 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) => { watch(loading.value, (newVal, oldVal) => {

View file

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

View file

@ -65,12 +65,30 @@ const rehydratateTracks = () => {
track_info.block = track track_info.block = track
track_info.id = track.id 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 return track_info
}) })
} }
onMounted(() => { onMounted(() => {
rehydratateTracks() if (typeof props.tracks === "object") {
rehydratateTracks();
}
}) })
const checkMove = (e) => { 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].title = trackList.value[event.added.newIndex].name;
trackList.value[event.added.newIndex].subtitle = trackList.value[event.added.newIndex].creator.login; 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) 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({ const props = defineProps({
title: String, title: String,
text: String, text: String,
bulk: Boolean confirm_text: {
type: String,
required: false
},
cancel_text: {
type: String,
required: false
},
item: {
type: Object,
required: false
},
bulk: Boolean,
loading: {
type: Boolean,
required: false
},
hide_confirm: {
type: Boolean,
required: false
}
}) })
console.log(props.bulk) console.log(props.loading)
</script> </script>
<template> <template>
@ -15,16 +35,19 @@ console.log(props.bulk)
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<p>{{ props.text }}</p> <p>{{ props.text }}</p>
<slot></slot>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-btn <v-btn
color="primary" color="primary"
@click="$emit('confirm',true, props.bulk)" @click="$emit('confirm',true, props.bulk, item)"
:loading="loading"
v-if="hide_confirm != true"
>Conferma</v-btn> >Conferma</v-btn>
<v-btn <v-btn
color="accent" color="accent"
@click="$emit('confirm',false)" @click="$emit('confirm',false)"
>Cancella</v-btn> >Annulla</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>

View file

@ -32,6 +32,7 @@ const uploadFiles = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileToUpload.file); formData.append('file', fileToUpload.file);
formData.append('track_type_id', selectedTrackType.value.id);
const response = await uploadItem(formData); const response = await uploadItem(formData);
fileToUpload.id = response?.id; fileToUpload.id = response?.id;
@ -41,9 +42,8 @@ const uploadFiles = async () => {
); );
}); });
await Promise.all(uploadPromises); await Promise.all(uploadPromises)
//uploadingFilesNow.value = false;
uploadingFilesNow.value = false;
}; };
const checkUploadStatus = async (file) => { const checkUploadStatus = async (file) => {
@ -54,8 +54,7 @@ const checkUploadStatus = async (file) => {
}).then(response => { }).then(response => {
if (response.status === 200) { if (response.status === 200) {
if (response.data.import_status === 1) { if (response.data.import_status === 1) {
//checkUploadStatus(id)
console.log('test')
} else { } else {
file.uploadingNow = false file.uploadingNow = false
clearInterval(file.checkAnalyzer); clearInterval(file.checkAnalyzer);
@ -64,6 +63,13 @@ const checkUploadStatus = async (file) => {
}) })
} }
watch(selectedFilesMetadata, (newValue, oldValue) => {
const notUploading = newValue.every(data => data.uploadingNow)
console.log(newValue)
if (!notUploading) {
uploadingFilesNow.value = false;
}
}, {deep: true})
</script> </script>
<template> <template>
@ -107,18 +113,25 @@ const checkUploadStatus = async (file) => {
{{ track.file.name }} {{ track.file.name }}
</v-list-item> </v-list-item>
</v-list> </v-list>
{{ selectedTrackType }}
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-btn <v-btn
color="primary" color="primary"
size="large" size="large"
variant="flat"
type="submit" type="submit"
variant="elevated" :loading="uploadingFilesNow"
:disabled="selectedFiles.length === 0 || uploadingFilesNow" :disabled="selectedFiles.length === 0"
@click="uploadFiles" @click="uploadFiles"
>Carica >Carica
</v-btn> </v-btn>
<v-btn
color="secondary"
size="large"
variant="flat"
:disabled="uploadingFilesNow"
@click="$emit('confirm',false)"
>Chiudi</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import {onMounted, type PropType, ref, watch} from 'vue';
import {showRepetitionData} from "@models/show/ShowRepetition.ts"; import {showRepetitionData} from "@models/show/ShowRepetition.ts";
import DaysCheckbox from "@partials/fields/show/DaysCheckbox.vue"; import DaysCheckbox from "@partials/fields/show/DaysCheckbox.vue";
import ShowStartEndTime from "@partials/fields/show/ShowStartEndTime.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 // Emits and props
const emits = defineEmits(['toggle-show-schedule-form', 'trigger-show-creation']) 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 isOnAir = async (): void => {
const now = DateTime.now().setZone(import.meta.env.VITE_APP_TIMEZONE).toISO(); const now = DateTime.now().setZone(import.meta.env.VITE_APP_TIMEZONE).toISO();
return await axios.get(`/api/v2/schedule`, { return await axios.get(`/schedule`, {
auth: {
username: auth.userData.user.login,
password: auth.userData.password
},
params: { params: {
ends_after: now, ends: now,
starts_before: now, start: now,
} }
}).then((response: AxiosResponse) => { }).then((response: AxiosResponse) => {
if (typeof response.data === Array && response.data.length > 0) { if (typeof response.data === Array && response.data.length > 0) {
color.value = 'error'; color.value = 'error';
} }
}).catch((error) => {
console.error(error)
}) })
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ export interface SmartBlock {
contents?: SmartBlockContent[]; // Contenuti associati (opzionale) contents?: SmartBlockContent[]; // Contenuti associati (opzionale)
criteria?: SmartBlockCriteria[]; // Criteri associati (opzionale) criteria?: SmartBlockCriteria[]; // Criteri associati (opzionale)
tracks?: SmartBlockContent[]; tracks?: SmartBlockContent[];
smart_block_type: 'show' | 'spot' | null;
} }
export const baseSmartBlock = (): SmartBlock => { export const baseSmartBlock = (): SmartBlock => {
@ -30,6 +31,7 @@ export const baseSmartBlock = (): SmartBlock => {
contents: null, contents: null,
criteria: [], criteria: [],
tracks: [], tracks: [],
smart_block_type: null,
} }
} }
@ -46,6 +48,7 @@ export const getSmartBlock = async (options: {
page?: Number | null; page?: Number | null;
per_page?: Number | null; per_page?: Number | null;
all?: string | null; all?: string | null;
smartblockType?: 'show' | 'spot' | null;
}): Promise<SmartBlock[]> => { }): Promise<SmartBlock[]> => {
const filteredParams = cleanOptions(options); const filteredParams = cleanOptions(options);
return await axios.get(`/smartblock`, {params: filteredParams}) 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 {reactive, ref} from "vue";
import axios from "axios"; import axios from "axios";
import {timeFormatter} from "@/helpers/TimeFormatter.ts"; import {timeFormatter} from "@/helpers/TimeFormatter.ts";
import {useShowTypeStore} from "@stores/showType.store.ts";
export function playlist_page() { export function playlist_page() {
const items = ref([]) const items = ref([])
@ -14,6 +15,7 @@ export function playlist_page() {
'total_items': 0, 'total_items': 0,
'page': 1, 'page': 1,
}) })
const showTypeStore = useShowTypeStore();
const headers = [ const headers = [
// {title: '', key: 'artwork'}, // {title: '', key: 'artwork'},
@ -30,7 +32,8 @@ export function playlist_page() {
params: { params: {
page: page_info.page, page: page_info.page,
per_page: page_info.itemsPerPage, per_page: page_info.itemsPerPage,
all: search.value all: search.value,
playlistType: showTypeStore.currentType,
} }
}).then((response) => { }).then((response) => {
console.log(response) console.log(response)
@ -65,6 +68,8 @@ export function playlist_page() {
item item
).then((response) => { ).then((response) => {
console.log(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 }
}

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