allow bonus accrual by category

This commit is contained in:
Spine
2025-08-23 19:54:36 +00:00
parent d686d0d54e
commit fa67f6781b
16 changed files with 402 additions and 144 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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';

View File

@@ -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
--------

View File

@@ -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

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CategoryBonusScale extends AbstractMigration {
public function up(): void {
$this->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
');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
require_once __DIR__ . '/../../lib/config.php';
// phpcs:enable PSR1.Files.SideEffects.FoundWithSymbols
use Phinx\Migration\AbstractMigration;
final class CategoryBonusScale extends AbstractMigration {
public function up(): 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')
->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();
}
}

View File

@@ -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,

View File

@@ -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,
]);

View File

@@ -12,39 +12,39 @@
<div class="thin">
{% if paginator.offset == 0 %}
<div class="pad box">
{% if pool_total %}
{% for p in pool_summary %}
{% if pool_total %}
{% for p in pool_summary %}
<p>
{%- 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 }}.
</p>
{% endfor %}
{% endif -%}
{%- if summary.total -%}
{% endfor %}
{% endif %}
{% if summary.total %}
<p>
{%- 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 }}.
</p>
{% endif %}
{%- if pool_total and summary.total %}
{% endif %}
{% if pool_total and summary.total %}
<p>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!</p>
{%- 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!</p>
{% endif %}
</div>
{% endif %}
{% if not history %}
<h3>No purchase history</h3>
{% else %}
{% if paginator.offset == 0 %}
{% if paginator.offset == 0 %}
<br />
<h3>Item summary</h3>
<table>
@@ -53,17 +53,17 @@
<td style="text-align: right; width: 100px">Total</td>
<td style="text-align: right; width: 100px">Cost</td>
</tr>
{% set total_item = 0 %}
{% set total_cost = 0 %}
{% for i in item %}
{% set total_item = 0 %}
{% set total_cost = 0 %}
{% for i in item %}
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
{% set total_item = total_item + i.total %}
{% set total_cost = total_cost + i.cost %}
{% set total_item = total_item + i.total %}
{% set total_cost = total_cost + i.cost %}
<td>{{ i.title }}</td>
<td style="text-align: right;">{{ i.total|number_format }}</td>
<td style="text-align: right;">{{ i.cost|number_format }}</td>
</tr>
{% endfor %}
{% endfor %}
<tr style="border-top: #333333 solid 1px;">
<td><i>Total</i></td>
<td style="text-align: right;">{{ total_item|number_format }}</td>
@@ -71,10 +71,10 @@
</tr>
</table>
<br />
{% endif %}
{% endif %}
<h3>Purchase Details</h3>
{{ paginator.linkbox|raw }}
{{ paginator.linkbox|raw }}
<table>
<tr class="colhead">
<td>Item</td>
@@ -82,14 +82,14 @@
<td style="width:180px">Purchase Date</td>
<td>For</td>
</tr>
{% for h in history %}
{% for h in history %}
<tr class="row{{ cycle(['a', 'b'], loop.index0) }}">
<td>{{ h.Title }}</td>
<td style="text-align: right;">{{ h.Price|number_format }}</td>
<td>{{ h.PurchaseDate|time_diff }}</td>
<td>{% if h.OtherUserID > 0 %}{{ h.OtherUserID|user_url }}{% else %}&nbsp;{% endif %}</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{{ paginator.linkbox|raw }}
{% endif %}

View File

@@ -1,3 +1,10 @@
{% set title %}
{% if user.id == viewer.id %}
Your Bonus Points Rates
{% else %}
{{ user.username }}'s Bonus Point Rates
{% endif %}
{% endset %}
{{ header(title) }}
<div class="header">
<h2>{{ title }}</h2>
@@ -42,16 +49,16 @@
<table>
<thead>
<tr class="colhead">
<td>Torrent</td>
<td class="nobr">{{ heading.emit('title')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('size')|raw }}</td>
<td class="nobr">{{ heading.emit('seeders')|raw }}</td>
<td class="nobr">{{ heading.emit('seedtime')|raw }}</td>
<td class="nobr">{{ heading.emit('hourlypoints')|raw }}</td>
<td class="nobr">{{ heading.emit('dailypoints')|raw }}</td>
<td class="nobr">{{ heading.emit('weeklypoints')|raw }}</td>
<td class="nobr">{{ heading.emit('monthlypoints')|raw }}</td>
<td class="nobr">{{ heading.emit('yearlypoints')|raw }}</td>
<td class="nobr">{{ heading.emit('pointspergb')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('seeders')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('seedtime')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('hourlypoints')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('dailypoints')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('weeklypoints')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('monthlypoints')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('yearlypoints')|raw }}</td>
<td class="nobr number_column">{{ heading.emit('pointspergb')|raw }}</td>
</tr>
</thead>
<tbody>
@@ -70,7 +77,7 @@
</tr>
{% else %}
<tr>
<td colspan="9" style="text-align:center;">No torrents being seeded currently</td>
<td colspan="10" style="text-align:center;">No torrents being seeded currently</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -132,6 +132,7 @@ class Helper {
array $tagName,
int $releaseType = 1
): \Gazelle\TGroup {
$user->requestContext()->setViewer($user);
$tgroup = new \Gazelle\Manager\TGroup()->create(
categoryId: (int)new Category()->findIdByName('Music'),
releaseType: $releaseType,

View File

@@ -16,8 +16,10 @@ class BonusTest extends TestCase {
Helper::removeTGroup($tgroup, current($this->userList));
}
}
foreach ($this->userList as $user) {
$user->remove();
if (isset($this->userList)) {
foreach ($this->userList as $user) {
$user->remove();
}
}
}
@@ -50,7 +52,10 @@ class BonusTest extends TestCase {
$giver->setPoints($startingPoints);
$this->assertEquals($startingPoints, $giver->user()->bonusPointsTotal(), 'bonus-set-points');
$itemList = new Manager\Bonus()->itemList();
$manager = new Manager\Bonus();
$manager->flushPriceCache();
$itemList = $manager->itemList();
$this->assertArrayHasKey('token-1', $itemList, 'item-token-1');
$token = $giver->item('token-1');
$this->assertArrayHasKey('Price', $token, 'item-price-1');
@@ -134,9 +139,100 @@ class BonusTest extends TestCase {
$this->assertTrue($giver->removePoints(1.125), 'bonus-taketh-away');
}
public function testBonusPool(): void {
global $Cache;
$Cache->delete_value("bonus_pool");
$manager = new Manager\Bonus();
$this->assertEquals(
[],
$manager->openPoolList(),
'bonus-open-pool',
);
}
public function testAddPoints(): void {
$this->userList = [
Helper::makeUser('bonusadd.' . randomString(6), 'bonus', enable: false),
Helper::makeUser('bonusadd.' . randomString(6), 'bonus', enable: true),
];
// back to the future
DB::DB()->prepared_query("
INSERT INTO user_last_access (user_id, last_access) values (?, ?)
", $this->userList[1]->id, date('Y-m-d H:i:s', time() + 10)
);
$manager = new Manager\Bonus();
$this->assertEquals(
1,
// but not too far
$manager->addActivePoints(23456, date('Y-m-d H:i:s', time() + 5)),
'bonus-add-active',
);
$this->assertEquals(
0,
$manager->addMultiPoints(789, []),
'bonus-add-no-multi-points',
);
$this->assertEquals(
2,
$manager->addMultiPoints(
12345,
array_map(fn ($u) => $u->id, $this->userList),
),
'bonus-add-multi-points',
);
$this->assertEquals(
0,
$manager->addUploadPoints(369, date('Y-m-d H:i:s', time() + 1)),
'bonus-add-upload-points',
);
$this->assertEquals(
12345,
$this->userList[0]->flush()->bonusPointsTotal(),
'bonus-added-user-0',
);
$this->tgroupList[] = Helper::makeTGroupMusic(
name: 'bonus add ' . randomString(10),
artistName: [[ARTIST_MAIN], ['phpunit bonus add ' . randomString(12)]],
tagName: ['hard.bop'],
user: $this->userList[1],
);
$torrent = Helper::makeTorrentMusic(
tgroup: $this->tgroupList[0],
user: $this->userList[1],
title: 'phpunit bonus add ' . randomString(10),
);
Helper::generateTorrentSeed($torrent, $this->userList[1]);
$this->assertGreaterThan(
0,
$manager->addSeedPoints(24680),
'bonus-add-seed-points',
);
$this->assertEquals(
12345 + 23456 + 24680,
$this->userList[1]->flush()->bonusPointsTotal(),
'bonus-added-user-1',
);
$this->userList[1]->toggleAttr('no-fl-gifts', true);
$this->assertGreaterThan(
0,
$manager->addGlobalPoints(7531),
'bonus-add-global-points',
);
$this->assertEquals(
12345 + 23456 + 24680,
$this->userList[1]->flush()->bonusPointsTotal(),
'bonus-added-no-global',
);
foreach ($this->userList as $u) {
new User\Bonus($u)->setPoints(0.0);
}
}
public function testUploadReward(): void {
$this->userList[] = Helper::makeUser('bonusup.' . randomString(6), 'bonus');
$this->userList[0]->requestContext()->setViewer($this->userList[0]);
$this->tgroupList[] = Helper::makeTGroupMusic(
name: 'bonus ' . randomString(10),
artistName: [[ARTIST_MAIN], ['phpunit bonus ' . randomString(12)]],

View File

@@ -262,8 +262,9 @@ class RequestTest extends TestCase {
// race condition between requests.LastPostTime and requests.created that would be
// difficult to remove without adding a lot of complications to the code.
$this->assertFalse($this->request->hasNewVote(), 'request-no-new-vote');
Helper::sleepTick(); // to ensure lastVoteDate() > created()
// add some bounty
Helper::sleepTick(); // to ensure lastVoteDate() > created()
$this->assertTrue($this->request->vote($user, $bounty), 'request-more-bounty');
$this->assertTrue($this->request->flush()->hasNewVote(), 'request-has-new-vote');
$this->assertEquals(2, $this->request->userVotedTotal(), 'request-total-voted');

View File

@@ -78,4 +78,34 @@ class TimeTest extends TestCase {
'time-convert-span'
);
}
public static function providerSeconds(): array {
$hour = 3600;
$day = $hour * 24;
$week = $day * 7;
$year = $week * 52;
return [
[ -1, '0s'],
[ 0, '0s'],
[ 1, '1s'],
[ 1, '1s'],
[ 60, '1m'],
[ 119, '1m59s'],
[ 3599, '59m59s'],
[ $hour * 19, '19h'],
[ $hour + 62, '1h1m'],
[ $day * 4, '4d'],
[ $week + 61, '1w1m'],
[$year + $hour * 3, '1y3h'],
];
}
#[DataProvider('providerSeconds')]
public function testConvertSeconds(int $seconds, string $expected): void {
$this->assertEquals(
$expected,
Time::convertSeconds($seconds),
"time-seconds-$seconds"
);
}
}