Add user_torrents ajax endpoint

This commit is contained in:
itismadness
2025-09-02 01:57:50 +00:00
committed by Spine
parent 540e520e00
commit d3a87015b1
7 changed files with 300 additions and 15 deletions

View File

@@ -80,6 +80,32 @@ abstract class ArtistRole extends Base {
];
}
/**
* Returns the id of the first artist that would be shown in the renderRole
* display. If it would be shown as "various ...", then it's the first
* artist of that role type.
*
* @return int|null
*/
public function primaryId(): int|null {
$roleList = $this->roleList();
$composerCount = count($roleList['composer'] ?? []);
$conductorCount = count($roleList['conductor'] ?? []);
$djCount = count($roleList['dj'] ?? []);
$mainCount = count($roleList['main'] ?? []);
if ($djCount) {
return $roleList['dj'][0]['id'];
} elseif ($composerCount) {
return $roleList['composer'][0]['id'];
} elseif ($mainCount) {
return $roleList['main'][0]['id'];
} elseif ($conductorCount) {
return $roleList['conductor'][0]['id'];
}
return null;
}
protected function renderRole(int $mode): string {
$roleList = $this->roleList();
$arrangerCount = count($roleList['arranger'] ?? []);

View File

@@ -3,7 +3,11 @@
namespace Gazelle\Enum;
enum UserTorrentSearch: string {
case seeding = 'seeding';
case snatched = 'snatched';
case uploaded = 'uploaded';
case downloaded = 'downloaded';
case leeching = 'leeching';
case seeding = 'seeding';
case snatched = 'snatched';
case snatchedUnseeded = 'snatched-unseeded';
case uploaded = 'uploaded';
case uploadedUnseeded = 'uploaded-unseeded';
}

91
app/Json/UserTorrents.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
namespace Gazelle\Json;
use Gazelle\Search\UserTorrent;
use Gazelle\Enum\UserTorrentSearch;
class UserTorrents extends \Gazelle\Json {
protected int $limit = 500;
protected int $offset = 0;
protected string $type = 'seeding';
public function __construct(
protected \Gazelle\User $user,
protected \Gazelle\User $viewer,
protected \Gazelle\Manager\Torrent $torMan = new \Gazelle\Manager\Torrent(),
) {}
public function setType(string $type): static {
$this->type = $type;
return $this;
}
public function setLimit(int $limit): static {
$this->limit = $limit;
return $this;
}
public function setOffset(int $offset): static {
$this->offset = $offset;
return $this;
}
public function payload(): array {
switch ($this->type) {
case 'downloaded':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::downloaded);
break;
case 'leeching':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::leeching);
break;
case 'seeding':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::seeding);
break;
case 'snatched':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::snatched);
break;
case 'snatched-unseeded':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::snatchedUnseeded);
break;
case 'uploaded':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::uploaded);
break;
case 'uploaded-unseeded':
$userTorrent = new UserTorrent($this->user, UserTorrentSearch::uploadedUnseeded);
break;
default:
json_error("bad type");
}
$userTorrent->setLimit($this->limit)->setOffset($this->offset);
return [
"$this->type" => array_reduce(
$userTorrent->idList(),
function ($acc, $id) {
$torrent = $this->torMan->findById($id);
if ($torrent) {
$item = [
'groupId' => $torrent->groupId(),
'torrentId' => $torrent->id(),
'name' => $torrent->group()->name(),
'torrentSize' => $torrent->size(),
];
if ($torrent->group()->hasArtistRole()) {
// artistId and artistName are returned mostly for compatibility with RED
// as people should probably just use `artists` array principally.
$item['artistId'] = $torrent->group()->artistRole()?->primaryId();
$item['artistName'] = $torrent->group()->artistName();
$item['artists'] = static::artistPayload($torrent->group());
}
$acc[] = $item;
}
return $acc;
},
[],
),
'total' => $userTorrent->total(),
];
}
}

View File

@@ -11,6 +11,9 @@ use Gazelle\Enum\UserTorrentSearch;
*/
class UserTorrent extends \Gazelle\Base {
private int $limit;
private int $offset;
public function __construct(
protected \Gazelle\User $user,
protected UserTorrentSearch $type,
@@ -20,22 +23,74 @@ class UserTorrent extends \Gazelle\Base {
return $this->type->value;
}
public function idList(): array {
self::$db->prepared_query("
SELECT DISTINCT t.ID
FROM torrents AS t "
public function setLimit(int $limit): static {
$this->limit = $limit;
return $this;
}
public function setOffset(int $offset): static {
$this->offset = $offset;
return $this;
}
protected function sql(): string {
return "FROM torrents AS t "
. match ($this->type) {
UserTorrentSearch::downloaded => "
INNER JOIN users_downloads AS ud ON (ud.TorrentID = t.ID)
WHERE ud.UserID = ?
ORDER BY ud.Time DESC",
UserTorrentSearch::leeching => "
INNER JOIN xbt_files_users AS xfu ON (xfu.fid = t.ID)
WHERE xfu.active = 1 AND xfu.Remaining > 0 AND xfu.uid = ?
ORDER BY from_unixtime(xfu.mtime - xfu.timespent) DESC",
UserTorrentSearch::seeding => "
INNER JOIN xbt_files_users AS xfu ON (t.ID = xfu.fid)
WHERE xfu.remaining = 0
AND xfu.uid = ?",
WHERE xfu.remaining = 0 AND xfu.uid = ?
ORDER BY from_unixtime(xfu.mtime - xfu.timespent) DESC",
UserTorrentSearch::snatched => "
INNER JOIN xbt_snatched AS x ON (t.ID = x.fid)
WHERE x.uid = ?",
default =>
"WHERE t.UserID = ?",
}, $this->user->id
);
INNER JOIN xbt_snatched AS xs ON (t.ID = xs.fid)
WHERE xs.uid = ?
ORDER BY from_unixtime(xs.tstamp) DESC",
UserTorrentSearch::snatchedUnseeded => "
INNER JOIN xbt_snatched AS xs ON (xs.fid = t.ID)
LEFT JOIN xbt_files_users AS xfu USING (uid, fid)
WHERE xfu.fid IS NULL AND xs.uid = ?
ORDER BY from_unixtime(xs.tstamp) DESC",
UserTorrentSearch::uploadedUnseeded => "
LEFT JOIN xbt_files_users AS xfu ON (xfu.fid = t.ID AND xfu.uid = t.UserID)
WHERE xfu.fid IS NULL AND t.UserID = ?
ORDER BY t.created DESC",
// UserTorrentSearch::uploaded
default => "
WHERE t.UserID = ?
ORDER BY t.created DESC",
};
}
public function total(): int {
return (int)self::$db->scalar("
SELECT COUNT(DISTINCT t.ID)
{$this->sql()}
", $this->user->id);
}
public function idList(): array {
$sql = "
SELECT DISTINCT t.ID
{$this->sql()}";
$args = [$this->user->id];
if (isset($this->limit)) {
$sql .= "\nLIMIT ?";
$args[] = $this->limit;
}
if (isset($this->offset)) {
$sql .= "\nOFFSET ?";
$args[] = $this->offset;
}
self::$db->prepared_query($sql, ...$args);
return self::$db->collect(0);
}
}

View File

@@ -46,6 +46,7 @@ exists for the sake of interoperability and may go away in the future.
* [Upload](#upload)
* [Download](#download)
* [Add Log](#add-log)
* [User Torrents](#user-torrents)
* [Better](#better)
* [Logchecker](#logchecker)
* [Requests](#requests)
@@ -1538,6 +1539,63 @@ __Note__: Must be the uploader of the torrent or moderator to add logs
}
```
### User Torrents
**URL:**
`ajax.php?action=user_torrents&userid=<User ID>&type=<Type>`
__Note__: Must be the uploader of the torrent or moderator to add logs
**GET Arguments:**
`userid` - The user id to get torrents for (can also use `id`)
`type` - Type of torrents to display (options are `downloaded`, `leeching`, `seeding`, `snatched`, `snatched-unseeded`, `uploaded`, `uploaded-unseeded`)
`limit` - (Optional) Number of results to display (default: 500)
`offset` - (Optional) Number of results to offset by (default: 0)
`page` - (Optional) Page of results to show (default: 1)
__NOTE__: You can only provide one of `offset` or `page`. Offset is a number of
rows to skip before starting to collect the result set, while page is the page
number of results to retrieve, where `(page - 1) * limit == offset`.
**Response format:**
```json
{
"status": "success",
"response": {
"uploaded": [
{
"groupId": 1,
"torrentId": 1,
"name": "Foo",
"torrentSize": 12345,
"artistId": 1,
"artistName": "Bar",
"artists": {
"artists": [
{
"id": 1,
"aliasid": 1,
"name": "Bar",
}
]
},
},
],
"total": 1
},
"info": {
"source": "Gazelle Dev",
"version": 1
}
}
```
__NOTE__: For a torrent that has many main artists, `artistName` will be
"Various ..." and the `artistId` will be the ID of the first main artist. It is
recommended to use the `artists` field to get artist information.
## Better
**URL:**

View File

@@ -257,6 +257,9 @@ switch ($Action) {
case 'add_log':
include_once __DIR__ . '/add_log.php';
break;
case 'user_torrents':
include_once __DIR__ . '/user_torrents.php';
break;
default:
// If they're screwing around with the query string
json_error("failure");

View File

@@ -0,0 +1,48 @@
<?php
/** @phpstan-var \Gazelle\User $Viewer */
declare(strict_types=1);
namespace Gazelle;
if (!isset($_GET['type']) || !in_array($_GET['type'], ['snatched', 'snatched-unseeded', 'seeding', 'leeching', 'uploaded', 'uploaded-unseeded', 'downloaded'])) {
json_error("bad type");
}
$type = (string)$_GET['type'];
foreach (['limit', 'offset', 'page'] as $key) {
if (isset($_GET[$key]) && !ctype_digit($_GET[$key])) {
json_error("non-numeric value for {$key}");
}
}
if (isset($_GET['offset']) && isset($_GET['page'])) {
json_error('can only use one of offset or page');
}
if (isset($_GET['offset']) && (int)$_GET['offset'] < 0) {
json_error('invalid offset parameter, must be 0 or greater');
}
if (isset($_GET['page']) && (int)$_GET['page'] < 1) {
json_error('invalid page parameter, must be 1 or greater');
}
$limit = (int)($_GET['limit'] ?? 500);
$offset = isset($_GET['page']) ? (int)($_GET['page'] - 1) * $limit : (int)($_GET['offset'] ?? 0);
if ($limit < 1) {
json_error('invalid limit parameter, must be 1 or greater');
}
// We accept id to match RED, but userid is the better param name and matches user_recents
$user = new Manager\User()->findById((int)($_GET['userid'] ?? $_GET['id'] ?? 0));
if (is_null($user)) {
json_error("bad userid");
}
if (!$user->propertyVisible($Viewer, str_replace('-unseeded', '', $type))) {
json_error('user has hidden this');
}
echo new Json\UserTorrents($user, $Viewer)
->setType($type)
->setLimit($limit)
->setOffset($offset)
->response();