mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 04:51:17 +00:00
305 lines
11 KiB
PHP
305 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Api\Rest\V1\Controllers;
|
|
|
|
use App\Entities\Episode;
|
|
use App\Entities\Location;
|
|
use App\Entities\Podcast;
|
|
use App\Entities\Post;
|
|
use App\Models\EpisodeModel;
|
|
use App\Models\PodcastModel;
|
|
use App\Models\PostModel;
|
|
use CodeIgniter\API\ResponseTrait;
|
|
use CodeIgniter\Controller;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
use CodeIgniter\I18n\Time;
|
|
use Modules\Api\Rest\V1\Config\Services;
|
|
use Modules\Auth\Models\UserModel;
|
|
|
|
class EpisodeController extends Controller
|
|
{
|
|
use ResponseTrait;
|
|
|
|
public function __construct()
|
|
{
|
|
Services::restApiExceptions()->initialize();
|
|
}
|
|
|
|
public function list(): ResponseInterface
|
|
{
|
|
$query = $this->request->getGet('query');
|
|
$order = $this->request->getGet('order') ?? 'newest';
|
|
$podcastIds = $this->request->getGet('podcastIds');
|
|
|
|
$builder = (new EpisodeModel());
|
|
|
|
if ($podcastIds !== null) {
|
|
$builder->whereIn('podcast_id', explode(',', (string) $podcastIds));
|
|
}
|
|
|
|
if ($query !== null) {
|
|
$builder->fullTextSearch($query);
|
|
|
|
if ($order === 'search') {
|
|
$builder->orderBy('(episodes_score + podcasts_score)', 'desc');
|
|
}
|
|
}
|
|
|
|
if ($order === 'newest') {
|
|
$builder->orderBy('episodes.created_at', 'desc');
|
|
}
|
|
|
|
$data = $builder->findAll(
|
|
(int) ($this->request->getGet('limit') ?? config('RestApi')->limit),
|
|
(int) $this->request->getGet('offset')
|
|
);
|
|
|
|
array_map(static function ($episode): void {
|
|
self::mapEpisode($episode);
|
|
}, $data);
|
|
|
|
return $this->respond($data);
|
|
}
|
|
|
|
public function view(int $id): ResponseInterface
|
|
{
|
|
$episode = (new EpisodeModel())->getEpisodeById($id);
|
|
|
|
if (! $episode instanceof Episode) {
|
|
return $this->failNotFound('Episode not found');
|
|
}
|
|
|
|
// @phpstan-ignore-next-line
|
|
return $this->respond(static::mapEpisode($episode));
|
|
}
|
|
|
|
public function attemptCreate(): ResponseInterface
|
|
{
|
|
$rules = [
|
|
'created_by' => 'required|is_natural_no_zero',
|
|
'updated_by' => 'required|is_natural_no_zero',
|
|
'title' => 'required',
|
|
'slug' => 'required|max_length[128]',
|
|
'podcast_id' => 'required|is_natural_no_zero',
|
|
'audio_file' => 'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]',
|
|
'cover' => 'permit_empty|is_image[cover]|ext_in[cover,jpg,jpeg,png]|min_dims[cover,1400,1400]|is_image_ratio[cover,1,1]',
|
|
'transcript_file' => 'permit_empty|ext_in[transcript_file,srt,vtt]',
|
|
'chapters_file' => 'permit_empty|ext_in[chapters_file,json]|is_json[chapters_file]',
|
|
'transcript-choice' => 'permit_empty|in_list[upload-file,remote-url]',
|
|
'chapters-choice' => 'permit_empty|in_list[upload-file,remote-url]',
|
|
];
|
|
|
|
if (! $this->validate($rules)) {
|
|
return $this->failValidationErrors(array_values($this->validator->getErrors()));
|
|
}
|
|
|
|
$podcastId = $this->request->getPost('podcast_id');
|
|
|
|
$podcast = (new PodcastModel())->getPodcastById($podcastId);
|
|
|
|
if (! $podcast instanceof Podcast) {
|
|
return $this->failNotFound('Podcast not found');
|
|
}
|
|
|
|
$createdByUserId = $this->request->getPost('created_by');
|
|
|
|
$userModel = new UserModel();
|
|
$createdByUser = $userModel->find($createdByUserId);
|
|
|
|
if (! $createdByUser) {
|
|
return $this->failNotFound('User not found');
|
|
}
|
|
|
|
$updatedByUserId = $this->request->getPost('updated_by');
|
|
|
|
$updatedByUser = $userModel->find($updatedByUserId);
|
|
|
|
if (! $updatedByUser) {
|
|
return $this->failNotFound('Updated by user not found');
|
|
}
|
|
|
|
if ($podcast->type === 'serial' && $this->request->getPost('type') === 'full') {
|
|
$rules['episode_number'] = 'required';
|
|
}
|
|
|
|
if (! $this->validate($rules)) {
|
|
return $this->failValidationErrors(array_values($this->validator->getErrors()));
|
|
}
|
|
|
|
$validData = $this->validator->getValidated();
|
|
|
|
if ((new EpisodeModel())
|
|
->where([
|
|
'slug' => $validData['slug'],
|
|
'podcast_id' => $podcast->id,
|
|
])
|
|
->first() instanceof Episode) {
|
|
return $this->fail('An episode with the same slug already exists in this podcast.', 409);
|
|
}
|
|
|
|
$newEpisode = new Episode([
|
|
'created_by' => $createdByUserId,
|
|
'updated_by' => $updatedByUserId,
|
|
'podcast_id' => $podcast->id,
|
|
'title' => $validData['title'],
|
|
'slug' => $validData['slug'],
|
|
'guid' => null,
|
|
'audio' => $this->request->getFile('audio_file'),
|
|
'cover' => $this->request->getFile('cover'),
|
|
'description_markdown' => $this->request->getPost('description'),
|
|
'location' => in_array($this->request->getPost('location_name'), ['', null], true)
|
|
? null
|
|
: new Location($this->request->getPost('location_name')),
|
|
'parental_advisory' => $this->request->getPost('parental_advisory') !== 'undefined'
|
|
? $this->request->getPost('parental_advisory')
|
|
: null,
|
|
'number' => $this->request->getPost('episode_number') ? (int) $this->request->getPost(
|
|
'episode_number'
|
|
) : null,
|
|
'season_number' => $this->request->getPost('season_number') ? (int) $this->request->getPost(
|
|
'season_number'
|
|
) : null,
|
|
'type' => $this->request->getPost('type'),
|
|
'is_blocked' => $this->request->getPost('block') === 'yes',
|
|
'custom_rss_string' => $this->request->getPost('custom_rss'),
|
|
'is_premium' => $this->request->getPost('premium') === 'yes',
|
|
'published_at' => null,
|
|
]);
|
|
|
|
$transcriptChoice = $this->request->getPost('transcript-choice');
|
|
if ($transcriptChoice === 'upload-file') {
|
|
$newEpisode->setTranscript($this->request->getFile('transcript_file'));
|
|
} elseif ($transcriptChoice === 'remote-url') {
|
|
$newEpisode->transcript_remote_url = $this->request->getPost(
|
|
'transcript_remote_url'
|
|
) === '' ? null : $this->request->getPost('transcript_remote_url');
|
|
}
|
|
|
|
$chaptersChoice = $this->request->getPost('chapters-choice');
|
|
if ($chaptersChoice === 'upload-file') {
|
|
$newEpisode->setChapters($this->request->getFile('chapters_file'));
|
|
} elseif ($chaptersChoice === 'remote-url') {
|
|
$newEpisode->chapters_remote_url = $this->request->getPost(
|
|
'chapters_remote_url'
|
|
) === '' ? null : $this->request->getPost('chapters_remote_url');
|
|
}
|
|
|
|
$episodeModel = new EpisodeModel();
|
|
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
|
|
return $this->fail($episodeModel->errors(), 400);
|
|
}
|
|
|
|
$episode = $episodeModel->find($newEpisodeId)
|
|
->toRawArray();
|
|
|
|
return $this->respond($episode);
|
|
}
|
|
|
|
public function attemptPublish(int $id): ResponseInterface
|
|
{
|
|
$episodeModel = new EpisodeModel();
|
|
$episode = $episodeModel->getEpisodeById($id);
|
|
|
|
if (! $episode instanceof Episode) {
|
|
return $this->failNotFound('Episode not found');
|
|
}
|
|
|
|
if ($episode->publication_status !== 'not_published') {
|
|
return $this->fail('Episode is already published or scheduled for publication', 409);
|
|
}
|
|
|
|
$rules = [
|
|
'publication_method' => 'required',
|
|
'created_by' => 'required|is_natural_no_zero',
|
|
];
|
|
|
|
if (! $this->validate($rules)) {
|
|
return $this->failValidationErrors(array_values($this->validator->getErrors()));
|
|
}
|
|
|
|
if ($this->request->getPost('publication_method') === 'schedule') {
|
|
$rules['scheduled_publication_date'] = 'required|valid_date[Y-m-d H:i]';
|
|
}
|
|
|
|
if (! $this->validate($rules)) {
|
|
return $this->failValidationErrors(array_values($this->validator->getErrors()));
|
|
}
|
|
|
|
$createdByUserId = $this->request->getPost('created_by');
|
|
|
|
$userModel = new UserModel();
|
|
$createdByUser = $userModel->find($createdByUserId);
|
|
|
|
if (! $createdByUser) {
|
|
return $this->failNotFound('User not found');
|
|
}
|
|
|
|
$validData = $this->validator->getValidated();
|
|
|
|
$db = db_connect();
|
|
$db->transStart();
|
|
|
|
$newPost = new Post([
|
|
'actor_id' => $episode->podcast->actor_id,
|
|
'episode_id' => $episode->id,
|
|
'message' => $this->request->getPost('message') ?? '',
|
|
'created_by' => $createdByUserId,
|
|
]);
|
|
|
|
$clientTimezone = $this->request->getPost('client_timezone') ?? app_timezone();
|
|
|
|
if ($episode->podcast->publication_status === 'published') {
|
|
$publishMethod = $validData['publication_method'];
|
|
if ($publishMethod === 'schedule') {
|
|
$scheduledPublicationDate = $validData['scheduled_publication_date'] ?? null;
|
|
if ($scheduledPublicationDate) {
|
|
$episode->published_at = Time::createFromFormat(
|
|
'Y-m-d H:i',
|
|
$scheduledPublicationDate,
|
|
$clientTimezone
|
|
)->setTimezone(app_timezone());
|
|
} else {
|
|
$db->transRollback();
|
|
return $this->fail('Scheduled publication date is required', 400);
|
|
}
|
|
} else {
|
|
$episode->published_at = Time::now();
|
|
}
|
|
} elseif ($episode->podcast->publication_status === 'scheduled') {
|
|
// podcast publication date has already been set
|
|
$episode->published_at = $episode->podcast->published_at->addSeconds(1);
|
|
} else {
|
|
$episode->published_at = Time::now();
|
|
}
|
|
|
|
$newPost->published_at = $episode->published_at;
|
|
|
|
$postModel = new PostModel();
|
|
if (! $postModel->addPost($newPost)) {
|
|
$db->transRollback();
|
|
return $this->fail($postModel->errors(), 400);
|
|
}
|
|
|
|
if (! $episodeModel->update($episode->id, $episode)) {
|
|
$db->transRollback();
|
|
return $this->fail($episodeModel->errors(), 400);
|
|
}
|
|
|
|
$db->transComplete();
|
|
|
|
// @phpstan-ignore-next-line
|
|
return $this->respond(self::mapEpisode($episode));
|
|
}
|
|
|
|
protected static function mapEpisode(Episode $episode): Episode
|
|
{
|
|
$episode->cover_url = $episode->getCover()
|
|
->file_url;
|
|
$episode->duration = round($episode->audio->duration);
|
|
|
|
return $episode;
|
|
}
|
|
}
|