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,113 @@
<?php
namespace App\Services\Admin;
use App\Models\Review;
use App\Models\Title;
use App\Models\User;
use App\Models\VideoPlay;
use Common\Admin\Analytics\Actions\GetAnalyticsHeaderDataAction;
use Common\Comments\Comment;
use Common\Database\Metrics\ValueMetric;
class GetAnalyticsHeaderData implements GetAnalyticsHeaderDataAction
{
public function execute(array $params): array
{
return [
array_merge(
[
'icon' => [
[
'tag' => 'path',
'attr' => [
'd' =>
'M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z',
],
],
],
'name' => __('New users'),
],
(new ValueMetric(
User::query(),
dateRange: $params['dateRange'],
))->count(),
),
array_merge(
[
'icon' => [
[
'tag' => 'path',
'attr' => [
'd' =>
'M4 6.47 5.76 10H20v8H4V6.47M22 4h-4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4z',
],
],
],
'name' => __('New titles'),
],
(new ValueMetric(
Title::query(),
dateRange: $params['dateRange'],
))->count(),
),
array_merge(
[
'icon' => [
[
'tag' => 'path',
'attr' => [
'd' =>
'M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27z',
],
],
],
'name' => __('New ratings'),
],
(new ValueMetric(
Review::query(),
dateRange: $params['dateRange'],
))->count(),
),
array_merge(
[
'icon' => [
[
'tag' => 'path',
'attr' => [
'd' =>
'M10 8.64 15.27 12 10 15.36V8.64M8 5v14l11-7L8 5z',
],
],
],
'name' => __('Video plays'),
],
(new ValueMetric(
VideoPlay::query(),
dateRange: $params['dateRange'],
))->count(),
),
array_merge(
[
'icon' => [
[
'tag' => 'path',
'attr' => [
'd' =>
'M4 4h16v12H5.17L4 17.17V4m0-2c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2H4zm2 10h8v2H6v-2zm0-3h12v2H6V9zm0-3h12v2H6V6z',
],
],
],
'name' => __('New comments'),
],
(new ValueMetric(
Comment::query(),
dateRange: $params['dateRange'],
))->count(),
),
];
}
}

View File

@@ -0,0 +1,15 @@
<?php namespace App\Services;
use Common\Core\Bootstrap\BaseBootstrapData;
class AppBootstrapData extends BaseBootstrapData
{
public function init(): self
{
parent::init();
$this->data['settings']['tmdb_is_setup'] = !is_null(config('services.tmdb.key'));
return $this;
}
}

50
app/Services/ChannelPresets.php Executable file
View File

@@ -0,0 +1,50 @@
<?php
namespace App\Services;
use Common\Channels\GenerateChannelsFromConfig;
class ChannelPresets
{
public function getAll(): array
{
return [
[
'name' => 'Movie database',
'preset' => 'database',
'description' =>
'Channel preset for a movie database site similar to IMDb',
],
[
'name' => 'Anime',
'preset' => 'anime',
'description' =>
'Channel preset for an anime based site similar to Crunchyroll',
],
[
'name' => 'Streaming',
'preset' => 'streaming',
'description' =>
'Channel preset for a streaming site similar to Netflix',
],
];
}
public function apply(string $preset): void
{
$presetConfig = match ($preset) {
'anime' => resource_path('defaults/channels/anime-channels.json'),
'streaming' => resource_path(
'defaults/channels/streaming-channels.json',
),
default => resource_path(
'defaults/channels/database-channels.json',
),
};
(new GenerateChannelsFromConfig())->execute([
resource_path('defaults/channels/shared-channels.json'),
$presetConfig,
]);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Services\Data\News;
use Illuminate\Support\Facades\Http;
use Symfony\Component\DomCrawler\Crawler;
class ImdbNewsProvider
{
public function getArticles(): array
{
$compiledNews = [];
$html = Http::withHeaders([
'User-Agent' =>
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' .
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 ' .
'Safari/537.36',
])
->get('https://www.imdb.com/news/top')
->getBody()
->getContents();
$strippedHtml = preg_replace(
'/<script(.*?)>(.*?)<\/script>/is',
'',
$html,
);
$crawler = new Crawler($strippedHtml);
// grab every news article on the page
foreach (
$crawler->filter(
'[data-testid="sub-section-news-card-section"] .ipc-list-card',
)
as $k => $node
) {
$articleCrawler = new Crawler($node);
// extract related people and title ids from article
$links = $articleCrawler->filter('a')->extract(['href']);
$imdbTitleIds = [];
$imdbPersonIds = [];
foreach ($links as $href) {
preg_match('/\/title\/(tt[0-9]+)\//', $href, $titleMatches);
preg_match('/\/name\/(nm[0-9]+)\//', $href, $nameMatches);
if (isset($titleMatches[1])) {
$imdbTitleIds[] = $titleMatches[1];
}
if (isset($nameMatches[1])) {
$imdbPersonIds[] = $nameMatches[1];
}
}
$date = head(
$articleCrawler
->filter('.ipc-inline-list li')
->extract(['_text']),
);
$byline = head(
$articleCrawler
->filter('.ipc-inline-list li')
->eq(1)
->extract(['_text']),
);
$sourceUrl = last(
$articleCrawler->filter('.ipc-link')->extract(['href']),
);
if (!isset(parse_url($sourceUrl)['scheme'])) {
$sourceUrl = "https://imdb.com{$sourceUrl}";
}
$img = head($articleCrawler->filter('img')->extract(['src']));
$body = trim(
$articleCrawler->filter('.ipc-html-content-inner-div')->html(),
);
$body = preg_replace(
'/<div data-reactroot="">.+?<\/div>/',
'',
$body,
);
$body = strip_tags($body, '<br>');
if (!$img) {
continue;
}
// transform each news article into array
$compiledNews[$k] = [
'title' => trim(
head(
$articleCrawler
->filter('.ipc-link')
->extract(['_text']),
),
),
'body' => $body,
'imdb_title_ids' => $imdbTitleIds,
'imdb_person_ids' => $imdbPersonIds,
'date' => trim($date),
'source' => trim(
last(
$articleCrawler
->filter('.ipc-link')
->extract(['_text']),
),
),
'source_url' => $sourceUrl,
'byline' => str_replace('by ', '', trim($byline)),
'image' => preg_replace(
'/([A-Z]+)([0-9]+)_CR([0-9]+),([0-9]+),100,150/',
'${1}400_CR$3,$4,270,400',
$img,
),
];
}
return $compiledNews;
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace App\Services\Data\Tmdb;
use App\Actions\People\DeletePeople;
use App\Actions\Titles\DeleteSeasons;
use App\Actions\Titles\DeleteTitles;
use App\Models\Person;
use App\Models\Title;
use Common\Core\HttpClient;
use Common\Settings\Settings;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class TmdbApi
{
public const TMDB_BASE = 'https://api.themoviedb.org/3/';
public const DEFAULT_TMDB_LANGUAGE = 'en-US';
protected bool $includeAdult = false;
protected string $language = self::DEFAULT_TMDB_LANGUAGE;
protected HttpClient $http;
public function __construct(private Settings $settings)
{
$this->http = new HttpClient(['exceptions' => true]);
$this->language = $this->settings->get(
'tmdb.language',
self::DEFAULT_TMDB_LANGUAGE,
);
$this->includeAdult = $this->settings->get('tmdb.includeAdult', false);
}
public function getPerson(Person $person): array|null
{
$appends = [];
// only import filmography if it's set by user
if ($this->settings->get('content.automate_filmography')) {
$appends[] = 'combined_credits';
}
$response = $this->call("person/{$person->tmdb_id}", [
'append_to_response' => implode(',', $appends),
]);
// person does not exist anymore on themoviedb
if (
Arr::get($response, 'success') === false &&
Arr::get($response, 'status_code') === 34
) {
if (config('common.site.tmdb_delete_when_sync')) {
app(DeletePeople::class)->execute([$person->id]);
return null;
} else {
return [];
}
}
$response['fully_synced'] = true;
return app(TransformData::class)
->execute([$response])
->first();
}
public function getSeason(Title $title, $seasonNumber)
{
if (!$title->tmdb_id) {
return [];
}
$response = $this->call("tv/{$title->tmdb_id}/season/{$seasonNumber}", [
'append_to_response' => 'credits',
]);
// season does not exist anymore on themoviedb
if (
Arr::get($response, 'success') === false &&
Arr::get($response, 'status_code') === 34
) {
if (config('common.site.tmdb_delete_when_sync')) {
$seasonId = $title
->seasons()
->where('number', $seasonNumber)
->value('id');
app(DeleteSeasons::class)->execute([$seasonId]);
return null;
} else {
return [];
}
}
$data = app(TransformData::class)
->execute([$response])
->first();
$data['fully_synced'] = true;
return $data;
}
public function getTitle(Title $title): array|null
{
$appends = [
'credits',
'external_ids',
'images',
'content_ratings',
'keywords',
'release_dates',
'videos',
'seasons',
];
$uri = $title->is_series ? 'tv' : 'movie';
$response = $this->call("$uri/{$title->tmdb_id}", [
'append_to_response' => implode(',', $appends),
]);
// title does not exist anymore on themoviedb
if (
Arr::get($response, 'success') === false &&
Arr::get($response, 'status_code') === 34
) {
if (config('common.site.tmdb_delete_when_sync')) {
app(DeleteTitles::class)->execute([$title->id]);
return null;
} else {
return [];
}
}
$data = app(TransformData::class)
->execute([$response])
->first();
// fall back to english videos if there are no videos in the current language
if (!Str::startsWith($this->language, 'en') && empty($data['videos'])) {
$videos = $this->call("$uri/{$title->tmdb_id}/videos", [
'language' => 'en-US',
]);
$videos = app(TransformData::class)->formatVideos(
$videos['results'],
);
$data['videos'] = $videos;
}
$data['fully_synced'] = true;
return $data;
}
public function search(string $query, array $params = []): Collection
{
$response = $this->call('search/multi', ['query' => $query]);
$results = app(TransformData::class)->execute($response['results']);
$type = Arr::get($params, 'type');
$limit = Arr::get($params, 'limit', 8);
if ($type) {
$results = $results->filter(
fn($result) => $result['type'] === $type,
);
}
return $results
->sortByDesc('popularity')
->slice(0, $limit)
->values();
}
public function browse($page = 1, $type = 'movie', $queryParams = []): array
{
if ($page > 500) {
throw new Exception('Maximum page is 500');
}
if ($type === 'series') {
$type = 'tv';
}
$apiParams = array_merge(
['sort_by' => 'popularity.desc', 'page' => $page],
$queryParams,
);
$response = $this->call("discover/$type", $apiParams);
$response['results'] = app(TransformData::class)->execute(
$response['results'],
);
return $response;
}
public function trendingPeople(): Collection
{
$response = $this->call('person/popular');
return app(TransformData::class)->execute($response['results']);
}
protected function call(string $uri, array $queryParams = []): array
{
$key = config('services.tmdb.key');
$url = self::TMDB_BASE . "$uri?api_key=$key";
$queryParams = array_merge(
[
// need to send "true" and not "1" otherwise tmdb will not work
'include_adult' => $this->includeAdult ? 'true' : 'false',
'language' => $this->language,
'region' => 'US',
'include_image_language' => 'en,null',
],
$queryParams,
);
$url .= '&' . urldecode(http_build_query($queryParams));
return $this->http->get($url, [
'verify' => false,
]);
}
}

View File

@@ -0,0 +1,522 @@
<?php
namespace App\Services\Data\Tmdb;
use App\Actions\Titles\HandlesEncodedTmdbId;
use App\Models\Episode;
use App\Models\Person;
use App\Models\Season;
use App\Models\Title;
use App\Models\Video;
use Carbon\Carbon;
use Common\Settings\Settings;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class TransformData
{
use HandlesEncodedTmdbId;
public const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/original';
public const BACKDROP_BASE_URI = 'https://image.tmdb.org/t/p/w1280';
public const PROFILE_BASE_URI = 'https://image.tmdb.org/t/p/w185';
public const YOUTUBE_BASE_URI = 'https://youtube.com/embed/';
public const SERIES_ENDED_STATUS = ['Ended', 'Canceled'];
public function __construct(private Settings $settings)
{
}
public function execute(array $tmdbMedia): Collection
{
return collect($tmdbMedia)->map(
fn($mediaItem) => $this->transformMediaItem($mediaItem),
);
}
public function transformMediaItem($mediaItem): ?array
{
$type = $this->getType($mediaItem);
if ($type === Person::MODEL_TYPE) {
return $this->transformPerson($mediaItem);
} elseif ($type === Episode::MODEL_TYPE) {
return $this->transformEpisode($mediaItem);
} elseif ($type === Season::MODEL_TYPE) {
return $this->transformSeason($mediaItem);
} else {
return $this->transformTitle($mediaItem, $type);
}
}
private function transformTitle($data, $type): array
{
$releaseKey =
$type === Title::MOVIE_TYPE ? 'release_date' : 'first_air_date';
$releaseDate = $this->getReleaseDate($releaseKey, $data);
$name = $this->getTitle($data);
$transformed = [
'id' => $this->encodeTmdbId('tmdb', $type, $data['id']),
'is_series' => $type === Title::SERIES_TYPE,
'model_type' => Title::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'poster_path')),
'release_date' => $releaseDate,
'cast' => $this->getCredits($data),
'name' => $name,
'description' => $data['overview'],
'tmdb_vote_count' => $data['vote_count'] ?? null,
'tmdb_vote_average' => isset($data['vote_average'])
? round($data['vote_average'], 1)
: null,
'original_title' => $this->getOriginalName($data),
'popularity' => Arr::get($data, 'popularity'),
'language' => Arr::get($data, 'original_language'),
'certification' => $this->getCertification($data, $type),
'countries' => $this->getCountries($data),
'tagline' => Arr::get($data, 'tagline'),
'budget' => Arr::get($data, 'budget') ?: null,
'revenue' => Arr::get($data, 'revenue') ?: null,
'runtime' => $this->getRuntime($data),
'videos' => $this->formatVideos(
Arr::get($data, 'videos.results', []),
),
'images' => $this->transformImages($data),
'backdrop' => $this->getBackdrop($data),
'genres' => $this->getGenres($data),
'imdb_id' => Arr::get($data, 'external_ids.imdb_id') ?: null,
'tmdb_id' => $data['id'],
'keywords' => $this->getKeywords($data),
'series_ended' => (bool) in_array(
Arr::get($data, 'status'),
self::SERIES_ENDED_STATUS,
),
'adult' => Arr::get($data, 'adult', false),
];
if (Arr::get($data, 'seasons')) {
$transformed['seasons'] = $this->getSeasons($data);
}
return $transformed;
}
/**
* Get US certification for title.
*
* @param array $data
* @param $type
* @return string|null
*/
private function getCertification($data, $type)
{
if ($type === Title::SERIES_TYPE) {
$firstKey = 'content_ratings.results';
$secondKey = 'rating';
} else {
$firstKey = 'release_dates.results';
$secondKey = 'release_dates.*.certification';
}
$rating = collect(Arr::get($data, $firstKey, []))
->where('iso_3166_1', 'US')
->pluck($secondKey)
->flatten()
->filter()
->first();
return $rating ? str_replace('tv-', '', strtolower($rating)) : null;
}
private function getCountries($data)
{
return array_map(
fn($country) => [
'name' => strtolower($country['iso_3166_1']),
'display_name' => $country['name'],
],
Arr::get($data, 'production_countries', []),
);
}
private function transformEpisode(array $data): array
{
$releaseDate = $this->getReleaseDate('air_date', $data);
$epNum = Arr::get($data, 'episode_number');
$sNum = Arr::get($data, 'season_number');
return [
'id' => $this->encodeTmdbId(
'tmdb',
Episode::MODEL_TYPE,
$data['id'],
),
'model_type' => Episode::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'still_path')),
'release_date' => $releaseDate,
'cast' => $this->getCredits($data),
// episode might not have a name sometimes, auto generate it in that case
'name' => $this->getTitle($data) ?? "Episode #$sNum.$epNum",
'description' => $data['overview'],
'tmdb_vote_count' => $data['vote_count'],
'tmdb_vote_average' => round($data['vote_average'], 1) ?: null,
'popularity' => Arr::get($data, 'popularity'),
'episode_number' => $epNum,
'season_number' => $sNum,
'runtime' => $data['runtime'] ?? null,
];
}
private function getPoster(?string $path): ?string
{
return $path ? self::TMDB_IMAGE_BASE . $path : null;
}
private function getBackdrop($data): ?string
{
$backdrop = Arr::get($data, 'backdrop_path');
return $backdrop ? self::BACKDROP_BASE_URI . $backdrop : null;
}
private function getSeasons(array $data): ?array
{
if (!Arr::has($data, 'seasons')) {
return null;
}
// skip "specials" season with number of "0"
$seasons = array_filter(
Arr::get($data, 'seasons', []),
fn($season) => $season['season_number'] !== 0,
);
return array_map(
fn($season) => Arr::except(
$this->transformSeason($season),
'model_type',
),
$seasons,
);
}
private function transformSeason(array $data): array
{
$releaseDate = $this->getReleaseDate(
['first_air_date', 'air_date'],
$data,
);
$transformedData = [
'id' => $this->encodeTmdbId(
'tmdb',
Season::MODEL_TYPE,
$data['id'],
),
'model_type' => Season::MODEL_TYPE,
'poster' => $this->getPoster(Arr::get($data, 'poster_path')),
'release_date' => $releaseDate,
'number' => Arr::get($data, 'season_number'),
];
if (isset($data['credits'])) {
$transformedData['cast'] = $this->getCredits($data);
}
if (isset($data['episodes'])) {
$transformedData['episodes'] = array_map(
fn($episode) => $this->transformEpisode($episode),
$data['episodes'],
);
}
return $transformedData;
}
private function getKeywords(array $data): array
{
$keywords = array_merge(
Arr::get($data, 'keywords.results', []),
Arr::get($data, 'keywords.keywords', []),
);
return array_map(
fn($keyword) => ['name' => $keyword['name']],
$keywords,
);
}
private function getRuntime(array $data): ?int
{
$runtime = Arr::get(
$data,
'runtime',
Arr::get($data, 'episode_run_time'),
);
if (is_array($runtime)) {
$runtime = !empty($runtime) ? min($runtime) : null;
}
return $runtime;
}
private function transformPerson($tmdbPerson): ?array
{
if (!isset($tmdbPerson['id'])) {
return null;
}
$syncCredits = $this->settings->get('content.automate_filmography');
$hasCredits = Arr::has($tmdbPerson, 'combined_credits') && $syncCredits;
$hasKnownForCredits =
Arr::has($tmdbPerson, 'known_for') && $syncCredits;
$data = [
'id' => $this->encodeTmdbId(
'tmdb',
Person::MODEL_TYPE,
$tmdbPerson['id'],
),
'name' => $tmdbPerson['name'],
'tmdb_id' => $tmdbPerson['id'],
'imdb_id' => Arr::get($tmdbPerson, 'imdb_id'),
'gender' => $this->transformGender(Arr::get($tmdbPerson, 'gender')),
'poster' => $this->getPoster($tmdbPerson['profile_path']),
'model_type' => Person::MODEL_TYPE,
'adult' => Arr::get($tmdbPerson, 'adult', false),
'fully_synced' => Arr::get($tmdbPerson, 'fully_synced') ?: false,
'relation_data' => [
'character' => Arr::get($tmdbPerson, 'character') ?: null,
'order' => Arr::get($tmdbPerson, 'order', 0),
'department' => strtolower(
Arr::get($tmdbPerson, 'department', 'actors'),
),
'job' => strtolower(Arr::get($tmdbPerson, 'job', 'actor')),
],
];
// "known_for" credits will only be returned from "search" tmdb api call.
if (
!$hasCredits &&
$hasKnownForCredits &&
isset($tmdbPerson['known_for'][0])
) {
$data['primary_credit'] = $this->transformMediaItem(
$tmdbPerson['known_for'][0],
);
}
if ($hasCredits) {
$credits = array_merge(
Arr::get($tmdbPerson, 'combined_credits.cast'),
Arr::get($tmdbPerson, 'combined_credits.crew'),
);
$credits = array_map(function ($credit) {
$title = array_filter(
$this->transformMediaItem($credit),
fn($value) => !is_array($value),
);
$title['relation_data'] = [
'department' => strtolower(
Arr::get($credit, 'department', 'actors'),
),
'job' => strtolower(Arr::get($credit, 'job', 'actor')),
'character' => Arr::get($credit, 'character') ?: null,
'order' => Arr::get($credit, 'order', 0),
];
return $title;
}, $credits);
$credits = array_filter(
$credits,
fn($credit) => !Arr::get($credit, 'adult') ||
$this->settings->get('tmdb.includeAdult'),
);
$data['credits'] = $credits;
}
$optionalProps = [
'biography' => 'description',
'birthday' => 'birth_date',
'deathday' => 'death_date',
'place_of_birth' => 'birth_place',
'known_for_department' => 'known_for',
'popularity' => 'popularity',
];
// can't set these as "null" as some data might not be contained
// when getting people via movie/series filmography
foreach ($optionalProps as $tmdbKey => $localKey) {
if (Arr::has($tmdbPerson, $tmdbKey)) {
$data[$localKey] = $tmdbPerson[$tmdbKey];
}
}
return $data;
}
private function getCredits(array $tmdbTitle): array
{
// cast/crew from series, movies and episodes
$credits = array_merge(
Arr::get($tmdbTitle, 'credits.cast', []),
Arr::get($tmdbTitle, 'credits.crew', []),
Arr::get($tmdbTitle, 'crew', []),
Arr::get($tmdbTitle, 'guest_stars', []),
);
if ($createdBy = Arr::get($tmdbTitle, 'created_by')) {
$creators = array_map(function ($person) {
$person['job'] = 'creator';
$person['department'] = 'creators';
$person['known_for_department'] = 'creators';
$person['popularity'] = 3;
return $person;
}, $createdBy);
$credits = array_merge($credits, $creators);
}
$transformedCredits = array_map(
fn($person) => $this->transformPerson($person),
$credits,
);
return array_filter($transformedCredits);
}
/**
* @param int|null $gender
* @return null|string
*/
private function transformGender($gender)
{
if ($gender === 1) {
return 'female';
} elseif ($gender === 2) {
return 'male';
} else {
return null;
}
}
private function transformImages(array $tmdbTitle): array
{
$images = Arr::get($tmdbTitle, 'images.backdrops', []);
return array_map(
fn($image) => [
'type' => 'backdrop',
'source' => 'tmdb',
'url' => self::TMDB_IMAGE_BASE . $image['file_path'],
],
$images,
);
}
/**
* @param $tmdbTitle
* @return array
*/
private function getGenres($tmdbTitle)
{
return array_map(
fn($genre) => ['name' => $genre['name']],
Arr::get($tmdbTitle, 'genres', []),
);
}
public function formatVideos(array $videos): array
{
$videos = array_map(
fn($video) => [
'name' => trim($video['name']),
'src' => self::YOUTUBE_BASE_URI . $video['key'],
'type' => Video::VIDEO_TYPE_EMBED,
'origin' => 'tmdb',
'category' => strtolower(Arr::get($video, 'type', 'trailer')),
],
$videos,
);
// show trailers first
usort($videos, function ($a, $b) {
if ($a['category'] === 'trailer') {
return -1;
} elseif ($b['category'] === 'trailer') {
return 1;
} else {
return 0;
}
});
return $videos;
}
/**
* @param array $data
* @return string
*/
private function getType($data)
{
$hasSeasonNumber = Arr::get($data, 'season_number');
$hasEpisodeNumber = Arr::get($data, 'episode_number');
if ($hasEpisodeNumber && $hasSeasonNumber) {
return Episode::MODEL_TYPE;
} elseif ($hasSeasonNumber) {
return Season::MODEL_TYPE;
} elseif (
Arr::get($data, 'media_type') === 'person' ||
Arr::has($data, 'gender')
) {
return Person::MODEL_TYPE;
} elseif (Arr::has($data, 'first_air_date')) {
return Title::SERIES_TYPE;
} else {
return Title::MOVIE_TYPE;
}
}
private function getReleaseDate(string|array $key, array $data): ?Carbon
{
$potentials = !is_array($key) ? [$key] : $key;
foreach ($potentials as $potential) {
if (isset($data[$potential])) {
return Carbon::parse($data[$potential]);
}
}
return null;
}
/**
* @param array $tmdbTitle
* @return string|null
*/
private function getTitle($tmdbTitle)
{
if (isset($tmdbTitle['title'])) {
return $tmdbTitle['title'];
} elseif (isset($tmdbTitle['name'])) {
return $tmdbTitle['name'];
} else {
return null;
}
}
/**
* @param array $tmdbTitle
* @return string|null
*/
private function getOriginalName($tmdbTitle)
{
if (isset($tmdbTitle['original_title'])) {
return $tmdbTitle['original_title'];
} elseif (isset($tmdbTitle['original_name'])) {
return $tmdbTitle['original_name'];
} else {
return null;
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Services\Settings\Validators;
use App\Services\Data\Tmdb\TmdbApi;
use Common\Settings\Validators\SettingsValidator;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Arr;
class TmdbApiKeyValidator implements SettingsValidator
{
public const KEYS = ['tmdb_api_key'];
public function fails($values)
{
if ($apiKey = Arr::get($values, 'tmdb_api_key')) {
config(['services.tmdb.key' => $apiKey]);
}
try {
app(TmdbApi::class)->browse();
} catch (ClientException $e) {
$errResponse = json_decode(
$e
->getResponse()
->getBody()
->getContents(),
true,
512,
JSON_THROW_ON_ERROR,
);
return $this->getMessage($errResponse);
}
}
/**
* @param array $errResponse
* @return array
*/
private function getMessage($errResponse)
{
return ['tmdb_api_key' => 'This Themoviedb api key is not valid.'];
}
}

View File

@@ -0,0 +1,82 @@
<?php namespace App\Services;
use App\Models\Channel;
use App\Models\Episode;
use App\Models\NewsArticle;
use App\Models\Person;
use App\Models\Season;
use App\Models\Title;
use App\Models\Video;
use Common\Admin\Sitemap\BaseSitemapGenerator;
use Common\Core\Contracts\AppUrlGenerator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class SitemapGenerator extends BaseSitemapGenerator
{
protected function getAppQueries(): array
{
return [
app(Title::class)
->where(function (Builder $query) {
$query->where('fully_synced', true)->orWhereNull('tmdb_id');
})
->whereNotNull('name')
->select(['id', 'name', 'updated_at']),
app(Person::class)
->where('fully_synced', true)
->orWhereNull('tmdb_id')
->select(['id', 'name', 'updated_at']),
app(Episode::class)
->whereHas('title')
->with(['title' => fn($q) => $q->compact()])
->select([
'id',
'name',
'title_id',
'season_number',
'episode_number',
'updated_at',
]),
app(Season::class)
->whereHas('title')
->with(['title' => fn($q) => $q->compact()])
->select(['id', 'title_id', 'number']),
Video::select(['id', 'name', 'updated_at']),
Channel::where('public', true)
->where('internal', false)
->select(['id', 'name', 'slug', 'updated_at']),
app(NewsArticle::class)->select([
'id',
'title',
'slug',
'updated_at',
]),
];
}
protected function getAppStaticUrls(): array
{
return ['series', 'movies', 'people', 'news'];
}
protected function addTitleLine(
string $url,
string $updatedAt,
string $name,
) {
$this->addNewLine($url, $updatedAt, $name);
$this->addNewLine("$url/full-credits", $updatedAt, $name);
}
protected function getModelUrl(Model $model): string
{
if ($model instanceof Season) {
return app(AppUrlGenerator::class)->season($model, $model->title);
}
if ($model instanceof Episode) {
return app(AppUrlGenerator::class)->episode($model, $model->title);
}
return parent::getModelUrl($model);
}
}

100
app/Services/UrlGenerator.php Executable file
View File

@@ -0,0 +1,100 @@
<?php
namespace App\Services;
use App\Http\Resources\ChannelResource;
use App\Models\Channel;
use App\Models\Episode;
use App\Models\Genre;
use App\Models\NewsArticle;
use App\Models\Person;
use App\Models\Season;
use App\Models\Title;
use App\Models\User;
use App\Models\Video;
use Common\Core\Prerender\BaseUrlGenerator;
class UrlGenerator extends BaseUrlGenerator
{
public function title(array|Title $title): string
{
$slug = slugify($title['name']);
return url("titles/{$title['id']}/{$slug}");
}
public function season(array|Season $season, $dataOrTitle): string
{
$title = $dataOrTitle['title'] ?? $dataOrTitle;
$titleUrl = $this->title($title);
return "$titleUrl/season/{$season['number']}";
}
public function episode(array|Episode $episode, $dataOrTitle): string
{
$title = $dataOrTitle['title'] ?? $dataOrTitle;
$titleUrl = $this->title($title);
return "$titleUrl/season/{$episode['season_number']}/episode/{$episode['episode_number']}";
}
public function video(array|Video $video): string
{
return $this->watch($video);
}
public function watch(array|Video $video): string
{
return url("watch/{$video['id']}");
}
public function person(array|Person $person): string
{
$slug = slugify($person['name']);
return url("people/{$person['id']}/{$slug}");
}
public function article(array|NewsArticle $article): string
{
return url("news/{$article['id']}");
}
public function genre(array|Genre $genre): string
{
return url("genre/{$genre['id']}");
}
public function search(string $query): string
{
return url("search/{$query}");
}
public function user(User|array $model): string
{
return url('users/' . $model['id']);
}
public function channel(Channel|ChannelResource|array $model): string
{
if ($model['type'] === 'list') {
return url("lists/{$model['id']}");
} elseif (
settings('homepage.type') === 'channels' &&
settings('homepage.value') === $model['id']
) {
return url('/');
} else {
$url = url($model['slug'] ?: slugify($model['name']));
if (isset($model['restriction'])) {
return "$url/{$model['restriction']['name']}";
}
return $url;
}
}
public function image(string|null $path): string|null
{
if ($path && !str_starts_with($path, 'http')) {
return url($path);
}
return $path;
}
}