Files
UNIT3D-Community-Edition/app/Http/Controllers/TorrentController.php
2024-09-03 16:20:04 +00:00

511 lines
19 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\Http\Controllers;
use App\Helpers\Bencode;
use App\Helpers\MediaInfo;
use App\Helpers\TorrentHelper;
use App\Helpers\TorrentTools;
use App\Http\Requests\StoreTorrentRequest;
use App\Http\Requests\UpdateTorrentRequest;
use App\Models\Audit;
use App\Models\Category;
use App\Models\Distributor;
use App\Models\FeaturedTorrent;
use App\Models\History;
use App\Models\Keyword;
use App\Models\Movie;
use App\Models\Region;
use App\Models\Resolution;
use App\Models\Scopes\ApprovedScope;
use App\Models\Torrent;
use App\Models\TorrentFile;
use App\Models\Tv;
use App\Models\Type;
use App\Models\User;
use App\Repositories\ChatRepository;
use App\Services\Tmdb\TMDBScraper;
use App\Services\Unit3dAnnounce;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Intervention\Image\Facades\Image;
use MarcReichel\IGDBLaravel\Models\Game;
use MarcReichel\IGDBLaravel\Models\PlatformLogo;
use Exception;
use ReflectionException;
use JsonException;
/**
* @see \Tests\Todo\Feature\Http\Controllers\TorrentControllerTest
*/
class TorrentController extends Controller
{
/**
* TorrentController Constructor.
*/
public function __construct(private readonly ChatRepository $chatRepository)
{
}
/**
* Display a listing of the Torrent resource.
*/
public function index(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return view('torrent.index');
}
/**
* Display The Torrent reasource.
*
* @throws JsonException
* @throws \MarcReichel\IGDBLaravel\Exceptions\MissingEndpointException
* @throws ReflectionException
* @throws \MarcReichel\IGDBLaravel\Exceptions\InvalidParamsException
*/
public function show(Request $request, int|string $id): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
$user = $request->user();
$torrent = Torrent::withoutGlobalScope(ApprovedScope::class)
->with(['user', 'comments', 'category', 'type', 'resolution', 'subtitles', 'playlists'])
->withCount([
'bookmarks',
'seeds' => fn ($query) => $query->where('active', '=', true)->where('visible', '=', true),
'leeches' => fn ($query) => $query->where('active', '=', true)->where('visible', '=', true),
])
->withExists([
'bookmarks' => fn ($query) => $query->where('user_id', '=', $user->id),
'freeleechTokens' => fn ($query) => $query->where('user_id', '=', $user->id),
'trump'
])
->findOrFail($id);
$meta = null;
$platforms = null;
if ($torrent->category->tv_meta && $torrent->tmdb) {
$meta = Tv::with([
'genres',
'credits' => ['person', 'occupation'],
'companies',
'networks',
'recommendedTv:id,name,poster,first_air_date'
])->find($torrent->tmdb);
}
if ($torrent->category->movie_meta && $torrent->tmdb) {
$meta = Movie::with([
'genres',
'credits' => ['person', 'occupation'],
'companies',
'collection',
'recommendedMovies:id,title,poster,release_date'
])
->find($torrent->tmdb);
}
if ($torrent->category->game_meta && $torrent->igdb) {
$meta = Game::with([
'cover' => ['url', 'image_id'],
'artworks' => ['url', 'image_id'],
'genres' => ['name'],
'videos' => ['video_id', 'name'],
'involved_companies.company',
'involved_companies.company.logo',
'platforms', ])
->find($torrent->igdb);
$link = collect($meta->videos)->take(1)->pluck('video_id');
$platforms = PlatformLogo::whereIn('id', collect($meta->platforms)->pluck('platform_logo')->toArray())->get();
}
return view('torrent.show', [
'torrent' => $torrent,
'user' => $user,
'canEdit' => $user->group->is_editor
|| $user->group->is_modo
|| (
$user->id === $torrent->user_id
&& (
$torrent->status !== Torrent::APPROVED
|| now()->isBefore($torrent->created_at->addDay())
)
),
'personal_freeleech' => cache()->get('personal_freeleech:'.$user->id),
'meta' => $meta,
'platforms' => $platforms,
'total_tips' => $torrent->tips()->sum('bon'),
'user_tips' => $torrent->tips()->where('sender_id', '=', $user->id)->sum('bon'),
'featured' => $torrent->featured === true ? FeaturedTorrent::where('torrent_id', '=', $id)->first() : null,
'mediaInfo' => $torrent->mediainfo !== null ? (new MediaInfo())->parse($torrent->mediainfo) : null,
'last_seed_activity' => History::where('torrent_id', '=', $torrent->id)->where('seeder', '=', 1)->latest('updated_at')->first(),
'playlists' => $user->playlists,
'audits' => Audit::with('user')->where('model_entry_id', '=', $torrent->id)->where('model_name', '=', 'Torrent')->latest()->get(),
]);
}
/**
* Show the form for editing the specified Torrent resource.
*/
public function edit(Request $request, int $id): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
$user = $request->user();
$torrent = Torrent::withoutGlobalScope(ApprovedScope::class)->findOrFail($id);
abort_unless($user->group->is_editor || $user->group->is_modo || $user->id === $torrent->user_id, 403);
return view('torrent.edit', [
'categories' => Category::query()
->orderBy('position')
->get()
->mapWithKeys(fn ($cat) => [
$cat['id'] => [
'name' => $cat['name'],
'type' => match (true) {
$cat->movie_meta => 'movie',
$cat->tv_meta => 'tv',
$cat->game_meta => 'game',
$cat->music_meta => 'music',
$cat->no_meta => 'no',
default => 'no',
},
]
]),
'types' => Type::orderBy('position')->get()->mapWithKeys(fn ($type) => [$type['id'] => ['name' => $type['name']]]),
'resolutions' => Resolution::orderBy('position')->get(),
'regions' => Region::orderBy('position')->get(),
'distributors' => Distributor::orderBy('name')->get(),
'keywords' => Keyword::where('torrent_id', '=', $torrent->id)->pluck('name'),
'torrent' => $torrent,
'user' => $user,
]);
}
/**
* Update the specified Torrent resource in storage.
*/
public function update(UpdateTorrentRequest $request, int $id): \Illuminate\Http\RedirectResponse
{
$user = $request->user();
$torrent = Torrent::withoutGlobalScope(ApprovedScope::class)->findOrFail($id);
abort_unless(
$user->group->is_editor
|| $user->group->is_modo
|| (
$user->id === $torrent->user_id
&& (
$torrent->status !== Torrent::APPROVED
|| now()->isBefore($torrent->created_at->addDay())
)
),
403
);
$torrent->update($request->validated());
// Cover Image for No-Meta Torrents
if ($request->hasFile('torrent-cover')) {
$image_cover = $request->file('torrent-cover');
abort_if(\is_array($image_cover), 400);
$filename_cover = 'torrent-cover_'.$torrent->id.'.jpg';
$path_cover = public_path('/files/img/'.$filename_cover);
Image::make($image_cover->getRealPath())->fit(400, 600)->encode('jpg', 90)->save($path_cover);
}
// Banner Image for No-Meta Torrents
if ($request->hasFile('torrent-banner')) {
$image_cover = $request->file('torrent-banner');
abort_if(\is_array($image_cover), 400);
$filename_cover = 'torrent-banner_'.$torrent->id.'.jpg';
$path_cover = public_path('/files/img/'.$filename_cover);
Image::make($image_cover->getRealPath())->fit(960, 540)->encode('jpg', 90)->save($path_cover);
}
// Torrent Keywords System
Keyword::where('torrent_id', '=', $torrent->id)->delete();
$keywords = [];
foreach (TorrentTools::parseKeywords($request->string('keywords')) as $keyword) {
$keywords[] = ['torrent_id' => $torrent->id, 'name' => $keyword];
}
foreach (collect($keywords)->chunk(65_000 / 2) as $keywords) {
Keyword::upsert($keywords->toArray(), ['torrent_id', 'name']);
}
$category = $torrent->category;
// TMDB Meta
if ($torrent->tmdb != 0) {
switch (true) {
case $category->tv_meta:
(new TMDBScraper())->tv($torrent->tmdb);
break;
case $category->movie_meta:
(new TMDBScraper())->movie($torrent->tmdb);
break;
}
}
return to_route('torrents.show', ['id' => $id])
->withSuccess('Successfully Edited!');
}
/**
* Delete A Torrent.
*
* @throws Exception
*/
public function destroy(Request $request, int $id): \Illuminate\Http\RedirectResponse
{
$request->validate([
'message' => [
'required',
'min:1',
],
]);
$user = $request->user();
$torrent = Torrent::withoutGlobalScope(ApprovedScope::class)->findOrFail($id);
abort_unless($user->group->is_modo || ($user->id === $torrent->user_id && Carbon::now()->lt($torrent->created_at->addDay())), 403);
foreach (History::where('torrent_id', '=', $torrent->id)->pluck('user_id') as $user_id) {
User::sendSystemNotificationTo(
userId: $user_id,
subject: 'Torrent Deleted! - '.$torrent->name,
message: '[b]Attention:[/b] Torrent '.$torrent->name." has been removed from our site. Our system shows that you were either the uploader, a seeder or a leecher on said torrent. We just wanted to let you know you can safely remove it from your client.\n\n[b]Removal Reason:[/b] ".$request->message,
);
}
// Reset Requests
$torrent->requests()->update([
'torrent_id' => null,
]);
//Remove Torrent related info
cache()->forget(\sprintf('torrent:%s', $torrent->info_hash));
$torrent->comments()->delete();
$torrent->peers()->delete();
$torrent->history()->delete();
$torrent->hitrun()->delete();
$torrent->files()->delete();
$torrent->playlists()->detach();
$torrent->subtitles()->delete();
$torrent->resurrections()->delete();
$torrent->featured()->delete();
$freeleechTokens = $torrent->freeleechTokens();
foreach ($freeleechTokens->get() as $freeleechToken) {
cache()->forget('freeleech_token:'.$freeleechToken->user_id.':'.$torrent->id);
}
$freeleechTokens->delete();
cache()->forget('announce-torrents:by-infohash:'.$torrent->info_hash);
Unit3dAnnounce::removeTorrent($torrent);
$torrent->delete();
return to_route('torrents.index')
->withSuccess('Torrent Has Been Deleted!');
}
/**
* Torrent Upload Form.
*/
public function create(Request $request): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
$user = $request->user();
abort_unless($user->can_upload ?? $user->group->can_upload, 403, __('torrent.cant-upload').' '.__('torrent.cant-upload-desc'));
return view('torrent.create', [
'categories' => Category::orderBy('position')
->get()
->mapWithKeys(fn ($category) => [$category->id => [
'name' => $category->name,
'type' => match (true) {
$category->movie_meta => 'movie',
$category->tv_meta => 'tv',
$category->game_meta => 'game',
$category->music_meta => 'music',
$category->no_meta => 'no',
default => 'no',
},
]])
->toArray(),
'types' => Type::orderBy('position')->get(),
'resolutions' => Resolution::orderBy('position')->get(),
'regions' => Region::orderBy('position')->get(),
'distributors' => Distributor::orderBy('name')->get(),
'user' => $request->user(),
'category_id' => $request->category_id ?? Category::query()->first()->id,
'title' => urldecode((string) $request->title),
'imdb' => $request->imdb,
'tmdb' => $request->tmdb,
'mal' => $request->mal,
'tvdb' => $request->tvdb,
'igdb' => $request->igdb,
]);
}
/**
* Upload A Torrent.
*/
public function store(StoreTorrentRequest $request): \Illuminate\Http\RedirectResponse
{
$user = $request->user();
abort_unless($user->can_upload ?? $user->group->can_upload, 403, __('torrent.cant-upload').' '.__('torrent.cant-upload-desc'));
abort_if(\is_array($request->file('torrent')), 400);
abort_if(\is_array($request->file('nfo')), 400);
$decodedTorrent = TorrentTools::normalizeTorrent($request->file('torrent'));
$meta = Bencode::get_meta($decodedTorrent);
$fileName = uniqid('', true).'.torrent'; // Generate a unique name
file_put_contents(getcwd().'/files/torrents/'.$fileName, Bencode::bencode($decodedTorrent));
$torrent = Torrent::create([
'mediainfo' => TorrentTools::anonymizeMediainfo($request->filled('mediainfo') ? $request->string('mediainfo') : null),
'info_hash' => Bencode::get_infohash($decodedTorrent),
'file_name' => $fileName,
'num_file' => $meta['count'],
'folder' => Bencode::get_name($decodedTorrent),
'size' => $meta['size'],
'nfo' => $request->hasFile('nfo') ? TorrentTools::getNfo($request->file('nfo')) : '',
'user_id' => $user->id,
'moderated_at' => now(),
'moderated_by' => User::SYSTEM_USER_ID,
] + $request->safe()->except(['torrent']));
// Populate the status/seeders/leechers/times_completed fields for the external tracker
$torrent->refresh();
Unit3dAnnounce::addTorrent($torrent);
if ($torrent->getAttribute('featured')) {
Unit3dAnnounce::addFeaturedTorrent($torrent->id);
}
$category = Category::findOrFail($request->integer('category_id'));
// Backup the files contained in the torrent
$files = TorrentTools::getTorrentFiles($decodedTorrent);
foreach ($files as &$file) {
$file['torrent_id'] = $torrent->id;
}
// Can't insert them all at once since some torrents have more files than mysql supports placeholders.
// Divide by 3 since we're inserting 3 fields: name, size and torrent_id
foreach (collect($files)->chunk(intdiv(65_000, 3)) as $files) {
TorrentFile::insert($files->toArray());
}
// TMDB Meta
if ($torrent->tmdb != 0) {
switch (true) {
case $category->tv_meta:
(new TMDBScraper())->tv($torrent->tmdb);
break;
case $category->movie_meta:
(new TMDBScraper())->movie($torrent->tmdb);
break;
}
}
// Torrent Keywords System
$keywords = [];
foreach (TorrentTools::parseKeywords($request->string('keywords')) as $keyword) {
$keywords[] = ['torrent_id' => $torrent->id, 'name' => $keyword];
}
foreach (collect($keywords)->chunk(intdiv(65_000, 2)) as $keywords) {
Keyword::upsert($keywords->toArray(), ['torrent_id', 'name']);
}
// Cover Image for No-Meta Torrents
if ($request->hasFile('torrent-cover')) {
$image_cover = $request->file('torrent-cover');
abort_if(\is_array($image_cover), 400);
$filename_cover = 'torrent-cover_'.$torrent->id.'.jpg';
$path_cover = public_path('/files/img/'.$filename_cover);
Image::make($image_cover->getRealPath())->fit(400, 600)->encode('jpg', 90)->save($path_cover);
}
// Banner Image for No-Meta Torrents
if ($request->hasFile('torrent-banner')) {
$image_cover = $request->file('torrent-banner');
abort_if(\is_array($image_cover), 400);
$filename_cover = 'torrent-banner_'.$torrent->id.'.jpg';
$path_cover = public_path('/files/img/'.$filename_cover);
Image::make($image_cover->getRealPath())->fit(960, 540)->encode('jpg', 90)->save($path_cover);
}
// check for trusted user and update torrent
if ($user->group->is_trusted && !$request->boolean('mod_queue_opt_in')) {
$appurl = config('app.url');
$user = $torrent->user;
$username = $user->username;
$anon = $torrent->anon;
// Announce To Shoutbox
if ($anon == 0) {
$this->chatRepository->systemMessage(
\sprintf('User [url=%s/users/', $appurl).$username.']'.$username.\sprintf('[/url] has uploaded a new '.$torrent->category->name.'. [url=%s/torrents/', $appurl).$torrent->id.']'.$torrent->name.'[/url], grab it now!'
);
} else {
$this->chatRepository->systemMessage(
\sprintf('An anonymous user has uploaded a new '.$torrent->category->name.'. [url=%s/torrents/', $appurl).$torrent->id.']'.$torrent->name.'[/url], grab it now!'
);
}
if ($torrent->free >= 1) {
$this->chatRepository->systemMessage(
\sprintf('Ladies and Gents, [url=%s/torrents/', $appurl).$torrent->id.']'.$torrent->name.'[/url] has been granted '.$torrent->free.'% FreeLeech! Grab It While You Can!'
);
}
TorrentHelper::approveHelper($torrent->id);
}
return to_route('download_check', ['id' => $torrent->id])
->withSuccess('Your torrent file is ready to be downloaded and seeded!');
}
}