Files
UNIT3D-Community-Edition/app/Models/Torrent.php
Roardom ad716d7cc3 update: remove XSS cleaner and remove XSS vulnerabilities
We've been mostly relying on the 3rd party xss cleaner to make sure user submitted content is clean. This PR fixes up any leftover holes in the bbcode parser that allow xss vulnerabilities, and as a result, the 3rd party library isn't needed anymore. It cleans responsibly by first, running `htmlspecialchars()` over the content, followed by sanitizing the untrusted urls and whitelisting their protocol.
2025-01-20 02:52:42 +00:00

968 lines
33 KiB
PHP

<?php
declare(strict_types=1);
/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author HDVinnie <hdinnovations@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/
namespace App\Models;
use App\Helpers\Bbcode;
use App\Helpers\Linkify;
use App\Helpers\StringHelper;
use App\Models\Scopes\ApprovedScope;
use App\Notifications\NewComment;
use App\Notifications\NewThank;
use App\Traits\Auditable;
use App\Traits\GroupedLastScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Scout\Searchable;
/**
* App\Models\Torrent.
*
* @property string $info_hash
* @property int $id
* @property string $name
* @property string $description
* @property string|null $mediainfo
* @property string|null $bdinfo
* @property string $file_name
* @property int $num_file
* @property string|null $folder
* @property float $size
* @property mixed|null $nfo
* @property int $leechers
* @property int $seeders
* @property int $times_completed
* @property int|null $category_id
* @property int $user_id
* @property int $imdb
* @property int $tvdb
* @property int $tmdb
* @property int $mal
* @property int $igdb
* @property int|null $season_number
* @property int|null $episode_number
* @property int $stream
* @property int $free
* @property bool $doubleup
* @property bool $refundable
* @property int $highspeed
* @property bool $featured
* @property int $status
* @property \Illuminate\Support\Carbon|null $moderated_at
* @property int|null $moderated_by
* @property int $anon
* @property bool $sticky
* @property int $sd
* @property int $internal
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $bumped_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $fl_until
* @property \Illuminate\Support\Carbon|null $du_until
* @property int $type_id
* @property int|null $resolution_id
* @property int|null $distributor_id
* @property int|null $region_id
* @property int $personal_release
* @property int|null $balance
* @property int|null $balance_offset
*/
class Torrent extends Model
{
use Auditable;
use GroupedLastScope;
/** @use HasFactory<\Database\Factories\TorrentFactory> */
use HasFactory;
use Searchable;
use SoftDeletes;
protected $guarded = [];
/**
* Get the attributes that should be cast.
*
* @return array{tmdb: 'int', igdb: 'int', bumped_at: 'datetime', fl_until: 'datetime', du_until: 'datetime', doubleup: 'bool', refundable: 'bool', featured: 'bool', moderated_at: 'datetime', sticky: 'bool'}
*/
protected function casts(): array
{
return [
'tmdb' => 'int',
'igdb' => 'int',
'bumped_at' => 'datetime',
'fl_until' => 'datetime',
'du_until' => 'datetime',
'doubleup' => 'bool',
'refundable' => 'bool',
'featured' => 'bool',
'moderated_at' => 'datetime',
'sticky' => 'bool',
];
}
/**
* The attributes that should not be included in audit log.
*
* @var string[]
*/
protected $discarded = [
'info_hash',
];
final public const PENDING = 0;
final public const APPROVED = 1;
final public const REJECTED = 2;
final public const POSTPONED = 3;
/**
* This query is to be added to a raw select from the torrents table.
*
* The fields it returns are used by Meilisearch to power the advanced
* torrent search, quick search, RSS, and the API.
*/
public const string SEARCHABLE = <<<'SQL'
torrents.id,
torrents.name,
torrents.description,
torrents.mediainfo,
torrents.bdinfo,
torrents.num_file,
torrents.folder,
torrents.size,
torrents.leechers,
torrents.seeders,
torrents.times_completed,
UNIX_TIMESTAMP(torrents.created_at) AS created_at,
UNIX_TIMESTAMP(torrents.bumped_at) AS bumped_at,
UNIX_TIMESTAMP(torrents.fl_until) AS fl_until,
UNIX_TIMESTAMP(torrents.du_until) AS du_until,
torrents.user_id,
torrents.imdb,
torrents.tvdb,
torrents.tmdb,
torrents.mal,
torrents.igdb,
torrents.season_number,
torrents.episode_number,
torrents.stream,
torrents.free,
torrents.doubleup,
torrents.refundable,
torrents.highspeed,
torrents.featured,
torrents.status,
torrents.anon,
torrents.sticky,
torrents.sd,
torrents.internal,
UNIX_TIMESTAMP(torrents.deleted_at) AS deleted_at,
torrents.distributor_id,
torrents.region_id,
torrents.personal_release,
LOWER(HEX(torrents.info_hash)) AS info_hash,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND seeder = 1
) AS json_history_seeders,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND seeder = 0
) AS json_history_leechers,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND active = 1
) AS json_history_active,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND active = 0
) AS json_history_inactive,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND completed_at IS NOT NULL
) AS json_history_complete,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', history.user_id
)), JSON_ARRAY())
FROM history
WHERE torrents.id = history.torrent_id
AND completed_at IS NULL
) AS json_history_incomplete,
(
SELECT JSON_OBJECT(
'id', users.id,
'username', users.username,
'group', (
SELECT JSON_OBJECT(
'name', "groups".name,
'color', "groups".color,
'icon', "groups".icon,
'effect', "groups".effect
)
FROM "groups"
WHERE "groups".id = users.group_id
LIMIT 1
)
)
FROM users
WHERE torrents.user_id = users.id
LIMIT 1
) AS json_user,
(
SELECT JSON_OBJECT(
'id', categories.id,
'name', categories.name,
'image', categories.image,
'icon', categories.icon,
'no_meta', categories.no_meta != 0,
'music_meta', categories.music_meta != 0,
'game_meta', categories.game_meta != 0,
'tv_meta', categories.tv_meta != 0,
'movie_meta', categories.movie_meta != 0
)
FROM categories
WHERE torrents.category_id = categories.id
LIMIT 1
) AS json_category,
(
SELECT JSON_OBJECT(
'id', types.id,
'name', types.name
)
FROM types
WHERE torrents.type_id = types.id
LIMIT 1
) AS json_type,
(
SELECT JSON_OBJECT(
'id', resolutions.id,
'name', resolutions.name
)
FROM resolutions
WHERE torrents.resolution_id = resolutions.id
LIMIT 1
) AS json_resolution,
(
SELECT vote_average
FROM movies
WHERE
torrents.tmdb = movies.id
AND torrents.category_id in (
SELECT id
FROM categories
WHERE movie_meta = 1
)
UNION
SELECT vote_average
FROM tv
WHERE
torrents.tmdb = tv.id
AND torrents.category_id in (
SELECT id
FROM categories
WHERE tv_meta = 1
)
LIMIT 1
) AS rating,
EXISTS(
SELECT *
FROM torrent_trumps
WHERE torrents.id = torrent_trumps.torrent_id
) AS trumpable,
(
SELECT JSON_OBJECT(
'id', movies.id,
'name', movies.title,
'year', YEAR(movies.release_date),
'poster', movies.poster,
'original_language', movies.original_language,
'adult', movies.adult != 0,
'rating', movies.vote_average,
'companies', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', companies.id,
'name', companies.name
)), JSON_ARRAY())
FROM companies
WHERE companies.id IN (
SELECT company_id
FROM company_movie
WHERE company_movie.movie_id = torrents.tmdb
)
),
'genres', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', genres.id,
'name', genres.name
)), JSON_ARRAY())
FROM genres
WHERE genres.id IN (
SELECT genre_id
FROM genre_movie
WHERE genre_movie.movie_id = torrents.tmdb
)
),
'collection', (
SELECT JSON_OBJECT(
'id', collection_movie.collection_id
)
FROM collection_movie
WHERE movies.id = collection_movie.movie_id
LIMIT 1
),
'wishes', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', wishes.user_id
)), JSON_ARRAY())
FROM wishes
WHERE wishes.movie_id = movies.id
)
)
FROM movies
WHERE torrents.tmdb = movies.id
AND torrents.category_id in (
SELECT id
FROM categories
WHERE movie_meta = 1
)
LIMIT 1
) AS json_movie,
(
SELECT JSON_OBJECT(
'id', tv.id,
'name', tv.name,
'year', YEAR(tv.first_air_date),
'poster', tv.poster,
'original_language', tv.original_language,
'rating', tv.vote_average,
'companies', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', companies.id,
'name', companies.name
)), JSON_ARRAY())
FROM companies
WHERE companies.id IN (
SELECT company_id
FROM company_tv
WHERE company_tv.tv_id = torrents.id
)
),
'genres', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', genres.id,
'name', genres.name
)), JSON_ARRAY())
FROM genres
WHERE genres.id IN (
SELECT genre_id
FROM genre_tv
WHERE genre_tv.tv_id = torrents.tmdb
)
),
'networks', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', networks.id,
'name', networks.name
)), JSON_ARRAY())
FROM networks
WHERE networks.id IN (
SELECT network_id
FROM network_tv
WHERE network_tv.tv_id = torrents.id
)
),
'wishes', (
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', wishes.user_id
)), JSON_ARRAY())
FROM wishes
WHERE wishes.tv_id = tv.id
)
)
FROM tv
WHERE torrents.tmdb = tv.id
AND torrents.category_id in (
SELECT id
FROM categories
WHERE tv_meta = 1
)
LIMIT 1
) AS json_tv,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', playlist_torrents.playlist_id
)), JSON_ARRAY())
FROM playlist_torrents
WHERE torrents.id = playlist_torrents.torrent_id
) AS json_playlists,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', freeleech_tokens.user_id
)), JSON_ARRAY())
FROM freeleech_tokens
WHERE torrents.id = freeleech_tokens.torrent_id
) AS json_freeleech_tokens,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'user_id', bookmarks.user_id
)), JSON_ARRAY())
FROM bookmarks
WHERE torrents.id = bookmarks.torrent_id
) AS json_bookmarks,
(
SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(
'id', files.id,
'name', files.name,
'size', files.size
)), JSON_ARRAY())
FROM files
WHERE torrents.id = files.torrent_id
) AS json_files,
(
SELECT COALESCE(JSON_ARRAYAGG(keywords.name), JSON_ARRAY())
FROM keywords
WHERE torrents.id = keywords.torrent_id
) AS json_keywords
SQL;
protected static function booted(): void
{
static::addGlobalScope(new ApprovedScope());
}
/**
* Belongs To A User.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<User, $this>
*/
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'username' => 'System',
'id' => User::SYSTEM_USER_ID,
]);
}
/**
* Belongs To A Category.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Category, $this>
*/
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Belongs To A Type.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Type, $this>
*/
public function type(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Type::class);
}
/**
* Belongs To A Resolution.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Resolution, $this>
*/
public function resolution(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Resolution::class);
}
/**
* Belongs To A Distributor.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Distributor, $this>
*/
public function distributor(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Distributor::class);
}
/**
* Belongs To A Region.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Region, $this>
*/
public function region(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Region::class);
}
/**
* Belongs To A Movie.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Movie, $this>
*/
public function movie(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Movie::class, 'tmdb');
}
/**
* Belongs To A Tv.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Tv, $this>
*/
public function tv(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Tv::class, 'tmdb');
}
/**
* Belongs To A Playlist.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<Playlist, $this>
*/
public function playlists(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Playlist::class, 'playlist_torrents')->using(PlaylistTorrent::class)->withPivot('id');
}
/**
* Torrent Has Been Moderated By.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<User, $this>
*/
public function moderated(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class, 'moderated_by')->withDefault([
'username' => 'System',
'id' => User::SYSTEM_USER_ID,
]);
}
/**
* Has Many Keywords.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Keyword, $this>
*/
public function keywords(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Keyword::class);
}
/**
* Has Many History.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<History, $this>
*/
public function history(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(History::class);
}
/**
* Has Many Tips.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TorrentTip, $this>
*/
public function tips(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(TorrentTip::class);
}
/**
* Has Many Thank.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Thank, $this>
*/
public function thanks(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Thank::class);
}
/**
* Has Many HitRuns.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Warning, $this>
*/
public function hitrun(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Warning::class, 'torrent');
}
/**
* Has Many Featured.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<FeaturedTorrent, $this>
*/
public function featured(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(FeaturedTorrent::class);
}
/**
* Has Many Files.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TorrentFile, $this>
*/
public function files(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(TorrentFile::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<Comment, $this>
*/
public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
/**
* Has Many Peers.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Peer, $this>
*/
public function peers(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Peer::class);
}
/**
* Has Many Seeds.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Peer, $this>
*/
public function seeds(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Peer::class)->where('seeder', '=', true);
}
/**
* Has Many Leeches.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Peer, $this>
*/
public function leeches(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Peer::class)->where('seeder', '=', false);
}
/**
* Has Many Subtitles.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Subtitle, $this>
*/
public function subtitles(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Subtitle::class);
}
/**
* Relationship To Many Requests.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<TorrentRequest, $this>
*/
public function requests(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(TorrentRequest::class);
}
/**
* Has many free leech tokens.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<FreeleechToken, $this>
*/
public function freeleechTokens(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(FreeleechToken::class);
}
/**
* Bookmarks.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Bookmark, $this>
*/
public function bookmarks(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Bookmark::class);
}
/**
* Resurrections.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Resurrection, $this>
*/
public function resurrections(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Resurrection::class);
}
/**
* Reports.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Report, $this>
*/
public function reports(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Report::class);
}
/**
* Trump.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TorrentTrump, $this>
*/
public function trump(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(TorrentTrump::class);
}
/**
* Parse Description And Return Valid HTML.
*/
public function getDescriptionHtml(): string
{
$bbcode = new Bbcode();
return (new Linkify())->linky($bbcode->parse($this->description));
}
/**
* Set The Torrents MediaInfo After Its Been Purified.
*/
public function setMediaInfoAttribute(?string $value): void
{
$this->attributes['mediainfo'] = $value;
}
/**
* Returns The Size In Human Format.
*/
public function getSize(): string
{
$bytes = $this->size;
return StringHelper::formatBytes($bytes, 2);
}
/**
* Notify Uploader When An Action Is Taken.
*/
public function notifyUploader(string $type, Thank|Comment $payload): bool
{
$user = User::with('notification')->findOrFail($this->user_id);
switch (true) {
case $payload instanceof Thank:
if ($user->acceptsNotification(auth()->user(), $user, 'torrent', 'show_torrent_thank')) {
$user->notify(new NewThank('torrent', $payload));
}
break;
case $payload instanceof Comment:
if ($user->acceptsNotification(auth()->user(), $user, 'torrent', 'show_torrent_comment')) {
$user->notify(new NewComment($this, $payload));
}
break;
}
return true;
}
/**
* Torrent Is Freeleech.
*/
public function isFreeleech(User $user = null): bool
{
$pfree = $user && ($user->group->is_freeleech || cache()->get('personal_freeleech:'.$user->id));
return $this->free || config('other.freeleech') || $pfree;
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
$missingRequiredAttributes = array_diff([
'id',
'name',
'description',
'mediainfo',
'bdinfo',
'num_file',
'folder',
'size',
'leechers',
'seeders',
'times_completed',
'created_at',
'bumped_at',
'fl_until',
'du_until',
'user_id',
'imdb',
'tvdb',
'tmdb',
'mal',
'igdb',
'season_number',
'episode_number',
'stream',
'free',
'doubleup',
'refundable',
'highspeed',
'featured',
'status',
'anon',
'sticky',
'sd',
'internal',
'deleted_at',
'distributor_id',
'region_id',
'personal_release',
'info_hash',
'trumpable',
'rating',
'json_user',
'json_type',
'json_category',
'json_resolution',
'json_movie',
'json_tv',
'json_playlists',
'json_freeleech_tokens',
'json_bookmarks',
'json_files',
'json_keywords',
'json_history_seeders',
'json_history_leechers',
'json_history_active',
'json_history_inactive',
'json_history_complete',
'json_history_incomplete',
], array_keys($this->getAttributes()));
if ([] == $missingRequiredAttributes) {
$torrent = $this;
} else {
// Refetch torrent if any required attributes are missing
$torrent = Torrent::query()
->withoutGlobalScope(ApprovedScope::class)
->whereKey($this->id)
->selectRaw(self::SEARCHABLE)
->first();
}
return [
'id' => $torrent->id,
'name' => $torrent->name,
'description' => $torrent->description,
'mediainfo' => $torrent->mediainfo,
'bdinfo' => $torrent->bdinfo,
'num_file' => $torrent->num_file,
'folder' => $torrent->folder,
'size' => $torrent->size,
'leechers' => $torrent->leechers,
'seeders' => $torrent->seeders,
'times_completed' => $torrent->times_completed,
'created_at' => $torrent->created_at?->timestamp,
'bumped_at' => $torrent->bumped_at?->timestamp,
'fl_until' => $torrent->fl_until?->timestamp,
'du_until' => $torrent->du_until?->timestamp,
'user_id' => $torrent->user_id,
'imdb' => $torrent->imdb,
'tvdb' => $torrent->tvdb,
'tmdb' => $torrent->tmdb,
'mal' => $torrent->mal,
'igdb' => $torrent->igdb,
'season_number' => $torrent->season_number,
'episode_number' => $torrent->episode_number,
'stream' => (bool) $torrent->stream,
'free' => $torrent->free,
'doubleup' => (bool) $torrent->doubleup,
'refundable' => (bool) $torrent->refundable,
'highspeed' => (bool) $torrent->highspeed,
'featured' => (bool) $torrent->featured,
'status' => $torrent->status,
'anon' => (bool) $torrent->anon,
'sticky' => (int) $torrent->sticky,
'sd' => (bool) $torrent->sd,
'internal' => (bool) $torrent->internal,
'deleted_at' => $torrent->deleted_at?->timestamp,
'distributor_id' => $torrent->distributor_id,
'region_id' => $torrent->region_id,
'personal_release' => (bool) $torrent->personal_release,
'info_hash' => bin2hex($torrent->info_hash),
'rating' => (float) $torrent->rating, /** @phpstan-ignore property.notFound (This property is selected in the query but doesn't exist on the model) */
'trumpable' => (bool) $torrent->trumpable, /** @phpstan-ignore property.notFound (This property is selected in the query but doesn't exist on the model) */
'user' => json_decode($torrent->json_user ?? 'null'),
'type' => json_decode($torrent->json_type ?? 'null'),
'category' => json_decode($torrent->json_category ?? 'null'),
'resolution' => json_decode($torrent->json_resolution ?? 'null'),
'movie' => json_decode($torrent->json_movie ?? 'null'),
'tv' => json_decode($torrent->json_tv ?? 'null'),
'playlists' => json_decode($torrent->json_playlists ?? '[]'),
'freeleech_tokens' => json_decode($torrent->json_freeleech_tokens ?? '[]'),
'bookmarks' => json_decode($torrent->json_bookmarks ?? '[]'),
'files' => json_decode($torrent->json_files ?? '[]'),
'keywords' => json_decode($torrent->json_keywords ?? '[]'),
'history_seeders' => json_decode($torrent->json_history_seeders ?? '[]'),
'history_leechers' => json_decode($torrent->json_history_leechers ?? '[]'),
'history_active' => json_decode($torrent->json_history_active ?? '[]'),
'history_inactive' => json_decode($torrent->json_history_inactive ?? '[]'),
'history_complete' => json_decode($torrent->json_history_complete ?? '[]'),
'history_incomplete' => json_decode($torrent->json_history_incomplete ?? '[]'),
];
}
/**
* Modify the query used to retrieve models when making all of the models searchable.
*
* @param Builder<self> $query
* @return Builder<self>
*/
protected function makeAllSearchableUsing(Builder $query): Builder
{
return $query->selectRaw(self::SEARCHABLE);
}
}