diff --git a/app/Manager/Bonus.php b/app/Manager/Bonus.php index f4467baca..2f0df2be8 100644 --- a/app/Manager/Bonus.php +++ b/app/Manager/Bonus.php @@ -2,6 +2,8 @@ namespace Gazelle\Manager; +use Gazelle\Enum\UserStatus; + class Bonus extends \Gazelle\Base { final public const CACHE_OPEN_POOL = 'bonus_pool'; // also defined in \Gazelle\Bonus final protected const CACHE_ITEM = 'bonus_item'; @@ -45,11 +47,11 @@ class Bonus extends \Gazelle\Base { } public function flushPriceCache(): void { - $this->items = []; + unset($this->items); self::$cache->delete_value(self::CACHE_ITEM); } - public function getOpenPool(): array { + public function openPoolList(): array { $key = self::CACHE_OPEN_POOL; $pool = self::$cache->get_value($key); if ($pool === false) { @@ -84,13 +86,14 @@ class Bonus extends \Gazelle\Base { self::$db->prepared_query(" SELECT um.ID FROM users_main um - AND um.Enabled = '1' - AND NOT EXISTS ( + WHERE NOT EXISTS ( SELECT 1 FROM user_has_attr uha INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts')) WHERE uha.UserID = um.ID ) - "); + AND um.Enabled = ? + ", UserStatus::enabled->value + ); return $this->addMultiPoints($points, self::$db->collect('ID')); } @@ -99,14 +102,14 @@ class Bonus extends \Gazelle\Base { SELECT um.ID FROM users_main um INNER JOIN user_last_access ula ON (ula.user_id = um.ID) - AND um.Enabled = '1' - AND NOT EXISTS ( + WHERE NOT EXISTS ( SELECT 1 FROM user_has_attr uha INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts')) WHERE uha.UserID = um.ID ) + AND um.Enabled = ? AND ula.last_access >= ? - ", $since + ", UserStatus::enabled->value, $since ); return $this->addMultiPoints($points, self::$db->collect('ID')); } @@ -116,14 +119,14 @@ class Bonus extends \Gazelle\Base { SELECT DISTINCT um.ID FROM users_main um INNER JOIN torrents t ON (t.UserID = um.ID) - AND um.Enabled = '1' - AND NOT EXISTS ( + WHERE NOT EXISTS ( SELECT 1 FROM user_has_attr uha INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts')) WHERE uha.UserID = um.ID ) + AND um.Enabled = ? AND t.created >= ? - ", $since + ", UserStatus::enabled->value, $since ); return $this->addMultiPoints($points, self::$db->collect('ID')); } @@ -133,24 +136,26 @@ class Bonus extends \Gazelle\Base { SELECT DISTINCT um.ID FROM users_main um INNER JOIN xbt_files_users xfu ON (xfu.uid = um.ID) - AND um.Enabled = '1' - AND NOT EXISTS ( + WHERE NOT EXISTS ( SELECT 1 FROM user_has_attr uha INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points', 'no-fl-gifts')) WHERE uha.UserID = um.ID ) - AND xfu.active = 1 and xfu.remaining = 0 and xfu.connectable = 1 and timespent > 0 - "); + AND xfu.remaining = 0 + AND xfu.active = 1 + AND um.Enabled = ? + ", UserStatus::enabled->value + ); return $this->addMultiPoints($points, self::$db->collect('ID')); } public function givePoints(\Gazelle\Task|null $task = null): int { //------------------------ Update Bonus Points -------------------------// // calculation: - // Size * (0.0754 + (0.1207 * ln(1 + seedtime)/ (seeders ^ 0.55))) - // Size (convert from bytes to GB) is in torrents - // Seedtime (convert from hours to days) is in xbt_files_history - // Seeders is in torrents_leech_stats + // size (convert from bytes to GB) is in torrents + // seedtime (convert from hours to days) is in xbt_files_history + // seeders is in torrents_leech_stats + // bonus_scale how the torrent size is adjusted according to category self::$db->dropTemporaryTable("bonus_update"); self::$db->prepared_query(" @@ -165,23 +170,29 @@ class Bonus extends \Gazelle\Base { self::$db->prepared_query(" INSERT INTO bonus_update (user_id, delta) SELECT xfu.uid, - sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders)) - FROM xbt_files_users AS xfu - INNER JOIN xbt_files_history AS xfh USING (uid, fid) - INNER JOIN users_main AS um ON (um.ID = xfu.uid) - INNER JOIN torrents AS t ON (t.ID = xfu.fid) - INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID) - WHERE xfu.active = 1 - AND xfu.remaining = 0 - AND xfu.mtime > unix_timestamp(now() - INTERVAL 1 HOUR) - AND um.Enabled = '1' - AND NOT EXISTS ( + sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale)) + FROM ( + SELECT DISTINCT uid, fid + FROM xbt_files_users + WHERE remaining = 0 + AND active = 1 + AND mtime > unix_timestamp(now() - INTERVAL 1 HOUR) + ) xfu + INNER JOIN xbt_files_history xfh USING (uid, fid) + INNER JOIN users_main um ON (um.ID = xfu.uid) + INNER JOIN torrents t ON (t.ID = xfu.fid) + INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID) + INNER JOIN torrents_group tg ON (tg.ID = t.GroupID) + INNER JOIN category c ON (c.category_id = tg.CategoryID) + WHERE NOT EXISTS ( SELECT 1 FROM user_has_attr uha INNER JOIN user_attr ua ON (ua.ID = uha.UserAttrID AND ua.Name IN ('disable-bonus-points')) WHERE uha.UserID = um.ID ) + AND um.Enabled = ? GROUP BY xfu.uid - "); + ", UserStatus::enabled->value + ); self::$db->prepared_query(" SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ "); @@ -189,7 +200,7 @@ class Bonus extends \Gazelle\Base { self::$db->prepared_query(" INSERT INTO user_bonus - (user_id, points) + (user_id, points) SELECT bu.user_id, bu.delta FROM bonus_update bu ON DUPLICATE KEY UPDATE points = points + bu.delta diff --git a/app/User/Bonus.php b/app/User/Bonus.php index 126acd26b..344273c97 100644 --- a/app/User/Bonus.php +++ b/app/User/Bonus.php @@ -3,6 +3,7 @@ namespace Gazelle\User; use Gazelle\BonusPool; +use Gazelle\Util\SortableTableHeader; /** * Note: there is no userHasItem() method to check if a user has bought a @@ -119,6 +120,21 @@ class Bonus extends \Gazelle\BaseUser { return $summary; } + public function heading(): SortableTableHeader { + return new SortableTableHeader('hourlypoints', [ + 'title' => ['dbColumn' => 'title', 'defaultSort' => 'asc', 'text' => 'Title'], + 'size' => ['dbColumn' => 'size', 'defaultSort' => 'desc', 'text' => 'Size'], + 'seeders' => ['dbColumn' => 'seeders', 'defaultSort' => 'desc', 'text' => 'Seeders'], + 'seedtime' => ['dbColumn' => 'seed_time', 'defaultSort' => 'desc', 'text' => 'Duration'], + 'hourlypoints' => ['dbColumn' => 'hourly_points', 'defaultSort' => 'desc', 'text' => 'BP/hour'], + 'dailypoints' => ['dbColumn' => 'daily_points', 'defaultSort' => 'desc', 'text' => 'BP/day'], + 'weeklypoints' => ['dbColumn' => 'weekly_points', 'defaultSort' => 'desc', 'text' => 'BP/week'], + 'monthlypoints' => ['dbColumn' => 'monthly_points', 'defaultSort' => 'desc', 'text' => 'BP/month'], + 'yearlypoints' => ['dbColumn' => 'yearly_points', 'defaultSort' => 'desc', 'text' => 'BP/year'], + 'pointspergb' => ['dbColumn' => 'points_per_gb', 'defaultSort' => 'desc', 'text' => 'BP/GB/year'], + ]); + } + public function history(int $limit, int $offset): array { $page = $offset / $limit; $key = sprintf(self::CACHE_HISTORY, $this->user->id, $page); @@ -494,15 +510,17 @@ class Bonus extends \Gazelle\BaseUser { public function hourlyRate(): float { return (float)self::$db->scalar(" - SELECT sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders)) + SELECT sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale)) FROM ( - SELECT DISTINCT uid,fid + SELECT DISTINCT uid, fid FROM xbt_files_users WHERE active = 1 AND remaining = 0 AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) AND uid = ? ) AS xfu - INNER JOIN xbt_files_history AS xfh USING (uid, fid) - INNER JOIN torrents AS t ON (t.ID = xfu.fid) + INNER JOIN xbt_files_history xfh USING (uid, fid) + INNER JOIN torrents t ON (t.ID = xfu.fid) INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID) + INNER JOIN torrents_group tg ON (tg.ID = t.GroupID) + INNER JOIN category c ON (c.category_id = tg.CategoryID) WHERE xfu.uid = ? ", $this->user->id, $this->user->id ); @@ -510,29 +528,31 @@ class Bonus extends \Gazelle\BaseUser { public function userTotals(): array { $stats = self::$db->rowAssoc(" - SELECT count(*) AS total_torrents, - coalesce(sum(t.Size), 0) AS total_size, - coalesce(sum(bonus_accrual(t.Size, xfh.seedtime, tls.Seeders)), 0) AS hourly_points, - coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 1), tls.Seeders)), 0) * (24 * 1) AS daily_points, - coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 7), tls.Seeders)), 0) * (24 * 7) AS weekly_points, - coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004/12), tls.Seeders)), 0) * (24 * 365.256363004/12) AS monthly_points, - coalesce(sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders)), 0) * (24 * 365.256363004) AS yearly_points, + SELECT count(*) AS total_torrents, + coalesce(sum(t.Size), 0) AS total_size, + coalesce(sum(category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale)), 0) AS hourly_points, + coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 1)), 0) AS daily_points, + coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 7)), 0) AS weekly_points, + coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636 / 12)), 0) AS monthly_points, + coalesce(sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636)), 0) AS yearly_points, if (coalesce(sum(t.Size), 0) = 0, 0, - sum(bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders)) * (24 * 365.256363004) - / (sum(t.Size) / (1024*1024*1024)) + sum(future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636)) + / (sum(t.Size) / (1024*1024*1024)) ) AS points_per_gb FROM ( SELECT DISTINCT uid, fid FROM xbt_files_users - WHERE active = 1 + WHERE active = 1 AND remaining = 0 - AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) - AND uid = ? + AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) + AND uid = ? ) AS xfu - INNER JOIN xbt_files_history AS xfh USING (uid, fid) - INNER JOIN torrents AS t ON (t.ID = xfu.fid) + INNER JOIN xbt_files_history xfh USING (uid, fid) + INNER JOIN torrents t ON (t.ID = xfu.fid) INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID) + INNER JOIN torrents_group tg ON (tg.ID = t.GroupID) + INNER JOIN category c ON (c.category_id = tg.CategoryID) WHERE xfu.uid = ? ", $this->user->id, $this->user->id ); @@ -540,37 +560,52 @@ class Bonus extends \Gazelle\BaseUser { return $stats; } - public function seedList(string $orderBy, string $orderWay, int $limit, int $offset): array { + public function seedList( + int $limit, + int $offset, + \Gazelle\Manager\Torrent $torMan = new \Gazelle\Manager\Torrent(), + ): array { + $heading = $this->heading(); self::$db->prepared_query(" SELECT t.ID, + tg.Name AS title, t.Size AS size, GREATEST(tls.Seeders, 1) AS seeders, xfh.seedtime AS seed_time, - bonus_accrual(t.Size, xfh.seedtime, tls.Seeders) AS hourly_points, - bonus_accrual(t.Size, xfh.seedtime + (24 * 1), tls.Seeders) * (24 * 1) AS daily_points, - bonus_accrual(t.Size, xfh.seedtime + (24 * 7), tls.Seeders) * (24 * 7) AS weekly_points, - bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004/12), tls.Seeders) * (24 * 365.256363004/12) AS monthly_points, - bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders) * (24 * 365.256363004) AS yearly_points, - bonus_accrual(t.Size, xfh.seedtime + (24 * 365.256363004), tls.Seeders) * (24 * 365.256363004) + category_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale) AS hourly_points, + future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 1) AS daily_points, + future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 7) AS weekly_points, + future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636 / 12) AS monthly_points, + future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636) AS yearly_points, + future_bonus_accrual(t.Size, xfh.seedtime, tls.Seeders, c.bonus_scale, 365.25636) / (t.Size / (1024*1024*1024)) AS points_per_gb FROM ( - SELECT DISTINCT uid,fid FROM xbt_files_users WHERE active=1 AND remaining=0 AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) AND uid = ? + SELECT DISTINCT uid, fid + FROM xbt_files_users + WHERE active = 1 + AND remaining = 0 + AND mtime > unix_timestamp(NOW() - INTERVAL 1 HOUR) + AND uid = ? ) AS xfu - INNER JOIN xbt_files_history AS xfh USING (uid, fid) - INNER JOIN torrents AS t ON (t.ID = xfu.fid) - INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID) + INNER JOIN xbt_files_history xfh USING (uid, fid) + INNER JOIN torrents t ON (t.ID = xfu.fid) + INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID) + INNER JOIN torrents_group tg ON (tg.ID = t.GroupID) + INNER JOIN category c ON (c.category_id = tg.CategoryID) WHERE xfu.uid = ? - ORDER BY $orderBy $orderWay + ORDER BY {$heading->orderBy()} {$heading->dir()} LIMIT ? OFFSET ? ", $this->user->id, $this->user->id, $limit, $offset ); $list = []; foreach (self::$db->to_array('ID', MYSQLI_ASSOC) as $r) { - $r['torrent'] = new \Gazelle\Torrent($r['ID']); - $list[] = $r; + if ($r['ID']) { + $r['torrent'] = $torMan->findById((int)$r['ID']); + $list[] = $r; + } } return $list; } diff --git a/app/Util/Time.php b/app/Util/Time.php index b3a8419e8..0319b33e0 100644 --- a/app/Util/Time.php +++ b/app/Util/Time.php @@ -218,7 +218,11 @@ class Time { return '0s'; } - $interval = [($seconds % 60) . 's']; + $interval = []; + $remainder = $seconds % 60; + if ($remainder) { + $interval[] = "{$remainder}s"; + } $minutes = (int)floor($seconds / 60); if ($minutes >= 60) { @@ -254,7 +258,16 @@ class Time { $interval[] = "{$day}d"; } if ($week) { - $interval[] = "{$week}w"; + if ($week < 52) { + $interval[] = "{$week}w"; + } else { + $year = (int)floor($week / 52); + $week = $week % 52; + if ($week) { + $interval[] = "{$week}w"; + } + $interval[] = "{$year}y"; + } } return implode('', array_slice(array_reverse($interval), 0, 2)); } diff --git a/docs/01-MysqlRoles.txt b/docs/01-MysqlRoles.txt index efa960595..4b0d1fa36 100644 --- a/docs/01-MysqlRoles.txt +++ b/docs/01-MysqlRoles.txt @@ -2,7 +2,7 @@ From: Spine To: Developers Date: 2024-12-07 Subject: Orpheus Development Papers #1 - Mysql Roles -Version: 2 +Version: 3 The default Gazelle installation defines a single Mysql role (with full privileges) and used for everything: the website, Ocelot and Sphinx. If any @@ -61,6 +61,8 @@ GRANT CREATE TEMPORARY TABLES, DELETE, INSERT, SELECT, UPDATE ON `gazelle`.* TO GRANT DROP ON `gazelle`.`drives` TO 'www'@'localhost'; GRANT EXECUTE ON FUNCTION `gazelle`.`binomial_ci` TO 'www'@'localhost'; GRANT EXECUTE ON FUNCTION `gazelle`.`bonus_accrual` TO 'www'@'localhost'; +GRANT EXECUTE ON FUNCTION `gazelle`.`category_bonus_accrual` TO 'www'@'localhost'; +GRANT EXECUTE ON FUNCTION `gazelle`.`future_bonus_accrual` TO 'www'@'localhost'; GRANT SELECT ON `sys`.`schema_unused_indexes` TO 'www'@'localhost'; GRANT SELECT ON `performance_schema`.`table_io_waits_summary_by_index_usage` TO 'www'@'localhost'; GRANT SELECT ON `performance_schema`.`table_io_waits_summary_by_table` TO 'www'@'localhost'; diff --git a/docs/Docker.txt b/docs/Docker.txt index 58785029f..f3273c866 100644 --- a/docs/Docker.txt +++ b/docs/Docker.txt @@ -27,13 +27,11 @@ docker exec -i $MYSQL_CONTAINER sh -c "exec mysql -uroot -p'$PASSWORD'" < all-da docker exec -it $MYSQL_CONTAINER mysql_upgrade -u root -p$PASSWORD # Some custom functions may need to be recreated (the site will error on load) -git grep CREATE FUNCTION misc/phinx/db/migrations -misc/phinx/db/migrations/20200320183228_bonus_accrual_function.php +git grep -l 'CREATE FUNCTION' misc/my-migrations +misc/my-migrations/20180104060449_tables.php +misc/my-migrations/20200320183228_bonus_accrual_function.php -Execute in the mysql client: - CREATE FUNCTION bonus_accrual(Size bigint, Seedtime float, Seeders integer) - RETURNS float DETERMINISTIC NO SQL - RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35)); +Execute the function definition statements in the mysql client. memcache -------- diff --git a/misc/docker/web/bootstrap-base.sh b/misc/docker/web/bootstrap-base.sh index 55c1f6d0f..f8bf10011 100755 --- a/misc/docker/web/bootstrap-base.sh +++ b/misc/docker/web/bootstrap-base.sh @@ -68,12 +68,15 @@ GRANT SELECT ON performance_schema.table_io_waits_summary_by_table TO 'ro_$MYSQL GRANT SELECT ON sys.schema_redundant_indexes TO 'ro_$MYSQL_USER'@'%'; GRANT SELECT ON sys.schema_unused_indexes TO 'ro_$MYSQL_USER'@'%'; GRANT SELECT ON sys.x\$schema_flattened_keys TO 'ro_$MYSQL_USER'@'%'; -CREATE FUNCTION IF NOT EXISTS bonus_accrual(Size bigint, Seedtime float, Seeders integer) - RETURNS float DETERMINISTIC NO SQL - RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35)); CREATE FUNCTION IF NOT EXISTS binomial_ci(p int, n int) RETURNS float DETERMINISTIC RETURN IF(n = 0,0.0,((p + 1.35336) / n - 1.6452 * SQRT((p * (n-p)) / n + 0.67668) / n) / (1 + 2.7067 / n)); +CREATE FUNCTION IF NOT EXISTS bonus_accrual(Size bigint, Seedtime float, Seeders integer) + RETURNS float DETERMINISTIC NO SQL + RETURN Size / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + Seedtime/24)) / pow(greatest(Seeders, 1), 0.35)); +CREATE FUNCTION IF NOT EXISTS category_bonus_accrual(size bigint, seedtime float, seeders integer, scale float) + RETURNS float DETERMINISTIC NO SQL + RETURN (size / scale) / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + seedtime/24)) / pow(greatest(seeders, 1), 0.35)); EOF ) | mysql -u root -p"$MYSQL_ROOT_PASSWORD" || exit 1 fi diff --git a/misc/my-migrations/20250822000000_category_bonus_scale.php b/misc/my-migrations/20250822000000_category_bonus_scale.php new file mode 100644 index 000000000..34e52851f --- /dev/null +++ b/misc/my-migrations/20250822000000_category_bonus_scale.php @@ -0,0 +1,44 @@ +table('category') + ->addColumn('bonus_scale', 'float', ['default' => '1.0']) + ->save(); + $this->execute(' + CREATE FUNCTION IF NOT EXISTS category_bonus_accrual(size bigint, seedtime float, seeders integer, scale float) + RETURNS float DETERMINISTIC NO SQL + RETURN (size / scale) / pow(1024, 3) * (0.0433 + (0.07 * ln(1 + seedtime/24)) / pow(greatest(seeders, 1), 0.35)) + '); + // 0.0433 * 24 = 1.0392, 0.07 * 24 = 1.68 + $this->execute(' + CREATE FUNCTION IF NOT EXISTS future_bonus_accrual(size bigint, seedtime float, seeders integer, scale float, days float) + RETURNS float DETERMINISTIC NO SQL + RETURN (size / scale) / (1024*1024*1024) + * ( + 1.0392 * days + + 1.68 * ( + (seedtime / 24 + days + 1) * (ln(seedtime / 24 + days + 1) - 1) + - (seedtime / 24 + 1) * (ln(seedtime / 24 + 1) - 1) + ) + / pow(greatest(seeders, 1), 0.35) + ); + '); + } + + public function down(): void { + $this->table('category') + ->removeColumn('bonus_scale') + ->save(); + $this->execute(' + DROP FUNCTION IF EXISTS category_bonus_accrual + '); + $this->execute(' + DROP FUNCTION IF EXISTS future_bonus_accrual + '); + } +} diff --git a/misc/pg-migrations/20250822000000_category_bonus_scale.php b/misc/pg-migrations/20250822000000_category_bonus_scale.php new file mode 100644 index 000000000..2549dbce2 --- /dev/null +++ b/misc/pg-migrations/20250822000000_category_bonus_scale.php @@ -0,0 +1,37 @@ +query(" + drop foreign table if exists relay.category + "); + $this->query(" + import foreign schema " . MYSQL_DB + . " limit to (category) from server relayer into relay; + "); + $this->table('category') + ->addColumn('bonus_scale', 'float', ['default' => '1.0']) + ->save(); + } + + public function down(): void { + $this->query(" + drop foreign table if exists relay.category + "); + $this->query(" + import foreign schema " . MYSQL_DB + . " limit to (category) from server relayer into relay; + "); + $this->table('category') + ->removeColumn('bonus_scale') + ->save(); + } +} diff --git a/sections/bonus/bprates.php b/sections/bonus/bprates.php index 2a5650ff8..aae39eb5b 100644 --- a/sections/bonus/bprates.php +++ b/sections/bonus/bprates.php @@ -6,35 +6,16 @@ declare(strict_types=1); namespace Gazelle; -$page = max(1, (int)($_GET['page'] ?? 1)); -$limit = TORRENTS_PER_PAGE; -$offset = TORRENTS_PER_PAGE * ($page - 1); - -$heading = new Util\SortableTableHeader('hourlypoints', [ - 'size' => ['dbColumn' => 'size', 'defaultSort' => 'desc', 'text' => 'Size'], - 'seeders' => ['dbColumn' => 'seeders', 'defaultSort' => 'desc', 'text' => 'Seeders'], - 'seedtime' => ['dbColumn' => 'seed_time', 'defaultSort' => 'desc', 'text' => 'Duration'], - 'hourlypoints' => ['dbColumn' => 'hourly_points', 'defaultSort' => 'desc', 'text' => 'BP/hour'], - 'dailypoints' => ['dbColumn' => 'daily_points', 'defaultSort' => 'desc', 'text' => 'BP/day'], - 'weeklypoints' => ['dbColumn' => 'weekly_points', 'defaultSort' => 'desc', 'text' => 'BP/week'], - 'monthlypoints' => ['dbColumn' => 'monthly_points', 'defaultSort' => 'desc', 'text' => 'BP/month'], - 'yearlypoints' => ['dbColumn' => 'yearly_points', 'defaultSort' => 'desc', 'text' => 'BP/year'], - 'pointspergb' => ['dbColumn' => 'points_per_gb', 'defaultSort' => 'desc', 'text' => 'BP/GB/year'], -]); - -$userMan = new Manager\User(); if (empty($_GET['userid'])) { $user = $Viewer; - $ownProfile = true; } else { if (!$Viewer->permitted('admin_bp_history')) { Error403::error(); } - $user = $userMan->findById((int)($_GET['userid'] ?? 0)); + $user = new Manager\User()->findById((int)($_GET['userid'] ?? 0)); if (is_null($user)) { Error404::error(); } - $ownProfile = false; } $bonus = new User\Bonus($user); @@ -43,10 +24,9 @@ $paginator = new Util\Paginator(TORRENTS_PER_PAGE, (int)($_GET['page'] ?? 1)); $paginator->setTotal($total['total_torrents']); echo $Twig->render('user/bonus.twig', [ - 'heading' => $heading, - 'list' => $bonus->seedList($heading->orderBy(), $heading->dir(), $paginator->limit(), $paginator->offset()), + 'heading' => $bonus->heading(), + 'list' => $bonus->seedList($paginator->limit(), $paginator->offset()), 'paginator' => $paginator, - 'title' => $ownProfile ? 'Your Bonus Points Rate' : ($user->username() . "'s Bonus Point Rate"), 'total' => $total, 'user' => $user, 'viewer' => $Viewer, diff --git a/sections/bonus/store.php b/sections/bonus/store.php index 7055ef3f1..699f35e70 100644 --- a/sections/bonus/store.php +++ b/sections/bonus/store.php @@ -38,7 +38,7 @@ echo $Twig->render('bonus/store.twig', [ 'bonus' => $bonus, 'discount' => $bonusMan->discount(), 'donate' => $donate, - 'pool' => $bonusMan->getOpenPool(), + 'pool' => $bonusMan->openPoolList(), 'purchase' => $purchase, 'viewer' => $Viewer, ]); diff --git a/templates/user/bonus-history.twig b/templates/user/bonus-history.twig index df6fe0401..8662c9b65 100644 --- a/templates/user/bonus-history.twig +++ b/templates/user/bonus-history.twig @@ -12,39 +12,39 @@
- {%- if self %}You{% else %}{{ user.username }}{% endif %} spent +{% if self %}You{% else %}{{ user.username }}{% endif %} spent {{ p.total|number_format }} bonus points to donate to the {{ p.name }} - {% if date(now) > date(p.until_date) %}ended {% else %}ending in {% endif -%} +{% if date(now) > date(p.until_date) %}ended {% else %}ending in {% endif -%} {{- p.until_date|time_diff }}.
- {% endfor %} - {% endif -%} - {%- if summary.total -%} +{% endfor %} +{% endif %} +{% if summary.total %}- {%- if self %}You{% else %}{{ user.id|user_url }}{% endif %} spent - {% if pool_total %} a further {% endif -%} {{ summary.total|number_format }} +{% if self %}You{% else %}{{ user.id|user_url }}{% endif %} spent +{% if pool_total %} a further {% endif -%} {{ summary.total|number_format }} bonus points to purchase {{ summary.nr|number_format }} item{{ summary.nr|plural }}.
- {% endif %} - {%- if pool_total and summary.total %} +{% endif %} +{% if pool_total and summary.total %}That makes a grand total of {{ (pool_total + summary.total)|number_format }} points, - {%- set total = pool_total + summary.total -%} - {%- if total > 500000 %} very - {%- elseif total > 1000000 %} very, very - {%- elseif total > 5000000 %} extremely - {%- elseif total > 10000000 %} exceptionally - {%- endif %} well done!
- {%- endif %} +{% set total = pool_total + summary.total -%} +{% if total > 500000 %} very +{% elseif total > 1000000 %} very, very +{% elseif total > 5000000 %} extremely +{% elseif total > 10000000 %} exceptionally +{% endif %} well done! +{% endif %}| Total | Cost | - {% set total_item = 0 %} - {% set total_cost = 0 %} - {% for i in item %} +{% set total_item = 0 %} +{% set total_cost = 0 %} +{% for i in item %}|
| {{ i.title }} | {{ i.total|number_format }} | {{ i.cost|number_format }} |
| Total | {{ total_item|number_format }} | @@ -71,10 +71,10 @@
| Item | @@ -82,14 +82,14 @@Purchase Date | For | |
| {{ h.Title }} | {{ h.Price|number_format }} | {{ h.PurchaseDate|time_diff }} | {% if h.OtherUserID > 0 %}{{ h.OtherUserID|user_url }}{% else %} {% endif %} |
| Torrent | +{{ heading.emit('title')|raw }} | {{ heading.emit('size')|raw }} | -{{ heading.emit('seeders')|raw }} | -{{ heading.emit('seedtime')|raw }} | -{{ heading.emit('hourlypoints')|raw }} | -{{ heading.emit('dailypoints')|raw }} | -{{ heading.emit('weeklypoints')|raw }} | -{{ heading.emit('monthlypoints')|raw }} | -{{ heading.emit('yearlypoints')|raw }} | -{{ heading.emit('pointspergb')|raw }} | +{{ heading.emit('seeders')|raw }} | +{{ heading.emit('seedtime')|raw }} | +{{ heading.emit('hourlypoints')|raw }} | +{{ heading.emit('dailypoints')|raw }} | +{{ heading.emit('weeklypoints')|raw }} | +{{ heading.emit('monthlypoints')|raw }} | +{{ heading.emit('yearlypoints')|raw }} | +{{ heading.emit('pointspergb')|raw }} |
| No torrents being seeded currently | +No torrents being seeded currently | |||||||||||||||||