mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-01-20 02:50:41 -06:00
511 lines
19 KiB
PHP
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!');
|
|
}
|
|
}
|