fix(cookies): не давать yt-dlp затирать мастер-файл кук, честнее алерты
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 52s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 52s
Реальный инцидент: cookies "протухли" за пару дней вместо ~года. Причина —
не срок годности, а сам yt-dlp: --cookies FILE читает И дописывает cookie
jar обратно в файл после каждого запуска (--help: "read cookies from and
dump cookie jar in"), а когда Instagram-экстрактор решает, что сессия
невалидна, он явно чистит sessionid из jar'а — и это тут же сохраняется на
диск через YoutubeDL.close(). Наш собственный health-check (каждые 30 мин)
и обычные скачивания медленно, но верно стирали себе рабочие cookies.
Фикс: yt-dlp больше никогда не видит мастер-файл, только одноразовую копию
в фиксированном /tmp-пути (безопасно — оба сервиса --workers=1, гонок нет).
Проверено: md5sum/mtime мастер-файлов не меняются ни после серии
/cookies/check, ни после реального /download/stream.
Заодно в bot.py: notify_admin_cookie_alert больше не заявляет "это НЕ
cookies" для extraction_failed — на практике это оказалось не всегда
верно (анонимный rate-limit тоже "не cookies" по факту, но валидная
сессия могла бы его обойти). В алерты добавлена проверяемая ссылка
(test_url из /cookies/check), чтобы сразу было видно, что это health-check
дёргает тестовый ролик, а не реальная ссылка пользователя. Новый статус
cookies_incomplete детектирует "файл есть, но sessionid нет" ещё до
сетевых проверок — ловит именно тот случай, что привёл к инциденту.
Отдельно: пользователю теперь показывается понятное сообщение, когда
Instagram сам блокирует контент как возрастной/чувствительный
("can't be seen by certain audiences") — вместо общего "Something went
wrong", раз повторная попытка всё равно не поможет. Админ по-прежнему
получает полный технический текст без изменений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8fa53f7cfc
commit
c9972f3d68
3 changed files with 93 additions and 25 deletions
34
bot.py
34
bot.py
|
|
@ -624,12 +624,14 @@ def _save_cookie_state(state: dict):
|
||||||
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
|
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def notify_admin_cookie_alert(service: str, status: str, detail: str):
|
async def notify_admin_cookie_alert(service: str, status: str, detail: str, test_url: str = ''):
|
||||||
"""Проактивное уведомление от health-check, не связано с ошибкой конкретного пользователя.
|
"""Проактивное уведомление от health-check, не связано с ошибкой конкретного пользователя.
|
||||||
|
|
||||||
status: 'ok' | 'cookies_invalid' | 'extraction_failed'
|
status: 'ok' | 'cookies_invalid' | 'cookies_incomplete' | 'extraction_failed'
|
||||||
Тексты разные намеренно: сбой извлечения (версия yt-dlp, JS-рантайм) не должен
|
Тексты разные намеренно, но НЕ утверждают того, чего не знаем: extraction_failed —
|
||||||
выглядеть как протухшие cookies, иначе разбор уходит не туда.
|
это просто "не получилось", а не гарантия, что дело не в cookies (на практике оба
|
||||||
|
пути — с cookies и анонимный — иногда падают по причинам, так или иначе связанным
|
||||||
|
с cookies, например анонимный rate-limit, который валидная сессия могла бы обойти).
|
||||||
"""
|
"""
|
||||||
if not ADMIN_BOT_TOKEN:
|
if not ADMIN_BOT_TOKEN:
|
||||||
return
|
return
|
||||||
|
|
@ -646,18 +648,27 @@ async def notify_admin_cookie_alert(service: str, status: str, detail: str):
|
||||||
)
|
)
|
||||||
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
|
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
|
||||||
|
|
||||||
|
url_line = f"🔗 Проверялось на: {test_url}\n" if test_url else ""
|
||||||
|
|
||||||
if status == 'ok':
|
if status == 'ok':
|
||||||
text = f"✅ Снова работает\n\n🍪 Сервис: {service}\nПроверка проходит успешно."
|
text = f"✅ Снова работает\n\n🍪 Сервис: {service}\n{url_line}Проверка проходит успешно."
|
||||||
elif status == 'cookies_invalid':
|
elif status == 'cookies_invalid':
|
||||||
text = (
|
text = (
|
||||||
f"🍪⚠️ COOKIES ПРОТУХЛИ\n\n🍪 Сервис: {service}\n❗ {detail}\n\n"
|
f"🍪⚠️ COOKIES ПРОТУХЛИ\n\n🍪 Сервис: {service}\n{url_line}❗ {detail}\n\n"
|
||||||
"Обновить по инструкции в cookies/README.md "
|
"Обновить по инструкции в cookies/README.md "
|
||||||
"(для YouTube — только через приватное окно)."
|
"(для YouTube — только через приватное окно)."
|
||||||
)
|
)
|
||||||
|
elif status == 'cookies_incomplete':
|
||||||
|
text = (
|
||||||
|
f"🍪⚠️ COOKIES НЕПОЛНЫЕ\n\n🍪 Сервис: {service}\n{url_line}❗ {detail}\n\n"
|
||||||
|
"Файл cookies есть, но реальной сессии в нём нет (похоже на неправильный "
|
||||||
|
"экспорт). Переэкспортировать по инструкции в cookies/README.md."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
text = (
|
text = (
|
||||||
f"🛠⚠️ СЕРВИС НЕ МОЖЕТ ИЗВЛЕЧЬ ВИДЕО\n\n🍪 Сервис: {service}\n❗ {detail}\n\n"
|
f"🛠⚠️ СЕРВИС НЕ МОЖЕТ ИЗВЛЕЧЬ ВИДЕО\n\n🍪 Сервис: {service}\n{url_line}❗ {detail}\n\n"
|
||||||
"Это НЕ cookies — проверить версию yt-dlp и логи сервиса."
|
"Причина неясна — может быть версия yt-dlp/JS-рантайм, может быть и cookies. "
|
||||||
|
"Смотреть логи сервиса и текст ошибки выше."
|
||||||
)
|
)
|
||||||
if len(text) > 4000:
|
if len(text) > 4000:
|
||||||
text = text[:4000] + "…"
|
text = text[:4000] + "…"
|
||||||
|
|
@ -695,21 +706,22 @@ async def check_cookies_health():
|
||||||
continue # куки для этого сервиса не настроены — нечего проверять
|
continue # куки для этого сервиса не настроены — нечего проверять
|
||||||
|
|
||||||
detail = data.get('detail', '')
|
detail = data.get('detail', '')
|
||||||
|
test_url = data.get('test_url', '')
|
||||||
currently_healthy = status == 'ok'
|
currently_healthy = status == 'ok'
|
||||||
was_healthy = state.get(service, 'healthy') == 'healthy'
|
was_healthy = state.get(service, 'healthy') == 'healthy'
|
||||||
|
|
||||||
if not currently_healthy and was_healthy:
|
if not currently_healthy and was_healthy:
|
||||||
state[service] = 'broken'
|
state[service] = 'broken'
|
||||||
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
||||||
await notify_admin_cookie_alert(service, status, detail)
|
await notify_admin_cookie_alert(service, status, detail, test_url)
|
||||||
elif not currently_healthy and not was_healthy:
|
elif not currently_healthy and not was_healthy:
|
||||||
last = state.get('last_reminder_ts', {}).get(service, 0)
|
last = state.get('last_reminder_ts', {}).get(service, 0)
|
||||||
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
|
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
|
||||||
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
state.setdefault('last_reminder_ts', {})[service] = time.time()
|
||||||
await notify_admin_cookie_alert(service, status, detail)
|
await notify_admin_cookie_alert(service, status, detail, test_url)
|
||||||
elif currently_healthy and not was_healthy:
|
elif currently_healthy and not was_healthy:
|
||||||
state[service] = 'healthy'
|
state[service] = 'healthy'
|
||||||
await notify_admin_cookie_alert(service, 'ok', '')
|
await notify_admin_cookie_alert(service, 'ok', '', test_url)
|
||||||
|
|
||||||
_save_cookie_state(state)
|
_save_cookie_state(state)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Instagram Video Downloader Service
|
||||||
Отдельный микросервис для скачивания видео с Instagram
|
Отдельный микросервис для скачивания видео с Instagram
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -26,6 +27,15 @@ CORS(app) # Разрешаем CORS для взаимодействия с ос
|
||||||
DOWNLOADS_DIR = Path('downloads')
|
DOWNLOADS_DIR = Path('downloads')
|
||||||
DOWNLOADS_DIR.mkdir(exist_ok=True)
|
DOWNLOADS_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# yt-dlp трактует --cookies/cookiefile как read-write: после каждого запуска он
|
||||||
|
# дописывает cookie jar обратно в файл (--help: "read cookies from and dump cookie
|
||||||
|
# jar in"), а когда решает, что сессия невалидна — сам чистит sessionid из jar'а и
|
||||||
|
# тут же сохраняет это на диск. В прошлый раз это молча стёрло единственный экземпляр
|
||||||
|
# настоящих cookies. Поэтому yt-dlp всегда получает одноразовую копию в этот файл,
|
||||||
|
# а мастер-файл он больше никогда не видит. Фиксированное имя безопасно, т.к. сервис
|
||||||
|
# работает в один воркер (см. Dockerfile CMD --workers=1).
|
||||||
|
INSTAGRAM_COOKIES_SCRATCH = Path('/tmp/instagram_cookies_scratch.txt')
|
||||||
|
|
||||||
|
|
||||||
def check_instagram_cookies_expiry() -> tuple[bool, int]:
|
def check_instagram_cookies_expiry() -> tuple[bool, int]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -150,7 +160,8 @@ def download_instagram_video(url: str, max_retries: int = 3) -> Path:
|
||||||
'socket_timeout': 30,
|
'socket_timeout': 30,
|
||||||
}
|
}
|
||||||
if use_cookies:
|
if use_cookies:
|
||||||
ydl_opts['cookiefile'] = str(cookies_file_path.absolute())
|
shutil.copy2(cookies_file_path, INSTAGRAM_COOKIES_SCRATCH)
|
||||||
|
ydl_opts['cookiefile'] = str(INSTAGRAM_COOKIES_SCRATCH)
|
||||||
headers = {
|
headers = {
|
||||||
'Referer': 'https://www.instagram.com/',
|
'Referer': 'https://www.instagram.com/',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
|
@ -208,7 +219,8 @@ def _probe_extract(url: str, cookies_file: Path | None) -> tuple[bool, str]:
|
||||||
"""Пробует достать метаданные (без скачивания). Возвращает (получилось, текст ошибки)."""
|
"""Пробует достать метаданные (без скачивания). Возвращает (получилось, текст ошибки)."""
|
||||||
ydl_opts = {'quiet': True, 'no_warnings': True, 'socket_timeout': 20}
|
ydl_opts = {'quiet': True, 'no_warnings': True, 'socket_timeout': 20}
|
||||||
if cookies_file is not None:
|
if cookies_file is not None:
|
||||||
ydl_opts['cookiefile'] = str(cookies_file.absolute())
|
shutil.copy2(cookies_file, INSTAGRAM_COOKIES_SCRATCH)
|
||||||
|
ydl_opts['cookiefile'] = str(INSTAGRAM_COOKIES_SCRATCH)
|
||||||
ydl_opts['http_headers'] = {'Referer': 'https://www.instagram.com/'}
|
ydl_opts['http_headers'] = {'Referer': 'https://www.instagram.com/'}
|
||||||
try:
|
try:
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
|
@ -218,6 +230,29 @@ def _probe_extract(url: str, cookies_file: Path | None) -> tuple[bool, str]:
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_instagram_sessionid(cookies_file: Path) -> bool:
|
||||||
|
"""Проверяет, есть ли в файле реальный sessionid для instagram.com.
|
||||||
|
|
||||||
|
Без sessionid yt-dlp считает сессию неавторизованной (_is_logged_in проверяет
|
||||||
|
именно эту куку) и всегда идёт анонимным путём — тогда прод может часами слать
|
||||||
|
непонятный "rate-limit for accessing posts anonymously", хотя причина в том, что
|
||||||
|
cookies были экспортированы неправильно (например, `yt-dlp --cookies-from-browser`
|
||||||
|
без реального логина или дампом всех доменов браузера сразу — это ровно тот
|
||||||
|
антипаттерн, который убрали из cookies/, см. cookies/README.md).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(cookies_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('#') or not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.rstrip('\n').split('\t')
|
||||||
|
if len(parts) >= 7 and 'instagram' in parts[0].lower() and parts[5] == 'sessionid':
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@app.route('/cookies/check', methods=['POST'])
|
@app.route('/cookies/check', methods=['POST'])
|
||||||
def cookies_check():
|
def cookies_check():
|
||||||
"""Проверяет, способен ли сервис прямо сейчас скачать публичное видео.
|
"""Проверяет, способен ли сервис прямо сейчас скачать публичное видео.
|
||||||
|
|
@ -233,13 +268,19 @@ def cookies_check():
|
||||||
cookies_file = Path(os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt'))
|
cookies_file = Path(os.getenv('INSTAGRAM_COOKIES_FILE', 'instagram_cookies.txt'))
|
||||||
if not cookies_file.exists():
|
if not cookies_file.exists():
|
||||||
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
||||||
'status': 'no_cookies',
|
'status': 'no_cookies', 'test_url': INSTAGRAM_COOKIE_TEST_URL,
|
||||||
'detail': 'cookies file missing'}), 200
|
'detail': 'cookies file missing'}), 200
|
||||||
|
|
||||||
|
if not _has_instagram_sessionid(cookies_file):
|
||||||
|
return jsonify({'cookies_present': True, 'cookies_valid': False,
|
||||||
|
'status': 'cookies_incomplete', 'test_url': INSTAGRAM_COOKIE_TEST_URL,
|
||||||
|
'detail': 'в файле нет sessionid для instagram.com — это не рабочая '
|
||||||
|
'сессия, yt-dlp всегда идёт анонимным путём'}), 200
|
||||||
|
|
||||||
ok_with, err_with = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, cookies_file)
|
ok_with, err_with = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, cookies_file)
|
||||||
if ok_with:
|
if ok_with:
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': True,
|
return jsonify({'cookies_present': True, 'cookies_valid': True,
|
||||||
'status': 'ok', 'detail': ''}), 200
|
'status': 'ok', 'test_url': INSTAGRAM_COOKIE_TEST_URL, 'detail': ''}), 200
|
||||||
|
|
||||||
ok_without, err_without = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, None)
|
ok_without, err_without = _probe_extract(INSTAGRAM_COOKIE_TEST_URL, None)
|
||||||
if ok_without:
|
if ok_without:
|
||||||
|
|
@ -247,12 +288,16 @@ def cookies_check():
|
||||||
# фоллбэк — так же, как реальный download_instagram_video. Не поднимаем тревогу.
|
# фоллбэк — так же, как реальный download_instagram_video. Не поднимаем тревогу.
|
||||||
detail = f"cookies-путь сломан, но fallback без cookies работает: {err_with[-400:]}"
|
detail = f"cookies-путь сломан, но fallback без cookies работает: {err_with[-400:]}"
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||||
'status': 'ok', 'detail': detail}), 200
|
'status': 'ok', 'test_url': INSTAGRAM_COOKIE_TEST_URL, 'detail': detail}), 200
|
||||||
|
|
||||||
# Не работает и анонимный путь, который cookies вообще не использует —
|
# Не работает и анонимный путь. cookies при этом присутствуют и содержат sessionid,
|
||||||
# значит дело не в cookies, а в более общей поломке (сайт/экстрактор/сеть).
|
# так что дело не в их отсутствии — но могло оказаться, что именно этот sessionid
|
||||||
|
# невалиден на сервере, и тогда yt-dlp всё равно скатывается в анонимный путь.
|
||||||
|
# Однозначно утверждать "это не cookies" при отказе анонимного пути нельзя — текст
|
||||||
|
# ошибки и решение показываем как есть, без готового вердикта.
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||||
'status': 'extraction_failed', 'detail': err_without[-500:]}), 200
|
'status': 'extraction_failed', 'test_url': INSTAGRAM_COOKIE_TEST_URL,
|
||||||
|
'detail': err_without[-500:]}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route('/download/stream', methods=['POST'])
|
@app.route('/download/stream', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ YouTube Video Downloader Service
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -135,6 +136,13 @@ INFO_TIMEOUT = 60
|
||||||
|
|
||||||
EXTRACTOR_ARGS = 'youtube:player_client=web,android;skip=translated_subs,hls'
|
EXTRACTOR_ARGS = 'youtube:player_client=web,android;skip=translated_subs,hls'
|
||||||
|
|
||||||
|
# yt-dlp трактует --cookies как read-write: после каждого запуска он дописывает cookie
|
||||||
|
# jar обратно в файл (--help: "read cookies from and dump cookie jar in"), а когда
|
||||||
|
# решает, что cookies "no longer valid" — сам чистит куки сессии из jar'а и тут же
|
||||||
|
# сохраняет это на диск. Поэтому yt-dlp всегда получает одноразовую копию, мастер-файл
|
||||||
|
# он больше не видит. Фиксированное имя безопасно — сервис в один воркер (--workers=1).
|
||||||
|
YOUTUBE_COOKIES_SCRATCH = Path('/tmp/youtube_cookies_scratch.txt')
|
||||||
|
|
||||||
|
|
||||||
def _build_ytdlp_base_cmd() -> list:
|
def _build_ytdlp_base_cmd() -> list:
|
||||||
"""Базовые аргументы yt-dlp CLI."""
|
"""Базовые аргументы yt-dlp CLI."""
|
||||||
|
|
@ -148,7 +156,8 @@ def _build_ytdlp_base_cmd() -> list:
|
||||||
'--no-playlist',
|
'--no-playlist',
|
||||||
]
|
]
|
||||||
if cookies_file.exists() and cookies_file.stat().st_size > 0:
|
if cookies_file.exists() and cookies_file.stat().st_size > 0:
|
||||||
cmd += ['--cookies', str(cookies_file.absolute())]
|
shutil.copy2(cookies_file, YOUTUBE_COOKIES_SCRATCH)
|
||||||
|
cmd += ['--cookies', str(YOUTUBE_COOKIES_SCRATCH)]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -506,7 +515,7 @@ def cookies_check():
|
||||||
cookies_file = Path(os.getenv('YOUTUBE_COOKIES_FILE', '/app/youtube_cookies.txt'))
|
cookies_file = Path(os.getenv('YOUTUBE_COOKIES_FILE', '/app/youtube_cookies.txt'))
|
||||||
if not (cookies_file.exists() and cookies_file.stat().st_size > 0):
|
if not (cookies_file.exists() and cookies_file.stat().st_size > 0):
|
||||||
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
return jsonify({'cookies_present': False, 'cookies_valid': None,
|
||||||
'status': 'no_cookies',
|
'status': 'no_cookies', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||||
'detail': 'cookies file missing or empty'}), 200
|
'detail': 'cookies file missing or empty'}), 200
|
||||||
|
|
||||||
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
|
# Без --quiet/--no-warnings: сигнал ротации кук — это подавляемый WARNING, а не ошибка с ненулевым returncode
|
||||||
|
|
@ -515,7 +524,7 @@ def cookies_check():
|
||||||
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
|
result = _run_ytdlp(cmd, timeout=INFO_TIMEOUT)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||||
'status': 'extraction_failed',
|
'status': 'extraction_failed', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||||
'detail': f'yt-dlp invocation failed: {e}'}), 200
|
'detail': f'yt-dlp invocation failed: {e}'}), 200
|
||||||
|
|
||||||
stderr = result.stderr
|
stderr = result.stderr
|
||||||
|
|
@ -523,14 +532,16 @@ def cookies_check():
|
||||||
|
|
||||||
if any(marker in stderr.lower() for marker in YOUTUBE_COOKIE_INVALID_MARKERS):
|
if any(marker in stderr.lower() for marker in YOUTUBE_COOKIE_INVALID_MARKERS):
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': False,
|
return jsonify({'cookies_present': True, 'cookies_valid': False,
|
||||||
'status': 'cookies_invalid', 'detail': detail}), 200
|
'status': 'cookies_invalid', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||||
|
'detail': detail}), 200
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
return jsonify({'cookies_present': True, 'cookies_valid': None,
|
||||||
'status': 'extraction_failed', 'detail': detail}), 200
|
'status': 'extraction_failed', 'test_url': YOUTUBE_COOKIE_TEST_URL,
|
||||||
|
'detail': detail}), 200
|
||||||
|
|
||||||
return jsonify({'cookies_present': True, 'cookies_valid': True,
|
return jsonify({'cookies_present': True, 'cookies_valid': True,
|
||||||
'status': 'ok', 'detail': ''}), 200
|
'status': 'ok', 'test_url': YOUTUBE_COOKIE_TEST_URL, 'detail': ''}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route('/download/stream', methods=['POST'])
|
@app.route('/download/stream', methods=['POST'])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue