startTime = microtime(true); } /** * Collector query preamble * * return string beginning of SQL query to collect torrents */ public function queryPreamble(array $list): string { $sql = 'SELECT '; if (count($list) == 0) { $sql .= '0 AS sequence, '; } else { $sql .= 'CASE '; foreach ($list as $Priority => $Selection) { if (!is_number($Priority)) { continue; } $sql .= 'WHEN '; match ($Selection) { '00' => $sql .= "t.Format = 'MP3' AND t.Encoding = 'V0 (VBR)'", '01' => $sql .= "t.Format = 'MP3' AND t.Encoding = 'APX (VBR)'", '02' => $sql .= "t.Format = 'MP3' AND t.Encoding = '256 (VBR)'", '03' => $sql .= "t.Format = 'MP3' AND t.Encoding = 'V1 (VBR)'", '10' => $sql .= "t.Format = 'MP3' AND t.Encoding = '224 (VBR)'", '11' => $sql .= "t.Format = 'MP3' AND t.Encoding = 'V2 (VBR)'", '12' => $sql .= "t.Format = 'MP3' AND t.Encoding = 'APS (VBR)'", '13' => $sql .= "t.Format = 'MP3' AND t.Encoding = '192 (VBR)'", '20' => $sql .= "t.Format = 'MP3' AND t.Encoding = '320'", '21' => $sql .= "t.Format = 'MP3' AND t.Encoding = '256'", '22' => $sql .= "t.Format = 'MP3' AND t.Encoding = '224'", '23' => $sql .= "t.Format = 'MP3' AND t.Encoding = '192'", '24' => $sql .= "t.Format = 'MP3' AND t.Encoding = '160'", '25' => $sql .= "t.Format = 'MP3' AND t.Encoding = '128'", '26' => $sql .= "t.Format = 'MP3' AND t.Encoding = '96'", '27' => $sql .= "t.Format = 'MP3' AND t.Encoding = '64'", '30' => $sql .= "t.Format = 'FLAC' AND t.Encoding = '24bit Lossless' AND t.Media = 'Vinyl'", '31' => $sql .= "t.Format = 'FLAC' AND t.Encoding = '24bit Lossless' AND t.Media = 'DVD'", '32' => $sql .= "t.Format = 'FLAC' AND t.Encoding = '24bit Lossless' AND t.Media = 'SACD'", '33' => $sql .= "t.Format = 'FLAC' AND t.Encoding = '24bit Lossless' AND t.Media = 'WEB'", '34' => $sql .= "t.Format = 'FLAC' AND t.Encoding = 'Lossless' AND HasLog = '1' AND LogScore = '100' AND HasCue = '1'", '35' => $sql .= "t.Format = 'FLAC' AND t.Encoding = 'Lossless' AND HasLog = '1' AND LogScore = '100'", '36' => $sql .= "t.Format = 'FLAC' AND t.Encoding = 'Lossless' AND HasLog = '1'", '37' => $sql .= "t.Format = 'FLAC' AND t.Encoding = 'Lossless' AND t.Media = 'WEB'", '38' => $sql .= "t.Format = 'FLAC' AND t.Encoding = 'Lossless'", '40' => $sql .= "t.Format = 'DTS'", '42' => $sql .= "t.Format = 'AAC' AND t.Encoding = '320'", '43' => $sql .= "t.Format = 'AAC' AND t.Encoding = '256'", '44' => $sql .= "t.Format = 'AAC' AND t.Encoding = 'q5.5'", '45' => $sql .= "t.Format = 'AAC' AND t.Encoding = 'q5'", '46' => $sql .= "t.Format = 'AAC' AND t.Encoding = '192'", default => error('Unknown collector selector'), }; $sql .= " THEN $Priority "; } $sql .= "ELSE 100 END AS sequence, "; } return $sql . "t.GroupID, t.ID AS TorrentID, t.Media, t.Format, t.Encoding, tg.ReleaseType, if(t.RemasterYear=0, tg.Year, t.RemasterYear) AS Year, tg.Name, t.Size"; } /** * This method is called repeatedly after the query is prepared, and returns * the resultset in chunks, to avoid blowing out the memory requirements on * artists with many, many, many releases. */ public function process(string $Key): array|null { $saveQid = self::$db->get_query_id(); self::$db->set_query_id($this->qid); if (!isset($this->idBoundary)) { if ($Key == 'TorrentID') { $this->idBoundary = false; } else { $this->idBoundary = self::$db->to_pair($Key, 'TorrentID', false); } } $downloadList = []; $insertArgs = []; while ($row = self::$db->next_record(MYSQLI_ASSOC, false)) { if (!$this->idBoundary || $row['TorrentID'] == $this->idBoundary[$row[$Key]]) { $downloadList[$row[$Key]] = $row; array_push($insertArgs, $this->user->id(), $row['TorrentID']); if (count($downloadList) >= self::CHUNK_SIZE) { break; } } } $found = (int)(count($insertArgs) / 2); $this->totalFound += $found; if ($insertArgs) { // Record the files as having been downloaded, in order to take them // into account in the download factor. Ideally the entire collector // operation should take place in a transaction, in case of an abort // half way through. Maybe some other day. self::$db->prepared_query( "INSERT INTO users_downloads (UserID, TorrentID) VALUES" . implode(',', array_fill(0, $found, '(?,?)')), ...$insertArgs ); } self::$db->set_query_id($saveQid); return $this->totalFound > 0 ? $downloadList : null; } /** * Add a file to the zip archive. If the torrent file cannot be found * it will be added to the list of errors. If the torrent does not * match the minimum format/encoding requirements, it will be skipped. * * $info file info stored as an array with at least the keys * Artist, Name, Year, Media, Format, Encoding and TorrentID */ public function addZip(\ZipStream\ZipStream $zip, array $info, string $folderName = null): void { if ($info['sequence'] == 100) { $this->skip($info); return; } $torrent = $this->torMan->findById($info['TorrentID']); if (is_null($torrent)) { $this->fail($info); return; } $contents = $torrent->torrentBody($this->user->announceUrl()); if ($contents === '') { $this->fail($info); return; } $folder = is_null($folderName) ? '' : (safeFilename($folderName) . '/'); $name = $torrent->torrentFilename(false, MAX_PATH_LEN - strlen($folder)); $zip->addFile("$folder$name", $contents); $this->totalAdded++; $this->totalSize += (int)$info['Size']; $this->totalTokens += (int)ceil($info['Size'] / BYTES_PER_FREELEECH_TOKEN); } /** * Add a file to the list of files that did not match the user's format or quality requirements */ public function skip(array $info): static { $this->skipped[] = "{$info['Artist']}/{$info['Year']}/{$info['Name']}"; return $this; } /** * Add a file to the list of files for which the torrent data is corrupt. */ public function fail(array $info): static { $this->error[] = "{$info['Artist']}/{$info['Year']}/{$info['Name']}"; return $this; } /** * Compile a list of files that could not be added to the archive */ public function errors(): string { return "The following torrents are in an broken or missing. This is bad!" . "\r\n" . implode("\r\n", $this->error) . "\r\n"; } /** * Add a summary to the archive and include a list of files that could not be added. Close the zip archive */ public function emitZip(\ZipStream\ZipStream $zip): void { $this->fillZip($zip); $zip->addFile("README.txt", $this->summary()); if ($this->error) { $zip->addFile("ERRORS.txt", $this->errors()); } header('X-Accel-Buffering: no'); $zip->finish(); } /** * Produce a summary text over the collector results */ public function summary(): string { return self::$twig->render('collector.twig', [ 'added' => $this->totalAdded, 'total' => $this->totalFound, 'size' => $this->totalSize, 'tokens' => $this->totalTokens, 'error' => count($this->error), 'skipped' => $this->skipped, 'title' => $this->title, 'date' => date("Y-m-d H:i"), 'time' => 1000 * (microtime(true) - $this->startTime), 'used' => memory_get_usage(true), 'user' => $this->user, ]); } public function sql(): string { return $this->sql; } public function args(): array { return $this->args; } }