mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 22:01:33 +00:00
578 lines
21 KiB
PHP
578 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Facades\Search;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class DeleteReleases extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'nntmux:delete-releases
|
|
{criteria?* : Advanced criteria in original format}
|
|
{--group= : Group name to filter by}
|
|
{--group-like= : Group name pattern to filter by (partial match)}
|
|
{--poster= : Poster name (fromname) to filter by}
|
|
{--poster-like= : Poster name pattern to filter by (partial match)}
|
|
{--name= : Release name to filter by}
|
|
{--name-like= : Release name pattern to filter by (partial match)}
|
|
{--search= : Search name to filter by}
|
|
{--search-like= : Search name pattern to filter by (partial match)}
|
|
{--category= : Category ID to filter by}
|
|
{--guid= : Specific GUID to filter by}
|
|
{--size-min= : Minimum size in bytes}
|
|
{--size-max= : Maximum size in bytes}
|
|
{--size= : Exact size in bytes}
|
|
{--hours-old= : Delete releases older than X hours}
|
|
{--hours-new= : Delete releases newer than X hours}
|
|
{--parts-min= : Minimum number of parts}
|
|
{--parts-max= : Maximum number of parts}
|
|
{--parts= : Exact number of parts}
|
|
{--completion-max= : Maximum completion percentage}
|
|
{--nzb-status= : NZB status to filter by}
|
|
{--imdb= : IMDB ID to filter by (use NULL for no IMDB)}
|
|
{--rage= : Rage ID to filter by}
|
|
{--dry-run : Show what would be deleted without actually deleting}
|
|
{--force : Skip confirmation prompt}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Delete releases based on specified criteria';
|
|
|
|
/**
|
|
* Create a new command instance.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle(): int
|
|
{
|
|
$dryRun = $this->option('dry-run');
|
|
$force = $this->option('force');
|
|
|
|
// Build criteria array from simple options
|
|
$criteria = $this->buildCriteriaFromOptions();
|
|
|
|
// Add any advanced criteria
|
|
$advancedCriteria = $this->argument('criteria');
|
|
if (! empty($advancedCriteria)) {
|
|
$criteria = array_merge($criteria, $advancedCriteria);
|
|
}
|
|
|
|
if (empty($criteria)) {
|
|
$this->showUsage();
|
|
|
|
return 1;
|
|
}
|
|
|
|
$this->info('Delete Releases Command');
|
|
$this->newLine();
|
|
|
|
if ($dryRun) {
|
|
$this->warn('DRY RUN MODE - No releases will actually be deleted');
|
|
$this->newLine();
|
|
|
|
return $this->performDryRun($criteria);
|
|
}
|
|
|
|
// Show confirmation unless force flag is set
|
|
if (! $force) {
|
|
$this->warn('You are about to delete releases matching the following criteria:');
|
|
foreach ($criteria as $criterion) {
|
|
$this->line(" - {$criterion}");
|
|
}
|
|
$this->newLine();
|
|
|
|
if (! $this->confirm('Are you sure you want to proceed with the deletion?')) {
|
|
$this->info('Operation cancelled.');
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
try {
|
|
$this->info('Starting release deletion process...');
|
|
|
|
// Build the query to find releases to delete
|
|
$query = $this->buildQueryFromCriteria($criteria);
|
|
if (! $query) {
|
|
$this->error('Could not build query from criteria.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Get releases to delete
|
|
$releases = DB::select($query);
|
|
if (empty($releases)) {
|
|
$this->info('No releases found matching the specified criteria.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
$count = count($releases);
|
|
$this->info("Found {$count} release(s) to delete...");
|
|
|
|
$deleted = 0;
|
|
foreach ($releases as $release) {
|
|
try {
|
|
// Delete the release using direct database operations
|
|
$releaseId = $release->id;
|
|
|
|
$releaseName = $release->searchname ?: ($release->name ?: 'Unknown');
|
|
$this->line("Deleting: {$releaseName}");
|
|
|
|
// Delete related data first (to maintain referential integrity)
|
|
DB::delete('DELETE FROM user_downloads WHERE releases_id = ?', [$releaseId]);
|
|
DB::delete('DELETE FROM users_releases WHERE releases_id = ?', [$releaseId]);
|
|
DB::delete('DELETE FROM release_files WHERE releases_id = ?', [$releaseId]);
|
|
DB::delete('DELETE FROM release_comments WHERE releases_id = ?', [$releaseId]);
|
|
DB::delete('DELETE FROM release_nfos WHERE releases_id = ?', [$releaseId]);
|
|
DB::delete('DELETE FROM release_subtitles WHERE releases_id = ?', [$releaseId]);
|
|
|
|
// Delete the main release record
|
|
DB::delete('DELETE FROM releases WHERE id = ?', [$releaseId]);
|
|
|
|
// Delete from search indexes
|
|
Search::deleteRelease($releaseId);
|
|
|
|
$deleted++;
|
|
|
|
// Show progress for large deletions
|
|
if ($deleted % 100 == 0) {
|
|
$this->info("Deleted {$deleted}/{$count} releases...");
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$releaseName = $release->searchname ?? 'Unknown';
|
|
$this->warn("Failed to delete release {$releaseName}: ".$e->getMessage());
|
|
}
|
|
}
|
|
|
|
$this->info("Successfully deleted {$deleted} release(s).");
|
|
|
|
return 0;
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error('An error occurred: '.$e->getMessage());
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform a dry run to show what releases would be deleted.
|
|
*
|
|
* @param array<string, mixed> $criteria
|
|
*/
|
|
protected function performDryRun(array $criteria): int
|
|
{
|
|
try {
|
|
$this->info('Analyzing releases matching the following criteria:');
|
|
foreach ($criteria as $criterion) {
|
|
$this->line(" - {$criterion}");
|
|
}
|
|
$this->newLine();
|
|
|
|
// Get the query that would be executed
|
|
$query = $this->buildQueryFromCriteria($criteria);
|
|
|
|
if (! $query) {
|
|
$this->error('Could not build query from criteria.');
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Execute the query to get preview results
|
|
$releases = DB::select($query);
|
|
|
|
if (empty($releases)) {
|
|
$this->info('No releases found matching the specified criteria.');
|
|
|
|
return 0;
|
|
}
|
|
|
|
$count = count($releases);
|
|
$this->info("Found {$count} release(s) that would be deleted:");
|
|
$this->newLine();
|
|
|
|
// Show sample of releases (first 10)
|
|
$displayCount = min(10, $count);
|
|
for ($i = 0; $i < $displayCount; $i++) {
|
|
$release = $releases[$i];
|
|
$releaseName = isset($release->searchname) ? $release->searchname : (isset($release->name) ? $release->name : 'Unknown');
|
|
$releaseGuid = isset($release->guid) ? $release->guid : $release->id;
|
|
$this->line(sprintf(' [%s] %s', $releaseGuid, $releaseName));
|
|
}
|
|
|
|
if ($count > 10) {
|
|
$this->line(' ... and '.($count - 10).' more releases');
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->warn("Total: {$count} release(s) would be deleted");
|
|
$this->info('Use without --dry-run to actually delete these releases');
|
|
|
|
return 0;
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error('Error during dry run: '.$e->getMessage());
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build the SQL query from criteria array.
|
|
*
|
|
* @param array<string, mixed> $criteria
|
|
*/
|
|
protected function buildQueryFromCriteria(array $criteria): ?string
|
|
{
|
|
// Start with base query
|
|
$query = 'SELECT id, guid, searchname, name FROM releases WHERE 1=1';
|
|
|
|
foreach ($criteria as $criterion) {
|
|
if ($criterion === 'ignore') {
|
|
continue;
|
|
}
|
|
|
|
$queryPart = $this->formatCriterionToSql($criterion);
|
|
if ($queryPart) {
|
|
$query .= $queryPart;
|
|
}
|
|
}
|
|
|
|
return $this->cleanSpaces($query);
|
|
}
|
|
|
|
/**
|
|
* Convert a single criterion to SQL WHERE clause.
|
|
*/
|
|
protected function formatCriterionToSql(string $criterion): ?string
|
|
{
|
|
$args = explode('=', $criterion);
|
|
if (count($args) !== 3) {
|
|
return null;
|
|
}
|
|
|
|
$column = trim($args[0]);
|
|
$modifier = trim($args[1]);
|
|
$value = trim($args[2], '"\'');
|
|
|
|
switch ($column) {
|
|
case 'categories_id':
|
|
if ($modifier === 'equals') {
|
|
return " AND categories_id = {$value}";
|
|
}
|
|
break;
|
|
case 'imdbid':
|
|
if ($modifier === 'equals') {
|
|
if ($value === 'NULL') {
|
|
return ' AND imdbid IS NULL';
|
|
}
|
|
|
|
return " AND imdbid = {$value}";
|
|
}
|
|
break;
|
|
case 'nzbstatus':
|
|
if ($modifier === 'equals') {
|
|
return " AND nzbstatus = {$value}";
|
|
}
|
|
break;
|
|
case 'rageid':
|
|
if ($modifier === 'equals') {
|
|
return " AND rageid = {$value}";
|
|
}
|
|
break;
|
|
case 'totalpart':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
return " AND totalpart = {$value}";
|
|
case 'bigger':
|
|
return " AND totalpart > {$value}";
|
|
case 'smaller':
|
|
return " AND totalpart < {$value}";
|
|
}
|
|
break;
|
|
case 'completion':
|
|
if ($modifier === 'smaller') {
|
|
return " AND completion < {$value}";
|
|
}
|
|
break;
|
|
case 'size':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
return " AND size = {$value}";
|
|
case 'bigger':
|
|
return " AND size > {$value}";
|
|
case 'smaller':
|
|
return " AND size < {$value}";
|
|
}
|
|
break;
|
|
case 'adddate':
|
|
switch ($modifier) {
|
|
case 'bigger':
|
|
return " AND adddate < (NOW() - INTERVAL {$value} HOUR)";
|
|
case 'smaller':
|
|
return " AND adddate > (NOW() - INTERVAL {$value} HOUR)";
|
|
}
|
|
break;
|
|
case 'postdate':
|
|
switch ($modifier) {
|
|
case 'bigger':
|
|
return " AND postdate < (NOW() - INTERVAL {$value} HOUR)";
|
|
case 'smaller':
|
|
return " AND postdate > (NOW() - INTERVAL {$value} HOUR)";
|
|
}
|
|
break;
|
|
case 'fromname':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
return ' AND fromname = '.DB::connection()->getPdo()->quote($value);
|
|
case 'like':
|
|
return ' AND fromname LIKE '.DB::connection()->getPdo()->quote('%'.str_replace(' ', '%', $value).'%');
|
|
}
|
|
break;
|
|
case 'groupname':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
$group = DB::select('SELECT id FROM usenet_groups WHERE name = ?', [$value]);
|
|
if (! empty($group)) {
|
|
return " AND groups_id = {$group[0]->id}";
|
|
}
|
|
break;
|
|
case 'like':
|
|
$groups = DB::select('SELECT id FROM usenet_groups WHERE name LIKE ?', ['%'.str_replace(' ', '%', $value).'%']);
|
|
if (! empty($groups)) {
|
|
$ids = array_column($groups, 'id');
|
|
|
|
return ' AND groups_id IN ('.implode(',', $ids).')';
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 'guid':
|
|
if ($modifier === 'equals') {
|
|
return ' AND guid = '.DB::connection()->getPdo()->quote($value);
|
|
}
|
|
break;
|
|
case 'name':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
return ' AND name = '.DB::connection()->getPdo()->quote($value);
|
|
case 'like':
|
|
return ' AND name LIKE '.DB::connection()->getPdo()->quote('%'.str_replace(' ', '%', $value).'%');
|
|
}
|
|
break;
|
|
case 'searchname':
|
|
switch ($modifier) {
|
|
case 'equals':
|
|
return ' AND searchname = '.DB::connection()->getPdo()->quote($value);
|
|
case 'like':
|
|
return ' AND searchname LIKE '.DB::connection()->getPdo()->quote('%'.str_replace(' ', '%', $value).'%');
|
|
}
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Clean multiple spaces from a string.
|
|
*/
|
|
protected function cleanSpaces(string $string): string
|
|
{
|
|
return preg_replace('/\s+/', ' ', trim($string));
|
|
}
|
|
|
|
/**
|
|
* Build criteria array from simple command options.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function buildCriteriaFromOptions(): array
|
|
{
|
|
$criteria = [];
|
|
|
|
// Group filters
|
|
if ($this->option('group')) {
|
|
$criteria[] = 'groupname=equals="'.$this->option('group').'"';
|
|
}
|
|
if ($this->option('group-like')) {
|
|
$criteria[] = 'groupname=like="'.$this->option('group-like').'"';
|
|
}
|
|
|
|
// Poster filters
|
|
if ($this->option('poster')) {
|
|
$criteria[] = 'fromname=equals="'.$this->option('poster').'"';
|
|
}
|
|
if ($this->option('poster-like')) {
|
|
$criteria[] = 'fromname=like="'.$this->option('poster-like').'"';
|
|
}
|
|
|
|
// Name filters
|
|
if ($this->option('name')) {
|
|
$criteria[] = 'name=equals="'.$this->option('name').'"';
|
|
}
|
|
if ($this->option('name-like')) {
|
|
$criteria[] = 'name=like="'.$this->option('name-like').'"';
|
|
}
|
|
|
|
// Search name filters
|
|
if ($this->option('search')) {
|
|
$criteria[] = 'searchname=equals="'.$this->option('search').'"';
|
|
}
|
|
if ($this->option('search-like')) {
|
|
$criteria[] = 'searchname=like="'.$this->option('search-like').'"';
|
|
}
|
|
|
|
// Category filter
|
|
if ($this->option('category')) {
|
|
$criteria[] = 'categories_id=equals='.$this->option('category');
|
|
}
|
|
|
|
// GUID filter
|
|
if ($this->option('guid')) {
|
|
$criteria[] = 'guid=equals="'.$this->option('guid').'"';
|
|
}
|
|
|
|
// Size filters
|
|
if ($this->option('size')) {
|
|
$criteria[] = 'size=equals='.$this->option('size');
|
|
}
|
|
if ($this->option('size-min')) {
|
|
$criteria[] = 'size=bigger='.$this->option('size-min');
|
|
}
|
|
if ($this->option('size-max')) {
|
|
$criteria[] = 'size=smaller='.$this->option('size-max');
|
|
}
|
|
|
|
// Age filters
|
|
if ($this->option('hours-old')) {
|
|
$criteria[] = 'adddate=bigger='.$this->option('hours-old');
|
|
}
|
|
if ($this->option('hours-new')) {
|
|
$criteria[] = 'adddate=smaller='.$this->option('hours-new');
|
|
}
|
|
|
|
// Parts filters
|
|
if ($this->option('parts')) {
|
|
$criteria[] = 'totalpart=equals='.$this->option('parts');
|
|
}
|
|
if ($this->option('parts-min')) {
|
|
$criteria[] = 'totalpart=bigger='.$this->option('parts-min');
|
|
}
|
|
if ($this->option('parts-max')) {
|
|
$criteria[] = 'totalpart=smaller='.$this->option('parts-max');
|
|
}
|
|
|
|
// Completion filter
|
|
if ($this->option('completion-max')) {
|
|
$criteria[] = 'completion=smaller='.$this->option('completion-max');
|
|
}
|
|
|
|
// Status filters
|
|
if ($this->option('nzb-status') !== null) {
|
|
$criteria[] = 'nzbstatus=equals='.$this->option('nzb-status');
|
|
}
|
|
|
|
// IMDB filter
|
|
if ($this->option('imdb') !== null) {
|
|
$criteria[] = 'imdbid=equals='.$this->option('imdb');
|
|
}
|
|
|
|
// Rage filter
|
|
if ($this->option('rage') !== null) {
|
|
$criteria[] = 'rageid=equals='.$this->option('rage');
|
|
}
|
|
|
|
return $criteria;
|
|
}
|
|
|
|
/**
|
|
* Show usage information for the command.
|
|
*/
|
|
protected function showUsage(): void
|
|
{
|
|
$this->info('Delete releases based on various criteria.');
|
|
$this->newLine();
|
|
|
|
$this->info('Simple Usage Examples:');
|
|
$this->line('# Delete releases from a specific group');
|
|
$this->line('php artisan nntmux:delete-releases --group="alt.binaries.teevee"');
|
|
$this->newLine();
|
|
|
|
$this->line('# Delete releases by poster name pattern');
|
|
$this->line('php artisan nntmux:delete-releases --poster-like="@spam.com"');
|
|
$this->newLine();
|
|
|
|
$this->line('# Delete small releases (under 100MB)');
|
|
$this->line('php artisan nntmux:delete-releases --size-max=104857600');
|
|
$this->newLine();
|
|
|
|
$this->line('# Delete old releases (older than 30 days)');
|
|
$this->line('php artisan nntmux:delete-releases --hours-old=720');
|
|
$this->newLine();
|
|
|
|
$this->line('# Delete releases from specific category');
|
|
$this->line('php artisan nntmux:delete-releases --category=2999');
|
|
$this->newLine();
|
|
|
|
$this->line('# Combine multiple criteria');
|
|
$this->line('php artisan nntmux:delete-releases --group-like="movies" --size-max=1000000 --hours-old=168');
|
|
$this->newLine();
|
|
|
|
$this->line('# Preview what would be deleted (dry run)');
|
|
$this->line('php artisan nntmux:delete-releases --dry-run --group-like="spam"');
|
|
$this->newLine();
|
|
|
|
$this->line('# Skip confirmation prompt');
|
|
$this->line('php artisan nntmux:delete-releases --force --poster-like="spammer"');
|
|
$this->newLine();
|
|
|
|
$this->info('Available Options:');
|
|
$this->line('--group : Exact group name');
|
|
$this->line('--group-like : Group name pattern');
|
|
$this->line('--poster : Exact poster name');
|
|
$this->line('--poster-like : Poster name pattern');
|
|
$this->line('--name : Exact release name');
|
|
$this->line('--name-like : Release name pattern');
|
|
$this->line('--search : Exact search name');
|
|
$this->line('--search-like : Search name pattern');
|
|
$this->line('--category : Category ID');
|
|
$this->line('--guid : Specific GUID');
|
|
$this->line('--size : Exact size in bytes');
|
|
$this->line('--size-min : Minimum size in bytes');
|
|
$this->line('--size-max : Maximum size in bytes');
|
|
$this->line('--hours-old : Delete releases older than X hours');
|
|
$this->line('--hours-new : Delete releases newer than X hours');
|
|
$this->line('--parts : Exact number of parts');
|
|
$this->line('--parts-min : Minimum number of parts');
|
|
$this->line('--parts-max : Maximum number of parts');
|
|
$this->line('--completion-max : Maximum completion percentage');
|
|
$this->line('--nzb-status : NZB status');
|
|
$this->line('--imdb : IMDB ID (use NULL for no IMDB)');
|
|
$this->line('--rage : Rage ID');
|
|
$this->line('--dry-run : Preview without deleting');
|
|
$this->line('--force : Skip confirmation');
|
|
$this->newLine();
|
|
|
|
$this->info('Advanced Usage:');
|
|
$this->line('You can still use the original complex criteria format:');
|
|
$this->line('php artisan nntmux:delete-releases groupname=equals="alt.binaries.teevee" searchname=like="olympics 2014"');
|
|
}
|
|
}
|