mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
Handle unseeded and never-seeded uploads in a sane manner
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged --verbose
|
||||
yarn lint-staged -q
|
||||
|
||||
@@ -246,7 +246,7 @@ class Debug {
|
||||
}
|
||||
|
||||
//Lets not be repetitive
|
||||
if (($Tracer[$Steps]['function'] == 'include' || $Tracer[$Steps]['function'] == 'require' ) && isset($Tracer[$Steps]['args'][0]) && $Tracer[$Steps]['args'][0] == $File) {
|
||||
if (isset($Tracer[$Steps]) && ($Tracer[$Steps]['function'] == 'include' || $Tracer[$Steps]['function'] == 'require' ) && isset($Tracer[$Steps]['args'][0]) && $Tracer[$Steps]['args'][0] == $File) {
|
||||
unset($Tracer[$Steps]['args']);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,10 +111,6 @@ class PM extends Base {
|
||||
return $this->info()['subject'];
|
||||
}
|
||||
|
||||
public function body(): string {
|
||||
return $this->info()['body'];
|
||||
}
|
||||
|
||||
public function isReadable(): bool {
|
||||
return in_array($this->user->id(), $this->info()['sender_list'])
|
||||
|| in_array($this->user->id(), $this->recipientList());
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Schedule\Tasks;
|
||||
|
||||
class DeleteNeverSeededTorrents extends \Gazelle\Schedule\Task {
|
||||
public function run(): void {
|
||||
$torrents = new \Gazelle\Torrent\Reaper;
|
||||
|
||||
$deleted = $torrents->deleteDeadTorrents(false, true);
|
||||
foreach ($deleted as $id) {
|
||||
$this->debug("Deleted torrent $id", $id);
|
||||
$this->processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Schedule\Tasks;
|
||||
|
||||
class DeleteUnseededTorrents extends \Gazelle\Schedule\Task {
|
||||
public function run(): void {
|
||||
$torrents = new \Gazelle\Torrent\Reaper;
|
||||
|
||||
$deleted = $torrents->deleteDeadTorrents(true, false);
|
||||
foreach ($deleted as $id) {
|
||||
$this->debug("Deleted torrent $id", $id);
|
||||
$this->processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Schedule\Tasks;
|
||||
|
||||
class NotifyNonseedingUploaders extends \Gazelle\Schedule\Task {
|
||||
public function run(): void {
|
||||
// Send warnings to uploaders of torrents that will be deleted this week
|
||||
self::$db->prepared_query("
|
||||
SELECT
|
||||
t.ID,
|
||||
t.GroupID,
|
||||
tg.Name,
|
||||
t.Format,
|
||||
t.Encoding,
|
||||
t.UserID
|
||||
FROM torrents AS t
|
||||
INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID)
|
||||
INNER JOIN torrents_group AS tg ON (tg.ID = t.GroupID)
|
||||
INNER JOIN users_info AS u ON (u.UserID = t.UserID)
|
||||
WHERE tls.last_action < NOW() - INTERVAL 20 DAY
|
||||
AND tls.last_action != 0
|
||||
AND u.UnseededAlerts = '1'
|
||||
ORDER BY tls.last_action ASC"
|
||||
);
|
||||
|
||||
$torrentIDs = self::$db->to_array();
|
||||
$torrentAlerts = [];
|
||||
|
||||
foreach ($torrentIDs as $torrentID) {
|
||||
[$id, $groupID, $name, $format, $encoding, $userID] = $torrentID;
|
||||
$userID = (int)$userID;
|
||||
|
||||
if (!array_key_exists($userID, $torrentAlerts)) {
|
||||
$torrentAlerts[$userID] = ['Count' => 0, 'Msg' => ''];
|
||||
}
|
||||
|
||||
$artistName = \Artists::display_artists(\Artists::get_artist($groupID), false, false, false);
|
||||
if ($artistName) {
|
||||
$name = "$artistName - $name";
|
||||
}
|
||||
|
||||
if ($format && $encoding) {
|
||||
$name .= " [$format / $encoding]";
|
||||
}
|
||||
|
||||
$torrentAlerts[$userID]['Msg'] .= "\n[url=torrents.php?torrentid=$id]".$name."[/url]";
|
||||
$torrentAlerts[$userID]['Count']++;
|
||||
|
||||
$this->processed++;
|
||||
}
|
||||
|
||||
$userMan = new \Gazelle\Manager\User;
|
||||
foreach ($torrentAlerts as $userID => $messageInfo) {
|
||||
$userMan->sendPM($userID, 0,
|
||||
'Unseeded torrent notification',
|
||||
$messageInfo['Count'] . " of your uploads will be deleted for inactivity soon. Unseeded torrents are deleted after 4 weeks. If you still have the files, you can seed your uploads by ensuring the torrents are in your client and that they aren't stopped. You can view the time that a torrent has been unseeded by clicking on the torrent description line and looking for the \"Last active\" time. For more information, please go [url=wiki.php?action=article&id=77]here[/url].\n\nThe following torrent".plural($messageInfo['Count']).' will be removed for inactivity:'.$messageInfo['Msg']."\n\nIf you no longer wish to receive these notifications, please disable them in your profile settings."
|
||||
);
|
||||
$this->debug("Warning user $userID about {$messageInfo['Count']} torrents", (int)$userID);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Schedule/Tasks/Reaper.php
Normal file
22
app/Schedule/Tasks/Reaper.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Schedule\Tasks;
|
||||
|
||||
class Reaper extends \Gazelle\Schedule\Task {
|
||||
public function run(): void {
|
||||
$reaper = new \Gazelle\Torrent\Reaper(new \Gazelle\Manager\Torrent, new \Gazelle\Manager\User);
|
||||
$this->processed = 0;
|
||||
if (REAPER_TASK_CLAIM) {
|
||||
$reaper->claim();
|
||||
}
|
||||
if (REAPER_TASK_NOTIFY) {
|
||||
$this->processed += $reaper->notify();
|
||||
}
|
||||
if (REAPER_TASK_REMOVE_UNSEEDED) {
|
||||
$this->processed += $reaper->removeUnseeded();
|
||||
}
|
||||
if (REAPER_TASK_REMOVE_NEVER_SEEDED) {
|
||||
$this->processed += $reaper->removeNeverSeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,6 +419,16 @@ class Torrent extends TorrentAbstract {
|
||||
$manager->softDelete(SQLDB, 'torrents_lossyweb_approved', [['TorrentID', $this->id]]);
|
||||
$manager->softDelete(SQLDB, 'torrents_missing_lineage', [['TorrentID', $this->id]]);
|
||||
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM torrent_unseeded WHERE torrent_id = ?
|
||||
", $this->id
|
||||
);
|
||||
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM torrent_unseeded_claim WHERE torrent_id = ?
|
||||
", $this->id
|
||||
);
|
||||
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO user_torrent_remove
|
||||
(user_id, torrent_id)
|
||||
|
||||
@@ -3,105 +3,587 @@
|
||||
namespace Gazelle\Torrent;
|
||||
|
||||
class Reaper extends \Gazelle\Base {
|
||||
public function __construct(
|
||||
protected \Gazelle\Manager\Torrent $torMan,
|
||||
protected \Gazelle\Manager\User $userMan,
|
||||
) {}
|
||||
|
||||
public function deleteDeadTorrents(bool $unseeded, bool $neverSeeded) {
|
||||
if (!$unseeded && !$neverSeeded) {
|
||||
/**
|
||||
* Send notifications of all the possible upload states. Processing the
|
||||
* initial states of unseeded and never seeded uploads is what puts
|
||||
* torrents onto the conveyor belt towards revival... or the reaper.
|
||||
*
|
||||
* @return int total number of uploads processed
|
||||
*/
|
||||
public function notify(): int {
|
||||
// move uploads along the conveyor belt
|
||||
return $this->process($this->initialUnseededList(), ReaperState::UNSEEDED, ReaperNotify::INITIAL)
|
||||
+ $this->process($this->initialNeverSeededList(), ReaperState::NEVER, ReaperNotify::INITIAL)
|
||||
+ $this->process($this->finalUnseededList(), ReaperState::UNSEEDED, ReaperNotify::FINAL)
|
||||
+ $this->process($this->finalNeverSeededList(), ReaperState::NEVER, ReaperNotify::FINAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications (to the users who wish to receive them) regarding
|
||||
* uploads that are in various states of unseededness. Initial notifications
|
||||
* kick off the timer that eventually lead to a torrent being reaped.
|
||||
*/
|
||||
public function process(array $userList, ReaperState $state, ReaperNotify $notify): int {
|
||||
$processed = 0;
|
||||
foreach ($userList as $userId => $ids) {
|
||||
$user = $this->userMan->findById($userId);
|
||||
if ($user?->isEnabled()) {
|
||||
$this->notifySeeder($user, $ids, $state, $notify);
|
||||
}
|
||||
$processed += count($ids);
|
||||
}
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PM to a seeder listing their unseeded items
|
||||
*
|
||||
* @return int PM conversation id
|
||||
*/
|
||||
public function notifySeeder(\Gazelle\User $user, array $ids, ReaperState $state, ReaperNotify $notify): ?int {
|
||||
if ($user->hasAttr($state->notifyAttr())) {
|
||||
// No conversation will be created as they didn't ask for one
|
||||
return null;
|
||||
}
|
||||
$never = $state === ReaperState::NEVER;
|
||||
$final = $notify === ReaperNotify::FINAL;
|
||||
$total = count($ids);
|
||||
$subject = ($never ? "You have " : "There " . ($total > 1 ? 'are' : 'is') . " ")
|
||||
. article($total, $never ? 'a' : 'an') // "a non-seeded" versus "an unseeded"
|
||||
. ($never ? " non-seeded new upload" : " unseeded upload")
|
||||
. plural($total)
|
||||
. ($final ? " scheduled for deletion very soon" : " to rescue");
|
||||
return $this->userMan->sendPM($user->id(), 0, $subject,
|
||||
self::$twig->render('notification/unseeded.twig', [
|
||||
'final' => $final,
|
||||
'never' => $never,
|
||||
'list' => $ids,
|
||||
'user' => $user,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function initialNeverSeededList(): array {
|
||||
return $this->initialList(
|
||||
cond: [
|
||||
'tls.last_action IS NULL',
|
||||
't.Time < now() - INTERVAL ? HOUR', // interval
|
||||
],
|
||||
interval: NOTIFY_NEVER_SEEDED_INITIAL_HOUR,
|
||||
state: ReaperState::NEVER,
|
||||
);
|
||||
// Unlike unseeded uploads, there's nobody else to contact to see if
|
||||
// they can help revive them, so the game stops here.
|
||||
}
|
||||
|
||||
public function initialUnseededList(): array {
|
||||
$list = $this->initialList(
|
||||
cond: [
|
||||
'tls.last_action < now() - INTERVAL ? HOUR', // interval
|
||||
// TODO: We do not want to spam people who have voluntarily unseeded their redundant V2 uploads.
|
||||
// We need to nuke these V2 torrents and afterwards the following condition may be removed.
|
||||
"NOT (t.Format = 'MP3' AND t.Encoding = 'V2')",
|
||||
],
|
||||
interval: NOTIFY_UNSEEDED_INITIAL_HOUR,
|
||||
state: ReaperState::UNSEEDED,
|
||||
);
|
||||
if (!$list) {
|
||||
self::$db->commit();
|
||||
return [];
|
||||
}
|
||||
|
||||
$criteria = [];
|
||||
if ($unseeded) {
|
||||
$criteria[] = '(tls.last_action IS NOT NULL AND tls.last_action < now() - INTERVAL 28 DAY)';
|
||||
// The community can do something about these, find out who snatched them.
|
||||
$args = [];
|
||||
foreach ($list as $torrentIds) {
|
||||
array_push($args, ...$torrentIds);
|
||||
}
|
||||
if ($neverSeeded) {
|
||||
$criteria[] = '(tls.last_action IS NULL AND t.Time < now() - INTERVAL 2 DAY)';
|
||||
}
|
||||
|
||||
$criteria = implode(' OR ', $criteria);
|
||||
|
||||
self::$db->prepared_query("
|
||||
SELECT t.ID
|
||||
FROM torrents AS t
|
||||
INNER JOIN torrents_leech_stats AS tls ON (tls.TorrentID = t.ID)
|
||||
WHERE $criteria
|
||||
LIMIT 8000
|
||||
");
|
||||
$torrents = self::$db->collect('ID');
|
||||
SELECT xs.uid AS user_id,
|
||||
group_concat(DISTINCT xs.fid ORDER BY xs.fid) AS ids
|
||||
FROM xbt_snatched xs
|
||||
INNER JOIN torrents t ON (t.ID = xs.fid AND t.UserID != xs.uid)
|
||||
INNER JOIN users_main um ON (um.ID = xs.uid)
|
||||
LEFT JOIN torrent_unseeded_claim tuc ON (tuc.user_id = xs.uid AND tuc.torrent_id = xs.fid)
|
||||
WHERE tuc.user_id IS NULL
|
||||
AND um.Enabled = '1'
|
||||
AND t.ID IN (" . placeholders($args) . ")
|
||||
GROUP BY xs.uid
|
||||
ORDER BY xs.uid
|
||||
", ...$args
|
||||
);
|
||||
|
||||
$logEntries = $deleteNotes = [];
|
||||
$torMan = new \Gazelle\Manager\Torrent;
|
||||
|
||||
$i = 0;
|
||||
foreach ($torrents as $id) {
|
||||
$torrent = $torMan->findById($id);
|
||||
if (is_null($torrent)) {
|
||||
continue;
|
||||
// Send an alert to each snatcher listing all the uploads that they could reseed
|
||||
$snatchList = $this->expand(NOTIFY_REAPER_MAX_PER_USER, self::$db->to_array(false, MYSQLI_NUM, false));
|
||||
foreach ($snatchList as $userId => $torrentIds) {
|
||||
$user = $this->userMan->findById($userId);
|
||||
// cannot say !$user?->hasAttr() because !null is true
|
||||
if ($user && !$user->hasAttr(ReaperState::UNSEEDED->notifyAttr())) {
|
||||
$this->notifySnatcher($user, $torrentIds);
|
||||
}
|
||||
|
||||
[$success, $message] = $torrent->remove(0, 'inactivity (unseeded)');
|
||||
if (!$success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$infohash = strtoupper($torrent->infohash());
|
||||
$name = $torrent->name();
|
||||
$userId = $torrent->uploaderId();
|
||||
|
||||
$log = "Torrent $id ($name) ($infohash) was deleted for inactivity (unseeded)";
|
||||
$logEntries[] = $log;
|
||||
|
||||
if (!array_key_exists($userId, $deleteNotes)) {
|
||||
$deleteNotes[$userId] = ['Count' => 0, 'Msg' => ''];
|
||||
}
|
||||
$deleteNotes[$userId]['Msg'] .= sprintf("\n[url=torrents.php?id=%s]%s[/url]", $torrent->groupId(), $name);
|
||||
$deleteNotes[$userId]['Count']++;
|
||||
|
||||
++$i;
|
||||
}
|
||||
|
||||
$userMan = new \Gazelle\Manager\User;
|
||||
foreach ($deleteNotes as $userId => $messageInfo) {
|
||||
$singular = (($messageInfo['Count'] == 1) ? true : false);
|
||||
$userMan->sendPM( $userId, 0,
|
||||
$messageInfo['Count'].' of your torrents '.($singular ? 'has' : 'have').' been deleted for inactivity',
|
||||
($singular ? 'One' : 'Some').' of your uploads '.($singular ? 'has' : 'have').' been deleted for being unseeded. Since '.($singular ? 'it' : 'they').' didn\'t break any rules (we hope), please feel free to re-upload '.($singular ? 'it' : 'them').".\n\nThe following torrent".($singular ? ' was' : 's were').' deleted:'.$messageInfo['Msg']
|
||||
// and open reseed claims for them
|
||||
$args = [];
|
||||
foreach ($snatchList as $userId => $torrentIds) {
|
||||
foreach ($torrentIds as $torrentId) {
|
||||
array_push($args, $userId, $torrentId);
|
||||
}
|
||||
}
|
||||
if ($args) {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO torrent_unseeded_claim (user_id, torrent_id) VALUES "
|
||||
. placeholders(array_fill(0, (int)(count($args) / 2), true), "(?,?)") , ...$args
|
||||
);
|
||||
}
|
||||
unset($deleteNotes);
|
||||
self::$db->commit();
|
||||
return $list;
|
||||
}
|
||||
|
||||
if (count($logEntries) > 0) {
|
||||
$chunks = array_chunk($logEntries, 100);
|
||||
foreach ($chunks as $messages) {
|
||||
/**
|
||||
* Send a PM to a snatcher with their unseeded uploads.
|
||||
* NB: A message is sent only on the initial phase. On the final round,
|
||||
* messages are sent only to the seeders.
|
||||
*
|
||||
* @return int PM conversation id
|
||||
*/
|
||||
public function notifySnatcher(\Gazelle\User $user, array $ids): int {
|
||||
$total = count($ids);
|
||||
return $this->userMan->sendPM($user->id(), 0,
|
||||
"You have " . article($total, 'an') . " unseeded snatch" . plural($total, 'es') . ' to save',
|
||||
self::$twig->render('notification/unseeded-snatch.twig', [
|
||||
'list' => $ids,
|
||||
'user' => $user,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash of user id/torrent ids that have never been/are no longer announced.
|
||||
* The query self-limits the number of ids returned: there may be more, but they
|
||||
* will be handled in a subsequent run. This is to help prevent users being swamped
|
||||
* with hundreds of notifications after the reaper task is deactivated for an extended
|
||||
* period of time.
|
||||
*
|
||||
* Returns list of [user id:torrent ids] pairs to process
|
||||
*/
|
||||
public function initialList(array $cond, int $interval, ReaperState $state): array {
|
||||
$condition = implode(' AND ', $cond);
|
||||
self::$db->prepared_query("
|
||||
SELECT t.UserID AS user_id,
|
||||
group_concat(t.ID ORDER BY t.ID) AS ids
|
||||
FROM torrents t
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
LEFT JOIN torrent_unseeded tu ON (tu.torrent_id = t.ID)
|
||||
WHERE $condition
|
||||
AND tu.torrent_id IS NULL
|
||||
GROUP BY t.UserID
|
||||
LIMIT ?
|
||||
", $interval, NOTIFY_REAPER_MAX_NOTIFICATION
|
||||
);
|
||||
$initial = $this->expand(NOTIFY_REAPER_MAX_PER_USER, self::$db->to_array(false, MYSQLI_NUM, false));
|
||||
|
||||
// We have already limited the number of users visited. We don't, however, know
|
||||
// if we have received more uploads than we care to handle. We go through what
|
||||
// the net caught and if there is more than the maximum then the rest are
|
||||
// discarded. We may not even notify as many as NOTIFY_REAPER_MAX_NOTIFICATION!
|
||||
// But there will always be a next time.
|
||||
$torrentIds = [];
|
||||
$result = [];
|
||||
$limit = $state === ReaperState::NEVER ? MAX_NEVER_SEEDED_PER_RUN : MAX_UNSEEDED_PER_RUN;
|
||||
foreach ($initial as $userId => $ids) {
|
||||
$result[$userId] = $ids;
|
||||
array_push($torrentIds, ...$ids);
|
||||
if (count($torrentIds) > $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($torrentIds) {
|
||||
// Register the uploads that need to be reseeded, so that another notification
|
||||
// can be sent later and then eventually reap what's left afterwards.
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO torrent_unseeded (torrent_id, state) VALUES "
|
||||
. placeholders($torrentIds, "(?,'" . $state->value . "')") , ...$torrentIds
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function finalNeverSeededList(): array {
|
||||
return $this->finalList(
|
||||
state: ReaperState::NEVER,
|
||||
interval: NOTIFY_NEVER_SEEDED_FINAL_HOUR,
|
||||
);
|
||||
}
|
||||
|
||||
public function finalUnseededList(): array {
|
||||
return $this->finalList(
|
||||
state: ReaperState::UNSEEDED,
|
||||
interval: NOTIFY_UNSEEDED_FINAL_HOUR,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash of user/torrent ids that were not/no longer announced
|
||||
* and an unseeded notification has already been issued.
|
||||
*
|
||||
* Returns a list of [user id:torrent ids] pairs to process
|
||||
*/
|
||||
public function finalList(ReaperState $state, int $interval): array {
|
||||
// get the notifications to perform
|
||||
self::$db->begin_transaction();
|
||||
self::$db->prepared_query("
|
||||
SELECT t.UserID AS user_id,
|
||||
group_concat(t.ID ORDER BY t.ID) AS ids
|
||||
FROM torrents t
|
||||
INNER JOIN torrent_unseeded tu ON (tu.torrent_id = t.ID)
|
||||
WHERE tu.unseeded_date < now() - INTERVAL ? HOUR
|
||||
AND tu.notify = ?
|
||||
AND tu.state = ?
|
||||
GROUP BY t.UserID
|
||||
", $interval, ReaperNotify::INITIAL->value, $state->value
|
||||
);
|
||||
$final = $this->expand(NOTIFY_REAPER_MAX_PER_USER, self::$db->to_array(false, MYSQLI_NUM, false));
|
||||
$limit = $state === ReaperState::NEVER ? MAX_NEVER_SEEDED_PER_RUN : MAX_UNSEEDED_PER_RUN;
|
||||
$torrentIds = [];
|
||||
$result = [];
|
||||
foreach ($final as $userId => $ids) {
|
||||
$result[$userId] = $ids;
|
||||
array_push($torrentIds, ...$ids);
|
||||
if (count($torrentIds) > $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$torrentIds) {
|
||||
self::$db->commit();
|
||||
return [];
|
||||
}
|
||||
|
||||
// mark the entries as having generated the second warning
|
||||
self::$db->prepared_query("
|
||||
UPDATE torrent_unseeded SET
|
||||
notify = ?
|
||||
WHERE torrent_id IN (" . placeholders($torrentIds) . ")
|
||||
", ReaperNotify::FINAL->value, ...$torrentIds
|
||||
);
|
||||
self::$db->commit();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function removeNeverSeeded(): int {
|
||||
return $this->remove(
|
||||
reaperList: $this->reaperList(ReaperState::NEVER, REMOVE_NEVER_SEEDED_HOUR),
|
||||
reason: 'inactivity (never seeded)',
|
||||
template: 'notification/removed-never-seeded.twig',
|
||||
);
|
||||
}
|
||||
|
||||
public function removeUnseeded(): int {
|
||||
return $this->remove(
|
||||
reaperList: $this->reaperList(ReaperState::UNSEEDED, REMOVE_UNSEEDED_HOUR),
|
||||
reason: 'inactivity (unseeded)',
|
||||
template: 'notification/removed-unseeded.twig',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Once we have listed some torrents in the torrent_unseeded table, we
|
||||
* no longer need to refer to the original torrent upload date or last
|
||||
* action date. That they have been here for enough time is sufficient
|
||||
* grounds to reap them.
|
||||
*
|
||||
* Return a hash of user id/torrent ids that are no longer/have never
|
||||
* been announced and must be removed.
|
||||
*/
|
||||
public function reaperList(ReaperState $state, int $interval): array {
|
||||
self::$db->prepared_query("
|
||||
SELECT t.UserID AS user_id,
|
||||
group_concat(t.ID ORDER BY t.ID) AS ids
|
||||
FROM torrents t
|
||||
INNER JOIN torrent_unseeded tu ON (tu.torrent_id = t.ID)
|
||||
WHERE tu.unseeded_date < now() - INTERVAL ? HOUR
|
||||
AND tu.notify = ?
|
||||
AND tu.state = ?
|
||||
GROUP BY t.UserID
|
||||
", $interval, ReaperNotify::FINAL->value, $state->value
|
||||
);
|
||||
return $this->expand(NOTIFY_REAPER_MAX_PER_USER, self::$db->to_array(false, MYSQLI_NUM, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* we have a list of users with lists of torrents to remove. Let's do this!
|
||||
*/
|
||||
protected function remove(array $reaperList, string $reason, string $template): int {
|
||||
$removed = 0;
|
||||
$userList = [];
|
||||
foreach ($reaperList as $userId => $torrentIds) {
|
||||
$notes = [];
|
||||
foreach ($torrentIds as $torrentId) {
|
||||
$torrent = $this->torMan->findById($torrentId);
|
||||
if (is_null($torrent)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the snatchers of this torrent
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO log (Message, Time)
|
||||
VALUES " . placeholders($messages, '(?, now())')
|
||||
, ...$messages
|
||||
SELECT DISTINCT xs.uid
|
||||
FROM xbt_snatched xs
|
||||
WHERE xs.fid = ?
|
||||
AND xs.uid != ?
|
||||
", $torrentId, $torrent->uploaderId()
|
||||
);
|
||||
foreach (self::$db->collect(0, false) as $snatcherId) {
|
||||
$snatcherId = (int)$snatcherId;
|
||||
if (!isset($userList[$snatcherId])) {
|
||||
$userList[$snatcherId] = [];
|
||||
}
|
||||
$userList[$snatcherId][] = $torrent->group();
|
||||
}
|
||||
|
||||
// grab the fields we want to use in the report before we blow the torrent away
|
||||
$note = [
|
||||
'id' => $torrent->id(),
|
||||
'infohash' => $torrent->infohash(),
|
||||
'name' => $torrent->name(),
|
||||
'tgroup' => $torrent->group(),
|
||||
];
|
||||
[$success, $message] = $torrent->remove($userId, $reason, -1);
|
||||
if ($success) {
|
||||
$removed++;
|
||||
$notes[] = $note;
|
||||
}
|
||||
}
|
||||
|
||||
if ($notes) {
|
||||
self::$db->prepared_query("
|
||||
INSERT INTO log (Message) VALUES " . placeholders($notes, '(?)'),
|
||||
...array_map(fn($t) => "Torrent {$t['id']} ({$t['name']}) ({$t['infohash']}) was deleted for $reason", $notes
|
||||
)
|
||||
);
|
||||
$total = count($notes);
|
||||
$user = $this->userMan->findById($userId);
|
||||
if ($user?->isEnabled()) {
|
||||
$this->userMan->sendPM($userId, 0,
|
||||
"$total of your uploads " . ($total == 1 ? 'has' : 'have') . " been deleted for $reason",
|
||||
self::$twig->render($template, [
|
||||
'notes' => $notes,
|
||||
'user' => $user,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now inform the snatchers of all the torrents that were reaped during this run
|
||||
foreach ($userList as $userId => $torrentList) {
|
||||
$user = $this->userMan->findById($userId);
|
||||
if ($user?->isEnabled()) {
|
||||
$total = count($torrentList);
|
||||
$this->userMan->sendPM($userId, 0,
|
||||
"$total of your snatches " . ($total == 1 ? 'was' : 'were') . " deleted for inactivity",
|
||||
self::$twig->render('notification/removed-unseeded-snatch.twig', [
|
||||
'list' => $torrentList,
|
||||
'user' => $user,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
return $removed;
|
||||
}
|
||||
|
||||
public function expand(int $limit, array $list): array {
|
||||
$result = [];
|
||||
foreach ($list as [$userId, $ids]) {
|
||||
$result[$userId] = array_map('intval', array_slice(explode(',', $ids), 0, $limit));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return array of [torrent_id, user_id, points]
|
||||
*/
|
||||
public function claim(): array {
|
||||
self::$db->begin_transaction();
|
||||
|
||||
// Remove claims from uploads that the owner has begun to reseed
|
||||
self::$db->prepared_query("
|
||||
SELECT SimilarID
|
||||
FROM artists_similar_scores
|
||||
WHERE Score <= 0");
|
||||
$similarIDs = self::$db->collect('SimilarID');
|
||||
DELETE tu, tuc
|
||||
FROM torrent_unseeded tu
|
||||
iNNER JOIN xbt_files_users xfu ON (xfu.fid = tu.torrent_id)
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid AND t.UserID = xfu.uid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = xfu.fid)
|
||||
LEFT JOIN torrent_unseeded_claim tuc USING (torrent_id)
|
||||
WHERE tu.unseeded_date < tls.last_action
|
||||
AND tuc.claim_date IS NULL
|
||||
AND xfu.remaining = 0
|
||||
AND xfu.timespent > 0
|
||||
");
|
||||
|
||||
if ($similarIDs) {
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM artists_similar
|
||||
WHERE SimilarID IN (" . placeholders($similarIDs, '(?)') . ")
|
||||
", ...$similarIDs);
|
||||
$placeholders = placeholders($similarIDs);
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM artists_similar_scores
|
||||
WHERE SimilarID IN ($placeholders)
|
||||
", ...$similarIDs);
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM artists_similar_votes
|
||||
WHERE SimilarID IN ($placeholders)
|
||||
", ...$similarIDs);
|
||||
// Get the snatchers who are seeding uploads for which a claim is open
|
||||
self::$db->prepared_query("
|
||||
SELECT xfu.fid as torrent_id,
|
||||
xfu.timespent,
|
||||
xfu.uid AS user_id
|
||||
FROM xbt_files_users xfu
|
||||
INNER JOIN torrents t ON (t.ID = xfu.fid)
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = xfu.fid)
|
||||
INNER JOIN torrent_unseeded tu ON (tu.torrent_id = xfu.fid)
|
||||
LEFT JOIN torrent_unseeded_claim tuc ON (tuc.torrent_id = xfu.fid AND tuc.user_id = xfu.uid)
|
||||
WHERE tuc.claim_date IS NULL
|
||||
AND tu.unseeded_date < tls.last_action
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM torrent_unseeded_claim tuc_prev
|
||||
WHERE tuc_prev.claim_date IS NOT NULL
|
||||
AND tuc_prev.torrent_id = tuc.torrent_id
|
||||
AND tuc_prev.user_id = tuc.user_id
|
||||
)
|
||||
AND xfu.active = 1
|
||||
AND xfu.remaining = 0
|
||||
ORDER BY torrent_id, xfu.timespent DESC
|
||||
");
|
||||
$seederList = self::$db->to_array(false, MYSQLI_NUM, false);
|
||||
if (empty($seederList)) {
|
||||
self::$db->commit();
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys($torrents);
|
||||
// The first user (who has been seeding the longest) is rewarded, as
|
||||
// well as any other user who reseeds within 15 minutes afterwards
|
||||
$win = [];
|
||||
$saved = [];
|
||||
$prevTorrentId = false;
|
||||
$longestTimeSpent = false;
|
||||
foreach ($seederList as [$torrentId, $timeSpent, $userId]) {
|
||||
if ($longestTimeSpent === false) {
|
||||
$longestTimeSpent = $timeSpent;
|
||||
}
|
||||
if ($torrentId !== $prevTorrentId) {
|
||||
$saved[] = $torrentId;
|
||||
}
|
||||
if ($timeSpent + 900 >= $longestTimeSpent) {
|
||||
$torrent = $this->torMan->findById($torrentId);
|
||||
$user = $this->userMan->findById($userId);
|
||||
if (is_null($torrent) || is_null($user)) {
|
||||
continue;
|
||||
}
|
||||
$win[] = [
|
||||
$torrentId,
|
||||
$userId,
|
||||
$this->notifyWinner($torrent, new \Gazelle\User\Bonus($user)),
|
||||
];
|
||||
}
|
||||
$prevTorrentid = $torrentId;
|
||||
}
|
||||
|
||||
if ($saved) {
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM torrent_unseeded
|
||||
WHERE torrent_id IN (" . placeholders($saved) . ")
|
||||
", ...$saved
|
||||
);
|
||||
// revoke all the remaining claims other seeders had on these uploads
|
||||
// NB: It might be worth keeping these longer for subsequent
|
||||
// arbitration, as it represents a snapshot that cannot be recreated
|
||||
// once the swarm population changes.
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM torrent_unseeded_claim
|
||||
WHERE claim_date IS NULL
|
||||
AND torrent_id IN (" . placeholders($saved) . ")
|
||||
", ...$saved
|
||||
);
|
||||
}
|
||||
|
||||
self::$db->commit();
|
||||
return $win;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PM to the snatchers to thank them for reseeding an upload.
|
||||
* Clear out all the other claims.
|
||||
*/
|
||||
public function notifyWinner(\Gazelle\Torrent $torrent, \Gazelle\User\Bonus $bonus): float {
|
||||
self::$db->prepared_query("
|
||||
UPDATE torrent_unseeded_claim SET
|
||||
claim_date = now()
|
||||
WHERE claim_date IS NULL
|
||||
AND torrent_id = ?
|
||||
AND user_id = ?
|
||||
", $torrent->id(), $bonus->user()->id()
|
||||
);
|
||||
|
||||
$points = REAPER_RESEED_REWARD_FACTOR * $bonus->torrentValue($torrent);
|
||||
$bonus->addPoints($points);
|
||||
$bonus->user()->addStaffNote("Awarded {$points} BP for reseeding [pl]{$torrent->id()}[/pl]")->modify();
|
||||
$this->userMan->sendPM($bonus->user()->id(), 0,
|
||||
"Thank you for reseeding {$torrent->group()->name()}!",
|
||||
self::$twig->render('notification/reseed.twig', [
|
||||
'points' => $points,
|
||||
'torrent' => $torrent,
|
||||
'user' => $bonus->user(),
|
||||
])
|
||||
);
|
||||
return $points;
|
||||
}
|
||||
|
||||
public function claimStats(): array {
|
||||
return array_map('intval', self::$db->rowAssoc("
|
||||
SELECT coalesce(sum(claim_date IS NULL), 0) AS open,
|
||||
coalesce(sum(claim_date IS NOT NULL), 0) AS claimed
|
||||
FROM torrent_unseeded_claim
|
||||
"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Some statistics regarding the state of unseeded/never seeded
|
||||
* uploads.
|
||||
*/
|
||||
public function stats(): array {
|
||||
$stats = [
|
||||
'never_seeded_initial' => 0,
|
||||
'unseeded_initial' => 0,
|
||||
'never_seeded_final' => 0,
|
||||
'unseeded_final' => 0,
|
||||
];
|
||||
self::$db->prepared_query("
|
||||
SELECT notify,
|
||||
state,
|
||||
count(*) as total
|
||||
FROM torrent_unseeded
|
||||
GROUP BY notify, state
|
||||
");
|
||||
$results = self::$db->to_array(false, MYSQLI_ASSOC, false);
|
||||
foreach($results as $r) {
|
||||
if ($r['state'] == ReaperState::UNSEEDED->value && $r['notify'] == ReaperNotify::INITIAL->value) {
|
||||
$stats['unseeded_initial'] = $r['total'];
|
||||
}
|
||||
elseif ($r['state'] == ReaperState::UNSEEDED->value && $r['notify'] == ReaperNotify::FINAL->value) {
|
||||
$stats['unseeded_final'] = $r['total'];
|
||||
}
|
||||
elseif ($r['state'] == ReaperState::NEVER->value && $r['notify'] == ReaperNotify::INITIAL->value) {
|
||||
$stats['never_seeded_initial'] = $r['total'];
|
||||
}
|
||||
else {
|
||||
$stats['never_seeded_final'] = $r['total'];
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
public function timeline(): array {
|
||||
self::$db->prepared_query("
|
||||
SELECT date(unseeded_date) as `day`,
|
||||
count(*) as total
|
||||
FROM torrent_unseeded
|
||||
GROUP BY `day`
|
||||
ORDER BY `day` DESC
|
||||
");
|
||||
return self::$db->to_pair('day', 'total', false);
|
||||
}
|
||||
}
|
||||
|
||||
8
app/Torrent/ReaperNotify.php
Normal file
8
app/Torrent/ReaperNotify.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Torrent;
|
||||
|
||||
enum ReaperNotify: string {
|
||||
case INITIAL = 'initial';
|
||||
case FINAL = 'final';
|
||||
}
|
||||
15
app/Torrent/ReaperState.php
Normal file
15
app/Torrent/ReaperState.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Gazelle\Torrent;
|
||||
|
||||
enum ReaperState: string {
|
||||
case NEVER = 'never';
|
||||
case UNSEEDED = 'unseeded';
|
||||
|
||||
public function notifyAttr(): string {
|
||||
return match($this) {
|
||||
ReaperState::NEVER => 'no-pm-unseeded-upload',
|
||||
ReaperState::UNSEEDED => 'no-pm-unseeded-snatch',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,6 @@ class User extends BaseObject {
|
||||
ui.RestrictedForums,
|
||||
ui.SiteOptions,
|
||||
ui.SupportFor,
|
||||
ui.UnseededAlerts,
|
||||
ui.Warned,
|
||||
uls.Uploaded,
|
||||
uls.Downloaded,
|
||||
@@ -977,6 +976,10 @@ class User extends BaseObject {
|
||||
DELETE FROM user_flt WHERE user_id = ?
|
||||
", $this->id
|
||||
);
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM user_has_attr WHERE UserID = ?
|
||||
", $this->id
|
||||
);
|
||||
self::$db->prepared_query("
|
||||
DELETE FROM users_leech_stats WHERE UserID = ?
|
||||
", $this->id
|
||||
@@ -1409,10 +1412,6 @@ class User extends BaseObject {
|
||||
return $change;
|
||||
}
|
||||
|
||||
public function notifyUnseeded(): bool {
|
||||
return $this->info()['UnseededAlerts'] == '1';
|
||||
}
|
||||
|
||||
public function notifyDeleteSeeding(): bool {
|
||||
return $this->info()['NotifyOnDeleteSeeding'] == '1';
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ services:
|
||||
- mysqld
|
||||
- --group-concat-max-len=1048576
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_0900_ai_ci
|
||||
- --userstat=on
|
||||
- --sql-mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
|
||||
# neither sphinxsearch or ocelot are compatible with the mysql8 caching_sha2_password plugin
|
||||
|
||||
83
docs/09-UnseededPurge.txt
Normal file
83
docs/09-UnseededPurge.txt
Normal file
@@ -0,0 +1,83 @@
|
||||
From: Spine
|
||||
To: Operators
|
||||
Date: 2022-11-20
|
||||
Subject: Orpheus Development Papers #9 - Unseeded Purges
|
||||
Version: 1
|
||||
|
||||
For various reasons, some uploads will end up in an unseeded state, either
|
||||
because the uploader forgets to load it into their client so no-one else can
|
||||
snatch it, or eventually people lose interest and stop seeding, hard disks
|
||||
fail, and the upload becomes unavailable.
|
||||
|
||||
There is a tension to be resolved between a site which appears to have a
|
||||
large catalogue but full of tombstones, and a site with a smaller catalogue
|
||||
with pretty much everything is available. Different sites resolve the issue
|
||||
using different policies. The design described below can handle most use
|
||||
cases.
|
||||
|
||||
The original Gazelle design removed tombstones but allowed little margin for
|
||||
error. There was an initial alert sent for uploads that had been unseeded
|
||||
for a specified amount of time, which was run as an hourly scheduled task
|
||||
that caught all the uploads that had been unseeded for between N and N+1
|
||||
hours. If the scheduler did not make the run in that hour, no notification
|
||||
alerts were sent and the upload was removed afterwards with no prior
|
||||
warning.
|
||||
|
||||
This became a massive problem if the reaper has to be stopped for an
|
||||
extended period of time for whatever reason (hardware crash, server move,
|
||||
DDoS or other instabilities). When things return to normal, starting up the
|
||||
reaper would immediately nuke a large number of uploads that might otherwise
|
||||
have been saved if people received an advance warning to do something about
|
||||
it.
|
||||
|
||||
To get around this problem. the approach to handling unseeded uploads on
|
||||
Orpheus is to use a table to register where an upload is in its unseeded
|
||||
state. A row is inserted into the `torrent_unseeded` table when it is deemed
|
||||
to have been unseeded for too long. From there, the row will either be
|
||||
removed when the upload is seeded once more, or removed along with the purge
|
||||
of the upload. Unseeded and "never seeded" uploads are managed using
|
||||
different schedules.
|
||||
|
||||
The Orpheus reaper sends out an initial alert well in advance, to give
|
||||
people time to even notice the message and do something about it. A second
|
||||
alert is sent just before the upload is removed.
|
||||
|
||||
To leave tombstones in the catalogue, simply disable the scheduled reaper
|
||||
task.
|
||||
|
||||
An unseeded upload is identified by the `torrents_leech_stats.last_action`
|
||||
column. If it is `NULL`, the upload was never seeded. Once the grace period
|
||||
since creation (based on `torrents.Time`) has passed, a "never seeded" row
|
||||
is inserted. If there is a value for `last_action` and the grace period for
|
||||
unseeded uploads has passed, then an "unseeded" row is inserted.
|
||||
|
||||
The current timestamp is recorded when the row is inserted. The subsequent
|
||||
actions (second alert, ultimate removal) are based on this. If a member
|
||||
pleads for extra time to get their act together, this can be done by
|
||||
updating the `unseeded_date` to a time in the future (e.g. `now() + INTERVAL
|
||||
2 WEEK`. There is no point deleting the row: it will be inserted again
|
||||
during the next scheduler run.
|
||||
|
||||
Snatchers are also pinged to see if they can reseed the upload. The first
|
||||
person to do so can "claim" the reseed and receive a reward of bonus points.
|
||||
This is to obviate the risk of a snatcher letting the upload be removed and
|
||||
then reuploaded under their own name. To prevent abuse, a given upload may
|
||||
only be claimed once by the same person. The `torrent_unseeded_claim` table
|
||||
can be reviewed to see if an individual is making excessive use of the
|
||||
feature.
|
||||
|
||||
Unseeded uploads are removed after `REMOVE_UNSEEDED_HOUR` hours (28 days by
|
||||
default). "Never seeded" uploads are removed after
|
||||
`REMOVE_NEVER_SEEDED_HOUR` hours (3 days by default). The final alert timer
|
||||
is most easily defined by calculating an interval prior to the removal
|
||||
deadline.
|
||||
|
||||
To spread the load following a long pause, no more than 100 uploads per
|
||||
user, and no more than 1000 users are considered in a single run. This helps
|
||||
produce a breadth-first search, rather than depth first. There is also a
|
||||
maximum cap of total uploads to process per run.
|
||||
|
||||
Remember: the `torrent_unseeded` represents the current state of unseeded
|
||||
notifications. No real harm will happen if the table is truncated: all this
|
||||
will do is reinitialize the process. The current stats can be viewed in the
|
||||
Torrent Stats toolbox.
|
||||
@@ -459,15 +459,42 @@ defined('IGNORE_PAGE_MAX_TIME') or define('IGNORE_PAGE_MAX_TIME', ['top10']);
|
||||
// It is easier to specify the first and second unseeded notifications
|
||||
// in terms of the interval remaining until reaping.
|
||||
|
||||
define('MAX_NEVER_SEEDED_PER_RUN', 1000);
|
||||
define('REMOVE_NEVER_SEEDED_HOUR', 72);
|
||||
define('NOTIFY_NEVER_SEEDED_INITIAL_HOUR', 8); // 8 hours after upload
|
||||
define('NOTIFY_NEVER_SEEDED_FINAL_HOUR', REMOVE_NEVER_SEEDED_HOUR - 24);
|
||||
defined('MAX_NEVER_SEEDED_PER_RUN') or define('MAX_NEVER_SEEDED_PER_RUN', 4000);
|
||||
defined('REMOVE_NEVER_SEEDED_HOUR') or define('REMOVE_NEVER_SEEDED_HOUR', 72);
|
||||
defined('NOTIFY_NEVER_SEEDED_INITIAL_HOUR') or define('NOTIFY_NEVER_SEEDED_INITIAL_HOUR', 8); // 8 hours after upload
|
||||
defined('NOTIFY_NEVER_SEEDED_FINAL_HOUR') or define('NOTIFY_NEVER_SEEDED_FINAL_HOUR', REMOVE_NEVER_SEEDED_HOUR - 24);
|
||||
|
||||
define('MAX_UNSEEDED_PER_RUN', 1000);
|
||||
define('REMOVE_UNSEEDED_HOUR', 24 * 28); // 28 days
|
||||
define('NOTIFY_UNSEEDED_INITIAL_HOUR', REMOVE_UNSEEDED_HOUR - (24 * 10));
|
||||
define('NOTIFY_UNSEEDED_FINAL_HOUR', REMOVE_UNSEEDED_HOUR - (24 * 3));
|
||||
defined('MAX_UNSEEDED_PER_RUN') or define('MAX_UNSEEDED_PER_RUN', 4000);
|
||||
defined('REMOVE_UNSEEDED_HOUR') or define('REMOVE_UNSEEDED_HOUR', 24 * 30); // 30 days
|
||||
defined('NOTIFY_UNSEEDED_INITIAL_HOUR') or define('NOTIFY_UNSEEDED_INITIAL_HOUR', REMOVE_UNSEEDED_HOUR - (24 * 10)); // 10 days before
|
||||
defined('NOTIFY_UNSEEDED_FINAL_HOUR') or define('NOTIFY_UNSEEDED_FINAL_HOUR', REMOVE_UNSEEDED_HOUR - (24 * 3)); // 3 days before
|
||||
|
||||
// There is a single task that handles the various phases of reaping.
|
||||
// In normal operations you want to perform all of the phases, but in certain
|
||||
// circumstances you might wish to suspend one phase or another.
|
||||
// If you think you need to suspend all the phases, disable the task in the
|
||||
// scheduler instead.
|
||||
|
||||
// Award winners who have begun to seed unseeded uploads
|
||||
defined('REAPER_TASK_CLAIM') or define('REAPER_TASK_CLAIM', true);
|
||||
|
||||
// Look for new never seeded or unseeded uploads to process
|
||||
defined('REAPER_TASK_NOTIFY') or define('REAPER_TASK_NOTIFY', true);
|
||||
|
||||
// Reap any expired unseeded torrents
|
||||
defined('REAPER_TASK_REMOVE_UNSEEDED') or define('REAPER_TASK_REMOVE_UNSEEDED', true);
|
||||
|
||||
// Reap any expired never seeded torrents
|
||||
defined('REAPER_TASK_REMOVE_NEVER_SEEDED') or define('REAPER_TASK_REMOVE_NEVER_SEEDED', true);
|
||||
|
||||
// How many notifications can a user receive in a single message
|
||||
defined('NOTIFY_REAPER_MAX_PER_USER') or define('NOTIFY_REAPER_MAX_PER_USER', 200);
|
||||
|
||||
// How many users will be notified in a single run?
|
||||
defined('NOTIFY_REAPER_MAX_NOTIFICATION') or define('NOTIFY_REAPER_MAX_NOTIFICATION', 2500);
|
||||
|
||||
// How much is the BP reward scaled up for a reseed?
|
||||
defined('REAPER_RESEED_REWARD_FACTOR') or define('REAPER_RESEED_REWARD_FACTOR', 1.25);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Source flag settings
|
||||
|
||||
@@ -381,10 +381,14 @@ function add_json_info($Json) {
|
||||
return $Json;
|
||||
}
|
||||
|
||||
function dump($thing) {
|
||||
function dump($thing): void {
|
||||
echo "<pre>" . json_encode($thing, JSON_PRETTY_PRINT) . "</pre>";
|
||||
}
|
||||
|
||||
function show(mixed $data): void {
|
||||
echo json_encode($data, JSON_PRETTY_PRINT) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that unserializes an array, and then if the unserialization fails,
|
||||
* it'll then return an empty array instead of a null or false which will break downstream
|
||||
|
||||
94
misc/phinx/migrations/20230225000000_torrent_reaper.php
Normal file
94
misc/phinx/migrations/20230225000000_torrent_reaper.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class TorrentReaper extends AbstractMigration {
|
||||
public function up(): void {
|
||||
$this->table('torrent_unseeded', ['id' => false, 'primary_key' => ['torrent_unseeded_id']])
|
||||
->addColumn('torrent_unseeded_id', 'integer', ['identity' => true])
|
||||
->addColumn('torrent_id', 'integer')
|
||||
->addColumn('state', 'enum', ['default' => 'never', 'values' => ['never', 'unseeded']])
|
||||
->addColumn('notify', 'enum', ['default' => 'initial', 'values' => ['initial', 'final']])
|
||||
->addColumn('unseeded_date', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
|
||||
->addForeignKey('torrent_id', 'torrents', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addIndex(['torrent_id'], ['name' => 'tu_t_uidx', 'unique' => true])
|
||||
->addIndex(['unseeded_date'], ['name' => 'tu_ud_idx'])
|
||||
->create();
|
||||
|
||||
$this->table('torrent_unseeded_claim', ['id' => false, 'primary_key' => ['torrent_id', 'user_id']])
|
||||
->addColumn('torrent_id', 'integer')
|
||||
->addColumn('user_id', 'integer')
|
||||
->addColumn('claim_date', 'datetime', ['null' => true])
|
||||
->addForeignKey('torrent_id', 'torrents', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('user_id', 'users_main', 'ID', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addIndex(['user_id'], ['name' => 'tuc_u_idx'])
|
||||
->create();
|
||||
|
||||
$this->getQueryBuilder()
|
||||
->delete('periodic_task')
|
||||
->where(fn($w) => $w->in('classname', [
|
||||
'DeleteNeverSeededTorrents',
|
||||
'DeleteUnseededTorrents',
|
||||
'NotifyNonseedingUploaders',
|
||||
])
|
||||
)
|
||||
->execute();
|
||||
|
||||
$this->table('periodic_task')
|
||||
->insert([
|
||||
'name' => 'Torrent Reaper',
|
||||
'classname' => 'Reaper',
|
||||
'description' => 'Process torrents that are unseeded/were never seeded',
|
||||
'period' => 4 * 3600,
|
||||
'is_enabled' => 0,
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->table('user_attr')->insert(
|
||||
[
|
||||
['Name' => 'no-pm-unseeded-snatch', 'Description' => 'Do not receive system PMs on imminent removal of an unseeded snatch'],
|
||||
['Name' => 'no-pm-unseeded-upload', 'Description' => 'Do not receive system PMs on imminent removal of an unseeded upload'],
|
||||
]
|
||||
)->save();
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$this->table('torrent_unseeded')->drop()->update();
|
||||
$this->table('torrent_unseeded_claim')->drop()->update();
|
||||
|
||||
$this->getQueryBuilder()
|
||||
->delete('periodic_task')
|
||||
->where(fn($w) => $w->in('classname', [
|
||||
'Reaper',
|
||||
])
|
||||
)
|
||||
->execute();
|
||||
|
||||
$this->table('periodic_task')
|
||||
->insert([
|
||||
[
|
||||
'name' => 'Torrent Reaper - Never Seeded',
|
||||
'classname' => 'DeleteNeverSeededTorrents',
|
||||
'description' => 'Deletes torrents that were never seeded',
|
||||
'period' => 3600 * 24
|
||||
],
|
||||
[
|
||||
'name' => 'Torrent Reaper - Unseeded',
|
||||
'classname' => 'DeleteUnseededTorrents',
|
||||
'description' => 'Deletes unseeded torrents',
|
||||
'period' => 3600 * 24
|
||||
],
|
||||
[
|
||||
'name' => 'Unseeded Notifications',
|
||||
'classname' => 'NotifyNonseedingUploaders',
|
||||
'description' => 'Sends warnings for unseeded torrents',
|
||||
'period' => 3600 * 24 * 7
|
||||
],
|
||||
])
|
||||
->save();
|
||||
|
||||
$this->execute("DELETE FROM user_attr where Name IN ('no-pm-unseeded-snatch', 'no-pm-unseeded-upload')");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class UsersInfoDropUnseededAlerts extends AbstractMigration {
|
||||
public function up(): void {
|
||||
$this->table('users_info')->removeColumn('UnseededAlerts')->save();
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
$this->table('users_info')->addColumn('UnseededAlerts', 'enum', [
|
||||
'null' => false,
|
||||
'default' => '0',
|
||||
'limit' => 1,
|
||||
'values' => ['0', '1'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -3540,11 +3540,6 @@ parameters:
|
||||
count: 1
|
||||
path: ../app/Torrent.php
|
||||
|
||||
-
|
||||
message: "#^Method Gazelle\\\\Torrent\\\\Reaper\\:\\:deleteDeadTorrents\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: ../app/Torrent/Reaper.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method addFlag\\(\\) on bool\\|Gazelle\\\\TorrentAbstract\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -31,6 +31,10 @@ parameters:
|
||||
- FEATURE_EMAIL_REENABLE
|
||||
- LASTFM_API_KEY
|
||||
- OPEN_REGISTRATION
|
||||
- REAPER_TASK_CLAIM
|
||||
- REAPER_TASK_NOTIFY
|
||||
- REAPER_TASK_REMOVE_NEVER_SEEDED
|
||||
- REAPER_TASK_REMOVE_UNSEEDED
|
||||
- RECOVERY
|
||||
- RECOVERY_AUTOVALIDATE
|
||||
- RECOVERY_DB
|
||||
@@ -54,6 +58,7 @@ parameters:
|
||||
message: '/^Property [^:]+::\$\w+ type has no value type specified in iterable type array\.$/'
|
||||
paths:
|
||||
- ../app/*
|
||||
- ../tests/phpunit/*
|
||||
-
|
||||
message: '/^Variable \$(?:Cache|DB|Debug|SessionID|Twig|Viewer) might not be defined\.$/'
|
||||
paths:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"lint:css": "stylelint \"sass/**/*.scss\" || exit 0",
|
||||
"lint:css-checkstyle": "stylelint \"sass/**/*.scss\" --custom-formatter ./node_modules/stylelint-checkstyle-formatter/index.js || exit 0",
|
||||
"lint:php:internal": "find . -path './vendor' -prune -o -path ./node_modules -prune -o -path './.docker' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 -P4 php -l -n | (! grep -v \"No syntax errors detected\" )",
|
||||
"lint:php:phpcs": "vendor/bin/phpcs --report-width 1000",
|
||||
"lint:php:phpcs": "vendor/bin/phpcs -p --report-width 1000",
|
||||
"lint:php": "yarn lint:php:internal && yarn lint:php:phpcs",
|
||||
"lint:php:fix": "./.bin/phpcbf",
|
||||
"pre-commit": "yarn lint:php:fix",
|
||||
|
||||
@@ -7,4 +7,5 @@ if (!$Viewer->permitted('site_view_flow')) {
|
||||
echo $Twig->render('admin/stats/torrent.twig', [
|
||||
'torr_stat' => new Gazelle\Stats\Torrent,
|
||||
'user_stat' => new Gazelle\Stats\Users,
|
||||
'reaper' => new Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User),
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
$userMan = new Gazelle\Manager\User;
|
||||
|
||||
$user = $userMan->findById((int)($_REQUEST['id'] ?? 0));
|
||||
$user = $userMan->findById(($_REQUEST['id'] ?? '') === 'me' ? $Viewer->id() : (int)($_REQUEST['id'] ?? 0));
|
||||
if (is_null($user)) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,6 @@ $Options['DisablePMAvatars'] = (!empty($_POST['disablepmavatars']) ? 1 : 0);
|
||||
$Options['ListUnreadPMsFirst'] = (!empty($_POST['list_unread_pms_first']) ? 1 : 0);
|
||||
$Options['ShowSnatched'] = (!empty($_POST['showsnatched']) ? 1 : 0);
|
||||
$Options['DisableAutoSave'] = (!empty($_POST['disableautosave']) ? 1 : 0);
|
||||
$Options['AcceptFL'] = (!empty($_POST['acceptfltoken']) ? 1 : 0);
|
||||
$Options['NoVoteLinks'] = (!empty($_POST['novotelinks']) ? 1 : 0);
|
||||
$Options['CoverArt'] = (int)!empty($_POST['coverart']);
|
||||
$Options['ShowExtraCovers'] = (int)!empty($_POST['show_extra_covers']);
|
||||
@@ -206,7 +205,6 @@ if ($Viewer->permitted('site_advanced_search')) {
|
||||
|
||||
// These are all enums of '0' or '1'
|
||||
$DownloadAlt = isset($_POST['downloadalt']) ? '1' : '0';
|
||||
$UnseededAlerts = isset($_POST['unseededalerts']) ? '1' : '0';
|
||||
$NotifyOnDeleteSeeding = (!empty($_POST['notifyondeleteseeding']) ? '1' : '0');
|
||||
$NotifyOnDeleteSnatched = (!empty($_POST['notifyondeletesnatched']) ? '1' : '0');
|
||||
$NotifyOnDeleteDownloaded = (!empty($_POST['notifyondeletedownloaded']) ? '1' : '0');
|
||||
@@ -245,8 +243,6 @@ if (is_null($OldFMUsername) && $LastFMUsername !== '') {
|
||||
$Cache->delete_value("lastfm_username_$userId");
|
||||
}
|
||||
|
||||
$user->toggleAcceptFL($Options['AcceptFL']);
|
||||
|
||||
/* transform
|
||||
* 'notifications_News_popup'
|
||||
* 'notifications_Blog_popup'
|
||||
@@ -270,6 +266,9 @@ foreach ($notification as $n) {
|
||||
}
|
||||
(new Gazelle\User\Notification($user))->save($settings, ["PushKey" => $_POST['pushkey']], $_POST['pushservice'], $_POST['pushdevice']);
|
||||
|
||||
$user->toggleAcceptFL(!empty($_POST['acceptfltoken']));
|
||||
$user->toggleAttr('no-pm-unseeded-snatch', empty($_POST['notifyonunseededsnatch']));
|
||||
$user->toggleAttr('no-pm-unseeded-upload', empty($_POST['notifyonunseededupload']));
|
||||
$user->toggleAttr('hide-vote-recent', empty($_POST['pattr_hide_vote_recent']));
|
||||
$user->toggleAttr('hide-vote-history', empty($_POST['pattr_hide_vote_history']));
|
||||
$user->toggleAttr('admin-error-reporting', isset($_POST['error_reporting']));
|
||||
@@ -286,7 +285,6 @@ INNER JOIN users_info AS i ON (m.ID = i.UserID) SET
|
||||
i.Info = ?,
|
||||
i.InfoTitle = ?,
|
||||
i.DownloadAlt = ?,
|
||||
i.UnseededAlerts = ?,
|
||||
i.NotifyOnDeleteSeeding = ?,
|
||||
i.NotifyOnDeleteSnatched = ?,
|
||||
i.NotifyOnDeleteDownloaded = ?,
|
||||
@@ -301,7 +299,6 @@ $Params = [
|
||||
$_POST['info'],
|
||||
$_POST['profile_title'],
|
||||
$DownloadAlt,
|
||||
$UnseededAlerts,
|
||||
$NotifyOnDeleteSeeding,
|
||||
$NotifyOnDeleteSnatched,
|
||||
$NotifyOnDeleteDownloaded,
|
||||
|
||||
@@ -138,6 +138,64 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="thin">
|
||||
<div class="box">
|
||||
<div class="head">Reaper information</div>
|
||||
<div class="pad">
|
||||
<h5>Claim statistics</h5>
|
||||
<table>
|
||||
<tr>
|
||||
<th width="50%">Open</th>
|
||||
<th width="50%">Claimed</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% set stats = reaper.claimStats %}
|
||||
<td>{{ stats.open|number_format }}</td>
|
||||
<td>{{ stats.claimed|number_format }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
<h5>Unseeded statistics</h5>
|
||||
<table>
|
||||
<tr>
|
||||
<th width="25%">Never seeded initial</th>
|
||||
<th width="25%">Never seeded final</th>
|
||||
<th width="25%">Unseeded initial</th>
|
||||
<th width="25%">Unseeded final</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% set stats = reaper.stats %}
|
||||
<td>{{ stats.never_seeded_initial|number_format }}</td>
|
||||
<td>{{ stats.never_seeded_final|number_format }}</td>
|
||||
<td>{{ stats.unseeded_initial|number_format }}</td>
|
||||
<td>{{ stats.unseeded_final|number_format }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
<h5>Timeline</h5>
|
||||
<table>
|
||||
<tr>
|
||||
<th width="50%">Date</th>
|
||||
<th width="50%">Total</th>
|
||||
</tr>
|
||||
{% for date, total in reaper.timeline %}
|
||||
<tr>
|
||||
<td>{{ date }}</td>
|
||||
<td>{{ total|number_format }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2">There are no uploads in the reaper pipeline at this time.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ footer() }}
|
||||
|
||||
177
templates/admin/torrent-stats.twig
Normal file
177
templates/admin/torrent-stats.twig
Normal file
@@ -0,0 +1,177 @@
|
||||
<div class="thin">
|
||||
|
||||
<div class="box"><div class="head">Overall Stats</div>
|
||||
<div class="pad">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Total torrents:</td><td class="number_column">{{ stats.torrentCount|number_format }}</td>
|
||||
<td style="padding-left:30px">Mean torrents per user:</td><td class="number_column">{{
|
||||
(stats.torrentCount / stats.totalUsers)|number_format }}</td>
|
||||
<td style="padding-left:30px">Mean files per torrent:</td><td class="number_column">{{
|
||||
(stats.totalFiles / stats.torrentCount)|number_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total size:</td><td style="vertical-align: top" class="number_column">{{ stats.totalSize()|octet_size }}</td>
|
||||
<td style="padding-left:30px">Mean torrent size:</td><td style="vertical-align: top" class="number_column">{{
|
||||
(stats.totalSize / stats.torrentCount)|octet_size }}</td>
|
||||
<td style="padding-left:30px">Mean filesize:</td><td style="vertical-align: top" class="number_column">{{
|
||||
(stats.totalSize / stats.totalFiles)|octet_size }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total files:</td><td class="number_column">{{ stats.totalFiles|number_format }}</td>
|
||||
<td colspan="4"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box"><div class="head">Timeline of Unseeded Uploads Removal</div>
|
||||
<div class="pad">
|
||||
<table>
|
||||
<tr>
|
||||
<td style="width: 25%">Initial unseeded:</td><td style="width: 25%" class="number_column">{{ unseeded.unseeded_initial|number_format }}</td>
|
||||
<td style="width: 25%; padding-left:30px">Final unseeded:</td><td style="width: 25%" class="number_column">{{ unseeded.unseeded_final|number_format }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Initial never seeded:</td><td class="number_column">{{ unseeded.never_seeded_initial|number_format }}</td>
|
||||
<td style="padding-left:30px">Final never seeded:</td><td class="number_column">{{ unseeded.never_seeded_final|number_format }}</td>
|
||||
</tr>
|
||||
{% for day, total in timeline %}
|
||||
{% if loop.first %}
|
||||
<td style="width: 25%">Open claims:</td><td style="width: 25%" class="number_column">{{ claim.open|number_format }}</td>
|
||||
{% else %}
|
||||
<td style="width: 50%" colspan="2"> </td>
|
||||
{% endif %}
|
||||
{% if loop.first %}
|
||||
<td style="width: 50%" colspan="2">Upcoming removals by date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Validated claims:</td><td class="number_column">{{ claim.claimed|number_format }}</td>
|
||||
{% endif %}
|
||||
<td style="width: 25%; padding-left:30px">{{ day }}</td>
|
||||
<td style="width: 25%; padding-left:30px" class="number_column">{{ total|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if timeline|length == 0 %}
|
||||
<tr>
|
||||
<td style="width: 25%;">Open claims:</td><td style="width: 25%;" class="number_column">{{ claim.open|number_format }}</td>
|
||||
<td style="width: 50%; padding-left:30px" colspan="2">Upcoming removals by date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Validated claims:</td><td class="number_column">{{ claim.claimed|number_format }}</td>
|
||||
<td style="width: 50%" colspan="2"> </td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box"><div class="head">Upload Frequency</div>
|
||||
<div class="pad">
|
||||
<table>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Today</th>
|
||||
<th>This week</th>
|
||||
<th>Per day this week</th>
|
||||
<th>This month</th>
|
||||
<th>Per day this month</th>
|
||||
<th>This quarter</th>
|
||||
<th>Per day this quarter</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Torrents</th>
|
||||
<td class="number_column">{{ stats.amount('day')|number_format }}</td>
|
||||
<td class="number_column">{{ stats.amount('week')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.amount('week') / 7)|number_format }}</td>
|
||||
<td class="number_column">{{ stats.amount('month')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.amount('month') / 30)|number_format }}</td>
|
||||
<td class="number_column">{{ stats.amount('quarter')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.amount('quarter') / 120)|number_format }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Size</th>
|
||||
<td class="number_column">{{ stats.size('day')|octet_size }}</td>
|
||||
<td class="number_column">{{ stats.size('week')|octet_size }}</td>
|
||||
<td class="number_column">{{ (stats.size('week') / 7)|octet_size }}</td>
|
||||
<td class="number_column">{{ stats.size('month')|octet_size }}</td>
|
||||
<td class="number_column">{{ (stats.size('month') / 30)|octet_size }}</td>
|
||||
<td class="number_column">{{ stats.size('quarter')|octet_size }}</td>
|
||||
<td class="number_column">{{ (stats.size('quarter') / 120)|octet_size }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Files</th>
|
||||
<td class="number_column">{{ stats.files('day')|number_format }}</td>
|
||||
<td class="number_column">{{ stats.files('week')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.files('week') / 7)|number_format }}</td>
|
||||
<td class="number_column">{{ stats.files('month')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.files('month') / 30)|number_format }}</td>
|
||||
<td class="number_column">{{ stats.files('quarter')|number_format }}</td>
|
||||
<td class="number_column">{{ (stats.files('quarter') / 120)|number_format }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box"><div class="head">Content Analysis</div>
|
||||
<div class="pad">
|
||||
<table>
|
||||
<tr>
|
||||
<th width="33%">Formats</th>
|
||||
<th width="33%">Media</th>
|
||||
<th width="34%">Categories</th>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
<td style="vertical-align: top;"><table>
|
||||
{% for f in stats.format %}
|
||||
<tr><td>{{ f.0|default('<i>Grand</i>')|raw }}</td>
|
||||
<td>{{ f.1|default('<i>Total</i>')|raw }}</td>
|
||||
<td class="number_column">{{ f.2|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table></td>
|
||||
|
||||
<td style="vertical-align: top;"><table>
|
||||
{% for m in stats.media %}
|
||||
<tr><td>{{ m.0|default('<i>Total</i>')|raw }}</td>
|
||||
<td class="number_column">{{ m.1|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table></td>
|
||||
|
||||
<td style="vertical-align: top;"><table>
|
||||
{% for c in stats.category %}
|
||||
<tr><td>{{ constant('CATEGORY')[c.0 - 1] }}</td>
|
||||
<td class="number_column">{{ c.1|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table></td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<th width="33%">Added in last month</th>
|
||||
<th width="33%"> </th>
|
||||
<th width="34%"> </th>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
<td style="vertical-align: top;"><table>
|
||||
{% for f in stats.formatMonth %}
|
||||
<tr><td>{{ f.0|default('<i>Grand</i>')|raw }}</td>
|
||||
<td>{{ f.1|default('<i>Total</i>')|raw }}</td>
|
||||
<td class="number_column">{{ f.2|number_format }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table></td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
14
templates/notification/removed-never-seeded.twig
Normal file
14
templates/notification/removed-never-seeded.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{%- set total = notes|length -%}
|
||||
{%- set many = total > 1 -%}
|
||||
Dear {{ user.username }},
|
||||
|
||||
{% if many %}{{ total|number_format }}{% else %}One%{% endif %} of your recent uploads {% if many %}have{% else %}has{%
|
||||
endif %} been removed because they were never announced to the tracker. Assuming {% if many %}they{% else %}it{%
|
||||
endif %} didn't break any rules (we hope), please feel free to re-upload {% if many %}them{% else %}it{% endif %}.
|
||||
|
||||
The following upload{{ total|plural }} {% if many %}were{% else %}was{% endif %} removed:
|
||||
{% for n in notes %}
|
||||
[*] [url=torrents.php?id={{ n.tgroup.id }}]{{ n.name }}[/url]
|
||||
{% endfor %}
|
||||
|
||||
Remember, your uploads earn bonus points on {{ constant('SITE_NAME') }}.
|
||||
16
templates/notification/removed-unseeded-snatch.twig
Normal file
16
templates/notification/removed-unseeded-snatch.twig
Normal file
@@ -0,0 +1,16 @@
|
||||
{%- set total = notes|length -%}
|
||||
{%- set many = total > 1 -%}
|
||||
Dear {{ user.username }},
|
||||
|
||||
The following {% if many %}uploads were{% else %}upload was{% endif %} removed {#
|
||||
-#} because nobody was seeding {% if many %}them{% else %}it{% endif %}. If you still {#
|
||||
-#} have the files (and assuming it didn't break any rules), please feel free to {#
|
||||
-#} re-upload it. Keep in mind, though, if you had reseeded it, you would have {#
|
||||
-#} earned more bonus points than if you upload a fresh version.
|
||||
|
||||
The following upload{{ total|plural }} {% if many %}were{% else %}was{% endif %} removed:
|
||||
{% for tgroup in list %}
|
||||
[*] [url=torrents.php?id={{ tgroup.id }}]{{ tgroup.name }}[/url]
|
||||
{% endfor %}
|
||||
|
||||
Remember, seeding uploads earns bonus points on {{ constant('SITE_NAME') }}.
|
||||
14
templates/notification/removed-unseeded.twig
Normal file
14
templates/notification/removed-unseeded.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
{%- set total = notes|length -%}
|
||||
{%- set many = total > 1 -%}
|
||||
Dear {{ user.username }},
|
||||
|
||||
{% if many %}{{ total|number_format }}{% else %}One%{% endif %} of your uploads {% if many %}have{% else %}has{%
|
||||
endif %} been removed for inactivity (nobody has been seeding them). Assuming {% if many %}they{% else %}it{%
|
||||
endif %} didn't break any rules (we hope), please feel free to re-upload {% if many %}them{% else %}it{% endif %}.
|
||||
|
||||
The following upload{{ total|plural }} {% if many %}were{% else %}was{% endif %} removed:
|
||||
{% for n in notes %}
|
||||
[*] [url=torrents.php?id={{ n.tgroup.id }}]{{ n.name }}[/url]
|
||||
{% endfor %}
|
||||
|
||||
Remember, seeding your uploads earns bonus points on {{ constant('SITE_NAME') }}.
|
||||
7
templates/notification/reseed.twig
Normal file
7
templates/notification/reseed.twig
Normal file
@@ -0,0 +1,7 @@
|
||||
Dear {{ user.username }},
|
||||
|
||||
Thank you for reseeding [pl]{{ torrent.id }}[/pl]. Without your diligence, this upload might have been removed due to inactivity and lost forever!
|
||||
|
||||
Your prompt action has ensured it will remain alive on {{ constant('SITE_NAME') }} for other people to discover and enjoy. To thank you for your efforts, you have been awarded {{ points|number_format }} bonus points. And you will continue to earn bonus points as long as you seed this upload.
|
||||
|
||||
<3
|
||||
20
templates/notification/unseeded-snatch.twig
Normal file
20
templates/notification/unseeded-snatch.twig
Normal file
@@ -0,0 +1,20 @@
|
||||
{%- set total = list|length -%}
|
||||
{%- set many = total > 1 -%}
|
||||
Dear {{ user.username }},
|
||||
|
||||
In the past, you snatched {% if many %}{{ total }} uploads{% else %}an upload{%
|
||||
endif %} that {% if many %}are{% else %}is{% endif %} no longer being seeded, which means {% if many %}they{% else %}it{%
|
||||
endif %} will be removed in the near future. You can save the situation and prevent this from happening by reseeding {%
|
||||
if many %}them{% else %}it{% endif %}. If you are the first to do so, you will be rewarded with extra bonus points for your efforts! Remember, seeding anything you snatched earns bonus points on {{
|
||||
constant('SITE_NAME') }}.
|
||||
|
||||
The exact details vary from client to client, but you will need to initiate a check and see that the torrent{% if many %}s are{% else %} is{% endif %} seeding at 100%. If you still have the files but have lost the torrent file, you may obtain a new copy by saving it from the [[n]DL] link. To check the state of an upload, click {% if many %}one of the links{% else %}the link{% endif %} below and look at the "Last active" time. For more information, please read [url=wiki.php?action=article&id=77]this article[/url].
|
||||
|
||||
Once you have successfully reseeded the upload, you don't need to do anything else. If you meet all the conditions, you will receive a system PM.
|
||||
{% for t in list %}
|
||||
[*] [pl]{{ t }}[/pl]
|
||||
{% endfor -%}
|
||||
|
||||
If you no longer wish to receive these notifications, you may turn them off in [url=/user.php?action=edit&userid=me#notification]your profile settings[/url]. At any time, you may view your [url=/torrents.php?userid=me&type=uploaded-unseeded]unseeded uploads[/url] and your [url=/torrents.php?userid=me&type=snatched-unseeded]unseeded snatches[/url].
|
||||
|
||||
<3
|
||||
33
templates/notification/unseeded.twig
Normal file
33
templates/notification/unseeded.twig
Normal file
@@ -0,0 +1,33 @@
|
||||
{%- set total = list|length -%}
|
||||
{%- set many = total > 1 -%}
|
||||
Dear {{ user.username }},
|
||||
|
||||
You have {% if many %}{{ total }} uploads{% else %}an upload{% endif %} that
|
||||
{%- if never -%}
|
||||
{%- if many %} were{% else %} was{% endif %} uploaded {% if final %}a couple of days ago{% else %}several hours ago{% endif %}, but {%
|
||||
if many %}have{% else %}has{% endif %} yet to be announced to the tracker.
|
||||
{%- else -%}
|
||||
{%- if many %} are{% else %} is{% endif %} {% if final %}still {% endif %}not currently seeding by you (or anyone else).
|
||||
{%- endif %} You are encouraged to check your client to see that it is working correctly.
|
||||
|
||||
{%- if final %}
|
||||
If {% if many %}these uploads are{% else %}this upload is{% endif %} not seeded within the next {%
|
||||
if never %}day{% else %}few days{% endif %}, {% if many %}they{% else %}it{% endif %} will be removed.
|
||||
{%- else %}
|
||||
We would be grateful if you could get {%
|
||||
if many %}these uploads{% else %}this upload{% endif %} seeding within the next {%
|
||||
if never %}two days{% else %}week{% endif %} to avoid {% if many %}their{% else %}its{% endif %} removal.
|
||||
{%- endif %} Remember, seeding uploads earn bonus points on {{ constant('SITE_NAME') }}.
|
||||
|
||||
The exact details vary from client to client, but you will need to initiate a check and see that the torrent{%
|
||||
if many %}s are{% else %} is{% endif %} seeding at 100%. If you still have the files but have lost the torrent file, {#
|
||||
#}you may obtain a new copy by saving it from the [[n]DL] link. To check the state of an upload, click {%
|
||||
if many %}one of the links{% else %}the link{% endif %} below and look at the "Last active" time. {#
|
||||
#}For more information, please read [url=wiki.php?action=article&id=77]this article[/url].
|
||||
{% for t in list %}
|
||||
[*] [pl]{{ t }}[/pl]
|
||||
{% endfor -%}
|
||||
|
||||
If you no longer wish to receive these notifications, you may turn them off in [url=/user.php?action=edit&userid=me#notification]your profile settings[/url]. At any time, you may view your [url=/torrents.php?userid=me&type=uploaded-unseeded]unseeded uploads[/url] and your [url=/torrents.php?userid=me&type=snatched-unseeded]unseeded snatches[/url].
|
||||
|
||||
<3
|
||||
@@ -1,8 +1,9 @@
|
||||
{% from 'macro/form.twig' import checked %}
|
||||
<div id="access">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="access_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Access Settings</strong>
|
||||
<a href="#access"><strong>Access Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -128,3 +129,4 @@
|
||||
{% endif %}
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% from 'macro/form.twig' import checked, selected %}
|
||||
<div id="site-appearance">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="site_appearance_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Site Appearance Settings</strong>
|
||||
<a href="#site-appearance"><strong>Site Appearance Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="site_style_tr">
|
||||
@@ -63,3 +64,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% from 'macro/form.twig' import checked, selected %}
|
||||
<div id="community">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="community_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Community Settings</strong>
|
||||
<a href="#community"><strong>Community Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="comm_ppp_tr">
|
||||
@@ -95,3 +96,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% from 'macro/form.twig' import checked, selected %}
|
||||
<div id="torrent">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="torrent_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Torrent Settings</strong>
|
||||
<a href="#torrent"><strong>Torrent Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -163,6 +164,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<script type="text/javascript" id="sortable_default">
|
||||
//<![CDATA[
|
||||
var sortable_list_default = '{{ release }}';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% from 'macro/form.twig' import checked %}
|
||||
<div id="navigation">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="navigation_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Navigation Settings</strong>
|
||||
<a href="#navigation"><strong>Navigation Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -19,3 +20,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
]) }} /> Traditional
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
<div id="notification">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="notification_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Notification Settings</strong>
|
||||
<a href="#notification"><strong>Notification Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="notif_autosubscribe_tr">
|
||||
@@ -40,8 +40,26 @@
|
||||
<label for="autosubscribe">Enable automatic thread subscriptions</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="notif_notifyunseededsnatch_tr">
|
||||
<td class="label tooltip" title="Enabling this will send you a PM alert whenever a torrent you have snatched is no longer being seeded. You can reseed it to earn BP.">
|
||||
<strong>Unseeded snatches alerts</strong>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="notifyonunseededsnatch" id="notifyonunseededsnatch"{{ checked(not user.hasAttr('no-pm-unseeded-snatch')) }} />
|
||||
<label for="notifyonunseededsnatch">Receive PM alerts when a snatched upload is not being seeded and faces imminent removal</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="notif_notifyunseededupload_tr">
|
||||
<td class="label tooltip" title="Enabling this will send you a PM alert whenever a torrent you have uploaded is no longer being seeded. You can reseed it to earn BP.">
|
||||
<strong>Unseeded uploads alerts</strong>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="notifyonunseededupload" id="notifyonunseededupload"{{ checked(not user.hasAttr('no-pm-unseeded-upload')) }} />
|
||||
<label for="notifyonunseededupload">Receive PM alerts when one of your uploads is not being seeded and faces imminent removal</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="notif_notifyondeleteseeding_tr">
|
||||
<td class="label tooltip" title="Enabling this will send you a PM alert whenever a torrent you're seeding is deleted.">
|
||||
<td class="label tooltip" title="Enabling this will send you a PM alert whenever a torrent you are seeding is deleted.">
|
||||
<strong>Deleted seeding torrent alerts</strong>
|
||||
</td>
|
||||
<td>
|
||||
@@ -67,15 +85,6 @@
|
||||
<label for="notifyondeletedownloaded">Enable PM notification for deleted downloaded torrents</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="notif_unseeded_tr">
|
||||
<td class="label tooltip" title="Enabling this will send you a PM alert before your uploads are deleted for being unseeded.">
|
||||
<strong>Unseeded torrent alerts</strong>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="unseededalerts" id="unseededalerts"{{ checked(user.notifyUnseeded) }} />
|
||||
<label for="unseededalerts">Enable unseeded torrent alerts</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="label"><strong>Push notifications</strong></td>
|
||||
@@ -176,3 +185,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{% from 'macro/form.twig' import checked %}
|
||||
{% from 'macro/user-edit.twig' import paranoia_list %}
|
||||
<div id="paranoia">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="paranoia_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Paranoia Settings</strong>
|
||||
<a href="#paranoia"><strong>Paranoia Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -150,3 +151,4 @@
|
||||
<td><a href="#" id="preview_paranoia" class="brackets">Preview paranoia</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div id="personal">
|
||||
<table cellpadding="6" cellspacing="1" border="0" width="100%" class="layout border user_options" id="personal_settings">
|
||||
<tr class="colhead_dark">
|
||||
<td colspan="2">
|
||||
<strong>Personal Settings</strong>
|
||||
<a href="#personal"><strong>Personal Settings</strong></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="pers_avatar_tr">
|
||||
@@ -109,3 +110,4 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
601
tests/phpunit/ReaperTest.php
Normal file
601
tests/phpunit/ReaperTest.php
Normal file
@@ -0,0 +1,601 @@
|
||||
<?php
|
||||
|
||||
use \PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Note to Developers/Testers
|
||||
* ==========================
|
||||
*
|
||||
* In the context of a local development environment, this unit
|
||||
* test is fairly sensitive to parameters outside its control.
|
||||
* Tests may fail because of _other_ unseeded/never seeded uploads
|
||||
* that you have created locally.
|
||||
*
|
||||
* If you receive errors (notably never-initial-0-count or
|
||||
* unseeded-initial-0-count) the tests are likely catching your
|
||||
* uploads in its net. To work around this:
|
||||
*
|
||||
* UPDATE torrents_leech_stats SET last_action = now()
|
||||
*
|
||||
* will make the tests happy and everything should succeed. Any
|
||||
* other problems are your own.
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../lib/bootstrap.php');
|
||||
|
||||
class ReaperTest extends TestCase {
|
||||
protected string $tgroupName;
|
||||
protected array $torrentList = [];
|
||||
protected array $userList = [];
|
||||
|
||||
public function setUp(): void {
|
||||
// we need two users, one who uploads and one who snatches
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'phpunit';
|
||||
$creator = new Gazelle\UserCreator;
|
||||
$this->userList = [
|
||||
$creator->setUsername('reaper.' . randomString(10))
|
||||
->setEmail(randomString(10) . '@reaper.example.com')
|
||||
->setPassword(randomString())
|
||||
->setIpaddr('127.0.0.1')
|
||||
->setAdminComment('created by phpunit ReaperTest')
|
||||
->create(),
|
||||
$creator->setUsername('reaper.' . randomString(10))
|
||||
->setUsername('reaper-' . randomString(10))
|
||||
->setEmail(randomString(10) . '@reaper.example.com')
|
||||
->setPassword(randomString())
|
||||
->setIpaddr('127.0.0.1')
|
||||
->setAdminComment('created by phpunit ReaperTest')
|
||||
->create(),
|
||||
];
|
||||
// enable them and wipe their inboxes (there is only one message)
|
||||
foreach ($this->userList as $user) {
|
||||
$user->setUpdate('Enabled', '1')->modify();
|
||||
foreach ((new Gazelle\Inbox($user))->messageList(1, 0) as $pm) {
|
||||
$pm->remove();
|
||||
}
|
||||
}
|
||||
|
||||
// create a torrent group
|
||||
$this->tgroupName = 'phpunit reaper ' . randomString(6);
|
||||
$tgroup = (new Gazelle\Manager\TGroup)->create(
|
||||
categoryId: 1,
|
||||
releaseType: 1,
|
||||
name: $this->tgroupName,
|
||||
description: 'phpunit reaper description',
|
||||
image: '',
|
||||
year: 2022,
|
||||
recordLabel: 'Unitest Artists Corporation',
|
||||
catalogueNumber: 'UA-808',
|
||||
showcase: false,
|
||||
);
|
||||
$tgroup->addArtists($this->userList[0], [ARTIST_MAIN], ['Reaper Girl ' . randomString(12)]);
|
||||
|
||||
$tagMan = new Gazelle\Manager\Tag;
|
||||
$tagId = $tagMan->create('electronic', $this->userList[0]->id());
|
||||
$tagMan->createTorrentTag($tagId, $tgroup->id(), $this->userList[0]->id(), 10);
|
||||
$tgroup->refresh();
|
||||
|
||||
// and add some torrents to the group
|
||||
$this->torrentList = array_map(fn($info) => (new \Gazelle\Manager\Torrent)->create(
|
||||
tgroupId: $tgroup->id(),
|
||||
userId: $this->userList[0]->id(),
|
||||
description: 'reaper release description',
|
||||
media: 'WEB',
|
||||
format: 'FLAC',
|
||||
encoding: 'Lossless',
|
||||
infohash: 'infohash-' . randomString(10),
|
||||
filePath: 'unit-test',
|
||||
fileList: [],
|
||||
size: $info['size'],
|
||||
isScene: false,
|
||||
isRemaster: true,
|
||||
remasterYear: 2023,
|
||||
remasterTitle: $info['title'],
|
||||
remasterRecordLabel: 'Unitest Artists',
|
||||
remasterCatalogueNumber: 'UA-REAP-1',
|
||||
), [
|
||||
[
|
||||
'title' => 'Deluxe Edition',
|
||||
'size' => 20_000_000,
|
||||
], [
|
||||
'title' => 'Limited Edition',
|
||||
'size' => 15_000_000,
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
$this->removeUnseededAlert($this->torrentList);
|
||||
$tgroup = $this->torrentList[0]->group();
|
||||
$torMan = new Gazelle\Manager\Torrent;
|
||||
foreach ($this->torrentList as $torrent) {
|
||||
$torrent = $torMan->findById($torrent->id());
|
||||
if (is_null($torrent)) {
|
||||
continue;
|
||||
}
|
||||
[$ok, $message] = $torrent->remove($this->userList[0]->id(), 'reaper unit test');
|
||||
if (!$ok) {
|
||||
print "error $message [{$this->userList[0]->id()}]\n";
|
||||
}
|
||||
}
|
||||
$tgroup->remove($this->userList[0]);
|
||||
|
||||
foreach ($this->userList as $user) {
|
||||
$user->remove();
|
||||
}
|
||||
}
|
||||
|
||||
// --- HELPER FUNCTIONS ----
|
||||
|
||||
protected function generateReseed(Gazelle\Torrent $torrent, Gazelle\User $user): void {
|
||||
$db = Gazelle\DB::DB();
|
||||
$db->prepared_query("
|
||||
UPDATE torrents_leech_stats SET last_action = now() WHERE TorrentID = ?
|
||||
", $torrent->id()
|
||||
);
|
||||
$db->prepared_query("
|
||||
INSERT INTO xbt_files_users
|
||||
(fid, uid, useragent, peer_id, active, remaining, ip, timespent, mtime)
|
||||
VALUES (?, ?, ?, ?, 1, 0, '127.0.0.1', 1, unix_timestamp(now() - interval 1 hour))
|
||||
", $torrent->id(), $user->id(), 'ua-' . randomString(12), randomString(20)
|
||||
);
|
||||
}
|
||||
|
||||
protected function generateSnatch(Gazelle\Torrent $torrent, Gazelle\User $user): void {
|
||||
Gazelle\DB::DB()->prepared_query("
|
||||
INSERT INTO xbt_snatched
|
||||
(fid, uid, tstamp, IP, seedtime)
|
||||
VALUES (?, ?, unix_timestamp(now()), '127.0.0.1', 1)
|
||||
", $torrent->id(), $user->id()
|
||||
);
|
||||
}
|
||||
|
||||
protected function modifyLastAction(Gazelle\Torrent $torrent, int $interval): void {
|
||||
Gazelle\DB::DB()->prepared_query("
|
||||
UPDATE torrents_leech_stats SET
|
||||
last_action = now() - INTERVAL ? HOUR
|
||||
WHERE TorrentID = ?
|
||||
", $interval, $torrent->id()
|
||||
);
|
||||
}
|
||||
|
||||
protected function modifyUnseededInterval(Gazelle\Torrent $torrent, int $hour): void {
|
||||
Gazelle\DB::DB()->prepared_query("
|
||||
UPDATE torrent_unseeded SET
|
||||
unseeded_date = ?
|
||||
WHERE torrent_id = ?
|
||||
", Date('Y-m-d H:i:s', (int)strtotime("-{$hour} hours")), $torrent->id()
|
||||
);
|
||||
}
|
||||
|
||||
protected function removeUnseededAlert(array $list): void {
|
||||
Gazelle\DB::DB()->prepared_query("
|
||||
DELETE FROM torrent_unseeded WHERE torrent_id in (" . placeholders($list) . ")"
|
||||
, ...array_map(fn($t) => $t->id(), $list)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is not necessary per se, but came in handy when debugging the tests
|
||||
*/
|
||||
protected function status(array $torrentList): void {
|
||||
$db = Gazelle\DB::DB();
|
||||
$db->prepared_query("
|
||||
SELECT t.ID,
|
||||
t.Time,
|
||||
tu.unseeded_date < tls.last_action,
|
||||
coalesce(tls.last_action, 'null'),
|
||||
coalesce(tu.unseeded_date, 'null'),
|
||||
coalesce(tu.notify, 'null'),
|
||||
coalesce(tu.state, 'null')
|
||||
FROM torrents t
|
||||
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
|
||||
LEFT JOIN torrent_unseeded tu ON (tu.torrent_id = t.ID)
|
||||
WHERE t.ID IN (" . placeholders($torrentList) . ")"
|
||||
, ...array_map(fn($t) => $t->id(), $torrentList)
|
||||
);
|
||||
echo implode("\t", ['id', 'created', 'created<last?', 'last_action', 'unseeded', 'final', 'never_seeded']), "\n";
|
||||
echo implode("\n",
|
||||
array_map(fn($r) => implode("\t", $r), $db->to_array(false, MYSQLI_NUM, false))
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
|
||||
public function testExpand(): void {
|
||||
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
|
||||
|
||||
$this->assertEquals(
|
||||
[456 => [99, 88, 77]],
|
||||
$reaper->expand(100, [[456, '99,88,77']]),
|
||||
'torrent-reaper-expand-1',
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
456 => [99, 88, 77],
|
||||
567 => [10, 20, 30, 40, 50],
|
||||
678 => [321],
|
||||
],
|
||||
$reaper->expand(100, [
|
||||
[456, '99,88,77'],
|
||||
[567, '10,20,30,40,50'],
|
||||
[678, '321'],
|
||||
]), 'torrent-reaper-expand-2',
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
456 => [99, 88],
|
||||
567 => [10, 20],
|
||||
678 => [321],
|
||||
],
|
||||
$reaper->expand(2, [
|
||||
[456, '99,88,77'],
|
||||
[567, '10,20,30,40,50'],
|
||||
[678, '321'],
|
||||
]),
|
||||
'torrent-reaper-expand-limit',
|
||||
);
|
||||
}
|
||||
|
||||
public function testNeverSeeded(): void {
|
||||
$user = $this->torrentList[0]->uploader();
|
||||
$inbox = new Gazelle\Inbox($user);
|
||||
$this->assertEquals(0, $inbox->messageTotal(), 'never-inbox-initial');
|
||||
|
||||
$torMan = new Gazelle\Manager\Torrent;
|
||||
$reaper = new \Gazelle\Torrent\Reaper($torMan, new Gazelle\Manager\User);
|
||||
$initialUnseededStats = $reaper->stats();
|
||||
|
||||
// reset the time of the never seeded alert back in time to hit the initial timeout
|
||||
$hour = NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 1;
|
||||
$neverSeededInitialDate = Date('Y-m-d H:i:s', strtotime("-{$hour} hours"));
|
||||
$this->torrentList[0]->setUpdate('Time', $neverSeededInitialDate)->modify();
|
||||
|
||||
// look for never seeded
|
||||
$neverInitial = $reaper->initialNeverSeededList();
|
||||
$this->assertCount(1, $neverInitial, 'never-initial-0-count');
|
||||
$this->assertEquals(
|
||||
1,
|
||||
$reaper->process(
|
||||
$neverInitial,
|
||||
Gazelle\Torrent\ReaperState::NEVER,
|
||||
Gazelle\Torrent\ReaperNotify::INITIAL
|
||||
),
|
||||
'never-initial-process'
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"never_seeded_initial" => $initialUnseededStats['never_seeded_initial'] + 1,
|
||||
"never_seeded_final" => 0,
|
||||
"unseeded_initial" => 0,
|
||||
"unseeded_final" => 0,
|
||||
],
|
||||
$reaper->stats(),
|
||||
'reaper-unseeded-stats-0'
|
||||
);
|
||||
|
||||
// check the notification
|
||||
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-initial-count');
|
||||
$pm = $inbox->messageList(1, 0)[0];
|
||||
$this->assertEquals('You have a non-seeded new upload to rescue', $pm->subject(), 'never-message-initial-subject');
|
||||
$pm->remove();
|
||||
|
||||
// reset the unseeded entries and time out a second upload
|
||||
$this->removeUnseededAlert($this->torrentList);
|
||||
$this->torrentList[1]->setUpdate('Time', $neverSeededInitialDate)->modify();
|
||||
|
||||
$neverInitial = $reaper->initialNeverSeededList();
|
||||
$this->assertCount(1, $neverInitial, 'never-initial-2'); // one user ...
|
||||
$this->assertEquals(2, // ... with two uploads
|
||||
$reaper->process(
|
||||
$neverInitial,
|
||||
Gazelle\Torrent\ReaperState::NEVER,
|
||||
Gazelle\Torrent\ReaperNotify::INITIAL
|
||||
),
|
||||
'never-2-process'
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"never_seeded_initial" => $initialUnseededStats['never_seeded_initinal'] + 2,
|
||||
"never_seeded_final" => 0,
|
||||
"unseeded_initial" => 0,
|
||||
"unseeded_final" => 0,
|
||||
],
|
||||
$reaper->stats(),
|
||||
'reaper-unseeded-stats-1'
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-2');
|
||||
$pm = $inbox->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('You have 2 non-seeded new uploads to rescue', $pm->subject(), 'never-message-2');
|
||||
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-body-2-dear');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'never-body-2-pl-0');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'never-body-2-pl-1');
|
||||
$pm->remove();
|
||||
|
||||
// reseed one of the torrents by the uploader
|
||||
$this->generateReseed($this->torrentList[0], $this->torrentList[0]->uploader());
|
||||
$this->torrentList[1]->setUpdate('Time', Date('Y-m-d H:i:s'))->modify();
|
||||
|
||||
// reset the time of the remaing never seeded alert back in time to hit
|
||||
// the final timeout.
|
||||
// reminder: once a torrent is referenced in the torrent_unseeded table,
|
||||
// we no longer need to consider the torrent creation date.
|
||||
$this->modifyUnseededInterval($this->torrentList[1], NOTIFY_NEVER_SEEDED_FINAL_HOUR + 1);
|
||||
|
||||
$this->assertEquals(0, $inbox->messageTotal(), 'never-message-final-none');
|
||||
$neverFinal = $reaper->finalNeverSeededList();
|
||||
$this->assertCount(1, $neverFinal, 'never-final-0'); // one user ...
|
||||
$this->assertEquals(1, // ... with one upload
|
||||
$reaper->process(
|
||||
$neverFinal,
|
||||
Gazelle\Torrent\ReaperState::NEVER,
|
||||
Gazelle\Torrent\ReaperNotify::FINAL
|
||||
),
|
||||
'never-final-process'
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"never_seeded_initial" => $initialUnseededStats['never_seeded_initial'] + 1,
|
||||
"never_seeded_final" => $initialUnseededStats['never_seeded_final'] + 1,
|
||||
"unseeded_initial" => 0,
|
||||
"unseeded_final" => 0,
|
||||
],
|
||||
$reaper->stats(),
|
||||
'reaper-unseeded-stats-final'
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $inbox->messageTotal(), 'never-message-final-count');
|
||||
$pm = $inbox->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('You have a non-seeded new upload scheduled for deletion very soon', $pm->subject(), 'never-message-final-subject');
|
||||
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-body-3-dear');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'never-body-3-pl-1');
|
||||
$pm->remove();
|
||||
|
||||
$this->modifyUnseededInterval($this->torrentList[1], REMOVE_NEVER_SEEDED_HOUR + 1);
|
||||
$id = $this->torrentList[1]->id();
|
||||
$list = $reaper->reaperList(
|
||||
state: Gazelle\Torrent\ReaperState::NEVER,
|
||||
interval: REMOVE_NEVER_SEEDED_HOUR,
|
||||
);
|
||||
$this->assertCount(1, $list, 'never-reap-list');
|
||||
|
||||
$this->assertEquals(1, $reaper->removeNeverSeeded(), 'never-reap-remove');
|
||||
$pm = $inbox->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('1 of your uploads has been deleted for inactivity (never seeded)', $pm->subject(), 'never-remove-message');
|
||||
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-remove-body-dear');
|
||||
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-body-pl');
|
||||
$pm->remove();
|
||||
|
||||
$deleted = $torMan->findById($id);
|
||||
$this->assertNull($deleted, 'never-was-reaped');
|
||||
}
|
||||
|
||||
public function testUnseeded(): void {
|
||||
$torMan = new Gazelle\Manager\Torrent;
|
||||
$reaper = new \Gazelle\Torrent\Reaper($torMan, new Gazelle\Manager\User);
|
||||
|
||||
$initialUnseededStats = $reaper->stats();
|
||||
$unseededInitial = $reaper->initialUnseededList();
|
||||
$this->assertCount(0, $unseededInitial, 'nseeded-initial-0-count');
|
||||
|
||||
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
|
||||
foreach ($this->torrentList as $torrent) {
|
||||
$hour = NOTIFY_UNSEEDED_INITIAL_HOUR + 1;
|
||||
$torrent->setUpdate('Time', Date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
|
||||
$this->modifyLastAction($torrent, NOTIFY_UNSEEDED_INITIAL_HOUR + 2);
|
||||
// pretend they were snatched
|
||||
foreach ($this->userList as $user) {
|
||||
$this->generateSnatch($torrent, $user);
|
||||
}
|
||||
}
|
||||
|
||||
// look for unseeded
|
||||
$unseededInitial = $reaper->initialUnseededList();
|
||||
$this->assertCount(1, $unseededInitial, 'unseeded-initial-1');
|
||||
$this->assertEquals(
|
||||
2,
|
||||
$reaper->process(
|
||||
$unseededInitial,
|
||||
Gazelle\Torrent\ReaperState::UNSEEDED,
|
||||
Gazelle\Torrent\ReaperNotify::INITIAL
|
||||
),
|
||||
'unseeded-initial-process'
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"never_seeded_initial" => 0,
|
||||
"never_seeded_final" => 0,
|
||||
"unseeded_initial" => $initialUnseededStats['unseeded_seeded_initial'] + 2,
|
||||
"unseeded_final" => 0,
|
||||
],
|
||||
$reaper->stats(),
|
||||
'reaper-unseeded-stats-0'
|
||||
);
|
||||
|
||||
$inboxList = [
|
||||
new Gazelle\Inbox($this->userList[0]),
|
||||
new Gazelle\Inbox($this->userList[1]),
|
||||
];
|
||||
|
||||
$this->assertEquals(1, $inboxList[0]->messageTotal(), 'unseeded-initial-0-inbox');
|
||||
$pm = $inboxList[0]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('There are 2 unseeded uploads to rescue', $pm->subject(), 'unseeded-initial-0-rescue');
|
||||
$this->assertStringContainsString("You have 2 uploads that are not currently seeding by you (or anyone else)", $body, 'unseeded-initial-0-body');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-0-body-pl-0');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-initial-0-body-pl-1');
|
||||
$pm->remove();
|
||||
|
||||
$this->assertEquals(1, $inboxList[1]->messageTotal(), 'unseeded-initial-1-inbox');
|
||||
$pm = $inboxList[1]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('You have 2 unseeded snatches to save', $pm->subject(), 'unseeded-initial-1-rescue');
|
||||
$this->assertStringContainsString("In the past, you snatched 2 uploads that are no longer being seeded", $body, 'unseeded-initial-1-body');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-0');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-1');
|
||||
$pm->remove();
|
||||
|
||||
$initialClaim = $reaper->claimStats();
|
||||
$this->assertEquals(['open', 'claimed'], array_keys($initialClaim), 'claim-initial');
|
||||
$this->assertEquals(2, $initialClaim['open'], 'claim-open');
|
||||
$this->assertEquals(0, $initialClaim['claimed'], 'claim-claimed');
|
||||
|
||||
// snatcher reseeds the first upload
|
||||
$this->modifyUnseededInterval($this->torrentList[0], NOTIFY_UNSEEDED_INITIAL_HOUR + 3);
|
||||
$this->generateReseed($this->torrentList[0], $this->userList[1]);
|
||||
|
||||
// and wins the glory
|
||||
$bonus = $this->userList[1]->bonusPointsTotal();
|
||||
$win = $reaper->claim();
|
||||
$this->assertCount(1, $win, 'unseeded-claim-win-count');
|
||||
[$torrentId, $userId, $bp] = $win[0];
|
||||
$this->assertEquals($this->torrentList[0]->id(), $torrentId, 'unseeded-claim-win-torrent');
|
||||
$this->assertEquals($this->userList[1]->id(), $userId, 'unseeded-claim-win-user');
|
||||
$this->assertEquals($this->userList[1]->flush()->bonusPointsTotal(), $bonus + $bp, 'unseeded-claim-win-bp');
|
||||
|
||||
// message
|
||||
$pm = $inboxList[1]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals("Thank you for reseeding {$this->torrentList[0]->group()->name()}!", $pm->subject(), 'unseeded-initial-1-thx');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[0]->id()}[/pl]", $body, 'unseeded-initial-1-body-pl-thx');
|
||||
$this->assertStringContainsString("$bp bonus points", $body, 'unseeded-initial-1-body-bp-thx');
|
||||
$pm->remove();
|
||||
|
||||
$newClaim = $reaper->claimStats();
|
||||
$this->assertEquals(1, $newClaim['open'], 'claim-new-open');
|
||||
$this->assertEquals(1, $newClaim['claimed'], 'claim-new-claimed');
|
||||
|
||||
// reset the time of the remaing unseeded alert back in time to hit the final timeout.
|
||||
$this->modifyLastAction($this->torrentList[1], NOTIFY_UNSEEDED_FINAL_HOUR + 2);
|
||||
$this->modifyUnseededInterval($this->torrentList[1], NOTIFY_UNSEEDED_FINAL_HOUR + 1);
|
||||
|
||||
$unseededStats = $reaper->stats();
|
||||
$unseededFinal = $reaper->finalUnseededList();
|
||||
|
||||
$this->assertCount(1, $unseededFinal, 'unseeded-final-0');
|
||||
$this->assertEquals(1,
|
||||
$reaper->process(
|
||||
$unseededFinal,
|
||||
Gazelle\Torrent\ReaperState::UNSEEDED,
|
||||
Gazelle\Torrent\ReaperNotify::FINAL
|
||||
),
|
||||
'unseeded-final-process'
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
"never_seeded_initial" => 0,
|
||||
"never_seeded_final" => 0,
|
||||
"unseeded_initial" => 0,
|
||||
"unseeded_final" => $unseededStats['unseeded_final'] + 1,
|
||||
],
|
||||
$reaper->stats(),
|
||||
'reaper-unseeded-stats-final'
|
||||
);
|
||||
|
||||
$this->assertEquals(1, $inboxList[0]->messageTotal(), 'unseeded-final-0-inbox');
|
||||
$pm = $inboxList[0]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('There is an unseeded upload scheduled for deletion very soon', $pm->subject(), 'unseeded-final-0-rescue');
|
||||
$this->assertStringContainsString("You have an upload that is still not currently seeding by you (or anyone else)", $body, 'unseeded-final-0-body');
|
||||
$this->assertStringContainsString("[pl]{$this->torrentList[1]->id()}[/pl]", $body, 'unseeded-final-0-body-pl-1');
|
||||
$pm->remove();
|
||||
|
||||
$this->assertEquals(0, $inboxList[1]->messageTotal(), 'unseeded-final-no-snatcher-inbox');
|
||||
|
||||
// too late
|
||||
$this->modifyUnseededInterval($this->torrentList[1], REMOVE_UNSEEDED_HOUR + 1);
|
||||
$id = $this->torrentList[1]->id();
|
||||
$list = $reaper->reaperList(
|
||||
state: Gazelle\Torrent\ReaperState::UNSEEDED,
|
||||
interval: REMOVE_UNSEEDED_HOUR,
|
||||
);
|
||||
$this->assertCount(1, $list, 'unseeded-reap-list');
|
||||
$this->assertEquals(1, $reaper->removeUnseeded(), 'unseeded-reap-remove');
|
||||
|
||||
$pm = $inboxList[0]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('1 of your uploads has been deleted for inactivity (unseeded)', $pm->subject(), 'never-remove-message');
|
||||
$this->assertStringContainsString("Dear {$this->userList[0]->username()}", $body, 'never-remove-body-dear');
|
||||
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-body-pl');
|
||||
$pm->remove();
|
||||
|
||||
$pm = $inboxList[1]->messageList(1, 0)[0];
|
||||
$body = $pm->postList(1, 0)[0]['body'];
|
||||
$this->assertEquals('1 of your snatches was deleted for inactivity', $pm->subject(), 'never-remove-snatcher-message');
|
||||
$this->assertStringContainsString("Dear {$this->userList[1]->username()}", $body, 'never-remove-snatcher-body-dear');
|
||||
$this->assertStringContainsString("[url=torrents.php?id={$this->torrentList[1]->group()->id()}]", $body, 'never-remove-snatcher-body-pl');
|
||||
$pm->remove();
|
||||
|
||||
$deleted = $torMan->findById($id);
|
||||
$this->assertNull($deleted, 'unseeded-was-reaped');
|
||||
}
|
||||
|
||||
public function testNeverNotify(): void {
|
||||
// like never, but checking that people who have asked not to receive notifications, do not.
|
||||
$this->userList[0]->toggleAttr('no-pm-unseeded-upload', true);
|
||||
$this->userList[1]->toggleAttr('no-pm-unseeded-snatch', true);
|
||||
|
||||
$this->assertTrue($this->userList[0]->hasAttr('no-pm-unseeded-upload'), 'reaper-no-notify-upload');
|
||||
$this->assertTrue($this->userList[1]->hasAttr('no-pm-unseeded-snatch'), 'reaper-no-notify-snatch');
|
||||
|
||||
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
|
||||
foreach ($this->torrentList as $torrent) {
|
||||
$hour = NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 1;
|
||||
$torrent->setUpdate('Time', Date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
|
||||
$this->modifyLastAction($torrent, NOTIFY_NEVER_SEEDED_INITIAL_HOUR + 2);
|
||||
}
|
||||
|
||||
// look for never seeded
|
||||
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
|
||||
$reaper->process(
|
||||
$reaper->initialNeverSeededList(),
|
||||
Gazelle\Torrent\ReaperState::NEVER,
|
||||
Gazelle\Torrent\ReaperNotify::INITIAL
|
||||
);
|
||||
|
||||
$this->assertEquals(0, (new Gazelle\Inbox($this->userList[0]))->messageTotal(), 'never-uploader-no-notify');
|
||||
$this->assertEquals(0, (new Gazelle\Inbox($this->userList[1]))->messageTotal(), 'never-snatcher-no-notify');
|
||||
}
|
||||
|
||||
public function testUnseededNotify(): void {
|
||||
// like unseeded, but checking that people who have asked not to receive notifications, do not.
|
||||
$this->userList[0]->toggleAttr('no-pm-unseeded-upload', true);
|
||||
$this->userList[1]->toggleAttr('no-pm-unseeded-snatch', true);
|
||||
|
||||
$this->assertTrue($this->userList[0]->hasAttr('no-pm-unseeded-upload'), 'reaper-no-notify-upload');
|
||||
$this->assertTrue($this->userList[1]->hasAttr('no-pm-unseeded-snatch'), 'reaper-no-notify-snatch');
|
||||
|
||||
// reset the last action and time of the unseeded alert back in time to hit the initial timeout
|
||||
foreach ($this->torrentList as $torrent) {
|
||||
$hour = NOTIFY_UNSEEDED_INITIAL_HOUR + 1;
|
||||
$torrent->setUpdate('Time', Date('Y-m-d H:i:s', strtotime("-{$hour} hours")))->modify();
|
||||
$this->modifyLastAction($torrent, NOTIFY_UNSEEDED_INITIAL_HOUR + 2);
|
||||
$this->generateSnatch($torrent, $this->userList[1]);
|
||||
}
|
||||
|
||||
// look for unseeded
|
||||
$reaper = new \Gazelle\Torrent\Reaper(new Gazelle\Manager\Torrent, new Gazelle\Manager\User);
|
||||
$reaper->process(
|
||||
$reaper->initialUnseededList(),
|
||||
Gazelle\Torrent\ReaperState::UNSEEDED,
|
||||
Gazelle\Torrent\ReaperNotify::INITIAL
|
||||
);
|
||||
|
||||
$this->assertEquals(1, (new Gazelle\Inbox($this->userList[0]))->messageTotal(), 'unseeded-uploader-no-notify');
|
||||
$this->assertEquals(0, (new Gazelle\Inbox($this->userList[1]))->messageTotal(), 'unseeded-snatcher-no-notify');
|
||||
|
||||
$timeline = $reaper->timeline();
|
||||
$this->assertEquals([2], array_values($timeline), 'reaper-two-today');
|
||||
$today = current(array_keys($timeline));
|
||||
// take into account the chance of running a few seconds before midnight
|
||||
$this->assertContains($today, [Date('Y-m-d'), Date('Y-m-d', strtotime('1 hour ago'))], 'reaper-timeline');
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user