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( '
%s:
%s
', $id, $rowStyle, $labelStyle, nexus_trans('torrent.approval.status_label'), implode('', $radios) ); $rows[] = sprintf( '
%s:
', $id, $rowStyle, $labelStyle, nexus_trans('torrent.approval.comment_label') ); $rows[] = sprintf('', $torrent->id); $html = sprintf('
%s
', $id, $formId, implode('', $rows)); return [ 'id' => $id, 'form_id' => $formId, 'title' => nexus_trans('torrent.approval.modal_title'), 'content' => $html, ]; } public function approval($user, array $params): array { $user = $this->getUser($user); user_can('torrent-approval', true); $torrent = Torrent::query()->findOrFail($params['torrent_id'], Torrent::$commentFields); $lastLog = TorrentOperationLog::query() ->where('torrent_id', $params['torrent_id']) ->where('uid', $user->id) ->orderBy('id', 'desc') ->first(); if ($torrent->approval_status == $params['approval_status'] && $lastLog && $lastLog->comment == $params['comment']) { //No change return $params; } $torrentUpdate = $torrentOperationLog = []; $torrentUpdate['approval_status'] = $params['approval_status']; $notifyUser = false; if ($params['approval_status'] == Torrent::APPROVAL_STATUS_ALLOW) { $torrentUpdate['banned'] = 'no'; $torrentUpdate['visible'] = 'yes'; if ($torrent->approval_status != $params['approval_status']) { $torrentOperationLog['action_type'] = TorrentOperationLog::ACTION_TYPE_APPROVAL_ALLOW; //increase promotion time if ( Setting::get('torrent.approval_status_none_visible') == 'no' && $torrent->sp_state != Torrent::PROMOTION_NORMAL && $torrent->promotion_until ) { $hasBeenDownloaded = Snatch::query()->where('torrentid', $torrent->id)->exists(); $log = "Torrent: {$torrent->id} is in promotion, hasBeenDownloaded: $hasBeenDownloaded"; if (!$hasBeenDownloaded) { $diffInSeconds = $torrent->promotion_until->diffInSeconds($torrent->added, true); $log .= ", addSeconds: $diffInSeconds"; $torrentUpdate['promotion_until'] = $torrent->promotion_until->addSeconds($diffInSeconds); } do_log($log); } } if ($torrent->approval_status == Torrent::APPROVAL_STATUS_DENY) { $notifyUser = true; } } elseif ($params['approval_status'] == Torrent::APPROVAL_STATUS_DENY) { $torrentUpdate['banned'] = 'yes'; $torrentUpdate['visible'] = 'no'; //Deny, record and notify all the time $torrentOperationLog['action_type'] = TorrentOperationLog::ACTION_TYPE_APPROVAL_DENY; $notifyUser = true; } elseif ($params['approval_status'] == Torrent::APPROVAL_STATUS_NONE) { $torrentUpdate['banned'] = 'no'; $torrentUpdate['visible'] = 'yes'; if ($torrent->approval_status != $params['approval_status']) { $torrentOperationLog['action_type'] = TorrentOperationLog::ACTION_TYPE_APPROVAL_NONE; } if ($torrent->approval_status == Torrent::APPROVAL_STATUS_DENY) { $notifyUser = true; } } else { throw new \InvalidArgumentException("Invalid approval_status: " . $params['approval_status']); } if (isset($torrentOperationLog['action_type'])) { $torrentOperationLog['uid'] = $user->id; $torrentOperationLog['torrent_id'] = $torrent->id; $torrentOperationLog['comment'] = $params['comment'] ?? ''; } NexusDB::transaction(function () use ($torrent, $torrentOperationLog, $torrentUpdate, $notifyUser) { $log = "torrent: " . $torrent->id; if (!empty($torrentUpdate)) { $log .= ", [UPDATE_TORRENT]: " . nexus_json_encode($torrentUpdate); $torrent->update($torrentUpdate); } if (!empty($torrentOperationLog)) { $log .= ", [ADD_TORRENT_OPERATION_LOG]: " . nexus_json_encode($torrentOperationLog); TorrentOperationLog::add($torrentOperationLog, $notifyUser); } do_log($log); }); return $params; } public function renderApprovalStatus($approvalStatus, $show = null): string { if ($show === null) { $show = $this->shouldShowApprovalStatusIcon($approvalStatus); } if ($show) { return sprintf( '%s', nexus_trans("torrent.approval.status_text.$approvalStatus"), \App\Models\Torrent::$approvalStatus[$approvalStatus]['icon'] ); } return ''; } public function shouldShowApprovalStatusIcon($approvalStatus): bool { if (get_setting('torrent.approval_status_icon_enabled') == 'yes') { //启用审核状态图标,肯定显示 return true; } if ( $approvalStatus != \App\Models\Torrent::APPROVAL_STATUS_ALLOW && get_setting('torrent.approval_status_none_visible') == 'no' ) { //不启用审核状态图标,尽量不显示。在种子不是审核通过状态,而审核不通过又不能被用户看到时,显示 return true; } return false; } public function syncTags($id, array $tagIdArr = [], $remove = true) { user_can('torrentmanage', true); $idArr = Arr::wrap($id); return NexusDB::transaction(function () use ($idArr, $tagIdArr, $remove) { $sql = "insert into torrent_tags (torrent_id, tag_id, created_at, updated_at) values "; $time = now()->toDateTimeString(); $values = []; foreach ($idArr as $torrentId) { foreach ($tagIdArr as $tagId) { $values[] = sprintf("(%s, %s, '%s', '%s')", $torrentId, $tagId, $time, $time); } } $sql .= implode(', ', $values) . " on duplicate key update updated_at = values(updated_at)"; if ($remove) { TorrentTag::query()->whereIn('torrent_id', $idArr)->delete(); } if (!empty($values)) { DB::insert($sql); } return count($values); }); } public function setPosState($id, $posState, $posStateUntil = null): int { user_can('torrentsticky', true); if ($posState == Torrent::POS_STATE_STICKY_NONE) { $posStateUntil = null; } if ($posStateUntil && Carbon::parse($posStateUntil)->lte(now())) { $posState = Torrent::POS_STATE_STICKY_NONE; $posStateUntil = null; } $update = [ 'pos_state' => $posState, 'pos_state_until' => $posStateUntil, ]; $idArr = Arr::wrap($id); return Torrent::query()->whereIn('id', $idArr)->update($update); } public function setPickType($id, $pickType): int { user_can('torrentmanage', true); if (!isset(Torrent::$pickTypes[$pickType])) { throw new \InvalidArgumentException("Invalid pickType: $pickType"); } $update = [ 'picktype' => $pickType, 'picktime' => now(), ]; $idArr = Arr::wrap($id); return Torrent::query()->whereIn('id', $idArr)->update($update); } public function setHr($id, $hrStatus): int { user_can('torrentmanage', true); if (!isset(Torrent::$hrStatus[$hrStatus])) { throw new \InvalidArgumentException("Invalid hrStatus: $hrStatus"); } $update = [ 'hr' => $hrStatus, ]; $idArr = Arr::wrap($id); do_log(sprintf("set torrent: %s hr: %s", implode(",", $idArr), $hrStatus)); return Torrent::query()->whereIn('id', $idArr)->update($update); } public function setSpState($id, $spState, $promotionTimeType, $promotionUntil = null): int { user_can('torrentonpromotion', true); if (!isset(Torrent::$promotionTypes[$spState])) { throw new \InvalidArgumentException("Invalid spState: $spState"); } if (!isset(Torrent::$promotionTimeTypes[$promotionTimeType])) { throw new \InvalidArgumentException("Invalid promotionTimeType: $promotionTimeType"); } if (in_array($promotionTimeType, [Torrent::PROMOTION_TIME_TYPE_GLOBAL, Torrent::PROMOTION_TIME_TYPE_PERMANENT])) { $promotionUntil = null; } elseif (!$promotionUntil || Carbon::parse($promotionUntil)->lte(now())) { throw new \InvalidArgumentException("Invalid promotionUntil: $promotionUntil"); } $update = [ 'sp_state' => $spState, 'promotion_time_type' => $promotionTimeType, 'promotion_until' => $promotionUntil, ]; $idArr = Arr::wrap($id); return Torrent::query()->whereIn('id', $idArr)->update($update); } public function buildUploadFieldInput($name, $value, $noteText, $btnText): string { $btn = $note = ''; if ($btnText) { $btn = '
'; } if ($noteText) { $note = ''.$noteText.''; } $input = <<
$note
$btn HTML; return $input; } public function getPaidIcon(array $torrentInfo, $size = 16, $verticalAlign = 'sub') { if (!isset($torrentInfo['price']) || $torrentInfo['price'] <= 0) { return ''; } return sprintf('', nexus_trans('torrent.paid_torrent'), $verticalAlign, $size, $size); } public function loadBoughtUser($torrentId): int { $size = 500; $page = 1; $key = $this->getBoughtUserCacheKey($torrentId); $redis = NexusDB::redis(); $total = 0; while (true) { $list = TorrentBuyLog::query()->where("torrent_id", $torrentId)->forPage($page, $size)->get(['torrent_id', 'uid']); if ($list->isEmpty()) { break; } foreach ($list as $item) { $redis->hSet($key, $item->uid, 1); $total += 1; do_log(sprintf("hset %s %s 1", $key, $item->uid)); } $page++; } do_log("torrent_purchasers:$torrentId LOAD DONE, total: $total"); if ($total > 0) { $redis->expire($key, 86400*30); } return $total; } /** * 购买成功缓存,保存为 hash,一个种子一个 hash,永久有效 * @param $uid * @param $torrentId * @return void * @throws \RedisException */ public function addBuySuccessCache($uid, $torrentId): void { NexusDB::redis()->hSet($this->getBoughtUserCacheKey($torrentId), $uid, 1); } public function hasBuySuccessCache($uid, $torrentId): bool { return NexusDB::redis()->hGet($this->getBoughtUserCacheKey($torrentId), $uid) == 1; } /** * 获取购买种子的缓存状态 * * @param $uid * @param $torrentId * @return int */ public function getBuyStatus($uid, $torrentId): int { //查询是否已经购买 if ($this->hasBuySuccessCache($uid, $torrentId)) { return self::BUY_STATUS_SUCCESS; } //是否购买失败过 $buyFailCount = $this->getBuyFailCache($uid, $torrentId); if ($buyFailCount > 0) { //根据失败次数,禁用下载权限并做提示等 return $buyFailCount; } //不是成功或失败,直接返回未知 return self::BUY_STATUS_UNKNOWN; } /** * 添加购买失败缓存, 结果累加 * @param $uid * @param $torrentId * @return void * @throws \RedisException */ public function addBuyFailCache($uid, $torrentId): void { $key = $this->getBuyFailCacheKey($uid, $torrentId); $result = NexusDB::redis()->incr($key); if ($result == 1) { NexusDB::redis()->expire($key, 3600); } } /** * 获取失败缓存 ,结果是失败的次数 * * @param $uid * @param $torrentId * @return int * @throws \RedisException */ public function getBuyFailCache($uid, $torrentId): int { return intval(NexusDB::redis()->get($this->getBuyFailCacheKey($uid, $torrentId))); } /** * 购买成功缓存 key * @param $torrentId * @return string */ public function getBoughtUserCacheKey($torrentId): string { return sprintf("%s:%s", self::BOUGHT_USER_CACHE_KEY_PREFIX, $torrentId); } /** * 购买失败缓存 key * @param int $userId * @param int $torrentId * @return string */ public function getBuyFailCacheKey(int $userId, int $torrentId): string { return sprintf("%s:%s:%s", self::BUY_FAIL_CACHE_KEY_PREFIX, $userId, $torrentId); } public function addPiecesHashCache(int $torrentId, string $piecesHash): bool|int|\Redis { $value = $this->buildPiecesHashCacheValue($torrentId, $piecesHash); return NexusDB::redis()->hSet(self::PIECES_HASH_CACHE_KEY, $piecesHash, $value); } private function buildPiecesHashCacheValue(int $torrentId, string $piecesHash): bool|string { return json_encode(['torrent_id' => $torrentId, 'pieces_hash' => $piecesHash]); } public function delPiecesHashCache(string $piecesHash): bool|int|\Redis { return NexusDB::redis()->hDel(self::PIECES_HASH_CACHE_KEY, $piecesHash); } public function getPiecesHashCache($piecesHash): array { if (!is_array($piecesHash)) { $piecesHash = [$piecesHash]; } $maxCount = 100; if (count($piecesHash) > $maxCount) { throw new \InvalidArgumentException("too many pieces hash, must less then $maxCount"); } $pipe = NexusDB::redis()->multi(\Redis::PIPELINE); foreach ($piecesHash as $hash) { $pipe->hGet(self::PIECES_HASH_CACHE_KEY, $hash); } $results = $pipe->exec(); $logPrefix = sprintf("piecesHashCount: %s, resultCount: %s", count($piecesHash), count($results)); $out = []; foreach ($results as $item) { $arr = json_decode($item, true); if (is_array($arr) && isset($arr['torrent_id'], $arr['pieces_hash'])) { $out[$arr['pieces_hash']] = $arr['torrent_id']; } else { do_log(sprintf("%s, invalid item: %s(%s)", $logPrefix, var_export($item, true), gettype($item))); } } return $out; } public function loadPiecesHashCache($id = 0): array { $page = 1; $size = 1000; $query = Torrent::query(); if ($id) { $query = $query->whereIn("id", Arr::wrap($id)); } $total = $success = 0; $torrentDir = sprintf( "%s/%s/", rtrim(ROOT_PATH, '/'), rtrim(get_setting("main.torrent_dir"), '/') ); while (true) { $list = (clone $query)->forPage($page, $size)->get(['id', 'pieces_hash']); if ($list->isEmpty()) { do_log("page: $page, size: $size, no more data..."); break; } $pipe = NexusDB::redis()->multi(\Redis::PIPELINE); $piecesHashCaseWhen = $updateIdArr = []; $currentCount = 0; foreach ($list as $item) { $total++; try { $piecesHash = $item->pieces_hash; if (!$piecesHash) { $torrentFile = $torrentDir . $item->id . ".torrent"; $loadResult = Bencode::load($torrentFile); $piecesHash = sha1($loadResult['info']['pieces']); $piecesHashCaseWhen[] = sprintf("when %s then '%s'", $item->id, $piecesHash); $updateIdArr[] = $item->id; do_log(sprintf("torrent: %s no pieces hash, load from torrent file: %s, pieces hash: %s", $item->id, $torrentFile, $piecesHash)); } $pipe->hSet(self::PIECES_HASH_CACHE_KEY, $piecesHash, $this->buildPiecesHashCacheValue($item->id, $piecesHash)); $success++; $currentCount++; } catch (\Exception $exception) { do_log(sprintf("load pieces hash of torrent: %s error: %s", $item->id, $exception->getMessage()), 'error'); } } $pipe->exec(); if (!empty($piecesHashCaseWhen)) { $sql = sprintf( "update torrents set pieces_hash = case id %s end where id in (%s)", implode(' ', $piecesHashCaseWhen), implode(", ", $updateIdArr) ); NexusDB::statement($sql); } do_log("success load page: $page, size: $size, count: $currentCount"); $page++; } do_log("[DONE], total: $total, success: $success"); return compact('total', 'success'); } public function fetchImdb(int $torrentId): void { $torrent = Torrent::query()->findOrFail($torrentId, ["id", "url", "cache_stamp"]); $imdb_id = parse_imdb_id($torrent->url); $log = sprintf("fetchImdb torrentId: %s", $torrentId); if (!$imdb_id) { do_log("$log, no imdb_id"); return; } $thenumbers = $imdb_id; $imdb = new Imdb(); $torrent->cache_stamp = time(); $torrent->save(); $imdb->purgeSingle($imdb_id); try { $imdb->updateCache($imdb_id); NexusDB::cache_del('imdb_id_'.$thenumbers.'_movie_name'); NexusDB::cache_del('imdb_id_'.$thenumbers.'_large', true); NexusDB::cache_del('imdb_id_'.$thenumbers.'_median', true); NexusDB::cache_del('imdb_id_'.$thenumbers.'_minor', true); do_log("$log, done"); } catch (\Exception $e) { $log .= ", error: " . $e->getMessage() . ", trace: " . $e->getTraceAsString(); do_log($log, 'error'); } } }