diff --git a/README.md b/README.md index 5cac883..a184aa9 100644 --- a/README.md +++ b/README.md @@ -729,631 +729,6 @@ fastanime serve fastanime serve --host --port ``` -An example instance is hosted by [render](https://fastanime.onrender.com/) - -Examples: - -**search for anime by title:** - -```bash -curl 'https://fastanime.onrender.com/search?title=dragon&translation_type=sub' - -``` - -
- -Result - - -```json -{ - "pageInfo": { - "total": 22839 - }, - "results": [ - { - "id": "ju2pgynxn9o9DZvse", - "title": "Dragon Ball Daima", - "type": "Show", - "availableEpisodes": { - "sub": 5, - "dub": 0, - "raw": 0 - } - }, - { - "id": "qpnhxfarTHfP7kjgR", - "title": "My WeChat connects to the Dragon Palace", - "type": "Show", - "availableEpisodes": { - "sub": 26, - "dub": 0, - "raw": 0 - } - }, - { - "id": "8aM5BBoEGLvjG3MZm", - "title": "Sayounara Ryuusei, Konnichiwa Jinsei", - "type": "Show", - "availableEpisodes": { - "sub": 6, - "dub": 0, - "raw": 0 - } - }, - { - "id": "Sg9Q9FyqBnJ9qtv5n", - "title": "Yarinaoshi Reijou wa Ryuutei Heika wo Kouryakuchuu", - "type": "Show", - "availableEpisodes": { - "sub": 5, - "dub": 0, - "raw": 0 - } - }, - { - "id": "gF2mKbWBatQudcF6A", - "title": "Throne of the Dragon King", - "type": "Show", - "availableEpisodes": { - "sub": 3, - "dub": 0, - "raw": 0 - } - }, - { - "id": "SXLNNoorPifT5ZStw", - "title": "Shi Cao Lao Long Bei Guan Yi E Long Zhi Ming Season 2", - "type": "Show", - "availableEpisodes": { - "sub": 7, - "dub": 0, - "raw": 0 - } - }, - { - "id": "v4ZkjtyftscNzYF2A", - "title": "I Have a Dragon in My Body Episode122-133", - "type": "Show", - "availableEpisodes": { - "sub": 77, - "dub": 0, - "raw": 0 - } - }, - { - "id": "9RSQCRJ3d554sBzoz", - "title": "City Immortal Emperor: Dragon King Temple", - "type": "Show", - "availableEpisodes": { - "sub": 20, - "dub": 0, - "raw": 0 - } - }, - { - "id": "t8C6zvsdJE5JJKDLE", - "title": "It Turns Out I Am the Peerless Dragon God", - "type": "Show", - "availableEpisodes": { - "sub": 2, - "dub": 0, - "raw": 0 - } - }, - { - "id": "xyDt3mJieZkD76P7S", - "title": "Urban Hidden Dragon", - "type": "Show", - "availableEpisodes": { - "sub": 13, - "dub": 0, - "raw": 0 - } - }, - { - "id": "8PoJiTEDAswkw8b3u", - "title": "The Collected Animations of ICAF (2001-2006)", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "KZeMmRSsyJgz37EmH", - "title": "Dragon Master", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "7a33i9m26poonyNLg", - "title": "I Have a Dragon in My Body", - "type": "Show", - "availableEpisodes": { - "sub": 79, - "dub": 0, - "raw": 0 - } - }, - { - "id": "uwwvBujGRsjCQ8kKM", - "title": "Cong Gu Huo Niao Kaishi: Long Cheng Fengyun", - "type": "Show", - "availableEpisodes": { - "sub": 16, - "dub": 0, - "raw": 0 - } - }, - { - "id": "RoexdZwHSTDwyzEzd", - "title": "Super Dragon Ball Heroes Meteor Mission", - "type": "Show", - "availableEpisodes": { - "sub": 6, - "dub": 0, - "raw": 0 - } - }, - { - "id": "gAcGCcMENjbWhBnR9", - "title": "Dungeon Meshi", - "type": "Show", - "availableEpisodes": { - "sub": 24, - "dub": 24, - "raw": 0 - } - }, - { - "id": "ZGh2QHiaCY5T5Mhi4", - "title": "Long Shidai", - "type": "Show", - "availableEpisodes": { - "sub": 9, - "dub": 0, - "raw": 1 - } - }, - { - "id": "gZSHt98fQpHRfJJXw", - "title": "Xanadu Dragonslayer Densetsu", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "wo8pX4Sba97mFCAkc", - "title": "Vanguard Dragon God", - "type": "Show", - "availableEpisodes": { - "sub": 86, - "dub": 0, - "raw": 0 - } - }, - { - "id": "rrbCftmca3Y2TEiBX", - "title": "Super Dragon Ball Heroes Ultra God Mission", - "type": "Show", - "availableEpisodes": { - "sub": 10, - "dub": 0, - "raw": 0 - } - }, - { - "id": "JzSeXC2WtBBhn3guN", - "title": "Dragon King's Son-In-Law", - "type": "Show", - "availableEpisodes": { - "sub": 11, - "dub": 0, - "raw": 0 - } - }, - { - "id": "eE3txJGGk9atw7k2v", - "title": "Majutsushi Orphen Hagure Tabi: Seiiki-hen", - "type": "Show", - "availableEpisodes": { - "sub": 12, - "dub": 0, - "raw": 0 - } - }, - { - "id": "4X2JbZgiQrb2PTzex", - "title": "Yowai 5000-nen no Soushoku Dragon, Iwarenaki Jaryuu Nintei (Japanese Dub)", - "type": "Show", - "availableEpisodes": { - "sub": 12, - "dub": 0, - "raw": 0 - } - }, - { - "id": "SHp5NFDakKjPT5nJE", - "title": "Starting from Gu Huoniao: Dragon City Hegemony", - "type": "Show", - "availableEpisodes": { - "sub": 22, - "dub": 0, - "raw": 0 - } - }, - { - "id": "8LgaCGrz7Gz35LRpk", - "title": "Yuan Zun", - "type": "Show", - "availableEpisodes": { - "sub": 5, - "dub": 0, - "raw": 0 - } - }, - { - "id": "4GKHyjFC7Dyc7fBpT", - "title": "Shen Ji Long Wei", - "type": "Show", - "availableEpisodes": { - "sub": 26, - "dub": 0, - "raw": 0 - } - }, - { - "id": "2PQiuXiuJoTQTdgy4", - "title": "Long Zu", - "type": "Show", - "availableEpisodes": { - "sub": 15, - "dub": 0, - "raw": 0 - } - }, - { - "id": "rE47AepmBFRvZ6cne", - "title": "Jidao Long Shen", - "type": "Show", - "availableEpisodes": { - "sub": 40, - "dub": 0, - "raw": 0 - } - }, - { - "id": "c4JcjPbRfiuoJPB4F", - "title": "Dragon Quest: Dai no Daibouken (2020)", - "type": "Show", - "availableEpisodes": { - "sub": 101, - "dub": 100, - "raw": 0 - } - }, - { - "id": "nGRTwG7kj5rCPiAX4", - "title": "Dragon Quest: Dai no Daibouken Tachiagare!! Aban no Shito", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "6LJBjT4RzJaucdmX3", - "title": "Dragon Slayer Eiyuu Densetsu: Ouji no Tabidachi", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 1, - "raw": 0 - } - }, - { - "id": "JKbtxdw2cRqqmZgnS", - "title": "Dragon Quest: Dai no Daibouken Buchiyabure!! Shinsei 6 Daishougun", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "pn32RijEHPfuTYt4h", - "title": "Dragon Quest Retsuden: Roto no Monshou", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "xHwk6oo7jaDrMG9to", - "title": "Dragon Fist", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "ugFXPFQW8kvLocZgx", - "title": "Yowai 5000-nen no Soushoku Dragon, Iwarenaki Jaryuu Nintei", - "type": "Show", - "availableEpisodes": { - "sub": 12, - "dub": 0, - "raw": 0 - } - }, - { - "id": "qSFMEcT4SufEhLZnq", - "title": "Doraemon Movie 8: Nobita to Ryuu no Kishi", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - }, - { - "id": "LTzXFSmQR878MdJaS", - "title": "Dragon Ball Specials", - "type": "Show", - "availableEpisodes": { - "sub": 2, - "dub": 0, - "raw": 0 - } - }, - { - "id": "XuTNNzF7DfapLFMFJ", - "title": "Dragon Ball Super: Super Hero", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 1, - "raw": 0 - } - }, - { - "id": "n4S2spjyTHXHNAMDW", - "title": "Shin Ikkitousen", - "type": "Show", - "availableEpisodes": { - "sub": 3, - "dub": 3, - "raw": 0 - } - }, - { - "id": "srMRCkMEJA9Rmt7do", - "title": "Dragon Ball Z: Atsumare! Goku World", - "type": "Show", - "availableEpisodes": { - "sub": 1, - "dub": 0, - "raw": 0 - } - } - ] -} -``` - -
- -**Get anime by id:** - -```bash -curl 'https://fastanime.onrender.com/anime/8aM5BBoEGLvjG3MZm' -``` - -
- -Result - - -```json -{ - "id": "8aM5BBoEGLvjG3MZm", - "title": "Sayounara Ryuusei, Konnichiwa Jinsei", - "availableEpisodesDetail": { - "sub": ["6", "5", "4", "3", "2", "1"], - "dub": [], - "raw": [] - }, - "type": null -} -``` - -
- -**Get episode streams by translation_type:** - -```bash -curl 'https://fastanime.onrender.com/anime/8aM5BBoEGLvjG3MZm/watch?episode=3&translation_type=sub' -``` - -
- -Result - - -```json -[ - { - "server": "Yt", - "episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3", - "headers": { - "Referer": "https://allanime.day/" - }, - "subtitles": [], - "links": [ - { - "link": "", - "quality": "1080" - } - ] - }, - { - "server": "sharepoint", - "headers": {}, - "subtitles": [], - "episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3", - "links": [ - { - "link": "", - "mp4": true, - "resolutionStr": "Mp4", - "src": "", - "quality": "1080" - } - ] - }, - { - "server": "gogoanime", - "headers": {}, - "subtitles": [], - "episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3", - "links": [ - { - "link": "", - "hls": true, - "mp4": false, - "resolutionStr": "hls P", - "priority": 3, - "quality": "1080" - }, - { - "link": "", - "hls": true, - "mp4": false, - "resolutionStr": "HLS1", - "priority": 2, - "quality": "720" - }, - { - "link": "", - "hls": true, - "resolutionStr": "Alt", - "src": "", - "priority": 1, - "quality": "480" - } - ] - } -] -``` - -
- -**Get Episode Streams by AniList Id:** - -```bash -curl 'https://fastanime.onrender.com/watch/269?episode=1&translation_type=dub' -``` - -
- -Results - - -```json -[ - { - "server": "gogoanime", - "headers": {}, - "subtitles": [], - "episode_title": "Bleach; Episode 1", - "links": [ - { - "link": "", - "hls": true, - "mp4": false, - "resolutionStr": "hls P", - "priority": 3, - "quality": "1080" - }, - { - "link": "", - "hls": true, - "mp4": false, - "resolutionStr": "HLS1", - "priority": 2, - "quality": "720" - }, - { - "link": "", - "hls": true, - "resolutionStr": "Alt", - "src": "", - "priority": 1, - "quality": "480" - } - ] - }, - { - "server": "Yt", - "episode_title": "Bleach; Episode 1", - "headers": { - "Referer": "https://allanime.day/" - }, - "subtitles": [], - "links": [ - { - "link": "", - "quality": "1080" - } - ] - }, - { - "server": "wixmp", - "headers": {}, - "subtitles": [], - "episode_title": "Bleach; Episode 1", - "links": [ - { - "link": "", - "hls": true, - "resolutionStr": "Hls", - "quality": "1080" - } - ] - }, - { - "server": "sharepoint", - "headers": {}, - "subtitles": [], - "episode_title": "Bleach; Episode 1", - "links": [ - { - "link": "", - "mp4": true, - "resolutionStr": "Mp4", - "src": "", - "quality": "1080" - } - ] - } -] -``` - -
### MPV specific commands diff --git a/fastanime/cli/commands/anilist/download.py b/fastanime/cli/commands/anilist/download.py index 3ef29ee..46ccad9 100644 --- a/fastanime/cli/commands/anilist/download.py +++ b/fastanime/cli/commands/anilist/download.py @@ -187,11 +187,7 @@ def download( translation_type = config.translation_type download_dir = config.downloads_dir anime_titles = [ - ( - anime["title"][config.preferred_language] - or anime["title"]["english"] - or anime["title"]["romaji"] - ) + (anime["title"]["romaji"] or anime["title"]["english"]) for anime in anilist_search_results["data"]["Page"]["media"] ] print(f"[green bold]Queued:[/] {anime_titles}") diff --git a/fastanime/cli/commands/grab.py b/fastanime/cli/commands/grab.py index d43a06e..885a312 100644 --- a/fastanime/cli/commands/grab.py +++ b/fastanime/cli/commands/grab.py @@ -213,25 +213,21 @@ def grab( # lets download em for episode in episodes_range: - try: - if episode not in episodes: - continue - streams = anime_provider.get_episode_streams( - anime["id"], episode, config.translation_type - ) - if not streams: - continue - episode_streams = {server["server"]: server for server in streams} + if episode not in episodes: + continue + streams = anime_provider.get_episode_streams( + anime["id"], episode, config.translation_type + ) + if not streams: + continue + episode_streams = {server["server"]: server for server in streams} - if episode_streams_only: - grabbed_anime[episode] = episode_streams - else: - grabbed_anime["episodes_streams"][ # pyright:ignore - episode - ] = episode_streams - - except Exception as e: - logger.error(e) + if episode_streams_only: + grabbed_anime[episode] = episode_streams + else: + grabbed_anime["episodes_streams"][ # pyright:ignore + episode + ] = episode_streams # grab the full data for single title and appen to final result or episode streams grabbed_animes.append(grabbed_anime) diff --git a/fastanime/cli/interfaces/anilist_interfaces.py b/fastanime/cli/interfaces/anilist_interfaces.py index 42d1f7a..ec7867e 100644 --- a/fastanime/cli/interfaces/anilist_interfaces.py +++ b/fastanime/cli/interfaces/anilist_interfaces.py @@ -45,11 +45,14 @@ def calculate_percentage_completion(start_time, end_time): [TODO:return] """ - start = start_time.split(":") - end = end_time.split(":") - start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2]) - end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2]) - return start_secs / end_secs * 100 + try: + start = start_time.split(":") + end = end_time.split(":") + start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2]) + end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2]) + return start_secs / end_secs * 100 + except Exception: + return 0 def media_player_controls( @@ -701,7 +704,7 @@ def provider_anime_episodes_menu( total_time = user_watch_history.get(str(anime_id_anilist), {}).get( "episode_total_length", "0" ) - if stop_time != "0" or total_time != "0": + if stop_time != "0" and total_time != "0": percentage_completion_of_episode = calculate_percentage_completion( stop_time, total_time ) diff --git a/fastanime/libs/anime_provider/hianime/api.py b/fastanime/libs/anime_provider/hianime/api.py index c391498..1aaf6d6 100644 --- a/fastanime/libs/anime_provider/hianime/api.py +++ b/fastanime/libs/anime_provider/hianime/api.py @@ -3,6 +3,7 @@ import re from html.parser import HTMLParser from itertools import cycle from urllib.parse import quote_plus +from .extractors import MegaCloud from yt_dlp.utils import ( clean_html, @@ -195,49 +196,83 @@ class HiAnimeApi(AnimeProvider): def _get_server(server_name, server_html): # keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ] servers_info = extract_attributes(server_html) - embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}" + server_id = servers_info["data-id"] + embed_url = ( + f"https://hianime.to/ajax/v2/episode/sources?id={server_id}" + ) embed_response = self.session.get(embed_url) if embed_response.ok: embed_json = embed_response.json() raw_link_to_streams = embed_json["link"] - match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams) - if not match: - return - provider_domain = match.group(1) - embed_type = match.group(2) - episode_number = match.group(3) - source_id = match.group(4) + match server_name: + # TODO: Finish the other servers + case "HD2": + data = MegaCloud(self.session).extract( + raw_link_to_streams + ) + return { + "headers": {}, + "subtitles": [ + { + "url": track["file"], + "language": track["label"], + } + for track in data["tracks"] + if track["kind"] == "captions" + ], + "server": server_name, + "episode_title": episode_details["title"], + "links": give_random_quality( + [ + {"link": link["url"]} + for link in data["sources"] + ] + ), + } + case _: + # NOTE: THIS METHOD DOES'NT WORK will get the other servers later + match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams) + if not match: + return + provider_domain = match.group(1) + embed_type = match.group(2) + episode_number = match.group(3) + source_id = match.group(4) - link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}" - link_to_streams_response = self.session.get(link_to_streams) - if link_to_streams_response.ok: - juicy_streams_json: "HiAnimeStream" = ( - link_to_streams_response.json() - ) - # TODO: Hianime decided to fucking encrypt shit - # so got to fix it later - return { - "headers": {}, - "subtitles": [ - { - "url": track["file"], - "language": track["label"], + link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}" + link_to_streams_response = self.session.get( + link_to_streams + ) + if link_to_streams_response.ok: + juicy_streams_json: "HiAnimeStream" = ( + link_to_streams_response.json() + ) + + # TODO: Hianime decided to fucking encrypt shit + # so got to fix it later + return { + "headers": {}, + "subtitles": [ + { + "url": track["file"], + "language": track["label"], + } + for track in juicy_streams_json["tracks"] + if track["kind"] == "captions" + ], + "server": server_name, + "episode_title": episode_details["title"], + "links": give_random_quality( + [ + {"link": link["file"]} + for link in juicy_streams_json["tracks"] + ] + ), } - for track in juicy_streams_json["tracks"] - if track["kind"] == "captions" - ], - "server": server_name, - "episode_title": episode_details["title"], - "links": give_random_quality( - [ - {"link": link["file"]} - for link in juicy_streams_json["tracks"] - ] - ), - } for server_name, server_html in zip( cycle(SERVERS_AVAILABLE), servers_html ): - if server := _get_server(server_name, server_html): - yield server + if server_name == "HD2": + if server := _get_server(server_name, server_html): + yield server diff --git a/fastanime/libs/anime_provider/hianime/constants.py b/fastanime/libs/anime_provider/hianime/constants.py index 28ad1dc..17706e7 100644 --- a/fastanime/libs/anime_provider/hianime/constants.py +++ b/fastanime/libs/anime_provider/hianime/constants.py @@ -1 +1,26 @@ SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"] +"""" + | "hd-1" + | "hd-2" + | "megacloud" + | "streamsb" + | "streamtape"; + +""" + + +""" + VidStreaming = "hd-1", + MegaCloud = "megacloud", + StreamSB = "streamsb", + StreamTape = "streamtape", + VidCloud = "hd-2", + AsianLoad = "asianload", + GogoCDN = "gogocdn", + MixDrop = "mixdrop", + UpCloud = "upcloud", + VizCloud = "vizcloud", + MyCloud = "mycloud", + Filemoon = "filemoon", + +""" diff --git a/fastanime/libs/anime_provider/hianime/extractors.py b/fastanime/libs/anime_provider/hianime/extractors.py new file mode 100644 index 0000000..5ad6c51 --- /dev/null +++ b/fastanime/libs/anime_provider/hianime/extractors.py @@ -0,0 +1,190 @@ +import hashlib +import time +import re +import json +from typing import List, Dict +from Crypto.Cipher import AES +from base64 import b64decode +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ...common.requests_cacher import CachedRequestsSession + + +# Constants +megacloud = { + "script": "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", + "sources": "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", +} + + +class HiAnimeError(Exception): + def __init__(self, message, context, status_code): + super().__init__(f"{context}: {message} (Status: {status_code})") + self.context = context + self.status_code = status_code + + +# Adapted from https://github.com/ghoshRitesh12/aniwatch +class MegaCloud: + def __init__(self, session): + self.session: "CachedRequestsSession" = session + + def extract(self, video_url: str) -> Dict: + try: + extracted_data = { + "tracks": [], + "intro": {"start": 0, "end": 0}, + "outro": {"start": 0, "end": 0}, + "sources": [], + } + + video_id = video_url.split("/")[-1].split("?")[0] + response = self.session.get( + megacloud["sources"] + video_id, + headers={ + "Accept": "*/*", + "X-Requested-With": "XMLHttpRequest", + "Referer": video_url, + }, + fresh=1, # pyright: ignore + ) + srcs_data = response.json() + + if not srcs_data: + raise HiAnimeError( + "Url may have an invalid video id", "getAnimeEpisodeSources", 400 + ) + + encrypted_string = srcs_data["sources"] + if not srcs_data["encrypted"] and isinstance(encrypted_string, list): + extracted_data.update( + { + "intro": srcs_data["intro"], + "outro": srcs_data["outro"], + "tracks": srcs_data["tracks"], + "sources": [ + {"url": s["file"], "type": s["type"]} + for s in encrypted_string + ], + } + ) + return extracted_data + + # Fetch decryption script + script_response = self.session.get( + megacloud["script"] + str(int(time.time() * 1000)) + ) + script_text = script_response.text + if not script_text: + raise HiAnimeError( + "Couldn't fetch script to decrypt resource", + "getAnimeEpisodeSources", + 500, + ) + + vars_ = self.extract_variables(script_text) + if not vars_: + raise Exception( + "Can't find variables. Perhaps the extractor is outdated." + ) + + secret, encrypted_source = self.get_secret(encrypted_string, vars_) + decrypted = self.decrypt(encrypted_source, secret) + + try: + sources = json.loads(decrypted) + extracted_data.update( + { + "intro": srcs_data["intro"], + "outro": srcs_data["outro"], + "tracks": srcs_data["tracks"], + "sources": [ + {"url": s["file"], "type": s["type"]} for s in sources + ], + } + ) + return extracted_data + except Exception: + raise HiAnimeError( + "Failed to decrypt resource", "getAnimeEpisodeSources", 500 + ) + except Exception as err: + raise err + + def extract_variables(self, text: str) -> List[List[int]]: + regex = r"case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);" + matches = re.finditer(regex, text) + vars_ = [] + for match in matches: + key1 = self.matching_key(match[1], text) + key2 = self.matching_key(match[2], text) + try: + vars_.append([int(key1, 16), int(key2, 16)]) + except ValueError: + continue + return vars_ + + def get_secret( + self, encrypted_string: str, values: List[List[int]] + ) -> tuple[str, str]: + secret = [] + encrypted_source_array = list(encrypted_string) + current_index = 0 + + for start, length in values: + start += current_index + end = start + length + secret.extend(encrypted_string[start:end]) + encrypted_source_array[start:end] = [""] * length + current_index += length + + encrypted_source = "".join(encrypted_source_array) # .replace("\x00", "") + return ("".join(secret), encrypted_source) + + def decrypt(self, encrypted: str, key_or_secret: str, maybe_iv: str = "") -> str: + if maybe_iv: + key = key_or_secret.encode() + iv = maybe_iv.encode() + contents = encrypted + else: + # Decode the Base64 string + cypher = b64decode(encrypted) + + # Extract the salt from the cypher text + salt = cypher[8:16] + + # Combine the key_or_secret with the salt + password = key_or_secret.encode() + salt + + # Generate MD5 hashes + md5_hashes = [] + digest = password + for _ in range(3): + md5 = hashlib.md5() + md5.update(digest) + md5_hashes.append(md5.digest()) + digest = md5_hashes[-1] + password + + # Derive the key and IV + key = md5_hashes[0] + md5_hashes[1] + iv = md5_hashes[2] + + # Extract the encrypted contents + contents = cypher[16:] + + # Initialize the AES decipher + decipher = AES.new(key, AES.MODE_CBC, iv) + + # Decrypt and decode + decrypted = decipher.decrypt(contents).decode("utf-8") # pyright: ignore + + # Remove any padding (PKCS#7) + pad = ord(decrypted[-1]) + return decrypted[:-pad] + + def matching_key(self, value: str, script: str) -> str: + match = re.search(rf",{value}=((?:0x)?[0-9a-fA-F]+)", script) + if match: + return match.group(1).replace("0x", "") + raise Exception("Failed to match the key") diff --git a/flake.nix b/flake.nix index 3165f79..6aaff8e 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,7 @@ preBuild = '' sed -i 's/rich>=13.9.2/rich>=13.8.1/' pyproject.toml + sed -i 's/pycryptodome>=3.21.0/pycryptodome>=3.20.0/' pyproject.toml ''; # Add runtime dependencies @@ -35,6 +36,7 @@ plyer mpv fastapi + pycryptodome ]; # Ensure compatibility with the pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index 43a5387..0cd8be1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ requires-python = ">=3.10" dependencies = [ "click>=8.1.7", "inquirerpy>=0.3.4", + "pycryptodome>=3.21.0", "requests>=2.32.3", "rich>=13.9.2", "thefuzz>=0.22.1", diff --git a/uv.lock b/uv.lock index 37dd8f1..dc64231 100644 --- a/uv.lock +++ b/uv.lock @@ -324,6 +324,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "inquirerpy" }, + { name = "pycryptodome" }, { name = "requests" }, { name = "rich" }, { name = "thefuzz" }, @@ -364,6 +365,7 @@ requires-dist = [ { name = "mpv", marker = "extra == 'standard'", specifier = ">=1.0.7" }, { name = "plyer", marker = "extra == 'notifications'", specifier = ">=2.1.0" }, { name = "plyer", marker = "extra == 'standard'", specifier = ">=2.1.0" }, + { name = "pycryptodome", specifier = ">=3.21.0" }, { name = "requests", specifier = ">=2.32.3" }, { name = "rich", specifier = ">=13.9.2" }, { name = "thefuzz", specifier = ">=0.22.1" }, @@ -721,6 +723,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pycryptodome" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/52/13b9db4a913eee948152a079fe58d035bd3d1a519584155da8e786f767e6/pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", size = 4818071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/88/5e83de10450027c96c79dc65ac45e9d0d7a7fef334f39d3789a191f33602/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", size = 2495937 }, + { url = "https://files.pythonhosted.org/packages/66/e1/8f28cd8cf7f7563319819d1e172879ccce2333781ae38da61c28fe22d6ff/pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", size = 1634629 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/f75a1aaff0c20c11df8dc8e2bf8057e7f73296af7dfd8cbb40077d1c930d/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", size = 2168708 }, + { url = "https://files.pythonhosted.org/packages/ea/66/6f2b7ddb457b19f73b82053ecc83ba768680609d56dd457dbc7e902c41aa/pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", size = 2254555 }, + { url = "https://files.pythonhosted.org/packages/2c/2b/152c330732a887a86cbf591ed69bd1b489439b5464806adb270f169ec139/pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", size = 2294143 }, + { url = "https://files.pythonhosted.org/packages/55/92/517c5c498c2980c1b6d6b9965dffbe31f3cd7f20f40d00ec4069559c5902/pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", size = 2160509 }, + { url = "https://files.pythonhosted.org/packages/39/1f/c74288f54d80a20a78da87df1818c6464ac1041d10988bb7d982c4153fbc/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", size = 2329480 }, + { url = "https://files.pythonhosted.org/packages/39/1b/d0b013bf7d1af7cf0a6a4fce13f5fe5813ab225313755367b36e714a63f8/pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", size = 2254397 }, + { url = "https://files.pythonhosted.org/packages/14/71/4cbd3870d3e926c34706f705d6793159ac49d9a213e3ababcdade5864663/pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", size = 1775641 }, + { url = "https://files.pythonhosted.org/packages/43/1d/81d59d228381576b92ecede5cd7239762c14001a828bdba30d64896e9778/pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", size = 1812863 }, + { url = "https://files.pythonhosted.org/packages/25/b3/09ff7072e6d96c9939c24cf51d3c389d7c345bf675420355c22402f71b68/pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", size = 1691593 }, + { url = "https://files.pythonhosted.org/packages/a8/91/38e43628148f68ba9b68dedbc323cf409e537fd11264031961fd7c744034/pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", size = 1765997 }, + { url = "https://files.pythonhosted.org/packages/08/16/ae464d4ac338c1dd41f89c41f9488e54f7d2a3acf93bb920bb193b99f8e3/pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", size = 1615855 }, + { url = "https://files.pythonhosted.org/packages/1e/8c/b0cee957eee1950ce7655006b26a8894cee1dc4b8747ae913684352786eb/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", size = 1650018 }, + { url = "https://files.pythonhosted.org/packages/93/4d/d7138068089b99f6b0368622e60f97a577c936d75f533552a82613060c58/pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", size = 1687977 }, + { url = "https://files.pythonhosted.org/packages/96/02/90ae1ac9f28be4df0ed88c127bf4acc1b102b40053e172759d4d1c54d937/pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", size = 1788273 }, +] + [[package]] name = "pycryptodomex" version = "3.21.0"