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 $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 $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 */ 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"'); } }