Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import customtkinter as ctk
- from customtkinter import filedialog
- import json
- import asyncio
- import aiohttp
- from pathlib import Path
- from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError, Error
- import logging
- import re
- import sys
- import threading
- import queue
- # --- CONFIGURAÇÃO INICIAL E VARIÁVEIS GLOBAIS ---
- try:
- with open('config.json', 'r', encoding='utf-8') as f:
- CONFIG = json.load(f)
- except Exception as e:
- print(f"Erro ao carregar config.json: {e}")
- CONFIG = {}
- try:
- with open('course-urls.json', 'r', encoding='utf-8') as f:
- COURSE_URLS = json.load(f)
- except Exception:
- COURSE_URLS = []
- # --- REDIRECIONAMENTO DO LOG PARA A INTERFACE GRÁFICA ---
- class QueueHandler(logging.Handler):
- def __init__(self, log_queue):
- super().__init__()
- self.log_queue = log_queue
- def emit(self, record):
- self.log_queue.put(self.format(record))
- class PrintRedirector:
- def __init__(self, logger_instance):
- self.logger = logger_instance
- def write(self, message):
- if message.strip(): self.logger.info(message.strip())
- def flush(self):
- pass
- # --- INÍCIO DA SEÇÃO DE LÓGICA COMPLETA DO DOWNLOADER ---
- PROGRESS_FILE = Path('progress.json')
- RESOLUCOES_DISPONIVEIS = ['720p', '480p', '360p']
- PDF_TYPES = {
- 1: {'name': 'versão simplificada', 'urlPart': 'pdfSimplificado/download'},
- 2: {'name': 'versão original', 'urlPart': 'pdf/download'},
- 3: {'name': 'marcação dos aprovados', 'urlPart': 'pdfGrifado/download'}
- }
- def sanitize_filename(filename, max_length=200):
- sanitized = ''.join('_' if c in '<>:"/\\|?*' else c for c in filename).strip()
- sanitized = re.sub(r'\s+', ' ', sanitized)
- if len(sanitized) > max_length:
- sanitized = sanitized[:max_length].rsplit(' ', 1)[0].rstrip('.')
- return sanitized
- async def download_file(url, file_path, logger, retries=3):
- for attempt in range(1, retries + 1):
- try:
- async with aiohttp.ClientSession() as session:
- async with session.get(url, timeout=300) as response:
- if response.status != 200:
- raise Exception(f'Status de resposta inválido: {response.status}')
- with open(file_path, 'wb') as f:
- async for chunk in response.content.iter_chunked(8192):
- f.write(chunk)
- logger.info(f'Arquivo baixado: {file_path}')
- return
- except Exception as e:
- logger.error(f'Tentativa {attempt}/{retries} falhou ao baixar {url}: {e}')
- if attempt == retries:
- raise Exception(f'Falha ao baixar {url} após {retries} tentativas.')
- await asyncio.sleep(2 ** attempt)
- async def verify_download(file_path, logger):
- size = file_path.stat().st_size
- if size == 0:
- raise ValueError(f'Arquivo {file_path} está vazio. Download pode ter falhado.')
- logger.info(f'Arquivo {file_path} verificado: {size} bytes')
- return True
- async def load_progress():
- try:
- with open(PROGRESS_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- except (FileNotFoundError, json.JSONDecodeError):
- return {}
- async def save_progress(progress):
- with open(PROGRESS_FILE, 'w', encoding='utf-8') as f:
- json.dump(progress, f, indent=2)
- def extract_materia_name(course_name):
- if 'Conhecimentos Regionais' in course_name: return 'Conhecimentos Regionais'
- materia = course_name
- # Esta linha é a mais importante e garante a remoção do prefixo
- materia = re.sub(r'^Concursos da Área Fiscal\s*-\s*', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'Curso (Completo|Básico) de ', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'\(Profs?\.?[\w\s,]+\)', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'\s*-\s*\d{4}(?!\d)', '', materia)
- materia = re.sub(r'\s*\(Pós-Edital\)', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'Prefeitura [\w\s-]+?-', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'\([^)]+\)', '', materia)
- materia = re.sub(r' - Área Administrativa', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'Noções de ', '', materia, flags=re.IGNORECASE)
- materia = re.sub(r'^\s*-\s*|\s*-\s*$', '', materia)
- materia = materia.strip()
- if ':' in materia: materia = materia.split(':')[1].strip()
- return materia or 'Matéria Desconhecida'
- async def ensure_logged_in(page, logger):
- LOGIN_URL = 'https://www.estrategiaconcursos.com.br/app/auth/login'
- ASSINATURAS_URL = 'https://www.estrategiaconcursos.com.br/app/dashboard/assinaturas'
- logger.info("Navegando para a página de login para verificação de estado...")
- await page.goto(LOGIN_URL, wait_until='domcontentloaded', timeout=60000)
- try:
- logger.info("Verificando se o login já está ativo...")
- await page.wait_for_selector('a:has-text("Catálogo de Cursos")', timeout=10000)
- logger.info("Login já está ativo.")
- except PlaywrightTimeoutError:
- logger.info("Login não está ativo. Procedendo com o preenchimento de credenciais...")
- try:
- await page.wait_for_selector('input[name="loginField"]', state='visible', timeout=60000)
- await page.fill('input[name="loginField"]', CONFIG['email'])
- await page.wait_for_selector('input[name="passwordField"]', state='visible', timeout=60000)
- await page.fill('input[name="passwordField"]', CONFIG['senha'])
- await page.click('button[type="submit"]')
- await page.wait_for_selector('a:has-text("Catálogo de Cursos")', timeout=60000)
- logger.info("Login realizado com sucesso!")
- except PlaywrightTimeoutError as e:
- logger.error(f"Tempo de espera excedido durante a tentativa de login.")
- raise Exception("Falha na etapa de login.")
- # ▼▼▼ CORREÇÃO APLICADA AQUI: LÓGICA DE PÓS-LOGIN ROBUSTA ▼▼▼
- try:
- logger.info("Verificando redirecionamento pós-login...")
- current_url = page.url
- # Cenário 1: Redirecionado para a página de perfil (NOVA LÓGICA)
- if "perfil.estrategia.com" in current_url:
- logger.warning(f"Redirecionado para a página de perfil. Navegando para o catálogo: {ASSINATURAS_URL}")
- await page.goto(ASSINATURAS_URL, wait_until='domcontentloaded', timeout=60000)
- current_url = page.url # Atualiza a URL após a navegação para a próxima verificação
- # Cenário 2: Redirecionado para o dashboard genérico (LÓGICA EXISTENTE - NÃO ALTERADA)
- if "/app/dashboard/cursos" == current_url.replace('https://www.estrategiaconcursos.com.br', ''):
- logger.warning("Página do dashboard genérico detectada. Clicando em 'Catálogo de Cursos' para prosseguir...")
- await page.locator('a:has-text("Catálogo de Cursos")').click()
- await page.wait_for_url("**/app/dashboard/assinaturas", timeout=30000)
- logger.info("Navegação para o catálogo de cursos concluída.")
- except Exception as e:
- logger.error(f"Ocorreu um erro inesperado durante a verificação de redirecionamento pós-login: {e}")
- raise
- async def ensure_lesson_expanded(page, aula_id, logger):
- logger.info(f'Garantindo que a aula #{aula_id} esteja expandida...')
- try:
- # Tenta expandir a aula clicando no cabeçalho
- await page.evaluate(f'''(id) => {{
- const header = document.querySelector(`#${{id}} .Collapse-header`);
- if (header && header.parentElement.nextElementSibling.style.display === "none") {{
- header.click();
- }}
- }}''', aula_id)
- # Aguarda a lista de vídeos ficar visível (seletor genérico para cobrir variações)
- await page.wait_for_selector(f'#{aula_id} [class*="ListVideos"], #{aula_id} [class*="VideoItem"]', state='visible', timeout=15000)
- logger.info(f'Aula #{aula_id} expandida com sucesso.')
- except PlaywrightTimeoutError:
- logger.warning(f'Timeout ao tentar expandir a aula #{aula_id}. Tentando continuar...')
- except Exception as e:
- logger.error(f'Erro ao expandir a aula #{aula_id}: {e}')
- # Aguarda mais tempo para carregamento assíncrono
- await asyncio.sleep(3)
- async def process_course_pdf(page, course_url, progress, logger):
- BASE_DIR = Path(CONFIG['pdfConfig']['pastaDownloads'])
- logger.info(f"Navegando diretamente para a página do curso: {course_url}")
- await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
- if "app/dashboard/cursos" in page.url and not re.search(r'/cursos/\d+/aulas', page.url):
- logger.warning(f"Redirecionado para o dashboard. Tentando navegar para {course_url} novamente.")
- await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
- logger.info('Extraindo o nome do curso...')
- try:
- course_name_element = await page.wait_for_selector('.CourseInfo-content-title', timeout=30000)
- course_name = await course_name_element.text_content()
- logger.info(f'Nome do curso extraído: {course_name}')
- except PlaywrightTimeoutError:
- course_name = "Curso Desconhecido"
- logger.warning('Timeout ao extrair o nome do curso. Usando título da página.')
- course_name = await page.title()
- materia_name = extract_materia_name(course_name)
- course_dir = BASE_DIR / sanitize_filename(materia_name)
- course_dir.mkdir(parents=True, exist_ok=True)
- logger.info(f'Pasta da matéria: {course_dir}')
- pdf_type_config = CONFIG['pdfConfig'].get('pdfType', 2)
- pdf_types_to_download = [1, 2, 3] if pdf_type_config == 4 else [pdf_type_config]
- aulas = await page.locator('.LessonList-item').all()
- logger.info(f'Total de aulas encontradas: {len(aulas)}')
- for i, aula_element in enumerate(aulas, 1):
- aula_id = await aula_element.get_attribute('id') or f'aula{i}'
- title_element = aula_element.locator(".LessonCollapseHeader-title .SectionTitle")
- subtitle_element = aula_element.locator(".LessonCollapseHeader-title .sc-gZMcBi")
- lesson_name_raw = await title_element.text_content() if await title_element.count() > 0 else f"Aula {i:02d}"
- lesson_subtitle_raw = await subtitle_element.text_content() if await subtitle_element.count() > 0 else "Sem Subtítulo"
- lesson_name = sanitize_filename(lesson_name_raw)
- lesson_subtitle = sanitize_filename(lesson_subtitle_raw)
- logger.info(f'Processando aula "{lesson_name}"...')
- await ensure_lesson_expanded(page, aula_id, logger)
- found_download_for_this_aula = False
- # ▼▼▼ CORREÇÃO APLICADA AQUI ▼▼▼
- # Alteramos o seletor para uma versão mais robusta, baseada no texto visível do botão.
- all_buttons = await aula_element.locator('a:has-text("Baixar Livro Eletrônico")').all()
- if all_buttons:
- found_download_for_this_aula = True
- for button in all_buttons:
- pdf_url = await button.get_attribute('href')
- if not pdf_url: continue
- pdf_url = "https://www.estrategiaconcursos.com.br" + pdf_url if pdf_url.startswith('/api') else pdf_url
- for pdf_type in pdf_types_to_download:
- pdf_info = PDF_TYPES.get(pdf_type)
- if not pdf_info: continue
- if pdf_info["urlPart"] in pdf_url:
- base_file_name = f'{lesson_name} - {lesson_subtitle}'
- file_name = f'{sanitize_filename(base_file_name, 180)} ({pdf_info["name"]}).pdf'
- file_path = course_dir / file_name
- progress_key = f'{aula_id}-{pdf_url}'
- if progress.get(progress_key):
- logger.info(f'JÁ BAIXADO: "{file_name}". Pulando...')
- continue
- try:
- logger.info(f'BAIXANDO: "{file_name}"')
- await download_file(pdf_url, file_path, logger)
- await verify_download(file_path, logger)
- progress[progress_key] = True
- await save_progress(progress)
- logger.info(f'SUCESSO: Download de "{file_name}" concluído.')
- except Exception as e:
- logger.error(f'FALHA ao baixar "{file_name}": {e}')
- break
- # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
- if not found_download_for_this_aula:
- logger.warning(f'AVISO: Nenhum link de download encontrado para a aula "{lesson_name}". Pode ser uma aula sem PDF ou de outro tipo.')
- async def get_video_url_by_resolution(page, preferred_resolution, logger):
- try:
- # Espera o player de vídeo estar visível e carregado
- await page.wait_for_selector('video.video-react-video', state='visible', timeout=30000)
- # Toca e pausa o vídeo para garantir que o player esteja ativo
- await page.evaluate('''() => {
- const v = document.querySelector("video.video-react-video");
- if(v){ v.play(); setTimeout(()=>v.pause(), 1000); }
- }''')
- video_player_locator = page.locator('video.video-react-video')
- current_url = await video_player_locator.get_attribute('src')
- # Se a resolução preferida for 360p e a URL atual já for 360p, retorna imediatamente
- if preferred_resolution == '360p' and '360p' in current_url:
- logger.info("Resolução 360p já está selecionada. Usando URL atual.")
- return {'url': current_url, 'resolution': '360p'}
- # Abre o menu de opções do player
- await page.locator('.PlayerControl-button').click()
- await page.wait_for_selector('.PlayerControl-options', state='visible', timeout=15000)
- resolution_buttons = await page.locator('.PlayerControlOptions-button').all()
- available_resolutions = {await btn.text_content(): btn for btn in resolution_buttons if await btn.text_content() in RESOLUCOES_DISPONIVEIS}
- for res in [preferred_resolution] + RESOLUCOES_DISPONIVEIS:
- if res in available_resolutions:
- logger.info(f'Selecionando resolução {res}...')
- old_url = await video_player_locator.get_attribute('src')
- # Usa JavaScript para clicar no botão, evitando problemas com interceptação
- await page.evaluate('''(res) => {
- const button = Array.from(document.querySelectorAll('.PlayerControlOptions-button')).find(btn => btn.textContent === res);
- if (button) button.click();
- }''', res)
- # Espera a URL do vídeo mudar
- try:
- logger.info(f"Aguardando a URL do vídeo ser atualizada para {res}...")
- await page.wait_for_function(
- f'() => document.querySelector("video.video-react-video").src !== "{old_url}"',
- timeout=20000 # Aumentado para 20s
- )
- logger.info("URL do vídeo atualizada com sucesso.")
- except PlaywrightTimeoutError:
- logger.warning(f"A URL do vídeo não mudou após selecionar {res}. Usando a URL atual.")
- new_url = await video_player_locator.get_attribute('src')
- return {'url': new_url, 'resolution': res}
- # Fallback para a URL atual
- logger.warning("Nenhuma resolução pôde ser selecionada. Usando a URL original.")
- return {'url': current_url, 'resolution': 'padrão'}
- except Exception as e:
- logger.error(f"Erro ao obter URL do vídeo na resolução {preferred_resolution}: {e}")
- return {'url': current_url, 'resolution': 'padrão'}
- async def process_course_video(page, course_url, progress, logger):
- BASE_DIR = Path(CONFIG['videoConfig']['pastaDownloads'])
- logger.info(f"Navegando diretamente para a página do curso: {course_url}")
- await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
- if "app/dashboard/cursos" in page.url and not re.search(r'/cursos/\d+/aulas', page.url):
- logger.warning(f"Redirecionado para o dashboard. Tentando navegar para {course_url} novamente.")
- await page.goto(course_url, wait_until='domcontentloaded', timeout=60000)
- logger.info('Extraindo o nome do curso...')
- try:
- course_name_element = await page.wait_for_selector('.CourseInfo-content-title', timeout=30000)
- course_name = await course_name_element.text_content()
- except PlaywrightTimeoutError:
- course_name = await page.title()
- materia_name = extract_materia_name(course_name)
- course_dir = BASE_DIR / sanitize_filename(materia_name)
- course_dir.mkdir(parents=True, exist_ok=True)
- logger.info(f'Pasta da matéria: {course_dir}')
- aulas = await page.locator('.LessonList-item').all()
- logger.info(f'Total de aulas encontradas: {len(aulas)}')
- for aula_element in aulas:
- aula_id = await aula_element.get_attribute('id')
- lesson_name_raw = await aula_element.locator(".LessonCollapseHeader-title .SectionTitle").text_content()
- lesson_name = sanitize_filename(lesson_name_raw)
- lesson_dir = course_dir / lesson_name
- lesson_dir.mkdir(parents=True, exist_ok=True)
- logger.info(f'Processando aula "{lesson_name}"...')
- await ensure_lesson_expanded(page, aula_id, logger)
- # ▼▼▼ CORREÇÃO APLICADA AQUI ▼▼▼
- # Usamos um seletor mais direto e robusto para a lista de vídeos (playlist)
- # e removemos a lógica de fallback que estava causando o comportamento incorreto.
- videos = await aula_element.locator('.ListVideos-items-video a.VideoItem').all()
- if not videos:
- logger.warning(f'AVISO: Nenhum vídeo encontrado na aula "{lesson_name}". Pulando...')
- continue
- logger.info(f'Encontrados {len(videos)} vídeos na aula "{lesson_name}".')
- # Lógica para processar a lista de vídeos encontrada
- for j, video_element in enumerate(videos, 1):
- video_title_raw = await video_element.locator(".VideoItem-info-title").text_content()
- video_title = sanitize_filename(video_title_raw)
- progress_key = f'{aula_id}-{video_title}-{j}'
- if progress.get(progress_key):
- logger.info(f'JÁ BAIXADO: "{video_title}". Pulando...')
- continue
- try:
- # Clica no item da playlist para carregar o vídeo correto no player
- await video_element.click()
- # A função get_video_url_by_resolution agora atuará no vídeo que acabamos de carregar
- video_info = await get_video_url_by_resolution(page, CONFIG['videoConfig']['resolucaoEscolhida'], logger)
- video_url, used_resolution = video_info['url'], video_info['resolution']
- file_name = f'{lesson_name} - Vídeo {j} {video_title} [{used_resolution}].mp4'
- file_path = lesson_dir / sanitize_filename(file_name)
- logger.info(f'BAIXANDO: "{file_name}"')
- await download_file(video_url, file_path, logger)
- await verify_download(file_path, logger)
- progress[progress_key] = True
- await save_progress(progress)
- logger.info(f'SUCESSO: Download de "{file_name}" concluído.')
- # Não é mais necessário voltar a página, pois estamos na mesma página, apenas
- # trocando o vídeo no player principal. Uma pequena espera é suficiente.
- await asyncio.sleep(1)
- except Exception as e:
- logger.error(f'FALHA ao baixar "{video_title}": {e}')
- # Se der erro, tenta recarregar a página do curso para se recuperar
- await page.goto(course_url, wait_until='domcontentloaded')
- # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
- async def download_logic_main(progress_callback, log_queue):
- logger = logging.getLogger(__name__)
- logger.setLevel(logging.INFO)
- if not logger.handlers:
- queue_handler = QueueHandler(log_queue)
- formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', datefmt='%H:%M:%S')
- queue_handler.setFormatter(formatter)
- logger.addHandler(queue_handler)
- sys.stdout = PrintRedirector(logger)
- logger.info("Iniciando processo de download...")
- with open('course-urls.json', 'r', encoding='utf-8') as f:
- current_course_urls = json.load(f)
- total_cursos = len(current_course_urls)
- if total_cursos == 0:
- logger.warning("Nenhum curso na lista para baixar.")
- return
- async with async_playwright() as p:
- context = None
- try:
- chrome_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
- if not Path(chrome_path).exists():
- chrome_path_x86 = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
- if Path(chrome_path_x86).exists():
- chrome_path = chrome_path_x86
- else:
- raise FileNotFoundError("Google Chrome não foi encontrado nos caminhos padrão.")
- logger.info(f"Usando Google Chrome de: {chrome_path}")
- context = await p.chromium.launch_persistent_context(
- user_data_dir=Path.home() / "AppData" / "Local" / "EstrategiaDownloaderCache",
- headless=CONFIG.get('headless', False),
- executable_path=chrome_path,
- args=['--no-sandbox', '--disable-setuid-sandbox']
- )
- page = await context.new_page()
- except (Error, FileNotFoundError) as e:
- logger.error(f"Falha ao iniciar o Google Chrome: {e}")
- logger.error("Verifique se o Chrome está instalado ou ajuste o caminho no app.py.")
- return
- try:
- await ensure_logged_in(page, logger)
- progress_data = await load_progress()
- for i, course_url in enumerate(current_course_urls):
- logger.info(f"--- Processando curso {i+1}/{total_cursos} ---")
- download_type = CONFIG.get('downloadType', 'pdf')
- if download_type == 'pdf':
- await process_course_pdf(page, course_url, progress_data, logger)
- elif download_type == 'video':
- await process_course_video(page, course_url, progress_data, logger)
- progress_callback((i + 1) / total_cursos)
- except Exception as e:
- logger.error(f"Ocorreu um erro crítico durante o processo: {e}", exc_info=True)
- finally:
- if context:
- logger.info("Fechando navegador...")
- await context.close()
- logger.info("--- PROCESSO FINALIZADO ---")
- # --- APLICAÇÃO GRÁFICA COM CUSTOMTKINTER ---
- class App(ctk.CTk):
- def __init__(self):
- super().__init__()
- self.title("🦉 Estratégia Downloader Pro")
- self.geometry("1100x700")
- self.minsize(800, 600) # Define um tamanho mínimo para a janela
- ctk.set_appearance_mode("dark")
- ctk.set_default_color_theme("blue")
- self.log_queue = queue.Queue()
- self.download_thread = None
- # --- Configuração do Grid Principal para Responsividade ---
- self.grid_columnconfigure(1, weight=1) # Permite que a coluna de conteúdo (1) se expanda
- self.grid_rowconfigure(0, weight=1) # Permite que a linha de conteúdo (0) se expanda
- # --- Frame de Navegação (Sidebar) ---
- self.nav_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
- self.nav_frame.grid(row=0, column=0, sticky="nsew")
- self.nav_frame.grid_rowconfigure(5, weight=1) # Empurra o tema para o final
- self.logo_label = ctk.CTkLabel(self.nav_frame, text="Downloader Pro", font=ctk.CTkFont(size=20, weight="bold"))
- self.logo_label.grid(row=0, column=0, padx=20, pady=20)
- self.home_button = ctk.CTkButton(self.nav_frame, text="▶️ Início", command=self.show_home_frame)
- self.home_button.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
- self.settings_button = ctk.CTkButton(self.nav_frame, text="⚙️ Configurações", command=self.show_settings_frame)
- self.settings_button.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
- self.urls_button = ctk.CTkButton(self.nav_frame, text="📚 Cursos", command=self.show_urls_frame)
- self.urls_button.grid(row=3, column=0, padx=20, pady=10, sticky="ew")
- self.logs_button = ctk.CTkButton(self.nav_frame, text="📄 Logs", command=self.show_logs_frame)
- self.logs_button.grid(row=4, column=0, padx=20, pady=10, sticky="ew")
- # --- Frames de Conteúdo ---
- self.home_frame = self.create_home_frame()
- self.settings_frame = self.create_settings_frame()
- self.urls_frame = self.create_urls_frame()
- self.logs_frame = self.create_logs_frame()
- self.after(100, self.process_log_queue)
- self.show_home_frame()
- def select_frame(self, frame_to_show):
- self.home_frame.grid_forget()
- self.settings_frame.grid_forget()
- self.urls_frame.grid_forget()
- self.logs_frame.grid_forget()
- frame_to_show.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
- def show_home_frame(self): self.select_frame(self.home_frame)
- def show_settings_frame(self): self.load_settings_into_ui(); self.select_frame(self.settings_frame)
- def show_urls_frame(self): self.load_urls_into_ui(); self.select_frame(self.urls_frame)
- def show_logs_frame(self): self.select_frame(self.logs_frame)
- def create_home_frame(self):
- frame = ctk.CTkFrame(self)
- frame.grid_columnconfigure(0, weight=1)
- title = ctk.CTkLabel(
- frame, text="🦉 Downloader Pro – Painel Inicial",
- font=ctk.CTkFont(size=26, weight="bold"),
- text_color="#1E90FF"
- )
- title.grid(row=0, column=0, padx=20, pady=(30, 10))
- self.start_button = ctk.CTkButton(
- frame, text="⏬ INICIAR DOWNLOADS", height=55, width=250,
- font=ctk.CTkFont(size=18, weight="bold"),
- fg_color="#00897B", hover_color="#00BFA5", text_color="white",
- corner_radius=8, command=self.start_download
- )
- self.start_button.grid(row=1, column=0, padx=20, pady=20)
- progress_label = ctk.CTkLabel(
- frame, text="📦 Progresso dos Downloads:",
- font=ctk.CTkFont(size=14), anchor="w"
- )
- progress_label.grid(row=2, column=0, sticky="w", padx=50, pady=(10, 5))
- self.progress_bar = ctk.CTkProgressBar(frame, height=15)
- self.progress_bar.set(0)
- self.progress_bar.grid(row=3, column=0, padx=50, pady=(0, 20), sticky="ew")
- return frame
- def create_logs_frame(self):
- frame = ctk.CTkFrame(self)
- frame.grid_rowconfigure(1, weight=1)
- frame.grid_columnconfigure(0, weight=1)
- title = ctk.CTkLabel(
- frame, text="📝 Log do Processo",
- font=ctk.CTkFont(size=18, weight="bold"), text_color="#42A5F5"
- )
- title.grid(row=0, column=0, sticky="w", padx=20, pady=(20, 10))
- self.log_textbox = ctk.CTkTextbox(
- frame, state="disabled", wrap="word",
- font=("Courier New", 12), text_color="#E0E0E0", fg_color="#101010"
- )
- self.log_textbox.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
- return frame
- def create_settings_frame(self):
- settings_frame = ctk.CTkScrollableFrame(self, label_text="🎛️ Painel de Configurações")
- settings_frame.grid_columnconfigure(0, weight=1)
- self.settings_widgets = {}
- def create_simple_setting_row(parent_frame, row, text, key, key_path=None, widget_type='entry', options=None):
- label = ctk.CTkLabel(parent_frame, text=text, anchor="w")
- label.grid(row=row, column=0, padx=20, pady=12, sticky="w")
- widget = None
- if widget_type == 'entry':
- widget = ctk.CTkEntry(parent_frame, width=350)
- elif widget_type == 'password':
- widget = ctk.CTkEntry(parent_frame, width=350, show="*")
- elif widget_type == 'combo':
- widget = ctk.CTkComboBox(parent_frame, width=350, values=options if options else [])
- elif widget_type == 'switch':
- widget = ctk.CTkSwitch(parent_frame, text="")
- if widget:
- widget.grid(row=row, column=1, padx=20, pady=12, sticky="ew")
- self.settings_widgets[key] = (widget, key_path)
- def create_section_title(frame, text):
- title = ctk.CTkLabel(frame, text=text, font=ctk.CTkFont(size=18, weight="bold"), text_color="#1E90FF")
- title.grid(row=0, column=0, columnspan=2, padx=20, pady=(10, 5), sticky="w")
- underline = ctk.CTkFrame(frame, height=1, fg_color="#444")
- underline.grid(row=1, column=0, columnspan=2, sticky="ew", padx=20)
- # --- Grupo Geral ---
- general_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
- general_group.pack(fill="x", pady=(10, 15), ipady=10, padx=10)
- general_group.grid_columnconfigure(1, weight=1)
- create_section_title(general_group, "GERAL")
- create_simple_setting_row(general_group, 2, "Email:", "email")
- create_simple_setting_row(general_group, 3, "Senha:", "senha", widget_type='password')
- create_simple_setting_row(general_group, 4, "Tipo de Download:", "downloadType", widget_type='combo', options=['pdf', 'video'])
- create_simple_setting_row(general_group, 5, "Navegador Invisível (headless):", "headless", widget_type='switch')
- # --- Grupo PDF ---
- pdf_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
- pdf_group.pack(fill="x", pady=15, ipady=10, padx=10)
- pdf_group.grid_columnconfigure(1, weight=1)
- create_section_title(pdf_group, "CONFIGURAÇÕES DE PDF")
- pdf_folder_label = ctk.CTkLabel(pdf_group, text="Pasta de PDFs:", anchor="w")
- pdf_folder_label.grid(row=2, column=0, padx=20, pady=12, sticky="w")
- pdf_input_frame = ctk.CTkFrame(pdf_group, fg_color="transparent")
- pdf_input_frame.grid(row=2, column=1, padx=20, pady=12, sticky="ew")
- pdf_input_frame.grid_columnconfigure(0, weight=1)
- pdf_folder_entry = ctk.CTkEntry(pdf_input_frame)
- pdf_folder_entry.grid(row=0, column=0, sticky="ew")
- pdf_folder_button = ctk.CTkButton(
- pdf_input_frame, text="🔍 Procurar...", width=100,
- fg_color="#00C853", hover_color="#00E676", text_color="black",
- command=lambda w=pdf_folder_entry: self.browse_folder(w)
- )
- pdf_folder_button.grid(row=0, column=1, padx=(10, 0))
- # CORREÇÃO APLICADA AQUI: A chave agora é única.
- self.settings_widgets["pastaDownloads_pdf"] = (pdf_folder_entry, ("pdfConfig",))
- create_simple_setting_row(pdf_group, 3, "Tipo de PDF:", "pdfType", ("pdfConfig",), 'combo',
- options=['1: Simplificado', '2: Original', '3: Marcado', '4: Todos'])
- # --- Grupo Vídeo ---
- video_group = ctk.CTkFrame(settings_frame, corner_radius=10, border_width=1, border_color="#444", fg_color="#1E1E1E")
- video_group.pack(fill="x", pady=15, ipady=10, padx=10)
- video_group.grid_columnconfigure(1, weight=1)
- create_section_title(video_group, "CONFIGURAÇÕES DE VÍDEO")
- video_folder_label = ctk.CTkLabel(video_group, text="Pasta de Vídeos:", anchor="w")
- video_folder_label.grid(row=2, column=0, padx=20, pady=12, sticky="w")
- video_input_frame = ctk.CTkFrame(video_group, fg_color="transparent")
- video_input_frame.grid(row=2, column=1, padx=20, pady=12, sticky="ew")
- video_input_frame.grid_columnconfigure(0, weight=1)
- video_folder_entry = ctk.CTkEntry(video_input_frame)
- video_folder_entry.grid(row=0, column=0, sticky="ew")
- video_folder_button = ctk.CTkButton(
- video_input_frame, text="🔍 Procurar...", width=100,
- fg_color="#00C853", hover_color="#00E676", text_color="black",
- command=lambda w=video_folder_entry: self.browse_folder(w)
- )
- video_folder_button.grid(row=0, column=1, padx=(10, 0))
- # CORREÇÃO APLICADA AQUI: A chave agora é única.
- self.settings_widgets["pastaDownloads_video"] = (video_folder_entry, ("videoConfig",))
- create_simple_setting_row(video_group, 3, "Resolução de Vídeo:", "resolucaoEscolhida", ("videoConfig",),
- 'combo', options=['720p', '480p', '360p'])
- # --- Botão Final ---
- save_button = ctk.CTkButton(
- settings_frame,
- text="💾 Salvar Configurações",
- width=300, height=40,
- font=ctk.CTkFont(size=16, weight="bold"),
- fg_color="#1976D2", hover_color="#2196F3", text_color="white",
- command=self.save_settings_from_ui
- )
- save_button.pack(pady=30)
- return settings_frame
- def create_urls_frame(self):
- frame = ctk.CTkFrame(self)
- frame.grid_columnconfigure(0, weight=1)
- frame.grid_rowconfigure(1, weight=1)
- add_frame = ctk.CTkFrame(frame, fg_color="transparent")
- add_frame.grid(row=0, column=0, padx=10, pady=(20, 10), sticky="ew")
- add_frame.grid_columnconfigure(0, weight=1)
- self.new_url_entry = ctk.CTkEntry(
- add_frame, placeholder_text="📎 Cole aqui a URL do curso (ex: https://...)"
- )
- self.new_url_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="ew")
- add_button = ctk.CTkButton(
- add_frame, text="➕ Adicionar Curso", width=180,
- fg_color="#3949AB", hover_color="#5C6BC0", text_color="white",
- command=self.add_url
- )
- add_button.grid(row=0, column=1, padx=(5, 10), pady=10)
- self.scrollable_urls_frame = ctk.CTkScrollableFrame(
- frame, label_text="📚 Cursos na Fila de Download"
- )
- self.scrollable_urls_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
- self.scrollable_urls_frame.grid_columnconfigure(0, weight=1)
- return frame
- def browse_folder(self, entry_widget):
- folder_path = filedialog.askdirectory()
- if folder_path:
- entry_widget.delete(0, "end"); entry_widget.insert(0, folder_path)
- def load_settings_into_ui(self):
- for key, (widget, key_path) in self.settings_widgets.items():
- value_source = CONFIG
- if key_path:
- for p in key_path:
- value_source = value_source.get(p, {})
- # ▼▼▼ CORREÇÃO DEFINITIVA APLICADA AQUI ▼▼▼
- # Determina a chave correta para usar na busca dentro do config.json
- lookup_key = key
- if key == "pastaDownloads_pdf" or key == "pastaDownloads_video":
- lookup_key = "pastaDownloads"
- value = value_source.get(lookup_key)
- # ▲▲▲ FIM DA CORREÇÃO ▲▲▲
- if isinstance(widget, ctk.CTkEntry):
- widget.delete(0, "end")
- widget.insert(0, str(value if value is not None else ""))
- elif isinstance(widget, ctk.CTkComboBox):
- if key == "pdfType":
- mapa_pdf = {'1': '1: Simplificado', '2': '2: Original', '3': '3: Marcado', '4': '4: Todos'}
- widget.set(mapa_pdf.get(str(value), '2: Original'))
- else:
- widget.set(str(value if value is not None else ""))
- elif isinstance(widget, ctk.CTkSwitch):
- if value:
- widget.select()
- else:
- widget.deselect()
- def save_settings_from_ui(self):
- for key, (widget, key_path) in self.settings_widgets.items():
- if isinstance(widget, ctk.CTkSwitch): value = widget.get() == 1
- elif isinstance(widget, ctk.CTkComboBox) and key == "pdfType":
- mapa_inverso = {'1: Simplificado': 1, '2: Original': 2, '3: Marcado': 3, '4: Todos': 4}
- value = mapa_inverso.get(widget.get(), 2)
- else: value = widget.get()
- if isinstance(value, str) and value.isdigit() and key != 'resolucaoEscolhida': value = int(value)
- target_dict = CONFIG
- if key_path:
- for p in key_path:
- if p not in target_dict: target_dict[p] = {}
- target_dict = target_dict[p]
- # CORREÇÃO APLICADA AQUI: Determina a chave correta para o JSON.
- final_key = key
- if key == "pastaDownloads_pdf" or key == "pastaDownloads_video":
- final_key = "pastaDownloads"
- target_dict[final_key] = value
- with open('config.json', 'w', encoding='utf-8') as f:
- json.dump(CONFIG, f, indent=4, ensure_ascii=False)
- self.settings_button.configure(text="⚙️ Configurações (Salvo!)")
- self.after(2000, lambda: self.settings_button.configure(text="⚙️ Configurações"))
- def load_urls_into_ui(self):
- for widget in self.scrollable_urls_frame.winfo_children():
- widget.destroy()
- try:
- with open('course-urls.json', 'r', encoding='utf-8') as f:
- current_urls = json.load(f)
- except (FileNotFoundError, json.JSONDecodeError):
- current_urls = []
- for url in current_urls:
- url_frame = ctk.CTkFrame(self.scrollable_urls_frame, corner_radius=8, fg_color="#202020")
- url_frame.pack(fill="x", padx=5, pady=5)
- label = ctk.CTkLabel(url_frame, text=url, wraplength=700, justify="left", font=ctk.CTkFont(size=13))
- label.pack(side="left", padx=10, pady=8, expand=True, fill="x")
- remove_button = ctk.CTkButton(
- url_frame, text="❌", width=50, fg_color="#C62828", hover_color="#E53935",
- text_color="white", command=lambda u=url: self.remove_url(u)
- )
- remove_button.pack(side="right", padx=10, pady=8)
- def add_url(self):
- new_url = self.new_url_entry.get().strip()
- if new_url:
- try:
- with open('course-urls.json', 'r', encoding='utf-8') as f: current_urls = json.load(f)
- except (FileNotFoundError, json.JSONDecodeError):
- current_urls = []
- if new_url not in current_urls:
- current_urls.append(new_url); self.new_url_entry.delete(0, "end")
- with open('course-urls.json', 'w', encoding='utf-8') as f: json.dump(current_urls, f, indent=2, ensure_ascii=False)
- self.load_urls_into_ui()
- def remove_url(self, url_to_remove):
- with open('course-urls.json', 'r', encoding='utf-8') as f: current_urls = json.load(f)
- current_urls.remove(url_to_remove)
- with open('course-urls.json', 'w', encoding='utf-8') as f: json.dump(current_urls, f, indent=2, ensure_ascii=False)
- self.load_urls_into_ui()
- def start_download(self):
- if self.download_thread and self.download_thread.is_alive():
- self.log_to_gui("Um processo de download já está em andamento.")
- return
- self.progress_bar.set(0)
- self.start_button.configure(state="disabled", text="Baixando...")
- self.log_textbox.configure(state="normal"); self.log_textbox.delete("1.0", "end"); self.log_textbox.configure(state="disabled")
- self.show_logs_frame()
- self.download_thread = threading.Thread(target=lambda: asyncio.run(download_logic_main(self.update_progress, self.log_queue)), daemon=True)
- self.download_thread.start()
- def process_log_queue(self):
- try:
- while True:
- message = self.log_queue.get_nowait()
- self.log_to_gui(message)
- except queue.Empty:
- pass
- finally:
- self.after(100, self.process_log_queue)
- def log_to_gui(self, message):
- self.log_textbox.configure(state="normal")
- self.log_textbox.insert("end", message + "\n")
- self.log_textbox.see("end")
- self.log_textbox.configure(state="disabled")
- if "PROCESSO FINALIZADO" in message:
- self.start_button.configure(state="normal", text="INICIAR DOWNLOADS")
- def update_progress(self, value):
- self.progress_bar.set(value)
- if __name__ == "__main__":
- app = App()
- app.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement