Advertisement
Payday4396

Script para fazer o download de videoaulas e PDFs no Estratégia - versão 3.1

Jul 5th, 2025
69
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 41.34 KB | None | 0 0
  1. import customtkinter as ctk
  2. from customtkinter import filedialog
  3. import json
  4. import asyncio
  5. import aiohttp
  6. from pathlib import Path
  7. from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError, Error
  8. import logging
  9. import re
  10. import sys
  11. import threading
  12. import queue
  13.  
  14. # --- CONFIGURAÇÃO INICIAL E VARIÁVEIS GLOBAIS ---
  15. try:
  16. with open('config.json', 'r', encoding='utf-8') as f:
  17. CONFIG = json.load(f)
  18. except Exception as e:
  19. print(f"Erro ao carregar config.json: {e}")
  20. CONFIG = {}
  21.  
  22. try:
  23. with open('course-urls.json', 'r', encoding='utf-8') as f:
  24. COURSE_URLS = json.load(f)
  25. except Exception:
  26. COURSE_URLS = []
  27.  
  28. # --- REDIRECIONAMENTO DO LOG PARA A INTERFACE GRÁFICA ---
  29. class QueueHandler(logging.Handler):
  30. def __init__(self, log_queue):
  31. super().__init__()
  32. self.log_queue = log_queue
  33. def emit(self, record):
  34. self.log_queue.put(self.format(record))
  35.  
  36. class PrintRedirector:
  37. def __init__(self, logger_instance):
  38. self.logger = logger_instance
  39. def write(self, message):
  40. if message.strip(): self.logger.info(message.strip())
  41. def flush(self):
  42. pass
  43.  
  44. # --- INÍCIO DA SEÇÃO DE LÓGICA COMPLETA DO DOWNLOADER ---
  45. PROGRESS_FILE = Path('progress.json')
  46. RESOLUCOES_DISPONIVEIS = ['720p', '480p', '360p']
  47. PDF_TYPES = {
  48. 1: {'name': 'versão simplificada', 'urlPart': 'pdfSimplificado/download'},
  49. 2: {'name': 'versão original', 'urlPart': 'pdf/download'},
  50. 3: {'name': 'marcação dos aprovados', 'urlPart': 'pdfGrifado/download'}
  51. }
  52.  
  53. def sanitize_filename(filename, max_length=200):
  54. sanitized = ''.join('_' if c in '<>:"/\\|?*' else c for c in filename).strip()
  55. sanitized = re.sub(r'\s+', ' ', sanitized)
  56. if len(sanitized) > max_length:
  57. sanitized = sanitized[:max_length].rsplit(' ', 1)[0].rstrip('.')
  58. return sanitized
  59. async def download_file(url, file_path, logger, retries=3):
  60. for attempt in range(1, retries + 1):
  61. try:
  62. async with aiohttp.ClientSession() as session:
  63. async with session.get(url, timeout=300) as response:
  64. if response.status != 200:
  65. raise Exception(f'Status de resposta inválido: {response.status}')
  66. with open(file_path, 'wb') as f:
  67. async for chunk in response.content.iter_chunked(8192):
  68. f.write(chunk)
  69. logger.info(f'Arquivo baixado: {file_path}')
  70. return
  71. except Exception as e:
  72. logger.error(f'Tentativa {attempt}/{retries} falhou ao baixar {url}: {e}')
  73. if attempt == retries:
  74. raise Exception(f'Falha ao baixar {url} após {retries} tentativas.')
  75. await asyncio.sleep(2 ** attempt)
  76. async def verify_download(file_path, logger):
  77. size = file_path.stat().st_size
  78. if size == 0:
  79. raise ValueError(f'Arquivo {file_path} está vazio. Download pode ter falhado.')
  80. logger.info(f'Arquivo {file_path} verificado: {size} bytes')
  81. return True
  82. async def load_progress():
  83. try:
  84. with open(PROGRESS_FILE, 'r', encoding='utf-8') as f:
  85. return json.load(f)
  86. except (FileNotFoundError, json.JSONDecodeError):
  87. return {}
  88. async def save_progress(progress):
  89. with open(PROGRESS_FILE, 'w', encoding='utf-8') as f:
  90. json.dump(progress, f, indent=2)
  91.  
  92. def extract_materia_name(course_name):
  93. if 'Conhecimentos Regionais' in course_name: return 'Conhecimentos Regionais'
  94.  
  95. materia = course_name
  96.  
  97. # Esta linha é a mais importante e garante a remoção do prefixo
  98. materia = re.sub(r'^Concursos da Área Fiscal\s*-\s*', '', materia, flags=re.IGNORECASE)
  99.  
  100. materia = re.sub(r'Curso (Completo|Básico) de ', '', materia, flags=re.IGNORECASE)
  101. materia = re.sub(r'\(Profs?\.?[\w\s,]+\)', '', materia, flags=re.IGNORECASE)
  102. materia = re.sub(r'\s*-\s*\d{4}(?!\d)', '', materia)
  103. materia = re.sub(r'\s*\(Pós-Edital\)', '', materia, flags=re.IGNORECASE)
  104. materia = re.sub(r'Prefeitura [\w\s-]+?-', '', materia, flags=re.IGNORECASE)
  105. materia = re.sub(r'\([^)]+\)', '', materia)
  106. materia = re.sub(r' - Área Administrativa', '', materia, flags=re.IGNORECASE)
  107. materia = re.sub(r'Noções de ', '', materia, flags=re.IGNORECASE)
  108. materia = re.sub(r'^\s*-\s*|\s*-\s*$', '', materia)
  109. materia = materia.strip()
  110.  
  111. if ':' in materia: materia = materia.split(':')[1].strip()
  112.  
  113. return materia or 'Matéria Desconhecida'
  114.  
  115. async def ensure_logged_in(page, logger):
  116. LOGIN_URL = 'https://www.estrategiaconcursos.com.br/app/auth/login'
  117. ASSINATURAS_URL = 'https://www.estrategiaconcursos.com.br/app/dashboard/assinaturas'
  118.  
  119. logger.info("Navegando para a página de login para verificação de estado...")
  120. await page.goto(LOGIN_URL, wait_until='domcontentloaded', timeout=60000)
  121.  
  122. try:
  123. logger.info("Verificando se o login já está ativo...")
  124. await page.wait_for_selector('a:has-text("Catálogo de Cursos")', timeout=10000)
  125. logger.info("Login já está ativo.")
  126.  
  127. except PlaywrightTimeoutError:
  128. logger.info("Login não está ativo. Procedendo com o preenchimento de credenciais...")
  129. try:
  130. await page.wait_for_selector('input[name="loginField"]', state='visible', timeout=60000)
  131. await page.fill('input[name="loginField"]', CONFIG['email'])
  132. await page.wait_for_selector('input[name="passwordField"]', state='visible', timeout=60000)
  133. await page.fill('input[name="passwordField"]', CONFIG['senha'])
  134. await page.click('button[type="submit"]')
  135. await page.wait_for_selector('a:has-text("Catálogo de Cursos")', timeout=60000)
  136. logger.info("Login realizado com sucesso!")
  137. except PlaywrightTimeoutError as e:
  138. logger.error(f"Tempo de espera excedido durante a tentativa de login.")
  139. raise Exception("Falha na etapa de login.")
  140.  
  141. # ▼▼▼ CORREÇÃO APLICADA AQUI: LÓGICA DE PÓS-LOGIN ROBUSTA ▼▼▼
  142. try:
  143. logger.info("Verificando redirecionamento pós-login...")
  144. current_url = page.url
  145.  
  146. # Cenário 1: Redirecionado para a página de perfil (NOVA LÓGICA)
  147. if "perfil.estrategia.com" in current_url:
  148. logger.warning(f"Redirecionado para a página de perfil. Navegando para o catálogo: {ASSINATURAS_URL}")
  149. await page.goto(ASSINATURAS_URL, wait_until='domcontentloaded', timeout=60000)
  150. current_url = page.url # Atualiza a URL após a navegação para a próxima verificação
  151.  
  152. # Cenário 2: Redirecionado para o dashboard genérico (LÓGICA EXISTENTE - NÃO ALTERADA)
  153. if "/app/dashboard/cursos" == current_url.replace('https://www.estrategiaconcursos.com.br', ''):
  154. logger.warning("Página do dashboard genérico detectada. Clicando em 'Catálogo de Cursos' para prosseguir...")
  155. await page.locator('a:has-text("Catálogo de Cursos")').click()
  156. await page.wait_for_url("**/app/dashboard/assinaturas", timeout=30000)
  157. logger.info("Navegação para o catálogo de cursos concluída.")
  158.  
  159. except Exception as e:
  160. logger.error(f"Ocorreu um erro inesperado durante a verificação de redirecionamento pós-login: {e}")
  161. raise
  162.  
  163. async def ensure_lesson_expanded(page, aula_id, logger):
  164. logger.info(f'Garantindo que a aula #{aula_id} esteja expandida...')
  165. try:
  166. # Tenta expandir a aula clicando no cabeçalho
  167. await page.evaluate(f'''(id) => {{
  168. const header = document.querySelector(`#${{id}} .Collapse-header`);
  169. if (header && header.parentElement.nextElementSibling.style.display === "none") {{
  170. header.click();
  171. }}
  172. }}''', aula_id)
  173. # Aguarda a lista de vídeos ficar visível (seletor genérico para cobrir variações)
  174. await page.wait_for_selector(f'#{aula_id} [class*="ListVideos"], #{aula_id} [class*="VideoItem"]', state='visible', timeout=15000)
  175. logger.info(f'Aula #{aula_id} expandida com sucesso.')
  176. except PlaywrightTimeoutError:
  177. logger.warning(f'Timeout ao tentar expandir a aula #{aula_id}. Tentando continuar...')
  178. except Exception as e:
  179. logger.error(f'Erro ao expandir a aula #{aula_id}: {e}')
  180. # Aguarda mais tempo para carregamento assíncrono
  181. await asyncio.sleep(3)
  182.  
  183. async def process_course_pdf(page, course_url, progress, logger):
  184. BASE_DIR = Path(CONFIG['pdfConfig']['pastaDownloads'])
  185.  
  186. logger.info(f"Navegando diretamente para a página do curso: {course_url}")
  187. await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
  188.  
  189. if "app/dashboard/cursos" in page.url and not re.search(r'/cursos/\d+/aulas', page.url):
  190. logger.warning(f"Redirecionado para o dashboard. Tentando navegar para {course_url} novamente.")
  191. await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
  192.  
  193. logger.info('Extraindo o nome do curso...')
  194. try:
  195. course_name_element = await page.wait_for_selector('.CourseInfo-content-title', timeout=30000)
  196. course_name = await course_name_element.text_content()
  197. logger.info(f'Nome do curso extraído: {course_name}')
  198. except PlaywrightTimeoutError:
  199. course_name = "Curso Desconhecido"
  200. logger.warning('Timeout ao extrair o nome do curso. Usando título da página.')
  201. course_name = await page.title()
  202.  
  203. materia_name = extract_materia_name(course_name)
  204. course_dir = BASE_DIR / sanitize_filename(materia_name)
  205. course_dir.mkdir(parents=True, exist_ok=True)
  206. logger.info(f'Pasta da matéria: {course_dir}')
  207. pdf_type_config = CONFIG['pdfConfig'].get('pdfType', 2)
  208. pdf_types_to_download = [1, 2, 3] if pdf_type_config == 4 else [pdf_type_config]
  209.  
  210. aulas = await page.locator('.LessonList-item').all()
  211. logger.info(f'Total de aulas encontradas: {len(aulas)}')
  212.  
  213. for i, aula_element in enumerate(aulas, 1):
  214. aula_id = await aula_element.get_attribute('id') or f'aula{i}'
  215. title_element = aula_element.locator(".LessonCollapseHeader-title .SectionTitle")
  216. subtitle_element = aula_element.locator(".LessonCollapseHeader-title .sc-gZMcBi")
  217. lesson_name_raw = await title_element.text_content() if await title_element.count() > 0 else f"Aula {i:02d}"
  218. lesson_subtitle_raw = await subtitle_element.text_content() if await subtitle_element.count() > 0 else "Sem Subtítulo"
  219. lesson_name = sanitize_filename(lesson_name_raw)
  220. lesson_subtitle = sanitize_filename(lesson_subtitle_raw)
  221.  
  222. logger.info(f'Processando aula "{lesson_name}"...')
  223. await ensure_lesson_expanded(page, aula_id, logger)
  224.  
  225. found_download_for_this_aula = False
  226.  
  227. # ▼▼▼ CORREÇÃO APLICADA AQUI ▼▼▼
  228. # Alteramos o seletor para uma versão mais robusta, baseada no texto visível do botão.
  229. all_buttons = await aula_element.locator('a:has-text("Baixar Livro Eletrônico")').all()
  230.  
  231. if all_buttons:
  232. found_download_for_this_aula = True
  233.  
  234. for button in all_buttons:
  235. pdf_url = await button.get_attribute('href')
  236. if not pdf_url: continue
  237.  
  238. pdf_url = "https://www.estrategiaconcursos.com.br" + pdf_url if pdf_url.startswith('/api') else pdf_url
  239.  
  240. for pdf_type in pdf_types_to_download:
  241. pdf_info = PDF_TYPES.get(pdf_type)
  242. if not pdf_info: continue
  243.  
  244. if pdf_info["urlPart"] in pdf_url:
  245. base_file_name = f'{lesson_name} - {lesson_subtitle}'
  246. file_name = f'{sanitize_filename(base_file_name, 180)} ({pdf_info["name"]}).pdf'
  247. file_path = course_dir / file_name
  248. progress_key = f'{aula_id}-{pdf_url}'
  249.  
  250. if progress.get(progress_key):
  251. logger.info(f'JÁ BAIXADO: "{file_name}". Pulando...')
  252. continue
  253. try:
  254. logger.info(f'BAIXANDO: "{file_name}"')
  255. await download_file(pdf_url, file_path, logger)
  256. await verify_download(file_path, logger)
  257. progress[progress_key] = True
  258. await save_progress(progress)
  259. logger.info(f'SUCESSO: Download de "{file_name}" concluído.')
  260. except Exception as e:
  261. logger.error(f'FALHA ao baixar "{file_name}": {e}')
  262.  
  263. break
  264. # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
  265.  
  266. if not found_download_for_this_aula:
  267. logger.warning(f'AVISO: Nenhum link de download encontrado para a aula "{lesson_name}". Pode ser uma aula sem PDF ou de outro tipo.')
  268.  
  269. async def get_video_url_by_resolution(page, preferred_resolution, logger):
  270. try:
  271. # Espera o player de vídeo estar visível e carregado
  272. await page.wait_for_selector('video.video-react-video', state='visible', timeout=30000)
  273. # Toca e pausa o vídeo para garantir que o player esteja ativo
  274. await page.evaluate('''() => {
  275. const v = document.querySelector("video.video-react-video");
  276. if(v){ v.play(); setTimeout(()=>v.pause(), 1000); }
  277. }''')
  278.  
  279. video_player_locator = page.locator('video.video-react-video')
  280. current_url = await video_player_locator.get_attribute('src')
  281.  
  282. # Se a resolução preferida for 360p e a URL atual já for 360p, retorna imediatamente
  283. if preferred_resolution == '360p' and '360p' in current_url:
  284. logger.info("Resolução 360p já está selecionada. Usando URL atual.")
  285. return {'url': current_url, 'resolution': '360p'}
  286.  
  287. # Abre o menu de opções do player
  288. await page.locator('.PlayerControl-button').click()
  289. await page.wait_for_selector('.PlayerControl-options', state='visible', timeout=15000)
  290.  
  291. resolution_buttons = await page.locator('.PlayerControlOptions-button').all()
  292. available_resolutions = {await btn.text_content(): btn for btn in resolution_buttons if await btn.text_content() in RESOLUCOES_DISPONIVEIS}
  293.  
  294. for res in [preferred_resolution] + RESOLUCOES_DISPONIVEIS:
  295. if res in available_resolutions:
  296. logger.info(f'Selecionando resolução {res}...')
  297. old_url = await video_player_locator.get_attribute('src')
  298.  
  299. # Usa JavaScript para clicar no botão, evitando problemas com interceptação
  300. await page.evaluate('''(res) => {
  301. const button = Array.from(document.querySelectorAll('.PlayerControlOptions-button')).find(btn => btn.textContent === res);
  302. if (button) button.click();
  303. }''', res)
  304.  
  305. # Espera a URL do vídeo mudar
  306. try:
  307. logger.info(f"Aguardando a URL do vídeo ser atualizada para {res}...")
  308. await page.wait_for_function(
  309. f'() => document.querySelector("video.video-react-video").src !== "{old_url}"',
  310. timeout=20000 # Aumentado para 20s
  311. )
  312. logger.info("URL do vídeo atualizada com sucesso.")
  313. except PlaywrightTimeoutError:
  314. logger.warning(f"A URL do vídeo não mudou após selecionar {res}. Usando a URL atual.")
  315.  
  316. new_url = await video_player_locator.get_attribute('src')
  317. return {'url': new_url, 'resolution': res}
  318.  
  319. # Fallback para a URL atual
  320. logger.warning("Nenhuma resolução pôde ser selecionada. Usando a URL original.")
  321. return {'url': current_url, 'resolution': 'padrão'}
  322.  
  323. except Exception as e:
  324. logger.error(f"Erro ao obter URL do vídeo na resolução {preferred_resolution}: {e}")
  325. return {'url': current_url, 'resolution': 'padrão'}
  326.  
  327. async def process_course_video(page, course_url, progress, logger):
  328. BASE_DIR = Path(CONFIG['videoConfig']['pastaDownloads'])
  329. logger.info(f"Navegando diretamente para a página do curso: {course_url}")
  330. await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
  331.  
  332. if "app/dashboard/cursos" in page.url and not re.search(r'/cursos/\d+/aulas', page.url):
  333. logger.warning(f"Redirecionado para o dashboard. Tentando navegar para {course_url} novamente.")
  334. await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
  335.  
  336. logger.info('Extraindo o nome do curso...')
  337. try:
  338. course_name_element = await page.wait_for_selector('.CourseInfo-content-title', timeout=30000)
  339. course_name = await course_name_element.text_content()
  340. except PlaywrightTimeoutError:
  341. course_name = await page.title()
  342.  
  343. materia_name = extract_materia_name(course_name)
  344. course_dir = BASE_DIR / sanitize_filename(materia_name)
  345. course_dir.mkdir(parents=True, exist_ok=True)
  346. logger.info(f'Pasta da matéria: {course_dir}')
  347.  
  348. aulas = await page.locator('.LessonList-item').all()
  349. logger.info(f'Total de aulas encontradas: {len(aulas)}')
  350.  
  351. for aula_element in aulas:
  352. aula_id = await aula_element.get_attribute('id')
  353. lesson_name_raw = await aula_element.locator(".LessonCollapseHeader-title .SectionTitle").text_content()
  354. lesson_name = sanitize_filename(lesson_name_raw)
  355. lesson_dir = course_dir / lesson_name
  356. lesson_dir.mkdir(parents=True, exist_ok=True)
  357.  
  358. logger.info(f'Processando aula "{lesson_name}"...')
  359. await ensure_lesson_expanded(page, aula_id, logger)
  360.  
  361. # ▼▼▼ CORREÇÃO APLICADA AQUI ▼▼▼
  362. # Usamos um seletor mais direto e robusto para a lista de vídeos (playlist)
  363. # e removemos a lógica de fallback que estava causando o comportamento incorreto.
  364. videos = await aula_element.locator('.ListVideos-items-video a.VideoItem').all()
  365.  
  366. if not videos:
  367. logger.warning(f'AVISO: Nenhum vídeo encontrado na aula "{lesson_name}". Pulando...')
  368. continue
  369.  
  370. logger.info(f'Encontrados {len(videos)} vídeos na aula "{lesson_name}".')
  371.  
  372. # Lógica para processar a lista de vídeos encontrada
  373. for j, video_element in enumerate(videos, 1):
  374. video_title_raw = await video_element.locator(".VideoItem-info-title").text_content()
  375. video_title = sanitize_filename(video_title_raw)
  376. progress_key = f'{aula_id}-{video_title}-{j}'
  377.  
  378. if progress.get(progress_key):
  379. logger.info(f'JÁ BAIXADO: "{video_title}". Pulando...')
  380. continue
  381.  
  382. try:
  383. # Clica no item da playlist para carregar o vídeo correto no player
  384. await video_element.click()
  385.  
  386. # A função get_video_url_by_resolution agora atuará no vídeo que acabamos de carregar
  387. video_info = await get_video_url_by_resolution(page, CONFIG['videoConfig']['resolucaoEscolhida'], logger)
  388. video_url, used_resolution = video_info['url'], video_info['resolution']
  389.  
  390. file_name = f'{lesson_name} - Vídeo {j} {video_title} [{used_resolution}].mp4'
  391. file_path = lesson_dir / sanitize_filename(file_name)
  392.  
  393. logger.info(f'BAIXANDO: "{file_name}"')
  394. await download_file(video_url, file_path, logger)
  395. await verify_download(file_path, logger)
  396. progress[progress_key] = True
  397. await save_progress(progress)
  398. logger.info(f'SUCESSO: Download de "{file_name}" concluído.')
  399.  
  400. # Não é mais necessário voltar a página, pois estamos na mesma página, apenas
  401. # trocando o vídeo no player principal. Uma pequena espera é suficiente.
  402. await asyncio.sleep(1)
  403.  
  404. except Exception as e:
  405. logger.error(f'FALHA ao baixar "{video_title}": {e}')
  406. # Se der erro, tenta recarregar a página do curso para se recuperar
  407. await page.goto(course_url, wait_until='domcontentloaded')
  408. # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
  409.  
  410. async def download_logic_main(progress_callback, log_queue):
  411. logger = logging.getLogger(__name__)
  412. logger.setLevel(logging.INFO)
  413. if not logger.handlers:
  414. queue_handler = QueueHandler(log_queue)
  415. formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', datefmt='%H:%M:%S')
  416. queue_handler.setFormatter(formatter)
  417. logger.addHandler(queue_handler)
  418. sys.stdout = PrintRedirector(logger)
  419. logger.info("Iniciando processo de download...")
  420. with open('course-urls.json', 'r', encoding='utf-8') as f:
  421. current_course_urls = json.load(f)
  422. total_cursos = len(current_course_urls)
  423. if total_cursos == 0:
  424. logger.warning("Nenhum curso na lista para baixar.")
  425. return
  426. async with async_playwright() as p:
  427. context = None
  428. try:
  429. chrome_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
  430. if not Path(chrome_path).exists():
  431. chrome_path_x86 = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
  432. if Path(chrome_path_x86).exists():
  433. chrome_path = chrome_path_x86
  434. else:
  435. raise FileNotFoundError("Google Chrome não foi encontrado nos caminhos padrão.")
  436. logger.info(f"Usando Google Chrome de: {chrome_path}")
  437. context = await p.chromium.launch_persistent_context(
  438. user_data_dir=Path.home() / "AppData" / "Local" / "EstrategiaDownloaderCache",
  439. headless=CONFIG.get('headless', False),
  440. executable_path=chrome_path,
  441. args=['--no-sandbox', '--disable-setuid-sandbox']
  442. )
  443. page = await context.new_page()
  444. except (Error, FileNotFoundError) as e:
  445. logger.error(f"Falha ao iniciar o Google Chrome: {e}")
  446. logger.error("Verifique se o Chrome está instalado ou ajuste o caminho no app.py.")
  447. return
  448. try:
  449. await ensure_logged_in(page, logger)
  450. progress_data = await load_progress()
  451. for i, course_url in enumerate(current_course_urls):
  452. logger.info(f"--- Processando curso {i+1}/{total_cursos} ---")
  453. download_type = CONFIG.get('downloadType', 'pdf')
  454. if download_type == 'pdf':
  455. await process_course_pdf(page, course_url, progress_data, logger)
  456. elif download_type == 'video':
  457. await process_course_video(page, course_url, progress_data, logger)
  458. progress_callback((i + 1) / total_cursos)
  459. except Exception as e:
  460. logger.error(f"Ocorreu um erro crítico durante o processo: {e}", exc_info=True)
  461. finally:
  462. if context:
  463. logger.info("Fechando navegador...")
  464. await context.close()
  465. logger.info("--- PROCESSO FINALIZADO ---")
  466.  
  467. # --- APLICAÇÃO GRÁFICA COM CUSTOMTKINTER ---
  468. class App(ctk.CTk):
  469. def __init__(self):
  470. super().__init__()
  471.  
  472. self.title("🦉 Estratégia Downloader Pro")
  473. self.geometry("1100x700")
  474. self.minsize(800, 600) # Define um tamanho mínimo para a janela
  475.  
  476. ctk.set_appearance_mode("dark")
  477. ctk.set_default_color_theme("blue")
  478.  
  479. self.log_queue = queue.Queue()
  480. self.download_thread = None
  481.  
  482. # --- Configuração do Grid Principal para Responsividade ---
  483. self.grid_columnconfigure(1, weight=1) # Permite que a coluna de conteúdo (1) se expanda
  484. self.grid_rowconfigure(0, weight=1) # Permite que a linha de conteúdo (0) se expanda
  485.  
  486. # --- Frame de Navegação (Sidebar) ---
  487. self.nav_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
  488. self.nav_frame.grid(row=0, column=0, sticky="nsew")
  489. self.nav_frame.grid_rowconfigure(5, weight=1) # Empurra o tema para o final
  490.  
  491. self.logo_label = ctk.CTkLabel(self.nav_frame, text="Downloader Pro", font=ctk.CTkFont(size=20, weight="bold"))
  492. self.logo_label.grid(row=0, column=0, padx=20, pady=20)
  493.  
  494. self.home_button = ctk.CTkButton(self.nav_frame, text="▶️ Início", command=self.show_home_frame)
  495. self.home_button.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
  496.  
  497. self.settings_button = ctk.CTkButton(self.nav_frame, text="⚙️ Configurações", command=self.show_settings_frame)
  498. self.settings_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
  499.  
  500. self.urls_button = ctk.CTkButton(self.nav_frame, text="📚 Cursos", command=self.show_urls_frame)
  501. self.urls_button.grid(row=3, column=0, padx=20, pady=10, sticky="ew")
  502.  
  503. self.logs_button = ctk.CTkButton(self.nav_frame, text="📄 Logs", command=self.show_logs_frame)
  504. self.logs_button.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
  505.  
  506. # --- Frames de Conteúdo ---
  507. self.home_frame = self.create_home_frame()
  508. self.settings_frame = self.create_settings_frame()
  509. self.urls_frame = self.create_urls_frame()
  510. self.logs_frame = self.create_logs_frame()
  511.  
  512. self.after(100, self.process_log_queue)
  513. self.show_home_frame()
  514.  
  515. def select_frame(self, frame_to_show):
  516. self.home_frame.grid_forget()
  517. self.settings_frame.grid_forget()
  518. self.urls_frame.grid_forget()
  519. self.logs_frame.grid_forget()
  520. frame_to_show.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
  521.  
  522. def show_home_frame(self): self.select_frame(self.home_frame)
  523. def show_settings_frame(self): self.load_settings_into_ui(); self.select_frame(self.settings_frame)
  524. def show_urls_frame(self): self.load_urls_into_ui(); self.select_frame(self.urls_frame)
  525. def show_logs_frame(self): self.select_frame(self.logs_frame)
  526.  
  527. def create_home_frame(self):
  528. frame = ctk.CTkFrame(self)
  529. frame.grid_columnconfigure(0, weight=1)
  530.  
  531. title = ctk.CTkLabel(
  532. frame, text="🦉 Downloader Pro – Painel Inicial",
  533. font=ctk.CTkFont(size=26, weight="bold"),
  534. text_color="#1E90FF"
  535. )
  536. title.grid(row=0, column=0, padx=20, pady=(30, 10))
  537.  
  538. self.start_button = ctk.CTkButton(
  539. frame, text="⏬ INICIAR DOWNLOADS", height=55, width=250,
  540. font=ctk.CTkFont(size=18, weight="bold"),
  541. fg_color="#00897B", hover_color="#00BFA5", text_color="white",
  542. corner_radius=8, command=self.start_download
  543. )
  544. self.start_button.grid(row=1, column=0, padx=20, pady=20)
  545.  
  546. progress_label = ctk.CTkLabel(
  547. frame, text="📦 Progresso dos Downloads:",
  548. font=ctk.CTkFont(size=14), anchor="w"
  549. )
  550. progress_label.grid(row=2, column=0, sticky="w", padx=50, pady=(10, 5))
  551.  
  552. self.progress_bar = ctk.CTkProgressBar(frame, height=15)
  553. self.progress_bar.set(0)
  554. self.progress_bar.grid(row=3, column=0, padx=50, pady=(0, 20), sticky="ew")
  555.  
  556. return frame
  557.  
  558. def create_logs_frame(self):
  559. frame = ctk.CTkFrame(self)
  560. frame.grid_rowconfigure(1, weight=1)
  561. frame.grid_columnconfigure(0, weight=1)
  562.  
  563. title = ctk.CTkLabel(
  564. frame, text="📝 Log do Processo",
  565. font=ctk.CTkFont(size=18, weight="bold"), text_color="#42A5F5"
  566. )
  567. title.grid(row=0, column=0, sticky="w", padx=20, pady=(20, 10))
  568.  
  569. self.log_textbox = ctk.CTkTextbox(
  570. frame, state="disabled", wrap="word",
  571. font=("Courier New", 12), text_color="#E0E0E0", fg_color="#101010"
  572. )
  573. self.log_textbox.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
  574.  
  575. return frame
  576.  
  577.  
  578. def create_settings_frame(self):
  579. settings_frame = ctk.CTkScrollableFrame(self, label_text="🎛️ Painel de Configurações")
  580. settings_frame.grid_columnconfigure(0, weight=1)
  581. self.settings_widgets = {}
  582.  
  583. def create_simple_setting_row(parent_frame, row, text, key, key_path=None, widget_type='entry', options=None):
  584. label = ctk.CTkLabel(parent_frame, text=text, anchor="w")
  585. label.grid(row=row, column=0, padx=20, pady=12, sticky="w")
  586. widget = None
  587. if widget_type == 'entry':
  588. widget = ctk.CTkEntry(parent_frame, width=350)
  589. elif widget_type == 'password':
  590. widget = ctk.CTkEntry(parent_frame, width=350, show="*")
  591. elif widget_type == 'combo':
  592. widget = ctk.CTkComboBox(parent_frame, width=350, values=options if options else [])
  593. elif widget_type == 'switch':
  594. widget = ctk.CTkSwitch(parent_frame, text="")
  595.  
  596. if widget:
  597. widget.grid(row=row, column=1, padx=20, pady=12, sticky="ew")
  598. self.settings_widgets[key] = (widget, key_path)
  599.  
  600. def create_section_title(frame, text):
  601. title = ctk.CTkLabel(frame, text=text, font=ctk.CTkFont(size=18, weight="bold"), text_color="#1E90FF")
  602. title.grid(row=0, column=0, columnspan=2, padx=20, pady=(10, 5), sticky="w")
  603. underline = ctk.CTkFrame(frame, height=1, fg_color="#444")
  604. underline.grid(row=1, column=0, columnspan=2, sticky="ew", padx=20)
  605.  
  606. # --- Grupo Geral ---
  607. general_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
  608. general_group.pack(fill="x", pady=(10, 15), ipady=10, padx=10)
  609. general_group.grid_columnconfigure(1, weight=1)
  610. create_section_title(general_group, "GERAL")
  611. create_simple_setting_row(general_group, 2, "Email:", "email")
  612. create_simple_setting_row(general_group, 3, "Senha:", "senha", widget_type='password')
  613. create_simple_setting_row(general_group, 4, "Tipo de Download:", "downloadType", widget_type='combo', options=['pdf', 'video'])
  614. create_simple_setting_row(general_group, 5, "Navegador Invisível (headless):", "headless", widget_type='switch')
  615.  
  616. # --- Grupo PDF ---
  617. pdf_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
  618. pdf_group.pack(fill="x", pady=15, ipady=10, padx=10)
  619. pdf_group.grid_columnconfigure(1, weight=1)
  620. create_section_title(pdf_group, "CONFIGURAÇÕES DE PDF")
  621.  
  622. pdf_folder_label = ctk.CTkLabel(pdf_group, text="Pasta de PDFs:", anchor="w")
  623. pdf_folder_label.grid(row=2, column=0, padx=20, pady=12, sticky="w")
  624. pdf_input_frame = ctk.CTkFrame(pdf_group, fg_color="transparent")
  625. pdf_input_frame.grid(row=2, column=1, padx=20, pady=12, sticky="ew")
  626. pdf_input_frame.grid_columnconfigure(0, weight=1)
  627. pdf_folder_entry = ctk.CTkEntry(pdf_input_frame)
  628. pdf_folder_entry.grid(row=0, column=0, sticky="ew")
  629. pdf_folder_button = ctk.CTkButton(
  630. pdf_input_frame, text="🔍 Procurar...", width=100,
  631. fg_color="#00C853", hover_color="#00E676", text_color="black",
  632. command=lambda w=pdf_folder_entry: self.browse_folder(w)
  633. )
  634. pdf_folder_button.grid(row=0, column=1, padx=(10, 0))
  635. # CORREÇÃO APLICADA AQUI: A chave agora é única.
  636. self.settings_widgets["pastaDownloads_pdf"] = (pdf_folder_entry, ("pdfConfig",))
  637.  
  638. create_simple_setting_row(pdf_group, 3, "Tipo de PDF:", "pdfType", ("pdfConfig",), 'combo',
  639. options=['1: Simplificado', '2: Original', '3: Marcado', '4: Todos'])
  640.  
  641. # --- Grupo Vídeo ---
  642. video_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
  643. video_group.pack(fill="x", pady=15, ipady=10, padx=10)
  644. video_group.grid_columnconfigure(1, weight=1)
  645. create_section_title(video_group, "CONFIGURAÇÕES DE VÍDEO")
  646.  
  647. video_folder_label = ctk.CTkLabel(video_group, text="Pasta de Vídeos:", anchor="w")
  648. video_folder_label.grid(row=2, column=0, padx=20, pady=12, sticky="w")
  649. video_input_frame = ctk.CTkFrame(video_group, fg_color="transparent")
  650. video_input_frame.grid(row=2, column=1, padx=20, pady=12, sticky="ew")
  651. video_input_frame.grid_columnconfigure(0, weight=1)
  652. video_folder_entry = ctk.CTkEntry(video_input_frame)
  653. video_folder_entry.grid(row=0, column=0, sticky="ew")
  654. video_folder_button = ctk.CTkButton(
  655. video_input_frame, text="🔍 Procurar...", width=100,
  656. fg_color="#00C853", hover_color="#00E676", text_color="black",
  657. command=lambda w=video_folder_entry: self.browse_folder(w)
  658. )
  659. video_folder_button.grid(row=0, column=1, padx=(10, 0))
  660. # CORREÇÃO APLICADA AQUI: A chave agora é única.
  661. self.settings_widgets["pastaDownloads_video"] = (video_folder_entry, ("videoConfig",))
  662.  
  663. create_simple_setting_row(video_group, 3, "Resolução de Vídeo:", "resolucaoEscolhida", ("videoConfig",),
  664. 'combo', options=['720p', '480p', '360p'])
  665.  
  666. # --- Botão Final ---
  667. save_button = ctk.CTkButton(
  668. settings_frame,
  669. text="💾 Salvar Configurações",
  670. width=300, height=40,
  671. font=ctk.CTkFont(size=16, weight="bold"),
  672. fg_color="#1976D2", hover_color="#2196F3", text_color="white",
  673. command=self.save_settings_from_ui
  674. )
  675. save_button.pack(pady=30)
  676.  
  677. return settings_frame
  678.  
  679. def create_urls_frame(self):
  680. frame = ctk.CTkFrame(self)
  681. frame.grid_columnconfigure(0, weight=1)
  682. frame.grid_rowconfigure(1, weight=1)
  683.  
  684. add_frame = ctk.CTkFrame(frame, fg_color="transparent")
  685. add_frame.grid(row=0, column=0, padx=10, pady=(20, 10), sticky="ew")
  686. add_frame.grid_columnconfigure(0, weight=1)
  687.  
  688. self.new_url_entry = ctk.CTkEntry(
  689. add_frame, placeholder_text="📎 Cole aqui a URL do curso (ex: https://...)"
  690. )
  691. self.new_url_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="ew")
  692.  
  693. add_button = ctk.CTkButton(
  694. add_frame, text="➕ Adicionar Curso", width=180,
  695. fg_color="#3949AB", hover_color="#5C6BC0", text_color="white",
  696. command=self.add_url
  697. )
  698. add_button.grid(row=0, column=1, padx=(5, 10), pady=10)
  699.  
  700. self.scrollable_urls_frame = ctk.CTkScrollableFrame(
  701. frame, label_text="📚 Cursos na Fila de Download"
  702. )
  703. self.scrollable_urls_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
  704. self.scrollable_urls_frame.grid_columnconfigure(0, weight=1)
  705.  
  706. return frame
  707.  
  708.  
  709. def browse_folder(self, entry_widget):
  710. folder_path = filedialog.askdirectory()
  711. if folder_path:
  712. entry_widget.delete(0, "end"); entry_widget.insert(0, folder_path)
  713.  
  714. def load_settings_into_ui(self):
  715. for key, (widget, key_path) in self.settings_widgets.items():
  716. value_source = CONFIG
  717. if key_path:
  718. for p in key_path:
  719. value_source = value_source.get(p, {})
  720.  
  721. # ▼▼▼ CORREÇÃO DEFINITIVA APLICADA AQUI ▼▼▼
  722. # Determina a chave correta para usar na busca dentro do config.json
  723. lookup_key = key
  724. if key == "pastaDownloads_pdf" or key == "pastaDownloads_video":
  725. lookup_key = "pastaDownloads"
  726.  
  727. value = value_source.get(lookup_key)
  728. # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
  729.  
  730. if isinstance(widget, ctk.CTkEntry):
  731. widget.delete(0, "end")
  732. widget.insert(0, str(value if value is not None else ""))
  733. elif isinstance(widget, ctk.CTkComboBox):
  734. if key == "pdfType":
  735. mapa_pdf = {'1': '1: Simplificado', '2': '2: Original', '3': '3: Marcado', '4': '4: Todos'}
  736. widget.set(mapa_pdf.get(str(value), '2: Original'))
  737. else:
  738. widget.set(str(value if value is not None else ""))
  739. elif isinstance(widget, ctk.CTkSwitch):
  740. if value:
  741. widget.select()
  742. else:
  743. widget.deselect()
  744.  
  745. def save_settings_from_ui(self):
  746. for key, (widget, key_path) in self.settings_widgets.items():
  747. if isinstance(widget, ctk.CTkSwitch): value = widget.get() == 1
  748. elif isinstance(widget, ctk.CTkComboBox) and key == "pdfType":
  749. mapa_inverso = {'1: Simplificado': 1, '2: Original': 2, '3: Marcado': 3, '4: Todos': 4}
  750. value = mapa_inverso.get(widget.get(), 2)
  751. else: value = widget.get()
  752. if isinstance(value, str) and value.isdigit() and key != 'resolucaoEscolhida': value = int(value)
  753.  
  754. target_dict = CONFIG
  755. if key_path:
  756. for p in key_path:
  757. if p not in target_dict: target_dict[p] = {}
  758. target_dict = target_dict[p]
  759.  
  760. # CORREÇÃO APLICADA AQUI: Determina a chave correta para o JSON.
  761. final_key = key
  762. if key == "pastaDownloads_pdf" or key == "pastaDownloads_video":
  763. final_key = "pastaDownloads"
  764.  
  765. target_dict[final_key] = value
  766.  
  767. with open('config.json', 'w', encoding='utf-8') as f:
  768. json.dump(CONFIG, f, indent=4, ensure_ascii=False)
  769. self.settings_button.configure(text="⚙️ Configurações (Salvo!)")
  770. self.after(2000, lambda: self.settings_button.configure(text="⚙️ Configurações"))
  771.  
  772. def load_urls_into_ui(self):
  773. for widget in self.scrollable_urls_frame.winfo_children():
  774. widget.destroy()
  775.  
  776. try:
  777. with open('course-urls.json', 'r', encoding='utf-8') as f:
  778. current_urls = json.load(f)
  779. except (FileNotFoundError, json.JSONDecodeError):
  780. current_urls = []
  781.  
  782. for url in current_urls:
  783. url_frame = ctk.CTkFrame(self.scrollable_urls_frame, corner_radius=8, fg_color="#202020")
  784. url_frame.pack(fill="x", padx=5, pady=5)
  785.  
  786. label = ctk.CTkLabel(url_frame, text=url, wraplength=700, justify="left", font=ctk.CTkFont(size=13))
  787. label.pack(side="left", padx=10, pady=8, expand=True, fill="x")
  788.  
  789. remove_button = ctk.CTkButton(
  790. url_frame, text="❌", width=50, fg_color="#C62828", hover_color="#E53935",
  791. text_color="white", command=lambda u=url: self.remove_url(u)
  792. )
  793. remove_button.pack(side="right", padx=10, pady=8)
  794.  
  795.  
  796. def add_url(self):
  797. new_url = self.new_url_entry.get().strip()
  798. if new_url:
  799. try:
  800. with open('course-urls.json', 'r', encoding='utf-8') as f: current_urls = json.load(f)
  801. except (FileNotFoundError, json.JSONDecodeError):
  802. current_urls = []
  803. if new_url not in current_urls:
  804. current_urls.append(new_url); self.new_url_entry.delete(0, "end")
  805. with open('course-urls.json', 'w', encoding='utf-8') as f: json.dump(current_urls, f, indent=2, ensure_ascii=False)
  806. self.load_urls_into_ui()
  807.  
  808. def remove_url(self, url_to_remove):
  809. with open('course-urls.json', 'r', encoding='utf-8') as f: current_urls = json.load(f)
  810. current_urls.remove(url_to_remove)
  811. with open('course-urls.json', 'w', encoding='utf-8') as f: json.dump(current_urls, f, indent=2, ensure_ascii=False)
  812. self.load_urls_into_ui()
  813.  
  814. def start_download(self):
  815. if self.download_thread and self.download_thread.is_alive():
  816. self.log_to_gui("Um processo de download já está em andamento.")
  817. return
  818. self.progress_bar.set(0)
  819. self.start_button.configure(state="disabled", text="Baixando...")
  820. self.log_textbox.configure(state="normal"); self.log_textbox.delete("1.0", "end"); self.log_textbox.configure(state="disabled")
  821. self.show_logs_frame()
  822. self.download_thread = threading.Thread(target=lambda: asyncio.run(download_logic_main(self.update_progress, self.log_queue)), daemon=True)
  823. self.download_thread.start()
  824.  
  825. def process_log_queue(self):
  826. try:
  827. while True:
  828. message = self.log_queue.get_nowait()
  829. self.log_to_gui(message)
  830. except queue.Empty:
  831. pass
  832. finally:
  833. self.after(100, self.process_log_queue)
  834.  
  835. def log_to_gui(self, message):
  836. self.log_textbox.configure(state="normal")
  837. self.log_textbox.insert("end", message + "\n")
  838. self.log_textbox.see("end")
  839. self.log_textbox.configure(state="disabled")
  840. if "PROCESSO FINALIZADO" in message:
  841. self.start_button.configure(state="normal", text="INICIAR DOWNLOADS")
  842.  
  843. def update_progress(self, value):
  844. self.progress_bar.set(value)
  845.  
  846. if __name__ == "__main__":
  847. app = App()
  848. app.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement