Prevent creation of par2 only releases

This commit is contained in:
DariusIII
2026-02-23 15:59:40 +01:00
parent eebecbac7e
commit d1d752d410
3 changed files with 93 additions and 3 deletions
+58 -2
View File
@@ -422,7 +422,24 @@ final class ReleaseProcessingService
? UsenetGroup::getActiveIDs()
: [['id' => $normalizedGroupId]];
$stats = ['minSize' => 0, 'maxSize' => 0, 'minFiles' => 0];
$stats = ['minSize' => 0, 'maxSize' => 0, 'minFiles' => 0, 'par2Only' => 0];
// Delete collections where ALL binaries are par2 files (no actual content)
DB::transaction(static function () use (&$stats): void {
$par2OnlyCollectionIds = DB::table('collections as c')
->join('binaries as b', 'c.id', '=', 'b.collections_id')
->where('c.filecheck', CollectionFileCheckStatus::Sized->value)
->where('c.filesize', '>', 0)
->groupBy('c.id')
->havingRaw('COUNT(b.id) = SUM(CASE WHEN b.name REGEXP %s THEN 1 ELSE 0 END)', ['\\.par2'])
->pluck('c.id');
if ($par2OnlyCollectionIds->isNotEmpty()) {
$stats['par2Only'] += Collection::query()
->whereIn('id', $par2OnlyCollectionIds)
->delete();
}
}, 10);
foreach ($groupIDs as $grpID) {
$groupSettings = UsenetGroup::getGroupByID($grpID['id']);
@@ -618,6 +635,7 @@ final class ReleaseProcessingService
$stats = $this->deleteDisabledCategoryReleases($stats);
$stats = $this->deleteCategoryMinSizeReleases($stats);
$stats = $this->deleteDisabledGenreReleases($stats);
$stats = $this->deletePar2OnlyReleases($stats);
$stats = $this->deleteMiscReleases($stats);
$this->outputReleaseDeleteStats($stats, $startTime);
@@ -1205,6 +1223,38 @@ final class ReleaseProcessingService
return $stats;
}
/**
* Delete releases that contain only PAR2 files (no actual content).
*
* PAR2-only releases are useless since they only contain repair/verification
* data without the original content files they are meant to repair.
*/
private function deletePar2OnlyReleases(ReleaseDeleteStats $stats): ReleaseDeleteStats
{
// Find releases where ALL associated release_files have names ending in .par2
$par2OnlyReleaseIds = DB::table('releases as r')
->join('release_files as rf', 'r.id', '=', 'rf.releases_id')
->groupBy('r.id')
->havingRaw('COUNT(rf.id) = SUM(CASE WHEN rf.name REGEXP %s THEN 1 ELSE 0 END)', ['\\.par2$'])
->pluck('r.id');
if ($par2OnlyReleaseIds->isNotEmpty()) {
Release::query()
->whereIn('id', $par2OnlyReleaseIds)
->select(['id', 'guid'])
->chunkById(self::BATCH_SIZE, function ($releases) use (&$stats): bool {
foreach ($releases as $release) {
$this->deleteSingleRelease($release);
$stats = $stats->increment('par2Only');
}
return true;
});
}
return $stats;
}
private function deleteMiscReleases(ReleaseDeleteStats $stats): ReleaseDeleteStats
{
if ($this->settings->miscOtherRetentionHours > 0) {
@@ -1396,12 +1446,15 @@ final class ReleaseProcessingService
*/
private function outputCollectionDeleteStats(array $stats, DateTimeInterface $startTime): void
{
$totalDeleted = $stats['minSize'] + $stats['maxSize'] + $stats['minFiles'];
$totalDeleted = $stats['minSize'] + $stats['maxSize'] + $stats['minFiles'] + ($stats['par2Only'] ?? 0);
if ($totalDeleted > 0) {
$this->outputStat('Too small', $stats['minSize']);
$this->outputStat('Too large', $stats['maxSize']);
$this->outputStat('Too few files', $stats['minFiles']);
if (($stats['par2Only'] ?? 0) > 0) {
$this->outputStat('Par2 only', $stats['par2Only']);
}
$this->outputStat('Total removed', $totalDeleted);
} else {
$this->outputInfo('No collections filtered');
@@ -1460,6 +1513,9 @@ final class ReleaseProcessingService
if ($stats->miscHashed > 0) {
$this->outputStat('Misc->Hashed expired', $stats->miscHashed);
}
if ($stats->par2Only > 0) {
$this->outputStat('Par2-only releases', $stats->par2Only);
}
$this->outputStat('Total releases removed', $total);
} else {
+29
View File
@@ -48,6 +48,8 @@ class ReleaseRemoverService
private const string TYPE_WMV_ALL = 'wmv_all';
private const string TYPE_PAR2ONLY = 'par2only';
protected string $blacklistID = '';
protected string $crapTime = '';
@@ -118,6 +120,7 @@ class ReleaseRemoverService
self::TYPE_NZB => fn () => $this->removeSingleNZB(),
self::TYPE_CODEC => fn () => $this->removeCodecPoster(),
self::TYPE_WMV_ALL => fn () => $this->removeWMV(),
self::TYPE_PAR2ONLY => fn () => $this->removePar2Only(),
];
}
@@ -272,6 +275,7 @@ class ReleaseRemoverService
self::TYPE_SIZE,
self::TYPE_NZB,
self::TYPE_CODEC,
self::TYPE_PAR2ONLY,
];
foreach ($defaultTypes as $removalType) {
@@ -858,6 +862,31 @@ class ReleaseRemoverService
GROUP BY r.id {$this->crapTime}");
}
/**
* Remove releases that contain only PAR2 files (no actual content files).
*
* These releases are useless since PAR2 files are only repair/verification
* data and cannot be used without the original content files.
*
* @throws Exception
*/
protected function removePar2Only(): bool|string
{
return $this->executeSimpleRemoval('Par2Only', sprintf(
"SELECT r.guid, r.searchname, r.id
FROM releases r
INNER JOIN release_files rf ON r.id = rf.releases_id
WHERE r.id NOT IN (
SELECT rf2.releases_id
FROM release_files rf2
WHERE rf2.name NOT REGEXP '\\.par2$'
)
GROUP BY r.id, r.guid, r.searchname
%s",
$this->crapTime
));
}
/**
* Build a comma-separated list of category IDs.
*
+6 -1
View File
@@ -21,6 +21,7 @@ final readonly class ReleaseDeleteStats
public int $disabledGenre = 0,
public int $miscOther = 0,
public int $miscHashed = 0,
public int $par2Only = 0,
) {}
/**
@@ -38,6 +39,7 @@ final readonly class ReleaseDeleteStats
'disabledGenre' => $this->disabledGenre,
'miscOther' => $this->miscOther,
'miscHashed' => $this->miscHashed,
'par2Only' => $this->par2Only,
];
if (isset($values[$field])) {
@@ -60,7 +62,8 @@ final readonly class ReleaseDeleteStats
+ $this->categoryMinSize
+ $this->disabledGenre
+ $this->miscOther
+ $this->miscHashed;
+ $this->miscHashed
+ $this->par2Only;
}
/**
@@ -80,6 +83,7 @@ final readonly class ReleaseDeleteStats
'disabledGenre' => $this->disabledGenre,
'miscOther' => $this->miscOther,
'miscHashed' => $this->miscHashed,
'par2Only' => $this->par2Only,
];
}
@@ -100,6 +104,7 @@ final readonly class ReleaseDeleteStats
disabledGenre: $data['disabledGenre'] ?? 0,
miscOther: $data['miscOther'] ?? 0,
miscHashed: $data['miscHashed'] ?? 0,
par2Only: $data['par2Only'] ?? 0,
);
}
}