diff --git a/.gitignore b/.gitignore index c63f5db..20d436a 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,5 @@ dmypy.json # Pyre type checker .pyre/ -"test.py" \ No newline at end of file +"test.py" +.vscode/settings.json diff --git a/README.md b/README.md index 55bbd8a..b428f7f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Sakurajima is a Python API wrapper for [AniWatch](https://aniwatch.me). +## Disclaimer + +Using this tool comes with a high risk of getting banned on AniWatch. + ## Installation Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Sakurajima. diff --git a/Sakurajima/api.py b/Sakurajima/api.py index 5d9ebba..e752949 100644 --- a/Sakurajima/api.py +++ b/Sakurajima/api.py @@ -2,6 +2,7 @@ import json import base64 import random +from urllib.parse import unquote from Sakurajima.models import ( Anime, RecommendationEntry, @@ -20,7 +21,7 @@ from Sakurajima.models.user_models import Friend, FriendRequestIncoming, FriendRequestOutgoing from Sakurajima.utils.episode_list import EpisodeList from Sakurajima.utils.network import Network - +from Sakurajima.errors import AniwatchError class Sakurajima: @@ -67,6 +68,19 @@ def using_proxy( proxy = random.choice(proxies).replace("\n", "") return cls(username, userId, authToken, {"https": proxy}) + @classmethod + def from_cookie(cls, cookie_file): + """An alternate constructor that reads a cookie file and automatically extracts + the data neccasary to initialize Sakurajime + + :param cookie_file: The file containing the cookie. + :type cookie_file: str + :rtype: :class:`Sakurajima` + """ + with open(cookie_file, "r") as cookie_file_handle: + cookie = json.loads(unquote(cookie_file_handle.read())) + return cls(cookie["username"], cookie["userid"], cookie["auth"]) + def get_episode(self, episode_id, lang="en-US"): """Gets an AniWatchEpisode by its episode ID. @@ -76,7 +90,7 @@ def get_episode(self, episode_id, lang="en-US"): (English Subbed) :type lang: str, optional :return: An AniWatchEpisode object which has data like streams and lamguages. - :rtype: AniWatchEpisode + :rtype: :class:`AniWatchEpisode` """ data = { "controller": "Anime", @@ -96,7 +110,7 @@ def get_episodes(self, anime_id: int): :return: An EpisodeList object. An EpisodeList is very similar to a normal list, you can access item on a specific index the same way you would do for a normal list. Check out the EpisodeList documentation for further details. - :rtype: EpisodeList + :rtype: :class:`EpisodeList` """ data = { "controller": "Anime", @@ -106,7 +120,7 @@ def get_episodes(self, anime_id: int): return EpisodeList( [ Episode(data_dict, self.network, self.API_URL, anime_id) - for data_dict in self.network.post(data)["episodes"] + for data_dict in self.network.post(data, f"/anime/{anime_id}")["episodes"] ] ) @@ -120,7 +134,16 @@ def get_anime(self, anime_id: int): :rtype: Anime """ data = {"controller": "Anime", "action": "getAnime", "detail_id": str(anime_id)} - return Anime(self.network.post(data)["anime"], network=self.network, api_url=self.API_URL,) + headers = { + "X-PATH": f"/anime/{anime_id}", + "REFERER": f"https://aniwatch.me/anime/{anime_id}" + } + json = self.network.post(data, headers) + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + return Anime(json["anime"], network=self.network, api_url=self.API_URL,) def get_recommendations(self, anime_id: int): """Gets a list of recommendations for an anime. @@ -138,7 +161,7 @@ def get_recommendations(self, anime_id: int): } return [ RecommendationEntry(data_dict, self.network) - for data_dict in self.network.post(data)["entries"] + for data_dict in self.network.post(data, f"/anime/{anime_id}")["entries"] ] def get_relation(self, relation_id: int): @@ -172,7 +195,7 @@ def get_seasonal_anime(self, index="null", year="null"): "current_index": index, "current_year": year, } - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_latest_releases(self): """Gets the latest anime releases. This includes currently airing @@ -182,7 +205,7 @@ def get_latest_releases(self): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getLatestReleases"} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_latest_uploads(self): """Gets latest uploads on "aniwatch.me". This includes animes that are not airing @@ -192,7 +215,7 @@ def get_latest_uploads(self): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getLatestUploads"} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_latest_anime(self): """Gets the latest animes on "aniwatch.me" @@ -201,7 +224,7 @@ def get_latest_anime(self): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getLatestAnime"} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_random_anime(self): """Gets a random anime from the aniwatch.me library. @@ -210,7 +233,7 @@ def get_random_anime(self): :rtype: Anime """ data = {"controller": "Anime", "action": "getRandomAnime"} - return Anime(self.network.post(data)["entries"][0], self.network, self.API_URL) + return Anime(self.network.post(data, "/random")["entries"][0], self.network, self.API_URL) def get_airing_anime(self, randomize=False): """Gets currently airing anime arranged according to weekdays. @@ -227,7 +250,7 @@ def get_airing_anime(self, randomize=False): "action": "getAiringAnime", "randomize": randomize, } - airing_anime_response = self.network.post(data)["entries"] + airing_anime_response = self.network.post(data, "/airing")["entries"] airing_anime = {} for day, animes in airing_anime_response.items(): airing_anime[day] = [Anime(anime_dict, self.network, self.API_URL) for anime_dict in animes] @@ -242,7 +265,7 @@ def get_popular_anime(self, page=1): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getPopularAnime", "page": page} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/top")["entries"]] def get_popular_seasonal_anime(self, page=1): """Gets popular anime of the current season. @@ -253,7 +276,7 @@ def get_popular_seasonal_anime(self, page=1): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getPopularSeasonals", "page": page} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/seasonal")["entries"]] def get_popular_upcoming_anime(self, page=1): """Gets popular anime that have not started airing yet. @@ -264,12 +287,12 @@ def get_popular_upcoming_anime(self, page=1): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getPopularUpcomings", "page": page} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_hot_anime(self, page=1): # TODO inspect this to figure out a correct description. data = {"controller": "Anime", "action": "getHotAnime", "page": page} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def get_best_rated_anime(self, page=1): """Gets the highest rated animes on "aniwatch.me". @@ -280,7 +303,7 @@ def get_best_rated_anime(self, page=1): :rtype: list[Anime] """ data = {"controller": "Anime", "action": "getBestRatedAnime", "page": page} - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, "/home")["entries"]] def add_recommendation(self, anime_id: int, recommended_anime_id: int): """Submit a recommendation for an anime. @@ -309,7 +332,7 @@ def get_stats(self): :rtype: AniwatchStats """ data = {"controller": "XML", "action": "getStatsData"} - return AniwatchStats(self.network.post(data)) + return AniwatchStats(self.network.post(data, "/stats")) def get_user_overview(self, user_id): """Gets a brief user overview which includes stats like total hours watched, @@ -325,8 +348,7 @@ def get_user_overview(self, user_id): "action": "getOverview", "profile_id": str(user_id), } - print(self.network.post(data)) - return UserOverview(self.network.post(data)["overview"]) + return UserOverview(self.network.post(data, f"/profile/{user_id}")["overview"]) def get_user_chronicle(self, user_id, page=1): """Gets the user's chronicle. A chronicle tracks a user's watch history. @@ -346,7 +368,7 @@ def get_user_chronicle(self, user_id, page=1): "page": page, } return [ - ChronicleEntry(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["chronicle"] + ChronicleEntry(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, f"/profile/{user_id}")["chronicle"] ] def get_user_anime_list(self): @@ -364,7 +386,7 @@ def get_user_anime_list(self): } return [ UserAnimeListEntry(data_dict, self.network) - for data_dict in self.network.post(data)["animelist"] + for data_dict in self.network.post(data, f"/profile/{user_id}")["animelist"] ] def get_user_media(self, page=1): @@ -382,7 +404,7 @@ def get_user_media(self, page=1): "profile_id": str(self.userId), "page": page, } - return [UserMedia(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)["entries"]] + return [UserMedia(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data, f"/profile/{user_id}")["entries"]] def send_image_to_discord(self, episode_id, base64_image, episode_time): data = { @@ -819,7 +841,16 @@ def search(self, query: str): "maxEpisodes": 0, "hasRelation": False, } - return [Anime(data_dict, self.network, self.API_URL) for data_dict in self.network.post(data)] + headers = { + "X-PATH": "/search", + "REFERER": f"https://aniwatch.me/search" + } + json = self.network.post(data, headers) + if type(json) == dict and json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + return [Anime(data_dict, self.network, self.API_URL) for data_dict in json] def get_media(self, anime_id: int): """Gets an anime's media. diff --git a/Sakurajima/errors.py b/Sakurajima/errors.py new file mode 100644 index 0000000..9f88789 --- /dev/null +++ b/Sakurajima/errors.py @@ -0,0 +1,3 @@ +class AniwatchError(Exception): + def __init__(self, *args): + super().__init__(*args) \ No newline at end of file diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 57770b5..dfd53bb 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime import requests import json from m3u8 import M3U8 @@ -10,6 +10,7 @@ from Sakurajima.models.helper_models import Language, Stream from Sakurajima.utils.episode_list import EpisodeList from Sakurajima.utils.downloader import Downloader, MultiThreadDownloader +from Sakurajima.errors import AniwatchError import subprocess from time import sleep from pathvalidate import sanitize_filename @@ -44,7 +45,7 @@ def __init__(self, data_dict: dict, network, api_url: str): self.episode_max = data_dict.get("episode_max", None) self.type = data_dict.get("type", None) try: - self.broadcast_start = datetime.datetime.utcfromtimestamp(data_dict.get("broadcast_start")) + self.broadcast_start = datetime.utcfromtimestamp(data_dict.get("broadcast_start")) except: self.broadcast_start = None try: @@ -72,6 +73,12 @@ def __init__(self, data_dict: dict, network, api_url: str): self.score_rank = data_dict.get("score_rank", None) self.__episodes = None + def __generate_default_headers(self): + return { + "X-PATH": f"/anime/{self.anime_id}", + "REFERER": f"https://aniwatch.me/anime/{self.anime_id}" + } + def get_episodes(self): """Gets a list of all available episodes of the anime. @@ -82,20 +89,27 @@ def get_episodes(self): episode number. :rtype: EpisodeList """ - data = { - "controller": "Anime", - "action": "getEpisodes", - "detail_id": str(self.anime_id), - } if self.__episodes: return self.__episodes else: - self.__episodes = EpisodeList( - [ - Episode(data_dict, self.__network, self.__API_URL, self.anime_id, self.title,) - for data_dict in self.__network.post(data)["episodes"] - ] - ) + data = { + "controller": "Anime", + "action": "getEpisodes", + "detail_id": str(self.anime_id), + } + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + self.__episodes = EpisodeList( + [ + Episode(data_dict, self.__network, self.__API_URL, self.anime_id, self.title,) + for data_dict in json["episodes"] + ] + ) return self.__episodes def __repr__(self): @@ -112,7 +126,14 @@ def get_relations(self): "action": "getRelation", "relation_id": self.relation_id, } - return Relation(self.__network.post(data)["relation"]) + + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + return Relation(json["relation"]) def get_recommendations(self): """Gets the recommendations for the anime. @@ -126,9 +147,16 @@ def get_recommendations(self): "action": "getRecommendations", "detail_id": str(self.anime_id), } - return [ - RecommendationEntry(data_dict, self.__network) - for data_dict in self.__network.post(data)["entries"] + headers = self.__generate_default_headers() + json = self.__network(data, headers) + + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + return [ + RecommendationEntry(data_dict, self.__network) + for data_dict in json["entries"] ] def get_chronicle(self, page=1): @@ -147,9 +175,16 @@ def get_chronicle(self, page=1): "detail_id": str(self.anime_id), "page": page, } - return [ - ChronicleEntry(data_dict, self.__network, self.__API_URL) - for data_dict in self.__network.post(data)["chronicle"] + headers = self.__generate_default_headers() + json = self.__network(data, headers) + + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + return [ + ChronicleEntry(data_dict, self.__network, self.__API_URL) + for data_dict in json["chronicle"] ] def mark_as_completed(self): @@ -163,7 +198,10 @@ def mark_as_completed(self): "action": "markAsCompleted", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def mark_as_plan_to_watch(self): """Marks the anime as "plan to watch" on the user's aniwatch anime list. @@ -176,7 +214,7 @@ def mark_as_plan_to_watch(self): "action": "markAsPlannedToWatch", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + return self.__network.post(data, f"/anime/{self.anime_id}")["success"] def mark_as_on_hold(self): """Marks the anime as "on hold" on the user's aniwatch anime list. @@ -189,7 +227,9 @@ def mark_as_on_hold(self): "action": "markAsOnHold", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def mark_as_dropped(self): """Marks the anime as "dropped" on the user's aniwatch anime list. @@ -202,7 +242,9 @@ def mark_as_dropped(self): "action": "markAsDropped", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def mark_as_watching(self): """Marks the anime as "watching" on the user's aniwatch anime list @@ -215,7 +257,9 @@ def mark_as_watching(self): "action": "markAsWatching", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def remove_from_list(self): """Removes the anime from the user's aniwatch anime list. @@ -228,7 +272,9 @@ def remove_from_list(self): "action": "removeAnime", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def rate(self, rating: int): """Set the user's rating for the anime on aniwatch. @@ -246,7 +292,9 @@ def rate(self, rating: int): "detail_id": str(self.anime_id), "rating": rating, } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def get_media(self): """Gets the anime's associated media from aniwatch.me @@ -254,12 +302,16 @@ def get_media(self): :return: A Media object that has attributes like ``opening``, ``osts``. :rtype: Media """ + data = { "controller": "Media", "action": "getMedia", "detail_id": str(self.anime_id), } - return Media(self.__network.post(data), self.__network, self.anime_id,) + + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return Media(json, self.__network, self.anime_id,) def get_complete_object(self): """Gets the current anime object but with complete attributes. Sometimes, the Anime @@ -275,8 +327,14 @@ def get_complete_object(self): "action": "getAnime", "detail_id": str(self.anime_id), } - data_dict = self.__network.post(data)["anime"] - return Anime(data_dict, self.__network, api_url=self.__API_URL,) + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + if json.get("success", True) != True: + error = json["error"] + raise AniwatchError(error) + else: + data_dict = json["anime"] + return Anime(data_dict, self.__network, api_url=self.__API_URL,) def add_recommendation(self, recommended_anime_id: int): """Adds the user's reccomendation for the anime. @@ -292,7 +350,9 @@ def add_recommendation(self, recommended_anime_id: int): "detail_id": str(self.anime_id), "recommendation": str(recommended_anime_id), } - return self.__network.post(data) + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json def get_dict(self): """Gets the JSON response in the form of a dictionary that was used to @@ -337,16 +397,11 @@ def __init__(self, data_dict, network, api_url, anime_id, anime_title=None): self.__aniwatch_episode = None self.__m3u8 = None - def __generate_referer(self): - return f"https://aniwatch.me/anime/{self.anime_id}/{self.number}" - - def __get_decrypt_key(self, url): - res = self.__network.get(url) - return res.content - - def __decrypt_chunk(self, chunk, key): - decrytor = AES.new(key, AES.MODE_CBC) - return decrytor.decrypt(chunk) + def __generate_default_headers(self): + headers = { + "REFERER": f"https://aniwatch.me/anime/{self.anime_id}/{self.number}", + "X-PATH": f"/anime/{self.anime_id}/{self.ep_id}" + } def get_aniwatch_episode(self, lang="en-US"): """Gets the AniWatchEpisode object associated with the episode. @@ -368,7 +423,10 @@ def get_aniwatch_episode(self, lang="en-US"): "ep_id": self.ep_id, "hoster": "", } - self.__aniwatch_episode = AniWatchEpisode(self.__network.post(data), self.ep_id) + + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + self.__aniwatch_episode = AniWatchEpisode(json, self.ep_id) return self.__aniwatch_episode def get_m3u8(self, quality: str) -> M3U8: @@ -384,28 +442,16 @@ def get_m3u8(self, quality: str) -> M3U8: if self.__m3u8: return self.__m3u8 else: - REFERER = self.__generate_referer() - self.__network.headers.update({"REFERER": REFERER, "ORIGIN": "https://aniwatch.me"}) - aniwatch_episode = self.get_aniwatch_episode() - res = self.__network.get(aniwatch_episode.stream.sources[quality]) - self.__m3u8 = M3U8(res.text) - return self.__m3u8 - - def download_chunk(self, file_name, chunk_num, segment): - try: - os.mkdir("chunks") - except FileExistsError: - pass - with open(f"chunks/{file_name}-{chunk_num}.chunk.ts", "wb") as videofile: - res = requests.get(segment["uri"], cookies=self.__cookies, headers=headers) - chunk = res.content - key_dict = segment.get("key", None) - if key_dict is not None: - key = self.__get_decrypt_key(key_dict["uri"]) - decrypted_chunk = self.__decrypt_chunk(chunk, key) - videofile.write(decrypted_chunk) - else: - videofile.write(chunk) + try: + headers = self.__generate_default_headers() + self.toggle_mark_as_watched() + aniwatch_episode = self.get_aniwatch_episode() + uri = aniwatch_episode.stream.sources[quality] # The uri to the M3U8 file. + res = self.__network.get_with_user_session(uri, headers) + self.__m3u8 = M3U8(res.text) + return self.__m3u8 + except: + return None def download( self, @@ -497,77 +543,6 @@ def download( dlr.remove_chunks() os.chdir(current_path) - def download_without_downloader( - self, - quality: str, - file_name: str = None, - multi_threading: bool = False, - use_ffmpeg: bool = False, - include_intro_chunk: bool = False, - delete_chunks: bool = True, - on_progress=None, - print_progress: bool = True, - ): - if file_name is None: - if self.anime_title is None: - file_name = f"Download-{self.ep_id}" - else: - file_name = f"{self.anime_title[:128]}-{self.number}" # limit anime title lenght to 128 chars so we don't surpass the filename limit - m3u8 = self.get_m3u8(quality) - REFERER = self.__generate_referer() - self.__network.headers.update({"REFERER": REFERER, "ORIGIN": "https://aniwatch.me"}) - chunks_done = 0 - threads = [] - cur_chunk = 0 - if not include_intro_chunk: - for x in m3u8.data["segments"]: - # Remove useless segments (intro) - if "img.aniwatch.me" in x["uri"]: - m3u8.data["segments"].remove(x) - total_chunks = len(m3u8.data["segments"]) - - for segment in m3u8.data["segments"]: - if not multi_threading: - if on_progress: - on_progress.__call__(chunks_done, total_chunks) - self.download_chunk(file_name, chunks_done, segment) - chunks_done += 1 - if print_progress: - print(f"{chunks_done}/{total_chunks} done.") - else: - threads.append(Process(target=self.download_chunk, args=(file_name, cur_chunk, segment,),)) - cur_chunk += 1 - if multi_threading: - for p in threads: - p.start() - print(f"[{datetime.now()}] Started download.") - for p in threads: - p.join() - print(f"[{datetime.now()}] Download finishing.") - if use_ffmpeg: - print("Merging chunks into mp4.") - concat = '"concat' - for x in range(0, total_chunks): - if x == 0: - concat += f":chunks/{file_name}-{x}.chunk.ts" - else: - concat += f"|chunks/{file_name}-{x}.chunk.ts" - concat += '"' - subprocess.run(f'ffmpeg -i {concat} -c copy "{file_name}.mp4"') - - else: - print("Merging chunks into mp4") - with open(f"{file_name}.mp4", "wb") as merged: - for ts_file in [ - f"chunks/{file_name}-{x}.chunk.ts" for x in range(0, total_chunks) - ]: - with open(ts_file, "rb") as ts: - shutil.copyfileobj(ts, merged) - if delete_chunks: - for x in range(0, total_chunks): - # Remove chunk files - os.remove(f"chunks/{file_name}-{x}.chunk.ts") - def get_available_qualities(self): """Gets a list of available qualities for the episode. @@ -576,7 +551,7 @@ def get_available_qualities(self): :rtype: list[str] """ aniwatch_episode = self.get_aniwatch_episode() - return list(aniwatch_episode.stream.sources.keys()) + return tuple(aniwatch_episode.stream.sources.keys()) def toggle_mark_as_watched(self): """Toggles the "mark as watched" status of the episode @@ -590,7 +565,9 @@ def toggle_mark_as_watched(self): "detail_id": str(self.anime_id), "episode_id": self.ep_id, } - return self.__network.post(data)["success"] + headers = self.__generate_default_headers() + json = self.__network.post(data, headers) + return json["success"] def __repr__(self): return f"" diff --git a/Sakurajima/models/user_models.py b/Sakurajima/models/user_models.py index c415572..6318e79 100644 --- a/Sakurajima/models/user_models.py +++ b/Sakurajima/models/user_models.py @@ -178,6 +178,8 @@ def get_chronicle(self, page=1): class FriendRequestIncoming(object): + """Represents a friend requests that the user has recieved. + """ def __init__(self, network, data_dict): self.__network = network self.username = data_dict.get("username", None) @@ -186,6 +188,11 @@ def __init__(self, network, data_dict): self.date = data_dict.get("date", None) def accept(self): + """Accepts the friend request. + + :return: True if the operation was successful, False if an error occured. + :rtype: :class:`bool` + """ data = { "controller": "Profile", "action": "acceptRequest", @@ -194,6 +201,11 @@ def accept(self): return self.__network.post(data)["success"] def decline(self): + """Declines the friend request. + + :return: True if the operation was successful, False if an error occured. + :rtype: :class:`bool` + """ data = { "controller": "Profile", "action": "rejectRequest", @@ -206,6 +218,8 @@ def __repr__(self): class FriendRequestOutgoing(object): + """Represents a friend request that the user has sent. + """ def __init__(self, network, data_dict): self.__network = network self.username = data_dict.get("username", None) @@ -214,12 +228,17 @@ def __init__(self, network, data_dict): self.date = data_dict.get("date", None) def withdraw(self): + """Withdraws the friend request. + + :return: True if the operation was successful, False if an error occured. + :rtype: :class:`bool` + """ data = { "controller": "Profile", "action": "withdrawRequest", "friend_id": self.user_id, } - return self.__network.post(data) + return self.__network.post(data)["success"] def __repr__(self): return f"" \ No newline at end of file diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py new file mode 100644 index 0000000..a29f70d --- /dev/null +++ b/Sakurajima/utils/decrypter_provider.py @@ -0,0 +1,44 @@ +from Crypto.Cipher import AES + +class DecrypterProvider(object): + + def __init__(self, network, m3u8, get_by_comparison = False): + self.__network = network + self.m3u8 = m3u8 + self.key = None + self.uri = self.m3u8.data["keys"][1]["uri"] + if get_by_comparison: + self.get_key_by_comparison() + else: + self.get_key() + + def get_key_by_comparison(self) -> bytearray: + if self.key == None: + key1 = bytearray(self.__network.get(self.uri).content) + key2 = key1 + tries = 1 + while key1 == key2 and tries <=25: + key2 = bytearray(self.__network.get(self.uri).content) + tries += 1 + final_key = [] + for index in range(len(key1)): + smaller = min(key1[index], key2[index]) + final_key.append(smaller) + self.key = bytearray(final_key) + return self.key + + def get_key(self) -> bytearray: + if self.key == None: + self.key = bytearray(self.__network.get(self.uri).content) + return self.key + + @staticmethod + def create_initialization_vector(chunk_number) -> bytearray: + iv = [0 for _ in range(0, 16)] + for i in range(12, 16): + iv[i] = chunk_number[0] >> 8 * (15 - i) & 255 + return bytearray(iv) + + def get_decrypter(self, chunk_number) -> AES: + iv = self.create_initialization_vector(chunk_number) + return AES.new(self.get_key(), AES.MODE_CBC, iv = iv) \ No newline at end of file diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index caf9aed..c4e9d15 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -1,12 +1,18 @@ import os from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad from Sakurajima.utils.merger import ChunkMerger, FFmpegMerger, ChunkRemover from threading import Thread, Lock from progress.bar import IncrementalBar from Sakurajima.utils.progress_tracker import ProgressTracker - +from Sakurajima.utils.decrypter_provider import DecrypterProvider +from concurrent.futures import ThreadPoolExecutor class Downloader(object): + """ + Facilitates downloading an episode from aniwatch.me using a single thread. + + """ def __init__( self, network, @@ -18,6 +24,29 @@ def __init__( delete_chunks: bool = True, on_progress=None, ): + """ + :param network: The Sakurajima :class:`Network` object that is used to make network requests. + :type network: :class:`Network` + :param m3u8: The M3U8 data of the episode that is to be downloaded. + :type m3u8: :class:`M3U8` + :param file_name: The name of the downloaded video file. + :type file_name: str + :param episode_id: The episode ID of the episode being downloaded. + This is only required to uniquely identify the progree + tracking data of the episode. + :type episode_id: int + :param use_ffmpeg: Whether to use ``ffmpeg`` to merge the downlaoded chunks, defaults to True + :type use_ffmpeg: bool, optional + :param include_intro: Whether to include the 5 second aniwatch intro, defaults to False + :type include_intro: bool, optional + :param delete_chunks: Whether to delete the downloaded chunks after that have been + merged into a single file, defaults to True + :type delete_chunks: bool, optional + :param on_progress: Register a function that is called every time a chunk is downloaded, the function + passed the chunk number of the downloaded chunk and the total number of chunks as + parameters, defaults to None + :type on_progress: ``function``, optional + """ self.__network = network self.m3u8 = m3u8 self.file_name = file_name @@ -39,11 +68,21 @@ def init_tracker(self): ) def download(self): + """Runs the downloader and starts downloading the video file. + """ + chunk_tuple_list = [] + # Will hold a list of tuples of the form (chunk_number, chunk). + # The chunk_number in this list will start from 1. + for chunk_number, chunk in enumerate(self.m3u8.data["segments"]): + chunk_tuple_list.append((chunk_number, chunk)) + if not self.include_intro: - for segment in self.m3u8.data["segments"]: - if "img.aniwatch.me" in segment["uri"]: - self.m3u8.data["segments"].remove(segment) - self.total_chunks = len(self.m3u8.data["segments"]) + for chunk_tuple in chunk_tuple_list: + # Check if the string is in the URI of the chunk + if "img.aniwatch.me" in chunk_tuple[1]["uri"]: + # Revome the tuple from the tuple list. + chunk_tuple_list.remove(chunk_tuple) + self.total_chunks = len(chunk_tuple_list) try: os.makedirs("chunks") @@ -52,10 +91,18 @@ def download(self): self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() - - for chunk_number, segment in enumerate(self.m3u8.data["segments"]): + decryter_provider = DecrypterProvider(self.__network, self.m3u8) + for chunk_number, chunk_tuple in enumerate(chunk_tuple_list): + # We need the chunk number here to name the files. Note that this is + # different from the chunk number that is inside the tuple. file_name = f"chunks\/{self.file_name}-{chunk_number}.chunk.ts" - ChunkDownloader(self.__network, segment, file_name).download() + ChunkDownloader( + self.__network, + chunk_tuple[1], # The segment data + file_name, + chunk_tuple[0], # The chunk number needed for decryption. + decryter_provider + ).download() self.progress_bar.next() self.progress_tracker.update_chunks_done(chunk_number) if self.on_progress: @@ -64,43 +111,64 @@ def download(self): self.progress_bar.finish() def merge(self): + """Merges the downloaded chunks into a single file. + """ if self.use_ffmpeg: FFmpegMerger(self.file_name, self.total_chunks).merge() else: ChunkMerger(self.file_name, self.total_chunks).merge() def remove_chunks(self): + """Deletes the downloaded chunks. + """ ChunkRemover(self.file_name, self.total_chunks).remove() class ChunkDownloader(object): - def __init__(self, network, segment, file_name): + """ + The object that actually downloads a single chunk. + """ + def __init__(self, network, segment, file_name, chunk_number, decrypt_provider: DecrypterProvider): + """ + :param network: The Sakurajima :class:`Network` object that is used to make network requests. + :type network: :class:`Network` + :param segment: The segement data from that M3U8 file that is to be downloaded. + :type segment: :class:`dict` + :param file_name: The file name of the downloaded chunk. + :type file_name: :class:`str` + :param chunk_number: The chunk number of the the chunk to be downloaded, required to generate + the AES decryption initialization vector. + :type chunk_number: int + """ self.__network = network self.segment = segment self.file_name = file_name + self.chunk_number = chunk_number, + self.decrypter_provider = decrypt_provider def download(self): + """Starts downloading the chunk. + """ with open(self.file_name, "wb") as videofile: res = self.__network.get(self.segment["uri"]) chunk = res.content key_dict = self.segment.get("key", None) + if key_dict is not None: - key = self.get_decrypt_key(key_dict["uri"]) - decrypted_chunk = self.decrypt_chunk(chunk, key) + decrypted_chunk = self.decrypt_chunk(chunk) videofile.write(decrypted_chunk) else: videofile.write(chunk) - - def get_decrypt_key(self, uri): - res = self.__network.get(uri) - return res.content - - def decrypt_chunk(self, chunk, key): - decryptor = AES.new(key, AES.MODE_CBC) - return decryptor.decrypt(chunk) + + def decrypt_chunk(self, chunk): + decryter = self.decrypter_provider.get_decrypter(self.chunk_number) + return decryter.decrypt(chunk) class MultiThreadDownloader(object): + """ + Facilitates downloading an episode from aniwatch.me using multiple threads. + """ def __init__( self, network, @@ -112,27 +180,37 @@ def __init__( include_intro: bool = False, delete_chunks: bool = True, ): + """ + :param network: The Sakurajima :class:`Network` object that is used to make network requests. + :type network: :class:`Network` + :type m3u8: :class:`M3U8` + :param file_name: The name of the downloaded video file. + :type file_name: str + :param episode_id: The episode ID of the episode being downloaded. + This is only required to uniquely identify the progree + tracking data of the episode. + :type episode_id: int + :param max_threads: The maximum number of threads that will be used for downloading, defaults to None, + if None, the maximum possible number of threads will be used. + :type max_threads: int, optional + :param use_ffmpeg: Whether to use ``ffmpeg`` to merge the downlaoded chunks, defaults to True + :type use_ffmpeg: bool, optional + :param include_intro: Whether to include the 5 second aniwatch intro, defaults to False + :type include_intro: bool, optional + :param delete_chunks: Whether to delete the downloaded chunks after that have been + merged into a single file, defaults to True + :type delete_chunks: bool, optional + """ self.__network = network self.m3u8 = m3u8 self.file_name = file_name self.use_ffmpeg = use_ffmpeg + self.max_threads = max_threads self.include_intro = include_intro self.delete_chunks = delete_chunks self.threads = [] self.progress_tracker = ProgressTracker(episode_id) self.__lock = Lock() - - if not include_intro: - for segment in self.m3u8.data["segments"]: - if "img.aniwatch.me" in segment["uri"]: - self.m3u8.data["segments"].remove(segment) - self.total_chunks = len(self.m3u8.data["segments"]) - - if max_threads is None: - self.max_threads = self.total_chunks - else: - self.max_threads = max_threads - try: os.makedirs("chunks") except FileExistsError: @@ -149,59 +227,88 @@ def init_tracker(self): } ) - def start_threads(self): - for t in self.threads: - t.start() - for t in self.threads: - t.join() - def reset_threads(self): - self.threads = [] + def assign_segments(self, segment): - def assign_target(self, network, segment, file_name, chunk_number): - ChunkDownloader(network, segment, file_name).download() + ChunkDownloader( + segment.network, + segment.segment, + segment.file_name, + segment.chunk_number, + segment.decrypter_provider + ).download() with self.__lock: - self.progress_tracker.update_chunks_done(chunk_number) + self.progress_tracker.update_chunks_done(segment.chunk_number) self.progress_bar.next() def download(self): - stateful_segment_list = StatefulSegmentList(self.m3u8.data["segments"]) + """Runs the downloader and starts downloading the video file. + """ + + decrypter_provider = DecrypterProvider(self.__network, self.m3u8) + chunk_tuple_list = [] + # Will hold a list of tuples of the form (chunk_number, chunk). + # The chunk_number in this list will start from 1. + for chunk_number, chunk in enumerate(self.m3u8.data["segments"]): + chunk_tuple_list.append((chunk_number, chunk)) + + if not self.include_intro: + for chunk_tuple in chunk_tuple_list: + # Check if the string is in the URI of the chunk + if "img.aniwatch.me" in chunk_tuple[1]["uri"]: + # Revome the tuple from the tuple list. + chunk_tuple_list.remove(chunk_tuple) + + self.total_chunks = len(chunk_tuple_list) self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() - while True: - try: - for _ in range(self.max_threads): - chunk_number, segment = stateful_segment_list.next() - file_name = f"chunks\/{self.file_name}-{chunk_number}.chunk.ts" - self.threads.append( - Thread(target=self.assign_target, args=(self.__network, segment, file_name, chunk_number),) - ) - self.start_threads() - self.reset_threads() - except IndexError: - if self.threads != []: - self.start_threads() - self.reset_threads() - break + + segment_wrapper_list = [] + + for chunk_number, chunk in enumerate(chunk_tuple_list): + file_name = f"chunks\/{self.file_name}-{chunk_number}.chunk.ts" + segment_wrapper = _SegmentWrapper( + self.__network, + chunk[1], # Segment data. + file_name, + chunk[0], # The chunk number needed for decryption. + decrypter_provider + ) + segment_wrapper_list.append(segment_wrapper) + + if self.max_threads == None: + # If the value for max threads is not provided, then it is set to + # the total number of chunks that are to be downloaded. + self.max_threads = self.total_chunks + + self.executor = ThreadPoolExecutor(max_workers = self.max_threads) + + with self.executor as exe: + futures = exe.map(self.assign_segments, segment_wrapper_list) + for future in futures: + # This loop servers to run the generator. + pass self.progress_bar.finish() def merge(self): + """Merges the downloaded chunks into a single file. + """ if self.use_ffmpeg: FFmpegMerger(self.file_name, self.total_chunks).merge() else: ChunkMerger(self.file_name, self.total_chunks).merge() def remove_chunks(self): + """Deletes the downloaded chunks. + """ ChunkRemover(self.file_name, self.total_chunks).remove() - -class StatefulSegmentList(object): - def __init__(self, segment_list): - self.segment_list = segment_list - self.index = 0 - - def next(self): - segment = self.segment_list[self.index] - index = self.index - self.index += 1 - return index, segment +class _SegmentWrapper(object): + # As the name suggests, this is only wrapper class introduced with a hope that it + # will lead to more readable code. + def __init__(self, network, segment, file_name, chunk_number, decrypter_provider): + self.network = network + self.segment = segment + self.file_name = file_name + self.chunk_number = chunk_number + self.decrypter_provider = decrypter_provider diff --git a/Sakurajima/utils/episode_list.py b/Sakurajima/utils/episode_list.py index 0099236..1899eb9 100644 --- a/Sakurajima/utils/episode_list.py +++ b/Sakurajima/utils/episode_list.py @@ -2,6 +2,10 @@ class EpisodeList(object): + """An :class:`EpisodeList` is very similar to a normal list. You can do everything + with a :class:`EpisodeList` that you can with a normal list. The only difference is that + an EpisodeList has some convinience methods that make selecting a particular episode easier. + """ def __init__(self, episode_list): self.validate_list(episode_list) self.__episode_list = episode_list @@ -15,30 +19,58 @@ def validate_list(self, episode_list): "EpisodeList only take in lists that contain only Episode objects" ) - def get_episode_by_number(self, episode_number): - result = list( - filter( - lambda episode: True if episode.number == episode_number else False, - self.__episode_list, - ) - ) - if len(result) == 0: - return None - else: - return result[0] - - def get_episode_by_title(self, title): - result = list( - filter( - lambda episode: True if episode.title == title else False, - self.__episode_list, - ) - ) - if len(result) == 0: - return None - else: - return result[0] + def get_episode_by_number(self, episode_number: int): + """Returns the first :class:`Episode` object from the list whose ``number`` attribue matches the + ``episode_number`` parameter. + :param episode_number: The episode number that you want to find in the list. + :type episode_number: int + + :rtype: :class:`Episode` + """ + def check_episode_number(episode): + if episode.number == episode_number: + return True + else: + return False + + result = None + for episode in self.__episode_list: + if check_episode_number(episode): + result = episode + break + return result + + def get_episode_by_title(self, title: str): + """Returns the first :class:`Episode` object from the list whose ``title`` attribue matches the + ``title`` parameter. + + :param title: The title of the episode that you want to find. + :type title: str + + :rtype: :class:`Episode` + """ + def check_episode_title(episode): + if episode.title == title: + return True + else: + return False + + result = None + + for episode in self.__episode_list: + if check_episode_title(episode): + result = episode + break + return result + + def last(self): + """Returns the last :class:`Episode` object from the list. + + :rtype: :class:`Episode` + """ + return self.__episode_list[:-1] + def __getitem__(self, position): if isinstance(position, int): return self.__episode_list[position] diff --git a/Sakurajima/utils/merger.py b/Sakurajima/utils/merger.py index 145fd61..3e7190e 100644 --- a/Sakurajima/utils/merger.py +++ b/Sakurajima/utils/merger.py @@ -4,11 +4,23 @@ class ChunkMerger(object): + """Merges the downloaded chunks by concatinating them into a single file. + """ def __init__(self, file_name, total_chunks): + """ + :param file_name: The file name prefix of the chunks. + :type file_name: str + :param total_chunks: The total number of chunks. The merger assumes thet the chunks are + in a ``chunks`` directory and are named according to a sequence + i.e. "{file_name}-{chunk_number}.chunk.ts" + :type total_chunks: int + """ self.file_name = file_name self.total_chunks = total_chunks def merge(self): + """Starts the merger and creates a single file ``.mp4`` file. + """ with open(f"{self.file_name}.mp4", "wb") as merged_file: for ts_file in [ open(f"chunks\/{self.file_name}-{chunk_number}.chunk.ts") @@ -18,11 +30,17 @@ def merge(self): class FFmpegMerger(object): + """Merges the downloaded chunks using ``ffmpeg``. + """ def __init__(self, file_name, total_chunks): + """ + The parameters have the same meaning as in :class:`ChunkMerger`""" self.file_name = file_name self.total_chunks = total_chunks def merge(self): + """Starts the merger and creates a single file ``.mp4`` file. + """ print("Merging chunks into mp4.") concat = '"concat' for x in range(0, self.total_chunks): diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 1fd3785..637c6f4 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -1,52 +1,75 @@ from Sakurajima.utils.misc import Misc from requests import Session - +import urllib.parse class Network: def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoint): self.API_URL = endpoint - self.session = Session() + self.session = Session() + # This session will have all the details that are required to access the API + self.userless_session = Session() + # This session will only have "USER-AGENT" and "REFERER" self.session.proxies = proxies self.headers = self.session.headers # Expose session headers - self.headers["referer"] = "https://aniwatch.me/" self.cookies = self.session.cookies # Expose session cookies - xsrf_token = Misc().generate_xsrf_token() - headers = { - "x-xsrf-token": xsrf_token, - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", - } - cookies = {"xsrf-token": xsrf_token} + self.xsrf_token = Misc().generate_xsrf_token() if username is not None and user_id is not None and user_id is not None: - headers["x-auth"] = auth_token - session_token = ( + session_token = urllib.parse.quote( '{"userid":' + str(user_id) + ',"username":"' + str(username) - + '","usergroup":4,"player_lang":1,"player_quality":0,"player_time_left_side":2,"player_time_right_side":3,"screen_orientation":1,"nsfw":1,"chrLogging":1,"mask_episode_info":0,"blur_thumbnails":0,"autoplay":1,"preview_thumbnails":1,"update_watchlist":1,"playheads":1,"seek_time":5,"cover":null,"title":"Member","premium":1,"lang":"en-US","auth":"' + + '","usergroup":4,"player_lang":1,"player_quality":0,"player_time_left_side":2,"player_time_right_side":3,"screen_orientation":1,"nsfw":1,"chrLogging":1,"mask_episode_info":0,"blur_thumbnails":0,"autoplay":1,"preview_thumbnails":1,"update_watchlist":1,"update_watchlist_notification":1,"playheads":1,"seek_time":5,"update_watchlist_percentage":80,"cover":null,"title":"Member","premium":1,"lang":"en-US","auth":"' + str(auth_token) + '","remember_login":true}' ) - cookies["SESSION"] = session_token - headers["COOKIE"] = f"SESSION={session_token}; XSRF-TOKEN={xsrf_token};" + chat_cookie = '%7B%22userlist_collapsed%22%3Atrue%2C%22scroll_msg%22%3Atrue%2C%22show_time%22%3Atrue%2C%22parse_smileys%22%3Atrue%2C%22show_system_msg%22%3Atrue%2C%22new_msg_beep_sound%22%3Afalse%2C%22auto_connect%22%3Atrue%2C%22retry_reconnection%22%3Atrue%7D' + headers = { + "ORIGIN": "https://aniwatch.me/", + "REFERER": "https://aniwatch.me/", + "X-XSRF-TOKEN": self.xsrf_token, + "USER-AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", + "COOKIE": f"SESSION={session_token}; XSRF-TOKEN={self.xsrf_token}; ANIWATCH_CHAT_SETTINGS={chat_cookie};", + "X-AUTH": auth_token + } + + cookies = { + "ANIWATCH_CHAT_SETTINGS": chat_cookie, + "SESSION": session_token, + "XSRF-TOKEN": self.xsrf_token + } + self.session.headers.update(headers) self.session.cookies.update(cookies) - + self.userless_session.headers.update( + { + "USER-AGENT": headers["USER-AGENT"], + "ORIGIN": headers["ORIGIN"], + "REFERER": headers["REFERER"] + } + ) def __repr__(self): return "" - def post(self, data): + def post(self, data, headers): try: - res = self.session.post(self.API_URL, json=data) + res = self.session.post(self.API_URL, json=data, headers = headers) return res.json() except Exception as e: self.session.close() raise e - def get(self, uri): + def get_with_user_session(self, uri, headers): try: - res = self.session.get(uri) + res = self.session.get(uri, headers = headers) return res except Exception as e: self.session.close() raise e + + def get(self, uri, headers = None): + try: + res = self.userless_session.get(uri, headers = headers) + return res + except Exception as e: + raise(e) \ No newline at end of file diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle index 08b2b24..b64a1f6 100644 Binary files a/docs/build/doctrees/environment.pickle and b/docs/build/doctrees/environment.pickle differ diff --git a/docs/build/doctrees/index.doctree b/docs/build/doctrees/index.doctree index 31d8b7f..d2306d8 100644 Binary files a/docs/build/doctrees/index.doctree and b/docs/build/doctrees/index.doctree differ diff --git a/docs/build/doctrees/models/friend_request_incoming.doctree b/docs/build/doctrees/models/friend_request_incoming.doctree new file mode 100644 index 0000000..1862728 Binary files /dev/null and b/docs/build/doctrees/models/friend_request_incoming.doctree differ diff --git a/docs/build/doctrees/models/friend_request_outgoing.doctree b/docs/build/doctrees/models/friend_request_outgoing.doctree new file mode 100644 index 0000000..8a4eb6a Binary files /dev/null and b/docs/build/doctrees/models/friend_request_outgoing.doctree differ diff --git a/docs/build/doctrees/models/models.doctree b/docs/build/doctrees/models/models.doctree index 4399f5f..7c28f25 100644 Binary files a/docs/build/doctrees/models/models.doctree and b/docs/build/doctrees/models/models.doctree differ diff --git a/docs/build/doctrees/models/user_overview_stats.doctree b/docs/build/doctrees/models/user_overview_stats.doctree index d860168..74d2028 100644 Binary files a/docs/build/doctrees/models/user_overview_stats.doctree and b/docs/build/doctrees/models/user_overview_stats.doctree differ diff --git a/docs/build/doctrees/sakurajima.doctree b/docs/build/doctrees/sakurajima.doctree index 8f93946..397d5be 100644 Binary files a/docs/build/doctrees/sakurajima.doctree and b/docs/build/doctrees/sakurajima.doctree differ diff --git a/docs/build/doctrees/utils/downloaders.doctree b/docs/build/doctrees/utils/downloaders.doctree new file mode 100644 index 0000000..5723287 Binary files /dev/null and b/docs/build/doctrees/utils/downloaders.doctree differ diff --git a/docs/build/doctrees/utils/episode_list.doctree b/docs/build/doctrees/utils/episode_list.doctree new file mode 100644 index 0000000..297ea2d Binary files /dev/null and b/docs/build/doctrees/utils/episode_list.doctree differ diff --git a/docs/build/doctrees/utils/mergers.doctree b/docs/build/doctrees/utils/mergers.doctree new file mode 100644 index 0000000..9488c1d Binary files /dev/null and b/docs/build/doctrees/utils/mergers.doctree differ diff --git a/docs/build/doctrees/utils/utils.doctree b/docs/build/doctrees/utils/utils.doctree new file mode 100644 index 0000000..0e43429 Binary files /dev/null and b/docs/build/doctrees/utils/utils.doctree differ diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt index 617e452..a797924 100644 --- a/docs/build/html/_sources/index.rst.txt +++ b/docs/build/html/_sources/index.rst.txt @@ -12,6 +12,7 @@ Welcome to Sakurajima's documentation! sakurajima models/models + utils/utils Indices and tables diff --git a/docs/build/html/_sources/models/friend_request_incoming.rst.txt b/docs/build/html/_sources/models/friend_request_incoming.rst.txt new file mode 100644 index 0000000..761f2ed --- /dev/null +++ b/docs/build/html/_sources/models/friend_request_incoming.rst.txt @@ -0,0 +1,7 @@ +FriendRequestIncoming +===================== + +.. module:: Sakurajima.models.user_models + +.. autoclass:: FriendRequestIncoming + :members: \ No newline at end of file diff --git a/docs/build/html/_sources/models/friend_request_outgoing.rst.txt b/docs/build/html/_sources/models/friend_request_outgoing.rst.txt new file mode 100644 index 0000000..8babfeb --- /dev/null +++ b/docs/build/html/_sources/models/friend_request_outgoing.rst.txt @@ -0,0 +1,7 @@ +FriendRequestOutgoing +===================== + +.. module:: Sakurajima.models.user_models + +.. autoclass:: FriendRequestOutgoing + :members: \ No newline at end of file diff --git a/docs/build/html/_sources/models/models.rst.txt b/docs/build/html/_sources/models/models.rst.txt index 4bd4b25..5eeda0c 100644 --- a/docs/build/html/_sources/models/models.rst.txt +++ b/docs/build/html/_sources/models/models.rst.txt @@ -19,4 +19,6 @@ Models user_overview user_overview_stats user_overview_watch_type - friend \ No newline at end of file + friend + friend_request_incoming + friend_request_outgoing \ No newline at end of file diff --git a/docs/build/html/_sources/utils/downloaders.rst.txt b/docs/build/html/_sources/utils/downloaders.rst.txt new file mode 100644 index 0000000..75a19a7 --- /dev/null +++ b/docs/build/html/_sources/utils/downloaders.rst.txt @@ -0,0 +1,14 @@ +Downloaders +=========== + +.. module:: Sakurajima.utils.downloader + +.. autoclass:: Downloader + :members: + :autoclass_content: both + +.. autoclass:: ChunkDownloader + :members: + +.. autoclass:: MultiThreadDownloader + :members: \ No newline at end of file diff --git a/docs/build/html/_sources/utils/episode_list.rst.txt b/docs/build/html/_sources/utils/episode_list.rst.txt new file mode 100644 index 0000000..e383562 --- /dev/null +++ b/docs/build/html/_sources/utils/episode_list.rst.txt @@ -0,0 +1,7 @@ +EpisodeList +=========== + +.. module:: Sakurajima.utils.episode_list + +.. autoclass:: EpisodeList + :members: \ No newline at end of file diff --git a/docs/build/html/_sources/utils/mergers.rst.txt b/docs/build/html/_sources/utils/mergers.rst.txt new file mode 100644 index 0000000..d7c4ea2 --- /dev/null +++ b/docs/build/html/_sources/utils/mergers.rst.txt @@ -0,0 +1,10 @@ +Mergers +======= + +.. module:: Sakurajima.utils.merger + +.. autoclass:: ChunkMerger + :members: + +.. autoclass:: FFmpegMerger + :members: \ No newline at end of file diff --git a/docs/build/html/_sources/utils/utils.rst.txt b/docs/build/html/_sources/utils/utils.rst.txt new file mode 100644 index 0000000..357149c --- /dev/null +++ b/docs/build/html/_sources/utils/utils.rst.txt @@ -0,0 +1,9 @@ +Utils +===== + +.. toctree:: + :maxdepth: 2 + + downloaders + episode_list + mergers \ No newline at end of file diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html index edc2a65..0b91960 100644 --- a/docs/build/html/genindex.html +++ b/docs/build/html/genindex.html @@ -84,6 +84,7 @@ @@ -174,6 +175,8 @@

Index

A

+ - - + @@ -300,11 +315,13 @@

E

  • Episode (class in Sakurajima.models.base_models)
  • - - + - +
  • favorites (Sakurajima.models.media.MediaEntry attribute)
  • -
    @@ -373,6 +398,10 @@

    G

  • get_dict() (Sakurajima.models.base_models.Anime method)
  • get_episode() (Sakurajima.api.Sakurajima method) +
  • +
  • get_episode_by_number() (Sakurajima.utils.episode_list.EpisodeList method) +
  • +
  • get_episode_by_title() (Sakurajima.utils.episode_list.EpisodeList method)
  • get_episodes() (Sakurajima.api.Sakurajima method) @@ -385,11 +414,11 @@

    G

  • get_latest_releases() (Sakurajima.api.Sakurajima method)
  • get_latest_uploads() (Sakurajima.api.Sakurajima method) -
  • -
  • get_m3u8() (Sakurajima.models.base_models.Episode method)
  • mean_score (Sakurajima.models.user_models.UserOverviewStats attribute)
  • - - +
  • movie (Sakurajima.models.user_models.UserOverview attribute) +
  • +
  • MultiThreadDownloader (class in Sakurajima.utils.downloader)
  • @@ -629,6 +676,8 @@

    R

  • (Sakurajima.models.chronicle.ChronicleEntry method)
  • +
  • remove_chunks() (Sakurajima.utils.downloader.MultiThreadDownloader method) +
  • remove_from_list() (Sakurajima.api.Sakurajima method)
      @@ -689,8 +738,6 @@

      S

    • module
  • - -
    • Sakurajima.models.relation @@ -705,11 +752,34 @@

      S

    • module
    + + + +
  • Utils
  • diff --git a/docs/build/html/models/friend.html b/docs/build/html/models/friend.html index 220a525..ed6dabe 100644 --- a/docs/build/html/models/friend.html +++ b/docs/build/html/models/friend.html @@ -36,6 +36,7 @@ + @@ -101,6 +102,8 @@
  • UserOverviewStats
  • UserOverviewWatchType
  • Friend
  • +
  • FriendRequestIncoming
  • +
  • FriendRequestOutgoing
  • @@ -234,6 +237,8 @@ diff --git a/docs/build/html/models/user_anime_list_entry.html b/docs/build/html/models/user_anime_list_entry.html index ff6ac35..43c274e 100644 --- a/docs/build/html/models/user_anime_list_entry.html +++ b/docs/build/html/models/user_anime_list_entry.html @@ -102,6 +102,8 @@
  • UserOverviewStats
  • UserOverviewWatchType
  • Friend
  • +
  • FriendRequestIncoming
  • +
  • FriendRequestOutgoing
  • diff --git a/docs/build/html/models/user_overview.html b/docs/build/html/models/user_overview.html index e8a8da2..e5d84e7 100644 --- a/docs/build/html/models/user_overview.html +++ b/docs/build/html/models/user_overview.html @@ -102,6 +102,8 @@
  • UserOverviewStats
  • UserOverviewWatchType
  • Friend
  • +
  • FriendRequestIncoming
  • +
  • FriendRequestOutgoing
  • diff --git a/docs/build/html/models/user_overview_stats.html b/docs/build/html/models/user_overview_stats.html index bd5b816..645c27b 100644 --- a/docs/build/html/models/user_overview_stats.html +++ b/docs/build/html/models/user_overview_stats.html @@ -102,6 +102,8 @@
  • UserOverviewStats
  • UserOverviewWatchType
  • Friend
  • +
  • FriendRequestIncoming
  • +
  • FriendRequestOutgoing
  • @@ -176,7 +178,7 @@
    class Sakurajima.models.user_models.UserOverviewStats(data_dict)
    -

    Hold data regarding the watch stats of a user.

    +

    Holds data regarding the watch stats of a user.

    mean_score
    diff --git a/docs/build/html/models/user_overview_watch_type.html b/docs/build/html/models/user_overview_watch_type.html index ac945d1..f845283 100644 --- a/docs/build/html/models/user_overview_watch_type.html +++ b/docs/build/html/models/user_overview_watch_type.html @@ -102,6 +102,8 @@
  • UserOverviewStats
  • UserOverviewWatchType
  • Friend
  • +
  • FriendRequestIncoming
  • +
  • FriendRequestOutgoing
  • diff --git a/docs/build/html/objects.inv b/docs/build/html/objects.inv index 58047aa..6ae09e6 100644 Binary files a/docs/build/html/objects.inv and b/docs/build/html/objects.inv differ diff --git a/docs/build/html/py-modindex.html b/docs/build/html/py-modindex.html index ac82458..5d171ac 100644 --- a/docs/build/html/py-modindex.html +++ b/docs/build/html/py-modindex.html @@ -87,6 +87,7 @@ @@ -210,6 +211,21 @@

    Python Module Index

        Sakurajima.models.user_models + + +     + Sakurajima.utils.downloader + + + +     + Sakurajima.utils.episode_list + + + +     + Sakurajima.utils.merger + diff --git a/docs/build/html/sakurajima.html b/docs/build/html/sakurajima.html index c8e419f..a9b5222 100644 --- a/docs/build/html/sakurajima.html +++ b/docs/build/html/sakurajima.html @@ -230,6 +230,21 @@
    +
    + +

    An alternate constructor that reads a cookie file and automatically extracts +the data neccasary to initialize Sakurajime

    +
    +
    Parameters
    +

    cookie_file (str) – The file containing the cookie.

    +
    +
    Return type
    +

    Sakurajima

    +
    +
    +
    +
    get_airing_anime(randomize=False)
    @@ -320,7 +335,7 @@

    An AniWatchEpisode object which has data like streams and lamguages.

    Return type
    -

    AniWatchEpisode

    +

    AniWatchEpisode

    @@ -340,7 +355,7 @@ a normal list. Check out the EpisodeList documentation for further details.

    Return type
    -

    EpisodeList

    +

    EpisodeList

    @@ -402,7 +417,7 @@ that each contain a list of MediaEntry objects representing the respective media.

    Return type
    -

    Media

    +

    Media

    @@ -417,7 +432,7 @@ data like date, notification ID and the content etc.

    Return type
    -

    list[Notification]

    +

    list[Notification]

    @@ -500,7 +515,7 @@ single recommendation.

    Return type
    -

    list[RecommendationEntry]

    +

    list[RecommendationEntry]

    @@ -517,7 +532,7 @@

    A Relation object that contains all the details of a relation.

    Return type
    -

    Relation

    +

    Relation

    @@ -553,7 +568,7 @@

    An AniwatchStats object which wraps all the relevant statistics.

    Return type
    -

    AniwatchStats

    +

    AniwatchStats

    @@ -567,7 +582,7 @@

    A list of Notification objects.

    Return type
    -

    list[Notification]

    +

    list[Notification]

    @@ -583,7 +598,7 @@ contains information like the status, progress and total episodes etc.

    Return type
    -

    list[UserAnimeListEntry]

    +

    list[UserAnimeListEntry]

    @@ -640,7 +655,7 @@
    Return type
    -

    UserOverview

    +

    UserOverview

    diff --git a/docs/build/html/search.html b/docs/build/html/search.html index d5c527a..9f36fef 100644 --- a/docs/build/html/search.html +++ b/docs/build/html/search.html @@ -86,6 +86,7 @@ diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js index 2ad06e1..6ade445 100644 --- a/docs/build/html/searchindex.js +++ b/docs/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index","models/anime","models/aniwatch_episode","models/aniwatch_stats","models/chronicle_entry","models/episode","models/friend","models/media","models/media_entry","models/models","models/notification","models/recommendation_entry","models/relation","models/relation_entry","models/user_anime_list_entry","models/user_overview","models/user_overview_stats","models/user_overview_watch_type","sakurajima"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst","models\\anime.rst","models\\aniwatch_episode.rst","models\\aniwatch_stats.rst","models\\chronicle_entry.rst","models\\episode.rst","models\\friend.rst","models\\media.rst","models\\media_entry.rst","models\\models.rst","models\\notification.rst","models\\recommendation_entry.rst","models\\relation.rst","models\\relation_entry.rst","models\\user_anime_list_entry.rst","models\\user_overview.rst","models\\user_overview_stats.rst","models\\user_overview_watch_type.rst","sakurajima.rst"],objects:{"Sakurajima.api":{Sakurajima:[18,1,1,""]},"Sakurajima.api.Sakurajima":{add_recommendation:[18,2,1,""],delete_all_notifications:[18,2,1,""],delete_notification:[18,2,1,""],favorite_media:[18,2,1,""],get_airing_anime:[18,2,1,""],get_anime:[18,2,1,""],get_anime_chronicle:[18,2,1,""],get_best_rated_anime:[18,2,1,""],get_episode:[18,2,1,""],get_episodes:[18,2,1,""],get_latest_anime:[18,2,1,""],get_latest_releases:[18,2,1,""],get_latest_uploads:[18,2,1,""],get_media:[18,2,1,""],get_notifications:[18,2,1,""],get_popular_anime:[18,2,1,""],get_popular_seasonal_anime:[18,2,1,""],get_popular_upcoming_anime:[18,2,1,""],get_random_anime:[18,2,1,""],get_recommendations:[18,2,1,""],get_relation:[18,2,1,""],get_seasonal_anime:[18,2,1,""],get_stats:[18,2,1,""],get_unread_notifications:[18,2,1,""],get_user_anime_list:[18,2,1,""],get_user_chronicle:[18,2,1,""],get_user_media:[18,2,1,""],get_user_overview:[18,2,1,""],get_watchlist:[18,2,1,""],mark_all_notifications_as_read:[18,2,1,""],mark_as_completed:[18,2,1,""],mark_as_dropped:[18,2,1,""],mark_as_on_hold:[18,2,1,""],mark_as_plan_to_watch:[18,2,1,""],mark_as_watching:[18,2,1,""],rateAnime:[18,2,1,""],remove_chronicle_entry:[18,2,1,""],remove_from_list:[18,2,1,""],report_missing_anime:[18,2,1,""],report_missing_streams:[18,2,1,""],search:[18,2,1,""],toggle_mark_as_watched:[18,2,1,""],toggle_notification_seen:[18,2,1,""],using_proxy:[18,2,1,""]},"Sakurajima.models":{base_models:[2,0,0,"-"],chronicle:[4,0,0,"-"],media:[8,0,0,"-"],notification:[10,0,0,"-"],recommendation:[11,0,0,"-"],relation:[13,0,0,"-"],stats:[3,0,0,"-"],user_models:[17,0,0,"-"]},"Sakurajima.models.base_models":{AniWatchEpisode:[2,1,1,""],Anime:[1,1,1,""],Episode:[5,1,1,""]},"Sakurajima.models.base_models.AniWatchEpisode":{episode_id:[2,3,1,""],languages:[2,3,1,""],stream:[2,3,1,""]},"Sakurajima.models.base_models.Anime":{add_recommendation:[1,2,1,""],get_chronicle:[1,2,1,""],get_complete_object:[1,2,1,""],get_dict:[1,2,1,""],get_episodes:[1,2,1,""],get_media:[1,2,1,""],get_recommendations:[1,2,1,""],get_relations:[1,2,1,""],mark_as_completed:[1,2,1,""],mark_as_dropped:[1,2,1,""],mark_as_on_hold:[1,2,1,""],mark_as_plan_to_watch:[1,2,1,""],mark_as_watching:[1,2,1,""],rate:[1,2,1,""],remove_from_list:[1,2,1,""]},"Sakurajima.models.base_models.Episode":{added:[5,3,1,""],anime_id:[5,3,1,""],anime_title:[5,3,1,""],description:[5,3,1,""],download:[5,2,1,""],duration:[5,3,1,""],ep_id:[5,3,1,""],filler:[5,3,1,""],get_aniwatch_episode:[5,2,1,""],get_available_qualities:[5,2,1,""],get_m3u8:[5,2,1,""],is_aired:[5,3,1,""],lang:[5,3,1,""],number:[5,3,1,""],thumbnail:[5,3,1,""],title:[5,3,1,""],toggle_mark_as_watched:[5,2,1,""],watched:[5,3,1,""]},"Sakurajima.models.chronicle":{ChronicleEntry:[4,1,1,""]},"Sakurajima.models.chronicle.ChronicleEntry":{anime_id:[4,3,1,""],anime_title:[4,3,1,""],chronicle_id:[4,3,1,""],date:[4,3,1,""],ep_title:[4,3,1,""],episode:[4,3,1,""],remove_chronicle_entry:[4,2,1,""]},"Sakurajima.models.media":{Media:[7,1,1,""],MediaEntry:[8,1,1,""]},"Sakurajima.models.media.Media":{anime_id:[7,3,1,""],endings:[7,3,1,""],openings:[7,3,1,""],osts:[7,3,1,""],theme_songs:[7,3,1,""]},"Sakurajima.models.media.MediaEntry":{favorite_media:[8,2,1,""],favorites:[8,3,1,""],id:[8,3,1,""],is_favorited:[8,3,1,""],thumbnail:[8,3,1,""],title:[8,3,1,""],type:[8,3,1,""]},"Sakurajima.models.notification":{Notification:[10,1,1,""]},"Sakurajima.models.notification.Notification":{"delete":[10,2,1,""],content:[10,3,1,""],href:[10,3,1,""],href_blank:[10,3,1,""],id:[10,3,1,""],seen:[10,3,1,""],time:[10,3,1,""],toggle_seen:[10,2,1,""],type:[10,3,1,""]},"Sakurajima.models.recommendation":{RecommendationEntry:[11,1,1,""]},"Sakurajima.models.recommendation.RecommendationEntry":{airing_start:[11,3,1,""],anime_id:[11,3,1,""],cover:[11,3,1,""],cur_episodes:[11,3,1,""],d_status:[11,3,1,""],episodes_max:[11,3,1,""],get_anime:[11,2,1,""],has_special:[11,3,1,""],progress:[11,3,1,""],recommendations:[11,3,1,""],title:[11,3,1,""],type:[11,3,1,""]},"Sakurajima.models.relation":{Relation:[12,1,1,""],RelationEntry:[13,1,1,""]},"Sakurajima.models.relation.Relation":{description:[12,3,1,""],entries:[12,3,1,""],relation_id:[12,3,1,""],title:[12,3,1,""]},"Sakurajima.models.relation.RelationEntry":{airing_start:[13,3,1,""],anime_id:[13,3,1,""],completed:[13,3,1,""],cover:[13,3,1,""],cur_episodes:[13,3,1,""],episodes_max:[13,3,1,""],has_nudity:[13,3,1,""],progress:[13,3,1,""],title:[13,3,1,""],type:[13,3,1,""]},"Sakurajima.models.stats":{AniwatchStats:[3,1,1,""]},"Sakurajima.models.stats.AniwatchStats":{new_registered_users:[3,3,1,""],new_registered_users_graph_data:[3,3,1,""],registered_users:[3,3,1,""],registered_users_graph_data:[3,3,1,""],total_1080p_streams:[3,3,1,""],total_360p_streams:[3,3,1,""],total_480p_streams:[3,3,1,""],total_720p_streams:[3,3,1,""],total_animes:[3,3,1,""],total_hentais:[3,3,1,""],total_movies:[3,3,1,""],total_shows:[3,3,1,""],total_specials:[3,3,1,""],total_streams:[3,3,1,""],total_unknowns:[3,3,1,""]},"Sakurajima.models.user_models":{Friend:[6,1,1,""],UserAnimeListEntry:[14,1,1,""],UserOverview:[15,1,1,""],UserOverviewStats:[16,1,1,""],UserOverviewWatchType:[17,1,1,""]},"Sakurajima.models.user_models.Friend":{get_chronicle:[6,2,1,""],get_overview:[6,2,1,""],unfriend:[6,2,1,""]},"Sakurajima.models.user_models.UserAnimeListEntry":{airing_start:[14,3,1,""],anime_id:[14,3,1,""],cover:[14,3,1,""],cur_episodes:[14,3,1,""],episodes_max:[14,3,1,""],get_anime:[14,2,1,""],progress:[14,3,1,""],status:[14,3,1,""],title:[14,3,1,""],type:[14,3,1,""]},"Sakurajima.models.user_models.UserOverview":{admin:[15,3,1,""],anime:[15,3,1,""],cover:[15,3,1,""],friend:[15,3,1,""],hentai:[15,3,1,""],movie:[15,3,1,""],special:[15,3,1,""],staff:[15,3,1,""],stats:[15,3,1,""],title:[15,3,1,""],username:[15,3,1,""]},"Sakurajima.models.user_models.UserOverviewStats":{mean_score:[16,3,1,""],ratings:[16,3,1,""],total:[16,3,1,""],total_episodes:[16,3,1,""],watched_days:[16,3,1,""],watched_hours:[16,3,1,""]},"Sakurajima.models.user_models.UserOverviewWatchType":{episodes:[17,3,1,""],total:[17,3,1,""]},Sakurajima:{api:[18,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute"},terms:{"1080p":[3,5],"360p":[3,5],"480p":[3,5],"720p":[3,5],"case":1,"class":[1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17,18],"default":[1,5,6,18],"function":[5,18],"int":[1,5,6,18],"new":5,"null":18,"return":[1,4,5,6,8,10,11,14,18],"true":[1,4,5,6,8,10],For:[5,11,13,14,18],The:[1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17,18],Use:1,Used:5,Using:5,abil:18,access:[1,5,18],accord:18,account:[10,18],add:1,add_recommend:[1,18],added:5,addit:[1,18],admin:15,administr:15,affect:5,after:5,air:[1,5,10,11,13,14,18],airing_start:[11,13,14],ajax:18,all:[1,3,5,8,16,18],almost:1,alreadi:14,also:[5,18],altern:18,amount:1,ani:3,anim:[0,3,4,5,7,9,10,11,13,14,15,16,18],anime_id:[1,4,5,7,11,13,14,18],anime_nam:[5,18],anime_titl:[4,5],anititl:5,aniwatch:[1,3,5,6,14,15,18],aniwatchepisod:[0,5,9,18],aniwatchstat:[0,9,18],annd:16,api:[1,5,18],api_url:[1,4,5],apihandl:18,argument:5,around:18,arrang:18,associ:[1,2,5,10],attribut:1,auth:18,authtoken:18,avail:[1,2,5,18],backend:1,base_model:[1,2,5],becaus:5,been:5,belon:7,belong:[2,5,17,18],benefit:5,between:1,bodi:10,bool:[1,4,5,6,8,10,18],breif:[],brief:18,call:[1,5],can:[1,5,13,14,18],categori:[3,7,16],caus:5,certain:5,chart:18,check:18,checkout:18,choos:18,chronicl:[1,4,6,18],chronicle_id:[4,18],chronicleentri:[0,1,6,9,18],chunk:5,classmethod:18,cober:15,com:[],combin:5,come:5,complet:[1,13,18],configur:18,connect:5,consol:5,constructor:18,contain:[1,7,8,18],content:[0,10,18],convini:1,core:18,correspond:18,cover:[11,13,14,15],creat:4,cur_episod:[11,13,14],current:[1,5,11,18],d_statu:11,dai:16,data:[1,3,5,6,8,16,17,18],data_dict:[1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17],date:[1,4,5,18],delet:[5,10,18],delete_all_notif:18,delete_chunk:5,delete_notif:18,descript:[1,5,12,18],detail:[1,18],detail_id:1,dict:[1,18],dictionari:[1,18],differ:16,directori:5,disabl:5,doc:[],document:18,doe:[1,18],don:3,done:5,dowload:5,download:[5,18],drop:[1,18],durat:5,each:[1,18],edg:1,els:5,enabl:5,end:[7,8,18],endpoint:18,english:18,engsub:[],entri:[4,7,8,12,14,18],ep_id:5,ep_titl:4,epiosod:13,episod:[0,1,2,4,9,10,11,13,14,16,17,18],episode_id:[2,18],episode_numb:[1,5],episodelist:[1,18],episodes_max:[11,13,14],eptitl:5,error:[1,4,5,6,8,10],especi:5,etc:[1,6,8,14,15,16,18],everi:5,exact:5,exampl:[5,8,10,11,13,14],fall:3,fals:[1,4,5,6,8,10,18],faster:5,favorit:[8,18],favorite_media:[8,18],feasibl:5,few:1,ffmpeg:5,figur:11,file:[5,18],file_nam:5,filler:5,form:1,friend:[0,9,15],from:[1,4,6,10,16,18],fullhd:5,further:18,gener:[4,17],get:[1,5,6,11,14,18],get_airing_anim:18,get_anim:[11,14,18],get_anime_chronicl:18,get_aniwatch_episod:5,get_available_qu:5,get_best_rated_anim:18,get_chronicl:[1,6],get_complete_object:1,get_dict:1,get_episod:[1,18],get_episode_by_numb:1,get_latest_anim:18,get_latest_releas:18,get_latest_upload:18,get_m3u8:5,get_media:[1,18],get_notif:18,get_overview:6,get_popular_anim:18,get_popular_seasonal_anim:18,get_popular_upcoming_anim:18,get_random_anim:18,get_recommend:[1,18],get_rel:[1,18],get_seasonal_anim:18,get_stat:18,get_unread_notif:18,get_user_anime_list:18,get_user_chronicl:18,get_user_media:18,get_user_overview:18,get_watchlist:18,github:[],given:[16,18],graph:3,has:[1,5,6,8,10,11,13,14,15,16,18],has_nud:13,has_speci:11,have:[3,5,8,11,13,14,18],hentai:[3,15],highest:18,histori:[1,4,6,18],hold:[1,16,17,18],hour:[15,16,18],how:18,howev:[5,18],href:10,href_blank:10,http:18,imag:[11,14,15],includ:[5,15,16,18],include_intro:5,index:[0,1,18],inform:18,initi:1,inord:18,instruct:18,intro:5,is_air:5,is_favorit:8,issu:[5,10,11],item:18,its:18,join:3,json:1,keep:5,kei:18,know:11,lamguag:18,lang:[5,18],languag:[2,5,18],latest:18,left:5,let:[5,11],librari:18,like:[1,6,7,8,15,16,18],list:[1,2,5,6,14,18],live:5,m3u8:5,macro:5,mai:5,mani:1,mark:[1,5,8,18],mark_all_notifications_as_read:18,mark_as_complet:[1,18],mark_as_drop:[1,18],mark_as_on_hold:[1,18],mark_as_plan_to_watch:[1,18],mark_as_watch:[1,18],max_thread:5,maximum:[1,5],mayb:4,mean:[6,16],mean_scor:16,media:[0,1,8,9,18],media_id:[8,18],mediaentri:[0,9,18],method:[1,5],miss:18,model:[0,1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17],modul:0,more:4,movi:[3,11,13,14,15,16,18],mp4:5,multi:5,multi_thread:5,multithread:5,name:[5,18],need:5,neg:5,network:[1,4,5,6,7,8,10,11,14],never:1,new_registered_us:3,new_registered_users_graph_data:3,none:[5,18],normal:[1,18],note:5,notic:5,notif:[0,9,18],notification_id:18,notificaton:18,nottif:10,nuditi:13,number:[1,3,4,5,8,11,13,14,15,16,17,18],object:[1,2,4,5,6,11,14,15,18],objetc:18,occur:[1,4,5,6,8,10],off:5,offer:5,offici:7,on_progress:5,onc:5,one:5,onli:5,open:[1,7,8,11,18],oper:[1,4,5,6,8,10],option:[1,5,6,18],order:5,ost:[1,7,18],our:11,out:[11,18],overview:[6,15,18],page:[0,1,6,18],param:18,paramet:[1,5,6,18],particul:17,particular:[1,17,18],pass:5,path:5,per:5,perform:5,plan:[1,18],playback:5,player:5,pleas:11,popular:18,possibl:1,prefer:18,print:5,print_progress:5,profil:[6,15],progress:[11,13,14,18],properti:[5,18],provid:[1,18],proxi:18,proxy_fil:18,qualiti:[5,18],queri:18,question:5,random:18,rate:[1,16,18],rateanim:18,read:18,reccomend:1,reccomendationentri:18,recent:[3,10],recommen:11,recommend:[1,5,11,13,18],recommendationentri:[0,1,9,18],recommended_anime_id:[1,18],recommened:11,refer:5,regard:[5,6,16,17,18],regist:[3,5],registered_us:3,registered_users_graph_data:3,relat:[0,1,4,8,9,13,18],relation_id:[12,18],relationentri:[0,9],releas:18,relev:[1,8,18],remov:[1,4,6,18],remove_chronicle_entri:[4,18],remove_from_list:[1,18],replac:5,repo:11,report:18,report_missing_anim:18,report_missing_stream:18,repres:[1,4,6,8,14,15,18],requir:[1,5,18],respect:[5,18],respons:1,result:5,resumabilti:5,saga:5,sai:5,sakurajima:[1,2,3,4,5,6,7,8,10,11,12,13,14,15,16,17],same:18,score:[6,16,18],script:5,search:[0,18],season:[11,13,14,18],second:5,seen:[10,18],select:5,seri:4,set:[1,5,18],should:1,show:[3,6,14,15,16,17],signific:5,similar:[1,18],singl:[1,4,5,8,14,18],site:18,skip:5,slower:5,smaller:5,some:18,sometim:1,song:7,sound:7,special:[3,11,13,14,15,16,18],specif:[1,4,18],staff:15,star:18,start:[11,13,14,18],stat:[3,15,16,18],statist:18,statu:[5,8,10,13,14,18],store:18,str:[1,5,18],stream:[2,3,5,18],strean:18,string:5,sub:18,submit:18,success:[1,4,5,6,8,10],sum:3,support:5,suppos:5,synonym:18,syntax:1,taken:5,target:18,thei:5,theme:7,theme_song:7,therefor:5,thi:[1,3,5,10,11,14,15,16,18],thing:[6,15,18],third:5,those:18,thread:5,thumbnail:[5,8],time:[5,6,10,16,18],titl:[1,4,5,8,11,12,13,14,15,18],toggl:[5,10,18],toggle_mark_as_watch:[5,18],toggle_notification_seen:18,toggle_seen:10,token:18,too:18,total:[3,5,6,11,13,14,15,16,17,18],total_1080p_stream:3,total_360p_stream:3,total_480p_stream:3,total_720p_stream:3,total_anim:3,total_episod:16,total_hentai:3,total_movi:3,total_show:3,total_speci:3,total_stream:3,total_unknown:3,track:[1,4,6,7,18],trade:5,troll:5,type:[1,4,5,6,8,10,11,13,14,15,17,18],unfriend:6,unread:18,upload:18,url:[5,8,10,11,13,14,15],use:18,use_ffmpeg:5,used:[1,5,18],user:[1,3,4,5,6,8,10,11,13,14,15,16,18],user_id:18,user_model:[6,14,15,16,17],user_overview:6,useranimelistentri:[0,9,18],userid:18,usermedia:18,usernam:[15,18],useroverview:[0,6,9,18],useroverviewstat:[0,9,15],useroverviewwatchtyp:[0,9,15],using:[1,5],using_proxi:18,valu:[1,18],veri:[1,18],veselysp:[],video:5,vinland:5,wai:18,want:[1,5,6,18],watch:[1,4,5,6,13,14,15,16,17,18],watched_dai:16,watched_hour:16,watchlist:18,weekdai:18,when:[5,11,13],where:[1,5,18],which:[1,2,7,18],who:[3,8],whose:[5,18],wiki:18,work:5,would:18,wrap:[1,18],wrapper:18,year:18,yet:18,you:[1,5,6,11,18],your:[5,18],zero:18},titles:["Welcome to Sakurajima\u2019s documentation!","Anime","AniWatchEpisode","AniwatchStats","ChronicleEntry","Episode","Friend","Media","MediaEntry","Models","Notification","RecommendationEntry","Relation","RelationEntry","UserAnimeListEntry","UserOverview","UserOverviewStats","UserOverviewWatchType","Sakurajima"],titleterms:{anim:1,aniwatchepisod:2,aniwatchstat:3,chronicleentri:4,document:0,episod:5,friend:6,indic:0,media:7,mediaentri:8,model:9,notif:10,recommendationentri:11,relat:12,relationentri:13,sakurajima:[0,18],tabl:0,useranimelistentri:14,useroverview:15,useroverviewstat:16,useroverviewwatchtyp:17,welcom:0}}) \ No newline at end of file +Search.setIndex({docnames:["index","models/anime","models/aniwatch_episode","models/aniwatch_stats","models/chronicle_entry","models/episode","models/friend","models/friend_request_incoming","models/friend_request_outgoing","models/media","models/media_entry","models/models","models/notification","models/recommendation_entry","models/relation","models/relation_entry","models/user_anime_list_entry","models/user_overview","models/user_overview_stats","models/user_overview_watch_type","sakurajima","utils/downloaders","utils/episode_list","utils/mergers","utils/utils"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst","models\\anime.rst","models\\aniwatch_episode.rst","models\\aniwatch_stats.rst","models\\chronicle_entry.rst","models\\episode.rst","models\\friend.rst","models\\friend_request_incoming.rst","models\\friend_request_outgoing.rst","models\\media.rst","models\\media_entry.rst","models\\models.rst","models\\notification.rst","models\\recommendation_entry.rst","models\\relation.rst","models\\relation_entry.rst","models\\user_anime_list_entry.rst","models\\user_overview.rst","models\\user_overview_stats.rst","models\\user_overview_watch_type.rst","sakurajima.rst","utils\\downloaders.rst","utils\\episode_list.rst","utils\\mergers.rst","utils\\utils.rst"],objects:{"Sakurajima.api":{Sakurajima:[20,1,1,""]},"Sakurajima.api.Sakurajima":{add_recommendation:[20,2,1,""],delete_all_notifications:[20,2,1,""],delete_notification:[20,2,1,""],favorite_media:[20,2,1,""],from_cookie:[20,2,1,""],get_airing_anime:[20,2,1,""],get_anime:[20,2,1,""],get_anime_chronicle:[20,2,1,""],get_best_rated_anime:[20,2,1,""],get_episode:[20,2,1,""],get_episodes:[20,2,1,""],get_latest_anime:[20,2,1,""],get_latest_releases:[20,2,1,""],get_latest_uploads:[20,2,1,""],get_media:[20,2,1,""],get_notifications:[20,2,1,""],get_popular_anime:[20,2,1,""],get_popular_seasonal_anime:[20,2,1,""],get_popular_upcoming_anime:[20,2,1,""],get_random_anime:[20,2,1,""],get_recommendations:[20,2,1,""],get_relation:[20,2,1,""],get_seasonal_anime:[20,2,1,""],get_stats:[20,2,1,""],get_unread_notifications:[20,2,1,""],get_user_anime_list:[20,2,1,""],get_user_chronicle:[20,2,1,""],get_user_media:[20,2,1,""],get_user_overview:[20,2,1,""],get_watchlist:[20,2,1,""],mark_all_notifications_as_read:[20,2,1,""],mark_as_completed:[20,2,1,""],mark_as_dropped:[20,2,1,""],mark_as_on_hold:[20,2,1,""],mark_as_plan_to_watch:[20,2,1,""],mark_as_watching:[20,2,1,""],rateAnime:[20,2,1,""],remove_chronicle_entry:[20,2,1,""],remove_from_list:[20,2,1,""],report_missing_anime:[20,2,1,""],report_missing_streams:[20,2,1,""],search:[20,2,1,""],toggle_mark_as_watched:[20,2,1,""],toggle_notification_seen:[20,2,1,""],using_proxy:[20,2,1,""]},"Sakurajima.models":{base_models:[2,0,0,"-"],chronicle:[4,0,0,"-"],media:[10,0,0,"-"],notification:[12,0,0,"-"],recommendation:[13,0,0,"-"],relation:[15,0,0,"-"],stats:[3,0,0,"-"],user_models:[19,0,0,"-"]},"Sakurajima.models.base_models":{AniWatchEpisode:[2,1,1,""],Anime:[1,1,1,""],Episode:[5,1,1,""]},"Sakurajima.models.base_models.AniWatchEpisode":{episode_id:[2,3,1,""],languages:[2,3,1,""],stream:[2,3,1,""]},"Sakurajima.models.base_models.Anime":{add_recommendation:[1,2,1,""],get_chronicle:[1,2,1,""],get_complete_object:[1,2,1,""],get_dict:[1,2,1,""],get_episodes:[1,2,1,""],get_media:[1,2,1,""],get_recommendations:[1,2,1,""],get_relations:[1,2,1,""],mark_as_completed:[1,2,1,""],mark_as_dropped:[1,2,1,""],mark_as_on_hold:[1,2,1,""],mark_as_plan_to_watch:[1,2,1,""],mark_as_watching:[1,2,1,""],rate:[1,2,1,""],remove_from_list:[1,2,1,""]},"Sakurajima.models.base_models.Episode":{added:[5,3,1,""],anime_id:[5,3,1,""],anime_title:[5,3,1,""],description:[5,3,1,""],download:[5,2,1,""],duration:[5,3,1,""],ep_id:[5,3,1,""],filler:[5,3,1,""],get_aniwatch_episode:[5,2,1,""],get_available_qualities:[5,2,1,""],get_m3u8:[5,2,1,""],is_aired:[5,3,1,""],lang:[5,3,1,""],number:[5,3,1,""],thumbnail:[5,3,1,""],title:[5,3,1,""],toggle_mark_as_watched:[5,2,1,""],watched:[5,3,1,""]},"Sakurajima.models.chronicle":{ChronicleEntry:[4,1,1,""]},"Sakurajima.models.chronicle.ChronicleEntry":{anime_id:[4,3,1,""],anime_title:[4,3,1,""],chronicle_id:[4,3,1,""],date:[4,3,1,""],ep_title:[4,3,1,""],episode:[4,3,1,""],remove_chronicle_entry:[4,2,1,""]},"Sakurajima.models.media":{Media:[9,1,1,""],MediaEntry:[10,1,1,""]},"Sakurajima.models.media.Media":{anime_id:[9,3,1,""],endings:[9,3,1,""],openings:[9,3,1,""],osts:[9,3,1,""],theme_songs:[9,3,1,""]},"Sakurajima.models.media.MediaEntry":{favorite_media:[10,2,1,""],favorites:[10,3,1,""],id:[10,3,1,""],is_favorited:[10,3,1,""],thumbnail:[10,3,1,""],title:[10,3,1,""],type:[10,3,1,""]},"Sakurajima.models.notification":{Notification:[12,1,1,""]},"Sakurajima.models.notification.Notification":{"delete":[12,2,1,""],content:[12,3,1,""],href:[12,3,1,""],href_blank:[12,3,1,""],id:[12,3,1,""],seen:[12,3,1,""],time:[12,3,1,""],toggle_seen:[12,2,1,""],type:[12,3,1,""]},"Sakurajima.models.recommendation":{RecommendationEntry:[13,1,1,""]},"Sakurajima.models.recommendation.RecommendationEntry":{airing_start:[13,3,1,""],anime_id:[13,3,1,""],cover:[13,3,1,""],cur_episodes:[13,3,1,""],d_status:[13,3,1,""],episodes_max:[13,3,1,""],get_anime:[13,2,1,""],has_special:[13,3,1,""],progress:[13,3,1,""],recommendations:[13,3,1,""],title:[13,3,1,""],type:[13,3,1,""]},"Sakurajima.models.relation":{Relation:[14,1,1,""],RelationEntry:[15,1,1,""]},"Sakurajima.models.relation.Relation":{description:[14,3,1,""],entries:[14,3,1,""],relation_id:[14,3,1,""],title:[14,3,1,""]},"Sakurajima.models.relation.RelationEntry":{airing_start:[15,3,1,""],anime_id:[15,3,1,""],completed:[15,3,1,""],cover:[15,3,1,""],cur_episodes:[15,3,1,""],episodes_max:[15,3,1,""],has_nudity:[15,3,1,""],progress:[15,3,1,""],title:[15,3,1,""],type:[15,3,1,""]},"Sakurajima.models.stats":{AniwatchStats:[3,1,1,""]},"Sakurajima.models.stats.AniwatchStats":{new_registered_users:[3,3,1,""],new_registered_users_graph_data:[3,3,1,""],registered_users:[3,3,1,""],registered_users_graph_data:[3,3,1,""],total_1080p_streams:[3,3,1,""],total_360p_streams:[3,3,1,""],total_480p_streams:[3,3,1,""],total_720p_streams:[3,3,1,""],total_animes:[3,3,1,""],total_hentais:[3,3,1,""],total_movies:[3,3,1,""],total_shows:[3,3,1,""],total_specials:[3,3,1,""],total_streams:[3,3,1,""],total_unknowns:[3,3,1,""]},"Sakurajima.models.user_models":{Friend:[6,1,1,""],FriendRequestIncoming:[7,1,1,""],FriendRequestOutgoing:[8,1,1,""],UserAnimeListEntry:[16,1,1,""],UserOverview:[17,1,1,""],UserOverviewStats:[18,1,1,""],UserOverviewWatchType:[19,1,1,""]},"Sakurajima.models.user_models.Friend":{get_chronicle:[6,2,1,""],get_overview:[6,2,1,""],unfriend:[6,2,1,""]},"Sakurajima.models.user_models.FriendRequestIncoming":{accept:[7,2,1,""],decline:[7,2,1,""]},"Sakurajima.models.user_models.FriendRequestOutgoing":{withdraw:[8,2,1,""]},"Sakurajima.models.user_models.UserAnimeListEntry":{airing_start:[16,3,1,""],anime_id:[16,3,1,""],cover:[16,3,1,""],cur_episodes:[16,3,1,""],episodes_max:[16,3,1,""],get_anime:[16,2,1,""],progress:[16,3,1,""],status:[16,3,1,""],title:[16,3,1,""],type:[16,3,1,""]},"Sakurajima.models.user_models.UserOverview":{admin:[17,3,1,""],anime:[17,3,1,""],cover:[17,3,1,""],friend:[17,3,1,""],hentai:[17,3,1,""],movie:[17,3,1,""],special:[17,3,1,""],staff:[17,3,1,""],stats:[17,3,1,""],title:[17,3,1,""],username:[17,3,1,""]},"Sakurajima.models.user_models.UserOverviewStats":{mean_score:[18,3,1,""],ratings:[18,3,1,""],total:[18,3,1,""],total_episodes:[18,3,1,""],watched_days:[18,3,1,""],watched_hours:[18,3,1,""]},"Sakurajima.models.user_models.UserOverviewWatchType":{episodes:[19,3,1,""],total:[19,3,1,""]},"Sakurajima.utils":{downloader:[21,0,0,"-"],episode_list:[22,0,0,"-"],merger:[23,0,0,"-"]},"Sakurajima.utils.downloader":{ChunkDownloader:[21,1,1,""],MultiThreadDownloader:[21,1,1,""]},"Sakurajima.utils.downloader.ChunkDownloader":{download:[21,2,1,""]},"Sakurajima.utils.downloader.MultiThreadDownloader":{download:[21,2,1,""],merge:[21,2,1,""],remove_chunks:[21,2,1,""]},"Sakurajima.utils.episode_list":{EpisodeList:[22,1,1,""]},"Sakurajima.utils.episode_list.EpisodeList":{get_episode_by_number:[22,2,1,""],get_episode_by_title:[22,2,1,""],last:[22,2,1,""]},"Sakurajima.utils.merger":{ChunkMerger:[23,1,1,""],FFmpegMerger:[23,1,1,""]},"Sakurajima.utils.merger.ChunkMerger":{merge:[23,2,1,""]},"Sakurajima.utils.merger.FFmpegMerger":{merge:[23,2,1,""]},Sakurajima:{api:[20,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute"},terms:{"1080p":[3,5],"360p":[3,5],"480p":[3,5],"720p":[3,5],"case":1,"class":[1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23],"default":[1,5,6,20],"function":[5,20],"int":[1,5,6,20,21,22],"new":5,"null":20,"return":[1,4,5,6,7,8,10,12,13,16,20,22],"true":[1,4,5,6,7,8,10,12,21],For:[5,13,15,16,20],The:[1,2,3,4,5,6,9,10,12,13,14,15,16,17,18,19,20,21,22],Use:1,Used:5,Using:5,abil:20,accept:7,access:[1,5,20],accord:20,account:[12,20],actual:21,add:1,add_recommend:[1,20],added:5,addit:[1,20],admin:17,administr:17,affect:5,after:5,air:[1,5,12,13,15,16,20],airing_start:[13,15,16],ajax:20,all:[1,3,5,10,18,20],almost:1,alreadi:16,also:[5,20],altern:20,amount:1,ani:3,anim:[0,3,4,5,9,11,12,13,15,16,17,18,20],anime_id:[1,4,5,9,13,15,16,20],anime_nam:[5,20],anime_titl:[4,5],anititl:5,aniwatch:[1,3,5,6,16,17,20,21],aniwatchepisod:[0,5,11,20],aniwatchstat:[0,11,20],annd:18,api:[1,5,20],api_url:[1,4,5],apihandl:20,argument:5,around:20,arrang:20,associ:[1,2,5,12],attribu:22,attribut:1,auth:20,authtoken:20,automat:20,avail:[1,2,5,20],backend:1,base_model:[1,2,5],becaus:5,been:5,belon:9,belong:[2,5,19,20],benefit:5,between:1,bodi:12,bool:[1,4,5,6,7,8,10,12,20,21],breif:[],brief:20,call:[1,5],can:[1,5,15,16,20,22],categori:[3,9,18],caus:5,certain:5,chart:20,check:20,checkout:20,choos:20,chronicl:[1,4,6,20],chronicle_id:[4,20],chronicleentri:[0,1,6,11,20],chunk:[5,21,23],chunkdownload:21,chunkmerg:23,classmethod:20,cober:17,com:[],combin:5,come:5,complet:[1,15,20],concatin:23,configur:20,connect:5,consol:5,constructor:20,contain:[1,9,10,20],content:[0,12,20],convini:[1,22],cooki:20,cookie_fil:20,core:20,correspond:20,cover:[13,15,16,17],creat:[4,23],cur_episod:[13,15,16],current:[1,5,13,20],d_statu:13,dai:18,data:[1,3,5,6,10,18,19,20],data_dict:[1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19],date:[1,4,5,20],declin:7,delet:[5,12,20,21],delete_all_notif:20,delete_chunk:[5,21],delete_notif:20,descript:[1,5,14,20],detail:[1,20],detail_id:1,dict:[1,20],dictionari:[1,20],differ:[18,22],directori:5,disabl:5,doc:[],document:20,doe:[1,20],don:3,done:5,dowload:5,download:[0,5,20,23,24],drop:[1,20],durat:5,each:[1,20],easier:22,edg:1,els:5,enabl:5,end:[9,10,20],endpoint:20,english:20,engsub:[],entri:[4,9,10,14,16,20],ep_id:5,ep_titl:4,epiosod:15,episod:[0,1,2,4,11,12,13,15,16,18,19,20,21,22],episode_id:[2,20,21],episode_list:22,episode_numb:[1,5,22],episodelist:[0,1,20,24],episodes_max:[13,15,16],eptitl:5,error:[1,4,5,6,7,8,10,12],especi:5,etc:[1,6,10,16,17,18,20],everi:5,everyth:22,exact:5,exampl:[5,10,12,13,15,16],extract:20,facilit:21,fall:3,fals:[1,4,5,6,7,8,10,12,20,21],faster:5,favorit:[10,20],favorite_media:[10,20],feasibl:5,few:1,ffmpeg:[5,23],ffmpegmerg:23,figur:13,file:[5,20,21,23],file_nam:[5,21,23],filler:5,find:22,first:22,form:1,friend:[0,7,8,11,17],friendrequestincom:[0,11],friendrequestoutgo:[0,11],from:[1,4,6,12,18,20,21,22],from_cooki:20,fullhd:5,further:20,gener:[4,19],get:[1,5,6,13,16,20],get_airing_anim:20,get_anim:[13,16,20],get_anime_chronicl:20,get_aniwatch_episod:5,get_available_qu:5,get_best_rated_anim:20,get_chronicl:[1,6],get_complete_object:1,get_dict:1,get_episod:[1,20],get_episode_by_numb:[1,22],get_episode_by_titl:22,get_latest_anim:20,get_latest_releas:20,get_latest_upload:20,get_m3u8:5,get_media:[1,20],get_notif:20,get_overview:6,get_popular_anim:20,get_popular_seasonal_anim:20,get_popular_upcoming_anim:20,get_random_anim:20,get_recommend:[1,20],get_rel:[1,20],get_seasonal_anim:20,get_stat:20,get_unread_notif:20,get_user_anime_list:20,get_user_chronicl:20,get_user_media:20,get_user_overview:20,get_watchlist:20,github:[],given:[18,20],graph:3,has:[1,5,6,7,8,10,12,13,15,16,17,18,20,22],has_nud:15,has_speci:13,have:[3,5,10,13,15,16,20],hentai:[3,17],highest:20,histori:[1,4,6,20],hold:[1,18,19,20],hour:[17,18,20],how:20,howev:[5,20],href:12,href_blank:12,http:20,imag:[13,16,17],includ:[5,17,18,20],include_intro:[5,21],index:[0,1,20],inform:20,initi:[1,20],inord:20,instruct:20,intro:5,is_air:5,is_favorit:10,issu:[5,12,13],item:20,its:20,join:3,json:1,keep:5,kei:20,know:13,lamguag:20,lang:[5,20],languag:[2,5,20],last:22,latest:20,left:5,let:[5,13],librari:20,like:[1,6,9,10,17,18,20],list:[1,2,5,6,16,20,22],live:5,m3u8:[5,21],macro:5,mai:5,make:22,mani:1,mark:[1,5,10,20],mark_all_notifications_as_read:20,mark_as_complet:[1,20],mark_as_drop:[1,20],mark_as_on_hold:[1,20],mark_as_plan_to_watch:[1,20],mark_as_watch:[1,20],match:22,max_thread:[5,21],maximum:[1,5],mayb:4,mean:[6,18],mean_scor:18,media:[0,1,10,11,20],media_id:[10,20],mediaentri:[0,11,20],merg:[21,23],merger:[0,24],method:[1,5,22],miss:20,model:[0,1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19],modul:0,more:4,movi:[3,13,15,16,17,18,20],mp4:[5,23],multi:5,multi_thread:5,multipl:21,multithread:5,multithreaddownload:21,name:[5,20],neccasari:20,need:5,neg:5,network:[1,4,5,6,7,8,9,10,12,13,16,21],never:1,new_registered_us:3,new_registered_users_graph_data:3,none:[5,20,21],normal:[1,20,22],note:5,notic:5,notif:[0,11,20],notification_id:20,notificaton:20,nottif:12,nuditi:15,number:[1,3,4,5,10,13,15,16,17,18,19,20,22],object:[1,2,4,5,6,13,16,17,20,21,22],objetc:20,occur:[1,4,5,6,7,8,10,12],off:5,offer:5,offici:9,on_progress:5,onc:5,one:5,onli:[5,22],open:[1,9,10,13,20],oper:[1,4,5,6,7,8,10,12],option:[1,5,6,20],order:5,ost:[1,9,20],our:13,out:[13,20],overview:[6,17,20],page:[0,1,6,20],param:20,paramet:[1,5,6,20,22],particul:19,particular:[1,19,20,22],pass:5,path:5,per:5,perform:5,plan:[1,20],playback:5,player:5,pleas:13,popular:20,possibl:1,prefer:20,print:5,print_progress:5,profil:[6,17],progress:[13,15,16,20],properti:[5,20],provid:[1,20],proxi:20,proxy_fil:20,qualiti:[5,20],queri:20,question:5,random:20,rate:[1,18,20],rateanim:20,read:20,reccomend:1,reccomendationentri:20,recent:[3,12],reciev:7,recommen:13,recommend:[1,5,13,15,20],recommendationentri:[0,1,11,20],recommended_anime_id:[1,20],recommened:13,refer:5,regard:[5,6,18,19,20],regist:[3,5],registered_us:3,registered_users_graph_data:3,relat:[0,1,4,10,11,15,20],relation_id:[14,20],relationentri:[0,11],releas:20,relev:[1,10,20],remov:[1,4,6,20],remove_chronicle_entri:[4,20],remove_chunk:21,remove_from_list:[1,20],replac:5,repo:13,report:20,report_missing_anim:20,report_missing_stream:20,repres:[1,4,6,7,8,10,16,17,20],request:[7,8],requir:[1,5,20],respect:[5,20],respons:1,result:5,resumabilti:5,run:21,saga:5,sai:5,sakurajim:20,sakurajima:[1,2,3,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,21,22,23],same:20,score:[6,18,20],script:5,search:[0,20],season:[13,15,16,20],second:5,seen:[12,20],segment:21,select:[5,22],sent:8,seri:4,set:[1,5,20],should:1,show:[3,6,16,17,18,19],signific:5,similar:[1,20,22],singl:[1,4,5,10,16,20,21,23],site:20,skip:5,slower:5,smaller:5,some:[20,22],sometim:1,song:9,sound:9,special:[3,13,15,16,17,18,20],specif:[1,4,20],staff:17,star:20,start:[13,15,16,20,21,23],stat:[3,17,18,20],statist:20,statu:[5,10,12,15,16,20],store:20,str:[1,5,20,21,22],stream:[2,3,5,20],strean:20,string:5,sub:20,submit:20,success:[1,4,5,6,7,8,10,12],sum:3,support:5,suppos:5,synonym:20,syntax:1,taken:5,target:20,thei:5,them:23,theme:9,theme_song:9,therefor:5,thi:[1,3,5,12,13,16,17,18,20],thing:[6,17,20],third:5,those:20,thread:[5,21],thumbnail:[5,10],time:[5,6,12,18,20],titl:[1,4,5,10,13,14,15,16,17,20,22],toggl:[5,12,20],toggle_mark_as_watch:[5,20],toggle_notification_seen:20,toggle_seen:12,token:20,too:20,total:[3,5,6,13,15,16,17,18,19,20],total_1080p_stream:3,total_360p_stream:3,total_480p_stream:3,total_720p_stream:3,total_anim:3,total_chunk:23,total_episod:18,total_hentai:3,total_movi:3,total_show:3,total_speci:3,total_stream:3,total_unknown:3,track:[1,4,6,9,20],trade:5,troll:5,type:[1,4,5,6,7,8,10,12,13,15,16,17,19,20,22],unfriend:6,unread:20,upload:20,url:[5,10,12,13,15,16,17],use:20,use_ffmpeg:[5,21],used:[1,5,20],user:[1,3,4,5,6,7,8,10,12,13,15,16,17,18,20],user_id:20,user_model:[6,7,8,16,17,18,19],user_overview:6,useranimelistentri:[0,11,20],userid:20,usermedia:20,usernam:[17,20],useroverview:[0,6,11,20],useroverviewstat:[0,11,17],useroverviewwatchtyp:[0,11,17],using:[1,5,21,23],using_proxi:20,util:[0,21,22,23],valu:[1,20],veri:[1,20,22],veselysp:[],video:[5,21],vinland:5,wai:20,want:[1,5,6,20,22],watch:[1,4,5,6,15,16,17,18,19,20],watched_dai:18,watched_hour:18,watchlist:20,weekdai:20,when:[5,13,15],where:[1,5,20],which:[1,2,9,20],who:[3,10],whose:[5,20,22],wiki:20,withdraw:8,work:5,would:20,wrap:[1,20],wrapper:20,year:20,yet:20,you:[1,5,6,13,20,22],your:[5,20],zero:20},titles:["Welcome to Sakurajima\u2019s documentation!","Anime","AniWatchEpisode","AniwatchStats","ChronicleEntry","Episode","Friend","FriendRequestIncoming","FriendRequestOutgoing","Media","MediaEntry","Models","Notification","RecommendationEntry","Relation","RelationEntry","UserAnimeListEntry","UserOverview","UserOverviewStats","UserOverviewWatchType","Sakurajima","Downloaders","EpisodeList","Mergers","Utils"],titleterms:{anim:1,aniwatchepisod:2,aniwatchstat:3,chronicleentri:4,document:0,download:21,episod:5,episodelist:22,friend:6,friendrequestincom:7,friendrequestoutgo:8,indic:0,media:9,mediaentri:10,merger:23,model:11,notif:12,recommendationentri:13,relat:14,relationentri:15,sakurajima:[0,20],tabl:0,useranimelistentri:16,useroverview:17,useroverviewstat:18,useroverviewwatchtyp:19,util:24,welcom:0}}) \ No newline at end of file diff --git a/docs/build/html/utils/downloaders.html b/docs/build/html/utils/downloaders.html new file mode 100644 index 0000000..e8d4da6 --- /dev/null +++ b/docs/build/html/utils/downloaders.html @@ -0,0 +1,258 @@ + + + + + + + + + + Downloaders — Sakurajima documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + +
    + +
    + + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Downloaders

    +
    +
    +class Sakurajima.utils.downloader.ChunkDownloader(network, segment, file_name)
    +

    The object that actually downloads a single chunk.

    +
    +
    +download()
    +

    Starts downloading the chunk.

    +
    + +
    + +
    +
    +class Sakurajima.utils.downloader.MultiThreadDownloader(network, m3u8, file_name: str, episode_id: int, max_threads: int = None, use_ffmpeg: bool = True, include_intro: bool = False, delete_chunks: bool = True)
    +

    Facilitates downloading an episode from aniwatch.me using multiple threads.

    +
    +
    +download()
    +

    Runs the downloader and starts downloading the video file.

    +
    + +
    +
    +merge()
    +

    Merges the downloaded chunks into a single file.

    +
    + +
    +
    +remove_chunks()
    +

    Deletes the downloaded chunks.

    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/utils/episode_list.html b/docs/build/html/utils/episode_list.html new file mode 100644 index 0000000..fef5685 --- /dev/null +++ b/docs/build/html/utils/episode_list.html @@ -0,0 +1,271 @@ + + + + + + + + + + EpisodeList — Sakurajima documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + +
    + +
    + + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    EpisodeList

    +
    +
    +class Sakurajima.utils.episode_list.EpisodeList(episode_list)
    +

    An EpisodeList is very similar to a normal list. You can do everything +with a EpisodeList that you can with a normal list. The only difference is that +an EpisodeList has some convinience methods that make selecting a particular episode easier.

    +
    +
    +get_episode_by_number(episode_number: int)
    +

    Returns the first Episode object from the list whose number attribue matches the +episode_number parameter.

    +
    +
    Parameters
    +

    episode_number (int) – The episode number that you want to find in the list.

    +
    +
    Return type
    +

    Episode

    +
    +
    +
    + +
    +
    +get_episode_by_title(title: str)
    +

    Returns the first Episode object from the list whose title attribue matches the +title parameter.

    +
    +
    Parameters
    +

    title (str) – The title of the episode that you want to find.

    +
    +
    Return type
    +

    Episode

    +
    +
    +
    + +
    +
    +last()
    +

    Returns the last Episode object from the list.

    +
    +
    Return type
    +

    Episode

    +
    +
    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/utils/mergers.html b/docs/build/html/utils/mergers.html new file mode 100644 index 0000000..a11b8c9 --- /dev/null +++ b/docs/build/html/utils/mergers.html @@ -0,0 +1,243 @@ + + + + + + + + + + Mergers — Sakurajima documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + +
    + +
    + + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Mergers

    +
    +
    +class Sakurajima.utils.merger.ChunkMerger(file_name, total_chunks)
    +

    Merges the downloaded chunks by concatinating them into a single file.

    +
    +
    +merge()
    +

    Starts the merger and creates a single file .mp4 file.

    +
    + +
    + +
    +
    +class Sakurajima.utils.merger.FFmpegMerger(file_name, total_chunks)
    +

    Merges the downloaded chunks using ffmpeg.

    +
    +
    +merge()
    +

    Starts the merger and creates a single file .mp4 file.

    +
    + +
    + +
    + + +
    + +
    +
    + + + + +
    + +
    +

    + + © Copyright 2020, Not Marek, Dhanraj Hira + +

    +
    + + + + Built with Sphinx using a + + theme + + provided by Read the Docs. + +
    + +
    +
    + +
    + +
    + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/utils/utils.html b/docs/build/html/utils/utils.html new file mode 100644 index 0000000..2540d28 --- /dev/null +++ b/docs/build/html/utils/utils.html @@ -0,0 +1,227 @@ + + + + + + + + + + Utils — Sakurajima documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + +
    + +
    + + + + + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    + +
    +

    Utils

    + +
    + + +
    + +
    + + +
    +
    + +
    + +
    + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 617e452..a797924 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,6 +12,7 @@ Welcome to Sakurajima's documentation! sakurajima models/models + utils/utils Indices and tables diff --git a/docs/source/models/friend_request_incoming.rst b/docs/source/models/friend_request_incoming.rst new file mode 100644 index 0000000..761f2ed --- /dev/null +++ b/docs/source/models/friend_request_incoming.rst @@ -0,0 +1,7 @@ +FriendRequestIncoming +===================== + +.. module:: Sakurajima.models.user_models + +.. autoclass:: FriendRequestIncoming + :members: \ No newline at end of file diff --git a/docs/source/models/friend_request_outgoing.rst b/docs/source/models/friend_request_outgoing.rst new file mode 100644 index 0000000..8babfeb --- /dev/null +++ b/docs/source/models/friend_request_outgoing.rst @@ -0,0 +1,7 @@ +FriendRequestOutgoing +===================== + +.. module:: Sakurajima.models.user_models + +.. autoclass:: FriendRequestOutgoing + :members: \ No newline at end of file diff --git a/docs/source/models/models.rst b/docs/source/models/models.rst index 4bd4b25..5eeda0c 100644 --- a/docs/source/models/models.rst +++ b/docs/source/models/models.rst @@ -19,4 +19,6 @@ Models user_overview user_overview_stats user_overview_watch_type - friend \ No newline at end of file + friend + friend_request_incoming + friend_request_outgoing \ No newline at end of file diff --git a/docs/source/utils/downloaders.rst b/docs/source/utils/downloaders.rst new file mode 100644 index 0000000..fc44538 --- /dev/null +++ b/docs/source/utils/downloaders.rst @@ -0,0 +1,13 @@ +Downloaders +=========== + +.. module:: Sakurajima.utils.downloader + +.. autoclass:: Downloader + :members: + +.. autoclass:: ChunkDownloader + :members: + +.. autoclass:: MultiThreadDownloader + :members: \ No newline at end of file diff --git a/docs/source/utils/episode_list.rst b/docs/source/utils/episode_list.rst new file mode 100644 index 0000000..e383562 --- /dev/null +++ b/docs/source/utils/episode_list.rst @@ -0,0 +1,7 @@ +EpisodeList +=========== + +.. module:: Sakurajima.utils.episode_list + +.. autoclass:: EpisodeList + :members: \ No newline at end of file diff --git a/docs/source/utils/mergers.rst b/docs/source/utils/mergers.rst new file mode 100644 index 0000000..d7c4ea2 --- /dev/null +++ b/docs/source/utils/mergers.rst @@ -0,0 +1,10 @@ +Mergers +======= + +.. module:: Sakurajima.utils.merger + +.. autoclass:: ChunkMerger + :members: + +.. autoclass:: FFmpegMerger + :members: \ No newline at end of file diff --git a/docs/source/utils/utils.rst b/docs/source/utils/utils.rst new file mode 100644 index 0000000..357149c --- /dev/null +++ b/docs/source/utils/utils.rst @@ -0,0 +1,9 @@ +Utils +===== + +.. toctree:: + :maxdepth: 2 + + downloaders + episode_list + mergers \ No newline at end of file diff --git a/setup.py b/setup.py index a6b3a68..dca9b98 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,11 @@ setup( name="sakurajima", - version="0.3.0", + version="0.3.1", license="MIT", - author="Not Marek", - author_email="notmarek@animex.tech", - description="AniWatch.me API wrapper", + author="NotMarek, Dhanraj Hira", + author_email="notmarek1337@gmail.com", + description="AniWatch.me API wrapper & downloader", long_description=open("README.md", "r").read(), long_description_content_type="text/markdown", url="https://github.com/veselysps/Sakurajima", @@ -19,9 +19,9 @@ ], python_requires=">=3.6", install_requires=[ - "requests==2.23.0", - "pycryptodome==3.9.7", - "m3u8==0.6.0", - "pathvalidate==2.3.0", + "requests>=2.23.0", + "pycryptodome>=3.9.7", + "m3u8>=0.6.0", + "pathvalidate>=2.3.0" ], )