diff --git a/.gitignore b/.gitignore index 1f1d7cb..b7cbc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ phpunit.xml .phpunit.result.cache composer.lock vendor/ +.dccache +.idea \ No newline at end of file diff --git a/README.md b/README.md index 82c56eb..5a341a2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![](.github/banner.png) +### Please do not use this package but instead use the official one. + # Laravel TMDB [![Latest Version](http://img.shields.io/packagist/v/astrotomic/laravel-tmdb.svg?label=Release&style=for-the-badge)](https://packagist.org/packages/astrotomic/laravel-tmdb) @@ -68,9 +70,11 @@ This will do one HTTP call per model and save multiple HTTP calls in the future. ```php use Astrotomic\Tmdb\Models\MovieGenre; +use Astrotomic\Tmdb\Models\TvGenre; use Astrotomic\Tmdb\Models\WatchProvider; MovieGenre::all(); +TvGenre::all(); WatchProvider::all(); ``` diff --git a/composer.json b/composer.json index 33d02ca..d2e56b4 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "astrotomic/laravel-tmdb", + "name": "murdercode/laravel-tmdb", "description": "Interact with TMDB data in your Laravel application.", "license": "MIT", "authors": [ @@ -15,15 +15,15 @@ "issues": "https://github.com/Astrotomic/laravel-tmdb/issues" }, "require": { - "php": "^8.0", + "php": "^8.0 || ^8.1 || ^8.2", "ext-json": "*", "guzzlehttp/guzzle": "^6.5.5 || ^7.0.1", - "illuminate/database": "^8.0 || ^9.0", - "illuminate/http": "^8.0 || ^9.0", - "illuminate/support": "^8.0 || ^9.0", + "illuminate/database": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/http": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", "nesbot/carbon": "^2.31", "spatie/laravel-enum": "^3.0", - "spatie/laravel-translatable": "^5.0" + "spatie/laravel-translatable": "^6.3.0" }, "require-dev": { "astrotomic/phpunit-assertions": "^0.6", diff --git a/database/migrations/2022_01_19_150000_create_tv_genres_table.php b/database/migrations/2022_01_19_150000_create_tv_genres_table.php new file mode 100644 index 0000000..905a5ff --- /dev/null +++ b/database/migrations/2022_01_19_150000_create_tv_genres_table.php @@ -0,0 +1,24 @@ +create(TvGenre::table(), static function (Blueprint $table): void { + $table->bigInteger('id')->unsigned()->primary(); + + $table->json('name'); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection(TvGenre::connection())->dropIfExists(TvGenre::table()); + } +}; diff --git a/database/migrations/2022_01_19_153000_create_networks_table.php b/database/migrations/2022_01_19_153000_create_networks_table.php new file mode 100644 index 0000000..0784469 --- /dev/null +++ b/database/migrations/2022_01_19_153000_create_networks_table.php @@ -0,0 +1,27 @@ +create(Network::table(), static function (Blueprint $table): void { + $table->bigInteger('id')->unsigned()->primary(); + $table->string('headquarters')->nullable(); + $table->json('homepage')->nullable(); + $table->json('logo_path')->nullable(); + $table->json('languages')->nullable(); + $table->json('name')->nullable(); + $table->string('origin_country')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection(Network::connection())->dropIfExists(Network::table()); + } +}; diff --git a/database/migrations/2022_01_19_160000_create_tvs_table.php b/database/migrations/2022_01_19_160000_create_tvs_table.php new file mode 100644 index 0000000..2b05791 --- /dev/null +++ b/database/migrations/2022_01_19_160000_create_tvs_table.php @@ -0,0 +1,45 @@ +create(Tv::table(), static function (Blueprint $table): void { + $table->bigInteger('id')->unsigned()->primary(); + $table->string('backdrop_path')->nullable(); + $table->json('episode_run_time')->nullable(); + $table->date('first_air_date')->nullable(); + $table->json('homepage')->nullable(); + $table->boolean('in_production')->nullable(); + $table->json('languages')->nullable(); + $table->date('last_air_date')->nullable(); + $table->json('name')->nullable(); + $table->integer('number_of_episodes')->nullable(); + $table->integer('number_of_seasons')->nullable(); + $table->json('origin_country')->nullable(); + $table->string('original_language', 2)->nullable(); + $table->string('original_name')->nullable(); + $table->json('overview')->nullable(); + $table->decimal('popularity')->unsigned()->nullable(); + $table->json('poster_path')->nullable(); + $table->json('production_companies')->nullable(); + $table->json('production_countries')->nullable(); + $table->json('spoken_languages')->nullable(); + $table->string('status')->nullable(); + $table->json('tagline')->nullable(); + $table->string('type')->nullable(); + $table->decimal('vote_average')->nullable(); + $table->integer('vote_count')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection(Tv::connection())->dropIfExists(Tv::table()); + } +}; diff --git a/database/migrations/2022_01_19_180000_create_tv_tv_gerne.php b/database/migrations/2022_01_19_180000_create_tv_tv_gerne.php new file mode 100644 index 0000000..174b160 --- /dev/null +++ b/database/migrations/2022_01_19_180000_create_tv_tv_gerne.php @@ -0,0 +1,24 @@ +create('tv_tv_genre', static function (Blueprint $table): void { + $table->foreignId('tv_id')->constrained(Tv::table()); + $table->foreignId('tv_genre_id')->constrained(TvGenre::table()); + + $table->unique(['tv_id', 'tv_genre_id']); + }); + } + + public function down(): void + { + Schema::connection(Tv::connection())->dropIfExists('tv_tv_genre'); + } +}; diff --git a/database/migrations/2022_01_20_121500_create_network_tv_table.php b/database/migrations/2022_01_20_121500_create_network_tv_table.php new file mode 100644 index 0000000..2d8c65d --- /dev/null +++ b/database/migrations/2022_01_20_121500_create_network_tv_table.php @@ -0,0 +1,24 @@ +create('network_tv', static function (Blueprint $table): void { + $table->foreignId('tv_id')->constrained(Tv::table()); + $table->foreignId('network_id')->constrained(Network::table()); + + $table->unique(['tv_id', 'network_id']); + }); + } + + public function down(): void + { + Schema::connection(Tv::connection())->dropIfExists('network_tv'); + } +}; diff --git a/database/migrations/2022_01_20_130000_create_tv_seasons_table.php b/database/migrations/2022_01_20_130000_create_tv_seasons_table.php new file mode 100644 index 0000000..076275c --- /dev/null +++ b/database/migrations/2022_01_20_130000_create_tv_seasons_table.php @@ -0,0 +1,29 @@ +create(TvSeason::table(), static function (Blueprint $table): void { + $table->bigInteger('id')->unsigned()->primary(); + $table->date('air_date')->nullable(); + $table->json('name')->nullable(); + $table->json('overview')->nullable(); + $table->json('poster_path')->nullable(); + $table->integer('season_number')->nullable(); + $table->foreignId('tv_id')->nullable()->constrained(Tv::table()); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection(TvSeason::connection())->dropIfExists(TvSeason::table()); + } +}; diff --git a/database/migrations/2022_01_21_110000_create_tv_episodes_table.php b/database/migrations/2022_01_21_110000_create_tv_episodes_table.php new file mode 100644 index 0000000..a6fecc1 --- /dev/null +++ b/database/migrations/2022_01_21_110000_create_tv_episodes_table.php @@ -0,0 +1,33 @@ +create(TvEpisode::table(), static function (Blueprint $table): void { + $table->bigInteger('id')->unsigned()->primary(); + $table->date('air_date')->nullable(); + $table->json('name')->nullable(); + $table->json('overview')->nullable(); + $table->string('production_code')->nullable(); + $table->integer('season_number')->nullable(); + $table->string('still_path')->nullable(); + $table->decimal('vote_average')->nullable(); + $table->integer('vote_count')->default(0); + $table->foreignId('tv_season_id')->nullable()->constrained(TvSeason::table()); + + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection(TvEpisode::connection())->dropIfExists(TvEpisode::table()); + } +}; diff --git a/src/Eloquent/Builders/NetworkBuilder.php b/src/Eloquent/Builders/NetworkBuilder.php new file mode 100644 index 0000000..3578c12 --- /dev/null +++ b/src/Eloquent/Builders/NetworkBuilder.php @@ -0,0 +1,18 @@ +getParentKey())->send()->json('parts.*.id'); + + return $this->query->findMany($ids, $columns); + } +} diff --git a/src/Eloquent/Relations/HasManyTvSeasons.php b/src/Eloquent/Relations/HasManyTvSeasons.php new file mode 100644 index 0000000..6b9b6ed --- /dev/null +++ b/src/Eloquent/Relations/HasManyTvSeasons.php @@ -0,0 +1,17 @@ +getParentKey())->send()->json('parts.*.id'); + + return $this->query->findMany($ids, $columns); + } +} diff --git a/src/Enums/CreditType.php b/src/Enums/CreditType.php index 22181d1..ea8bf08 100644 --- a/src/Enums/CreditType.php +++ b/src/Enums/CreditType.php @@ -15,6 +15,7 @@ protected static function values(): array return [ 'CAST' => 'cast', 'CREW' => 'crew', + 'GUEST_STARS' => 'guest_stars', ]; } } diff --git a/src/Enums/TvStatus.php b/src/Enums/TvStatus.php new file mode 100644 index 0000000..c120651 --- /dev/null +++ b/src/Enums/TvStatus.php @@ -0,0 +1,35 @@ + 'Rumored', + 'PLANNED' => 'Planned', + 'IN_PRODUCTION' => 'In Production', + 'POST_PRODUCTION' => 'Post Production', + 'RELEASED' => 'Released', + 'CANCELED' => 'Canceled', + 'RETURNING_SERIES' => 'Returning Series', + 'ENDED' => 'Ended', + 'PILOT' => 'Pilot', + // TODO: Implement more values, need testing, no official documentation available + ]; + } +} diff --git a/src/Enums/TvType.php b/src/Enums/TvType.php new file mode 100644 index 0000000..92c807c --- /dev/null +++ b/src/Enums/TvType.php @@ -0,0 +1,33 @@ + 'Scripted', + 'REALITY' => 'Reality', + 'MINISERIES' => 'Miniseries', + 'DOCUMENTARY' => 'Documentary', + 'TALK_SHOW' => 'Talk Show', + 'NEWS' => 'News', + 'VIDEO' => 'Video', + 'PILOT' => 'Pilot', + // TODO: Add more values, need testing, no official documentation available + ]; + } +} diff --git a/src/Images/Still.php b/src/Images/Still.php new file mode 100644 index 0000000..655885d --- /dev/null +++ b/src/Images/Still.php @@ -0,0 +1,27 @@ +size ?? 2000; + } + + public function height(): int + { + return $this->size + ? $this->size / 2 * 3 + : 3000; + } +} diff --git a/src/Models/Movie.php b/src/Models/Movie.php index 7135bdc..d5fd1b1 100644 --- a/src/Models/Movie.php +++ b/src/Models/Movie.php @@ -105,7 +105,7 @@ class Movie extends Model 'release_date' => 'date', 'production_countries' => 'array', 'spoken_languages' => 'array', - 'status' => MovieStatus::class.':nullable', + 'status' => MovieStatus::class . ':nullable', 'collection_id' => 'int', ]; @@ -248,7 +248,7 @@ public function updateFromTmdb(?string $locale = null, array $with = []): bool return false; } - if (! $this->fillFromTmdb($data, $locale)->save()) { + if (!$this->fillFromTmdb($data, $locale)->save()) { return false; } @@ -336,6 +336,10 @@ public function similars(?int $limit): EloquentCollection return static::query()->findMany($ids); } + public function getWatchProvidersAttribute() + { + return $this->watchProviders; + } public function watchProviders(?string $region = null, ?WatchProviderType $type = null): EloquentCollection { return WatchProvider::query()->findMany( diff --git a/src/Models/Network.php b/src/Models/Network.php new file mode 100644 index 0000000..216d147 --- /dev/null +++ b/src/Models/Network.php @@ -0,0 +1,125 @@ + 'int', + 'headquarters' => 'string', + 'homepage' => 'string', + 'logo_path' => 'string', + 'name' => 'string', + 'origin_country' => 'array', + ]; + + public array $translatable = [ + 'name', + 'homepage', + 'logo_path', + ]; + + public function tvs(): BelongsToMany + { + return $this->belongsToMany(Tv::class, 'tv_network'); + } + + public function fillFromTmdb(array $data, ?string $locale = null): static + { + $this->fill([ + 'id' => $data['id'], + 'headquarters' => $data['headquarters'] ?? null, + 'homepage' => $data['homepage'] ?? null, + 'logo_path' => $data['logo_path'] ?? null, + 'name' => $data['name'] ?? null, + 'origin_country' => $data['origin_country'] ?? [], 'iso_3166_1', + ]); + + $locale ??= $this->getLocale(); + + $this->setTranslation('homepage', $locale, trim($data['homepage']) ?: null); + $this->setTranslation('name', $locale, trim($data['name']) ?: null); + $this->setTranslation('logo_path', $locale, trim($data['logo_path']) ?: null); + + return $this; + } + + public function updateFromTmdb(?string $locale = null, array $with = []): bool + { + /*$append = collect($with) + ->map(fn (string $relation) => match ($relation) { + 'cast', 'crew', 'credits' => Details::APPEND_CREDITS, + default => null, + }) + ->filter() + ->unique() + ->values() + ->all();*/ + + $data = rescue( + fn () => Details::request($this->id) + ->language($locale) + //->append(...$append) + ->send() + ->json() + ); + + if ($data === null) { + return false; + } + + if (! $this->fillFromTmdb($data, $locale)->save()) { + return false; + } + + return true; + } + + public function newEloquentBuilder($query): NetworkBuilder + { + return new NetworkBuilder($query); + } + + public function logo(): Logo + { + return new Logo( + $this->logo_path, + $this->name + ); + } +} diff --git a/src/Models/Person.php b/src/Models/Person.php index 135aa7e..8aed4fe 100644 --- a/src/Models/Person.php +++ b/src/Models/Person.php @@ -108,7 +108,7 @@ public function fillFromTmdb(array $data, ?string $locale = null): static 'name' => $data['name'] ?: null, 'also_known_as' => $data['also_known_as'] ?: [], 'homepage' => $data['homepage'] ?: null, - 'imdb_id' => trim($data['imdb_id']) ?: null, + 'imdb_id' => $data['imdb_id'] ? trim($data['imdb_id']) : null, 'birthday' => $data['birthday'] ?: null, 'deathday' => $data['deathday'] ?: null, 'gender' => $data['gender'] ?: 0, @@ -149,7 +149,7 @@ public function updateFromTmdb(?string $locale = null, array $with = []): bool return false; } - if (! $this->fillFromTmdb($data, $locale)->save()) { + if (!$this->fillFromTmdb($data, $locale)->save()) { return false; } diff --git a/src/Models/Tv.php b/src/Models/Tv.php new file mode 100644 index 0000000..800fe4e --- /dev/null +++ b/src/Models/Tv.php @@ -0,0 +1,363 @@ + 'int', + 'episode_run_time' => 'array', + 'languages' => 'array', + 'origin_country' => 'array', + 'vote_count' => 'int', + 'popularity' => 'float', + 'vote_average' => 'float', + 'first_air_date' => 'date', + 'last_air_date' => 'date', + 'production_countries' => 'array', + 'production_companies' => 'array', + 'spoken_languages' => 'array', + 'status' => TvStatus::class . ':nullable', + 'type' => TvType::class . ':nullable', + ]; + + public array $translatable = [ + 'name', + 'tagline', + 'overview', + 'poster_path', + 'homepage', + ]; + + public static function popular(?int $limit): EloquentCollection + { + $ids = Popular::request() + ->cursor() + ->when($limit, fn (LazyCollection $collection) => $collection->take($limit)) + ->pluck('id'); + + return static::query()->findMany($ids); + } + + public static function toprated(?int $limit): EloquentCollection + { + $ids = TopRated::request() + ->cursor() + ->when($limit, fn (LazyCollection $collection) => $collection->take($limit)) + ->pluck('id'); + + return static::query()->findMany($ids); + } + + public function genres(): BelongsToMany + { + return $this->belongsToMany(TvGenre::class, 'tv_tv_genre'); + } + + public function networks(): BelongsToMany + { + return $this->belongsToMany(Network::class, 'network_tv'); + } + + public function collection(): BelongsTo + { + return $this->belongsTo(Collection::class); + } + + public function credits(): MorphManyCredits + { + /** @var \Astrotomic\Tmdb\Models\Credit $instance */ + $instance = $this->newRelatedInstance(Credit::class); + + return new MorphManyCredits( + $instance->newQuery(), + $this, + $instance->qualifyColumn('media_type'), + $instance->qualifyColumn('media_id'), + $this->getKeyName() + ); + } + + public function cast(): MorphManyCredits + { + return $this->credits()->whereCreditType(CreditType::CAST()); + } + + public function crew(): MorphManyCredits + { + return $this->credits()->whereCreditType(CreditType::CREW()); + } + + public function seasons(): HasManyTvSeasons + { + /** @var \Astrotomic\Tmdb\Models\TvSeason $instance */ + $instance = $this->newRelatedInstance(TvSeason::class); + + return new HasManyTvSeasons( + $instance->newQuery(), + $this, + $instance->qualifyColumn($this->getForeignKey()), + $this->getKeyName() + ); + } + + public function fillFromTmdb(array $data, ?string $locale = null): static + { + $this->fill([ + 'id' => $data['id'], + 'backdrop_path' => $data['backdrop_path'] ?: null, + 'episode_run_time' => $data['episode_run_time'] ?: null, + 'first_air_date' => $data['first_air_date'] ?: null, + 'homepage' => $data['homepage'] ?: null, + 'in_production' => $data['in_production'] ?: null, + 'languages' => $data['languages'] ?: null, + 'last_air_date' => $data['last_air_date'] ?: null, + 'name' => $data['name'] ?: null, + 'number_of_episodes' => $data['number_of_episodes'] ?: null, + 'number_of_seasons' => $data['number_of_seasons'] ?: null, + 'origin_country' => array_column($data['origin_country'] ?? [], 'iso_3166_1'), + 'original_language' => $data['original_language'] ?: null, + 'original_name' => $data['original_name'] ?: null, + 'overview' => $data['overview'] ?: null, + 'popularity' => $data['popularity'] ?: null, + 'poster_path' => $data['poster_path'] ?: null, + 'production_companies' => array_column($data['production_companies'] ?? [], 'name'), + 'production_countries' => array_column($data['production_countries'] ?: [], 'iso_3166_1'), + 'spoken_languages' => array_column($data['spoken_languages'] ?: [], 'iso_639_1'), + 'status' => $data['status'] ?: null, + 'tagline' => $data['tagline'] ?: null, + 'type' => $data['type'] ?: null, + 'vote_average' => $data['vote_average'] ?: null, + 'vote_count' => $data['vote_count'] ?: 0, + + ]); + + $locale ??= $this->getLocale(); + + $this->setTranslation('overview', $locale, trim($data['overview']) ?: null); + $this->setTranslation('tagline', $locale, trim($data['tagline']) ?: null); + $this->setTranslation('name', $locale, trim($data['name']) ?: null); + $this->setTranslation('poster_path', $locale, trim($data['poster_path']) ?: null); + + return $this; + } + + public function updateFromTmdb(?string $locale = null, array $with = []): bool + { + /*$append = collect($with) + ->map(fn (string $relation) => match ($relation) { + 'networks' => Details::APPEND_NETWORKS, + default => null, + }) + ->filter() + ->unique() + ->values() + ->all();*/ + + $data = rescue( + fn () => Details::request($this->id) + ->language($locale) + //->append(...$append) + ->send() + ->json() + ); + + if ($data === null) { + return false; + } + + if (!$this->fillFromTmdb($data, $locale)->save()) { + return false; + } + + $this->genres()->sync( + collect($data['genres'] ?: []) + ->map(static function (array $data) use ($locale): TvGenre { + $genre = TvGenre::query()->findOrNew($data['id']); + $genre->fillFromTmdb($data, $locale)->save(); + + return $genre; + }) + ->pluck('id') + ); + + /*$this->networks()->sync( + collect($data['networks'] ?: []) + ->map(static function (array $data) use ($locale): Network { + $network = Network::query()->findOrNew($data['id']); + $network->fillFromTmdb($data, $locale)->save(); + + return $network; + }) + ->pluck('id') + );*/ + + + /*if (isset($data['seasons'])) { + $this->seasons()->saveMany( + (collect($data['seasons']) + ->map(static function (array $data) use ($locale): TvSeason { + $season = TvSeason::query()->findOrNew($data['id']); + $season->fillFromTmdb($data, $locale)->save(); + + //$seasonFetch = TvSeason::query()->updateFromTmdb($locale, [$season->tv_id, $season->season_number]); + //ray($seasonFetch); + + return $season; + }) + ->all()) + ); + }*/ + + /*if ($data['belongs_to_collection']) { + $this->collection()->associate( + Collection::query()->findOrFail($data['belongs_to_collection']['id']) + )->save(); + }*/ + + /*if (isset($data['credits'])) { + if (in_array('credits', $with) || in_array('cast', $with)) { + foreach ($data['credits']['cast'] as $cast) { + Credit::query()->findOrFail($cast['credit_id']); + } + } + + if (in_array('credits', $with) || in_array('crew', $with)) { + foreach ($data['credits']['crew'] as $crew) { + Credit::query()->findOrFail($crew['credit_id']); + } + } + }*/ + + return true; + } + + public function newEloquentBuilder($query): TvBuilder + { + return new TvBuilder($query); + } + + public function runtime(): ?CarbonInterval + { + if ($this->runtime === null) { + return null; + } + + return CarbonInterval::minutes($this->runtime)->cascade(); + } + + public function poster(): Poster + { + return new Poster( + $this->poster_path, + $this->title + ); + } + + public function backdrop(): Backdrop + { + return new Backdrop( + $this->backdrop_path, + $this->title + ); + } + + public function recommendations(?int $limit): EloquentCollection + { + $ids = Recommendations::request($this->id) + ->cursor() + ->when($limit, fn (LazyCollection $collection) => $collection->take($limit)) + ->pluck('id'); + + return static::query()->findMany($ids); + } + + public function similars(?int $limit): EloquentCollection + { + $ids = Similars::request($this->id) + ->cursor() + ->when($limit, fn (LazyCollection $collection) => $collection->take($limit)) + ->pluck('id'); + + return static::query()->findMany($ids); + } + + //TODO: Make TopRated, Popular, OnTheAir, AiringToday + + public function watchProviders(?string $region = null, ?WatchProviderType $type = null): EloquentCollection + { + return WatchProvider::query()->findMany( + WatchProviders::request($this->id)->send()->collect(sprintf( + 'results.%s.%s.*.provider_id', + $region ?? '*', + $type?->value ?? '*' + )) + ); + } +} diff --git a/src/Models/TvEpisode.php b/src/Models/TvEpisode.php new file mode 100644 index 0000000..c678700 --- /dev/null +++ b/src/Models/TvEpisode.php @@ -0,0 +1,217 @@ + 'int', + 'air_date' => 'date', + 'episode_number' => 'int', + 'name' => 'string', + 'overview' => 'string', + 'production_code' => 'string', + 'season_number' => 'int', + 'still_path' => 'string', + 'vote_average' => 'float', + 'vote_count' => 'int', + 'tv_season_id' => 'int', + ]; + + public array $translatable = [ + 'name', + 'overview', + 'still_path', + ]; + + public function season(): BelongsTo + { + return $this->belongsTo(TvSeason::class, 'tv_season_id'); + } + + public function credits(): MorphManyCredits + { + /** @var \Astrotomic\Tmdb\Models\Credit $instance */ + $instance = $this->newRelatedInstance(Credit::class); + + return new MorphManyCredits( + $instance->newQuery(), + $this, + $instance->qualifyColumn('media_type'), + $instance->qualifyColumn('media_id'), + $this->getKeyName() + ); + } + + public function guest_stars(): MorphManyCredits + { + return $this->guest_stars()->whereCreditType(CreditType::GUEST_STARS()); + } + + public function cast(): MorphManyCredits + { + return $this->credits()->whereCreditType(CreditType::CAST()); + } + + public function crew(): MorphManyCredits + { + return $this->credits()->whereCreditType(CreditType::CREW()); + } + + public function fillFromTmdb(array $data, ?string $locale = null): static + { + $this->fill([ + 'id' => $data['id'], + 'air_date' => $data['air_date'] ?: null, + 'episode_number' => $data['episode_number'] ?: null, + 'name' => $data['name'] ?: null, + 'overview' => $data['overview'] ?: null, + 'production_code' => $data['production_code'] ?: null, + 'season_number' => $data['season_number'] ?: null, + 'still_path' => $data['still_path'] ?: null, + 'vote_average' => $data['vote_average'] ?: null, + 'vote_count' => $data['vote_count'] ?: null, + ]); + + $locale ??= $this->getLocale(); + + $this->setTranslation('overview', $locale, trim($data['overview']) ?: null); + $this->setTranslation('name', $locale, trim($data['title']) ?: null); + $this->setTranslation('still_path', $locale, trim($data['poster_path']) ?: null); + + return $this; + } + + public function updateFromTmdb(?string $locale = null, array $with = []): bool + { + $append = collect($with) + ->map(fn (string $relation) => match ($relation) { + 'cast', 'crew', 'credits' => Details::APPEND_CREDITS, + default => null, + }) + ->filter() + ->unique() + ->values() + ->all(); + + $data = rescue( + fn () => Details::request($this->id) + ->language($locale) + ->append(...$append) + ->send() + ->json() + ); + + if ($data === null) { + return false; + } + + if (!$this->fillFromTmdb($data, $locale)->save()) { + return false; + } + + $this->genres()->sync( + collect($data['genres'] ?: []) + ->map(static function (array $data) use ($locale): MovieGenre { + $genre = MovieGenre::query()->findOrNew($data['id']); + $genre->fillFromTmdb($data, $locale)->save(); + + return $genre; + }) + ->pluck('id') + ); + + if ($data['belongs_to_collection']) { + $this->collection()->associate( + Collection::query()->findOrFail($data['belongs_to_collection']['id']) + )->save(); + } + + if (isset($data['credits'])) { + if (in_array('credits', $with) || in_array('cast', $with)) { + foreach ($data['credits']['cast'] as $cast) { + Credit::query()->findOrFail($cast['credit_id']); + } + } + + if (in_array('credits', $with) || in_array('crew', $with)) { + foreach ($data['credits']['crew'] as $crew) { + Credit::query()->findOrFail($crew['credit_id']); + } + } + } + + return true; + } + + public function newEloquentBuilder($query): TvEpisodeBuilder + { + return new TvEpisodeBuilder($query); + } + + public function runtime(): ?CarbonInterval + { + if ($this->runtime === null) { + return null; + } + + return CarbonInterval::minutes($this->runtime)->cascade(); + } +} diff --git a/src/Models/TvGenre.php b/src/Models/TvGenre.php new file mode 100644 index 0000000..51eb1a1 --- /dev/null +++ b/src/Models/TvGenre.php @@ -0,0 +1,96 @@ + 'int', + ]; + + public array $translatable = [ + 'name', + ]; + + public static function all($columns = ['*']): EloquentCollection + { + $data = rescue(fn () => ListAll::request()->send()->collect('genres')); + + if ($data instanceof Collection) { + $data->each(fn (array $genre) => static::query()->updateOrCreate( + ['id' => $genre['id']], + ['name' => $genre['name']], + )); + } + + return parent::all($columns); + } + + public function tvs(): BelongsToMany + { + return $this->belongsToMany(Tv::class, 'tv_tv_genre'); + } + + public function fillFromTmdb(array $data, ?string $locale = null): static + { + $genre = $this->fill([ + 'id' => $data['id'], + ]); + + $locale ??= $this->getLocale(); + + $this->setTranslation('name', $locale, trim($data['name']) ?: null); + + return $genre; + } + + public function updateFromTmdb(?string $locale = null, array $with = []): bool + { + $data = rescue(fn () => ListAll::request()->language($locale)->send()->collect('genres')); + + if ($data === null) { + return false; + } + + $data = $data->keyBy('id'); + + if (! $data->has($this->id)) { + return false; + } + + return $this->fillFromTmdb($data->get($this->id), $locale)->save(); + } + + public function newEloquentBuilder($query): TvGenreBuilder + { + return new TvGenreBuilder($query); + } +} diff --git a/src/Models/TvSeason.php b/src/Models/TvSeason.php new file mode 100644 index 0000000..ee62b19 --- /dev/null +++ b/src/Models/TvSeason.php @@ -0,0 +1,150 @@ + 'int', + 'air_date' => 'string', + 'name' => 'string', + 'overview' => 'string', + 'poster_path' => 'string', + 'season_number' => 'int', + ]; + + public array $translatable = [ + 'name', + 'overview', + 'poster_path', + ]; + + public function episodes(): HasManyTvEpisodes + { + /** @var \Astrotomic\Tmdb\Models\TvEpisode $instance */ + $instance = $this->newRelatedInstance(TvEpisode::class); + + return new HasManyTvEpisodes( + $instance->newQuery(), + $this, + $instance->qualifyColumn($this->getForeignKey()), + $this->getKeyName() + ); + } + + public function fillFromTmdb(array $data, ?string $locale = null): static + { + $this->fill([ + 'id' => $data['id'], + 'air_date' => $data['air_date'] ?: null, + 'name' => $data['name'] ?: null, + 'overview' => $data['overview'] ?: null, + 'poster_path' => $data['poster_path'] ?: null, + 'season_number' => $data['season_number'] ?: null, + ]); + + $locale ??= $this->getLocale(); + + $this->setTranslation('overview', $locale, trim($data['overview']) ?: null); + $this->setTranslation('name', $locale, trim($data['name']) ?: null); + $this->setTranslation('poster_path', $locale, trim($data['poster_path']) ?: null); + + return $this; + } + + public function updateFromTmdb(?string $locale = null, array $with = []): bool + { + /*$append = collect($with) + ->map(fn (string $relation) => match ($relation) { + 'networks' => Details::APPEND_NETWORKS, + default => null, + }) + ->filter() + ->unique() + ->values() + ->all();*/ + + $data = rescue( + fn () => Details::request($this->tv_id, $this->season_number) + ->language($locale) + //->append(...$append) + ->send() + ->json() + ); + + if ($data === null) { + return false; + } + + if (!$this->fillFromTmdb($data, $locale)->save()) { + return false; + } + + /*if (isset($data['episodes'])) { + $this->seasons()->saveMany( + (collect($data['episodes']) + ->map(static function (array $data) use ($locale): TvEpisode { + $season = TvEpisode::query()->findOrNew($data['id']); + $season->fillFromTmdb($data, $locale)->save(); + + return $season; + }) + ->all()) + ); + }*/ + + if ($data['belongs_to_tv']) { + $this->tv()->associate( + Tv::query()->findOrFail($data['belongs_to_tv']['id']) + )->save(); + } + + return true; + } + + public function newEloquentBuilder($query): TvSeasonBuilder + { + return new TvSeasonBuilder($query); + } + + public function poster(): Poster + { + return new Poster( + $this->poster_path, + $this->title + ); + } +} diff --git a/src/Models/backend-nospoiler.code-workspace b/src/Models/backend-nospoiler.code-workspace new file mode 100644 index 0000000..7f8f070 --- /dev/null +++ b/src/Models/backend-nospoiler.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "../../../../.." + }, + { + "path": "../.." + } + ], + "settings": { + "workbench.preferredDarkColorTheme": "Monokai Dimmed", + "workbench.colorTheme": "One Dark Pro Darker" + } +} \ No newline at end of file diff --git a/src/Requests/Network/Details.php b/src/Requests/Network/Details.php new file mode 100644 index 0000000..fc61a51 --- /dev/null +++ b/src/Requests/Network/Details.php @@ -0,0 +1,32 @@ +request->get( + sprintf('/network/%d', $this->networkId), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'append_to_response' => implode(',', $this->append), + ]) + )->throw(); + } +} diff --git a/src/Requests/Tv/Credits.php b/src/Requests/Tv/Credits.php new file mode 100644 index 0000000..0a7be3f --- /dev/null +++ b/src/Requests/Tv/Credits.php @@ -0,0 +1,31 @@ +request->get( + sprintf('/tv/%d/credits', $this->tvId), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + ]) + )->throw(); + } +} diff --git a/src/Requests/Tv/Details.php b/src/Requests/Tv/Details.php new file mode 100644 index 0000000..e1d26f8 --- /dev/null +++ b/src/Requests/Tv/Details.php @@ -0,0 +1,34 @@ +request->get( + sprintf('/tv/%d', $this->tvId), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'append_to_response' => implode(',', $this->append), + ]) + )->throw(); + } +} diff --git a/src/Requests/Tv/Popular.php b/src/Requests/Tv/Popular.php new file mode 100644 index 0000000..f12da07 --- /dev/null +++ b/src/Requests/Tv/Popular.php @@ -0,0 +1,61 @@ +page = $page; + + return $this; + } + + public function region(string $region): static + { + $this->region = $region; + + return $this; + } + + public function send(): Response + { + return $this->request->get( + '/tv/popular', + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'region' => $this->region ?? Tmdb::region(), + 'page' => $this->page, + ]) + )->throw(); + } + + public function cursor(): LazyCollection + { + return LazyCollection::make(function (): Generator { + $this->page = 1; + do { + $response = $this->send()->json(); + + yield from $response['results']; + + $totalPages = $response['total_pages']; + $this->page++; + } while ($this->page <= $totalPages); + }); + } +} diff --git a/src/Requests/Tv/Recommendations.php b/src/Requests/Tv/Recommendations.php new file mode 100644 index 0000000..f48b40f --- /dev/null +++ b/src/Requests/Tv/Recommendations.php @@ -0,0 +1,58 @@ +page = $page; + + return $this; + } + + public function send(): Response + { + return $this->request->get( + sprintf('/tv/%d/recommendations', $this->tvId), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'page' => $this->page, + ]) + )->throw(); + } + + public function cursor(): LazyCollection + { + return LazyCollection::make(function (): Generator { + $this->page = 1; + do { + $response = $this->send()->json(); + + yield from $response['results']; + + $totalPages = $response['total_pages']; + $this->page++; + } while ($this->page <= $totalPages); + }); + } +} diff --git a/src/Requests/Tv/Similars.php b/src/Requests/Tv/Similars.php new file mode 100644 index 0000000..e7bb44c --- /dev/null +++ b/src/Requests/Tv/Similars.php @@ -0,0 +1,58 @@ +page = $page; + + return $this; + } + + public function send(): Response + { + return $this->request->get( + sprintf('/tv/%d/similar', $this->tvId), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'page' => $this->page, + ]) + )->throw(); + } + + public function cursor(): LazyCollection + { + return LazyCollection::make(function (): Generator { + $this->page = 1; + do { + $response = $this->send()->json(); + + yield from $response['results']; + + $totalPages = $response['total_pages']; + $this->page++; + } while ($this->page <= $totalPages); + }); + } +} diff --git a/src/Requests/Tv/TopRated.php b/src/Requests/Tv/TopRated.php new file mode 100644 index 0000000..8f9b3c6 --- /dev/null +++ b/src/Requests/Tv/TopRated.php @@ -0,0 +1,61 @@ +page = $page; + + return $this; + } + + public function region(string $region): static + { + $this->region = $region; + + return $this; + } + + public function send(): Response + { + return $this->request->get( + '/tv/top_rated', + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'region' => $this->region ?? Tmdb::region(), + 'page' => $this->page, + ]) + )->throw(); + } + + public function cursor(): LazyCollection + { + return LazyCollection::make(function (): Generator { + $this->page = 1; + do { + $response = $this->send()->json(); + + yield from $response['results']; + + $totalPages = $response['total_pages']; + $this->page++; + } while ($this->page <= $totalPages); + }); + } +} diff --git a/src/Requests/Tv/WatchProviders.php b/src/Requests/Tv/WatchProviders.php new file mode 100644 index 0000000..c7e5eb3 --- /dev/null +++ b/src/Requests/Tv/WatchProviders.php @@ -0,0 +1,27 @@ +request->get( + sprintf('/tv/%d/watch/providers', $this->tvId), + )->throw(); + } +} diff --git a/src/Requests/TvEpisode/Details.php b/src/Requests/TvEpisode/Details.php new file mode 100644 index 0000000..9511ec1 --- /dev/null +++ b/src/Requests/TvEpisode/Details.php @@ -0,0 +1,36 @@ +request->get( + sprintf('/tv/%d/season/%d/episode/%d', [$this->tvId, $this->tvSeasonNumber, $this->tvEpisodeNumber]), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'append_to_response' => implode(',', $this->append), + ]) + )->throw(); + } +} diff --git a/src/Requests/TvGenre/ListAll.php b/src/Requests/TvGenre/ListAll.php new file mode 100644 index 0000000..c709369 --- /dev/null +++ b/src/Requests/TvGenre/ListAll.php @@ -0,0 +1,25 @@ +request->get( + '/genre/tv/list', + [ + 'language' => $this->language ?? Tmdb::language(), + ] + )->throw(); + } +} diff --git a/src/Requests/TvSeason/Details.php b/src/Requests/TvSeason/Details.php new file mode 100644 index 0000000..0fc451f --- /dev/null +++ b/src/Requests/TvSeason/Details.php @@ -0,0 +1,33 @@ +request->get( + sprintf('/tv/%d/season/%d', [$this->tvId, $this->tvSeasonNumber]), + array_filter([ + 'language' => $this->language ?? Tmdb::language(), + 'append_to_response' => implode(',', $this->append), + ]) + )->throw(); + } +} diff --git a/tests/Datasets/Builder.php b/tests/Datasets/Builder.php index fbbbc5c..dd399f3 100644 --- a/tests/Datasets/Builder.php +++ b/tests/Datasets/Builder.php @@ -4,6 +4,8 @@ use Astrotomic\Tmdb\Models\Movie; use Astrotomic\Tmdb\Models\MovieGenre; use Astrotomic\Tmdb\Models\Person; +use Astrotomic\Tmdb\Models\Tv; +use Astrotomic\Tmdb\Models\TvGenre; use Astrotomic\Tmdb\Models\WatchProvider; use Illuminate\Support\Collection; use Illuminate\Support\Fluent; @@ -14,6 +16,8 @@ MovieGenre::class => 35, Person::class => 561, Credit::class => '5a30d4a40e0a264cbe180b27', + TvGenre::class => 10751, + Tv::class => 63174, WatchProvider::class => 8, \Astrotomic\Tmdb\Models\Collection::class => 529892, ]; @@ -29,6 +33,8 @@ MovieGenre::class => [35, 99], Person::class => [561, 10393], Credit::class => ['5a30d4a40e0a264cbe180b27', '5bb637c10e0a2633a7011036'], + TvGenre::class => [10751, 10759], + Tv::class => [63174, 71914], WatchProvider::class => [8, 9], \Astrotomic\Tmdb\Models\Collection::class => [529892, 748], ]; @@ -46,6 +52,8 @@ MovieGenre::class => [35, 0], Person::class => [561, 0], Credit::class => ['5a30d4a40e0a264cbe180b27', ''], + TvGenre::class => [10751, 0], + Tv::class => [63174, 0], WatchProvider::class => [8, 0], \Astrotomic\Tmdb\Models\Collection::class => [529892, 0], ]; @@ -63,6 +71,8 @@ MovieGenre::class => [], Person::class => [], Credit::class => [], + TvGenre::class => [], + Tv::class => [], WatchProvider::class => [], \Astrotomic\Tmdb\Models\Collection::class => [], ]; diff --git a/tests/Feature/Models/TvGenreTest.php b/tests/Feature/Models/TvGenreTest.php new file mode 100644 index 0000000..29fec69 --- /dev/null +++ b/tests/Feature/Models/TvGenreTest.php @@ -0,0 +1,73 @@ + 10762]); + $genre->updateFromTmdb(); + + Assert::assertSame(10762, $genre->id); + Assert::assertSame('Kids', $genre->name); +}); + +it('translates to undefined language on the fly', function (): void { + $genre = TvGenre::query()->find(16); + + expect($genre) + ->toBeInstanceOf(TvGenre::class) + ->exists->toBeTrue() + ->wasRecentlyCreated->toBeTrue() + ->id->toBe(35) + ->translate('name', 'it')->toBe('Commedia') + ->translate('name', 'en')->toBe('Comedy'); +}); + +it('does not translate empty language', function (): void { + $genre = TvGenre::query()->find(35); + $genre->setTranslation('name', 'en', null)->save(); + + expect($genre) + ->toBeInstanceOf(TvGenre::class) + ->exists->toBeTrue() + ->wasRecentlyCreated->toBeTrue() + ->id->toBe(35) + ->translate('name', 'it')->toBe('Commedia') + ->translate('name', 'en')->toBeNull(); +}); + +it('does return fallback for empty language', function (): void { + app()->setLocale('en'); + $genre = TvGenre::query()->find(35); + $genre->setTranslation('name', 'it', null)->save(); + + expect($genre) + ->toBeInstanceOf(TvGenre::class) + ->exists->toBeTrue() + ->wasRecentlyCreated->toBeTrue() + ->id->toBe(35) + ->translate('name', 'it', true)->toBe('Comedy') + ->translate('name', 'en', true)->toBe('Comedy'); +}); + +it('does return fallback for unknown language', function (): void { + app()->setLocale('en'); + $genre = TvGenre::query()->find(35); + + expect($genre) + ->toBeInstanceOf(TvGenre::class) + ->exists->toBeTrue() + ->wasRecentlyCreated->toBeTrue() + ->id->toBe(35) + ->translate('name', 'foo', true)->toBe('Comedy') + ->translate('name', 'en', true)->toBe('Comedy'); +}); + +it('loads all genres from tmdb', function (): void { + $genres = TvGenre::all(); + + expect($genres) + ->toBeInstanceOf(EloquentCollection::class) + ->toHaveCount(16); +}); diff --git a/tests/Feature/Requests/ListMovieGenresTest.php b/tests/Feature/Requests/ListMovieGenresTest.php index 7a79f59..ca0022c 100644 --- a/tests/Feature/Requests/ListMovieGenresTest.php +++ b/tests/Feature/Requests/ListMovieGenresTest.php @@ -1,8 +1,8 @@ send()->json(); expect($data) diff --git a/tests/Pest.php b/tests/Pest.php index 506dfd4..62c5fdf 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -14,11 +14,17 @@ use Astrotomic\PhpunitAssertions\UrlAssertions; use Astrotomic\Tmdb\Models\Model; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Http; use Pest\Expectation; uses(\Tests\Feature\TestCase::class)->in('Feature'); uses(\Tests\TestCase::class)->in('Live'); +class Pest +{ + use UrlAssertions; +} + /* |-------------------------------------------------------------------------- | Expectations @@ -26,13 +32,16 @@ */ expect()->extend('assert', function (Closure $assertions): Expectation { + $helper = new Pest(); + $assertions = $assertions->bindTo($helper, $helper); $assertions($this->value); return $this; }); expect()->extend('toBeUrl', function (string $expected): Expectation { - UrlAssertions::assertValidLoose($this->value); + $helper = new Pest(); + $helper->assertValidLoose($this->value); return $this->toBe($expected); }); diff --git a/tests/fixtures/genre/tv/list/language=en.json b/tests/fixtures/genre/tv/list/language=en.json new file mode 100644 index 0000000..aa9b7a4 --- /dev/null +++ b/tests/fixtures/genre/tv/list/language=en.json @@ -0,0 +1,68 @@ +{ + "genres": [ + { + "id": 16, + "name": "Animation" + }, + { + "id": 18, + "name": "Drama" + }, + { + "id": 35, + "name": "Comedy" + }, + { + "id": 37, + "name": "Western" + }, + { + "id": 80, + "name": "Crime" + }, + { + "id": 99, + "name": "Documentary" + }, + { + "id": 9648, + "name": "Mistery" + }, + { + "id": 10751, + "name": "Family" + }, + { + "id": 10759, + "name": "Action & Adventure" + }, + { + "id": 10762, + "name": "Kids" + }, + { + "id": 10763, + "name": "News" + }, + { + "id": 10764, + "name": "Reality" + }, + { + "id": 10765, + "name": "Sci-Fi & Fantasy" + }, + { + "id": 10766, + "name": "Soap" + }, + { + "id": 10767, + "name": "Talk" + }, + { + "id": 10768, + "name": "War & Politics" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/genre/tv/list/language=foo.json b/tests/fixtures/genre/tv/list/language=foo.json new file mode 100644 index 0000000..aa9b7a4 --- /dev/null +++ b/tests/fixtures/genre/tv/list/language=foo.json @@ -0,0 +1,68 @@ +{ + "genres": [ + { + "id": 16, + "name": "Animation" + }, + { + "id": 18, + "name": "Drama" + }, + { + "id": 35, + "name": "Comedy" + }, + { + "id": 37, + "name": "Western" + }, + { + "id": 80, + "name": "Crime" + }, + { + "id": 99, + "name": "Documentary" + }, + { + "id": 9648, + "name": "Mistery" + }, + { + "id": 10751, + "name": "Family" + }, + { + "id": 10759, + "name": "Action & Adventure" + }, + { + "id": 10762, + "name": "Kids" + }, + { + "id": 10763, + "name": "News" + }, + { + "id": 10764, + "name": "Reality" + }, + { + "id": 10765, + "name": "Sci-Fi & Fantasy" + }, + { + "id": 10766, + "name": "Soap" + }, + { + "id": 10767, + "name": "Talk" + }, + { + "id": 10768, + "name": "War & Politics" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/genre/tv/list/language=it.json b/tests/fixtures/genre/tv/list/language=it.json new file mode 100644 index 0000000..122e91b --- /dev/null +++ b/tests/fixtures/genre/tv/list/language=it.json @@ -0,0 +1,68 @@ +{ + "genres": [ + { + "id": 16, + "name": "Animazione" + }, + { + "id": 18, + "name": "Dramma" + }, + { + "id": 35, + "name": "Commedia" + }, + { + "id": 37, + "name": "Western" + }, + { + "id": 80, + "name": "Crime" + }, + { + "id": 99, + "name": "Documentario" + }, + { + "id": 9648, + "name": "Mistero" + }, + { + "id": 10751, + "name": "Famiglia" + }, + { + "id": 10759, + "name": "Action & Adventure" + }, + { + "id": 10762, + "name": "Kids" + }, + { + "id": 10763, + "name": "News" + }, + { + "id": 10764, + "name": "Reality" + }, + { + "id": 10765, + "name": "Sci-Fi & Fantasy" + }, + { + "id": 10766, + "name": "Soap" + }, + { + "id": 10767, + "name": "Talk" + }, + { + "id": 10768, + "name": "War & Politics" + } + ] +} \ No newline at end of file