Files
newznab-tmux-NNTmux/app/Services/ForkingService.php
T
2026-08-10 20:12:00 +02:00

322 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Settings;
use App\Services\Runners\BackfillRunner;
use App\Services\Runners\BinariesRunner;
use App\Services\Runners\PostProcessRunner;
use App\Services\Runners\ReleasesRunner;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Process;
/**
* Service for multiprocessing various tasks.
*
* This service orchestrates parallel processing of usenet indexing tasks
* by delegating to specialized runner classes.
*/
class ForkingService
{
protected BackfillRunner $backfillRunner;
protected BinariesRunner $binariesRunner;
protected ReleasesRunner $releasesRunner;
protected PostProcessRunner $postProcessRunner;
protected int $maxRetries;
public function __construct()
{
$this->maxRetries = (int) Settings::settingValue('maxnforetries') >= 0
? -((int) Settings::settingValue('maxnforetries') + 1)
: NfoService::NFO_UNPROC;
$this->maxRetries = max($this->maxRetries, -8);
// Initialize runners
$this->backfillRunner = new BackfillRunner;
$this->binariesRunner = new BinariesRunner;
$this->releasesRunner = new ReleasesRunner;
$this->postProcessRunner = new PostProcessRunner;
}
/**
* Process backfill for all groups with backfill enabled.
*
* @param array<string, mixed> $options
*/
public function backfill(array $options = []): void
{
$this->runWithTiming('backfill', fn () => $this->backfillRunner->backfill($options));
}
/**
* Process safe backfill (ordered by oldest first).
*/
public function safeBackfill(): void
{
$this->runWithTiming('safe_backfill', fn () => $this->backfillRunner->safeBackfill());
}
/**
* Download binaries (new headers) for all active groups.
*/
public function binaries(int $maxPerGroup = 0): void
{
$this->runWithTiming('binaries', fn () => $this->binariesRunner->binaries($maxPerGroup));
}
/**
* Process safe binaries (ordered by most recent activity).
*/
public function safeBinaries(): void
{
$this->runWithTiming('safe_binaries', fn () => $this->binariesRunner->safeBinaries());
}
/**
* Process releases for all groups.
*/
public function releases(): void
{
$this->runWithTiming('releases', function () {
$this->releasesRunner->releases();
$this->processReleasesEndWork();
});
}
/**
* Update binaries and releases per group.
*/
public function updatePerGroup(): void
{
$this->runWithTiming('update_per_group', function () {
$this->releasesRunner->updatePerGroup();
$this->processReleasesEndWork();
});
}
/**
* Fix release names using specified mode.
*
* @param string $mode 'standard' or 'predbft'
*/
public function fixRelNames(string $mode): void
{
$this->runWithTiming("fixRelNames_{$mode}", fn () => $this->releasesRunner->fixRelNames(
$mode,
(int) Settings::settingValue('fixnamesperrun'),
(int) Settings::settingValue('fixnamethreads')
));
}
/**
* Process additional post-processing (preview images, samples, etc).
*/
public function processAdditional(): void
{
$this->runWithTiming('postProcess_add', fn () => $this->postProcessRunner->processAdditional());
}
/**
* Process NFO files.
*/
public function processNfo(): void
{
$this->runWithTiming('postProcess_nfo', fn () => $this->postProcessRunner->processNfo());
}
/**
* Process movie metadata.
*/
public function processMovies(bool $renamedOnly = false): void
{
$this->runWithTiming('postProcess_mov', fn () => $this->postProcessRunner->processMovies($renamedOnly));
}
/**
* Process TV metadata.
*/
public function processTv(bool $renamedOnly = false): void
{
$this->runWithTiming('postProcess_tv', function () use ($renamedOnly) {
if ($this->postProcessRunner->hasTvWork($renamedOnly)) {
$this->postProcessRunner->processTv($renamedOnly);
} else {
$this->log('No TV work to do.');
}
});
}
/**
* Process anime metadata.
*/
public function processAnime(): void
{
$this->runWithTiming('postProcess_ani', fn () => $this->postProcessRunner->processAnime());
}
/**
* Process book metadata.
*/
public function processBooks(): void
{
$this->runWithTiming('postProcess_boo', fn () => $this->postProcessRunner->processBooks());
}
/**
* Process music metadata.
*/
public function processMusic(): void
{
$this->runWithTiming('postProcess_mus', fn () => $this->postProcessRunner->processMusic());
}
/**
* Process console game metadata.
*/
public function processConsoles(): void
{
$this->runWithTiming('postProcess_con', fn () => $this->postProcessRunner->processConsoles());
}
/**
* Process PC game metadata.
*/
public function processGames(): void
{
$this->runWithTiming('postProcess_gam', fn () => $this->postProcessRunner->processGames());
}
/**
* Process books, music, console, and games metadata in parallel.
*/
public function processAmazon(): void
{
$this->runWithTiming('postProcess_ama', fn () => $this->postProcessRunner->processAmazon());
}
/**
* Generic work type processor for backwards compatibility.
*
* @deprecated Use specific methods instead (e.g., backfill(), binaries(), etc.)
*
* @param array<string, mixed> $options
*/
public function processWorkType(string $type, array $options = []): void
{
match ($type) {
'backfill' => $this->backfill($options),
'binaries' => $this->binaries((int) ($options[0] ?? 0)),
'fixRelNames_standard' => $this->fixRelNames('standard'),
'fixRelNames_predbft' => $this->fixRelNames('predbft'),
'releases' => $this->releases(),
'postProcess_add' => $this->processAdditional(),
'postProcess_ani' => $this->processAnime(),
'postProcess_ama' => $this->processAmazon(),
'postProcess_boo' => $this->processBooks(),
'postProcess_con' => $this->processConsoles(),
'postProcess_gam' => $this->processGames(),
'postProcess_mov' => $this->processMovies($options[0] ?? false),
'postProcess_mus' => $this->processMusic(),
'postProcess_nfo' => $this->processNfo(),
'postProcess_tv' => $this->processTv($options[0] ?? false),
'safe_backfill' => $this->safeBackfill(),
'safe_binaries' => $this->safeBinaries(),
'update_per_group' => $this->updatePerGroup(),
default => Log::warning("Unknown work type: {$type}"),
};
}
/**
* Run a task with timing output.
*/
protected function runWithTiming(string $taskName, callable $task): void
{
$startTime = now()->timestamp;
try {
$task();
} catch (\Throwable $e) {
Log::error("Task {$taskName} failed: {$e->getMessage()}", [
'exception' => $e,
]);
throw $e;
}
if (config('nntmux.echocli')) {
cli()->header(
"Multi-processing for {$taskName} finished in ".(now()->timestamp - $startTime).
' seconds at '.now()->toRfc2822String().'.'.PHP_EOL
);
}
}
/**
* Process end work for releases (DNR signalling).
*/
protected function processReleasesEndWork(): void
{
$count = $this->getReleaseWorkCount();
$command = $this->backfillRunner->buildDnrCommandPublic("releases {$count}_");
$this->executeCommand($command);
}
/**
* Count groups with pending collections.
*/
protected function getReleaseWorkCount(): int
{
$groups = DB::select('SELECT id FROM usenet_groups WHERE (active = 1 OR backfill = 1)');
$count = 0;
foreach ($groups as $group) {
try {
$query = DB::select(
sprintf('SELECT id FROM collections WHERE groups_id = %d LIMIT 1', $group->id)
);
if (! empty($query)) {
$count++;
}
} catch (\PDOException $e) {
if (config('app.debug')) {
Log::debug($e->getMessage());
}
}
}
return $count;
}
/**
* Execute a shell command.
*/
protected function executeCommand(string $command): string
{
$process = Process::fromShellCommandline($command);
$process->setTimeout((int) config('nntmux.multiprocessing_max_child_time', 1800));
$process->run(function ($type, $buffer) {
if ($type === Process::ERR) {
echo $buffer;
}
});
return $process->getOutput();
}
/**
* Log a message to console.
*/
protected function log(string $message): void
{
if (config('nntmux.echocli')) {
echo $message.PHP_EOL;
}
}
}