first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
class AdminTitleTagsController extends BaseController
{
public function index(string $tagType)
{
$this->authorize('index', Title::class);
$builder = app(modelTypeToNamespace($tagType))->newQuery();
$dataSource = new Datasource($builder, request()->all());
$pagination = $dataSource->paginate();
return $this->success(['pagination' => $pagination]);
}
public function store(string $type) {
$this->authorize('store', Title::class);
$data = $this->validate(request(), [
'name' => 'required|string',
'display_name' => 'string',
]);
$tag = app(modelTypeToNamespace($type))->create($data);
return $this->success(['tag' => $tag]);
}
public function update(string $type, int $tagId) {
$this->authorize('update', Title::class);
$data = $this->validate(request(), [
'name' => 'string',
'display_name' => 'string',
]);
$tag = app(modelTypeToNamespace($type))->findOrFail($tagId);
$tag->update($data);
return $this->success(['tag' => $tag]);
}
public function destroy($type, string $ids)
{
$tagIds = explode(',', $ids);
$this->authorize('destroy', Title::class);
foreach ($tagIds as $tagId) {
$tag = app(modelTypeToNamespace($type))->findOrFail($tagId);
$tag->titles()->detach();
$tag->delete();
}
return $this->success();
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace App\Http\Controllers;
use Common\Core\BaseController;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Http;
use App\Models\Genre;
use App\Models\ProductionCountry;
use App\Models\Title;
use App\Models\Image;
use App\Models\Keyword;
use App\Models\Person;
use App\Models\Video;
use Intervention\Image\Drivers\Gd\Driver;
use Intervention\Image\ImageManager;
use Illuminate\Support\Facades\DB;
class ApiTmdbController extends BaseController
{
protected $baseUrl = 'https://api.themoviedb.org/3';
protected $token = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjYTJiNzZmNDkyZWQxNGFmMmU2Njk4N2E2YmRjZDY0ZiIsIm5iZiI6MTc0OTQ2MzY4NC41NjksInN1YiI6IjY4NDZiMjg0NDQ5MDEyOGMxMzNmYzk1NiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.PXMuq0fxXaC2kR1qrd-LDeezz0HAhL8o3-4QGaik0D4";
private array $sizes = [
'original' => null,
'large' => 500,
'medium' => 300,
'small' => 92,
];
public function index()
{
$movieId = request('movieId');
//$movieId = 1284120;
/****************************************** */
$responseMovie = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/movie/$movieId", [
'language' => 'fr-FR'
]);
$movie = $responseMovie->successful() ? $responseMovie->json() : [];
$title = Title::create([
'imdb_id' => $movie['id'],
'backdrop' => "https://image.tmdb.org/t/p/original".$movie['backdrop_path'],
'poster' => "https://image.tmdb.org/t/p/original".$movie['poster_path'],
'name' => $movie['title'],
'original_title' => $movie['original_title'],
'language' => $movie['original_language'],
'is_series' => false,
'release_date' => $movie['release_date'],
'tagline' => $movie['tagline'],
'description' => $movie['overview'],
'runtime' => $movie['runtime'],
//////////////////'certification' => $movie[''],
'budget' => $movie['budget'],
'revenue' => $movie['revenue'],
'popularity' => $movie['popularity'],
'language' => $movie['original_language'],
'adult' => $movie['adult'],
'type' => 'movie',
'tmdb_vote_average' => $movie['vote_average']
]);
$genres = $countrys = [];
foreach($movie['production_countries'] as $country)
{
$productionCountry = ProductionCountry::updateOrCreate(
['name' => $country['iso_3166_1']],
['name' => $country['iso_3166_1'], 'display_name' => $country['name']
]);
$countrys[] = $productionCountry->id;
}
foreach($movie['genres'] as $genre)
{
$genre = Genre::updateOrCreate(
['name' => $genre['name']],
[
'name' => $genre['name'],
'display_name' => $genre['name']
]);
$genres[] = $genre->id;
}
$title->productionCountries()->attach($countrys);
$title->genres()->attach($genres);
/*********************************************** */
$responseMovieKeyword = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/movie/$movieId/keywords", [
'language' => 'fr-FR'
]);
$movieKeywords = $responseMovieKeyword->successful() ? $responseMovieKeyword->json() : [];
$keywords = [];
foreach($movieKeywords['keywords'] as $valKey)
{
$keyword = Keyword::updateOrCreate(
['name' => $valKey['name']],
[
'name' => $valKey['name'],
'display_name' => $valKey['name']
]);
$keywords[] = $keyword->id;
}
$title->keywords()->attach($keywords);
/************************************************/
$responseMovieCredits = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/movie/$movieId/credits", [
'language' => 'fr-FR'
]);
$movieCredits = $responseMovieCredits->successful() ? $responseMovieCredits->json() : [];
foreach($movieCredits['cast'] as $cast)
{
$person_id = $this->getPeopleByTMDBid($cast['id']);
$title->createCredit([
'person_id'=>$person_id,
'department'=> "actors",
'job'=>"actor",
'order'=>$cast['order'],
'character'=>$cast['character'],
]);
}
foreach($movieCredits['crew'] as $cast)
{
$person_id = $this->getPeopleByTMDBid($cast['id']);
$title->createCredit([
'person_id'=>$person_id,
'department'=> $cast['department'],
'job'=>$cast['job'],
]);
}
/********************* Videos *******************************/
$responseMovieVideos = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/movie/$movieId/videos", [
'language' => 'fr-FR'
]);
$MovieVideos = $responseMovieVideos->successful() ? $responseMovieVideos->json() : [];
foreach($MovieVideos['results'] as $keyV =>$video)
{
if( ($video['site'] == "YouTube") && ($video['type'] == "Trailer") )
{
Video::create([
'name'=>$video['name'],
'src'=>"https://youtube.com/embed/".$video['key'],
'type'=>'embed',
'quality'=> ($video['size']==1080)? "1080p" : null,
'title_id'=>$title->id,
'origin'=>"local",
'downvotes'=>0,
'upvotes'=>0,
'approved'=>1,
'order'=>$keyV,
'user_id'=>auth()->id(),
'language'=>$video['iso_639_1'],
'category'=>'trailer',
]);
}
}
/***********************Images***************************/
$responseMovieImages = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/movie/$movieId/images", []);
$movieImages = $responseMovieImages->successful() ? $responseMovieImages->json() : [];
for($i=0; $i<=5; $i++)
{
$nameFileImage = isset($movieImages['backdrops'][$i]['file_path'])? $movieImages['backdrops'][$i]['file_path'] : null;
if($nameFileImage)
{
$url = $this->saveImage("https://image.tmdb.org/t/p/original$nameFileImage");
Image::where('model_type', Title::MODEL_TYPE)
->where('model_id', $title->id)
->increment('order');
Image::create([
'url' => $url,
'type' => 'backdrop',
'source' => 'local',
'model_type' => Title::MODEL_TYPE,
'model_id' => $title->id,
'order' => 0
]);
}
}
}
function getPeopleByTMDBid($tmdb_id)
{
$person = DB::table('people')->where('tmdb_id', $tmdb_id)->first();
if(!$person)
{
$responsePeople = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->token,
'Accept' => 'application/json',
])->get("$this->baseUrl/person/$tmdb_id", [
'language' => 'fr-FR'
]);
$people = $responsePeople->successful() ? $responsePeople->json() : [];
$person = Person::create([
'tmdb_id' => $people['id'],
'name' => $people['name'],
'description'=>$people['biography'],
'gender'=>$people['gender'],
'birth_date'=>$people['birthday'],
'birth_place'=>$people['place_of_birth'],
'poster'=>"https://image.tmdb.org/t/p/w500".$people['profile_path'],
'imdb_id'=>$people['imdb_id'],
'known_for'=>$people['known_for_department'],
'popularity'=>$people['popularity'],
'death_date'=>$people['deathday'],
'adult'=>$people['adult'],
]);
}
return $person->id ;
}
public function saveImage(string $urlImage): string
{
$hash = Str::random(30);
$manager = new ImageManager(new Driver());
// Télécharger l'image depuis l'URL
$imageContent = file_get_contents($urlImage);
$tempFile = tempnam(sys_get_temp_dir(), 'img');
file_put_contents($tempFile, $imageContent);
try {
$img = $manager->read($tempFile);
// Déterminer l'extension à partir de l'URL ou utiliser jpeg par défaut
$pathInfo = pathinfo(parse_url($urlImage, PHP_URL_PATH));
$extension = $pathInfo['extension'] ?? 'jpeg';
$extension = in_array(strtolower($extension), ['jpg', 'jpeg', 'png']) ? $extension : 'jpeg';
foreach ($this->sizes as $key => $size) {
if ($size) {
$img->scale($size);
}
Storage::disk('public')->put(
"media-images/backdrops/$hash/$key.$extension",
$extension === 'png' ? $img->toPng() : $img->toJpeg(),
);
}
$endpoint = config('common.site.file_preview_endpoint');
$uri = "media-images/backdrops/$hash/original.$extension";
return $endpoint
? "$endpoint/storage/$uri"
: Storage::disk('public')->url($uri);
} finally {
// Nettoyer le fichier temporaire
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Auth;
use Common\Billing\Models\Product;
use Common\Core\BaseController;
class AppHomeController extends BaseController
{
public function __invoke()
{
if (
settings('homepage.type') === 'channels' ||
(Auth::check() && settings('homepage.type') === 'landingPage')
) {
return app(FallbackRouteController::class)->renderChannel(
settings('homepage.value'),
);
} else {
return $this->renderClientOrApi([
'pageName' => 'landing-page',
'data' => [
'loader' => 'landingPage',
'products' => Product::with(['permissions', 'prices'])
->limit(15)
->orderBy('position', 'asc')
->get(),
'trendingTitles' => Title::orderBy('popularity', 'desc')
->compact()
->limit(6)
->get(),
],
]);
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use App\Models\Channel;
use Common\Core\BaseController;
use Illuminate\Support\Str;
class ChannelItemController extends BaseController
{
public function add(Channel $channel)
{
$this->authorize('update', $channel);
$data = $this->validate(request(), [
'itemId' => 'required|integer',
'itemType' => 'required|string',
]);
$relationName = Str::plural($data['itemType']);
$channel->$relationName()->sync(
[
$data['itemId'] => [
'order' => $channel->$relationName()->count() + 1,
],
],
false,
);
$channel->touch();
return $this->success(['channel' => $channel]);
}
public function remove(Channel $channel)
{
$this->authorize('update', $channel);
$data = $this->validate(request(), [
'itemId' => 'required|integer',
'itemType' => 'required|string',
]);
$relationName = Str::plural($data['itemType']);
$channel->$relationName()->detach($data['itemId']);
$channel->touch();
return $this->success(['channel' => $channel]);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers;
use App\Loaders\EpisodeLoader;
use App\Models\Episode;
use App\Models\Title;
use Common\Core\BaseController;
use Illuminate\Database\Query\Builder;
use Illuminate\Validation\Rule;
class EpisodeController extends BaseController
{
public function show()
{
$data = (new EpisodeLoader())->loadData(request('loader'));
$this->authorize('show', $data['title']);
return $this->renderClientOrApi([
'data' => $data,
'pageName' => 'episode-page',
]);
}
public function update(Title $title, int $seasonNumber, int $episodeNumber)
{
$this->authorize('update', $title);
$episode = $title
->episodes()
->where('season_number', $seasonNumber)
->where('episode_number', $episodeNumber)
->firstOrFail();
$this->validate(request(), [
'episode_number' => [
'integer',
Rule::unique('episodes')
->ignore($episode->episode_number, 'episode_number')
->where(function (Builder $query) use ($episode) {
$query
->where('season_number', $episode->season_number)
->where('title_id', $episode->title_id);
}),
],
]);
$episode->fill(request()->all())->save();
return $this->success(['episode' => $episode]);
}
public function store(Title $title, int $seasonNumber)
{
$this->authorize('update', $title);
$season = $title->findSeason($seasonNumber)->loadCount('episodes');
$this->validate(request(), [
'episode_number' => [
'integer',
Rule::unique('episodes')->where(function (Builder $query) use (
$season,
) {
$query
->where('season_number', $season->number)
->where('title_id', $season->title_id);
}),
],
]);
$epNum = request('episode_number');
if (!$epNum) {
$epNum =
$season
->episodes()
->orderBy('episode_number', 'desc')
->value('episode_number') + 1;
}
$episode = Episode::create(
array_merge(request()->all(), [
'season_number' => $season->number,
'episode_number' => $epNum,
'season_id' => $season->id,
'title_id' => $season->title_id,
]),
);
return $this->success(['episode' => $episode]);
}
public function destroy(int $id)
{
$this->authorize('destroy', Title::class);
$episode = Episode::findOrFail($id);
$episode->credits()->detach();
$episode->delete();
return $this->success();
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers;
use App\Models\Channel;
use Common\Channels\ChannelController;
use Common\Core\Controllers\HomeController;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class FallbackRouteController
{
static array $defaultRoutes = ['lists', 'titles', 'search', 'news', 'user'];
public function __invoke(string $path)
{
$parts = explode('/', $path);
if (
count($parts) > 2 ||
count($parts) < 1 ||
(count($parts) === 1 && in_array($parts[0], self::$defaultRoutes))
) {
return $this->renderClient();
}
// first try to match a channel, if none is found, fallback to rendering client side app
try {
if ($parts[0] === 'lists' && isset($parts[1])) {
request()->merge(['channelType' => 'list']);
$parts[0] = $parts[1];
$parts[1] = null;
}
if ($parts[0] === 'channel' && isset($parts[1])) {
$parts[0] = $parts[1];
$parts[1] = null;
}
$slugOrId = $parts[0];
$restriction = $parts[1] ?? null;
return $this->renderChannel($slugOrId, $restriction);
} catch (ModelNotFoundException) {
return $this->renderClient();
}
}
public function renderChannel(string $slugOrId, ?string $restriction = null)
{
$channel = app(Channel::class)->resolveRouteBinding($slugOrId);
if ($restriction) {
request()->merge(['restriction' => $restriction]);
}
return app(ChannelController::class)->show($channel);
}
public function renderClient()
{
// no need to prerender channels here, use base HomeController
request()->route()->action['uses'] = HomeController::class . '@show';
return app(HomeController::class)->show();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Common\Core\BaseController;
use DB;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ImageOrderController extends BaseController
{
public function __construct(private Title $title, private Request $request)
{
}
/**
* @param int $titleId
* @return JsonResponse
*/
public function changeOrder($titleId) {
$title = $this->title->findOrFail($titleId);
$this->authorize('update', $title);
$this->validate($this->request, [
'ids' => 'array|min:1',
'ids.*' => 'integer'
]);
$queryPart = '';
foreach($this->request->get('ids') as $order => $id) {
$queryPart .= " when id=$id then $order";
}
DB::table('images')
->whereIn('id', $this->request->get('ids'))
->update(['order' => DB::raw("(case $queryPart end)")]);
return $this->success();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\StoreMediaImageOnDisk;
use App\Models\Image;
use App\Models\Title;
use Common\Core\BaseController;
use Storage;
class ImagesController extends BaseController
{
public function store()
{
$titleId = request('titleId');
$model = app(Title::class)->findOrFail($titleId);
$this->authorize('store', $model);
$this->validate(request(), [
'file' => 'required|image|max:10240',
'titleId' => 'required|integer',
]);
$url = app(StoreMediaImageOnDisk::class)->execute(
request()->file('file'),
);
// put new image at the start of the list when sorted by "order"
Image::where('model_type', Title::MODEL_TYPE)
->where('model_id', $titleId)
->increment('order');
$image = Image::create([
'url' => $url,
'type' => 'backdrop',
'source' => 'local',
'model_type' => Title::MODEL_TYPE,
'model_id' => $titleId,
'order' => 0,
]);
return $this->success(['image' => $image]);
}
public function destroy(int $id)
{
$img = Image::findOrFail($id);
$model = app(modelTypeToNamespace($img->model_type))->findOrFail(
$img->model_id,
);
$this->authorize('destroy', $model);
if ($img->source === 'local') {
// storage/media-images/backdrops/kw4q4eg5g8q4eq6/original.jpg
$dir = str_replace('storage/', '', dirname($img->url));
if (Storage::disk('public')->exists($dir)) {
Storage::disk('public')->deleteDirectory($dir);
}
}
$img->delete();
return $this->success();
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers;
use App\Models\Person;
use App\Models\Title;
use App\Services\Data\Tmdb\TmdbApi;
use Carbon\Carbon;
use Common\Core\BaseController;
class ImportMediaController extends BaseController
{
public function importMediaItem()
{
$this->authorize('store', Title::class);
$data = $this->validate(request(), [
'media_type' => 'required|string',
'tmdb_id' => 'required|integer',
]);
if ($data['media_type'] === Person::MODEL_TYPE) {
$mediaItem = Person::withoutGlobalScope('adult')
->firstOrCreate([
'tmdb_id' => $data['tmdb_id'],
])
->maybeUpdateFromExternal([
'forceAutomation' => true,
'ignoreLastUpdate' => true,
]);
} else {
$mediaItem = Title::withoutGlobalScope('adult')
->firstOrCreate([
'tmdb_id' => $data['tmdb_id'],
'is_series' => $data['media_type'] === Title::SERIES_TYPE,
])
->maybeUpdateFromExternal([
'forceAutomation' => true,
'updateLast3Seasons' => true,
'ignoreLastUpdate' => true,
]);
}
if (!$mediaItem) {
abort(404);
}
return ['mediaItem' => $mediaItem];
}
public function importViaBrowse()
{
$this->authorize('store', Title::class);
if (!config('services.tmdb.key')) {
abort(
403,
'Enter your Themoviedb API key in settings page before importing titles.',
);
}
@set_time_limit(0);
@ini_set('memory_limit', '200M');
$tmdbParams = [
'with_release_type' => '2|3',
];
if (request('country')) {
$tmdbParams['with_origin_country'] = strtolower(request('country'));
}
if (request('language')) {
$tmdbParams['with_original_language'] = request('language');
}
if (request('min_rating')) {
$tmdbParams['vote_average.gte'] = request('min_rating');
}
if (request('max_rating')) {
$tmdbParams['vote_average.lte'] = request('max_rating');
}
if (request('genres')) {
$tmdbParams['with_genres'] = request('genres');
}
if (request('keywords')) {
$tmdbParams['with_keywords'] = request('keywords');
}
if (request('start_date') && request('end_date')) {
$tmdbParams['release_date.gte'] = Carbon::parse(
request('start_date'),
)->format('Y-m-d');
$tmdbParams['release_date.lte'] = Carbon::parse(
request('end_date'),
)->format('Y-m-d');
}
$response = app(TmdbApi::class)->browse(
request('current_page', 1),
request('type', 'movie'),
$tmdbParams,
);
$titles = $response['results']
->map(function ($result) {
return Title::withoutGlobalScope('adult')
->firstOrCreate([
'tmdb_id' => $result['tmdb_id'],
'is_series' => $result['is_series'],
])
->maybeUpdateFromExternal([
'forceAutomation' => true,
]);
})
->filter()
->values();
return [
'titles' => $titles,
'total_pages' => $response['total_pages'],
'current_page' => request('current_page', 1),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Plays\BuildPlaysReport;
use Common\Core\BaseController;
class InsightsReportController extends BaseController
{
public function __construct()
{
// will authorize based on specified model in "BuildInsightsReport"
$this->middleware('auth');
}
public function __invoke()
{
$report = app(BuildPlaysReport::class)->execute(
request()->all(),
);
return $this->success(['report' => $report]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Lists\ListsLoader;
use App\Models\Channel;
use Common\Core\BaseController;
class ListsController extends BaseController
{
public function index()
{
$this->authorize('index', [Channel::class, 'list']);
$pagination = (new ListsLoader())->allLists(request()->all());
return $this->success(['pagination' => $pagination]);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers;
use App\Actions\News\ImportNewsFromRemoteProvider;
use App\Models\NewsArticle;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Illuminate\Support\Str;
class NewsController extends BaseController
{
public function index()
{
$this->authorize('show', NewsArticle::class);
$paginator = new Datasource(NewsArticle::query(), request()->all());
$pagination = $paginator->paginate();
if (request('stripHtml') || request('truncateBody')) {
$pagination
->map(function (NewsArticle $article) {
if (request('stripHtml')) {
// remove html tags
$article->body = strip_tags($article->body);
// remove last "...see full article"
$parts = explode('...', $article->body);
if (
count($parts) > 1 &&
Str::contains(last($parts), 'See full article')
) {
array_pop($parts);
}
$article->body = implode('', $parts);
}
if ($newLength = (int) request('truncateBody')) {
$article->body = Str::limit($article->body, $newLength);
}
return $article;
})
->values();
}
return $this->success(['pagination' => $pagination]);
}
public function show($slugOrId)
{
$article = NewsArticle::where('id', $slugOrId)
->orWhere('slug', $slugOrId)
->firstOrFail();
$this->authorize('show', $article);
$data = [
'article' => $article,
'related' => NewsArticle::compact()
->where('id', '!=', $article->id)
->orderBy('created_at', 'desc')
->limit(10)
->get(),
'loader' => 'newsArticlePage',
];
return $this->renderClientOrApi([
'pageName' => 'news-article-page',
'data' => $data,
]);
}
public function update($id)
{
$article = NewsArticle::findOrFail($id);
$this->authorize('update', $article);
$data = $this->validate(request(), [
'title' => 'min:5|max:250',
'body' => 'min:5',
'image' => 'string',
'slug' => 'string',
]);
$article->fill($data)->save();
return $this->success(['article' => $article]);
}
public function store()
{
$this->authorize('store', NewsArticle::class);
$data = $this->validate(request(), [
'title' => 'required|min:5|max:250',
'body' => 'required|min:5',
'image' => 'string',
'slug' => 'required|string',
]);
$article = NewsArticle::create($data);
return $this->success(['article' => $article]);
}
public function destroy(string $ids)
{
$ids = explode(',', $ids);
$this->authorize('destroy', NewsArticle::class);
NewsArticle::whereIn('id', $ids)->delete();
return $this->success();
}
public function importFromRemoteProvider()
{
$this->authorize('store', NewsArticle::class);
app(ImportNewsFromRemoteProvider::class)->execute();
return $this->success();
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Actions\People\DeletePeople;
use App\Actions\People\GetPersonCredits;
use App\Actions\People\PaginatePeople;
use App\Jobs\IncrementModelViews;
use App\Models\Person;
use Common\Core\BaseController;
use Illuminate\Support\Arr;
class PersonController extends BaseController
{
public function index()
{
$this->authorize('index', Person::class);
$pagination = (new PaginatePeople())->execute(request()->all());
return $this->success(['pagination' => $pagination]);
}
public function show($id, $name = null)
{
$this->authorize('show', Person::class);
$loader = request('loader', 'personPage');
if (is_numeric($id) || ctype_digit($id)) {
$person = Person::findOrFail($id);
} else {
$person = Person::firstOrCreateFromEncodedTmdbId($id);
}
if ($loader === 'personPage' && requestIsFromFrontend()) {
$person->maybeUpdateFromExternal();
}
$data = array_merge(
['person' => $person, 'loader' => $loader],
app(GetPersonCredits::class)->execute($person),
);
(new IncrementModelViews())->execute($data['person']);
return $this->renderClientOrApi([
'data' => $data,
'pageName' => 'person-page',
]);
}
public function store()
{
$this->authorize('store', Person::class);
$data = request()->all();
$data['popularity'] = Arr::get($data, 'popularity') ?: 50;
$person = Person::create($data);
return $this->success(['person' => $person]);
}
public function update($id)
{
$this->authorize('update', Person::class);
$person = Person::findOrFail($id);
$data = request()->all();
$data['popularity'] = Arr::get($data, 'popularity') ?: 50;
$person->fill($data)->save();
return $this->success(['person' => $person]);
}
public function destroy(string $ids)
{
$ids = explode(',', $ids);
$this->authorize('destroy', Person::class);
app(DeletePeople::class)->execute($ids);
return $this->success();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Actions\People\GetPersonCredits;
use App\Models\Person;
use Common\Core\BaseController;
use Illuminate\Support\Arr;
class PersonCreditsController extends BaseController
{
public function fullTitleCredits(
Person $person,
int $titleId,
string $department,
) {
$this->authorize('show', Person::class);
$credits = app(GetPersonCredits::class)->execute($person, [
'titleId' => $titleId,
]);
$title = Arr::first(
$credits['credits'][$department],
fn($title) => $title['id'] === (int) $titleId,
);
return $this->success(['credits' => $title['episodes']]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\Retrieve\GetRelatedTitles;
use App\Models\Title;
use Common\Core\BaseController;
class RelatedTitlesController extends BaseController
{
public function index(int $id)
{
$this->authorize('index', Title::class);
$title = Title::with('keywords', 'genres')->findOrFail($id);
$related = app(GetRelatedTitles::class)->execute(
$title,
request()->all(),
);
return $this->success(['titles' => $related]);
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Reviews\UpdateReviewableAverageScore;
use App\Models\Review;
use Auth;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Illuminate\Support\Str;
class ReviewController extends BaseController
{
public function index()
{
$this->authorize('index', Review::class);
$builder = Review::withCount('reports')->withTextOnly();
// will need to specify this outside of filters on edit title reviews page
if (request('reviewable_id') && request('reviewable_type')) {
$builder->where([
'reviewable_id' => request('reviewable_id'),
'reviewable_type' => request('reviewable_type'),
]);
}
$datasource = new Datasource($builder, request()->all());
$order = $datasource->getOrder();
if (Str::endsWith($order['col'], 'mostHelpful')) {
$datasource->order = false;
$builder->orderByMostHelpful();
}
$pagination = $datasource->paginate()->through(function ($review) {
if ($review->relationLoaded('reviewable') && $review->reviewable) {
$normalized = $review->reviewable->toNormalizedArray();
$review->unsetRelation('reviewable');
$review->setAttribute('reviewable', $normalized);
}
return $review;
});
return $this->success(['pagination' => $pagination]);
}
public function update($id)
{
$review = Review::findOrFail($id);
$this->authorize('update', $review);
$data = request()->all();
if (isset($data['body'])) {
$data['has_text'] = true;
}
$review->fill($data)->save();
app(UpdateReviewableAverageScore::class)->execute(
$review->reviewable_id,
$review->reviewable_type,
);
return $this->success(['review' => $review]);
}
public function store()
{
$this->authorize('store', Review::class);
$data = $this->validate(request(), [
'reviewable_id' => 'required|integer',
'reviewable_type' => 'required|string',
'title' => 'string|min:10|max:150',
'body' => 'string|min:100|max:5000',
'score' => 'required|integer|min:1|max:10',
]);
$reviewableId = $data['reviewable_id'];
$reviewableType = $data['reviewable_type'];
$values = [
'score' => request('score'),
];
// don't override review body or title when only score changes
if (request('body')) {
$values['body'] = request('body');
$values['has_text'] = true;
}
if (request('title')) {
$values['title'] = request('title');
}
$review = Review::updateOrCreate(
[
'user_id' => Auth::id(),
'reviewable_type' => $reviewableType,
'reviewable_id' => $reviewableId,
],
$values,
);
$review->load('user');
app(UpdateReviewableAverageScore::class)->execute(
$reviewableId,
$reviewableType,
);
return $this->success(['review' => $review]);
}
public function destroy(string $ids)
{
$reviewIds = explode(',', $ids);
$reviews = Review::whereIn('id', $reviewIds)->get();
$this->authorize('destroy', [Review::class, $reviews]);
$reviews->each(function (Review $review) {
app(UpdateReviewableAverageScore::class)->execute(
$review->reviewable_id,
$review->reviewable_type,
);
});
Review::whereIn('id', $reviews->pluck('id'))->delete();
return $this->success();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Models\Review;
use Common\Core\BaseController;
class ReviewFeedbackController extends BaseController
{
public function store(Review $review)
{
$this->authorize('show', $review);
$data = $this->validate(request(), [
'is_helpful' => 'required|boolean',
]);
$isHelpful = $data['is_helpful'];
$review->feedback()->updateOrCreate(
[
'user_id' => auth()->id(),
],
[
'is_helpful' => $isHelpful,
],
);
$review->timestamps = false;
if ($isHelpful) {
$review->increment('helpful_count');
if ($review->not_helpful_count > 0) {
$review->decrement('not_helpful_count');
}
} else {
if ($review->helpful_count > 0) {
$review->decrement('helpful_count');
}
$review->increment('not_helpful_count');
}
return $this->success(['review' => $review->load('feedback')]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Loaders\ReviewsLoader;
use Common\Core\BaseController;
class ReviewableController extends BaseController
{
public function index()
{
$data = (new ReviewsLoader())->loadData([
'reviewableType' => request('reviewable_type'),
'reviewableId' => request('reviewable_id'),
'page' => request('page'),
'orderBy' => request('orderBy'),
'orderDir' => request('orderDir'),
'perPage' => request('perPage'),
]);
if (!$data) {
abort(404);
}
$this->authorize('show', $data['reviewable']);
return $this->success($data);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Actions\LocalSearch;
use App\Services\Data\Tmdb\TmdbApi;
use Common\Core\BaseController;
use Illuminate\Support\Collection;
use Str;
class SearchController extends BaseController
{
public function index($query)
{
$dataProvider =
request('provider') ?: settings('content.search_provider');
$results = $this->searchUsing($dataProvider, $query)
->map(function ($result) {
if (isset($result['description'])) {
$result['description'] = Str::limit(
$result['description'],
140,
);
}
return $result;
})
->values();
$data = [
'results' => $results,
'query' => trim(strip_tags($query), '"\''),
'loader' => 'searchPage',
];
return $this->renderClientOrApi([
'pageName' => 'search-page',
'data' => $data,
]);
}
private function searchUsing($provider, $query)
{
$params = request()->all();
$params['limit'] =
request('loader', 'searchPage') === 'searchPage' ? 20 : 8;
if ($provider === 'tmdb') {
return app(TmdbApi::class)->search($query, $params);
}
$results = app(LocalSearch::class)->execute($query, $params);
if ($provider === 'all') {
$tmdb = app(TmdbApi::class)->search($query, $params);
$results = $results
->concat($tmdb)
->unique(
fn($item) => ($item['tmdb_id'] ?: $item['name']) .
$item['model_type'],
)
->groupBy('model_type')
// make sure specified limit is enforced per group
// (title, person) instead of the whole collection
->map(
fn(Collection $group) => $group->slice(0, $params['limit']),
)
->flatten(1)
->sortByDesc('popularity');
}
return $results;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\DeleteSeasons;
use App\Actions\Titles\LoadSeasonEpisodeNumbers;
use App\Loaders\SeasonLoader;
use App\Models\Title;
use Common\Core\BaseController;
class SeasonController extends BaseController
{
public function show()
{
$data = (new SeasonLoader())->loadData(request('loader'));
$this->authorize('show', $data['title']);
return $this->renderClientOrApi([
'pageName' => 'season-page',
'data' => $data,
]);
}
public function store($titleId)
{
$this->authorize('update', Title::class);
$title = Title::withCount('seasons')->findOrFail($titleId);
$season = $title->seasons()->create([
'number' => $title->seasons_count + 1,
]);
return $this->success(['season' => $season]);
}
public function destroy(int $seasonId)
{
$this->authorize('update', Title::class);
app(DeleteSeasons::class)->execute([$seasonId]);
return $this->success();
}
public function episodeNumbers()
{
$titleId = request()->route('titleId');
$seasonNumber = request()->route('seasonNumber');
$episodeNumbers = (new LoadSeasonEpisodeNumbers())->execute(
$titleId,
$seasonNumber,
);
return $this->success(['episodeNumbers' => $episodeNumbers]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\Retrieve\PaginateSeasonEpisodes;
use App\Models\Title;
use Common\Core\BaseController;
class SeasonEpisodesController extends BaseController
{
public function __invoke(Title $title, int $seasonNumber)
{
$this->authorize('show', $title);
$pagination = app(PaginateSeasonEpisodes::class)->execute(
$title,
$seasonNumber,
request()->all(),
);
return $this->success(['pagination' => $pagination]);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\LoadSeasonEpisodeNumbers;
use App\Models\Season;
use App\Models\Title;
use Common\Core\BaseController;
class TitleAutocompleteController extends BaseController
{
public function __invoke()
{
$this->authorize('index', Title::class);
$search = request('searchQuery');
$selectedTitleId = request('selectedTitleId');
$seasonNumber = request('seasonNumber');
$builder = app(Title::class);
if ($search) {
$builder = $builder->search($search);
}
$results = $builder
->take(10)
->get(['id', 'name', 'poster', 'release_date']);
$results = $results->map(function (Title $title) {
$normalized = $title->toNormalizedArray();
if ($title->relationLoaded('season')) {
$normalized['episodes_count'] =
$title->season->episodes_count ?? 0;
}
$normalized['seasons_count'] = $title->seasons_count;
return $normalized;
});
if ($selectedTitleId) {
$title = Title::find($selectedTitleId);
if ($title) {
$normalizedTitle = $title->toNormalizedArray();
$normalizedTitle['seasons_count'] = Season::where(
'title_id',
$title->id,
)->count();
if ($seasonNumber) {
$normalizedTitle[
'episode_numbers'
] = (new LoadSeasonEpisodeNumbers())->execute(
$title->id,
$seasonNumber,
);
}
$results->prepend($normalizedTitle);
}
}
return ['titles' => $results];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\DeleteTitles;
use App\Actions\Titles\Retrieve\PaginateTitles;
use App\Actions\Titles\Store\StoreTitleData;
use App\Jobs\IncrementModelViews;
use App\Loaders\TitleLoader;
use App\Models\Title;
use Common\Core\BaseController;
class TitleController extends BaseController
{
public function index()
{
$this->authorize('index', Title::class);
$pagination = app(PaginateTitles::class)->execute(request()->all());
return $this->success(['pagination' => $pagination]);
}
public function show()
{
$data = (new TitleLoader())->loadData(request('loader'));
$this->authorize('show', $data['title']);
(new IncrementModelViews())->execute($data['title']);
return $this->renderClientOrApi([
'pageName' => 'title-page',
'data' => $data,
]);
}
public function update(int $id)
{
$this->authorize('update', Title::class);
$data = request()->all();
$title = Title::findOrFail($id);
$title = app(StoreTitleData::class)->execute($title, $data, [
'overrideWithEmptyValues' => true,
]);
return $this->success(['title' => $title]);
}
public function store()
{
$this->authorize('store', Title::class);
$title = Title::create(request()->all());
return $this->success(['title' => $title]);
}
public function destroy(string $ids)
{
$titleIds = explode(',', $ids);
$this->authorize('destroy', Title::class);
app(DeleteTitles::class)->execute($titleIds);
return $this->success();
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers;
use App\Models\Episode;
use App\Models\Season;
use App\Models\Title;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
class TitleCreditsController extends BaseController
{
public function index(Title $title)
{
$this->authorize('show', $title);
$model = $this->resolveCreditableModel($title, request()->all());
$builder = $model->credits();
if (request('crewOnly')) {
$builder->wherePivot('department', '!=', 'actors');
}
if ($department = request('department')) {
$builder->wherePivot('department', $department);
}
$datasource = new Datasource($builder, request()->all());
$datasource->order = false;
return $this->success(['pagination' => $datasource->paginate()]);
}
public function update(Title $title, int $pivotId)
{
$this->authorize('update', $title);
$data = $this->validate(request(), [
'character' => 'string|nullable',
'department' => 'string|nullable',
'job' => 'string|nullable',
]);
$model = $this->resolveCreditableModel($title, request()->all());
$model->updateCredit($pivotId, $data);
return $this->success();
}
public function store(Title $title)
{
$this->authorize('update', $title);
$data = $this->validate(request(), [
'person_id' => 'required|integer|exists:people,id',
'character' => 'required_if:department,cast|string',
'department' => 'required|string',
'job' => 'string|nullable',
]);
$model = $this->resolveCreditableModel($title, request()->all());
$model->createCredit($data);
return $this->success();
}
public function destroy(Title $title, int $pivotId)
{
$this->authorize('update', $title);
$model = $this->resolveCreditableModel($title, request()->all());
$model
->credits()
->wherePivot('id', $pivotId)
->detach();
return $this->success();
}
public function changeOrder()
{
$this->authorize('update', Title::class);
$data = $this->validate(request(), [
'ids' => 'array|min:1',
'ids.*' => 'integer',
]);
$queryPart = '';
foreach ($data['ids'] as $order => $id) {
$queryPart .= " when id=$id then $order";
}
DB::table('creditables')
->whereIn('id', $data['ids'])
->update(['order' => DB::raw("(case $queryPart end)")]);
return $this->success();
}
public function resolveCreditableModel(
Title $title,
array $params,
): Title|Season|Episode {
if (Arr::get($params, 'season') && Arr::get($params, 'episode')) {
return $title->findEpisode($params['season'], $params['episode']);
} elseif (Arr::get($params, 'season')) {
return $title->findSeason($params['season']);
}
return $title;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Common\Core\BaseController;
class TitleNewsController extends BaseController
{
public function __invoke(Title $title)
{
$this->authorize('show', $title);
$articles = $title->load([
'newsArticles' => fn($q) => $q->limit(4),
])->newsArticles;
return $this->success(['news_articles' => $articles]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Titles\Retrieve\PaginateTitleSeasons;
use App\Models\Title;
use Common\Core\BaseController;
class TitleSeasonsController extends BaseController
{
public function __invoke(Title $title)
{
$this->authorize('show', $title);
$pagination = app(PaginateTitleSeasons::class)->execute(
$title,
request()->all(),
);
return $this->success(['pagination' => $pagination]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Common\Core\BaseController;
use Illuminate\Support\Str;
class TitleTagsController extends BaseController
{
public function store(Title $title, string $type)
{
$this->authorize('update', $title);
$data = $this->validate(request(), [
'tag_name' => 'required|string',
]);
$relation = $this->getRelationName($type);
$tags = $title
->$relation()
->getModel()
->insertOrRetrieve([$data['tag_name']]);
$title->$relation()->syncWithoutDetaching($tags->pluck('id'));
return $this->success();
}
public function destroy(Title $title, string $type, int $tagId)
{
$this->authorize('update', $title);
$relation = $this->getRelationName($type);
$title->$relation()->detach([$tagId]);
return $this->success();
}
private function getRelationName($type)
{
return Str::plural(Str::camel($type));
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Lists\ListsLoader;
use App\Models\Episode;
use App\Models\Title;
use App\Models\User;
use Auth;
use Common\Auth\Events\UserAvatarChanged;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class UserProfileController extends BaseController
{
public function show(User $user)
{
$this->authorize('show', $user);
$user->load(['profile', 'links']);
$user->loadCount(['followers', 'followedUsers', 'lists']);
$user->is_pro = $user->subscribed();
return $this->success(['user' => $user]);
}
public function update()
{
$user = Auth::user();
$this->authorize('update', $user);
$data = $this->validate(request(), [
'user' => 'array',
'profile' => 'array',
'links' => 'array',
]);
User::unguard(true);
$oldAvatar = $user->avatar;
$user->fill($data['user'])->save();
if (
isset($data['user']['avatar']) &&
$oldAvatar !== $data['user']['avatar']
) {
event(new UserAvatarChanged($user));
}
$profile = $user
->profile()
->updateOrCreate(['user_id' => $user->id], $data['profile']);
$user->links()->delete();
$links = $user->links()->createMany($data['links']);
$user->setRelation('profile', $profile);
$user->setRelation('links', $links);
return $this->success(['user' => $user]);
}
public function lists(User $user)
{
$this->authorize('show', $user);
$pagination = (new ListsLoader())->forUser($user, request()->all());
return $this->success(['pagination' => $pagination]);
}
public function ratings(User $user)
{
$this->authorize('show', $user);
$datasource = new Datasource(
$user
->reviews()
->whereNull('body')
->with([
'reviewable' => function (MorphTo $morphTo) {
$morphTo
->morphWith([
Episode::class => ['title'],
])
->with('primaryVideo');
},
'user',
]),
request()->all(),
);
$pagination = $datasource->paginate();
return $this->success(['pagination' => $pagination]);
}
public function reviews(User $user)
{
$this->authorize('show', $user);
$datasource = new Datasource(
$user
->reviews()
->where('reviewable_type', Title::MODEL_TYPE)
->whereNotNull('body')
->with([
'reviewable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Episode::class => ['title'],
]);
},
'user',
]),
request()->all(),
);
$pagination = $datasource->paginate();
return $this->success(['pagination' => $pagination]);
}
public function comments(User $user)
{
$this->authorize('show', $user);
$datasource = new Datasource(
$user
->comments()
->with([
'commentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Episode::class => ['title'],
]);
},
'user',
])
->where('deleted', false),
request()->all(),
);
$pagination = $datasource->paginate();
return $this->success(['pagination' => $pagination]);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use Common\Core\BaseController;
use Illuminate\Support\Facades\Auth;
class UserRatingsController extends BaseController
{
public function __construct()
{
$this->middleware('auth');
}
public function __invoke()
{
$ratings = Auth::user()
->reviews()
->select(['id', 'reviewable_id', 'reviewable_type', 'score'])
->limit(1000)
->get()
->map(
fn($review) => [
'id' => $review->id,
'score' => $review->score,
'reviewable_id' => $review->reviewable_id,
'type' => $review->reviewable_type,
],
)
->groupBy('type')
->map(
fn($group) => $group
->mapWithKeys(
fn($item) => [
$item['reviewable_id'] => [
'id' => $item['id'],
'score' => $item['score'],
],
],
)
->all(),
);
return $this->success(['ratings' => $ratings]);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use Common\Core\BaseController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class UserWatchlistController extends BaseController
{
public function __construct()
{
$this->middleware('auth');
}
public function __invoke()
{
$list = Auth::user()
->watchlist()
->firstOrFail();
$items = DB::table('channelables')
->where('channel_id', $list->id)
->pluck('channelable_type', 'channelable_id')
->map(
fn($modelType, $itemId) => [
'id' => $itemId,
'type' => $modelType,
],
)
->groupBy('type')
->map(
fn($group) => $group->mapWithKeys(
fn($item) => [
$item['id'] => true,
],
),
);
return $this->success([
'watchlist' => [
'id' => $list->id,
'items' => $items,
],
]);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Models\Video;
use Common\Core\BaseController;
class VideoApproveController extends BaseController
{
public function approve(Video $video)
{
$this->authorize('update', $video);
$video->update(['approved' => true]);
return $this->success(['video' => $video]);
}
public function disapprove(Video $video)
{
$this->authorize('update', $video);
$video->update(['approved' => false]);
return $this->success(['video' => $video]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use App\Models\Title;
use Common\Core\BaseController;
use Illuminate\Support\Facades\DB;
class VideoOrderController extends BaseController
{
public function changeOrder(int $titleId)
{
$title = Title::findOrFail($titleId);
$this->authorize('update', $title);
request()->validate([
'ids' => 'array|min:1',
'ids.*' => 'integer',
]);
$queryPart = '';
foreach (request('ids') as $order => $id) {
$queryPart .= " when id=$id then $order";
}
DB::table('videos')
->whereIn('id', request('ids'))
->update(['order' => DB::raw("(case $queryPart end)")]);
return $this->success();
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Models\Video;
use Auth;
use Common\Core\BaseController;
use Illuminate\Database\Eloquent\Builder;
class VideoReportController extends BaseController
{
public function report(Video $video)
{
$userId = Auth::id();
$userIp = $this->request->ip();
// if we can't match current user, bail
if ( ! $userId && ! $userIp) return null;
$alreadyReported = $video->reports()
->where(function(Builder $query) use($userId, $userIp) {
$query->where('user_id', $userId)->orWhere('user_ip', $userIp);
})->first();
if ($alreadyReported) {
return $this->error(__('You have already reported this video.'));
} else {
$report = $video->reports()->create([
'user_id' => $userId,
'user_ip' => $userIp
]);
return $this->success(['report' => $report]);
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Plays\LogVideoPlay;
use App\Actions\Videos\CrupdateVideo;
use App\Models\Video;
use Common\Core\BaseController;
use Common\Database\Datasource\Datasource;
use Common\Database\Datasource\DatasourceFilters;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class VideosController extends BaseController
{
public function index()
{
$this->authorize('index', Video::class);
$builder = Video::with([
'captions',
'title' => fn(BelongsTo $query) => $query->with('seasons'),
])->withCount(['reports', 'plays']);
$filters = new DatasourceFilters(request('filters'));
if (
$titleFilter =
$filters->getAndRemove('title_id') ?? request('title_id')
) {
$titleId = $titleFilter['value'] ?? request('title_id');
$season = $titleFilter['season'] ?? request('season');
$episode = $titleFilter['episode'] ?? request('episode');
$builder->where('title_id', $titleId);
$builder->when($season, fn($q) => $q->where('season_num', $season));
$builder->when(
$episode,
fn($q) => $q->where('episode_num', $episode),
);
}
$datasource = new Datasource($builder, request()->all(), $filters);
$order = $datasource->getOrder();
if (Str::endsWith($order['col'], 'upvotes')) {
$datasource->order = false;
$builder->orderByMostUpvotes();
}
// order by percentage of likes, taking into account total amount of likes and dislikes
if (Str::endsWith($order['col'], 'score')) {
$datasource->order = false;
$builder->orderByWeightedScore();
}
// add a secondary order by episode number if ordering by season number
if (Str::endsWith($order['col'], 'season_num')) {
$datasource->order = false;
$builder
->orderBy('season_num', 'desc')
->orderBy('episode_num', 'desc');
}
return $this->success(['pagination' => $datasource->paginate()]);
}
public function show(Video $video)
{
$this->authorize('show', Video::class);
$video->load(['captions', 'title']);
return $this->success(['video' => $video]);
}
public function store()
{
$this->authorize('store', Video::class);
$this->validate(
request(),
[
'title_id' => 'required|integer',
'name' => ['required', 'string', 'min:3', 'max:250'],
'src' => 'required|max:1000',
'type' => 'required|string|min:3|max:250',
'category' => 'required|string|min:3|max:20',
'quality' => 'nullable|string|min:2|max:250',
'language' => 'required|nullable|string|max:10',
'season_num' => 'nullable|integer',
'episode_num' => 'requiredWith:season|integer|nullable',
'captions' => 'nullable|array',
'captions.*.name' => 'required|string|max:100',
'captions.*.url' => 'required|string|max:250',
'captions.*.language' => 'required|string|max:100',
],
[
'title_id.*' => __(
'Select a title this video should be attached to.',
),
],
);
$video = app(CrupdateVideo::class)->execute(request()->all());
return $this->success(['video' => $video]);
}
public function update($id)
{
$this->authorize('update', Video::class);
$this->validate(
request(),
[
'name' => 'string|min:3|max:250',
'src' => 'required|max:1000',
'type' => 'string|min:3|max:1000',
'quality' => 'nullable|string|min:2|max:250',
'language' => 'required|nullable|string|max:10',
'title_id' => 'integer',
'season_num' => 'nullable|integer',
'episode_num' => 'requiredWith:season|integer|nullable',
'captions' => 'nullable|array',
'captions.*.name' => 'required|string|max:100',
'captions.*.url' => 'required|string|max:250',
'captions.*.language' => 'required|string|max:100',
],
[
'title_id.*' => __(
'Select a title this video should be attached to.',
),
],
);
$video = app(CrupdateVideo::class)->execute(request()->all(), $id);
return $this->success(['video' => $video]);
}
public function destroy($ids)
{
$ids = explode(',', $ids);
$this->authorize('destroy', [Video::class, $ids]);
foreach ($ids as $id) {
$video = Video::find($id);
if (is_null($video)) {
continue;
}
$video->delete();
}
return $this->success();
}
public function logPlay(Video $video)
{
$this->authorize('show', Video::class);
if (request()->getContentType() === 'application/json') {
$data = request()->all();
} else {
$data = json_decode(request()->getContent(), true);
}
app(LogVideoPlay::class)->execute($video, $data);
return $this->success();
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Plays\LogVideoPlay;
use App\Actions\Titles\Retrieve\GetRelatedTitles;
use App\Models\Episode;
use App\Models\Title;
use App\Models\Video;
use Common\Core\BaseController;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Str;
class WatchController extends BaseController
{
public function __invoke(Video $video)
{
$this->authorize('show', $video);
$video = $this->loadVideoRelations($video);
(new LogVideoPlay())->execute($video);
$title = $this->loadTitle($video);
$episode = $video->episode_id
? Episode::findOrFail($video->episode_id)
: null;
$data = [
'loader' => 'watchPage',
'title' => $title,
'episode' => $episode,
'video' => $video,
'related_videos' => $this->videosByTags($video->title),
'alternative_videos' => settings('streaming.show_video_selector')
? $this->loadAlternativeVideos($video)
: [],
];
return $this->renderClientOrApi([
'data' => $data,
'pageName' => 'watch-page',
]);
}
protected function loadTitle(Video $video)
{
$title = Title::select([
'id',
'name',
'description',
'backdrop',
'poster',
'is_series',
])
->withCount('seasons')
->findOrFail($video->title_id);
$title->description = Str::limit($title->description, 310);
return $title;
}
protected function loadVideoRelations(Video $video)
{
$video->load([
'captions',
'reports' => fn($q) => $q
->where('user_id', auth()->id())
->orWhere('user_ip', getIp()),
'latestPlay' => function (HasOne $builder) {
$builder
->forCurrentUser()
->whereNotNull('time_watched')
->select(['id', 'video_id', 'time_watched']);
},
]);
if ($video->reports->first()) {
$video->current_user_reported = true;
}
return $video;
}
private function videosByEpisode(Title $title, int $seasonNum)
{
[$col, $direction] = explode(':', settings('streaming.default_sort'));
$currSeasonNum = $seasonNum;
$prevSeasonNum = $seasonNum - 1;
$nextSeasonNum = $seasonNum + 1;
$videos = $title
->videos()
->with(['captions', 'episode', 'title' => fn($q) => $q->compact()])
->where('approved', true)
->where('category', 'full')
->whereNotNull('episode_id')
->whereIn('season_num', [
$currSeasonNum,
$nextSeasonNum,
$prevSeasonNum,
])
->orderBy($col, $direction)
->groupBy(['season_num', 'episode_num'])
->get();
if ($videos->isEmpty()) {
return [];
}
$grouped = $videos
->groupBy('season_num')
->map(function (Collection $videos, $seasonNum) use (
$prevSeasonNum,
$nextSeasonNum,
) {
if ($seasonNum === $prevSeasonNum) {
return $videos->sortByDesc('episode_num')->first();
} elseif ($seasonNum === $nextSeasonNum) {
return $videos->sortByDesc('episode_num')->last();
} else {
// current season episodes
return $videos->sortBy('season')->sortBy('episode_num');
}
});
$videos = $grouped->get($currSeasonNum)->values();
// make sure prev season last episode appears first
if ($prevSeason = $grouped->get($prevSeasonNum)) {
$videos->prepend($prevSeason);
}
// make sure next season first episode appears last
if ($nextSeason = $grouped->get($nextSeasonNum)) {
$videos->push($nextSeason);
}
return $videos->values();
}
private function videosByTags(Title $title)
{
$title->load(['keywords', 'genres']);
$related = app(GetRelatedTitles::class)->execute($title, [
'limit' => 6,
'compact' => true,
]);
$videos = [];
if ($related->isNotEmpty()) {
$related->load([
'videos' => fn(HasMany $builder) => $builder
->where('approved', true)
->fromConfiguredCategory(),
]);
$videos = $related
->map(function (Title $title) {
if ($video = $title->videos->first()) {
$title->setRelation('videos', []);
$video->title = $title;
return $video;
}
})
->filter()
->values();
}
return $videos;
}
protected function loadAlternativeVideos(Video $video)
{
$builder = Video::where('title_id', $video->title_id);
if ($video->season_num) {
$builder->where('season_num', $video->season_num);
}
if ($video->episode_num) {
$builder->where('episode_num', $video->episode_num);
}
return $builder
->where('approved', true)
->limit(10)
->when(
settings('streaming.prefer_full'),
fn($query) => $query->where('category', 'full'),
)
->applySelectedSort()
->get();
}
}

75
app/Http/Kernel.php Executable file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Http;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustHosts;
use App\Http\Middleware\VerifyCsrfToken;
use Common\Auth\Middleware\OptionalAuthenticate;
use Common\Core\Middleware\EnsureFrontendRequestsAreStateful;
use Common\Core\Middleware\PrerenderIfCrawler;
use Common\Core\Middleware\TrustProxies;
use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
protected $middleware = [
TrustHosts::class,
TrustProxies::class,
HandleCors::class,
CheckForMaintenanceMode::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
protected $middlewareGroups = [
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
],
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:1500,1',
SubstituteBindings::class,
],
];
protected $routeMiddleware = [
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'bindings' => SubstituteBindings::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'password.confirm' => RequirePassword::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
'prerenderIfCrawler' => PrerenderIfCrawler::class,
'optionalAuth' => OptionalAuthenticate::class,
];
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
class EncryptCookies extends BaseEncrypter
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return response()->json(['status' => 'error', 'message' => 'already logged in'], 403);
}
return $next($request);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as BaseTrimmer;
class TrimStrings extends BaseTrimmer
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Http\Middleware;
use Common\Core\BaseTrustHosts;
class TrustHosts extends BaseTrustHosts
{
//
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Common\Core\BaseVerifyCsrfToken;
class VerifyCsrfToken extends BaseVerifyCsrfToken
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'secure/update/run',
'api/v1/videos/*/log-play'
];
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests;
use Auth;
use Common\Core\BaseFormRequest;
use Illuminate\Validation\Rule;
class CrupdateCaptionRequest extends BaseFormRequest
{
/**
* @return bool
*/
public function authorize()
{
return true;
}
/**
* @return array
*/
public function rules()
{
$required = $this->getMethod() === 'POST' ? 'required' : '';
$ignore = $this->getMethod() === 'PUT' ? $this->route('caption')->id : '';
$userId = $this->route('caption') ? $this->route('caption')->user_id : Auth::id();
return [
'name' => [
$required, 'string', 'min:2',
Rule::unique('video_captions')->where('video_id', $userId)->ignore($ignore)
],
'language' => "$required|string|max:5",
'caption_file' => "$required|file|mimes:txt",
'video_id' => "$required|integer",
];
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Resources;
use App\Models\Channel;
use App\Models\NewsArticle;
use App\Models\Person;
use App\Models\Title;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class ChannelResource extends JsonResource
{
public function toArray($request)
{
$config = Arr::except($this->config, [
'seoTitle',
'seoDescription',
'adminDescription',
'presetId',
]);
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'public' => $this->public,
'description' => $this->description,
'config' => $config,
'model_type' => $this->model_type,
'restriction' => $this->restriction?->toArray(),
'type' => $this->type,
'content' => [
'current_page' => $this->content->currentPage(),
'from' => $this->content->firstItem(),
//'last_page' => $this->content->lastPage(),
'next_page' => $this->content->hasMorePages()
? $this->content->currentPage() + 1
: null,
'per_page' => $this->content->perPage(),
'prev_page' =>
$this->content->currentPage() > 1
? $this->content->currentPage() - 1
: null,
'to' => $this->content->lastItem(),
'total' =>
$this->content instanceof LengthAwarePaginator
? $this->content->total()
: null,
'data' => $this->content
->getCollection()
->map(function ($item) use ($request) {
return match ($item->model_type) {
Channel::MODEL_TYPE => (new ChannelResource(
$item,
))->toArray($request),
Title::MODEL_TYPE => [
'id' => $item->id,
'name' => $item->name,
'release_date' => $item->release_date,
'poster' => $item->poster,
'backdrop' => $item->backdrop,
'is_series' => $item->is_series,
'rating' => $item->rating,
'runtime' => $item->runtime,
'model_type' => $item::MODEL_TYPE,
'status' => $item->status,
'certification' => $item->certification,
'description' => Str::limit(
$item->description,
200,
),
'primary_video' => $item->relationLoaded(
'primaryVideo',
)
? $item->primaryVideo?->toArray()
: null,
],
Person::MODEL_TYPE => [
'id' => $item->id,
'name' => $item->name,
'poster' => $item->poster,
'primary_credit' => $item->primary_credit,
'known_for' => $item->known_for,
'birth_date' => $item->birth_date,
'death_date' => $item->death_date,
'model_type' => $item::MODEL_TYPE,
'description' => Str::limit(
$item->description,
200,
),
],
NewsArticle::MODEL_TYPE => [
'id' => $item->id,
'title' => $item->title,
'slug' => $item->slug,
'image' => $item->image,
'source' => $item->source,
'source_url' => $item->source_url,
'byline' => $item->byline,
'model_type' => $item::MODEL_TYPE,
'created_at' => $item->created_at,
'body' => Str::limit($item->body, 340),
],
};
}),
],
];
}
}