-
-
Save felipeadeildo/961587b1f5660a5db42102666e9884d0 to your computer and use it in GitHub Desktop.
| import json | |
| import os | |
| import pickle | |
| import queue | |
| import random | |
| import re | |
| import threading | |
| from pathlib import Path | |
| from typing import Optional | |
| from urllib.parse import parse_qs | |
| import m3u8 | |
| import requests | |
| from bs4 import BeautifulSoup | |
| BASE_API = "https://skylab-api.rocketseat.com.br" | |
| BASE_URL = "https://app.rocketseat.com.br" | |
| SESSION_PATH = Path(".session.pkl") | |
| def clear_screen(): | |
| os.system("cls" if os.name == "nt" else "clear") | |
| def sanitize_string(string: str): | |
| return re.sub(r'[@#$%&*/:^{}<>?"]', "", string).strip() | |
| class PandaVideo: | |
| def __init__(self, url: str, save_path: str, threads_count=10): | |
| def create_session(headers: dict = {}): | |
| session = requests.session() | |
| session.headers.update( | |
| { | |
| "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", | |
| **headers, | |
| } | |
| ) | |
| return session | |
| qs = parse_qs(url.split("?")[-1]) | |
| if "v" not in qs: | |
| raise Exception("ID do vídeo não encontrado na querystring") | |
| self.video_id = qs["v"][0] | |
| _match = re.search(r"https://([^/]+)/", url) | |
| if not _match: | |
| raise Exception("Domínio não encontrado na URL") | |
| self.domain = _match.group(1).replace("player", "b") | |
| self.session = create_session( | |
| {"referer": url, "origin": f"https://{self.domain}"} | |
| ) | |
| self.save_path = save_path | |
| self.threads_count = threads_count | |
| def _create_temp_folder(self): | |
| foldername = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=5)) | |
| self.temp_folder = Path(".temp") / foldername | |
| if not os.path.exists(self.temp_folder): | |
| os.makedirs(self.temp_folder) | |
| return self.temp_folder | |
| def __convert_segments(self): | |
| ffmpeg_cmd = f'ffmpeg -hide_banner -loglevel error -stats -y -i "{self.temp_folder}/playlist.m3u8" -c copy -bsf:a aac_adtstoasc "{self.save_path}"' | |
| os.system(ffmpeg_cmd) | |
| for filename in os.listdir(self.temp_folder): # type: ignore [StrPath is str :D] | |
| os.remove(self.temp_folder / filename) | |
| os.removedirs(self.temp_folder) | |
| def __download_playlist(self, playlist_url: str): | |
| self._create_temp_folder() | |
| playlist_content = self.session.get(playlist_url).text | |
| # writing the local playlist | |
| with open(self.temp_folder / "playlist.m3u8", "w") as file: | |
| for line in playlist_content.splitlines(): | |
| line = line.split("/")[-1] if line.startswith("https") else line | |
| file.write(f"{line}\n") | |
| playlist = m3u8.loads(playlist_content) | |
| threads = [] | |
| segment_queue = queue.Queue() | |
| self.downloaded_segments = 0 | |
| self.total_segments = len(playlist.segments) | |
| for segment in playlist.segments: | |
| segment_queue.put(segment) | |
| def worker(): | |
| while not segment_queue.empty(): | |
| segment = segment_queue.get() | |
| filename = segment.uri.split("/")[-1] | |
| with open(self.temp_folder / filename, "wb") as file: | |
| file.write(self.session.get(segment.uri).content) | |
| segment_queue.task_done() | |
| self.downloaded_segments += 1 | |
| print( | |
| "\r\t\t\t\t\tBaixando segmento %d de %d" | |
| % (self.downloaded_segments, self.total_segments), | |
| end="", | |
| flush=True, | |
| ) | |
| for _ in range(self.threads_count): | |
| thread = threading.Thread(target=worker) | |
| thread.start() | |
| threads.append(thread) | |
| for thread in threads: | |
| thread.join() | |
| segment_queue.join() | |
| print("\n\t\t\t\t\tConcluído!", flush=True) | |
| self.__convert_segments() | |
| def download(self): | |
| if os.path.exists(self.save_path): | |
| print("\t\t\t\tArquivo já existe (PANDA)") | |
| return | |
| playlists_url = f"https://{self.domain}/{self.video_id}/playlist.m3u8" | |
| playlists_content = self.session.get(playlists_url).text | |
| playlists_loaded = m3u8.loads(playlists_content) | |
| best_playlist = max( | |
| playlists_loaded.playlists, | |
| key=lambda x: x.stream_info.resolution[0] * x.stream_info.resolution[1], | |
| ) | |
| best_playlist_url = ( | |
| (f"https://{self.domain}/{self.video_id}/{best_playlist.uri}") | |
| if not best_playlist.uri.startswith("http") | |
| else best_playlist.uri | |
| ) | |
| self.__download_playlist(best_playlist_url) | |
| class RocketseatProperties: | |
| specialization: dict | |
| module: dict | |
| levels: list | |
| level: dict | |
| groups: list | |
| group: dict = dict() | |
| lesson: dict | |
| @property | |
| def specialization_name(self) -> str: | |
| return self.specialization["title"] | |
| @property | |
| def specialization_uri(self) -> str: | |
| return self.specialization["uri"] | |
| @property | |
| def specialization_slug(self) -> str: | |
| return self.specialization["slug"] | |
| @property | |
| def modules(self) -> list[dict]: | |
| return self.level["contents"] | |
| @modules.setter | |
| def modules(self, value: list[dict]): | |
| self.level["contents"] = value | |
| @property | |
| def modules_count(self) -> int: | |
| return len(self.modules) | |
| @property | |
| def module_name(self) -> str: | |
| return self.module["title"] | |
| @property | |
| def module_index(self) -> int: | |
| return self.module["script_index"] | |
| @property | |
| def f_module_name(self) -> str: | |
| return f"{self.module_index} - {sanitize_string(self.module_name)}" | |
| @property | |
| def module_slug(self) -> str: | |
| return self.module["slug"] | |
| @property | |
| def levels_count(self) -> int: | |
| return len(self.levels) | |
| @property | |
| def level_name(self) -> str: | |
| return self.level["title"] | |
| @property | |
| def level_type(self) -> str: | |
| return self.level["type"] | |
| @property | |
| def level_index(self) -> int: | |
| return self.level["script_index"] | |
| @property | |
| def f_level_name(self) -> str: | |
| return f"{self.level_index} - {sanitize_string(self.level_name)}" | |
| @property | |
| def group_name(self) -> str: | |
| return self.group["title"] | |
| @property | |
| def group_index(self) -> int: | |
| return self.group["script_index"] | |
| @property | |
| def f_group_name(self) -> str: | |
| return f"{self.group_index} - {sanitize_string(self.group_name)}" | |
| @property | |
| def groups_count(self) -> int: | |
| return len(self.groups) | |
| @property | |
| def lessons(self) -> list: | |
| return self.group["lessons"] | |
| @lessons.setter | |
| def lessons(self, value: list): | |
| self.group["lessons"] = value | |
| @property | |
| def lessons_count(self) -> int: | |
| return len(self.lessons) | |
| @property | |
| def lesson_name(self) -> str: | |
| return self.lesson["last"]["title"] | |
| @property | |
| def lesson_index(self) -> int: | |
| return self.lesson["script_index"] | |
| @property | |
| def f_lesson_name(self) -> str: | |
| return f"{self.lesson_index} - {sanitize_string(self.lesson_name)}" | |
| @property | |
| def lesson_type(self) -> str: | |
| return self.lesson["type"] | |
| @property | |
| def lesson_video_id(self) -> str: | |
| return self.lesson["last"]["resource"] | |
| @property | |
| def lesson_description(self) -> Optional[str]: | |
| return self.lesson["last"].get("description") | |
| @property | |
| def lesson_attachments(self) -> list[dict]: | |
| return self.lesson["last"].get("downloads", []) or [] | |
| @property | |
| def save_path(self) -> Path: | |
| _path = Path("Cursos") / self.specialization_name / self.f_level_name | |
| if self.level_type != "lesson": | |
| _path /= self.f_module_name | |
| if self.modules_count == 1 or self.groups_count == 1: | |
| _path /= self.f_lesson_name | |
| else: | |
| _path /= self.f_group_name | |
| _path /= self.f_lesson_name | |
| _path.mkdir(exist_ok=True, parents=True) | |
| return _path | |
| class Rocketseat(RocketseatProperties): | |
| def __init__(self): | |
| self._session_exists = SESSION_PATH.exists() | |
| if self._session_exists: | |
| print( | |
| f"Carregando sessão salva (se ocorrer algum erro, considere apagar o arquivo {SESSION_PATH})..." | |
| ) | |
| self.session = pickle.load(SESSION_PATH.open("rb")) | |
| else: | |
| self.session = requests.session() | |
| self.session.headers["User-Agent"] = ( | |
| "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" | |
| ) | |
| self.session.headers["Referer"] = "https://app.rocketseat.com.br/" | |
| def _download_video(self): | |
| """essa disgraça vai baixar o video, vai basicamente pegar o módulo id e instanciar a classe de download passando como argumento o save_path""" | |
| PandaVideo( | |
| f"https://player-vz-762f4670-e04.tv.pandavideo.com.br/embed/?v={self.lesson_video_id}", | |
| str(self.save_path / "aulinha.mp4"), | |
| ).download() | |
| def _save_lesson_description(self): | |
| """Salva a descrição da aula se houver num aquivo txt""" | |
| if self.lesson_description: | |
| with (self.save_path / "descricao.txt").open("w") as file: | |
| file.write(self.lesson_description) | |
| def _download_lesson_attachments(self): | |
| """Baixa, se houver, anexos para a aula (só iterar sobre downloads e dar get em file_url)""" | |
| for attach in self.lesson_attachments: | |
| filename = f"{sanitize_string(attach['title'])} - {attach['file']}" | |
| file_path = self.save_path / filename | |
| if file_path.exists(): | |
| print(f"\t\t\t\tAnexo {filename} já existe. Pulando...") | |
| continue | |
| with (self.save_path / filename).open("wb") as f: | |
| f.write(self.session.get(attach["file_url"]).content) | |
| print(f"\t\t\t\tAnexo baixado: {filename}") | |
| def _download_lesson(self): | |
| """finalmente sapoha vai indentifcicar o tipo de conteúdo e chamar a função que trata do tipo específico da entrega de copnteúdo da lessson""" | |
| print( | |
| f"\t\t\t\tBaixando aula {self.lesson_index} de {self.lessons_count}: {self.lesson_name}" | |
| ) | |
| factory = { | |
| "video": self._download_video, | |
| } | |
| factory.get( | |
| self.lesson_type, | |
| lambda: print(f"Tipo de aula desconhecido: {self.lesson_type}"), | |
| )() | |
| self._save_lesson_description() | |
| self._download_lesson_attachments() | |
| def _download_group(self): | |
| """Só itera sobre as lessons de dentro do grupo e chama o downloader""" | |
| print( | |
| f"\t\t\tBaixando grupo {self.group_index} de {self.groups_count}: {self.group_name}" | |
| ) | |
| for i, self.lesson in enumerate(self.lessons, 1): | |
| self.lesson.update(script_index=i) | |
| self._download_lesson() | |
| def _download_module(self): | |
| """essa disgraça lista os grupos, itera sobre eles e chama o downloader group""" | |
| print( | |
| f"\t\tBaixando módulo {self.module_index} de {self.modules_count}: {self.module_name}" | |
| ) | |
| data = self.session.get(f"{BASE_API}/journey-nodes/{self.module_slug}").json() | |
| if data["cluster"] is None: | |
| self.groups = [data["group"]] | |
| else: | |
| self.groups = data["cluster"]["groups"] | |
| for i, self.group in enumerate(self.groups, 1): | |
| self.group.update(script_index=i) | |
| self._download_group() | |
| def _download_level(self): | |
| """Itera sobre os níveis e seus módulos e bota pra baixar topado""" | |
| print( | |
| f"\tBaixando level {self.level_index} de {self.levels_count}: {self.level_name}" | |
| ) | |
| if self.level_type == "lesson": | |
| self.lessons = [self.level["lesson"]] | |
| self.lesson = self.lessons[0] | |
| self.lesson.update(script_index=1) | |
| self._download_lesson() | |
| elif self.level_type == "cluster": | |
| self.modules = [self.level] | |
| self.module = self.modules[0] | |
| self._download_module() | |
| else: | |
| print( | |
| f"\tO tipo de level é {self.level_type} o qual não foi (talvez nem será) implementado neste script." | |
| ) | |
| def __load_nodes(self) -> list[dict]: | |
| """Esta função (agora) vai carregar o json vindo em embedding num javascript renderizado no html pelo servidor.""" | |
| res = self.session.get(f"{BASE_URL}/{self.specialization_uri}/contents") | |
| soup = BeautifulSoup(res.text, "html.parser") | |
| try: | |
| script_tag = next( | |
| script | |
| for script in soup.find_all("script") | |
| if "journeyId" in script.text | |
| ) | |
| except StopIteration: | |
| print("O script contendo as aulas não foi encontrado...") | |
| with open("server_response.html", "wb") as f: | |
| f.write(res.content) | |
| exit(1) | |
| script_text = script_tag.text.strip().replace('\\"', '"').replace("\\\\", "\\") | |
| start = script_text.index('{"journey') | |
| json_content = json.loads(script_text[start:].split(',"children')[0] + "}") | |
| with open("json_content.json", "w", encoding="utf-8") as f: | |
| json.dump(json_content, f, indent=4, ensure_ascii=False) | |
| return json_content["journey"]["nodes"] | |
| def _download_courses(self): | |
| """Lista levels (são nodes agora) e itera sobre elas chamando download modules""" | |
| clear_screen() | |
| print(f"Baixando especialização {self.specialization_name}") | |
| self.levels = self.__load_nodes() | |
| for i, self.level in enumerate(self.levels, 1): | |
| self.level.update(script_index=i) | |
| self._download_level() | |
| def login(self, username: str, password: str): | |
| """Faz login setando a sessão utilizando username e password fornecidos na instancialização""" | |
| payload = {"email": username, "password": password} | |
| res = self.session.post(f"{BASE_API}/sessions", json=payload).json() | |
| self.session.headers["Authorization"] = ( | |
| f"{res['type'].capitalize()} {res['token']}" | |
| ) | |
| self.session.cookies.update( | |
| { | |
| "skylab_next_access_token_v3": res["token"], | |
| "skylab_next_refresh_token_v3": res["refreshToken"], | |
| } | |
| ) | |
| account_infos = self.session.get(f"{BASE_API}/account").json() | |
| print("Welcome, {}!".format(account_infos["name"])) | |
| pickle.dump(self.session, SESSION_PATH.open("wb")) | |
| def select_specializations(self): | |
| """Permite seleconar uma ou todas as especializações para download dos cursos""" | |
| params = { | |
| "types[0]": "SPECIALIZATION", | |
| "limit": "12", | |
| "offset": "0", | |
| "page": "1", | |
| "sort_by": "relevance", | |
| } | |
| specializations = self.session.get( | |
| f"{BASE_API}/catalog/list", params=params | |
| ).json()["items"] | |
| clear_screen() | |
| print("Selecione uma formação ou 0 para selecionar todas:") | |
| for i, specialization in enumerate(specializations, 1): | |
| print(f"[{i}] - {specialization['title']}") | |
| choice = int(input(">> ")) | |
| if choice == 0: | |
| for self.specialization in specializations: | |
| self._download_courses() | |
| else: | |
| self.specialization = specializations[choice - 1] | |
| self._download_courses() | |
| def run(self): | |
| """Inicia a execução do script""" | |
| if not self._session_exists: | |
| self.login( | |
| username=input("Seu email Rocketseat: "), password=input("Sua senha: ") | |
| ) | |
| self.select_specializations() | |
| if __name__ == "__main__": | |
| agent = Rocketseat() | |
| agent.run() |
como faço para utilizar
a versão mais atualizada está no repo do @alefd2
vulgo https://github.com/alefd2/script-download-lessons-rs
eu executei essa versão nova aí, mas infelizmente ele nao baixa os videos do curso, só baixa varios arquivos .txt
eu executei essa versão nova aí, mas infelizmente ele nao baixa os videos do curso, só baixa varios arquivos .txt
Cara. Manda aqui o print das tuas pastas.
Os arquivos de txt são das aulas ? Se sim ele está achando o curso. Caso ele não ache o vídeo ele dá um log no terminal. Manda o log aqui também.
A Rocket muda a estrutura deles e isso é um saco de mapear então, como foi na versão anterior a minha, ele vai sempre precisar de manutenção de acordo com a estrutura da rocket..
Manda aí que tento te ajudar.
Cara. Manda aqui o print das tuas pastas.
Os arquivos de txt são das aulas ? Se sim ele está achando o curso. Caso ele não ache o vídeo ele dá um log no terminal. Manda o log aqui também. A Rocket muda a estrutura deles e isso é um saco de mapear então, como foi na versão anterior a minha, ele vai sempre precisar de manutenção de acordo com a estrutura da rocket..
Manda aí que tento te ajudar.
cara, nao sei oq aconteceu. Ele baixava certinho as pastas do curso que eu escolhi, mas só baixava arquivos .txt dentro delas, nao baixava as aulas .mp4, entao eu tinha desistido e apaguei do meu pc.
Como voce me respondeu pedindo os prints, hoje eu baixei novamente o script para te mostrar oq estava acontecendo, e incrivelmente agora deu tudo certo, magicamente kkkkkk, pelo que estou vendo aqui baixou certinho os .mp4 (ao menos acho que nao esta faltando aulas).
conclusão: obrigado por me fazer baixar denovo kkkkkk
Kk boa.
Rodei esse script hoje para aproveitar os 2 dias free da Rocketseat, mas só está baixando o .txt
alguem tem uma forma de baixar atualizada?? me manda por favor
ngm sabe ler o resto dos comentários, é foda kkkk
Seguinte, pra ser curto: estou sem tempo pra rever o script original, e preparar algo automatizado, está BEM tarde...
Mas consegui resolver o problema de uma forma mais 'bruta'. Pra quem se interessar, siga os passos, e vai dar certo...
A plataforma da rocketseat usa esse .m3u8 pra formatar, e dar segurança aos vídeos, por isso, precisamos 'quebrar' essa lógica, pra poder baixar ele localmente.
- Baixem isso aqui https://github.com/yt-dlp/yt-dlp, se estiverem no windows, siga usando o .exe.
- Depois de baixar, tenha certeza de ter feito login no site da Rocketseat, e feche o browser, pra evitar o conflito de o dlp pegar os Cookies - se o browser estiver aberto, pode ser que ele bloqueie por segurança o acesso aos Cookies, e por consequência, o seu acesso aos conteúdos.
- Baixe também essa extensão aqui Get cookies.txt LOCALLY em vossos navegadores, acesse a página da Rocketseat - acho que pode ser em qualquer rota, seja / ou /nome-do-curso-alvo, é importante que o .txt que você deve salvar com seus Cookies esteja na raíz, pra garantir que dará certo, e que você tenha feito login!
- O dlp precisa ser executado via prompt no Windows, então abra ele onde salvou o .exe, conforme ensinam na documentação oficial da aplicação.
- Com seus Cookies de acesso ao site, e dlp instanciados no cmd, abram o navegador: F12 > Network > F5, depois filtre o conteúdo usando somente a extensão .m3u8, se não aparecer, dê F5 novamente, com o mesmo filtro.
- Você deve ver aí 3 GET, retornando 200, e um link que se parece com isso aqui:
https://hash.cdn.net/hash/playlist.m3u8 [Playlist]
https://hash.net/hash/video.m3u8 [Vídeo]
https://hash.net/hash/video.m3u8 [Vídeo] - Vamos usar apenas o playlist.m3u8 aqui, então copie ele por inteiro
- Use o seguinte comando pra executar o DLP, usando seus Cookies de acesso á página, e direcionando o link de requisição para o conteúdo do vídeo ser exibido
bash
yt-dlp --cookies "C:\Users\[NOMEDOPC]\Downloads\cookies.txt" -o "[NOME PRA SALVAR O ARQUIVO]" "https://hash.cdn.net/hash/playlist.m3u8"
É um processo manual chato, mas pode ser otimizado se feito através de uma automatização - esse py deve fazer isso, mas com as mudanças de arquitetura da plataforma, fica difícil estabelecer um padrão sempre.
Enfim, baixem os cursos de vocês aí, pois acabará amanhã, as 00.
Infelizmente, nem todos tempos tempo de sobra pra consumir e absorver tudo em apenas 03 dias, mas é o que dá pra fazer...
Espero ter ajudado vocês, de alguma forma. E sucesso!!
Segue uma forma simples de automatizar parte do processo; ainda é necessário fazer login no site oficial, ter acesso ao curso, e realizar a captura dos respectivos links de playlist, que como falei: F12 > Network > Filtrar por .m3u8 > F5 > Copiar somente o playlist.m3u8, e em caso de erros, verificar pelos video.m3u8.
Como executar? Windows, crie um arquivo de texto normal, cole o conteúdo, mude a extensão renomeando o arquivo pra .bat, e tenha tanto o DLP, quanto os Cookies definidos conforme.
@echo off
:: --- CONFIGURAÇÕES ---
set "COOKIES_PATH=C:\Users\[EXEMPLO]\Downloads\cookies.txt"
set "OUTPUT_FOLDER=rocketseat"
:: --- FIM DAS CONFIGURAÇÕES ---
cls
echo.
echo --- SCRIPT DE DOWNLOAD DE AULAS ---
echo.
:: Pede o URL do vídeo para o usuário
set /p "VIDEO_URL=Cole o URL do playlist.m3u8 e pressione Enter: "
:: Pede o nome do arquivo para o usuário
set /p "VIDEO_NAME=Digite o nome para salvar o arquivo (ex: aula-01) e pressione Enter: "
echo.
echo Iniciando o download...
echo.
:: Executa o yt-dlp com as variáveis
yt-dlp --cookies "%COOKIES_PATH%" -o "%OUTPUT_FOLDER%\%VIDEO_NAME%.mp4" "%VIDEO_URL%"
echo.
echo Download concluido!
pause
está rodando lindamente! aproveitando meus últimos dias de free na rocket, obrigado pelo programa rapaziada
está rodando lindamente! aproveitando meus últimos dias de free na rocket, obrigado pelo programa rapaziada
Selecione uma formação ou 0 para selecionar todas:
[1] - Discover
[2] - Masterclass: conhecendo as IAs da Rocketseat
[3] - Angular - Curso Introdutório
[4] - Carreira Internacional
[5] - Dev Global - Starter Pack
[6] - Gestão de Tempo
[7] - Posicionamento nas redes sociais
[8] - React Router 7 + IA
[9] - Banco de dados
[10] - C# e .NET - Curso Introdutório
[11] - Clean Code - Python
[12] - Go - Curso Introdutório
[13] - Java com Spring Boot - Curso Introdutório
[14] - Masterclass: IA
[15] - Python com Flask - Curso Introdutório
[16] - React Native com Expo - Curso Introdutório
[17] - Desacoplando o Component State com headless em React
[18] - Select customizado de um jeito FÁCIL (HTML, CSS e JS) | #boraCodar 34
[19] - Clean Architecture: importância e princípios
[20] - Entendendo Design Patterns
[21] - TDD na prática com Java
[22] - Transcrição de vídeo com IA (Node.js + WhisperAI +TRANSFORMERS.JS) | #boraCodar 31
[23] - Node.js
[24] - Next.js
[25] - React
[26] - Full-Stack
[27] - Inglês para Devs
[28] - React Native
[29] - Soft Skills
[30] - Tech Lead
[31] - Android com Kotlin
[32] - C#
[33] - C# Avançado com .NET MAUI
[34] - Data Analytics
[35] - DevOps
[36] - Go
[37] - IA para devs
[38] - iOS com Swift
[39] - Java
[40] - Lógica de programação
[41] - Machine Learning em Inteligência Artificial
[42] - PHP
[43] - Python
[44] - Acessibilidade com React
[45] - Clean Code - React e Node
[46] - Masterizando o Tailwind - React
[47] - Next.js App Router e Testes - React
[48] - Redux + Zustand - React
[49] - Apps desktop com Electron
[50] - Como criar e monetizar um Micro SaaS?
[51] - Comunicação assertiva
[52] - Criando SaaS com Next.js e RBAC - Node e React
[53] - Da estratégia ao produto
[54] - Desbloqueando a programação
[55] - Engenharia de Prompt
[56] - Higher
[57] - IA para Dados
[58] - Inovação
[59] - Liderança Técnica
[60] - Marca pessoal na prática & Carreira
9
Baixando cursos da especialização: Banco de dados
Início do download: 21/09/2025 12:07:37
Buscando módulos para a formação: banco-de-dados
Recebendo dados dos cursos disponíveis; lembre-se de que é necessário ter acesso legítimo para concluir o download!
Módulo Fundamentos não é do tipo cluster
Módulo Manipulação Básica de Dados não é do tipo cluster
Módulo Modelagem de Dados Essencial não é do tipo cluster
Módulo Quiz - Fundamentos não é do tipo cluster
Módulo Mini Projeto: Modelando um sistema simples não é do tipo cluster
Módulo Consultas e Modelagem não é do tipo cluster
Módulo Relacionamentos e Junções não é do tipo cluster
Módulo Quiz - Consultas e Modelagem não é do tipo cluster
Módulo Técnicas Avançadas não é do tipo cluster
Módulo Otimização e Performance não é do tipo cluster
Módulo Recursos Avançados do Postgres não é do tipo cluster
Módulo Quiz - Técnicas Avançadas não é do tipo cluster
Módulo Projeto e Aplicações Práticas não é do tipo cluster
Módulo Tópicos Especiais para o Mercado não é do tipo cluster
Módulo Quiz - Projeto e Aplicações Práticas não é do tipo cluster
Encontrados 15 módulos.
Início: 12:07:39 | Busca pelos módulos concluída! | Número de módulos encontrados: 15 | Tempo executado: 1.51 segundos
Escolha os módulos que você quer baixar:
[0] - Baixar todos os módulos
[1] - Fundamentos
[2] - Manipulação Básica de Dados
[3] - Modelagem de Dados Essencial
[4] - Quiz - Fundamentos
[5] - Mini Projeto: Modelando um sistema simples
[6] - Consultas e Modelagem
[7] - Relacionamentos e Junções
[8] - Quiz - Consultas e Modelagem
[9] - Técnicas Avançadas
[10] - Otimização e Performance
[11] - Recursos Avançados do Postgres
[12] - Quiz - Técnicas Avançadas
[13] - Projeto e Aplicações Práticas
[14] - Tópicos Especiais para o Mercado
[15] - Quiz - Projeto e Aplicações Práticas
Digite 0 para baixar todos os módulos ou os números dos módulos separados por vírgula (ex: 1, 3, 5): 0
Baixando todos os módulos...
Baixando módulo: Fundamentos do curso: Sem Nome
Módulo não possui cluster_slug: Fundamentos. Pulando.
Baixando módulo: Manipulação Básica de Dados do curso: Sem Nome
Módulo não possui cluster_slug: Manipulação Básica de Dados. Pulando.
Baixando módulo: Modelagem de Dados Essencial do curso: Sem Nome
Módulo não possui cluster_slug: Modelagem de Dados Essencial. Pulando.
Baixando módulo: Quiz - Fundamentos do curso: Sem Nome
Módulo não possui cluster_slug: Quiz - Fundamentos. Pulando.
Baixando módulo: Mini Projeto: Modelando um sistema simples do curso: Sem Nome
Módulo não possui cluster_slug: Mini Projeto: Modelando um sistema simples. Pulando.
Baixando módulo: Consultas e Modelagem do curso: Sem Nome
Módulo não possui cluster_slug: Consultas e Modelagem. Pulando.
Baixando módulo: Relacionamentos e Junções do curso: Sem Nome
Módulo não possui cluster_slug: Relacionamentos e Junções. Pulando.
Baixando módulo: Quiz - Consultas e Modelagem do curso: Sem Nome
Módulo não possui cluster_slug: Quiz - Consultas e Modelagem. Pulando.
Baixando módulo: Técnicas Avançadas do curso: Sem Nome
Módulo não possui cluster_slug: Técnicas Avançadas. Pulando.
Baixando módulo: Otimização e Performance do curso: Sem Nome
Módulo não possui cluster_slug: Otimização e Performance. Pulando.
Baixando módulo: Recursos Avançados do Postgres do curso: Sem Nome
Módulo não possui cluster_slug: Recursos Avançados do Postgres. Pulando.
Baixando módulo: Quiz - Técnicas Avançadas do curso: Sem Nome
Módulo não possui cluster_slug: Quiz - Técnicas Avançadas. Pulando.
Baixando módulo: Projeto e Aplicações Práticas do curso: Sem Nome
Módulo não possui cluster_slug: Projeto e Aplicações Práticas. Pulando.
Baixando módulo: Tópicos Especiais para o Mercado do curso: Sem Nome
Módulo não possui cluster_slug: Tópicos Especiais para o Mercado. Pulando.
Baixando módulo: Quiz - Projeto e Aplicações Práticas do curso: Sem Nome
Módulo não possui cluster_slug: Quiz - Projeto e Aplicações Práticas. Pulando.
==================================================
=== RELATÓRIO DE DOWNLOAD ===
Data: 21/09/2025 12:07:45
Duração total: 0:00:07.572073
Total de aulas: 0
Aulas baixadas com sucesso: 0
Aulas com erro: 0
=== AULAS BAIXADAS COM SUCESSO ===
Relatório salvo em: relatorios\relatorio_20250921_120745.txt
Vou verificar o que mudou dessa aula pras demais - também estou fazendo esse curso :P
Vou verificar o que mudou dessa aula pras demais - também estou fazendo esse curso :P
Tive que mudar um pouco a lógica do Script; agora, ao invés de procurar por dados apenas no HTML, e em alguns dados retornados, ele vai procurar pelos respectivos .jsons que são baixados ao se acessar as páginas dos referidos cursos.
Não sei se vai funcionar EM TODOS, mas o curso de Banco de Dados pode ser baixado tranquilamente - se puderem testar em outros cursos, e reportar, agradeço.
https://github.com/Kazbonfim/rocketseat-downloader2/tree/Kazbonfim-patch-1
De nada
Acho que quebrou novamente, sempre estou recebendo a mesma mensagem ao tentar baixar um módulo:
...
Processando módulo: 'Fundamentos do Angular' (Tipo: cluster)
Módulo 'Fundamentos do Angular' não é um cluster de aulas ou não possui slug. Pulando.
Fiz algumas mudanças e agora está funcionando perfeitamente com todos os cursos da Rocketseat.
https://github.com/jplinharescosta/rocketseat-downloader
- Novidades
- Suporte a nós type=cluster e type=group (parsing de cluster.groups e group.lessons).
- Requisições com retries/backoff e timeout configurável.
- Execução do yt-dlp via subprocess com captura de logs e melhor tratamento de erro.
- Variáveis de ambiente para configurar credenciais, timeout, domínio da CDN e diretório da sessão.
- Logs de debug em logs/{slug}_cluster_details.json.
- Ajustes na seção de uso (python main.py, senha mascarada, sessão persistida).
Se você testar e estiver funcionando perfeitamente eu fico a disposição para fazer um PR e deletar o meu repositório. @Kazbonfim
@jplinharescosta ficou excelente! Fiz um teste em um curso que não baixava nem ferrando, e agora está funcional; mantenha em seu repositório, e pode abrir a PR no meu, pra aprovar, e unificamos - vai te servir como um projeto pequeno pro futuro também!
Obrigado pelas melhorias, bora que bora 🐦🔥
como faço para utilizar