diff --git a/app/Enums/ModelEventEnum.php b/app/Enums/ModelEventEnum.php index 905b6d2d..d164aa77 100644 --- a/app/Enums/ModelEventEnum.php +++ b/app/Enums/ModelEventEnum.php @@ -2,6 +2,15 @@ namespace App\Enums; +use App\Events\AgentAllowCreated; +use App\Events\AgentAllowDeleted; +use App\Events\AgentAllowUpdated; +use App\Events\AgentDenyCreated; +use App\Events\AgentDenyDeleted; +use App\Events\AgentDenyUpdated; +use App\Events\HitAndRunCreated; +use App\Events\HitAndRunDeleted; +use App\Events\HitAndRunUpdated; use App\Events\MessageCreated; use App\Events\NewsCreated; use App\Events\SnatchedUpdated; @@ -13,6 +22,9 @@ use App\Events\UserDeleted; use App\Events\UserDisabled; use App\Events\UserEnabled; use App\Events\UserUpdated; +use App\Models\AgentAllow; +use App\Models\AgentDeny; +use App\Models\HitAndRun; use App\Models\Message; use App\Models\News; use App\Models\Snatch; @@ -32,6 +44,18 @@ final class ModelEventEnum { const NEWS_CREATED = 'news_created'; + const HIT_AND_RUN_CREATED = 'hit_and_run_created'; + const HIT_AND_RUN_UPDATED = 'hit_and_run_updated'; + const HIT_AND_RUN_DELETED = 'hit_and_run_deleted'; + + const AGENT_ALLOW_CREATED = 'agent_allow_created'; + const AGENT_ALLOW_UPDATED = 'agent_allow_updated'; + const AGENT_ALLOW_DELETED = 'agent_allow_deleted'; + + const AGENT_DENY_CREATED = 'agent_deny_created'; + const AGENT_DENY_UPDATED = 'agent_deny_updated'; + const AGENT_DENY_DELETED = 'agent_deny_deleted'; + const SNATCHED_UPDATED = 'snatched_updated'; const MESSAGE_CREATED = 'message_created'; @@ -51,5 +75,17 @@ final class ModelEventEnum { self::SNATCHED_UPDATED => ['event' => SnatchedUpdated::class, 'model' => Snatch::class], self::MESSAGE_CREATED => ['event' => MessageCreated::class, 'model' => Message::class], + + self::HIT_AND_RUN_CREATED => ['event' => HitAndRunCreated::class, 'model' => HitAndRun::class], + self::HIT_AND_RUN_UPDATED => ['event' => HitAndRunUpdated::class, 'model' => HitAndRun::class], + self::HIT_AND_RUN_DELETED => ['event' => HitAndRunDeleted::class, 'model' => HitAndRun::class], + + self::AGENT_ALLOW_CREATED => ['event' => AgentAllowCreated::class, 'model' => AgentAllow::class], + self::AGENT_ALLOW_UPDATED => ['event' => AgentAllowUpdated::class, 'model' => AgentAllow::class], + self::AGENT_ALLOW_DELETED => ['event' => AgentAllowDeleted::class, 'model' => AgentAllow::class], + + self::AGENT_DENY_CREATED => ['event' => AgentDenyCreated::class, 'model' => AgentDeny::class], + self::AGENT_DENY_UPDATED => ['event' => AgentDenyUpdated::class, 'model' => AgentDeny::class], + self::AGENT_DENY_DELETED => ['event' => AgentDenyDeleted::class, 'model' => AgentDeny::class], ]; } diff --git a/app/Events/AgentAllowCreated.php b/app/Events/AgentAllowCreated.php new file mode 100644 index 00000000..6197aa2b --- /dev/null +++ b/app/Events/AgentAllowCreated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/AgentAllowDeleted.php b/app/Events/AgentAllowDeleted.php new file mode 100644 index 00000000..aadebb66 --- /dev/null +++ b/app/Events/AgentAllowDeleted.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/AgentAllowUpdated.php b/app/Events/AgentAllowUpdated.php new file mode 100644 index 00000000..4a7d5cca --- /dev/null +++ b/app/Events/AgentAllowUpdated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/AgentDenyCreated.php b/app/Events/AgentDenyCreated.php new file mode 100644 index 00000000..f9984981 --- /dev/null +++ b/app/Events/AgentDenyCreated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/AgentDenyDeleted.php b/app/Events/AgentDenyDeleted.php new file mode 100644 index 00000000..83db2eeb --- /dev/null +++ b/app/Events/AgentDenyDeleted.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/AgentDenyUpdated.php b/app/Events/AgentDenyUpdated.php new file mode 100644 index 00000000..d4efb0c6 --- /dev/null +++ b/app/Events/AgentDenyUpdated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/HitAndRunCreated.php b/app/Events/HitAndRunCreated.php new file mode 100644 index 00000000..a822f1ad --- /dev/null +++ b/app/Events/HitAndRunCreated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/HitAndRunDeleted.php b/app/Events/HitAndRunDeleted.php new file mode 100644 index 00000000..1c892a35 --- /dev/null +++ b/app/Events/HitAndRunDeleted.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Events/HitAndRunUpdated.php b/app/Events/HitAndRunUpdated.php new file mode 100644 index 00000000..1a4b435b --- /dev/null +++ b/app/Events/HitAndRunUpdated.php @@ -0,0 +1,39 @@ +model = $model; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Filament/Resources/User/HitAndRunResource.php b/app/Filament/Resources/User/HitAndRunResource.php index efc59b27..ca9b09d0 100644 --- a/app/Filament/Resources/User/HitAndRunResource.php +++ b/app/Filament/Resources/User/HitAndRunResource.php @@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\HtmlString; use Filament\Infolists\Components; use Filament\Infolists; +use Nette\Utils\Html; class HitAndRunResource extends Resource { @@ -146,7 +147,7 @@ class HitAndRunResource extends Resource ->label(__("label.inspect_time_left")) , Infolists\Components\TextEntry::make('comment') - ->formatStateUsing(fn ($record) => nl2br($record->comment)) + ->formatStateUsing(fn ($record) => new HtmlString(nl2br($record->comment))) ->label(__("label.comment")) , Infolists\Components\TextEntry::make('created_at') diff --git a/app/Listeners/ClearTorrentCache.php b/app/Listeners/ClearTorrentCache.php new file mode 100644 index 00000000..acb33dc1 --- /dev/null +++ b/app/Listeners/ClearTorrentCache.php @@ -0,0 +1,33 @@ +model?->id ?? 0; + if ($torrentId > 0) { + $infoHash = Torrent::query()->where('id', $torrentId)->value('info_hash'); + clear_torrent_cache($infoHash); + do_log("success clear torrent: $torrentId cache with info_hash: " . rawurlencode($infoHash)); + } else { + do_log("no torrent id", 'error'); + } + } +} diff --git a/app/Models/AgentAllow.php b/app/Models/AgentAllow.php index c87ac0fd..1d887e22 100644 --- a/app/Models/AgentAllow.php +++ b/app/Models/AgentAllow.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Enums\ModelEventEnum; + class AgentAllow extends NexusModel { protected $table = 'agent_allowed_family'; @@ -20,6 +22,19 @@ class AgentAllow extends NexusModel self::MATCH_TYPE_HEX => 'hex', ]; + protected static function booted() + { + static::created(function ($model) { + fire_event(ModelEventEnum::AGENT_ALLOW_CREATED, $model); + }); + static::updated(function ($model) { + fire_event(ModelEventEnum::AGENT_ALLOW_UPDATED, $model); + }); + static::deleted(function ($model) { + fire_event(ModelEventEnum::AGENT_ALLOW_DELETED, $model); + }); + } + public function denies(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(AgentDeny::class, 'family_id'); diff --git a/app/Models/AgentDeny.php b/app/Models/AgentDeny.php index 2d5ca5b3..66b987ac 100644 --- a/app/Models/AgentDeny.php +++ b/app/Models/AgentDeny.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Enums\ModelEventEnum; + class AgentDeny extends NexusModel { protected $table = 'agent_allowed_exception'; @@ -10,6 +12,19 @@ class AgentDeny extends NexusModel 'family_id', 'name', 'peer_id', 'agent', 'comment' ]; + protected static function booted() + { + static::created(function ($model) { + fire_event(ModelEventEnum::AGENT_DENY_CREATED, $model); + }); + static::updated(function ($model) { + fire_event(ModelEventEnum::AGENT_DENY_UPDATED, $model); + }); + static::deleted(function ($model) { + fire_event(ModelEventEnum::AGENT_DENY_DELETED, $model); + }); + } + public function family(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(AgentAllow::class, 'family_id'); diff --git a/app/Models/HitAndRun.php b/app/Models/HitAndRun.php index bd2ea9ff..12667b0a 100644 --- a/app/Models/HitAndRun.php +++ b/app/Models/HitAndRun.php @@ -2,9 +2,10 @@ namespace App\Models; +use App\Enums\ModelEventEnum; use Carbon\Carbon; -use Carbon\Exceptions\InvalidArgumentException; use Illuminate\Database\Eloquent\Casts\Attribute; +use Nexus\Database\NexusDB; class HitAndRun extends NexusModel { @@ -43,6 +44,31 @@ class HitAndRun extends NexusModel const MINIMUM_IGNORE_USER_CLASS = User::CLASS_VIP; + protected static function booted() + { + static::saved(function ($model) { + self::clearCache($model); + }); + static::deleted(function ($model) { + self::clearCache($model, ModelEventEnum::HIT_AND_RUN_DELETED); + }); + } + + public static function getCacheKey(int $userId, int $torrentId): string + { + return sprintf("hit_and_run:user:%d:torrent:%d", $userId, $torrentId); + } + + public static function clearCache(HitAndRun $hitAndRun, string $event = ModelEventEnum::HIT_AND_RUN_UPDATED): void + { + NexusDB::cache_del(self::getCacheKey($hitAndRun->uid, $hitAndRun->torrent_id)); + fire_event($event, $hitAndRun); + do_log(sprintf( + "userId: %s, torrentId: %s hit and run cache cleared, and trigger event: %s", + $hitAndRun->uid, $hitAndRun->torrent_id, $event + )); + } + protected function seedTimeRequired(): Attribute { return new Attribute( diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d9fa9783..ff3b5d08 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -8,6 +8,7 @@ use App\Events\TorrentDeleted; use App\Events\TorrentUpdated; use App\Events\UserDeleted; use App\Events\UserDisabled; +use App\Listeners\ClearTorrentCache; use App\Listeners\DeductUserBonusWhenTorrentDeleted; use App\Listeners\FetchTorrentImdb; use App\Listeners\FetchTorrentPTGen; @@ -48,6 +49,7 @@ class EventServiceProvider extends ServiceProvider SyncTorrentToElasticsearch::class, SyncTorrentToMeilisearch::class, SendEmailNotificationWhenTorrentCreated::class, + ClearTorrentCache::class, ], TorrentDeleted::class => [ DeductUserBonusWhenTorrentDeleted::class, diff --git a/app/Repositories/HitAndRunRepository.php b/app/Repositories/HitAndRunRepository.php index cae72169..2fa60c0f 100644 --- a/app/Repositories/HitAndRunRepository.php +++ b/app/Repositories/HitAndRunRepository.php @@ -1,6 +1,7 @@ findOrFail($id); $result = $model->delete(); + HitAndRun::clearCache($model, ModelEventEnum::HIT_AND_RUN_DELETED); return $result; } public function bulkDelete(array $params, User $user) { - $result = $this->getBulkQuery($params)->delete(); + $baseQuery = $this->getBulkQuery($params); + $list = $baseQuery->clone()->get(); + if ($list->isEmpty()) { + return 0; + } + $result = $baseQuery->delete(); do_log(sprintf( 'user: %s bulk delete by filter: %s, result: %s', $user->id, json_encode($params), json_encode($result) ), 'alert'); + if ($result) { + foreach ($list as $record) { + HitAndRun::clearCache($record, ModelEventEnum::HIT_AND_RUN_DELETED); + } + } return $result; } @@ -204,7 +216,8 @@ class HitAndRunRepository extends BaseRepository //check leech time if (isset($setting['leech_time_minimum']) && $setting['leech_time_minimum'] > 0) { - $targetLeechTime = $row->snatch->leechtime; +// $targetLeechTime = $row->snatch->leechtime; + $targetLeechTime = $row->leech_time_no_seeder;//使用自身记录的值 $requireLeechTime = bcmul($setting['leech_time_minimum'], 3600); do_log("$currentLog, targetLeechTime: $targetLeechTime, requireLeechTime: $requireLeechTime"); if ($targetLeechTime >= $requireLeechTime) { @@ -331,6 +344,7 @@ class HitAndRunRepository extends BaseRepository } else { do_log($hitAndRun->toJson() . ", [$logPrefix], user do not accept hr_reached notification", 'notice'); } + HitAndRun::clearCache($hitAndRun); return true; } @@ -369,7 +383,7 @@ class HitAndRunRepository extends BaseRepository ], $hitAndRun->user->locale), ]; Message::query()->insert($message); - + HitAndRun::clearCache($hitAndRun); return true; } @@ -423,6 +437,7 @@ class HitAndRunRepository extends BaseRepository 'reason' => $comment ]; UserBanLog::query()->insert($userBanLog); + fire_event(ModelEventEnum::USER_UPDATED, $user); do_log("Disable user: " . nexus_json_encode($userBanLog)); } } @@ -499,16 +514,25 @@ class HitAndRunRepository extends BaseRepository public function bulkPardon(array $params, User $user): int { - $query = $this->getBulkQuery($params)->whereIn('status', $this->getCanPardonStatus()); + $baseQuery = $this->getBulkQuery($params)->whereIn('status', $this->getCanPardonStatus()); + $list = $baseQuery->clone()->get(); + if ($list->isEmpty()) { + return 0; + } $update = [ 'status' => HitAndRun::STATUS_PARDONED, 'comment' => $this->getCommentUpdateRaw(addslashes('Pardon by ' . $user->username)), ]; - $affected = $query->update($update); + $affected = $baseQuery->update($update); do_log(sprintf( 'user: %s bulk pardon by filter: %s, affected: %s', $user->id, json_encode($params), $affected ), 'alert'); + if ($affected) { + foreach ($list as $item) { + HitAndRun::clearCache($item); + } + } return $affected; } diff --git a/database/migrations/2025_07_19_023351_add_leech_time_no_seeder_to_hit_and_runs_table.php b/database/migrations/2025_07_19_023351_add_leech_time_no_seeder_to_hit_and_runs_table.php new file mode 100644 index 00000000..08040b47 --- /dev/null +++ b/database/migrations/2025_07_19_023351_add_leech_time_no_seeder_to_hit_and_runs_table.php @@ -0,0 +1,28 @@ +bigInteger('leech_time_no_seeder')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('hit_and_runs', function (Blueprint $table) { + $table->dropColumn('leech_time_no_seeder'); + }); + } +}; diff --git a/include/constants.php b/include/constants.php index 03398c07..a371a315 100644 --- a/include/constants.php +++ b/include/constants.php @@ -1,6 +1,6 @@ where("torrent_id", $torrentId)->delete(); -// sql_query("delete from torrent_tags where torrent_id = $torrentId"); + $delQuery = \App\Models\TorrentTag::query()->where("torrent_id", $torrentId); + if (!$canSetSpecialTag) { + $delQuery->whereNotIn("tag_id", $specialTags); + } + $delQuery->delete(); } if (empty($tagIdArr)) { return; @@ -5956,7 +5959,6 @@ function insert_torrent_tags($torrentId, $tagIdArr, $sync = false) $insertTagsSql .= implode(', ', $values); do_log("[INSERT_TAGS], torrent: $torrentId with tags: " . nexus_json_encode($tagIdArr)); \Nexus\Database\NexusDB::statement($insertTagsSql); -// sql_query($insertTagsSql); } function get_smile($num) diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 41e5143d..cfa90114 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -1208,6 +1208,19 @@ function clear_agent_allow_deny_cache() \Nexus\Database\NexusDB::cache_del($denyCacheKey . $suffix); } } + +/** + * @see announce.php + * @param $infoHash + * @return void + */ +function clear_torrent_cache($infoHash) +{ + do_log("clear_torrent_cache"); + \Nexus\Database\NexusDB::cache_del('torrent_hash_'.$infoHash.'_content'); + \Nexus\Database\NexusDB::cache_del("torrent_not_exists:$infoHash"); +} + function user_can($permission, $fail = false, $uid = 0): bool { $log = "permission: $permission, fail: $fail, user: $uid"; @@ -1338,6 +1351,7 @@ function is_danger_url($url): bool return false; } +//here must retrieve the real time info, no cache!!! function get_snatch_info($torrentId, $userId) { return mysql_fetch_assoc(sql_query(sprintf('select * from snatched where torrentid = %s and userid = %s order by id desc limit 1', $torrentId, $userId))); diff --git a/public/announce.php b/public/announce.php index 0a364ca8..47299313 100644 --- a/public/announce.php +++ b/public/announce.php @@ -562,9 +562,11 @@ if (($left > 0 || $event == "completed") && $az['class'] < \App\Models\HitAndRun $hrMode = \App\Models\HitAndRun::getConfig('mode', $torrent['mode']); $hrLog = sprintf("[HR_LOG] user: %d, torrent: %d, hrMode: %s", $userid, $torrentid, $hrMode); if ($hrMode == \App\Models\HitAndRun::MODE_GLOBAL || ($hrMode == \App\Models\HitAndRun::MODE_MANUAL && $torrent['hr'] == \App\Models\Torrent::HR_YES)) { - $hrCacheKey = sprintf("hit_and_run:%d:%d", $userid, $torrentid); - $hrExists = \Nexus\Database\NexusDB::remember($hrCacheKey, mt_rand(86400*365*5, 86400*365*10), function () use ($torrentid, $userid) { - return \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->exists() ? 1 : 0; + //change key to expire cache, so ttl don't set too long + $hrCacheKey = \App\Models\HitAndRun::getCacheKey( $userid, $torrentid); + $hrExists = \Nexus\Database\NexusDB::remember($hrCacheKey, mt_rand(86400, 86400*3), function () use ($torrentid, $userid) { + $record = \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->first(); + return $record ? $record->toJson() : null; }); $hrLog .= ", hrExists: $hrExists"; if (!$hrExists) { @@ -589,12 +591,30 @@ if (($left > 0 || $event == "completed") && $az['class'] < \App\Models\HitAndRun do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} >= required: $requiredDownloaded, [INSERT_H&R], sql: $sql, affectedRows: $affectedRows, hitAndRunId: $hitAndRunId"); if ($hitAndRunId > 0) { sql_query("update snatched set hit_and_run_id = $hitAndRunId where id = {$snatchInfo['id']}"); + $hitAndRunRecord = \App\Models\HitAndRun::query()->where("uid", $userid)->where("torrent_id", $torrentid)->first(); + fire_event(\App\Enums\ModelEventEnum::HIT_AND_RUN_CREATED, $hitAndRunRecord); } } else { do_log("$hrLog, total downloaded: {$snatchInfo['downloaded']} < required: $requiredDownloaded", "debug"); } } else { - do_log("$hrLog, already exists", "debug"); + $hrLog .= ", already exists"; + if (isset($self) && $torrent['seeders'] <= 0) { + $hrLeechTimeMin = \App\Models\HitAndRun::getConfig('leech_time_minimum', $torrent['mode']); + if ($hrLeechTimeMin > 0) { + $hrLog .= ", enable hrLeechTimeMin: $hrLeechTimeMin"; + $hrInfo = json_decode($hrExists, true); + if ($hrInfo['status'] == \App\Models\HitAndRun::STATUS_INSPECTING) { + sql_query("update hit_and_runs set leech_time_no_seeder = leech_time_no_seeder + {$self['announcetime']} where id = {$hrInfo['id']} limit 1"); + } else { + do_log("$hrLog, hr status != STATUS_INSPECTING", "debug"); + } + } else { + do_log("$hrLog, not enable hrLeechTimeMin", "debug"); + } + } else { + do_log("$hrLog, no self or seeders({$torrent['seeders']}) > 0", "debug"); + } } } else { do_log("$hrLog, not match", "debug"); diff --git a/public/invite.php b/public/invite.php index 6d2a2681..0491468b 100644 --- a/public/invite.php +++ b/public/invite.php @@ -164,7 +164,7 @@ JS; } else { list($pagertop, $pagerbottom, $limit) = pager($pageSize, $number, "?id=$id&menu=$menuSelected&"); $haremAdditionFactor = (float)get_setting('bonus.harem_addition'); - $ret = sql_query("SELECT id, username, email, uploaded, downloaded, status, warned, enabled, donor, email,seeding_torrent_count, seeding_torrent_size, last_announce_at FROM users WHERE $whereStr $limit") or sqlerr(); + $ret = sql_query("SELECT id, username, email, uploaded, downloaded, status, warned, enabled, donor, email, seed_points_per_hour, seeding_torrent_count, seeding_torrent_size, last_announce_at FROM users WHERE $whereStr $limit") or sqlerr(); $num = mysql_num_rows($ret); print("