'datetime', 'promotion_until' => 'datetime', 'pos_state_until' => 'datetime', 'last_action' => 'datetime', ]; public static $commentFields = [ 'id', 'name', 'added', 'visible', 'banned', 'owner', 'sp_state', 'promotion_time_type', 'promotion_until', 'pos_state', 'hr', 'picktype', 'picktime', 'last_action', 'leechers', 'seeders', 'times_completed', 'views', 'size', 'cover', 'anonymous', 'approval_status', 'pos_state_until', 'category', 'source', 'medium', 'codec', 'standard', 'processing', 'team', 'audiocodec', 'price', ]; public static $basicRelations = [ 'basic_category', 'basic_audio_codec', 'basic_codec', 'basic_media', 'basic_source', 'basic_standard', 'basic_team', ]; const POS_STATE_STICKY_NONE = 'normal'; const POS_STATE_STICKY_FIRST = 'sticky'; /** * alphabet 'r' is after 'n' and before 's', so it will fit: order by pos_state desc, * first sticky, then r_sticky, then normal */ const POS_STATE_STICKY_SECOND = 'r_sticky'; public static $posStates = [ self::POS_STATE_STICKY_NONE => ['text' => 'Normal', 'icon_counts' => 0], self::POS_STATE_STICKY_SECOND => ['text' => 'Sticky second', 'icon_counts' => 1], self::POS_STATE_STICKY_FIRST => ['text' => 'Sticky first', 'icon_counts' => 2], ]; const HR_YES = 1; const HR_NO = 0; public static $hrStatus = [ self::HR_NO => ['text' => 'NO'], self::HR_YES => ['text' => 'YES'], ]; const PROMOTION_NORMAL = 1; const PROMOTION_FREE = 2; const PROMOTION_TWO_TIMES_UP = 3; const PROMOTION_FREE_TWO_TIMES_UP = 4; const PROMOTION_HALF_DOWN = 5; const PROMOTION_HALF_DOWN_TWO_TIMES_UP = 6; const PROMOTION_ONE_THIRD_DOWN = 7; public static array $promotionTypes = [ self::PROMOTION_NORMAL => [ 'text' => 'Normal', 'up_multiplier' => 1, 'down_multiplier' => 1, 'color' => '' ], self::PROMOTION_FREE => [ 'text' => 'Free', 'up_multiplier' => 1, 'down_multiplier' => 0, 'color' => 'linear-gradient(to right, rgba(0,52,206,0.5), rgba(0,52,206,1), rgba(0,52,206,0.5))' ], self::PROMOTION_TWO_TIMES_UP => [ 'text' => '2X', 'up_multiplier' => 2, 'down_multiplier' => 1, 'color' => 'linear-gradient(to right, rgba(0,153,0,0.5), rgba(0,153,0,1), rgba(0,153,0,0.5))' ], self::PROMOTION_FREE_TWO_TIMES_UP => [ 'text' => '2X Free', 'up_multiplier' => 2, 'down_multiplier' => 0, 'color' => 'linear-gradient(to right, rgba(0,153,0,1), rgba(0,52,206,1)' ], self::PROMOTION_HALF_DOWN => [ 'text' => '50%', 'up_multiplier' => 1, 'down_multiplier' => 0.5, 'color' => 'linear-gradient(to right, rgba(220,0,3,0.5), rgba(220,0,3,1), rgba(220,0,3,0.5))' ], self::PROMOTION_HALF_DOWN_TWO_TIMES_UP => [ 'text' => '2X 50%', 'up_multiplier' => 2, 'down_multiplier' => 0.5, 'color' => 'linear-gradient(to right, rgba(0,153,0,1), rgba(220,0,3,1)' ], self::PROMOTION_ONE_THIRD_DOWN => [ 'text' => '30%', 'up_multiplier' => 1, 'down_multiplier' => 0.3, 'color' => 'linear-gradient(to right, rgba(65,23,73,0.5), rgba(65,23,73,1), rgba(65,23,73,0.5))' ], ]; const PICK_NORMAL = 'normal'; const PICK_HOT = 'hot'; const PICK_CLASSIC = 'classic'; const PICK_RECOMMENDED = 'recommended'; public static array $pickTypes = [ self::PICK_NORMAL => ['text' => self::PICK_NORMAL, 'color' => ''], self::PICK_HOT => ['text' => self::PICK_HOT, 'color' => '#e78d0f'], self::PICK_CLASSIC => ['text' => self::PICK_CLASSIC, 'color' => '#77b300'], self::PICK_RECOMMENDED => ['text' => self::PICK_RECOMMENDED, 'color' => '#820084'], ]; const PROMOTION_TIME_TYPE_GLOBAL = 0; const PROMOTION_TIME_TYPE_PERMANENT = 1; const PROMOTION_TIME_TYPE_DEADLINE = 2; public static array $promotionTimeTypes = [ self::PROMOTION_TIME_TYPE_GLOBAL => ['text' => 'Global'], self::PROMOTION_TIME_TYPE_PERMANENT => ['text' => 'Permanent'], self::PROMOTION_TIME_TYPE_DEADLINE => ['text' => 'Until'], ]; const BONUS_REWARD_VALUES = [50, 100, 200, 500, 1000]; const APPROVAL_STATUS_NONE = 0; const APPROVAL_STATUS_ALLOW = 1; const APPROVAL_STATUS_DENY = 2; public static array $approvalStatus = [ self::APPROVAL_STATUS_NONE => [ 'text' => 'None', 'badge_color' => 'primary', 'icon' => '', ], self::APPROVAL_STATUS_ALLOW => [ 'text' => 'Allow', 'badge_color' => 'success', 'icon' => '', ], self::APPROVAL_STATUS_DENY => [ 'text' => 'Deny', 'badge_color' => 'danger', 'icon' => '', ], ]; const NFO_VIEW_STYLE_DOS = 'magic'; const NFO_VIEW_STYLE_WINDOWS = 'latin-1'; public static array $nfoViewStyles = [ self::NFO_VIEW_STYLE_DOS => ['text' => 'DOS-vy'], self::NFO_VIEW_STYLE_WINDOWS => ['text' => 'Windows-vy'], ]; public function getPickInfoAttribute() { $info = self::$pickTypes[$this->picktype] ?? null; if ($info) { $info['text'] = nexus_trans('torrent.pick_info.' . $this->picktype); return $info; } } public function getPromotionInfoAttribute() { return self::$promotionTypes[$this->sp_state_real] ?? null; } public function getSpStateRealTextAttribute() { $spStateReal = $this->sp_state_real; return self::$promotionTypes[$spStateReal]['text'] ?? ''; } public function getSpStateRealAttribute() { if ($this->getRawOriginal('sp_state') === null) { throw new \RuntimeException('no select sp_state field'); } $spState = $this->sp_state; $global = get_global_sp_state(); $log = sprintf('torrent: %s sp_state: %s, global sp state: %s', $this->id, $spState, $global); if ($global != self::PROMOTION_NORMAL) { $spState = $global; $log .= sprintf(", global != %s, set sp_state to global: %s", self::PROMOTION_NORMAL, $global); } if (!isset(self::$promotionTypes[$spState])) { $log .= ", but now sp_state: $spState, is invalid, reset to: " . self::PROMOTION_NORMAL; $spState = self::PROMOTION_NORMAL; } do_log($log, 'debug'); return $spState; } protected function getPosStateTextAttribute() { $text = nexus_trans('torrent.pos_state_' . $this->pos_state); if ($this->pos_state != Torrent::POS_STATE_STICKY_NONE) { if ($this->pos_state_until) { $append = format_datetime($this->pos_state_until); } else { $append = nexus_trans('label.permanent'); } $text .= "($append)"; } return $text; } protected function approvalStatusText(): Attribute { return new Attribute( get: fn($value, $attributes) => nexus_trans('torrent.approval.status_text.' . $attributes['approval_status']) ); } protected function spStateText(): Attribute { return new Attribute( get: fn($value, $attributes) => self::$promotionTypes[$this->sp_state]['text'] ?? '' ); } public static function getFieldsForList($appendTableName = false): array|bool { $fields = 'id, sp_state, promotion_time_type, promotion_until, banned, picktype, pos_state, category, source, medium, codec, standard, processing, team, audiocodec, leechers, seeders, name, small_descr, times_completed, size, added, comments,anonymous,owner,url,cache_stamp, hr, approval_status, cover, price'; $fields = preg_split('/[,\s]+/', $fields); if ($appendTableName) { foreach ($fields as &$value) { $value = "torrents." . $value; } } return $fields; } public static function listApprovalStatus($onlyKeyValue = false, $valueField = 'text'): array { $result = self::$approvalStatus; $keyValue = []; foreach ($result as $status => &$info) { $text = nexus_trans("torrent.approval.status_text.$status"); $info['text'] = $text; $keyValue[$status] = $info[$valueField]; } if ($onlyKeyValue) { return $keyValue; } return $result; } public static function listPromotionTypes($onlyKeyValue = false, $valueField = 'text'): array { $result = self::$promotionTypes; $keyValue = []; foreach ($result as $status => &$info) { $text = $info['text']; $info['text'] = $text; $keyValue[$status] = $info[$valueField]; } if ($onlyKeyValue) { return $keyValue; } return $result; } public static function listPromotionTimeTypes($onlyKeyValue = false, $valueField = 'text'): array { return self::listStaticProps(self::$promotionTimeTypes, 'torrent.promotion_time_types', $onlyKeyValue, $valueField); } public static function listPickInfo($onlyKeyValue = false, $valueField = 'text'): array { $result = self::$pickTypes; $keyValue = []; foreach ($result as $status => &$info) { $text = nexus_trans('torrent.pick_info.' . $status); $info['text'] = $text; $keyValue[$status] = $info[$valueField]; } if ($onlyKeyValue) { return $keyValue; } return $result; } public function getHrRealAttribute(): string { $searchBoxId = $this->basic_category->mode ?? 0; if ($searchBoxId == 0) { do_log(sprintf('[INVALID_CATEGORY], Torrent: %s, category: %s invalid', $this->id, $this->category), 'error'); return self::HR_NO; } $hrMode = HitAndRun::getConfig('mode', $searchBoxId); if ($hrMode == HitAndRun::MODE_GLOBAL) { return self::HR_YES; } if ($hrMode == HitAndRun::MODE_DISABLED) { return self::HR_NO; } return $this->getRawOriginal('hr'); } public function getHrTextAttribute() { return self::$hrStatus[$this->hr] ?? ''; } public function getTagsFormattedAttribute(): string { $html = []; foreach ($this->tags as $tag) { $html[] = sprintf( '%s', $tag->font_color, $tag->color, $tag->border_radius, $tag->font_size, $tag->padding, $tag->margin, $tag->name ); } return implode('', $html); } public static function listPosStates($onlyKeyValue = false, $valueField = 'text'): array { $result = self::$posStates; $keyValues = []; foreach ($result as $key => &$value) { $value['text'] = nexus_trans('torrent.pos_state_' . $key); $keyValues[$key] = $value[$valueField]; } if ($onlyKeyValue) { return $keyValues; } return $result; } public static function getFieldLabels(): array { $fields = [ 'comments', 'times_completed', 'peers_count', 'thank_users_count', 'numfiles', 'bookmark_yes', 'bookmark_no', 'reward_yes', 'reward_no', 'reward_logs', 'download', 'thanks_yes', 'thanks_no' ]; $result = []; foreach($fields as $field) { $result[$field] = nexus_trans("torrent.show.{$field}_label"); } return $result; } public function checkIsNormal(array $fields = ['visible', 'banned']) { if (in_array('visible', $fields) && $this->getAttribute('visible') != self::VISIBLE_YES) { throw new \InvalidArgumentException(sprintf('Torrent: %s is not visible.', $this->id)); } if (in_array('banned', $fields) && $this->getAttribute('banned') == self::BANNED_YES) { throw new \InvalidArgumentException(sprintf('Torrent: %s is banned.', $this->id)); } return true; } public function getSubCategoryLabel($field): string { return $this->basic_category->search_box->getTaxonomyLabel($field); } public function bookmarks(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(Bookmark::class, 'torrentid'); } public function user() { return $this->belongsTo(User::class, 'owner')->withDefault(User::getDefaultUserAttributes()); } public function thanks() { return $this->hasMany(Thank::class, 'torrentid'); } public function thank_users() { return $this->belongsToMany(User::class, 'thanks', 'torrentid', 'userid'); } /** * 同伴 * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function peers() { return $this->hasMany(Peer::class, 'torrent'); } /** * 完成情况 * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function snatches() { return $this->hasMany(Snatch::class, 'torrentid'); } public function upload_peers() { return $this->peers()->where('seeder', Peer::SEEDER_YES); } public function download_peers() { return $this->peers()->where('seeder', Peer::SEEDER_NO); } public function finish_peers() { return $this->peers()->where('finishedat', '>', 0); } public function files() { return $this->hasMany(File::class, 'torrent'); } public function basic_category() { return $this->belongsTo(Category::class, 'category'); } public function basic_source() { return $this->belongsTo(Source::class, 'source'); } public function basic_medium() { return $this->belongsTo(Media::class, 'medium'); } public function basic_codec() { return $this->belongsTo(Codec::class, 'codec'); } public function basic_standard() { return $this->belongsTo(Standard::class, 'standard'); } public function basic_processing() { return $this->belongsTo(Processing::class, 'processing'); } public function basic_team() { return $this->belongsTo(Team::class, 'team'); } public function basic_audiocodec() { return $this->belongsTo(AudioCodec::class, 'audiocodec'); } public function claim_users(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(User::class, 'claims', 'torrent_id'); } public function claims() { return $this->hasMany(Claim::class, 'torrent_id'); } public function scopeVisible($query, $visible = self::VISIBLE_YES) { $query->where('visible', $visible); } public function scopeNormal($query) { $query->where('visible', self::VISIBLE_YES)->where('banned', self::BANNED_NO); } public function torrent_tags(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(TorrentTag::class, 'torrent_id'); } public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany(Tag::class, 'torrent_tags', 'torrent_id', 'tag_id') ->orderByRaw(sprintf("field(`tags`.`id`,%s)", TagRepository::getOrderByFieldIdString())); } public function reward_logs(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(Reward::class, 'torrentid'); } public function operationLogs(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(TorrentOperationLog::class, 'torrent_id'); } public function extra(): \Illuminate\Database\Eloquent\Relations\HasOne { return $this->hasOne(TorrentExtra::class, 'torrent_id'); } }