113
app/Services/Admin/GetAnalyticsHeaderData.php
Executable file
113
app/Services/Admin/GetAnalyticsHeaderData.php
Executable 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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
15
app/Services/AppBootstrapData.php
Executable file
15
app/Services/AppBootstrapData.php
Executable 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
50
app/Services/ChannelPresets.php
Executable 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
app/Services/Data/News/ImdbNewsProvider.php
Executable file
121
app/Services/Data/News/ImdbNewsProvider.php
Executable 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;
|
||||
}
|
||||
}
|
||||
225
app/Services/Data/Tmdb/TmdbApi.php
Executable file
225
app/Services/Data/Tmdb/TmdbApi.php
Executable 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
522
app/Services/Data/Tmdb/TransformData.php
Executable file
522
app/Services/Data/Tmdb/TransformData.php
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/Services/Settings/Validators/TmdbApiKeyValidator.php
Executable file
44
app/Services/Settings/Validators/TmdbApiKeyValidator.php
Executable 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.'];
|
||||
}
|
||||
}
|
||||
82
app/Services/SitemapGenerator.php
Executable file
82
app/Services/SitemapGenerator.php
Executable 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
100
app/Services/UrlGenerator.php
Executable 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user