mirror of
https://github.com/HDInnovations/UNIT3D.git
synced 2026-01-31 01:35:31 +01:00
add: use meilisearch to search torrents
This commit is contained in:
@@ -44,3 +44,6 @@ DEFAULT_OWNER_PASSWORD=UNIT3D
|
||||
TMDB_API_KEY=
|
||||
TWITCH_CLIENT_ID=
|
||||
TWITCH_CLIENT_SECRET=
|
||||
|
||||
MEILISEARCH_HOST=http://127.0.0.1:7700
|
||||
MEILISEARCH_KEY=
|
||||
|
||||
56
app/Console/Commands/AutoSyncTorrentsToMeilisearch.php
Normal file
56
app/Console/Commands/AutoSyncTorrentsToMeilisearch.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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 Roardom <roardom@protonmail.com>
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Torrent;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class AutoSyncTorrentsToMeilisearch extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'auto:sync_torrents_to_meilisearch {--wipe}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Syncs torrents and their relations to meilisearch';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$start = now();
|
||||
|
||||
if ($this->option('wipe')) {
|
||||
Torrent::removeAllFromSearch();
|
||||
}
|
||||
|
||||
Torrent::query()->searchable();
|
||||
|
||||
$this->comment('Synced all torrents to Meilisearch in '.(now()->diffInMilliseconds($start) / 1000).' seconds.');
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command('auto:refund_download')->daily();
|
||||
$schedule->command('auth:clear-resets')->daily();
|
||||
$schedule->command('fetch:release_years')->everyTenMinutes();
|
||||
$schedule->command('auto:sync_torrents_to_meilisearch')->everyThirtyMinutes();
|
||||
//$schedule->command('auto:ban_disposable_users')->weekends();
|
||||
//$schedule->command('backup:clean')->daily();
|
||||
//$schedule->command('backup:run --only-db')->daily();
|
||||
|
||||
@@ -24,9 +24,10 @@ use Closure;
|
||||
readonly class TorrentSearchFiltersDTO
|
||||
{
|
||||
use TorrentFilter;
|
||||
private ?User $user;
|
||||
|
||||
public function __construct(
|
||||
private ?User $user = null,
|
||||
User $user = null,
|
||||
private string $name = '',
|
||||
private string $description = '',
|
||||
private string $mediainfo = '',
|
||||
@@ -84,6 +85,7 @@ readonly class TorrentSearchFiltersDTO
|
||||
private ?bool $userSeeder = null,
|
||||
private ?bool $userActive = null,
|
||||
) {
|
||||
$this->user = $user ?? auth()->user();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,8 +93,9 @@ readonly class TorrentSearchFiltersDTO
|
||||
*/
|
||||
final public function toSqlQueryBuilder(): Closure
|
||||
{
|
||||
$user = $this->user ?? auth()->user();
|
||||
$isRegexAllowed = $user->group->is_modo || $user->group->is_editor;
|
||||
$group = $this->user->group;
|
||||
|
||||
$isRegexAllowed = $group->is_modo || $group->is_editor;
|
||||
$isRegex = fn ($field) => $isRegexAllowed
|
||||
&& \strlen((string) $field) > 2
|
||||
&& $field[0] === '/'
|
||||
@@ -136,8 +139,8 @@ readonly class TorrentSearchFiltersDTO
|
||||
->when($this->stream, fn ($query) => $query->streamOptimized())
|
||||
->when($this->sd, fn ($query) => $query->sd())
|
||||
->when($this->highspeed, fn ($query) => $query->highspeed())
|
||||
->when($this->userBookmarked, fn ($query) => $query->bookmarkedBy($user))
|
||||
->when($this->userWished, fn ($query) => $query->wishedBy($user))
|
||||
->when($this->userBookmarked, fn ($query) => $query->bookmarkedBy($this->user))
|
||||
->when($this->userWished, fn ($query) => $query->wishedBy($this->user))
|
||||
->when($this->internal, fn ($query) => $query->internal())
|
||||
->when($this->personalRelease, fn ($query) => $query->personalRelease())
|
||||
->when($this->trumpable, fn ($query) => $query->trumpable())
|
||||
@@ -145,10 +148,236 @@ readonly class TorrentSearchFiltersDTO
|
||||
->when($this->dying, fn ($query) => $query->dying())
|
||||
->when($this->dead, fn ($query) => $query->dead())
|
||||
->when($this->graveyard, fn ($query) => $query->graveyard())
|
||||
->when($this->userDownloaded === false, fn ($query) => $query->notDownloadedBy($user))
|
||||
->when($this->userDownloaded === true, fn ($query) => $query->downloadedBy($user))
|
||||
->when($this->userSeeder === true && $this->userActive === true, fn ($query) => $query->seededBy($user))
|
||||
->when($this->userSeeder === false && $this->userActive === true, fn ($query) => $query->leechedBy($user))
|
||||
->when($this->userSeeder === false && $this->userActive === false, fn ($query) => $query->uncompletedBy($user));
|
||||
->when($this->userDownloaded === false, fn ($query) => $query->notDownloadedBy($this->user))
|
||||
->when($this->userDownloaded === true, fn ($query) => $query->downloadedBy($this->user))
|
||||
->when($this->userSeeder === true && $this->userActive === true, fn ($query) => $query->seededBy($this->user))
|
||||
->when($this->userSeeder === false && $this->userActive === true, fn ($query) => $query->leechedBy($this->user))
|
||||
->when($this->userSeeder === false && $this->userActive === false, fn ($query) => $query->uncompletedBy($this->user));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string|list<string>>
|
||||
*/
|
||||
final public function toMeilisearchFilter(): array
|
||||
{
|
||||
$group = $this->user->group;
|
||||
|
||||
$filters = [
|
||||
'deleted_at IS NULL',
|
||||
'status = 1',
|
||||
];
|
||||
|
||||
if ($this->uploader !== '') {
|
||||
$filters[] = 'user.username = '.json_encode($this->uploader);
|
||||
|
||||
if (!$group->is_modo) {
|
||||
$filters[] = 'anon = 0';
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->keywords !== []) {
|
||||
$filters[] = 'keywords.name IN '.json_encode($this->keywords);
|
||||
}
|
||||
|
||||
if ($this->startYear !== null) {
|
||||
$filters[] = [
|
||||
'movie.year >= '.$this->startYear,
|
||||
'tv.year >= '.$this->startYear,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->endYear !== null) {
|
||||
$filters[] = [
|
||||
'movie.year <= '.$this->startYear,
|
||||
'tv.year <= '.$this->startYear,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->minSize !== null) {
|
||||
$filters[] = 'size >= '.$this->minSize;
|
||||
}
|
||||
|
||||
if ($this->maxSize !== null) {
|
||||
$filters[] = 'size <= '.$this->maxSize;
|
||||
}
|
||||
|
||||
if ($this->seasonNumber !== null) {
|
||||
$filters[] = 'season_number = '.$this->seasonNumber;
|
||||
}
|
||||
|
||||
if ($this->episodeNumber !== null) {
|
||||
$filters[] = 'episode_number = '.$this->episodeNumber;
|
||||
}
|
||||
|
||||
if ($this->categoryIds !== []) {
|
||||
$filters[] = 'category.id IN '.json_encode(array_map('intval', $this->categoryIds));
|
||||
}
|
||||
|
||||
if ($this->typeIds !== []) {
|
||||
$filters[] = 'type.id IN '.json_encode(array_map('intval', $this->typeIds));
|
||||
}
|
||||
|
||||
if ($this->resolutionIds !== []) {
|
||||
$filters[] = 'resolution.id IN '.json_encode(array_map('intval', $this->resolutionIds));
|
||||
}
|
||||
|
||||
if ($this->genreIds !== []) {
|
||||
$filters[] = [
|
||||
'movie.genres.id IN '.json_encode(array_map('intval', $this->genreIds)),
|
||||
'tv.genres.id IN '.json_encode(array_map('intval', $this->genreIds)),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->regionIds !== []) {
|
||||
$filters[] = 'region_id IN '.json_encode(array_map('intval', $this->regionIds));
|
||||
}
|
||||
|
||||
if ($this->distributorIds !== []) {
|
||||
$filters[] = 'distributor_id IN '.json_encode(array_map('intval', $this->distributorIds));
|
||||
}
|
||||
|
||||
if ($this->adult !== null) {
|
||||
$filters[] = 'movie.adult = '.($this->adult ? '1' : '0');
|
||||
}
|
||||
|
||||
if ($this->tmdbId !== null) {
|
||||
$filters[] = 'tmdb = '.$this->tmdbId;
|
||||
}
|
||||
|
||||
if ($this->imdbId !== null) {
|
||||
$filters[] = 'imdb = '.$this->imdbId;
|
||||
}
|
||||
|
||||
if ($this->tvdbId !== null) {
|
||||
$filters[] = 'tvdb = '.$this->tvdbId;
|
||||
}
|
||||
|
||||
if ($this->malId !== null) {
|
||||
$filters[] = 'mal = '.$this->malId;
|
||||
}
|
||||
|
||||
if ($this->playlistId !== null) {
|
||||
$filters[] = 'playlists.id = '.$this->playlistId;
|
||||
}
|
||||
|
||||
if ($this->collectionId !== null) {
|
||||
$filters[] = 'movie.collection.id = '.$this->collectionId;
|
||||
}
|
||||
|
||||
if ($this->companyId !== null) {
|
||||
$filters[] = [
|
||||
'movie.companies.id = '.$this->companyId,
|
||||
'tv.companies.id = '.$this->companyId,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->networkId !== null) {
|
||||
$filters[] = 'tv.networks.id = '.$this->networkId;
|
||||
}
|
||||
|
||||
if ($this->primaryLanguageNames !== []) {
|
||||
$filters[] = [
|
||||
'movie.original_language IN '.json_encode(array_map('strval', $this->primaryLanguageNames)),
|
||||
'tv.original_language IN '.json_encode(array_map('strval', $this->primaryLanguageNames)),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->free !== []) {
|
||||
/** @phpstan-ignore booleanNot.alwaysTrue */
|
||||
if (!config('other.freeleech')) {
|
||||
$filters[] = 'free IN '.json_encode(array_map('intval', $this->free));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->doubleup) {
|
||||
$filters[] = 'doubleup = 1';
|
||||
}
|
||||
|
||||
if ($this->featured) {
|
||||
$filters[] = 'featured = 1';
|
||||
}
|
||||
|
||||
if ($this->refundable) {
|
||||
$filters[] = 'refundable = 1';
|
||||
}
|
||||
|
||||
if ($this->stream) {
|
||||
$filters[] = 'stream = 1';
|
||||
}
|
||||
|
||||
if ($this->sd) {
|
||||
$filters[] = 'sd = 1';
|
||||
}
|
||||
|
||||
if ($this->highspeed) {
|
||||
$filters[] = 'highspeed = 1';
|
||||
}
|
||||
|
||||
if ($this->internal) {
|
||||
$filters[] = 'internal = 1';
|
||||
}
|
||||
|
||||
if ($this->personalRelease) {
|
||||
$filters[] = 'personal_release = 1';
|
||||
}
|
||||
|
||||
if ($this->alive) {
|
||||
$filters[] = 'seeders > 0';
|
||||
}
|
||||
|
||||
if ($this->dying) {
|
||||
$filters[] = 'seeders = 1';
|
||||
$filters[] = 'times_completed >= 3';
|
||||
}
|
||||
|
||||
if ($this->dead) {
|
||||
$filters[] = 'seeders = 0';
|
||||
}
|
||||
|
||||
if ($this->filename !== '') {
|
||||
$filters[] = 'files.name = '.json_encode($this->filename);
|
||||
}
|
||||
|
||||
if ($this->graveyard) {
|
||||
$filters[] = 'seeders = 0';
|
||||
$filters[] = 'created_at < '.now()->subDays(30)->timestamp;
|
||||
}
|
||||
|
||||
if ($this->userBookmarked) {
|
||||
$filters[] = 'bookmarks.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userWished) {
|
||||
$filters[] = [
|
||||
'movie.wishes.user_id = '.$this->user->id,
|
||||
'tv.wishes.user_id = '.$this->user->id,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->userDownloaded === true) {
|
||||
$filters[] = 'history_complete.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userDownloaded === false) {
|
||||
$filters[] = 'history_incomplete.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userSeeder === false) {
|
||||
$filters[] = 'history_leechers.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userSeeder === true) {
|
||||
$filters[] = 'history_seeders.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userActive === true) {
|
||||
$filters[] = 'history_active.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
if ($this->userActive === false) {
|
||||
$filters[] = 'history_inactive.user_id = '.$this->user->id;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Models\Torrent;
|
||||
|
||||
class BookmarkController extends BaseController
|
||||
{
|
||||
final public function store(int $torrentId): bool
|
||||
{
|
||||
auth()->user()->bookmarks()->attach($torrentId);
|
||||
|
||||
Torrent::query()->whereKey($torrentId)->searchable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -29,6 +33,8 @@ class BookmarkController extends BaseController
|
||||
{
|
||||
auth()->user()->bookmarks()->detach($torrentId);
|
||||
|
||||
Torrent::query()->whereKey($torrentId)->searchable();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@ use App\Services\Tmdb\TMDBScraper;
|
||||
use App\Services\Unit3dAnnounce;
|
||||
use App\Traits\TorrentMeta;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use MarcReichel\IGDBLaravel\Models\Game;
|
||||
use Meilisearch\Endpoints\Indexes;
|
||||
|
||||
/**
|
||||
* @see \Tests\Todo\Feature\Http\Controllers\TorrentControllerTest
|
||||
@@ -490,6 +492,20 @@ class TorrentController extends BaseController
|
||||
{
|
||||
$user = auth()->user();
|
||||
$isRegexAllowed = $user->group->is_modo;
|
||||
$isSqlAllowed = $user->group->is_modo && $request->driver === 'sql';
|
||||
|
||||
$request->validate([
|
||||
'sortField' => [
|
||||
'nullable',
|
||||
'sometimes',
|
||||
'in:name,size,seeders,leechers,times_completed,created_at,bumped_at',
|
||||
],
|
||||
'sortDirection' => [
|
||||
'nullable',
|
||||
'sometimes',
|
||||
'in:asc,desc'
|
||||
]
|
||||
]);
|
||||
|
||||
// Caching
|
||||
$url = $request->url();
|
||||
@@ -498,6 +514,7 @@ class TorrentController extends BaseController
|
||||
// Don't cache the api_token so that multiple users can share the cache
|
||||
unset($queryParams['api_token']);
|
||||
$queryParams['isRegexAllowed'] = $isRegexAllowed;
|
||||
$queryParams['isSqlAllowed'] = $isSqlAllowed;
|
||||
|
||||
// Sorting query params by key (acts by reference)
|
||||
ksort($queryParams);
|
||||
@@ -506,8 +523,9 @@ class TorrentController extends BaseController
|
||||
$queryString = http_build_query($queryParams);
|
||||
$cacheKey = $url.'?'.$queryString;
|
||||
|
||||
$torrents = cache()->remember($cacheKey, 300, function () use ($request) {
|
||||
$torrents = Torrent::with(['user:id,username', 'category', 'type', 'resolution', 'distributor', 'region', 'files'])
|
||||
$torrents = cache()->remember($cacheKey, 300, function () use ($request, $isSqlAllowed) {
|
||||
$eagerLoads = fn (Builder $query) => $query
|
||||
->with(['user:id,username', 'category', 'type', 'resolution', 'distributor', 'region', 'files'])
|
||||
->select('*')
|
||||
->selectRaw("
|
||||
CASE
|
||||
@@ -517,46 +535,68 @@ class TorrentController extends BaseController
|
||||
WHEN category_id IN (SELECT `id` from `categories` where `music_meta` = 1) THEN 'music'
|
||||
WHEN category_id IN (SELECT `id` from `categories` where `no_meta` = 1) THEN 'no'
|
||||
END as meta
|
||||
")
|
||||
->where((new TorrentSearchFiltersDTO(
|
||||
name: $request->filled('name') ? $request->string('name')->toString() : '',
|
||||
description: $request->filled('description') ? $request->string('description')->toString() : '',
|
||||
mediainfo: $request->filled('mediainfo') ? $request->string('mediainfo')->toString() : '',
|
||||
uploader: $request->filled('uploader') ? $request->string('uploader')->toString() : '',
|
||||
keywords: $request->filled('keywords') ? array_map('trim', explode(',', $request->string('keywords')->toString())) : [],
|
||||
startYear: $request->filled('startYear') ? $request->integer('startYear') : null,
|
||||
endYear: $request->filled('endYear') ? $request->integer('endYear') : null,
|
||||
categoryIds: $request->filled('categories') ? array_map('intval', $request->categories) : [],
|
||||
typeIds: $request->filled('types') ? array_map('intval', $request->types) : [],
|
||||
resolutionIds: $request->filled('resolutions') ? array_map('intval', $request->resolutions) : [],
|
||||
genreIds: $request->filled('genres') ? array_map('intval', $request->genres) : [],
|
||||
tmdbId: $request->filled('tmdbId') ? $request->integer('tmdbId') : null,
|
||||
imdbId: $request->filled('imdbId') ? $request->integer('imdbId') : null,
|
||||
tvdbId: $request->filled('tvdbId') ? $request->integer('tvdbId') : null,
|
||||
malId: $request->filled('malId') ? $request->integer('malId') : null,
|
||||
playlistId: $request->filled('playlistId') ? $request->integer('playlistId') : null,
|
||||
collectionId: $request->filled('collectionId') ? $request->integer('collectionId') : null,
|
||||
primaryLanguageNames: $request->filled('primaryLanguages') ? array_map('str', $request->primaryLanguages) : [],
|
||||
adult: $request->filled('adult') ? $request->boolean('adult') : null,
|
||||
free: $request->filled('free') ? array_map('intval', (array) $request->free) : [],
|
||||
doubleup: $request->filled('doubleup'),
|
||||
refundable: $request->filled('refundable'),
|
||||
featured: $request->filled('featured'),
|
||||
stream: $request->filled('stream'),
|
||||
sd: $request->filled('sd'),
|
||||
highspeed: $request->filled('highspeed'),
|
||||
internal: $request->filled('internal'),
|
||||
personalRelease: $request->filled('personalRelease'),
|
||||
alive: $request->filled('alive'),
|
||||
dying: $request->filled('dying'),
|
||||
dead: $request->filled('dead'),
|
||||
filename: $request->filled('file_name') ? $request->string('file_name')->toString() : '',
|
||||
seasonNumber: $request->filled('seasonNumber') ? $request->integer('seasonNumber') : null,
|
||||
episodeNumber: $request->filled('episodeNumber') ? $request->integer('episodeNumber') : null,
|
||||
))->toSqlQueryBuilder())
|
||||
->latest('sticky')
|
||||
->orderBy($request->input('sortField') ?? $this->sortField, $request->input('sortDirection') ?? $this->sortDirection)
|
||||
->cursorPaginate(min($request->input('perPage') ?? $this->perPage, 100));
|
||||
");
|
||||
|
||||
$filters = new TorrentSearchFiltersDTO(
|
||||
name: $request->filled('name') ? $request->string('name')->toString() : '',
|
||||
description: $request->filled('description') ? $request->string('description')->toString() : '',
|
||||
mediainfo: $request->filled('mediainfo') ? $request->string('mediainfo')->toString() : '',
|
||||
uploader: $request->filled('uploader') ? $request->string('uploader')->toString() : '',
|
||||
keywords: $request->filled('keywords') ? array_map('trim', explode(',', $request->string('keywords')->toString())) : [],
|
||||
startYear: $request->filled('startYear') ? $request->integer('startYear') : null,
|
||||
endYear: $request->filled('endYear') ? $request->integer('endYear') : null,
|
||||
categoryIds: $request->filled('categories') ? array_map('intval', $request->categories) : [],
|
||||
typeIds: $request->filled('types') ? array_map('intval', $request->types) : [],
|
||||
resolutionIds: $request->filled('resolutions') ? array_map('intval', $request->resolutions) : [],
|
||||
genreIds: $request->filled('genres') ? array_map('intval', $request->genres) : [],
|
||||
tmdbId: $request->filled('tmdbId') ? $request->integer('tmdbId') : null,
|
||||
imdbId: $request->filled('imdbId') ? $request->integer('imdbId') : null,
|
||||
tvdbId: $request->filled('tvdbId') ? $request->integer('tvdbId') : null,
|
||||
malId: $request->filled('malId') ? $request->integer('malId') : null,
|
||||
playlistId: $request->filled('playlistId') ? $request->integer('playlistId') : null,
|
||||
collectionId: $request->filled('collectionId') ? $request->integer('collectionId') : null,
|
||||
primaryLanguageNames: $request->filled('primaryLanguages') ? array_map('str', $request->primaryLanguages) : [],
|
||||
adult: $request->filled('adult') ? $request->boolean('adult') : null,
|
||||
free: $request->filled('free') ? array_map('intval', (array) $request->free) : [],
|
||||
doubleup: $request->filled('doubleup'),
|
||||
refundable: $request->filled('refundable'),
|
||||
featured: $request->filled('featured'),
|
||||
stream: $request->filled('stream'),
|
||||
sd: $request->filled('sd'),
|
||||
highspeed: $request->filled('highspeed'),
|
||||
internal: $request->filled('internal'),
|
||||
personalRelease: $request->filled('personalRelease'),
|
||||
alive: $request->filled('alive'),
|
||||
dying: $request->filled('dying'),
|
||||
dead: $request->filled('dead'),
|
||||
filename: $request->filled('file_name') ? $request->string('file_name')->toString() : '',
|
||||
seasonNumber: $request->filled('seasonNumber') ? $request->integer('seasonNumber') : null,
|
||||
episodeNumber: $request->filled('episodeNumber') ? $request->integer('episodeNumber') : null,
|
||||
);
|
||||
|
||||
if ($isSqlAllowed) {
|
||||
$torrents = Torrent::query()
|
||||
->where($filters->toSqlQueryBuilder())
|
||||
->latest('sticky')
|
||||
->orderBy($request->input('sortField') ?? $this->sortField, $request->input('sortDirection') ?? $this->sortDirection)
|
||||
->cursorPaginate(min($request->input('perPage') ?? $this->perPage, 100));
|
||||
} else {
|
||||
$torrents = Torrent::search(
|
||||
$request->filled('name') ? $request->string('name')->toString() : '',
|
||||
function (Indexes $meilisearch, string $query, array $options) use ($request, $filters) {
|
||||
$options['sort'] = [
|
||||
($request->input('sortField') ?: $this->sortField).':'.($request->input('sortDirection') ?? $this->sortDirection),
|
||||
];
|
||||
$options['filter'] = $filters->toMeilisearchFilter();
|
||||
|
||||
$results = $meilisearch->search($query, $options);
|
||||
|
||||
return $results;
|
||||
}
|
||||
)
|
||||
->query($eagerLoads)
|
||||
->paginate(min($request->input('perPage') ?? $this->perPage, 100));
|
||||
}
|
||||
|
||||
// See app/Traits/TorrentMeta.php
|
||||
$this->scopeMeta($torrents);
|
||||
|
||||
@@ -39,7 +39,9 @@ class PlaylistTorrentController extends Controller
|
||||
|
||||
abort_unless($request->user()->id === $playlist->user_id, 403);
|
||||
|
||||
PlaylistTorrent::create($request->validated());
|
||||
$playlistTorrent = PlaylistTorrent::create($request->validated());
|
||||
|
||||
$playlistTorrent->torrent()->searchable();
|
||||
|
||||
return to_route('playlists.show', ['playlist' => $playlist])
|
||||
->withSuccess(trans('playlist.attached-success'));
|
||||
@@ -69,6 +71,8 @@ class PlaylistTorrentController extends Controller
|
||||
|
||||
PlaylistTorrent::upsert($playlistTorrents, ['playlist_id', 'torrent_id', 'tmdb_id']);
|
||||
|
||||
$playlist->torrents()->searchable();
|
||||
|
||||
return to_route('playlists.show', ['playlist' => $playlist])
|
||||
->withSuccess(trans('playlist.attached-success'));
|
||||
}
|
||||
@@ -84,6 +88,8 @@ class PlaylistTorrentController extends Controller
|
||||
|
||||
$playlistTorrent->delete();
|
||||
|
||||
$playlistTorrent->torrent()->searchable();
|
||||
|
||||
return to_route('playlists.show', ['playlist' => $playlistTorrent->playlist])
|
||||
->withSuccess(trans('playlist.detached-success'));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ use App\Models\Type;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Meilisearch\Endpoints\Indexes;
|
||||
|
||||
/**
|
||||
* @see \Tests\Todo\Feature\Http\Controllers\RssControllerTest
|
||||
@@ -152,33 +154,8 @@ class RssController extends Controller
|
||||
if (\is_object($search)) {
|
||||
$cacheKey = 'rss:'.$rss->id;
|
||||
|
||||
$torrents = cache()->remember($cacheKey, 300, fn () => Torrent::query()
|
||||
->select([
|
||||
'name',
|
||||
'id',
|
||||
'category_id',
|
||||
'type_id',
|
||||
'resolution_id',
|
||||
'size',
|
||||
'created_at',
|
||||
'seeders',
|
||||
'leechers',
|
||||
'times_completed',
|
||||
'user_id',
|
||||
'anon',
|
||||
'imdb',
|
||||
'tmdb',
|
||||
'tvdb',
|
||||
'mal',
|
||||
'internal',
|
||||
])
|
||||
->with([
|
||||
'user:id,username,rsskey',
|
||||
'category:id,name,movie_meta,tv_meta',
|
||||
'type:id,name',
|
||||
'resolution:id,name'
|
||||
])
|
||||
->where((new TorrentSearchFiltersDTO(
|
||||
$torrents = cache()->remember($cacheKey, 300, function () use ($search, $user) {
|
||||
$filters = new TorrentSearchFiltersDTO(
|
||||
user: $user,
|
||||
name: $search->search ?? '',
|
||||
description: $search->description ?? '',
|
||||
@@ -203,10 +180,53 @@ class RssController extends Controller
|
||||
alive: (bool) ($search->alive ?? false),
|
||||
dying: (bool) ($search->dying ?? false),
|
||||
dead: (bool) ($search->dead ?? false),
|
||||
))->toSqlQueryBuilder())
|
||||
->orderByDesc('bumped_at')
|
||||
->take(50)
|
||||
->get());
|
||||
);
|
||||
|
||||
$torrents = Torrent::search(
|
||||
'',
|
||||
function (Indexes $meilisearch, string $query, array $options) use ($filters) {
|
||||
$options['sort'] = [
|
||||
'bumped_at:desc',
|
||||
];
|
||||
$options['filter'] = $filters->toMeilisearchFilter();
|
||||
|
||||
$results = $meilisearch->search($query, $options);
|
||||
|
||||
return $results;
|
||||
}
|
||||
)
|
||||
->query(
|
||||
fn (Builder $query) => $query->
|
||||
select([
|
||||
'name',
|
||||
'id',
|
||||
'category_id',
|
||||
'type_id',
|
||||
'resolution_id',
|
||||
'size',
|
||||
'created_at',
|
||||
'seeders',
|
||||
'leechers',
|
||||
'times_completed',
|
||||
'user_id',
|
||||
'anon',
|
||||
'imdb',
|
||||
'tmdb',
|
||||
'tvdb',
|
||||
'mal',
|
||||
'internal',
|
||||
])
|
||||
->with([
|
||||
'user:id,username,rsskey',
|
||||
'category:id,name,movie_meta,tv_meta',
|
||||
'type:id,name',
|
||||
'resolution:id,name'
|
||||
])
|
||||
)
|
||||
->paginate(50);
|
||||
|
||||
return $torrents;
|
||||
});
|
||||
|
||||
return response()->view('rss.show', [
|
||||
'torrents' => $torrents,
|
||||
|
||||
@@ -264,6 +264,8 @@ class TorrentBuffController extends Controller
|
||||
|
||||
cache()->put('freeleech_token:'.$user->id.':'.$torrent->id, true);
|
||||
|
||||
$torrent->searchable();
|
||||
|
||||
return to_route('torrents.show', ['id' => $torrent->id])
|
||||
->withSuccess('You Have Successfully Activated A Freeleech Token For This Torrent!');
|
||||
}
|
||||
|
||||
@@ -16,11 +16,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\Movie;
|
||||
use App\Models\Person;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\Tv;
|
||||
use Livewire\Component;
|
||||
use Meilisearch\Endpoints\Indexes;
|
||||
|
||||
class QuickSearchDropdown extends Component
|
||||
{
|
||||
@@ -32,79 +31,62 @@ class QuickSearchDropdown extends Component
|
||||
{
|
||||
$search = '%'.str_replace(' ', '%', $this->quicksearchText).'%';
|
||||
|
||||
$filters = [
|
||||
'deleted_at IS NULL',
|
||||
'status = 1',
|
||||
];
|
||||
|
||||
if (preg_match('/^(\d+)$/', $this->quicksearchText, $matches)) {
|
||||
$filters[] = 'tmdb = '.$matches[1];
|
||||
}
|
||||
|
||||
if (preg_match('/tt0*(?=(\d{7,}))/', $this->quicksearchText, $matches)) {
|
||||
$filters[] = 'imdb = '.$matches[1];
|
||||
}
|
||||
|
||||
$searchResults = [];
|
||||
|
||||
switch ($this->quicksearchRadio) {
|
||||
case 'movies':
|
||||
$query = Movie::query()
|
||||
->select(['id', 'poster', 'title', 'release_date'])
|
||||
->selectSub(
|
||||
Torrent::query()
|
||||
->select('category_id')
|
||||
->whereColumn('torrents.tmdb', '=', 'movies.id')
|
||||
->whereRelation('category', 'movie_meta', '=', true)
|
||||
->limit(1),
|
||||
'category_id'
|
||||
)
|
||||
->selectRaw("concat(title, ' ', release_date) as title_and_year")
|
||||
->when(
|
||||
preg_match('/^\d+$/', $this->quicksearchText),
|
||||
fn ($query) => $query->where('id', '=', $this->quicksearchText),
|
||||
fn ($query) => $query
|
||||
->when(
|
||||
preg_match('/tt0*(?=(\d{7,}))/', $this->quicksearchText, $matches),
|
||||
fn ($query) => $query->where('imdb_id', '=', $matches[1]),
|
||||
fn ($query) => $query->having('title_and_year', 'LIKE', $search),
|
||||
)
|
||||
)
|
||||
->havingNotNull('category_id')
|
||||
->oldest('title')
|
||||
->take(10);
|
||||
$filters[] = 'category.movie_meta = 1';
|
||||
$filters[] = 'movie.name IS NOT NULL';
|
||||
|
||||
$searchResults = Torrent::search(
|
||||
$this->quicksearchText,
|
||||
function (Indexes $meilisearch, string $query, array $options) use ($filters) {
|
||||
$options['filter'] = $filters;
|
||||
$options['distinct'] = 'movie.id';
|
||||
|
||||
return $meilisearch->search($query, $options);
|
||||
}
|
||||
)
|
||||
->simplePaginateRaw(20);
|
||||
|
||||
break;
|
||||
case 'series':
|
||||
$query = Tv::query()
|
||||
->select(['id', 'poster', 'name', 'first_air_date'])
|
||||
->selectSub(
|
||||
Torrent::query()
|
||||
->select('category_id')
|
||||
->whereColumn('torrents.tmdb', '=', 'tv.id')
|
||||
->whereRelation('category', 'tv_meta', '=', true)
|
||||
->limit(1),
|
||||
'category_id'
|
||||
)
|
||||
->selectRaw("concat(name, ' ', first_air_date) as title_and_year")
|
||||
->when(
|
||||
preg_match('/^\d+$/', $this->quicksearchText),
|
||||
fn ($query) => $query->where('id', '=', $this->quicksearchText),
|
||||
fn ($query) => $query
|
||||
->when(
|
||||
preg_match('/tt0*(?=(\d{7,}))/', $this->quicksearchText, $matches),
|
||||
fn ($query) => $query->where('imdb_id', '=', $matches[1]),
|
||||
fn ($query) => $query->having('title_and_year', 'LIKE', $search),
|
||||
)
|
||||
)
|
||||
->havingNotNull('category_id')
|
||||
->oldest('name')
|
||||
->take(10);
|
||||
$filters[] = 'category.tv_meta = 1';
|
||||
$filters[] = 'tv.name IS NOT NULL';
|
||||
|
||||
$searchResults = Torrent::search(
|
||||
$this->quicksearchText,
|
||||
function (Indexes $meilisearch, string $query, array $options) use ($filters) {
|
||||
$options['filter'] = $filters;
|
||||
$options['distinct'] = 'tv.id';
|
||||
|
||||
return $meilisearch->search($query, $options);
|
||||
}
|
||||
)
|
||||
->simplePaginateRaw(20);
|
||||
|
||||
break;
|
||||
case 'persons':
|
||||
$query = Person::query()
|
||||
$searchResults = Person::query()
|
||||
->select(['id', 'still', 'name'])
|
||||
->where('name', 'LIKE', $search)
|
||||
->oldest('name')
|
||||
->take(10);
|
||||
}
|
||||
|
||||
if (isset($query)) {
|
||||
// 56 characters whitelisted, 3 characters long, 3 search categories, ~3000 byte response each
|
||||
// Cache should fill 56 ^ 3 * 3000 = ~526 MB
|
||||
if (preg_match("/^[a-zA-Z0-9-_ .'@:\\[\\]+&\\/,!#()?\"]{0,3}$/", $this->quicksearchText)) {
|
||||
$searchResults = cache()->remember('quicksearch:'.$this->quicksearchRadio.':'.strtolower($search), 3600 * 24, fn () => $query->get()->toArray());
|
||||
} else {
|
||||
$searchResults = $query->get()->toArray();
|
||||
}
|
||||
->take(10)
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return view('livewire.quick-search-dropdown', [
|
||||
|
||||
@@ -35,6 +35,7 @@ use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Meilisearch\Endpoints\Indexes;
|
||||
use Closure;
|
||||
|
||||
class TorrentSearch extends Component
|
||||
@@ -220,6 +221,9 @@ class TorrentSearch extends Component
|
||||
#[Url(history: true)]
|
||||
public bool $incomplete = false;
|
||||
|
||||
#[Url(history: true, except: 'meilisearch')]
|
||||
public ?string $driver = 'meilisearch';
|
||||
|
||||
#[Url(history: true)]
|
||||
public int $perPage = 25;
|
||||
|
||||
@@ -339,7 +343,7 @@ class TorrentSearch extends Component
|
||||
/**
|
||||
* @return Closure(Builder<Torrent>): Builder<Torrent>
|
||||
*/
|
||||
final public function filters(): Closure
|
||||
final public function filters(): TorrentSearchFiltersDTO
|
||||
{
|
||||
return (new TorrentSearchFiltersDTO(
|
||||
name: $this->name,
|
||||
@@ -404,7 +408,7 @@ class TorrentSearch extends Component
|
||||
$this->leeching => true,
|
||||
default => null,
|
||||
},
|
||||
))->toSqlQueryBuilder();
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -428,7 +432,13 @@ class TorrentSearch extends Component
|
||||
$this->reset('sortField');
|
||||
}
|
||||
|
||||
$torrents = Torrent::with(['user:id,username,group_id', 'user.group', 'category', 'type', 'resolution'])
|
||||
// Only allow sql for now to prevent user complaints of limiting page count to 1000 results (meilisearch limitation).
|
||||
// However, eventually we want to switch to meilisearch only to reduce server load.
|
||||
// $isSqlAllowed = $user->group->is_modo && $this->driver === 'sql';
|
||||
$isSqlAllowed = $this->driver !== 'sql';
|
||||
|
||||
$eagerLoads = fn (Builder $query) => $query
|
||||
->with(['user:id,username,group_id', 'user.group', 'category', 'type', 'resolution'])
|
||||
->withCount([
|
||||
'thanks',
|
||||
'comments',
|
||||
@@ -465,11 +475,34 @@ class TorrentSearch extends Component
|
||||
WHEN category_id IN (SELECT `id` from `categories` where `music_meta` = 1) THEN 'music'
|
||||
WHEN category_id IN (SELECT `id` from `categories` where `no_meta` = 1) THEN 'no'
|
||||
END as meta
|
||||
")
|
||||
->where($this->filters())
|
||||
->latest('sticky')
|
||||
->orderBy($this->sortField, $this->sortDirection)
|
||||
->paginate(min($this->perPage, 100));
|
||||
");
|
||||
|
||||
if ($isSqlAllowed) {
|
||||
$torrents = Torrent::query()
|
||||
->where($this->filters()->toSqlQueryBuilder())
|
||||
->latest('sticky')
|
||||
->orderBy($this->sortField, $this->sortDirection);
|
||||
|
||||
$eagerLoads($torrents);
|
||||
$torrents = $torrents->paginate(min($this->perPage, 100));
|
||||
} else {
|
||||
$torrents = Torrent::search(
|
||||
$this->name,
|
||||
function (Indexes $meilisearch, string $query, array $options) {
|
||||
$options['sort'] = [
|
||||
'sticky:desc',
|
||||
$this->sortField.':'.$this->sortDirection,
|
||||
];
|
||||
$options['filter'] = $this->filters()->toMeilisearchFilter();
|
||||
|
||||
$results = $meilisearch->search($query, $options);
|
||||
|
||||
return $results;
|
||||
}
|
||||
)
|
||||
->query($eagerLoads)
|
||||
->paginate(min($this->perPage, 100));
|
||||
}
|
||||
|
||||
// See app/Traits/TorrentMeta.php
|
||||
$this->scopeMeta($torrents);
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Models\Genre;
|
||||
use App\Models\Movie;
|
||||
use App\Models\Person;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\Torrent;
|
||||
use App\Services\Tmdb\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -93,5 +94,10 @@ class ProcessMovieJob implements ShouldQueue
|
||||
// Recommendations
|
||||
|
||||
Recommendation::upsert($movieScraper->getRecommendations(), ['recommendation_movie_id', 'movie_id']);
|
||||
|
||||
Torrent::query()
|
||||
->where('tmdb', '=', $this->id)
|
||||
->whereRelation('category', 'movie_meta', '=', true)
|
||||
->searchable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use App\Models\Network;
|
||||
use App\Models\Person;
|
||||
use App\Models\Recommendation;
|
||||
use App\Models\Season;
|
||||
use App\Models\Torrent;
|
||||
use App\Models\Tv;
|
||||
use App\Services\Tmdb\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
@@ -112,5 +113,10 @@ class ProcessTvJob implements ShouldQueue
|
||||
// Recommendations
|
||||
|
||||
Recommendation::upsert($tvScraper->getRecommendations(), ['recommendation_tv_id', 'tv_id']);
|
||||
|
||||
Torrent::query()
|
||||
->where('tmdb', '=', $this->id)
|
||||
->whereRelation('category', 'tv_meta', '=', true)
|
||||
->searchable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,12 @@ class Movie extends Model
|
||||
$q->where('movie_meta', '-', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Wish, $this>
|
||||
*/
|
||||
public function wishes(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Wish::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ use App\Notifications\NewThank;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\GroupedLastScope;
|
||||
use App\Traits\TorrentFilter;
|
||||
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;
|
||||
use voku\helper\AntiXSS;
|
||||
|
||||
/**
|
||||
@@ -92,6 +94,7 @@ class Torrent extends Model
|
||||
|
||||
/** @use HasFactory<\Database\Factories\TorrentFactory> */
|
||||
use HasFactory;
|
||||
use Searchable;
|
||||
use SoftDeletes;
|
||||
use TorrentFilter;
|
||||
|
||||
@@ -481,4 +484,373 @@ class Torrent extends Model
|
||||
|
||||
return $this->free || config('other.freeleech') || $pfree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) $this->id,
|
||||
'name' => $this->name,
|
||||
'num_file' => $this->num_file,
|
||||
'folder' => $this->folder,
|
||||
'size' => $this->size,
|
||||
'leechers' => $this->leechers,
|
||||
'seeders' => $this->seeders,
|
||||
'times_completed' => $this->times_completed,
|
||||
'created_at' => $this->created_at?->timestamp,
|
||||
'bumped_at' => $this->bumped_at?->timestamp,
|
||||
'fl_until' => $this->fl_until?->timestamp,
|
||||
'du_until' => $this->du_until?->timestamp,
|
||||
'user_id' => $this->user_id,
|
||||
'imdb' => $this->imdb,
|
||||
'tvdb' => $this->tvdb,
|
||||
'tmdb' => $this->tmdb,
|
||||
'mal' => $this->mal,
|
||||
'igdb' => $this->igdb,
|
||||
'season_number' => $this->season_number,
|
||||
'episode_number' => $this->episode_number,
|
||||
'stream' => $this->stream,
|
||||
'free' => $this->free,
|
||||
'doubleup' => $this->doubleup,
|
||||
'refundable' => $this->refundable,
|
||||
'highspeed' => $this->highspeed,
|
||||
'featured' => $this->featured,
|
||||
'status' => $this->status,
|
||||
'anon' => $this->anon,
|
||||
'sticky' => $this->sticky,
|
||||
'sd' => $this->sd,
|
||||
'internal' => $this->internal,
|
||||
'release_year' => $this->release_year,
|
||||
'deleted_at' => $this->deleted_at?->timestamp,
|
||||
'distributor_id' => $this->distributor_id,
|
||||
'region_id' => $this->region_id,
|
||||
'personal_release' => $this->personal_release,
|
||||
'info_hash' => bin2hex($this->info_hash),
|
||||
'user' => json_decode($this->json_user ?? 'null'),
|
||||
'type' => json_decode($this->json_type ?? 'null'),
|
||||
'category' => json_decode($this->json_category ?? 'null'),
|
||||
'resolution' => json_decode($this->json_resolution ?? 'null'),
|
||||
'movie' => json_decode($this->json_movie ?? 'null'),
|
||||
'tv' => json_decode($this->json_tv ?? 'null'),
|
||||
'playlists' => json_decode($this->json_playlists ?? []),
|
||||
'freeleech_tokens' => json_decode($this->json_freeleech_tokens ?? []),
|
||||
'bookmarks' => json_decode($this->json_bookmarks ?? []),
|
||||
'files' => json_decode($this->json_files ?? []),
|
||||
'keywords' => json_decode($this->json_keywords ?? []),
|
||||
'history_seeders' => json_decode($this->json_history_seeders ?? []),
|
||||
'history_leechers' => json_decode($this->json_history_leechers ?? []),
|
||||
'history_active' => json_decode($this->json_history_active ?? []),
|
||||
'history_inactive' => json_decode($this->json_history_inactive ?? []),
|
||||
'history_complete' => json_decode($this->json_history_complete ?? []),
|
||||
'history_incomplete' => json_decode($this->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("
|
||||
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,
|
||||
torrents.release_year,
|
||||
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,
|
||||
'music_meta', categories.music_meta,
|
||||
'game_meta', categories.game_meta,
|
||||
'tv_meta', categories.tv_meta,
|
||||
'movie_meta', categories.movie_meta
|
||||
)
|
||||
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 JSON_OBJECT(
|
||||
'id', movies.id,
|
||||
'name', movies.title,
|
||||
'year', YEAR(movies.release_date),
|
||||
'poster', movies.poster,
|
||||
'original_language', movies.original_language,
|
||||
'adult', movies.adult,
|
||||
'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_id', (
|
||||
SELECT 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,
|
||||
'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.id
|
||||
)), JSON_ARRAY())
|
||||
FROM playlist_torrents
|
||||
WHERE torrents.id = playlist_torrents.playlist_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
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,4 +150,12 @@ class Tv extends Model
|
||||
{
|
||||
return $this->belongsToMany(__CLASS__, Recommendation::class, 'tv_id', 'recommendation_tv_id', 'id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Wish, $this>
|
||||
*/
|
||||
public function wishes(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(Wish::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,19 +25,30 @@ use ReflectionException;
|
||||
trait TorrentMeta
|
||||
{
|
||||
/**
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, \App\Models\Torrent>|\Illuminate\Pagination\CursorPaginator<\App\Models\Torrent>|\Illuminate\Pagination\LengthAwarePaginator<\App\Models\Torrent> $torrents
|
||||
* @param \Illuminate\Database\Eloquent\Collection<int, \App\Models\Torrent>|\Illuminate\Pagination\CursorPaginator<\App\Models\Torrent>|\Illuminate\Pagination\LengthAwarePaginator<\App\Models\Torrent>|\Illuminate\Contracts\Pagination\LengthAwarePaginator<\App\Models\Torrent> $torrents
|
||||
*
|
||||
* @throws \MarcReichel\IGDBLaravel\Exceptions\MissingEndpointException
|
||||
* @throws \MarcReichel\IGDBLaravel\Exceptions\InvalidParamsException
|
||||
* @throws ReflectionException
|
||||
* @throws JsonException
|
||||
* @return ($torrents is \Illuminate\Database\Eloquent\Collection<int, \App\Models\Torrent> ? \Illuminate\Support\Collection<int, \App\Models\Torrent> : ($torrents is \Illuminate\Pagination\CursorPaginator<\App\Models\Torrent> ? \Illuminate\Pagination\CursorPaginator<\App\Models\Torrent> : \Illuminate\Pagination\LengthAwarePaginator<\App\Models\Torrent>))
|
||||
* @return (
|
||||
* $torrents is \Illuminate\Database\Eloquent\Collection<int, \App\Models\Torrent> ? \Illuminate\Support\Collection<int, \App\Models\Torrent>
|
||||
* : ($torrents is \Illuminate\Pagination\CursorPaginator<\App\Models\Torrent> ? \Illuminate\Pagination\CursorPaginator<\App\Models\Torrent>
|
||||
* : ($torrents is \Illuminate\Pagination\LengthAwarePaginator<\App\Models\Torrent> ? \Illuminate\Pagination\LengthAwarePaginator<\App\Models\Torrent>
|
||||
* : \Illuminate\Contracts\Pagination\LengthAwarePaginator<\App\Models\Torrent>
|
||||
* )))
|
||||
*/
|
||||
public function scopeMeta(\Illuminate\Database\Eloquent\Collection|\Illuminate\Pagination\CursorPaginator|\Illuminate\Pagination\LengthAwarePaginator $torrents): \Illuminate\Support\Collection|\Illuminate\Pagination\CursorPaginator|\Illuminate\Pagination\LengthAwarePaginator
|
||||
public function scopeMeta(\Illuminate\Database\Eloquent\Collection|\Illuminate\Pagination\CursorPaginator|\Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Contracts\Pagination\LengthAwarePaginator $torrents): \Illuminate\Support\Collection|\Illuminate\Pagination\CursorPaginator|\Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
{
|
||||
$movieIds = $torrents->where('meta', '=', 'movie')->pluck('tmdb');
|
||||
$tvIds = $torrents->where('meta', '=', 'tv')->pluck('tmdb');
|
||||
$gameIds = $torrents->where('meta', '=', 'game')->pluck('igdb');
|
||||
if ($torrents instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator || $torrents instanceof \Illuminate\Contracts\Pagination\CursorPaginator) {
|
||||
$movieIds = collect($torrents->items())->where('meta', '=', 'movie')->pluck('tmdb');
|
||||
$tvIds = collect($torrents->items())->where('meta', '=', 'tv')->pluck('tmdb');
|
||||
$gameIds = collect($torrents->items())->where('meta', '=', 'game')->pluck('igdb');
|
||||
} else {
|
||||
$movieIds = $torrents->where('meta', '=', 'movie')->pluck('tmdb');
|
||||
$tvIds = $torrents->where('meta', '=', 'tv')->pluck('tmdb');
|
||||
$gameIds = $torrents->where('meta', '=', 'game')->pluck('igdb');
|
||||
}
|
||||
|
||||
$movies = Movie::with('genres')->whereIntegerInRaw('id', $movieIds)->get()->keyBy('id');
|
||||
$tv = Tv::with('genres')->whereIntegerInRaw('id', $tvIds)->get()->keyBy('id');
|
||||
@@ -65,6 +76,14 @@ trait TorrentMeta
|
||||
return $torrents->map($setRelation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel's \Illuminate\Contracts\Pagination\LengthAwarePaginator does not have a through method
|
||||
* but we are passed a \Illuminate\Pagination\LengthAwarePaginator which does have such a method.
|
||||
* Seems to be caused by some Laravel type error that's returning an interface instead of the type
|
||||
* itself, or that the interface is missing the method.
|
||||
*
|
||||
* @phpstan-ignore method.notFound
|
||||
*/
|
||||
return $torrents->through($setRelation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
"laravel/fortify": "1.20.0",
|
||||
"laravel/framework": "^11.19.0",
|
||||
"laravel/octane": "^2.5.2",
|
||||
"laravel/scout": "^10.11",
|
||||
"laravel/tinker": "^2.9.0",
|
||||
"livewire/livewire": "^3.5.4",
|
||||
"marcreichel/igdb-laravel": "^4.3.0",
|
||||
"meilisearch/meilisearch-php": "^1.9",
|
||||
"nesbot/carbon": "2.72.3",
|
||||
"paragonie/constant_time_encoding": "^2.7.0",
|
||||
"spatie/laravel-backup": "^8.8.1",
|
||||
|
||||
226
composer.lock
generated
226
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6df7a44ea0115c5842c2d7e199210b81",
|
||||
"content-hash": "8a579c31f1ba56bdc01cf189e5b971d8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "assada/laravel-achievements",
|
||||
@@ -2617,6 +2617,84 @@
|
||||
},
|
||||
"time": "2024-06-17T13:58:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/scout",
|
||||
"version": "v10.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/scout.git",
|
||||
"reference": "42482b9fb8d3f82bdb52307a990107231c50b882"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/scout/zipball/42482b9fb8d3f82bdb52307a990107231c50b882",
|
||||
"reference": "42482b9fb8d3f82bdb52307a990107231c50b882",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/bus": "^9.0|^10.0|^11.0",
|
||||
"illuminate/contracts": "^9.0|^10.0|^11.0",
|
||||
"illuminate/database": "^9.0|^10.0|^11.0",
|
||||
"illuminate/http": "^9.0|^10.0|^11.0",
|
||||
"illuminate/pagination": "^9.0|^10.0|^11.0",
|
||||
"illuminate/queue": "^9.0|^10.0|^11.0",
|
||||
"illuminate/support": "^9.0|^10.0|^11.0",
|
||||
"php": "^8.0",
|
||||
"symfony/console": "^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"algolia/algoliasearch-client-php": "^3.2",
|
||||
"meilisearch/meilisearch-php": "^1.0",
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^7.31|^8.11|^9.0",
|
||||
"php-http/guzzle7-adapter": "^1.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.3|^10.4",
|
||||
"typesense/typesense-php": "^4.9.3"
|
||||
},
|
||||
"suggest": {
|
||||
"algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
|
||||
"meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).",
|
||||
"typesense/typesense-php": "Required to use the Typesense engine (^4.9)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Scout\\ScoutServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Scout\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel Scout provides a driver based solution to searching your Eloquent models.",
|
||||
"keywords": [
|
||||
"algolia",
|
||||
"laravel",
|
||||
"search"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/scout/issues",
|
||||
"source": "https://github.com/laravel/scout"
|
||||
},
|
||||
"time": "2024-07-30T15:28:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v1.3.3",
|
||||
@@ -3331,6 +3409,73 @@
|
||||
},
|
||||
"time": "2024-03-31T07:05:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "meilisearch/meilisearch-php",
|
||||
"version": "v1.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/meilisearch/meilisearch-php.git",
|
||||
"reference": "c4eb8ecd08799abd65d00dc74f4372b61af1fc37"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/c4eb8ecd08799abd65d00dc74f4372b61af1fc37",
|
||||
"reference": "c4eb8ecd08799abd65d00dc74f4372b61af1fc37",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"php-http/discovery": "^1.7",
|
||||
"psr/http-client": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.8.1",
|
||||
"http-interop/http-factory-guzzle": "^1.2.0",
|
||||
"php-cs-fixer/shim": "^3.59.3",
|
||||
"phpstan/extension-installer": "^1.4.1",
|
||||
"phpstan/phpstan": "^1.11.5",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.4.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.6.0",
|
||||
"phpunit/phpunit": "^9.5 || ^10.5"
|
||||
},
|
||||
"suggest": {
|
||||
"guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client",
|
||||
"http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MeiliSearch\\": "src/",
|
||||
"Meilisearch\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Clementine",
|
||||
"email": "clementine@meilisearch.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP wrapper for the Meilisearch API",
|
||||
"keywords": [
|
||||
"api",
|
||||
"client",
|
||||
"instant",
|
||||
"meilisearch",
|
||||
"php",
|
||||
"search"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/meilisearch/meilisearch-php/issues",
|
||||
"source": "https://github.com/meilisearch/meilisearch-php/tree/v1.9.1"
|
||||
},
|
||||
"time": "2024-07-25T15:54:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.7.0",
|
||||
@@ -3900,6 +4045,85 @@
|
||||
},
|
||||
"time": "2024-05-08T12:18:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/discovery",
|
||||
"version": "1.19.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/discovery.git",
|
||||
"reference": "0700efda8d7526335132360167315fdab3aeb599"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599",
|
||||
"reference": "0700efda8d7526335132360167315fdab3aeb599",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0|^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"nyholm/psr7": "<1.0",
|
||||
"zendframework/zend-diactoros": "*"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "*",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"psr/http-message-implementation": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^1.0.2|^2.0",
|
||||
"graham-campbell/phpspec-skip-example-extension": "^5.0",
|
||||
"php-http/httplug": "^1.0 || ^2.0",
|
||||
"php-http/message-factory": "^1.0",
|
||||
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
|
||||
"sebastian/comparator": "^3.0.5 || ^4.0.8",
|
||||
"symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "Http\\Discovery\\Composer\\Plugin",
|
||||
"plugin-optional": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Discovery\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/Composer/Plugin.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"adapter",
|
||||
"client",
|
||||
"discovery",
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr17",
|
||||
"psr7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/discovery/issues",
|
||||
"source": "https://github.com/php-http/discovery/tree/1.19.4"
|
||||
},
|
||||
"time": "2024-03-29T13:00:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.3",
|
||||
|
||||
288
config/scout.php
Normal file
288
config/scout.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'meilisearch'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', ''),
|
||||
'secret' => env('ALGOLIA_SECRET', ''),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
App\Models\Torrent::class => [
|
||||
'searchableAttributes' => [
|
||||
'name',
|
||||
'movie.name',
|
||||
'tv.name',
|
||||
'movie.year',
|
||||
'tv.year',
|
||||
'type.name',
|
||||
'resolution.name',
|
||||
],
|
||||
'filterableAttributes' => [
|
||||
'id',
|
||||
'name',
|
||||
'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',
|
||||
'release_year',
|
||||
'deleted_at',
|
||||
'personal_release',
|
||||
'info_hash',
|
||||
'history_seeders.user_id',
|
||||
'history_leechers.user_id',
|
||||
'history_active.user_id',
|
||||
'history_inactive.user_id',
|
||||
'history_complete.user_id',
|
||||
'history_incomplete.user_id',
|
||||
'user.username',
|
||||
'category.id',
|
||||
'category.movie_meta',
|
||||
'category.tv_meta',
|
||||
'type.id',
|
||||
'resolution.id',
|
||||
'movie.id',
|
||||
'movie.name',
|
||||
'movie.original_language',
|
||||
'movie.adult',
|
||||
'movie.genres.id',
|
||||
'movie.collection.id',
|
||||
'movie.companies.id',
|
||||
'tv.id',
|
||||
'tv.name',
|
||||
'tv.original_language',
|
||||
'tv.genres.id',
|
||||
'tv.networks.id',
|
||||
'tv.companies.id',
|
||||
'playlists.id',
|
||||
'bookmarks.user_id',
|
||||
'freeleech_tokens.user_id',
|
||||
'files.name',
|
||||
'keywords',
|
||||
'distributor_id',
|
||||
'region_id',
|
||||
],
|
||||
'sortableAttributes' => [
|
||||
'name',
|
||||
'size',
|
||||
'seeders',
|
||||
'leechers',
|
||||
'times_completed',
|
||||
'created_at',
|
||||
'bumped_at',
|
||||
'sticky',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -89,6 +89,20 @@ services:
|
||||
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
|
||||
networks:
|
||||
- sail
|
||||
meilisearch:
|
||||
image: 'getmeili/meilisearch:latest'
|
||||
ports:
|
||||
- '${FORWARD_MEILISEARCH_PORT:-7700}:7700'
|
||||
environment:
|
||||
MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}'
|
||||
volumes:
|
||||
- 'sail-meilisearch:/meili_data'
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health" ]
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
networks:
|
||||
sail:
|
||||
driver: bridge
|
||||
@@ -99,3 +113,5 @@ volumes:
|
||||
driver: local
|
||||
sail-redis:
|
||||
driver: local
|
||||
sail-meilisearch:
|
||||
driver: local
|
||||
|
||||
10
phpstan.neon
10
phpstan.neon
@@ -22,3 +22,13 @@ parameters:
|
||||
- bootstrap/cache
|
||||
level: 7
|
||||
checkOctaneCompatibility: true
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\<App\\Models\\Torrent\>\:\:searchable\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
-
|
||||
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany\<App\\Models\\Torrent, [a-zA-Z0-9\\_]+\>\:\:searchable\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
-
|
||||
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsTo\<App\\Models\\Torrent, [a-zA-Z0-9\\_]+\>\:\:searchable\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
-->
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="SCOUT_DRIVER" value="null"/>
|
||||
</php>
|
||||
<coverage/>
|
||||
<source>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
/>
|
||||
@if (strlen($quicksearchText) > 0)
|
||||
<div class="quick-search__results" x-ref="searchResults">
|
||||
@forelse ($search_results as $search_result)
|
||||
@forelse ($search_results['hits'] ?? $search_results as $search_result)
|
||||
<article
|
||||
class="quick-search__result"
|
||||
x-on:keydown.down.prevent="quickSearchArrowDown($el)"
|
||||
@@ -69,21 +69,21 @@
|
||||
@case('movies')
|
||||
<a
|
||||
class="quick-search__result-link"
|
||||
href="{{ route('torrents.similar', ['category_id' => $search_result['category_id'], 'tmdb' => $search_result['id']]) }}"
|
||||
href="{{ route('torrents.similar', ['category_id' => $search_result['category']['id'], 'tmdb' => $search_result['tmdb']]) }}"
|
||||
>
|
||||
<img
|
||||
class="quick-search__image"
|
||||
src="{{ isset($search_result['poster']) ? \tmdb_image('poster_small', $search_result['poster']) : 'https://via.placeholder.com/90x135' }}"
|
||||
src="{{ isset($search_result['movie']['poster']) ? \tmdb_image('poster_small', $search_result['movie']['poster']) : 'https://via.placeholder.com/90x135' }}"
|
||||
alt=""
|
||||
/>
|
||||
<h2 class="quick-search__result-text">
|
||||
{{ $search_result['title'] }}
|
||||
{{ $search_result['movie']['name'] }}
|
||||
<time
|
||||
class="quick-search__result-year"
|
||||
datetime="{{ $search_result['release_date'] }}"
|
||||
title="{{ $search_result['release_date'] }}"
|
||||
datetime="{{ $search_result['movie']['year'] }}"
|
||||
title="{{ $search_result['movie']['year'] }}"
|
||||
>
|
||||
{{ substr($search_result['release_date'], 0, 4) }}
|
||||
{{ substr($search_result['movie']['year'], 0, 4) }}
|
||||
</time>
|
||||
</h2>
|
||||
</a>
|
||||
@@ -92,21 +92,21 @@
|
||||
@case('series')
|
||||
<a
|
||||
class="quick-search__result-link"
|
||||
href="{{ route('torrents.similar', ['category_id' => $search_result['category_id'], 'tmdb' => $search_result['id']]) }}"
|
||||
href="{{ route('torrents.similar', ['category_id' => $search_result['category']['id'], 'tmdb' => $search_result['tmdb']]) }}"
|
||||
>
|
||||
<img
|
||||
class="quick-search__image"
|
||||
src="{{ isset($search_result['poster']) ? \tmdb_image('poster_small', $search_result['poster']) : 'https://via.placeholder.com/90x135' }}"
|
||||
src="{{ isset($search_result['tv']['poster']) ? \tmdb_image('poster_small', $search_result['tv']['poster']) : 'https://via.placeholder.com/90x135' }}"
|
||||
alt=""
|
||||
/>
|
||||
<h2 class="quick-search__result-text">
|
||||
{{ $search_result['name'] }}
|
||||
{{ $search_result['tv']['name'] }}
|
||||
<time
|
||||
class="quick-search__result-year"
|
||||
datetime="{{ $search_result['first_air_date'] }}"
|
||||
title="{{ $search_result['first_air_date'] }}"
|
||||
datetime="{{ $search_result['tv']['year'] }}"
|
||||
title="{{ $search_result['tv']['year'] }}"
|
||||
>
|
||||
{{ substr($search_result['first_air_date'], 0, 4) }}
|
||||
{{ $search_result['tv']['year'] }}
|
||||
</time>
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user