diff --git a/public/php/add_topics_to_client.php b/public/php/add_topics_to_client.php index 95e424010..c19cbe15c 100644 --- a/public/php/add_topics_to_client.php +++ b/public/php/add_topics_to_client.php @@ -2,12 +2,8 @@ require __DIR__ . '/../../vendor/autoload.php'; +use KeepersTeam\Webtlo\Action\ClientAddTopics; use KeepersTeam\Webtlo\App; -use KeepersTeam\Webtlo\Config\ApiCredentials; -use KeepersTeam\Webtlo\Config\SubFolderType; -use KeepersTeam\Webtlo\Config\SubForums; -use KeepersTeam\Webtlo\Config\TorrentClients; -use KeepersTeam\Webtlo\Config\TorrentDownload; use KeepersTeam\Webtlo\Helper; // Подключаем контейнер. @@ -15,284 +11,21 @@ $log = $app->getLogger(); try { - $result = ''; - $starttime = microtime(true); - - // список ID раздач + // Список добавляемых раздач (info_hash). if (empty($_POST['topic_hashes'])) { - throw new Exception('Выберите раздачи'); + throw new RuntimeException('Выберите раздачи'); } parse_str($_POST['topic_hashes'], $topicHashes); $topicHashes = Helper::convertKeysToString((array) $topicHashes['topic_hashes']); - /** @var SubForums $subsections хранимые подразделы */ - $subsections = $app->get(SubForums::class); - if (!$subsections->count()) { - throw new Exception('В настройках не найдены хранимые подразделы'); - } - - /** @var TorrentClients $clients используемые торрент-клиенты */ - $clients = $app->get(TorrentClients::class); - - if (!$clients->count()) { - throw new Exception('В настройках не найдены торрент-клиенты'); - } - - $db = $app->getDataBase(); - - $forumClient = $app->getForumClient(); - if (!$forumClient->checkAccess()) { - throw new RuntimeException('Ошибка подключения к форуму.'); - } - - /** @var TorrentDownload $downloadOptions */ - $downloadOptions = $app->get(TorrentDownload::class); - - /** - * Ключи для скачивания файлов. - * - * @var ApiCredentials $apiCredentials - */ - $apiCredentials = $app->get(ApiCredentials::class); - - // Записываем ключи доступа к API. - $forumClient->setApiCredentials(apiCredentials: $apiCredentials); - - $log->info('Запущен процесс добавления раздач в торрент-клиенты...'); - // получение ID раздач с привязкой к подразделу - $topicHashesByForums = []; - - $topicHashes = array_chunk($topicHashes, 999); - foreach ($topicHashes as $topicHashesChunk) { - $placeholders = str_repeat('?,', count($topicHashesChunk) - 1) . '?'; - - $data = $db->query( - 'SELECT forum_id, info_hash FROM Topics WHERE info_hash IN (' . $placeholders . ')', - $topicHashesChunk, - PDO::FETCH_GROUP | PDO::FETCH_COLUMN, - ); - unset($placeholders); - - foreach ($data as $forumID => $forumTopicHashes) { - if (isset($topicHashesByForums[$forumID])) { - $topicHashesByForums[$forumID] = array_merge( - $topicHashesByForums[$forumID], - $forumTopicHashes - ); - } else { - $topicHashesByForums[$forumID] = $forumTopicHashes; - } - } - - unset($topicHashesChunk, $forumTopicHashes, $data); - } - unset($topicHashes); - - if (empty($topicHashesByForums)) { - throw new Exception('Не получены идентификаторы раздач с привязкой к подразделу'); - } - - // полный путь до каталога для сохранения торрент-файлов - $localPath = Helper::getStorageSubFolderPath(subFolder: 'tfiles'); - // очищаем каталог от старых торрент-файлов - Helper::removeDirRecursive($localPath); - // создаём каталог для торрент-файлов - Helper::checkDirRecursive($localPath); - - // шаблон для сохранения - $formatPathTorrentFile = Helper::normalizePathEncoding($localPath . DIRECTORY_SEPARATOR . '[webtlo].h%s.torrent'); - - $clientFactory = $app->getClientFactory(); - - $totalTorrentFilesAdded = 0; - $usedTorrentClientsIDs = []; - foreach ($topicHashesByForums as $forumID => $topicHashes) { - if (empty($topicHashes)) { - continue; - } - - $subForum = $subsections->getSubForum(subForumId: (int) $forumID); - if ($subForum === null) { - $log->warning('В настройках нет данных о подразделе с идентификатором "' . $forumID . '"'); - - continue; - } - - // идентификатор торрент-клиента - $torrentClientId = $subForum->clientId; - if (!$torrentClientId) { - $log->warning('К подразделу "' . $forumID . '" не привязан торрент-клиент'); - - continue; - } - - // Подключаемся к торрент-клиенту - $client = $clientFactory->getClientById(clientId: $torrentClientId); - - // Если клиент недоступен, пропускаем. - if ($client === null) { - continue; - } - - $downloadedTorrentFiles = []; - foreach ($topicHashes as $topicHash) { - $topicHash = (string) $topicHash; - - $torrentFile = $forumClient->downloadTorrent( - infoHash : $topicHash, - addRetracker: $downloadOptions->addRetracker, - ); - if ($torrentFile === null) { - $log->error('Не удалось скачать торрент-файл (' . $topicHash . ')'); - - continue; - } - - $torrentFile = $torrentFile->getContents(); - if (empty($torrentFile)) { - continue; - } - - // сохранить в каталог - $response = file_put_contents( - sprintf($formatPathTorrentFile, $topicHash), - $torrentFile - ); - if ($response === false) { - $log->error('Произошла ошибка при сохранении торрент-файла (' . $topicHash . ')'); - - continue; - } - - $downloadedTorrentFiles[] = $topicHash; - } - - $numberDownloadedTorrentFiles = count($downloadedTorrentFiles); - if (!$numberDownloadedTorrentFiles) { - $log->notice('Нет скачанных торрент-файлов для добавления их в торрент-клиент "' . $client->getClientTag() . '"'); - - continue; - } - - $clientAddingSleep = $client->getTorrentAddingSleep(); - - // Убираем последний слэш в пути каталога для данных - $dataFolder = trim($subForum->dataFolder); - if (preg_match('/(\/|\\\)$/', $dataFolder)) { - $dataFolder = substr($dataFolder, 0, -1); - } - - // Определяем направление слэша в пути каталога для данных - $delimiter = !str_contains($dataFolder, '/') ? '\\' : '/'; - - // добавление раздач - $downloadedTorrentFiles = array_chunk($downloadedTorrentFiles, 999); - foreach ($downloadedTorrentFiles as $downloadedTorrentFilesChunk) { - // получаем идентификаторы раздач - $placeholders = str_repeat('?,', count($downloadedTorrentFilesChunk) - 1) . '?'; - $topicIDsByHash = $db->query( - 'SELECT info_hash, id FROM Topics WHERE info_hash IN (' . $placeholders . ')', - $downloadedTorrentFilesChunk, - PDO::FETCH_KEY_PAIR - ); - unset($placeholders); - - foreach ($downloadedTorrentFilesChunk as $topicHash) { - $torrentSavePath = $dataFolder; - - // Дописываем подкаталог для сохранения. - if ($subForum->subFolderType !== null) { - $subFolderPath = match ($subForum->subFolderType) { - SubFolderType::Topic => $topicIDsByHash[$topicHash], - SubFolderType::Hash => $topicHash, - }; - - $torrentSavePath .= $delimiter . $subFolderPath; - } - - // Добавляем раздачу в торрент-клиент. - $response = $client->addTorrent( - torrentFilePath: sprintf($formatPathTorrentFile, $topicHash), - savePath : $torrentSavePath, - label : $subForum->label - ); - if ($response !== false) { - $addedTorrentFiles[] = $topicHash; - } - - // Пауза между добавлениями раздач, в зависимости от клиента (0.5 сек по умолчанию) - usleep($clientAddingSleep); - } - unset($downloadedTorrentFilesChunk, $topicIDsByHash); - } - unset($downloadedTorrentFiles); - - if (empty($addedTorrentFiles)) { - $log->warning('Не удалось добавить раздачи в торрент-клиент "' . $client->getClientTag() . '"'); - - continue; - } - $numberAddedTorrentFiles = count($addedTorrentFiles); - - // Указываем раздачам метку, если она не выставлена при добавлении раздач. - if ($subForum->label !== '' && !$client->isLabelAddingAllowed()) { - // ждём добавления раздач, чтобы проставить метку - sleep((int) round(count($addedTorrentFiles) / 20) + 1); - - // устанавливаем метку - $response = $client->setLabel($addedTorrentFiles, $subForum->label); - if ($response === false) { - $log->warning('Возникли проблемы при отправке запроса на установку метки'); - } - } - - // помечаем в базе добавленные раздачи - $addedTorrentFilesChunks = array_chunk($addedTorrentFiles, 998); - unset($addedTorrentFiles); - - foreach ($addedTorrentFilesChunks as $addedTorrentFilesChunk) { - $placeholders = str_repeat('?,', count($addedTorrentFilesChunk) - 1) . '?'; - $db->query( - 'INSERT INTO Torrents ( - info_hash, - client_id, - topic_id, - name, - total_size - ) - SELECT - Topics.info_hash, - ?, - Topics.id, - Topics.name, - Topics.size - FROM Topics - WHERE info_hash IN (' . $placeholders . ')', - array_merge([$torrentClientId], $addedTorrentFilesChunk) - ); - - unset($placeholders, $addedTorrentFilesChunk); - } - unset($addedTorrentFilesChunks); - - $log->info('Добавлено раздач в торрент-клиент "' . $client->getClientTag() . '": ' . $numberAddedTorrentFiles . ' шт.'); - - $usedTorrentClientsIDs[] = $torrentClientId; - $totalTorrentFilesAdded += $numberAddedTorrentFiles; - - unset($client); - } - - $totalTorrentClients = count(array_unique($usedTorrentClientsIDs)); - - $result = 'Задействовано торрент-клиентов — ' . $totalTorrentClients . ', добавлено раздач всего — ' . $totalTorrentFilesAdded . ' шт.'; - $endtime = microtime(true); + /** @var ClientAddTopics $addTopics */ + $addTopics = $app->get(ClientAddTopics::class); - $log->info('Процесс добавления раздач в торрент-клиенты завершён за ' . Helper::convertSeconds((int) ($endtime - $starttime))); + $addTopics->process($topicHashes); - $log->info($result); -} catch (Exception $e) { + $result = 'Добавление завершено.'; +} catch (RuntimeException $e) { $result = $e->getMessage(); $log->error($result); } finally { diff --git a/src/back/Action/ClientAddTopics.php b/src/back/Action/ClientAddTopics.php new file mode 100644 index 000000000..b36d537d3 --- /dev/null +++ b/src/back/Action/ClientAddTopics.php @@ -0,0 +1,321 @@ +subsections->count()) { + throw new RuntimeException('В настройках не найдены хранимые подразделы'); + } + + if (!$this->clients->count()) { + throw new RuntimeException('В настройках не найдены торрент-клиенты'); + } + } + + /** + * @param string[] $hashes + */ + public function process(array $hashes): void + { + Timers::start('add_topics_to_client'); + $this->logger->info('Запущен процесс добавления раздач в торрент-клиенты...'); + + // Подключаемся к форуму используя ключи API. + $this->forumConnect(); + + // Получение ID раздач с привязкой к подразделу + $topicHashesByForums = $this->topics->getGroupedTopics(hashes: $hashes); + if (!count($topicHashesByForums)) { + throw new RuntimeException('Не получены идентификаторы раздач с привязкой к подразделу'); + } + + $totalTorrentFilesAdded = 0; + $usedTorrentClients = []; + foreach ($topicHashesByForums as $subForumId => $forumTopics) { + if (empty($forumTopics)) { + continue; + } + + // Параметры хранимого подраздела. Если не нашли - пропускаем. + $subForum = $this->getSubForumOptions(subForumId: (int) $subForumId); + if ($subForum === null) { + continue; + } + + // Подключаемся к торрент-клиенту. Если недоступен - пропускаем. + $client = $this->clientFactory->getClientById(clientId: $subForum->clientId); + if ($client === null) { + continue; + } + + // Пробуем скачать торрент-файлы раздач во временную папку. + $downloadedTorrents = $this->downloadTorrentFiles(forumTopics: $forumTopics); + unset($subForumId, $forumTopics); + + if (!count($downloadedTorrents)) { + $this->logger->notice( + 'Нет скачанных торрент-файлов для добавления их в торрент-клиент "{tag}"', + ['tag' => $client->getClientTag()], + ); + + continue; + } + + $logClient = ['tag' => $client->getClientTag()]; + + // Добавляем раздачи в нужный торрент-клиент. + $addedTorrentHashes = $this->addTorrentsToClient( + topics : $downloadedTorrents, + client : $client, + subForum: $subForum, + ); + unset($downloadedTorrents); + + // Сохраним количество добавленных раздач для общего счётчика. + $countAddedTorrents = count($addedTorrentHashes); + if (!$countAddedTorrents) { + $this->logger->warning('Не удалось добавить раздачи в торрент-клиент "{tag}"', $logClient); + + continue; + } + + // Указываем раздачам метку, если она не выставлена при добавлении раздач. + if ($subForum->label !== '' && !$client->isLabelAddingAllowed()) { + // Ждём добавления раздач, чтобы проставить метку + sleep((int) round(count($addedTorrentHashes) / 20) + 1); + + // устанавливаем метку + $response = $client->setLabel(torrentHashes: $addedTorrentHashes, label: $subForum->label); + if ($response === false) { + $this->logger->warning('Возникли проблемы при отправке запроса на установку метки', $logClient); + } + } + + // Записываем новые раздачи как хранимые в торрент-клиенте. + $this->torrents->addDownloadedTorrents(hashes: $addedTorrentHashes, clientId: $subForum->clientId); + + $this->logger->info( + 'Добавлено раздач в торрент-клиент "{tag}": {count} шт.', + [...$logClient, 'count' => $countAddedTorrents] + ); + + $usedTorrentClients[] = $subForum->clientId; + $totalTorrentFilesAdded += $countAddedTorrents; + } + + $totalTorrentClients = count(array_unique($usedTorrentClients)); + + $this->logger->info( + 'Процесс добавления раздач в торрент-клиенты завершён за {sec}', + ['sec' => Timers::getExecTime('add_topics_to_client')] + ); + + $this->logger->info( + 'Задействовано торрент-клиентов — {clients}, добавлено раздач всего — {topics} шт.', + ['clients' => $totalTorrentClients, 'topics' => $totalTorrentFilesAdded] + ); + } + + /** + * Скачать торрент-файлы по списку раздач. + * + * @param array{topic_id: int, info_hash: string}[] $forumTopics + * + * @return DownloadedTopic[] + */ + private function downloadTorrentFiles(array $forumTopics): array + { + $torrentFilePathTemplate = $this->getTorrentFilePathTemplate(); + + $downloadedTorrents = []; + foreach ($forumTopics as $row) { + $topic = new DownloadedTopic( + hash : $row['info_hash'], + topicId : $row['topic_id'], + filePath: sprintf($torrentFilePathTemplate, $row['info_hash']) + ); + + $stream = $this->forumClient->downloadTorrent( + infoHash : $topic->hash, + addRetracker: $this->downloadOptions->addRetracker, + ); + if ($stream === null) { + $this->logger->error('Не удалось скачать торрент-файл', $topic->jsonSerialize()); + + continue; + } + + $torrentFile = $stream->getContents(); + if (empty($torrentFile)) { + continue; + } + + // Сохранить содержимое в файл. + $response = file_put_contents($topic->filePath, $torrentFile); + if ($response === false) { + $this->logger->error('Произошла ошибка при сохранении торрент-файла', $topic->jsonSerialize()); + + continue; + } + + $downloadedTorrents[] = $topic; + } + + return $downloadedTorrents; + } + + /** + * @param DownloadedTopic[] $topics + * + * @return string[] + */ + private function addTorrentsToClient(array $topics, ClientInterface $client, SubForum $subForum): array + { + $clientAddingSleep = $client->getTorrentAddingSleep(); + + // Убираем последний слэш в пути каталога для данных + $dataFolder = trim($subForum->dataFolder); + $dataFolder = rtrim($dataFolder, '/\\'); + + $addedTorrentHashes = []; + // Добавление раздач в торрент-клиенты. + foreach ($topics as $topic) { + $torrentSavePath = $this->makeTopicContentPath(topic: $topic, dataPath: $dataFolder, subForum: $subForum); + + // Добавляем раздачу в торрент-клиент. + $response = $client->addTorrent( + torrentFilePath: $topic->filePath, + savePath : $torrentSavePath, + label : $subForum->label + ); + + if ($response !== false) { + $addedTorrentHashes[] = $topic->hash; + } + + // Пауза между добавлениями раздач, в зависимости от клиента (0.5 сек по умолчанию) + usleep($clientAddingSleep); + } + + return $addedTorrentHashes; + } + + private function forumConnect(): void + { + // Проверим подключение к форуму. + if (!$this->forumClient->checkAccess()) { + throw new RuntimeException('Ошибка подключения к форуму.'); + } + + // Записываем ключи доступа к API. + $this->forumClient->setApiCredentials(apiCredentials: $this->apiCredentials); + } + + private function getTorrentFilePathTemplate(): string + { + // Полный путь до каталога сохранения файлов. + $localPath = Helper::getStorageSubFolderPath(subFolder: 'tfiles'); + + // Очищаем каталог от старых файлов. + Helper::removeDirRecursive($localPath); + + // Создаём каталог для файлов. + Helper::checkDirRecursive($localPath); + + // Шаблон пути для сохранения файлов + return Helper::normalizePathEncoding($localPath . DIRECTORY_SEPARATOR . '[webtlo].h%s.torrent'); + } + + /** + * Создать путь хранения содержимого раздачи на диске в клиенте. + * В зависимости от настроек подраздела. + */ + private function makeTopicContentPath(DownloadedTopic $topic, string $dataPath, SubForum $subForum): string + { + // Если путь хранения не указан - используем пустую строку. + // Потому что разные клиенты по разному понимают не абсолютный путь хранения добавляемых раздач. + // TODO Возможно стоит этот момент доработать. + if (empty($dataPath)) { + return ''; + } + + // Если создание подкаталога не выбрано, используем указанный путь без изменений. + if ($subForum->subFolderType === null) { + return $dataPath; + } + + // Подкаталог для каждой раздачи. + $subFolderPath = match ($subForum->subFolderType) { + SubFolderType::Topic => (string) $topic->topicId, + SubFolderType::Hash => $topic->hash, + }; + + // Попытка угадать, тип ФС торрент-клиента по пути хранения. + $delimiter = !str_contains($dataPath, '/') ? '\\' : '/'; + + return $dataPath . $delimiter . $subFolderPath; + } + + private function getSubForumOptions(int $subForumId): ?SubForum + { + $subForum = $this->subsections->getSubForum(subForumId: $subForumId); + if ($subForum === null) { + $this->logger->warning('В настройках нет данных о подразделе с идентификатором "' . $subForumId . '"'); + + return null; + } + + if (!$subForum->clientId) { + $this->logger->warning('К подразделу "' . $subForumId . '" не привязан торрент-клиент'); + + return null; + } + + return $subForum; + } +} diff --git a/src/back/Data/DownloadedTopic.php b/src/back/Data/DownloadedTopic.php new file mode 100644 index 000000000..e623c18d2 --- /dev/null +++ b/src/back/Data/DownloadedTopic.php @@ -0,0 +1,30 @@ + + */ + public function jsonSerialize(): array + { + return [ + 'topicId' => $this->topicId, + 'hash' => $this->hash, + ]; + } +} diff --git a/src/back/Storage/Table/Topics.php b/src/back/Storage/Table/Topics.php index 5c656fd77..466796a78 100644 --- a/src/back/Storage/Table/Topics.php +++ b/src/back/Storage/Table/Topics.php @@ -83,7 +83,8 @@ public function searchPrevious(array $topicIds): array /** * Поиск в БД ид раздач, по хешу. * - * @param string[] $hashes + * @param string[] $hashes + * @param positive-int $chunkSize * * @return array */ @@ -91,7 +92,7 @@ public function getTopicsIdsByHashes(array $hashes, int $chunkSize = 500): array { $result = []; - $hashes = array_chunk($hashes, max(1, $chunkSize)); + $hashes = array_chunk($hashes, $chunkSize); foreach ($hashes as $chunk) { $search = KeysObject::create($chunk); @@ -110,6 +111,44 @@ public function getTopicsIdsByHashes(array $hashes, int $chunkSize = 500): array return array_merge(...$result); } + /** + * Найти в БД раздачи и сгруппировать по ид подраздела. + * + * @param string[] $hashes + * @param positive-int $chunkSize + * + * @return array + */ + public function getGroupedTopics(array $hashes, int $chunkSize = 500): array + { + if (!count($hashes)) { + return []; + } + + $result = []; + + $chunks = array_chunk(array_unique($hashes), $chunkSize); + foreach ($chunks as $chunk) { + $search = KeysObject::create($chunk); + + $grouped = $this->con->query( + sql : "SELECT forum_id, id AS topic_id, info_hash FROM Topics WHERE info_hash IN ($search->keys)", + param: $search->values, + pdo : PDO::FETCH_GROUP | PDO::FETCH_ASSOC + ); + + if (empty($grouped)) { + continue; + } + + foreach ($grouped as $forumId => $topics) { + $result[$forumId] = array_merge($result[$forumId] ?? [], $topics); + } + } + + return $result; + } + /** * Удаление раздач по списку их ИД. * diff --git a/src/back/Storage/Table/Torrents.php b/src/back/Storage/Table/Torrents.php index e68d4a150..aeda29f5b 100644 --- a/src/back/Storage/Table/Torrents.php +++ b/src/back/Storage/Table/Torrents.php @@ -48,7 +48,8 @@ public function getTopicsIdsByHashes(array $hashes, int $chunkSize = 500): array /** * Найти список раздач, сгруппировав их по ид клиента. * - * @param string[] $hashes + * @param string[] $hashes + * @param positive-int $chunkSize * * @return array */ @@ -56,10 +57,7 @@ public function getGroupedByClientTopics(array $hashes, int $chunkSize = 499): a { $result = []; - $hashes = array_chunk( - array_unique($hashes), - max(1, $chunkSize) - ); + $hashes = array_chunk(array_unique($hashes), $chunkSize); foreach ($hashes as $chunk) { $search = KeysObject::create($chunk); @@ -86,6 +84,41 @@ public function getGroupedByClientTopics(array $hashes, int $chunkSize = 499): a return $result; } + /** + * @param string[] $hashes + * @param positive-int $chunkSize + */ + public function addDownloadedTorrents(array $hashes, int $clientId, int $chunkSize = 500): void + { + $chunks = array_chunk($hashes, $chunkSize); + foreach ($chunks as $chunk) { + $object = KeysObject::create($chunk); + + $sql = " + INSERT INTO Torrents ( + info_hash, + client_id, + topic_id, + name, + total_size + ) + SELECT + Topics.info_hash, + ?, + Topics.id, + Topics.name, + Topics.size + FROM Topics + WHERE info_hash IN ($object->keys) + "; + + $this->db->executeStatement( + sql : $sql, + param: [$clientId, ...$object->values], + ); + } + } + /** * Удалить раздачи в БД по хешу. *