feat(BE): webstream
This commit is contained in:
parent
42dcde7fc9
commit
ebe6b72efe
10 changed files with 387 additions and 75 deletions
|
@ -5,7 +5,7 @@ namespace App\Enums;
|
|||
enum PlaylistContentType: int
|
||||
{
|
||||
case audioclip = 0;
|
||||
case stream = 1;
|
||||
case webstream = 1;
|
||||
case block = 2;
|
||||
|
||||
public static function fromName(string $name){
|
||||
|
|
33
app/Filters/Webstream/WebstreamFilters.php
Normal file
33
app/Filters/Webstream/WebstreamFilters.php
Normal 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));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use App\Models\File;
|
|||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistContent;
|
||||
use App\Models\SmartBlock;
|
||||
use App\Models\Webstream;
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
|
@ -41,6 +42,7 @@ class PlaylistController extends Controller
|
|||
* @throws \DateMalformedIntervalStringException
|
||||
*/
|
||||
public function store(Request $request) {
|
||||
try{
|
||||
$user = Auth::user();
|
||||
$request->validate([
|
||||
'name' => 'required',
|
||||
|
@ -66,8 +68,8 @@ class PlaylistController extends Controller
|
|||
case 'block':
|
||||
$model = SmartBlock::whereId($file['id'])->first();
|
||||
break;
|
||||
case 'stream':
|
||||
//Todo: $model = Stream::whereId($file['id'])->first();
|
||||
case 'webstream':
|
||||
$model = Webstream::whereId($file['id'])->first();
|
||||
break;
|
||||
}
|
||||
$modelTime = new LengthFormatter($model->length);
|
||||
|
@ -80,7 +82,7 @@ class PlaylistController extends Controller
|
|||
'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,
|
||||
'stream_id' => ($file['type'] === 'webstream') ? $file['id'] : null,
|
||||
'type' => PlaylistContentType::fromName($file['type']),
|
||||
'position' => $key,
|
||||
'trackoffset' => 0, //ToDo: understand this field
|
||||
|
@ -95,6 +97,9 @@ class PlaylistController extends Controller
|
|||
$dbPlaylist->update(['length' => $length]);
|
||||
|
||||
return $dbPlaylist->with('tracks')->get()->toJson();
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,7 +146,7 @@ class PlaylistController extends Controller
|
|||
];
|
||||
break;
|
||||
case 'stream':
|
||||
//Todo: $model = Stream::whereId($file['id'])->first();
|
||||
$model = Webstream::whereId($file['id'])->first();
|
||||
$data = [
|
||||
'playlist_id' => $playlist->id,
|
||||
'stream_id' => $file['id'],
|
||||
|
|
154
app/Http/Controllers/WebstreamController.php
Normal file
154
app/Http/Controllers/WebstreamController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ class PlaylistContent extends Model
|
|||
return $this->belongsTo(SmartBlock::class, 'block_id');
|
||||
}
|
||||
|
||||
public function stream() {
|
||||
//ToDo create belongsTo relationship after create stream model
|
||||
public function webstream() {
|
||||
return $this->belongsTo(Webstream::class, 'stream_id');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ class Schedule extends Model
|
|||
'broadcasted',
|
||||
'position',
|
||||
'file_id',
|
||||
'stream_id',
|
||||
'instance_id'
|
||||
];
|
||||
|
||||
|
@ -34,13 +35,18 @@ class Schedule extends Model
|
|||
'media_item_played' => 'boolean',
|
||||
];
|
||||
|
||||
public function ccFile(): BelongsTo
|
||||
public function file(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(File::class, 'cc_file_id');
|
||||
return $this->belongsTo(File::class, 'file_id');
|
||||
}
|
||||
|
||||
public function ccShowInstance(): BelongsTo
|
||||
public function webstream(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ShowInstances::class, 'cc_show_instance_id');
|
||||
return $this->belongsTo(Webstream::class, 'stream_id');
|
||||
}
|
||||
|
||||
public function showInstance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ShowInstances::class, 'show_instance_id');
|
||||
}
|
||||
}
|
||||
|
|
35
app/Models/Webstream.php
Normal file
35
app/Models/Webstream.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -50,15 +50,14 @@ class ScheduleService
|
|||
->where('ends', '<=', $showInstance->ends)
|
||||
->get()
|
||||
->keyBy(function ($item) {
|
||||
return $item->starts->timestamp . '_' . $item->ends->timestamp . '_' . $item->file_id;
|
||||
return $item->starts->timestamp . '_' . $item->ends->timestamp;
|
||||
});
|
||||
|
||||
// Compare and insert only new entries
|
||||
$newItems = collect($this->prepareScheduleData($scheduleItems))
|
||||
->filter(function ($item) use ($existingSchedule) {
|
||||
$key = Carbon::parse($item['starts'])->timestamp . '_' .
|
||||
Carbon::parse($item['ends'])->timestamp . '_' .
|
||||
$item['file_id'];
|
||||
Carbon::parse($item['ends'])->timestamp;
|
||||
return !$existingSchedule->has($key);
|
||||
})
|
||||
->toArray();
|
||||
|
@ -117,7 +116,7 @@ class ScheduleService
|
|||
$nextContent = $playlistContent[$playlistIndex];
|
||||
$contentType = $this->resolveContentType($nextContent->type);
|
||||
$content = $nextContent->$contentType()->first();
|
||||
$nextLength = LengthFormatter::contentLengthToSeconds($content->length);
|
||||
//$nextLength = LengthFormatter::contentLengthToSeconds($content->length);
|
||||
|
||||
$result = $this->managePlaylistContentEntry(
|
||||
$playlistContent[$playlistIndex],
|
||||
|
@ -153,19 +152,29 @@ class ScheduleService
|
|||
$contentLength = LengthFormatter::contentLengthToSeconds($content->length);
|
||||
$contentFinalLength = $this->calculateFinalLength($currentTime, $contentLength, $endTime);
|
||||
|
||||
$fileId = null;
|
||||
$webstreamId = null;
|
||||
|
||||
if ($contentType === 'file') {
|
||||
$content->update(['is_scheduled' => true]);
|
||||
$fileId = $content->id;
|
||||
}
|
||||
|
||||
if ($contentType === 'webstream') {
|
||||
$webstreamId = $content->id;
|
||||
}
|
||||
|
||||
|
||||
$schedule = $this->createScheduleItem(
|
||||
$currentTime,
|
||||
$contentLength,
|
||||
$contentFinalLength,
|
||||
$playlistContentEntry,
|
||||
$content->id,
|
||||
$fileId,
|
||||
$webstreamId,
|
||||
$showInstanceId
|
||||
);
|
||||
|
||||
if ($contentType === 'file') {
|
||||
$content->update(['is_scheduled' => true]);
|
||||
}
|
||||
|
||||
return [
|
||||
'currentTime' => $currentTime->addSeconds($contentLength),
|
||||
'schedule' => $schedule
|
||||
|
@ -176,7 +185,7 @@ class ScheduleService
|
|||
{
|
||||
return match (PlaylistContentType::from($type)) {
|
||||
PlaylistContentType::audioclip => 'file',
|
||||
PlaylistContentType::stream => 'stream',
|
||||
PlaylistContentType::webstream => 'webstream',
|
||||
PlaylistContentType::block => 'block',
|
||||
default => throw new InvalidArgumentException('Invalid content type'),
|
||||
};
|
||||
|
@ -194,7 +203,8 @@ class ScheduleService
|
|||
int $contentLength,
|
||||
int $contentDuration,
|
||||
PlaylistContent $playlistContentEntry,
|
||||
int $contentId,
|
||||
int | null $fileId,
|
||||
int | null $webstreamId,
|
||||
int $showInstanceId
|
||||
): Schedule {
|
||||
return new Schedule([
|
||||
|
@ -209,7 +219,8 @@ class ScheduleService
|
|||
'playout_status' => 0,
|
||||
'broadcasted' => self::BROADCAST_STATUS_PENDING,
|
||||
'position' => $playlistContentEntry->position,
|
||||
'file_id' => $contentId,
|
||||
'file_id' => $fileId,
|
||||
'stream_id' => $webstreamId,
|
||||
'instance_id' => $showInstanceId
|
||||
]);
|
||||
}
|
||||
|
@ -219,7 +230,7 @@ class ScheduleService
|
|||
return collect($scheduleItems)->map(fn (Schedule $schedule) => $schedule->only([
|
||||
'starts', 'ends', 'clip_length', 'fade_in', 'fade_out',
|
||||
'cue_in', 'cue_out', 'media_item_played', 'playout_status',
|
||||
'broadcasted', 'position', 'instance_id', 'file_id'
|
||||
'broadcasted', 'position', 'instance_id', 'file_id', 'stream_id'
|
||||
]))->toArray();
|
||||
}
|
||||
}
|
65
app/Services/WebstreamService.php
Normal file
65
app/Services/WebstreamService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -77,8 +77,11 @@ trait ShowInstancesTrait
|
|||
public function manageShowInstances(Show $show)
|
||||
{
|
||||
try {
|
||||
$generationLimitDate = Preference::where('keystr', 'shows_populated_until')->value('valstr');
|
||||
$generationLimitDate = Carbon::createFromFormat('Y-m-d H:i:s', $generationLimitDate);
|
||||
$generationLimitDateSetting = Preference::where('keystr', 'shows_populated_until')->value('valstr');
|
||||
if (empty($generationLimitDateSetting)) {
|
||||
$generationLimitDateSetting = Carbon::now()->addYears(3)->format('Y-m-d H:i:s');
|
||||
}
|
||||
$generationLimitDate = Carbon::createFromFormat('Y-m-d H:i:s', $generationLimitDateSetting);
|
||||
$showInstances = [];
|
||||
foreach ($show->showDays as $showDay) {
|
||||
try {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue