find($sectionId); } else { $searchBox = SearchBox::query()->where('name', $sectionName)->first(); } if (empty($searchBox)) { throw new NexusException(nexus_trans("upload.invalid_section")); } if ($searchBox->isSectionSpecial() && !Permission::canViewSpecialSection()) { throw new InsufficientPermissionException(); } $categoryIdList = $searchBox->categories()->pluck('id')->toArray(); //query this info default $query = Torrent::query()->with([ 'basic_category', 'basic_category.search_box', 'basic_audiocodec', 'basic_codec', 'basic_medium', 'basic_source', 'basic_processing', 'basic_standard', 'basic_team', ]) ->whereIn('category', $categoryIdList) ->orderBy("pos_state", "DESC"); $allowIncludes = ['user', 'extra', 'tags']; $allowIncludeCounts = ['thank_users', 'reward_logs', 'claims']; $allowIncludeFields = [ 'has_bookmarked', 'has_claimed', 'has_thanked', 'has_rewarded', 'description', 'download_url' ]; $allowFilters = [ 'title', 'category', 'source', 'medium', 'codec', 'audiocodec', 'standard', 'processing', 'team', 'owner', 'visible', 'added', 'size', 'sp_state', 'leechers', 'seeders', 'times_completed' ]; $allowSorts = ['id', 'comments', 'size', 'seeders', 'leechers', 'times_completed']; $apiQueryBuilder = ApiQueryBuilder::for(TorrentResource::NAME, $query, $request) ->allowIncludes($allowIncludes) ->allowIncludeCounts($allowIncludeCounts) ->allowIncludeFields($allowIncludeFields) ->allowFilters($allowFilters) ->allowSorts($allowSorts) ->registerCustomFilter('title', function (Builder $query, Request $request) { $title = $request->input(ApiQueryBuilder::PARAM_NAME_FILTER.".title"); if ($title) { $query->where(function (Builder $query) use ($title) { $query->where('name', 'like', '%' . $title . '%') ->orWhere('small_descr', 'like', '%' . $title . '%'); }); } }) ; $query = $apiQueryBuilder->build(); if (!$apiQueryBuilder->hasSort()) { $query->orderBy("id", "DESC"); } $torrents = $query->paginate($this->getPerPageFromRequest($request)); return $this->appendIncludeFields($apiQueryBuilder, $user, $torrents); } public function getDetail($id, Authenticatable $user) { //query this info default $query = Torrent::query()->with([ 'basic_category', 'basic_category.search_box', 'basic_audiocodec', 'basic_codec', 'basic_medium', 'basic_source', 'basic_processing', 'basic_standard', 'basic_team', ]); $allowIncludes = ['user', 'extra', 'tags']; $allowIncludeCounts = ['thank_users', 'reward_logs', 'claims']; $allowIncludeFields = [ 'has_bookmarked', 'has_claimed', 'has_thanked', 'has_rewarded', 'description', 'download_url' ]; $apiQueryBuilder = ApiQueryBuilder::for(TorrentResource::NAME, $query) ->allowIncludes($allowIncludes) ->allowIncludeCounts($allowIncludeCounts) ->allowIncludeFields($allowIncludeFields) ; $torrent = $apiQueryBuilder->build()->findOrFail($id); $torrentList = $this->appendIncludeFields($apiQueryBuilder, $user, [$torrent]); return $torrentList[0]; } private function appendIncludeFields(ApiQueryBuilder $apiQueryBuilder, Authenticatable $user, $torrentList) { $torrentIdArr = $bookmarkData = $claimData = $thankData = $rewardData =[]; foreach ($torrentList as $torrent) { $torrentIdArr[] = $torrent->id; } unset($torrent); if ($hasFieldHasBookmarked = $apiQueryBuilder->hasIncludeField('has_bookmarked')) { $bookmarkData = $user->bookmarks()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); } if ($hasFieldHasClaimed = $apiQueryBuilder->hasIncludeField('has_claimed')) { $claimData = $user->claims()->whereIn('torrent_id', $torrentIdArr)->get()->keyBy('torrent_id'); } if ($hasFieldHasThanked = $apiQueryBuilder->hasIncludeField('has_thanked')) { $thankData = $user->thank_torrent_logs()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); } if ($hasFieldHasRewarded = $apiQueryBuilder->hasIncludeField('has_rewarded')) { $rewardData = $user->reward_torrent_logs()->whereIn('torrentid', $torrentIdArr)->get()->keyBy('torrentid'); } foreach ($torrentList as $torrent) { $id = $torrent->id; if ($hasFieldHasBookmarked) { $torrent->has_bookmarked = $bookmarkData->has($id); } if ($hasFieldHasClaimed) { $torrent->has_claimed = $claimData->has($id); } if ($hasFieldHasThanked) { $torrent->has_thanked = $thankData->has($id); } if ($hasFieldHasRewarded) { $torrent->has_rewarded = $rewardData->has($id); } if ($apiQueryBuilder->hasIncludeField('description') && $apiQueryBuilder->hasInclude('extra')) { $descriptionArr = format_description($torrent->extra->descr ?? ''); $torrent->description = $descriptionArr; $torrent->images = get_image_from_description($descriptionArr); } if ($apiQueryBuilder->hasIncludeField("download_url")) { $torrent->download_url = $this->getDownloadUrl($id, $user); } } return $torrentList; } public function getDownloadUrl($id, array|User $user): string { return sprintf( '%s/download.php?downhash=%s.%s', getSchemeAndHttpHost(), is_array($user) ? $user['id'] : $user->id, $this->encryptDownHash($id, $user) ); } private function handleGetListSort(Builder $query, array $params) { if (empty($params['sort_field']) && empty($params['sort_type'])) { //the default torrent list sort return $query->orderBy('pos_state', 'desc')->orderBy('id', 'desc'); } list($sortField, $sortType) = $this->getSortFieldAndType($params); return $query->orderBy($sortField, $sortType); } public function getSearchBox($id = null) { if (is_null($id)) { $id = Setting::get('main.browsecat'); } $searchBox = SearchBox::query()->findOrFail($id); $category = $searchBox->categories()->orderBy('sort_index')->orderBy('id')->get(); $modalRows = []; $modalRows[] = $categoryFormatted = $this->formatRow(Category::getLabelName(), $category, 'category'); if ($searchBox->showsubcat) { if ($searchBox->showsource) { $source = Source::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Source::getLabelName(), $source, 'source'); } if ($searchBox->showmedia) { $media = Media::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Media::getLabelName(), $media, 'medium'); } if ($searchBox->showcodec) { $codec = Codec::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Codec::getLabelName(), $codec, 'codec'); } if ($searchBox->showstandard) { $standard = Standard::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Standard::getLabelName(), $standard, 'standard'); } if ($searchBox->showprocessing) { $processing = Processing::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Processing::getLabelName(), $processing, 'processing'); } if ($searchBox->showteam) { $team = Team::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(Team::getLabelName(), $team, 'team'); } if ($searchBox->showaudiocodec) { $audioCodec = AudioCodec::query()->orderBy('sort_index')->orderBy('id')->get(); $modalRows[] = $this->formatRow(AudioCodec::getLabelName(), $audioCodec, 'audio_codec'); } } $results = []; $categories = $categoryFormatted['rows']; $categories[0]['active'] = 1; $results['categories'] = $categories; $results['modal_rows'] = $modalRows; return $results; } private function formatRow($header, $items, $name) { $result['header'] = $header; $result['rows'][] = [ 'label' => 'All', 'value' => 0, 'name' => $name, 'active' => 1, ]; foreach ($items as $value) { $item = [ 'label' => $value->name, 'value' => $value->id, 'name' => $name, 'active' => 0, ]; $result['rows'][] = $item; } return $result; } public function listPeers($torrentId) { $seederList = $leecherList = collect(); $peers = Peer::query() ->where('torrent', $torrentId) ->groupBy('peer_id') ->with(['user', 'relative_torrent']) ->get() ->groupBy('seeder'); if ($peers->has(Peer::SEEDER_YES)) { $seederList = $peers->get(Peer::SEEDER_YES)->sort(function ($a, $b) { $x = $a->uploaded; $y = $b->uploaded; if ($x == $y) return 0; if ($x < $y) return 1; return -1; }); $seederList = $this->formatPeers($seederList); } if ($peers->has(Peer::SEEDER_NO)) { $leecherList = $peers->get(Peer::SEEDER_NO)->sort(function ($a, $b) { $x = $a->to_go; $y = $b->to_go; if ($x == $y) return 0; if ($x < $y) return -1; return 1; }); $leecherList = $this->formatPeers($leecherList); } return [ 'seeder_list' => $seederList, 'leecher_list' => $leecherList, ]; } public function getPeerUploadSpeed($peer): string { $diff = $peer->uploaded - $peer->uploadoffset; $seconds = max(1, $peer->started->diffInSeconds($peer->last_action, true)); return mksize($diff / $seconds) . '/s'; } public function getPeerDownloadSpeed($peer): string { $diff = $peer->downloaded - $peer->downloadoffset; if ($peer->isSeeder()) { $seconds = max(1, $peer->started->diffInSeconds($peer->finishedat, true)); } else { $seconds = max(1, $peer->started->diffInSeconds($peer->last_action, true)); } return mksize($diff / $seconds) . '/s'; } public function getDownloadProgress($peer): string { return sprintf("%.2f%%", 100 * (1 - ($peer->to_go / $peer->relative_torrent->size))); } public function getShareRatio($peer) { if ($peer->downloaded) { $ratio = floor(($peer->uploaded / $peer->downloaded) * 1000) / 1000; } elseif ($peer->uploaded) { //@todo 读语言文件 $ratio = '无限'; } else { $ratio = '---'; } return $ratio; } private function formatPeers($peers) { foreach ($peers as &$item) { $item->upload_text = sprintf('%s@%s', mksize($item->uploaded), $this->getPeerUploadSpeed($item)); $item->download_text = sprintf('%s@%s', mksize($item->downloaded), $this->getPeerDownloadSpeed($item)); $item->download_progress = $this->getDownloadProgress($item); $item->share_ratio = $this->getShareRatio($item); $item->connect_time_total = $item->started->diffForHumans(); $item->last_action_human = $item->last_action->diffForHumans(); $item->agent_human = htmlspecialchars(get_agent($item->peer_id, $item->agent)); } return $peers; } public function listSnatches($torrentId) { $snatches = Snatch::query() ->where('torrentid', $torrentId) ->where('finished', Snatch::FINISHED_YES) ->with(['user']) ->orderBy('completedat', 'desc') ->paginate(); return $snatches; } public function getSnatchUploadSpeed($snatch) { if ($snatch->seedtime <= 0) { $speed = mksize(0); } else { $speed = mksize($snatch->uploaded / ($snatch->seedtime + $snatch->leechtime)); } return "$speed/s"; } public function getSnatchDownloadSpeed($snatch) { if ($snatch->leechtime <= 0) { $speed = mksize(0); } else { $speed = mksize($snatch->downloaded / $snatch->leechtime); } return "$speed/s"; } public function encryptDownHash($id, $user): string { $key = $this->getEncryptDownHashKey($user); $payload = [ 'id' => $id, 'exp' => time() + 3600 ]; return JWT::encode($payload, $key, 'HS256'); } public function decryptDownHash($downHash, $user) { $key = $this->getEncryptDownHashKey($user); try { $decoded = JWT::decode($downHash, new Key($key, 'HS256')); return [$decoded->id]; } catch (\Exception $e) { do_log("Invalid down hash: $downHash, " . $e->getMessage(), "error"); return ''; } } private function getEncryptDownHashKey($user) { $passkey = ""; if ($user instanceof User && $user->passkey) { $passkey = $user->passkey; } elseif (is_array($user) && !empty($user['passkey'])) { $passkey = $user['passkey']; } elseif (is_scalar($user)) { $user = User::query()->findOrFail(intval($user), ['id', 'passkey']); $passkey = $user->passkey; } if (empty($passkey)) { throw new \InvalidArgumentException("Invalid user: " . json_encode($user)); } //down hash is relative to user passkey return md5($passkey . date('Ymd') . $user['id']); } /** * @deprecated * @param $id * @param $uid * @param $initializeIfNotExists * @return string * @throws NexusException */ public function getTrackerReportAuthKey($id, $uid, $initializeIfNotExists = false): string { $key = $this->getTrackerReportAuthKeySecret($id, $uid, $initializeIfNotExists); $hash = (new Hashids($key))->encode(date('Ymd')); return sprintf('%s|%s|%s', $id, $uid, $hash); } /** * @deprecated * * check tracker report authkey * if valid, the result will be the date the key generate, else if will be empty string * * @date 2021/6/3 * @time 20:29 * @param $authKey * @return array * @throws NexusException */ public function checkTrackerReportAuthKey($authKey) { $arr = explode('|', $authKey); if (count($arr) != 3) { throw new NexusException('Invalid authkey'); } $id = $arr[0]; $uid = $arr[1]; $hash = $arr[2]; $key = $this->getTrackerReportAuthKeySecret($id, $uid); return (new Hashids($key))->decode($hash); } private function getTrackerReportAuthKeySecret($id, $uid, $initializeIfNotExists = false) { $secret = NexusDB::remember("torrent_secret_{$uid}_{$id}", 3600, function () use ($id, $uid) { return TorrentSecret::query() ->where('uid', $uid) ->whereIn('torrent_id', [0, $id]) ->orderBy('torrent_id', 'desc') ->orderBy('id', 'desc') ->first(); }); if ($secret) { return $secret->secret; } if ($initializeIfNotExists) { $insert = [ 'uid' => $uid, 'torrent_id' => 0, 'secret' => Str::random(), ]; do_log("[INSERT_TORRENT_SECRET] " . json_encode($insert)); TorrentSecret::query()->insert($insert); return $insert['secret']; } throw new NexusException('No valid report secret, please re-download this torrent.'); } /** * reset user tracker report authkey secret * * @param $uid * @param int $torrentId * @return string * @todo wrap with transaction * * @date 2021/6/3 * @time 20:15 */ public function resetTrackerReportAuthKeySecret($uid, $torrentId = 0): string { $insert = [ 'uid' => $uid, 'secret' => Str::random(), 'torrent_id' => $torrentId, ]; if ($torrentId > 0) { return TorrentSecret::query()->insert($insert); } TorrentSecret::query()->where('uid', $uid)->delete(); TorrentSecret::query()->insert($insert); return $insert['secret']; } public function buildApprovalModal($user, $torrentId) { $user = $this->getUser($user); user_can('torrent-approval', true); $torrent = Torrent::query()->findOrFail($torrentId, ['id', 'approval_status', 'banned']); $radios = []; foreach (Torrent::$approvalStatus as $key => $value) { if ($torrent->approval_status == $key) { $checked = " checked"; } else { $checked = ""; } $radios[] = sprintf( '', $key, $checked, nexus_trans("torrent.approval.status_text.$key") ); } $id = "torrent-approval"; $rows = []; $rowStyle = "display: flex; padding: 10px; align-items: center"; $labelStyle = "width: 80px"; $formId = "$id-form"; $rows[] = sprintf( '