Files
ops-Gazelle/app/TGroup.php
2025-10-01 09:34:01 +02:00

1066 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Gazelle;
use Gazelle\Enum\LeechReason;
use Gazelle\Enum\LeechType;
use Gazelle\Intf\Bookmarked;
use Gazelle\Intf\CategoryHasArtist;
use Gazelle\Intf\CollageEntry;
class TGroup extends BaseAttrObject implements Bookmarked, CategoryHasArtist, CollageEntry {
final public const tableName = 'torrents_group';
final public const CACHE_KEY = 'tg_%d';
final public const CACHE_TLIST_KEY = 'tlist_%d';
final public const CACHE_COVERART_KEY = 'tg_cover_%d';
final public const USER_RECENT_UPLOAD = 'u_recent_up_%d';
final public const CACHE_REQUEST_TGROUP = 'req_tg_%d';
final protected const ObjectName = 'torrent_group';
final protected const PkColumn = 'ID';
final protected const JoinColumn = 'TorrentGroupAttrID';
final protected const ObjectColumn = 'TorrentGroupID';
final protected const USER_RECENT_SNATCH = 'u_recent_snatch_%d';
protected int $revisionId = 0;
protected bool $showFallbackImage = true;
protected ArtistRole\TGroup $artistRole;
protected Stats\TGroup $stats;
public function flush(): static {
self::$cache->delete_multi([
sprintf(self::CACHE_KEY, $this->id),
sprintf(self::CACHE_TLIST_KEY, $this->id),
sprintf(self::CACHE_COVERART_KEY, $this->id),
sprintf(self::CACHE_REQUEST_TGROUP, $this->id),
"torrent_group_{$this->id}",
"groups_artists_{$this->id}",
]);
unset($this->artistRole, $this->info);
return parent::flush();
}
public function link(): string {
$url = "<a href=\"{$this->url()}\" title=\"" . ($this->hashTag() ?: 'View torrent group') . '" dir="ltr">'
. display_str($this->name()) . '</a>';
return match ($this->categoryName()) {
'Music' => "{$this->artistRole()->link()} $url [{$this->year()} {$this->releaseTypeName()}]",
'Audiobooks',
'Comedy' => "$url [{$this->year()}]",
default => $url,
};
}
public function location(): string {
return "torrents.php?id={$this->id}";
}
public function bookmarkTable(): string {
return 'bookmark_tgroup';
}
public function bookmarkColumnName(): string {
return 'id_tgroup';
}
public function torrentLink(int $torrentId): string {
$url = '<a href="' . $this->url() . "&amp;torrentid={$torrentId}#torrent{$torrentId}\" dir=\"ltr\">"
. display_str($this->name()) . '</a>';
return match ($this->categoryName()) {
'Music' => "{$this->artistRole()->link()} $url [{$this->year()} {$this->releaseTypeName()}]",
'Audiobooks',
'Comedy' => "$url [{$this->year()}]",
default => $url,
};
}
public function text(): string {
return match ($this->categoryName()) {
'Music' => "{$this->artistRole()->text()} {$this->name()} [{$this->year()} {$this->releaseTypeName()}]"
. ($this->isShowcase() ? '[Showcase]' : ''),
'Audiobooks',
'Comedy' => "{$this->name()} [{$this->year()}]",
default => $this->name(),
};
}
/**
* When the image of a release group is changed, we need to flush other things
*/
public function imageFlush(): static {
self::$db->prepared_query("
SELECT CollageID FROM collages_torrents WHERE GroupID = ?
", $this->id
);
if (self::$db->has_results()) {
self::$cache->delete_multi(array_map(fn ($id) => "collagev2_$id", self::$db->collect(0)));
}
self::$db->prepared_query("
SELECT DISTINCT UserID
FROM torrents AS t
LEFT JOIN torrents_group AS tg ON (t.GroupID = tg.ID)
WHERE tg.ID = ?
", $this->id
);
if (self::$db->has_results()) {
self::$cache->delete_multi(array_map(fn ($id) => sprintf(self::USER_RECENT_UPLOAD, $id), self::$db->collect(0)));
}
self::$db->prepared_query("
SELECT DISTINCT xs.uid
FROM xbt_snatched xs
INNER JOIN torrents t ON (t.ID = xs.fid)
WHERE t.GroupID = ?
", $this->id
);
if (self::$db->has_results()) {
self::$cache->delete_multi(array_map(fn ($id) => sprintf(self::USER_RECENT_SNATCH, $id), self::$db->collect(0)));
}
return $this;
}
public function touch(): static {
self::$db->prepared_query('
UPDATE torrents_group SET
Time = now()
WHERE ID = ?
', $this->id
);
return $this;
}
/**
* Toggle whether an internal URL is returnd for missing cover artwork
* is returned, or null. Used by API endpoints.
*/
public function showFallbackImage(bool $showFallbackImage): static {
$this->showFallbackImage = $showFallbackImage;
return $this;
}
public function revisionList(): array {
self::$db->prepared_query("
SELECT RevisionID AS revision,
Summary AS summary,
Time AS time,
UserID AS user_id
FROM wiki_torrents
WHERE PageID = ?
ORDER BY RevisionID DESC
", $this->id
);
return self::$db->to_array(false, MYSQLI_ASSOC);
}
/**
* Get the metadata of the torrent
*
* @return array of many things
*/
public function info(int $revisionId = 0): ?array {
if (isset($this->info)) {
return $this->info;
}
$key = sprintf(self::CACHE_KEY, $this->id);
$this->revisionId = $revisionId;
if (!$revisionId) {
$cached = self::$cache->get_value($key);
if (is_array($cached)) {
$refresh = false;
if (!isset($cached['Image'])) {
$cached['Image'] = $this->showFallbackImage
? (STATIC_SERVER . '/common/noartwork/' . CATEGORY_ICON[$cached['CategoryID'] - 1])
: null;
$refresh = true;
}
$cached['Flags'] = [
'IsSnatched' => $this->fetchIsSnatched(),
];
if (!isset($cached['torrent_list'])) {
self::$db->prepared_query("
SELECT t.ID
FROM torrents t
WHERE t.GroupID = ?
ORDER BY t.Remastered, (t.RemasterYear != 0) DESC, t.RemasterYear, t.RemasterTitle,
t.RemasterRecordLabel, t.RemasterCatalogueNumber, t.Media, t.Format, t.Encoding, t.ID
", $this->id
);
$cached['torrent_list'] = self::$db->collect(0);
$refresh = true;
}
if ($refresh) {
self::$cache->cache_value($key, $cached, 0);
}
$this->info = $cached;
$this->info['from_cache'] = true;
return $this->info;
}
}
$sql = 'SELECT '
. ($this->revisionId ? 'w.Body, w.Image,' : 'tg.WikiBody AS Body, tg.WikiImage AS Image,')
. " tg.ID,
tg.Name,
tg.Year,
tg.RecordLabel,
tg.CatalogueNumber,
tg.ReleaseType,
tg.CategoryID,
tg.Time,
tg.VanityHouse,
group_concat(tag.Name ORDER BY tt.PositiveVotes - tt.NegativeVotes DESC, tag.Name) AS tagNames,
group_concat(tag.ID ORDER BY tt.PositiveVotes - tt.NegativeVotes DESC, tag.Name) AS tagIds,
group_concat(tag.UserID ORDER BY tt.PositiveVotes - tt.NegativeVotes DESC, tag.Name) AS tagVoteUserIds,
group_concat(tt.PositiveVotes ORDER BY tt.PositiveVotes - tt.NegativeVotes DESC, tag.Name) AS tagUpvotes,
group_concat(tt.NegativeVotes ORDER BY tt.PositiveVotes - tt.NegativeVotes DESC, tag.Name) AS tagDownvotes,
(tgha.TorrentGroupID IS NOT NULL) AS noCoverArt,
max(coalesce(t.Size, 0)) AS max_torrent_size,
max(coalesce(t.created, '2000-01-01 00:00:00')) AS most_recent_upload
FROM torrents_group AS tg
LEFT JOIN torrents AS t ON (t.GroupID = tg.ID)
LEFT JOIN torrents_tags AS tt ON (tt.GroupID = tg.ID)
LEFT JOIN tags as tag ON (tag.ID = tt.TagID)
LEFT JOIN torrent_group_has_attr AS tgha ON (tgha.TorrentGroupID = tg.ID
AND tgha.TorrentGroupAttrID = (SELECT tga.ID FROM torrent_group_attr tga WHERE tga.Name = 'no-cover-art')
)
";
$args = [];
if ($this->revisionId) {
$sql .= '
LEFT JOIN wiki_torrents AS w ON (w.PageID = ? AND w.RevisionID = ?)';
$args[] = $this->id;
$args[] = $this->revisionId;
}
$sql .= " WHERE tg.ID = ? GROUP BY tg.ID";
$args[] = $this->id;
$info = self::$db->rowAssoc($sql, ...$args);
// make the values sane (null, boolean as appropriate)
// TODO: once all get_*_info calls have been ported over, do this prior to caching
foreach (['CatalogueNumber', 'RecordLabel'] as $nullable) {
$info[$nullable] = $info[$nullable] == '' ? null : $info[$nullable];
}
$info['VanityHouse'] = ($info['VanityHouse'] == 1);
$info['ReleaseType'] = (int)$info['ReleaseType'];
// Reorganize tag info to be useful
$tagNames = explode(',', $info['tagNames'] ?? '');
$tagIds = array_map('intval', explode(',', $info['tagIds'] ?? ''));
$tagVoteUserIds = array_map('intval', explode(',', $info['tagVoteUserIds'] ?? ''));
$tagUpvotes = array_map('intval', explode(',', $info['tagUpvotes'] ?? ''));
$tagDownvotes = array_map('intval', explode(',', $info['tagDownvotes'] ?? ''));
$info['tags'] = [];
for ($n = 0; $n < count($tagIds); ++$n) {
$info['tags'][$tagIds[$n]] = [
'name' => $tagNames[$n],
'id' => $tagIds[$n],
'userId' => $tagVoteUserIds[$n],
'upvotes' => $tagUpvotes[$n],
'downvotes' => $tagDownvotes[$n],
'score' => $tagUpvotes[$n] - $tagDownvotes[$n],
];
}
self::$db->prepared_query("
SELECT t.ID
FROM torrents t
WHERE t.GroupID = ?
ORDER BY t.Remastered, (t.RemasterYear != 0) DESC, t.RemasterYear, t.RemasterTitle,
t.RemasterRecordLabel, t.RemasterCatalogueNumber, t.Media, t.Format, t.Encoding, t.ID
", $this->id
);
$info['torrent_list'] = self::$db->collect(0);
if (!$this->revisionId) {
self::$cache->cache_value($key, $info, 0);
}
if (!$info['Image']) {
$info['Image'] = $this->showFallbackImage
? (STATIC_SERVER . '/common/noartwork/' . CATEGORY_ICON[$info['CategoryID'] - 1])
: null;
}
$info['Flags'] = [
'IsSnatched' => $this->fetchIsSnatched(),
];
$info['from_cache'] = false;
$this->info = $info;
return $this->info;
}
protected function fetchIsSnatched(): bool {
$viewer = $this->requestContext()->viewer();
return $viewer->option('ShowSnatched') && (bool)self::$db->scalar("
SELECT 1
FROM torrents_group tg
WHERE exists(
SELECT 1
FROM torrents t
INNER JOIN xbt_snatched xs ON (xs.fid = t.ID)
WHERE t.GroupID = tg.ID
AND xs.uid = ?
)
AND tg.ID = ?
", $viewer->id, $this->id
);
}
public function artistName(): ?string {
return $this->artistRole()?->text();
}
public function artistRole(): ?ArtistRole\TGroup {
if ($this->categoryName() !== 'Music') {
return null;
}
if (!isset($this->artistRole)) {
$this->artistRole = new ArtistRole\TGroup($this, new Manager\Artist());
}
return $this->artistRole;
}
public function hasArtistRole(): bool {
return $this->artistRole() instanceof ArtistRole\TGroup;
}
public function catalogueNumber(): ?string {
return $this->info()['CatalogueNumber'];
}
public function categoryId(): int {
return $this->info()['CategoryID'];
}
public function categoryGrouped(): bool {
return isset(CATEGORY_GROUPED[$this->categoryId() - 1]);
}
public function categoryName(): string {
return CATEGORY[$this->categoryId() - 1];
}
public function categoryCss(): string {
return 'cats_' . strtolower(str_replace(['-', ' '], '', $this->categoryName()));
}
public function cover(): string {
return $this->image() === ''
? (STATIC_SERVER . '/common/noartwork/' . CATEGORY_ICON[$this->categoryId() - 1])
: $this->image();
}
public function description(): string {
return $this->info()['Body'] ?? '';
}
public function hasNoCoverArt(): bool {
return $this->info()['noCoverArt'] ?? false;
}
public function image(): string {
return $this->info()['Image'];
}
public function isOwner(User $user): bool {
return (bool)self::$db->scalar("
SELECT 1
FROM torrents t
WHERE t.GroupID = ?
AND t.UserID = ?
", $this->id, $user->id
);
}
public function isShowcase(): bool {
return $this->info()['VanityHouse'];
}
public function isSnatched(): bool {
return $this->info()['Flags']['IsSnatched'];
}
public function label(): string {
return $this->id . " (" . $this->name() . ")";
}
public function maxTorrentSize(): int {
return $this->info()['max_torrent_size'];
}
public function mostRecentUpload(): string {
return $this->info()['most_recent_upload'];
}
public function name(): string {
return $this->info()['Name'];
}
public function recordLabel(): ?string {
return $this->info()['RecordLabel'];
}
/**
* @return int Release type (will be null for non-Music categories)
*/
public function releaseType(): ?int {
return $this->info()['ReleaseType'] == 0 ? null : $this->info()['ReleaseType'];
}
public function releaseTypeName(): ?string {
static $releaseTypes;
if (is_null($releaseTypes)) {
$releaseTypes = new ReleaseType()->list();
}
return $this->info()['ReleaseType'] == 0 ? null : $releaseTypes[$this->releaseType()];
}
public function remasterList(): array {
self::$db->prepared_query("
SELECT group_concat(ID) AS id_concat,
RemasterYear AS year,
RemasterTitle AS title,
RemasterRecordLabel AS record_label,
RemasterCatalogueNumber AS catalogue_number
FROM torrents
WHERE Remastered = '1'
AND RemasterYear != 0
AND GroupID = ?
GROUP BY RemasterYear,
cast(RemasterTitle AS binary),
cast(RemasterRecordLabel AS binary),
cast(RemasterCatalogueNumber AS binary)
ORDER BY RemasterYear DESC,
cast(RemasterTitle AS binary) ASC,
cast(RemasterRecordLabel AS binary) ASC,
cast(RemasterCatalogueNumber AS binary) ASC
", $this->id
);
$list = [];
foreach (self::$db->to_array(false, MYSQLI_BOTH) as $item) {
$item['id_list'] = array_map('intval', explode(',', $item['id_concat']));
$list[] = $item;
}
return $list;
}
/**
* Delegate stats methods to the Stats\TGroup class
*/
public function stats(): Stats\TGroup {
if (!isset($this->stats)) {
$this->stats = new Stats\TGroup($this->id);
}
return $this->stats;
}
public function tagList(): array {
return $this->info()['tags'];
}
public function primaryTag(): string {
$tagList = $this->tagList();
return $tagList ? ucfirst(current($tagList)['name']) : '';
}
public function primaryTagCss(): string {
return 'tags_' . str_replace('.', '_', $this->primaryTag());
}
public function tagNameList(): array {
return array_map(fn($t) => $t['name'], $this->tagList());
}
public function hashTag(): string {
return implode(' ', array_map(fn($t) => htmlentities("#$t"), $this->tagNameList()));
}
/**
* To be consistent with requests
*/
public function title(): string {
return $this->info()['Name'];
}
public function time(): string {
return $this->info()['Time'];
}
public function year(): ?int {
return $this->info()['Year'];
}
public function torrentTagList(): array {
$tag = [];
foreach ($this->info()['tags'] as $t) {
$tag[] = "<a href=\"torrents.php?taglist={$t['name']}\">{$t['name']}</a>";
}
return $tag;
}
public function torrentIdList(): array {
return $this->info()['torrent_list'];
}
/**
* Is the user allowed to edit this group? They can if they have the appropriate privilege,
* or, for User or Members etc, if they own an upload in the group (they have a stake in the game) if the user is in
* a entry-level userclass.
*/
public function canEdit(User $user): bool {
return $user->permitted('torrents_edit')
|| (bool)self::$db->scalar("
SELECT 1 FROM torrents WHERE GroupID = ? AND UserID = ? LIMIT 1
", $this->id, $user->id
);
}
public function addCoverArt(string $image, string $summary): int {
self::$db->prepared_query("
INSERT IGNORE INTO cover_art
(GroupID, Image, Summary, UserID)
VALUES (?, ?, ?, ?)
", $this->id, $image, $summary, $this->requestContext()->viewer()->id
);
$id = self::$db->inserted_id();
if ($id) {
$this->logger()->group(
$this,
$this->requestContext()->viewer(),
"Additional cover \"$summary - $image\" added to group"
);
self::$cache->delete_value(sprintf(self::CACHE_COVERART_KEY, $this->id));
}
return $id;
}
public function removeCoverArt(int $coverId): int {
[$image, $summary] = self::$db->row("
SELECT Image, Summary FROM cover_art WHERE ID = ?
", $coverId
);
if (is_null($image)) {
return 0;
}
self::$db->prepared_query("
DELETE FROM cover_art WHERE ID = ?
", $coverId
);
$affected = self::$db->affected_rows();
if ($affected) {
$this->logger()->group(
$this,
$this->requestContext()->viewer(),
"Additional cover \"$summary - $image\" removed from group"
);
self::$cache->delete_value(sprintf(self::CACHE_COVERART_KEY, $this->id));
}
return $affected;
}
public function coverArt(): array {
$key = sprintf(self::CACHE_COVERART_KEY, $this->id);
$list = self::$cache->get_value($key);
if ($list === false) {
self::$db->prepared_query("
SELECT ID AS id,
Image AS image,
Summary AS summary,
UserID AS user_id,
Time AS created
FROM cover_art
WHERE GroupID = ?
ORDER BY Time ASC
", $this->id
);
$list = self::$db->to_array(false, MYSQLI_ASSOC);
self::$cache->cache_value($key, $list, 0);
}
return $list;
}
public function toggleNoCoverArt(bool $noCoverArt): int {
if ($noCoverArt) {
self::$db->prepared_query("
INSERT INTO torrent_group_has_attr
(TorrentGroupID, TorrentGroupAttrID)
VALUES (?, (SELECT ID FROM torrent_group_attr WHERE Name = 'no-cover-art'))
", $this->id
);
} else {
self::$db->prepared_query("
DELETE FROM torrent_group_has_attr
WHERE TorrentGroupAttrID = (SELECT ID FROM torrent_group_attr WHERE Name = 'no-cover-art')
AND TorrentGroupID = ?
", $this->id
);
}
return self::$db->affected_rows();
}
public function primaryArtist(): ?Artist {
$roleList = $this->artistRole()->roleList();
foreach (['dj', 'composer', 'conductor', 'main'] as $role) {
if (count($roleList[$role])) {
return new Artist($roleList[$role][0]['id'], $roleList[$role][0]['aliasid']);
}
}
return null;
}
/**
* Add artists to a group. The role and name arrays must be the same length, and
* are walked down in step, to match the artist with their role in the group
*/
public function addArtists(
array $roles,
array $names,
Manager\Artist $artistMan = new Manager\Artist(),
): int {
$add = [];
$args = [];
$seen = [];
foreach ($this->artistRole()?->idList() ?? [] as $role => $artistList) {
foreach ($artistList as $artistData) {
$seen["$role:{$artistData['aliasid']}"] = true;
}
}
$n = count($names);
for ($i = 0; $i < $n; $i++) {
$role = $roles[$i];
$name = Artist::sanitize($names[$i]);
if (!$name || !in_array($role, array_keys(ARTIST_TYPE))) {
continue;
}
$artist = $artistMan->findByName($name) ?? $artistMan->create($name);
if (!isset($seen["$role:{$artist->aliasId()}"])) {
$seen["$role:{$artist->aliasId()}"] = true;
array_push(
$args,
$this->id,
$this->requestContext()->viewer()->id,
$artist->aliasId(),
$role,
);
$add[] = "{$artist->label()} as " . ARTIST_TYPE[$role];
}
}
$this->artistRole()->flush();
if ($add === []) {
return 0;
}
self::$db->prepared_query("
INSERT INTO torrents_artists
(GroupID, UserID, AliasID, artist_role_id)
VALUES " . placeholders($add, '(?, ?, ?, ?)'),
...$args
);
foreach ($add as $artistLabel) {
$this->logger()
->group(
$this,
$this->requestContext()->viewer(),
"Added artist $artistLabel"
)
->general(
"Artist $artistLabel was added to the group {$this->label()} by user {$this->requestContext()->viewer()->label()}"
);
}
self::$cache->increment_value('stats_album_count', count($names));
$this->refresh();
return count($add);
}
public function removeArtist(Artist $artist, int $role): bool {
self::$db->prepared_query('
DELETE FROM torrents_artists
WHERE GroupID = ?
AND AliasID = ?
AND artist_role_id = ?
', $this->id, $artist->aliasId(), $role
);
if (!self::$db->affected_rows()) {
return false;
}
if ($artist->usageTotal() === 0) {
$artist->remove();
}
$this->artistRole()?->flush();
$this->flush();
return true;
}
/**
* How many unresolved torrent reports are there in this group?
*/
public function unresolvedReportsTotal(): int {
return (int)self::$db->scalar("
SELECT count(*)
FROM reportsv2 AS r
INNER JOIN torrents AS t ON (t.ID = r.TorrentID)
WHERE r.Status != 'Resolved'
AND t.GroupID = ?
", $this->id
);
}
/**
* Update the cache and sphinx delta index to keep everything up-to-date.
*/
public function refresh(): static {
$qid = self::$db->get_query_id();
$voteScore = (int)self::$db->scalar("
SELECT Score FROM torrents_votes WHERE GroupID = ?
", $this->id
);
$artistName = (string)self::$db->scalar("
SELECT group_concat(aa.Name separator ' ')
FROM torrents_artists ta
INNER JOIN artists_alias aa USING (AliasID)
INNER JOIN artist_role ar USING (artist_role_id)
WHERE ar.slug IN ('main', 'composer', 'conductor', 'dj')
AND ta.GroupID = ?
GROUP BY ta.GroupID
", $this->id
);
self::$db->begin_transaction();
// todo: remove this legacy code once TagList replacement is confirmed working
$hasTags = (bool)self::$db->scalar("
SELECT 1 FROM torrents_tags tt WHERE tt.GroupID = ? LIMIT 1
", $this->id
);
if ($hasTags) {
self::$db->prepared_query("
UPDATE torrents_group SET
TagList = (
SELECT REPLACE(GROUP_CONCAT(tags.Name SEPARATOR ' '), '.', '_')
FROM torrents_tags AS t
INNER JOIN tags ON (tags.ID = t.TagID)
WHERE t.GroupID = ?
GROUP BY t.GroupID
)
WHERE ID = ?
", $this->id, $this->id
);
}
self::$db->prepared_query("
REPLACE INTO sphinx_delta
(ID, GroupID, GroupName, Year, CategoryID, Time, ReleaseType, RecordLabel,
CatalogueNumber, VanityHouse, Size, Snatched, Seeders, Leechers, LogScore, Scene, HasLog,
HasCue, FreeTorrent, Media, Format, Encoding, Description, RemasterYear, RemasterTitle,
RemasterRecordLabel, RemasterCatalogueNumber, FileList, TagList, VoteScore, ArtistName)
SELECT
t.ID, g.ID, g.Name, g.Year, g.CategoryID, t.created, g.ReleaseType,
g.RecordLabel, g.CatalogueNumber, g.VanityHouse, t.Size, tls.Snatched, tls.Seeders,
tls.Leechers, t.LogScore, cast(t.Scene AS CHAR), cast(t.HasLog AS CHAR), cast(t.HasCue AS CHAR),
cast(t.FreeTorrent AS CHAR), t.Media, t.Format, t.Encoding, t.Description,
coalesce(t.RemasterYear, 0), t.RemasterTitle, t.RemasterRecordLabel, t.RemasterCatalogueNumber,
replace(replace(t.FileList, '_', ' '), '/', ' ') AS FileList,
replace(group_concat(coalesce(t2.Name, '') SEPARATOR ' '), '.', '_'), ?, ?
FROM torrents t
INNER JOIN torrents_leech_stats tls ON (tls.TorrentID = t.ID)
INNER JOIN torrents_group g ON (g.ID = t.GroupID)
LEFT JOIN torrents_tags tt ON (tt.GroupID = g.ID)
LEFT JOIN tags t2 ON (t2.ID = tt.TagID)
WHERE g.ID = ?
GROUP BY t.ID
", $voteScore, shortenString($artistName, 2048, false, false), $this->id
);
self::$db->commit();
$this->flush();
self::$db->set_query_id($qid);
return $this;
}
/**
* Insert a new revision (description or image has changed).
*
* return int revision id
*/
public function createRevision(string $description, ?string $image, string $summary): int {
self::$db->prepared_query("
INSERT INTO wiki_torrents
(PageID, Body, Image, UserID, Summary)
VALUES (?, ?, ?, ?, ?)
", $this->id,
$description,
$image,
$this->requestContext()->viewer()->id,
mb_substr(trim($summary), 0, 100),
);
$revisionId = self::$db->inserted_id();
self::$db->prepared_query("
UPDATE torrents_group SET
RevisionID = ?
WHERE ID = ?
", $revisionId, $this->id
);
$this->refresh();
return $revisionId;
}
public function revertRevision(int $userId, int $revisionId): ?array {
$revert = self::$db->row("
SELECT Body, Image
FROM wiki_torrents
WHERE PageID = ? AND RevisionID = ?
", $this->id, $revisionId
);
if (is_null($revert)) {
return null;
}
self::$db->prepared_query("
INSERT INTO wiki_torrents
(PageID, Body, Image, UserID, Summary)
SELECT ?, Body, Image, ?, ?
FROM wiki_torrents
WHERE RevisionID = ?
", $this->id, $userId, "Reverted to revision $revisionId",
$revisionId
);
return $revert;
}
public function setFreeleech(
User $user,
LeechType $leechType,
LeechReason $reason,
bool $all = false,
int $threshold = 0,
Manager\Torrent $torMan = new Manager\Torrent(),
Tracker $tracker = new Tracker(),
): int {
$regular = [];
$large = [];
foreach ($this->torrentIdList() as $torrentId) {
$torrent = $torMan->findById($torrentId);
if ($all || $torrent->format() == 'FLAC') {
if ($threshold > 0 and $torrent->size() > $threshold) {
$large[] = $torrent->id;
} else {
$regular[] = $torrent->id;
}
}
}
if ($regular) {
$torMan->setListFreeleech($tracker, $user, $regular, $leechType, $reason);
}
if ($large) {
$torMan->setListFreeleech($tracker, $user, $large, LeechType::Neutral, $reason);
}
return count($regular) + count($large);
}
public function absorb(Torrent $torrent): int {
self::$db->begin_transaction();
self::$db->prepared_query("
UPDATE torrents SET
GroupID = ?
WHERE ID = ?
", $this->id, $torrent->id
);
$affected = self::$db->affected_rows();
$old = $torrent->group();
$oldId = $old->id;
if ((bool)self::$db->scalar("SELECT count(*) FROM torrents WHERE GroupID = ?", $oldId)) {
$old->flush();
$old->refresh();
} else {
new Manager\Bookmark()->merge($old, $this);
new Manager\Comment()->merge('torrents', $oldId, $this->id);
new Manager\Vote()->merge($old, $this, new Manager\User());
$this->logger()->merge($old, $this);
$old->removeTGroup();
}
$this->logger()
->group(
$this,
$this->requestContext()->viewer(),
"merged group $oldId"
)
->general(
"Torrent {$torrent->id} was edited by {$this->requestContext()->viewer()->label()}"
);
self::$db->commit();
$this->flush()->refresh();
$torrent->uploader()->flushRecentUpload();
$torrent->flush();
return $affected;
}
public function rename(string $name): bool {
$oldName = $this->name();
$success = $this->setField('Name', $name)->modify();
if ($success) {
$this->refresh();
$this->logger()
->group(
$this, $this->requestContext()->viewer(), "renamed to \"$name\" from \"$oldName\""
)
->general(
"Torrent Group {$this->id} was renamed to \"$name\" from \"$oldName\" by {$this->requestContext()->viewer()->username()}"
);
}
return $success;
}
/**
* Return info about the deleted masterings of a torrent group.
*
* @return array of strings imploded by '!!'
* [torrent_id, remastered, title, year, record_label, catalogue_number]
*/
public function deletedMasteringList(): array {
self::$db->prepared_query("
SELECT d.Media AS media,
d.Remastered = '1' AS remastered,
d.RemasterTitle AS title,
if(d.Remastered = '1', d.RemasterYear, tg.Year) AS year,
if(d.Remastered = '1', d.RemasterRecordLabel, tg.RecordLabel) AS record_label,
if(d.Remastered = '1', d.RemasterCatalogueNumber, tg.CatalogueNumber) AS catalogue_number
FROM deleted_torrents d
LEFT JOIN torrents_group tg ON (tg.ID = d.GroupID)
WHERE d.GroupID = ?
GROUP BY remastered, year, title, record_label, catalogue_number, media
ORDER BY remastered, year, title, record_label, catalogue_number, media
", $this->id
);
return self::$db->to_array(false, MYSQLI_ASSOC);
}
/**
* In order to let the BaseObject remove() machinery do its job, a certain
* number of preparatory operations need to be performed first.
*/
public function removeTGroup(): int {
$isMusic = ($this->categoryName() === 'Music');
// Artists
// Collect the artist IDs and then wipe the torrents_artist entry
self::$db->prepared_query("
SELECT DISTINCT AliasID FROM torrents_artists WHERE GroupID = ?
", $this->id
);
$artistList = self::$db->collect(0);
self::$db->prepared_query("
DELETE FROM torrents_artists WHERE GroupID = ?
", $this->id
);
$artistMan = new Manager\Artist();
foreach ($artistList as $artistId) {
$artist = $artistMan->findByAliasId($artistId);
if ($artist) {
if ($artist->usageTotal() === 0) {
$artist->remove();
} else {
$this->flush();
}
}
}
// Bookmarks
self::$db->prepared_query("
DELETE FROM bookmarks_torrents WHERE GroupID = ?
", $this->id
);
$this->pg()->prepared_query("
delete from bookmark_tgroup where id_tgroup = ?
", $this->id
);
// Collages
self::$db->prepared_query("
SELECT CollageID FROM collages_torrents WHERE GroupID = ?
", $this->id
);
$CollageIDs = self::$db->collect(0);
if ($CollageIDs) {
self::$db->prepared_query("
UPDATE collages SET
NumTorrents = NumTorrents - 1
WHERE ID IN (" . placeholders($CollageIDs) . ")
", ...$CollageIDs
);
self::$db->prepared_query("
DELETE FROM collages_torrents WHERE GroupID = ?
", $this->id
);
foreach ($CollageIDs as $CollageID) {
self::$cache->delete_value(sprintf(Collage::CACHE_KEY, $CollageID));
}
self::$cache->delete_value("torrent_collages_" . $this->id);
}
new Manager\Comment()->remove('torrents', $this->id);
// Requests
self::$db->prepared_query("
SELECT ID FROM requests WHERE GroupID = ?
", $this->id
);
$Requests = self::$db->collect(0);
self::$db->prepared_query("
UPDATE requests SET
GroupID = NULL
WHERE GroupID = ?
", $this->id
);
foreach ($Requests as $RequestID) {
self::$cache->delete_value("request_$RequestID");
}
self::$db->prepared_query("
DELETE FROM torrents_tags WHERE GroupID = ?
", $this->id
);
self::$db->prepared_query("
DELETE FROM torrents_tags_votes WHERE GroupID = ?
", $this->id
);
self::$db->prepared_query("
DELETE FROM wiki_torrents WHERE PageID = ?
", $this->id
);
$manager = new DB();
$manager->relaxConstraints(true);
// we don´t delete the row just yet
[$ok, $message] = $manager->softDelete(
MYSQL_DB, 'torrents_group', 'ID', $this->id, delete: false
);
$manager->relaxConstraints(false);
if (!$ok) {
return 0;
}
parent::remove();
if ($isMusic) {
self::$cache->decrement('stats_album_count');
}
self::$cache->decrement('stats_group_count');
self::$cache->delete_multi([
"groups_artists_{$this->id}",
"torrent_group_{$this->id}",
sprintf(self::CACHE_KEY, $this->id),
sprintf(self::CACHE_TLIST_KEY, $this->id),
sprintf(Manager\TGroup::ID_KEY, $this->id),
sprintf(Manager\Torrent::CACHE_KEY_LATEST_UPLOADS, 5),
]);
return (int)$ok;
}
}