new version
This commit is contained in:
@@ -2,22 +2,69 @@
|
|||||||
|
|
||||||
Скрипт для рекурсивного скачивания большой публичной папки Яндекс Диска, защищённой паролем.
|
Скрипт для рекурсивного скачивания большой публичной папки Яндекс Диска, защищённой паролем.
|
||||||
|
|
||||||
Скрипт использует cookie `passToken`, скачивает каждый файл отдельно и не пытается сформировать единый ZIP-архив.
|
Он получает список объектов через веб-API Яндекс Диска, сохраняет структуру каталогов и скачивает каждый файл отдельно через `wget`.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
* скачивание защищённой публичной папки;
|
- чтение всех пользовательских настроек из файла `.env` рядом со скриптом;
|
||||||
* рекурсивный обход вложенных каталогов;
|
- использование готового cookie `passToken` либо автоматическое получение токена по паролю через Chromium и Playwright;
|
||||||
* сохранение исходной структуры папок;
|
- постоянный профиль Chromium с повторным использованием cookies;
|
||||||
* скачивание файлов по одному;
|
- рекурсивный обход вложенных каталогов;
|
||||||
* продолжение прерванной загрузки через `wget --continue`;
|
- двухэтапная работа: сначала полный список, затем скачивание;
|
||||||
* автоматический пропуск полностью скачанных файлов;
|
- сохранение полного списка в `yandex-download-manifest.jsonl`;
|
||||||
* проверка размера скачанного файла;
|
- исключение служебного элемента текущего каталога из расчёта `offset`;
|
||||||
* автоматическая пагинация списка файлов;
|
- сохранение исходной структуры каталогов;
|
||||||
* остановка при появлении CAPTCHA;
|
- продолжение прерванной загрузки через `wget --continue`;
|
||||||
* сохранение ссылки CAPTCHA в текстовый файл;
|
- автоматический пропуск файлов, размер которых уже совпадает с серверным;
|
||||||
* паузы между запросами для снижения вероятности блокировки;
|
- проверка итогового размера каждого скачанного файла;
|
||||||
* повторный запуск без потери уже скачанных данных.
|
- паузы между запросами прямых ссылок и периодические длительные паузы;
|
||||||
|
- повторные HTTP-запросы при временных ошибках `429`, `500`, `502`, `503` и `504`;
|
||||||
|
|
||||||
|
## Как работает двухэтапная загрузка
|
||||||
|
|
||||||
|
### Этап 1 — получение полного списка
|
||||||
|
|
||||||
|
Скрипт быстро обходит страницы каталога:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 → 40 → 80 → 120 → ... → completed=True
|
||||||
|
```
|
||||||
|
|
||||||
|
На этом этапе:
|
||||||
|
|
||||||
|
- `download-url` не запрашивается;
|
||||||
|
- `wget` не запускается;
|
||||||
|
- файлы не скачиваются;
|
||||||
|
- для каждого файла сохраняются серверные метаданные и относительный каталог;
|
||||||
|
- вложенные каталоги также обходятся полностью;
|
||||||
|
- служебное описание текущей папки не учитывается при увеличении `offset`.
|
||||||
|
|
||||||
|
После успешного завершения обхода создаётся:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OUTPUT_DIR/yandex-download-manifest.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Manifest записывается атомарно: сначала создаётся временный файл `.tmp`, после чего он заменяет предыдущий manifest.
|
||||||
|
|
||||||
|
Пример одной строки:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"relative_directory":".","item":{"name":"FL_9J1915234BP_1654_40013103010803_V001_S.frf","type":"file","size":793122,"path":"public_hash:/ODIS/Flashdaten/Brand-A/FL_9J1915234BP_1654_40013103010803_V001_S.frf"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 2 — скачивание файлов
|
||||||
|
|
||||||
|
После получения `completed=True` скрипт начинает скачивание по полностью собранному плану:
|
||||||
|
|
||||||
|
1. проверяет наличие локального файла;
|
||||||
|
2. сравнивает его размер с серверным;
|
||||||
|
3. пропускает полностью скачанный файл;
|
||||||
|
4. для отсутствующего или неполного файла получает временный `download-url`;
|
||||||
|
5. запускает `wget --continue`;
|
||||||
|
6. проверяет размер после завершения.
|
||||||
|
|
||||||
|
Текущая версия сохраняет manifest для контроля и последующего анализа, но при новом запуске всё равно заново выполняет этап 1 и получает актуальный список с Яндекса.
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@@ -29,38 +76,36 @@
|
|||||||
sudo apt install -y python3 python3-requests wget
|
sudo apt install -y python3 python3-requests wget
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Playwright и Chromium для автоматического получения `passToken`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install --user playwright
|
||||||
|
python3 -m playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
## Настройка
|
## Настройка
|
||||||
|
|
||||||
Все основные параметры находятся в начале файла:
|
Все основные параметры находятся в файле sample.env
|
||||||
|
|
||||||
```python
|
### `TARGET_URL`
|
||||||
TARGET_URL = (
|
|
||||||
"https://disk.yandex.ru/d/"
|
|
||||||
"cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-V"
|
|
||||||
)
|
|
||||||
|
|
||||||
PASS_TOKEN = ""
|
Полная публичная ссылка на нужный каталог:
|
||||||
|
|
||||||
OUTPUT_DIR = "/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V"
|
```dotenv
|
||||||
|
TARGET_URL="https://disk.yandex.ru/d/cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-A"
|
||||||
```
|
```
|
||||||
|
|
||||||
### TARGET_URL
|
Для вложенного каталога необходимо указывать полный путь после идентификатора публичной ссылки.
|
||||||
|
|
||||||
Полная ссылка на скачиваемую папку Яндекс Диска:
|
### `PUBLIC_LINK_PASSWORD`
|
||||||
|
|
||||||
```python
|
Пароль публичной ссылки:
|
||||||
TARGET_URL = "https://disk.yandex.ru/d/cWn4RgSdFcSZXw/ODIS/Flashdaten/Brand-V"
|
|
||||||
|
```dotenv
|
||||||
|
PUBLIC_LINK_PASSWORD="реальный пароль публичной ссылки"
|
||||||
```
|
```
|
||||||
|
|
||||||
### PASS_TOKEN
|
Если оставить значение пустым, программа попытается использовать cookies из постоянного профиля Chromium. Если действующего `passToken` нет и запуск производится из терминала, пароль будет запрошен без отображения введённых символов.
|
||||||
|
|
||||||
Значение cookie `passToken`, полученное после ввода пароля в браузере.
|
|
||||||
|
|
||||||
Можно указать только значение:
|
|
||||||
|
|
||||||
```python
|
|
||||||
PASS_TOKEN = "значение_cookie_passToken"
|
|
||||||
```
|
|
||||||
|
|
||||||
### OUTPUT_DIR
|
### OUTPUT_DIR
|
||||||
|
|
||||||
@@ -71,31 +116,131 @@ OUTPUT_DIR = "/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V"
|
|||||||
```
|
```
|
||||||
|
|
||||||
Каталог будет создан автоматически, если у пользователя есть права на его создание.
|
Каталог будет создан автоматически, если у пользователя есть права на его создание.
|
||||||
|
Внутри него также могут появиться:
|
||||||
|
|
||||||
|
```text
|
||||||
|
yandex-download-manifest.jsonl
|
||||||
|
yandex-captcha-url.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройки Playwright
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
PLAYWRIGHT_PROFILE_DIR="./playwright-profile"
|
||||||
|
PLAYWRIGHT_HEADLESS="false"
|
||||||
|
PLAYWRIGHT_TIMEOUT="120"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `PLAYWRIGHT_PROFILE_DIR` — постоянный профиль Chromium;
|
||||||
|
- `PLAYWRIGHT_HEADLESS=false` — показывает окно браузера и позволяет вручную пройти CAPTCHA;
|
||||||
|
- `PLAYWRIGHT_HEADLESS=true` — запускает Chromium без окна; при CAPTCHA программа остановится;
|
||||||
|
- `PLAYWRIGHT_TIMEOUT` — максимальное время ожидания получения `passToken`, в секундах.
|
||||||
|
|
||||||
|
Относительный путь профиля вычисляется относительно каталога, где находится `.env` и скрипт.
|
||||||
|
|
||||||
|
### Задержки между страницами
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
PAGE_DELAY_MIN="0.5"
|
||||||
|
PAGE_DELAY_MAX="1.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Эти задержки используются только на этапе 1 между запросами `fetch-list`.
|
||||||
|
|
||||||
|
После перехода на двухэтапную схему страницы перечисляются подряд и больше не разделяются длительным скачиванием файлов. Поэтому состояние пагинации живёт значительно меньше времени.
|
||||||
|
|
||||||
|
### Задержки между файлами
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
FILE_DELAY_MIN="1.5"
|
||||||
|
FILE_DELAY_MAX="3.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Случайная пауза перед каждым запросом `download-url` на этапе 2.
|
||||||
|
|
||||||
|
### Длительные паузы
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
COOLDOWN_EVERY_FILES="100"
|
||||||
|
COOLDOWN_MIN="60"
|
||||||
|
COOLDOWN_MAX="120"
|
||||||
|
```
|
||||||
|
|
||||||
|
После каждых 100 запросов `download-url` программа дополнительно ждёт от 60 до 120 секунд.
|
||||||
|
|
||||||
|
Чтобы отключить длительные паузы:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
COOLDOWN_EVERY_FILES="0"
|
||||||
|
COOLDOWN_MIN="0"
|
||||||
|
COOLDOWN_MAX="0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP-таймауты и повторы
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CONNECT_TIMEOUT="30"
|
||||||
|
READ_TIMEOUT="120"
|
||||||
|
HTTP_RETRIES="5"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `CONNECT_TIMEOUT` — таймаут установки соединения;
|
||||||
|
- `READ_TIMEOUT` — таймаут чтения ответа API;
|
||||||
|
- `HTTP_RETRIES` — число автоматических повторов временных ошибок.
|
||||||
|
|
||||||
## Пример вывода
|
## Пример вывода
|
||||||
|
|
||||||
|
### Получение токена
|
||||||
|
|
||||||
|
```text
|
||||||
|
[AUTH] Открываю защищённую ссылку в Chromium...
|
||||||
|
[AUTH] Используется passToken из сохранённого профиля.
|
||||||
|
```
|
||||||
|
|
||||||
|
Либо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[AUTH] Поле пароля найдено (input[type="password"]).
|
||||||
|
[AUTH] Ввожу пароль публичной ссылки...
|
||||||
|
[AUTH] Пароль введён; отправляю форму...
|
||||||
|
[AUTH] passToken получен.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 1
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[INFO] Открываю исходную ссылку...
|
[INFO] Открываю исходную ссылку...
|
||||||
[INFO] Папка открыта.
|
[INFO] Папка открыта.
|
||||||
[INFO] Корневой hash: public_hash:
|
[INFO] Корневой hash: public_hash:/ODIS/Flashdaten/Brand-A
|
||||||
|
[INFO] Этап 1/2: получаю полный список файлов...
|
||||||
|
|
||||||
[DIR ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V
|
[DIR ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A
|
||||||
[GET ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V/038997016C__7000.sgo (1.75 MiB)
|
[PAGE] получено API=41, дочерних=40, служебных=1, offset=0->40, completed=False
|
||||||
[WAIT] 2.4 с перед запросом ссылки
|
[WAIT] 0.8 с перед следующей страницей
|
||||||
2026-07-19 URL:https://downloader.disk.yandex.ru/... [1835008/1835008]
|
[PAGE] получено API=10, дочерних=9, служебных=1, offset=12080->12089, completed=True
|
||||||
[PAGE] получено=41, следующий offset=41, completed=False
|
[INFO] Полный список получен: 12089 файлов.
|
||||||
[WAIT] 4.2 с перед следующей страницей
|
[INFO] Manifest сохранён: /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/yandex-download-manifest.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
После завершения выводится статистика:
|
### Этап 2
|
||||||
|
|
||||||
|
```text
|
||||||
|
[INFO] Этап 2/2: начинаю скачивание по сохранённому списку...
|
||||||
|
[SKIP] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/FL_0DL300014F_3123_idMA_sw.frf (2.14 MiB)
|
||||||
|
[GET ] /mnt/hdd1-4TB/ODIS/Flashdaten/Brand-A/FL_9J1915234BP_1654_40013103010803_V001_S.frf (774.53 KiB)
|
||||||
|
[WAIT] 2.6 с перед запросом ссылки
|
||||||
|
2026-08-03 URL:https://s684vla.storage.yandex.net/rdisk/... [793122/793122]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Итоговая статистика
|
||||||
|
|
||||||
```text
|
```text
|
||||||
================ РЕЗУЛЬТАТ ================
|
================ РЕЗУЛЬТАТ ================
|
||||||
Найдено файлов: 4500
|
Найдено файлов: 12089
|
||||||
Скачано сейчас: 250
|
Скачано сейчас: 529
|
||||||
Уже было скачано: 4250
|
Уже было скачано: 11560
|
||||||
Ошибок: 0
|
Ошибок: 0
|
||||||
Известный общий объём: 115.40 GiB
|
Известный общий объём: 49.04 GiB
|
||||||
===========================================
|
===========================================
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,48 +274,26 @@ https://disk.yandex.ru/showcaptcha?...
|
|||||||
/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V/yandex-captcha-url.txt
|
/mnt/hdd1-4TB/ODIS/Flashdaten/Brand-V/yandex-captcha-url.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Настройка задержек
|
## Ошибка `409 Conflict`
|
||||||
|
|
||||||
### FILE_DELAY_MIN и FILE_DELAY_MAX
|
Внутренний API `/public/api/fetch-list` может вернуть `409 Conflict`, если серверное состояние пагинации изменилось или стало недействительным.
|
||||||
|
|
||||||
Случайная задержка перед запросом прямой ссылки на каждый файл:
|
Двухэтапная схема значительно снижает вероятность такой ошибки, потому что все страницы каталога запрашиваются до начала скачивания файлов.
|
||||||
|
|
||||||
```python
|
`409` не следует считать признаком конца каталога. Если он всё же возник на этапе 1:
|
||||||
FILE_DELAY_MIN = 1.5
|
|
||||||
FILE_DELAY_MAX = 3.5
|
1. не удаляйте уже скачанные файлы;
|
||||||
|
2. повторно запустите программу;
|
||||||
|
3. скрипт получит новый `sk` и начнёт перечисление заново;
|
||||||
|
4. на этапе 2 существующие файлы будут пропущены.
|
||||||
|
|
||||||
|
Полным считается только обход, завершившийся серверным признаком:
|
||||||
|
|
||||||
|
```text
|
||||||
|
completed=True
|
||||||
```
|
```
|
||||||
|
|
||||||
Скрипт будет ждать от 1.5 до 3.5 секунды перед каждым запросом `download-url`.
|
|
||||||
|
|
||||||
### PAGE_DELAY_MIN и PAGE_DELAY_MAX
|
|
||||||
|
|
||||||
Задержка между страницами списка файлов:
|
|
||||||
|
|
||||||
```python
|
|
||||||
PAGE_DELAY_MIN = 3.0
|
|
||||||
PAGE_DELAY_MAX = 6.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### COOLDOWN_EVERY_FILES
|
|
||||||
|
|
||||||
Количество запросов файлов, после которого выполняется длинная пауза:
|
|
||||||
|
|
||||||
```python
|
|
||||||
COOLDOWN_EVERY_FILES = 100
|
|
||||||
```
|
|
||||||
|
|
||||||
При значении `100` длинная пауза выполняется после каждых 100 запросов.
|
|
||||||
|
|
||||||
### COOLDOWN_MIN и COOLDOWN_MAX
|
|
||||||
|
|
||||||
Диапазон длительной паузы:
|
|
||||||
|
|
||||||
```python
|
|
||||||
COOLDOWN_MIN = 60.0
|
|
||||||
COOLDOWN_MAX = 120.0
|
|
||||||
```
|
|
||||||
|
|
||||||
После каждых 100 запросов скрипт будет ждать от 60 до 120 секунд.
|
|
||||||
|
|
||||||
## Коды завершения
|
## Коды завершения
|
||||||
|
|
||||||
|
|||||||
@@ -1161,6 +1161,10 @@ class YandexProtectedFolderDownloader:
|
|||||||
|
|
||||||
self.visited_directories: set[str] = set()
|
self.visited_directories: set[str] = set()
|
||||||
self.seen_files: set[str] = set()
|
self.seen_files: set[str] = set()
|
||||||
|
self.planned_file_hashes: set[str] = set()
|
||||||
|
self.planned_files: list[
|
||||||
|
tuple[dict[str, Any], Path]
|
||||||
|
] = []
|
||||||
|
|
||||||
self.file_count = 0
|
self.file_count = 0
|
||||||
self.downloaded_count = 0
|
self.downloaded_count = 0
|
||||||
@@ -1883,17 +1887,10 @@ class YandexProtectedFolderDownloader:
|
|||||||
|
|
||||||
self.seen_files.add(resource_hash)
|
self.seen_files.add(resource_hash)
|
||||||
|
|
||||||
self.file_count += 1
|
|
||||||
|
|
||||||
expected_size = self.get_file_size(
|
expected_size = self.get_file_size(
|
||||||
item
|
item
|
||||||
)
|
)
|
||||||
|
|
||||||
if expected_size is not None:
|
|
||||||
self.total_known_bytes += (
|
|
||||||
expected_size
|
|
||||||
)
|
|
||||||
|
|
||||||
destination_directory = (
|
destination_directory = (
|
||||||
self.output_dir
|
self.output_dir
|
||||||
/ relative_directory
|
/ relative_directory
|
||||||
@@ -2016,14 +2013,11 @@ class YandexProtectedFolderDownloader:
|
|||||||
relative_directory: Path,
|
relative_directory: Path,
|
||||||
) -> list[tuple[str, str]]:
|
) -> list[tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Обработать страницу.
|
Добавить файлы страницы в план скачивания.
|
||||||
|
|
||||||
Возвращает список вложенных каталогов:
|
На первом этапе прямые ссылки не запрашиваются
|
||||||
|
и wget не запускается. Метод только сохраняет
|
||||||
[
|
описание файлов и возвращает вложенные каталоги.
|
||||||
(hash_каталога, локальное_имя),
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
subdirectories: list[
|
subdirectories: list[
|
||||||
@@ -2064,35 +2058,126 @@ class YandexProtectedFolderDownloader:
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item_type == "file":
|
if item_type != "file":
|
||||||
try:
|
continue
|
||||||
self.download_file(
|
|
||||||
item,
|
|
||||||
relative_directory,
|
|
||||||
)
|
|
||||||
|
|
||||||
except CaptchaRequired:
|
resource_hash = (
|
||||||
# CAPTCHA должна остановить
|
item.get("path")
|
||||||
# весь процесс, а не один файл.
|
or item.get("hash")
|
||||||
raise
|
)
|
||||||
|
|
||||||
except Exception as exc:
|
if (
|
||||||
local_path = (
|
not isinstance(resource_hash, str)
|
||||||
self.output_dir
|
or not resource_hash
|
||||||
/ relative_directory
|
):
|
||||||
/ name
|
print(
|
||||||
)
|
"[WARN] У файла "
|
||||||
|
f"{name!r} отсутствует path/hash.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
self.failed_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
print(
|
if resource_hash in self.planned_file_hashes:
|
||||||
f"[ERR ] {local_path}: "
|
continue
|
||||||
f"{exc}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.failed_count += 1
|
self.planned_file_hashes.add(
|
||||||
|
resource_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
self.planned_files.append(
|
||||||
|
(
|
||||||
|
item,
|
||||||
|
relative_directory,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.file_count += 1
|
||||||
|
|
||||||
|
expected_size = self.get_file_size(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
|
||||||
|
if expected_size is not None:
|
||||||
|
self.total_known_bytes += (
|
||||||
|
expected_size
|
||||||
|
)
|
||||||
|
|
||||||
return subdirectories
|
return subdirectories
|
||||||
|
|
||||||
|
def save_manifest(self) -> Path:
|
||||||
|
"""Атомарно сохранить полный план скачивания в JSONL."""
|
||||||
|
|
||||||
|
manifest_path = (
|
||||||
|
self.output_dir
|
||||||
|
/ "yandex-download-manifest.jsonl"
|
||||||
|
)
|
||||||
|
|
||||||
|
temporary_path = manifest_path.with_suffix(
|
||||||
|
manifest_path.suffix + ".tmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
with temporary_path.open(
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as manifest_file:
|
||||||
|
for item, relative_directory in self.planned_files:
|
||||||
|
record = {
|
||||||
|
"relative_directory": (
|
||||||
|
relative_directory.as_posix()
|
||||||
|
),
|
||||||
|
"item": item,
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest_file.write(
|
||||||
|
json.dumps(
|
||||||
|
record,
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
temporary_path.replace(
|
||||||
|
manifest_path
|
||||||
|
)
|
||||||
|
|
||||||
|
return manifest_path
|
||||||
|
|
||||||
|
def download_planned_files(self) -> None:
|
||||||
|
"""Скачать файлы из полностью собранного плана."""
|
||||||
|
|
||||||
|
for item, relative_directory in self.planned_files:
|
||||||
|
name = safe_component(
|
||||||
|
str(
|
||||||
|
item.get("name")
|
||||||
|
or "unnamed-file"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.download_file(
|
||||||
|
item,
|
||||||
|
relative_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
except CaptchaRequired:
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
local_path = (
|
||||||
|
self.output_dir
|
||||||
|
/ relative_directory
|
||||||
|
/ name
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[ERR ] {local_path}: {exc}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.failed_count += 1
|
||||||
|
|
||||||
def split_fetch_list_items(
|
def split_fetch_list_items(
|
||||||
self,
|
self,
|
||||||
items: list[dict[str, Any]],
|
items: list[dict[str, Any]],
|
||||||
@@ -2451,11 +2536,34 @@ class YandexProtectedFolderDownloader:
|
|||||||
f"{self.root_hash}"
|
f"{self.root_hash}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[INFO] Этап 1/2: получаю полный список файлов..."
|
||||||
|
)
|
||||||
|
|
||||||
self.crawl_directory(
|
self.crawl_directory(
|
||||||
self.root_hash,
|
self.root_hash,
|
||||||
Path("."),
|
Path("."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
manifest_path = self.save_manifest()
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[INFO] Полный список получен: "
|
||||||
|
f"{self.file_count} файлов."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[INFO] Manifest сохранён: "
|
||||||
|
f"{manifest_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[INFO] Этап 2/2: начинаю скачивание "
|
||||||
|
"по сохранённому списку..."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.download_planned_files()
|
||||||
|
|
||||||
self.print_summary()
|
self.print_summary()
|
||||||
|
|
||||||
def print_summary(self) -> None:
|
def print_summary(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user