From ca857a8fcfd379dd538ac6282b4a2e5a9e468a19 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Tue, 28 Jul 2020 15:15:03 +0530 Subject: [PATCH 01/23] More docs and some changes to the EpisodeList class. --- Sakurajima/api.py | 18 +- Sakurajima/models/user_models.py | 21 +- Sakurajima/utils/downloader.py | 76 +++++ Sakurajima/utils/episode_list.py | 78 +++-- Sakurajima/utils/merger.py | 18 ++ docs/build/doctrees/environment.pickle | Bin 69107 -> 79170 bytes docs/build/doctrees/index.doctree | Bin 4891 -> 4913 bytes .../models/friend_request_incoming.doctree | Bin 0 -> 9658 bytes .../models/friend_request_outgoing.doctree | Bin 0 -> 7230 bytes docs/build/doctrees/models/models.doctree | Bin 3034 -> 3110 bytes .../models/user_overview_stats.doctree | Bin 11817 -> 11819 bytes docs/build/doctrees/sakurajima.doctree | Bin 178093 -> 181784 bytes docs/build/doctrees/utils/downloaders.doctree | Bin 0 -> 16184 bytes .../build/doctrees/utils/episode_list.doctree | Bin 0 -> 16482 bytes docs/build/doctrees/utils/mergers.doctree | Bin 0 -> 9196 bytes docs/build/doctrees/utils/utils.doctree | Bin 0 -> 2648 bytes docs/build/html/_sources/index.rst.txt | 1 + .../models/friend_request_incoming.rst.txt | 7 + .../models/friend_request_outgoing.rst.txt | 7 + .../build/html/_sources/models/models.rst.txt | 4 +- .../html/_sources/utils/downloaders.rst.txt | 14 + .../html/_sources/utils/episode_list.rst.txt | 7 + .../build/html/_sources/utils/mergers.rst.txt | 10 + docs/build/html/_sources/utils/utils.rst.txt | 9 + docs/build/html/genindex.html | 108 +++++-- docs/build/html/index.html | 9 + docs/build/html/models/friend.html | 5 + .../html/models/friend_request_incoming.html | 270 +++++++++++++++++ .../html/models/friend_request_outgoing.html | 253 ++++++++++++++++ docs/build/html/models/models.html | 4 + .../html/models/user_anime_list_entry.html | 2 + docs/build/html/models/user_overview.html | 2 + .../html/models/user_overview_stats.html | 4 +- .../html/models/user_overview_watch_type.html | 2 + docs/build/html/objects.inv | Bin 1973 -> 2279 bytes docs/build/html/py-modindex.html | 16 ++ docs/build/html/sakurajima.html | 35 ++- docs/build/html/search.html | 1 + docs/build/html/searchindex.js | 2 +- docs/build/html/utils/downloaders.html | 258 +++++++++++++++++ docs/build/html/utils/episode_list.html | 271 ++++++++++++++++++ docs/build/html/utils/mergers.html | 243 ++++++++++++++++ docs/build/html/utils/utils.html | 227 +++++++++++++++ docs/source/index.rst | 1 + .../source/models/friend_request_incoming.rst | 7 + .../source/models/friend_request_outgoing.rst | 7 + docs/source/models/models.rst | 4 +- docs/source/utils/downloaders.rst | 14 + docs/source/utils/episode_list.rst | 7 + docs/source/utils/mergers.rst | 10 + docs/source/utils/utils.rst | 9 + 51 files changed, 1983 insertions(+), 58 deletions(-) create mode 100644 docs/build/doctrees/models/friend_request_incoming.doctree create mode 100644 docs/build/doctrees/models/friend_request_outgoing.doctree create mode 100644 docs/build/doctrees/utils/downloaders.doctree create mode 100644 docs/build/doctrees/utils/episode_list.doctree create mode 100644 docs/build/doctrees/utils/mergers.doctree create mode 100644 docs/build/doctrees/utils/utils.doctree create mode 100644 docs/build/html/_sources/models/friend_request_incoming.rst.txt create mode 100644 docs/build/html/_sources/models/friend_request_outgoing.rst.txt create mode 100644 docs/build/html/_sources/utils/downloaders.rst.txt create mode 100644 docs/build/html/_sources/utils/episode_list.rst.txt create mode 100644 docs/build/html/_sources/utils/mergers.rst.txt create mode 100644 docs/build/html/_sources/utils/utils.rst.txt create mode 100644 docs/build/html/models/friend_request_incoming.html create mode 100644 docs/build/html/models/friend_request_outgoing.html create mode 100644 docs/build/html/utils/downloaders.html create mode 100644 docs/build/html/utils/episode_list.html create mode 100644 docs/build/html/utils/mergers.html create mode 100644 docs/build/html/utils/utils.html create mode 100644 docs/source/models/friend_request_incoming.rst create mode 100644 docs/source/models/friend_request_outgoing.rst create mode 100644 docs/source/utils/downloaders.rst create mode 100644 docs/source/utils/episode_list.rst create mode 100644 docs/source/utils/mergers.rst create mode 100644 docs/source/utils/utils.rst diff --git a/Sakurajima/api.py b/Sakurajima/api.py index 5d9ebba..b6d2d95 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, @@ -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", 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/downloader.py b/Sakurajima/utils/downloader.py index caf9aed..c4195c8 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -7,6 +7,10 @@ class Downloader(object): + """ + Facilitates downloading an episode from aniwatch.me using a single thread. + + """ def __init__( self, network, @@ -18,6 +22,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,6 +66,8 @@ def init_tracker(self): ) def download(self): + """Runs the downloader and starts downloading the video file. + """ if not self.include_intro: for segment in self.m3u8.data["segments"]: if "img.aniwatch.me" in segment["uri"]: @@ -64,22 +93,39 @@ 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): + """ + The object that actually downloads a single chunk. + """ def __init__(self, network, segment, file_name): + """ + :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` + """ self.__network = network self.segment = segment self.file_name = file_name 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 @@ -101,6 +147,9 @@ def decrypt_chunk(self, chunk, key): class MultiThreadDownloader(object): + """ + Facilitates downloading an episode from aniwatch.me using multiple threads. + """ def __init__( self, network, @@ -112,6 +161,27 @@ 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 @@ -165,6 +235,8 @@ def assign_target(self, network, segment, file_name, chunk_number): self.progress_bar.next() def download(self): + """Runs the downloader and starts downloading the video file. + """ stateful_segment_list = StatefulSegmentList(self.m3u8.data["segments"]) self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() @@ -186,12 +258,16 @@ 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() 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/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle index 08b2b2419a4e98a56ce910303a8fbbc6a6a6c13c..b64a1f6c10d7255177a5a5033610535bd445341c 100644 GIT binary patch literal 79170 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 literal 69107 zcmc(I3AiLzb>3+9?ai(o!PAD`NKDU2qh%zGG@8-OAbA!EX%yKo<5s`!dat{tUw5mQ zS-`T94U%3cBY~PlfN_YOIKg&4I}mUP;sb-R4J72VKVo7GHV$Sn0fPxnaB!Tn+*@^S z)v2zke$rTRBsq8?{%wGcx#Trz4oA6^`KC--I{644uc89 zwf(*PZcYf3gkSquuHaPYk2VpXrw>gMJ&>8&-l9 z9j?SMSRK`37_6$cJ7>F%**Xa5vKw~Xu;UezN8A0$BbBaq@)eW&>y;MNck-S_w*rg; zTD=XTxiB@$y1qBpX;%7PuiUBh>&VXPa`+C|*x;S1HU~8illxwmIbL1w&o#^ax;F>n z*uYXKcl)*Q6{Yw3m42fd;;tz33jkf`bq3SrYOhzGX*5ANKy@7zab8W(D_>-qU=@^m zgPEDenPIS|+HN*M(Im-W&3)aP*Y#@q8`b_0bnw71SX%1#uqAc9UcWo2_6LyM2k8%c z!9;5?2U;x8w7YYaKDDY9J;p+j^S>sq#zq3!px`iAaj-F4qF2lMmFXtDS$Pbf^lEjr zUGuh0H`~>dQ2ubE*Kf3DF=Y^3VzgRrwCY~B(Jy0L>~|r(H+(8seyY+OPz#x=Ur@g= zSOT43c=X9({gV2n^}FyN7u7EgmZC85b+7{36t!HTZe^eR1XSU<=wmO&$` zoDF}jqMzlNx&H7dXIR;+v}OmDS4wJNCp z3trc4xBHKkUT4!DdmTvlF~i&Hxk?Y(4OSU?K%-Uj&TvZo74;pAz20cmYk>$sDA33U z{h4jMw)LQsah7!^OQ+K9LG3|X^uGSG`XpyqTk~crgJ!?nZG&D+EibCnYUPD z!!DSWy4M3)KgFdsV1~JNZ2&dL%7h7*fp+GgX|(&TcHb*^y%}g&t*Te&)+RW46_XzIuYoF6T~eb+g&@dFbcxuX4@nfXO>_9zoW3)^B9&^VC6VhOXL}Ics07!Bw(z%j}D;ag~Hq<-jyrVpohVdNB2XYCy8#6>BeMZH<=1n*=|Ywda+*t`@53HLF`B; za4-b{gQ+CD=nWWef-Lkj7!~T|^$S@Q^w!#+SKhUfM(;(zQhZOwX*K2Gk)i(K`iD`0 zo1%AR(XJ`Ys1`$hg;>V zS;*ztSBAT;7Y=otmAUA&vDcU}kP_-IuV2FyS!{OrbutAkjefgon(z_^WzO+6_!*`f zhFN>yIe?Jcl0J0!GIUbtT(*&Kp?c0$%=_}`Mh*MOA}+?k<{!773Q6WWIUPHe2DH#3z}4G@HRS)ZzJrPTVh_3QW_SJtoM{Oc({ z7|L$Nt?T&=4Fhg@(5Zn_2csX2`8+~UY7g4xG>oD+gwks+1Op*Ry4|WY0TP>oP2NnU z*o3228&xt&RFu@>j>;&w96tZ)K4xMkyO zKg9`Km~|X&IRQyQ>ms-dIFX!ld>?v;OG|z5^f(iO4Vk2Py>&n>y6Yy@x#^eu-X4%VjL&x* z^L}#M-XjlgJ96Z3`JQ7(4!67TFC!}&`jKK>6zeQ2#@^w$x%l`LwD#j$haRgUEL>~! zU`}ZYUa-QvqH%SDO-H|+jM7v7ahN=l zgJjbNI6TdV?>E`^IQ=NokZ%WtOTwh6-}ftBc#AFp+xiP_o+cU;FgC3o$P%ZM7ax4+ z@Zrg$dygEL+=;GM&e`1w(3o@v0WvNAUewjADj^py|oeI*n< zeE6p9h0XL4WaKN+m+h12{J=5=Sx>672( z;P?z+Ea?v`)Tmc8Jlke^vw`10v;o>HnLguKUdjn*Q21YSF!=!c-5kudj7w^m#VlvQ z%vn$MV$R=hcbeWQuUR&8PMr6!BfNz(#7!>oV=%=4ytoD92)2UFT#S;?goBp~gE}Rx z<0P0Q(!>Nz552yCUklT3oeWA|!^!N+`2%chf({S0Q8%G9BQ9qcrUb(QD6R^v2}c)` zA8ZErS=EylkJ(wFz_^Sq|vK-O>nwFT)nWHY_fEc-C~1HO0+ebpf>A} z^Qg+fN;~`JrgFXoXhHqFex=cbwmdKm9Lx7@Ppa0@y$M!ad&jn)z2dq@j5V8}EG_Re z))(tp%mCQ8q))PDF$Au8*>wcL(60cKimos8 z8FHs7qcUNL5^;pE&baZl*FOpaBX*XE2Q&XL2^4~)0HZ0f)+F#494u?W1nrb{>t$NP zU)_j4AC0wOOkl!+>nY24>}8v`1v5ac%5xsYxu9)<;~lK6G&}Xmv|E1=1DKFQ=Ygdo=ij`+h5`#D^@G83@WGJUQL)~#&MfPM z%~DLbuw&Pa>Z>q}9jpY_Iez>!%nQ!PXhpNPz0zu&uJo&QA_^6nFz@*>4D&(s zo?LqHdi&k8(85eU@Ep5k3@@E(c&CTKOXO?7o&(ntW`efVCW{WMVHNw*lAHlgs8t&V zmzqpn4}-q&$|1Z~LO@{O{H)etNv=`F{>2P#HVF_LhX|UPk3gI@tTrwSm{`B-^Q<#@ zAg=99xi zi-U`d?!%X`pa$U(uaAxB!gylC1c)Hbu3&}*VS)0{l3+`$2**1h|HZKczjCU=@iAm$ zb1W5SpZNa#*!#{|Sm0}6N}N8fdnvDb{TkL}IpYiu?HUAI%rJ^^Hs~cB2O7=?o1FJB zD}}K7JQ&S3M)T3(4pxF(Fg%ApK%Qtwz=gt94+zA9$mWCdVUk`sS@xhvvkVRvE=7Pl z^i;661FIEi2x;gHChgSz^y$+jt`trTw}XY=4mo?d4p~s23r4Qu$Capc70$F ztb{cRT2dr6r1jcY?){VByJF(K@Ax^}Gx5dqd;ZJgwOG%^_rLGIdrd!x_f}5%=+m!p zIyJxj(Z1`_dnac3%#(lrxy>P|IkZwke*cf(`d*+}FG_uC>A(GYpc))bUh~$yqpccd z`|Ppjcl>pbFejAs&VTr`=h93uWq5A+wqs$2m`%qFpS$9tSN}$6+>NE>gin3*yC;NT#4RjITc{*PSB?ZyCy zqXqQi;WM_eJw5c_Y)Za7tt6QPMxnC)(}hbsV@h0@pi${G81yZLLEmahOeR20m8mf^ z`m+V{4o%)`(s1SY%J6S9MYg6DaR+uT^mbF|%Jf3)fN=JA6wCfjlRZ^Bxa*_G(9fB? zsZD^~0NDiIUAV-13YYk$!X+M{64a-#FXXNc*y=`$w$s#x{=Wba|5xA-cC26mg5eO# zg7xOpm9xF!)6fIwAj|;{TE%32A7l-dn*K>;meZg_qc8PK8oqF9)ho4Nuz;?+jB?;+ z7~VIvs#`hDD=6svuB_H!I~0T?AU@&$3Bex&<P^y1T8@cYu7pHx3EF|1-=LPYjmx z-io3Br;t3Dt^}(^>;rv+B@LLl!mzi3rXau>{CgRD%w(9mb_A-Byr ztjGlB<~jlmHCdP&98)s5Sq~a}7-xg3bP}n|*R3vJV|Sx?3&RmDgWqLG$U}1kXWlVy zM_k_geefGJxSt;f`Ed_Feu5rGgNNY&yN+PoB<85GK#p*z&){hD1I{#F5tN#P`8)0M_ky)8^fog1UipBDc(?{Edd(cZ z=|$oY9h?jfCoG4GOk-)x>o2>!`a}2)z2WWrcqc#J#gF&U!}Nyt!Vl;T=tX7m1Qvwi z84sdE<%({?O=F&Zlc2*s@q%d>y|@fsmmlPSlhaG=OXltqE_eQiNGksC@Z%%=_$WU< zMh_#gkHZg;*t>FYaRj^|hy%M^n3v4O=Lsy7QDH97Kh2QEriUObK;t!T3RW7FmVvqZ z@&dU-UpRxiwaDFlHpu8m%1vU9zP&(>(AUo3XrtsvY3oWl^Y)_!@`h974BpO{ywPxe z3}cIq*i!N^k1Lj9;|<%}!uq8DMc{ZaJqT7e+pu(md(xuG0QutVE2E>zUM0GPG_Cg0 z0b?%;ogbe(7<4y$@(pls=MfqQR-Z;k9MF6km2l|r$@Dvl;oK8XjmqQW`?2D4~Z!!wgL(*xM*&zqmZ`Ap39rzy-T z3W~!vqPdi8!Fm%!g?OZyCT%pkZg2^;GTW_m>M3QJmsMdHF)bTSLgLdT*-HGsLY>ew zVu#x#{BJ=D3?1L*$9MSgH~jcMJ#ZGu3tbRlTn@|(x{V>v09W%;BTbp=2ial32RoOY~aG=$@YNoF;KpCUv*P44~_q{FP9 zPhh2WA2XxQBY`mVK($`s;Z9x~*jQ{i6|1~GiGl6Q{4 za=;-_3tQ=Ss|owTg4Ga?i8!X8mWuxj3X&&op}1h(#+*UhnftujaawZXb5Kih^ULti zDsFG5kwP>U<&d`F$LVQ|#cKC#EF?cxqW}Lh;wrU!`eXLaP51H!zR60fHc6+qVt%2P zf$R3$h2@m9Uk0161m)ni!fn9SHp3MQ*>3|?rvL8>(>D;OLWo}<7vlXP1nZ@^hW%e5 z+U$R%nuRoLJg&Nx0GYo5Y$i+eD=~3oN1t?!Vx%sorTtQ2lBTnz4!K_(7ctT0zA!G5 zV+df#rRd-}({UJ5bR4UzLM=}$bM4!>TE<$bTK2ySg5wsox-c!%M}HVXtQZ&K$05Yh zaUqsaL7oqsGyTaptfRskLKW^RTp_)qVz@X+pzZ@4LQsSSF3t^Ndn-si0XBv+dHXSv;6%@w9*}ho4vR4mvHDk4M zs(DXgl38lrJuYHG&9{z=6jw8?Oq??{#}w%cxH7a+!O83m44d2SD&$>JcnVOgnsnX%?Q3e&8GJ&+sJu{IebZT#tWt;-8!F&n@`pR``>) z=6Uo5UFbC9Qn;+?zYAJ+aC`nGOWOh7 zB2~V6TnM8|+V6DERO#;9bCmeEp%UL*xJtSbv1(aLd{bds2_-%?E<~inH;xNolt^3E z&Y2RGU5<_>7ln&{|FLkrbWLLYvNZXj!n6{a{J^*nktTm@TnM8{+UR-CG?}s=)Y0a; zP@7+i)olZ$K*VnoKeq7W0)AXX4_%vBM_rpGD8+-^R}0fJ+T0pKd}UmSD?^AcjSKNo zfWY0a>tQz;Ue`)~9d7=zJ4X+lN1Ff-EvC&U@C#S+;eo5c@W91Hc;I>yJaEkd9vJn8 z2L?>xfdMsmV5|imI9Z1WPUql(9yB~~6oUuaL3m&%f(J_f(0PMk9kM1a6?zO#*5NC- zhmLoJA+x~-vmFj@|DnTyTm#(Db^`XmMjO(IB))o~y}m)Pfi}Rwy(w@+o^OgnAtGrw z;7C$@hBn1TzmNy~g<^PyT+rwj`l8VMwSrNMp!$obpgU#=+p>*3W={|5XUtfFe)*S!y~%wK1(y}lpnKS#Ge*f)E-Z~?_p z|3)f$6aBpz{?7Ziz+W((x6%XZYYv=>+L}8`L_NY8F}}mI?k9#*Q^C?s1-~$GB}_Qr zs_@&zk5hlY!y+CtHVJ?WC z!^qvC?iM1CTMBR@2|BFYj+ge1EevrPL!!mS8bb*QIi&Z%?J5h?MUfU^i8(CXX)f~_ zTL3o{(1E=;NcM`uc$?cP4-BVs;nCFALL9-d{d&6wU1Vb>wzy3y?L`|g(iDl}P~R()^%WTy&MN$MXodFTm5c6;6}qewFq5H> z6$_BaT}sk%_}C41)kaD$%)<3K>Rob3IMnx15?uQW$D81KY8sOZ^PrfYq!<)J4kx!@ z2E1URF!@*H6#-peVfq}kf!mR$>5RR?ba&*XYbiMO-G#I%99@{OGP>*X=4iL9G3ncF z^~EcmTi-E4PW`Ff+o*VP9&)$)kc311F8IpD$#Mn0h+*1(t*{nu%A@`S8HbBIZE~&K zhFhMC^PmiS$(9f!qH_Qjht=E77nuV?#NlASWe;vGhr3AI@Ua-UsG_hoZY`n>hm^z3 z9?K1J0rR55jNDj+5lhA4VJA$oyux%9Upm zqnHRf75-@)l3xs$!Y4z&;{aa^Q^29PdbA!5pd-v%V5s-hS&h_$WkpVCuFFLXT?shJ zC++!dc6y_EiCI)jIjNfInnU=Gj2ToG?F{>Egk)xK&b)~fdWXC=JfrDr0?$caP;qL1 zYYo2G1Gh`&u$Pv4=rrb(x~1UIy{!gc6vSH|Uqp3V5)R>8VA7;d*s~O!vuP8)Lvj~H z1)E-XoSJ5Mgtl=p0f*=T^va-VlVP^5VYW?7DEYR0T}z|p@N*C4I0ZMD(jC)}!WSdp zQ#oD^e<{Jz9R*lQ5_Gt_m3-HogI!}tCTH`v6dby@HF{+ThK*-qa}u^B9Ktu_MX2Mn zL?uS1beIytI|O%ew09hmIU7F{aERXCufuoeTF}_Wp{qoIw5ohJO?cCl;8~ zz9ff`SN7Yp@I{NVB5MfysKb92I$BxP>Yn zWQNO2`MPjOkTmeUbVaU;q@cT5Pyhlo;!|fOycabp<)EQ2H99=w7LI~4vi-YxvO+Q7 zR8SZMuQZm|pT@1NiyBduAwV}zDGk}F^DdK1(vDeJ7d9dtQ;d+RB`rZ6sypH1Z}4e5 z2>)o&h%|!I=W_FuTs()?O*EV}@awZU%Fe~7IWAIEJ!wYWJQUl4F(E{B=y7TRLNbh~ z5ke@30)=td7Y-x03)dNmiQMiG)S*fP@HBpc2xoL~achAp%C@0YF^=lc-GPjGJeSH@ zOF55H$Qdw)%JqUu-oE$p$d1I!3bg@425y_?g(#XtP;0~wO@SPXNHC){N<@bqwH1>J zVJZA-a^C8V=-();7^p*)0;?v&po5c^yhKM>QbE*UIy6bpKr?J>|&dO%=(-^2jm8_nPR_7#_Vf8GU zLx=3aSgk3;9=MneO|l1c7kkc%W!M9Pawyz^ePz(1Ys{TKoRi=P-z^QN27ZLU=;ArF$mS9+_ESVT ztCwMO9Y}}b4ag16kqT(=n$gZj2xZ+-xVoY0F`0 zZFhv66OazY8*Q?_^p!?$YgTo8Wdo>F)0epxKuufHy3N+jS!c?rP#H#X`6{WfPCak3 zS*Xb+m#i$ z$-6zn<*5<#bpgq=Jle*7?skRk0#oy__>P4hXM`9gT)bWqfnx|eN+&Cs0r!VVU@OYA zt6*7rzX=}XSOAN`IDu+8QfG5eiikSQ+>J{G1Ng*tFDphrikIviJW?98JV7)aR_td4}zC+4y7eH;RVw9)Dipw$;kFag`j0! zCR`FputX4!#BQiiOiy1(B|I+Vv>Q3cOX4T?Hxv8U;}_%M+lZQO(&)&Juh21wZhhMD z9k-%%GU{U?9YUpcuV1J#SBz8&!8rtO;?KrIgr%6!q(7<%O`tM4kB<7hSgm#&H97jq zL$0PfT1x!22oo@+X0_W5?Z?KIWc`_waA;9BU5Lg=nNt=Ii25!|J*y=!S-HPP&URDS z3@L{(V(v+qoi#~qbWh@X!bixZWN(LMXInjW2T@#5j){%Ai@Dx(N#6?Im6NryY}8@z=pCnxo(wD1EQq(_eq??oN^>%RZ|O= z3^j)ZvpB2UoK&c2iYp!Jth$>uQ-}WUIvgh(zy5MIkx3p&ImGwjoANaHekaFgK{>6E zDz{QZ&|%~@-5jRkIBh`AQ8Q%_GmyJ+?lgYg3yB{|Ikn#1r^6^opCVtq3lT?B4)J|v zzNbtcvxFG6e#PNW<-H^bI-KlIv!kg{j{1v&eNK@ z35UIVN6lukfgRSA&579viMbC+?O+wx3N%xZjp*>C?BUEtfUygxJ90Ww9dzxc3DtGY zMs@faPmPiZhrN4iWn4}hyb!F7r$)h&hqi- zq7>;Lm`f0}s}jN;5y9mnJOkJ*1!pXb_#>YHM9V1|jsRx@7eKCd8DGsZ5Mo&+uU!oz z{_WzxiEi^4J1p&JRtWyAOAgSCy1v8m z3T;yfrP$Q4CIslnZLjnOb2SxaN(SSUgR?qi)%Bz*o6~3dWK;)pdWsT>)Jr8?Od@}0 zxJ_bA5h%fk6#5}0M-8`)@Nr|!Hz|K*8lFmA__9>wv-0`%FNrzz-z)vH7o81B z%wb~>Iy$XEt%}?pK=S z>v!zh(SgVUZ6z9qEoEMyv87OR*xAn+cHX=&Zj{{{>D)+S8Z$R8P&9>@k;F7+Zn@zc zN=6cMn7NbHL=ly-tdZhUr7MWZI1KDz2I^j`UulfRfijk-b71H=EEpNUHDD?8Fa^%B zdWB@bWE=)y(;9Z&UcG&KED0#igu*hcFN9Pa@^_g8j$Vz$ff95d!GR&V@aT?NnzHUge__O}EVPZD0~@0*dXw2q$bmeO?FS*? z0AIu&HDeWJ!QByOjO{IhB14Js99h!2khH@QCMJ`Fh!)P2E|ttqvDlD^L!Yj>N;$lc zgS^tCQpj5x4)tAw7JtD(4^k(gqKYd{ax#0#;@hT!7xKbofp{_k(HveBuX%K7cNLBpxmN}gG=Fq7qQg&dbxGoGE0V#5bIz6|?yzO=!AYFZ zS8Ucp$vzk^Z7Fla(Q-~MYjLZ#(T4?!sCcWkgdOqRjZTHWVmi8dPBT(AWsELeVzLe^ z`%)t?qp=)Ligz$4B10XQEou}p7;*N5240E;|+>ip4V#INHJvYRWl1n`I=wWLxcFmoA8`Plh zVApBvs%Q9avJsV-lvDRRc#3#V$TKO2{BD{bj!nHtqa#WV@jFNCV2B$8Rr48cyCZu; z%VA(QSlStNyJs%)MQTr!62$f64lf4rez@VCT0QJ_>^IL%e@~7fd$5A7$^z_-GWkvu zTFDY);d}J`lyfnD-1d+dKOW4C^$Y%C3al*hiwva%w9~Q<&UtXjZm-(z7Sb^DDA#av z_;t2lQsGiW*kR~yyv{P^%+1JZId(pWBjs{qs5-3drRY-;gO?Ob4*$hvglIZ!`~(NV ztY^M3ew3YMBc*CWbC$Tn*gdqK1PeyB%2_ogH4@p;mvUj=$igK_+F|Nmn^JEN^i`~p zzz$bAW_7Oe9NNs5M_*q;BVxbX@?wO~q9wyS9}=eIVLTdpjLy1@_=JlP2$+)s_A-RG z8u$T*2k)_srASK+p&SJiXci%WJtrFuV#E}$K`or49MeVHBqEMPpY{fWV6o4i)Vdw6 zxQ6d-tpLv-Hm~Em@WV$-=PLa=eW1u>n!we54{m$KeP~=9zXvo=ALE#(Z*k1i<=OLe zf#W=#!=9(FiOtgq!+AQ6G*6!ro2LzO^RyFbp1vbCPa)oU`h3lUA${P; ze>L9wy_`idd~*1Bv9AjG6Kmj8PAD1wrx9owHG9TU8g|HJ{DU+$WHuCl6$D%rj(H5K z%#$-f*~byMH4Rr8z*2F;1l-dmfM+#e#ajr=$E84@H2IL~p3*Q=@Z4|6NYp11sLHuf9hJV=A}TzefKp~!I+Puz zBxgYaPFeNP;pmvE#Cke`m0Nc1cITc%eQN?Ww}$OLG6(N%2|UGRP1e|60u=2=6iRt# z0#h*(>6rGuCV}|wg(2EInFQi{6Nt*tp3b9lk(1QV`x3ay3UoT|Y4q4&{v#oOH36w? z)lEX;^+^)u*Ag(fI7?hmB!u|>1f;V4OmcRBZ;CZ+;RjIt-$o2+29@383WWLe1v5iH zA4H(|_^fPgRl&?P9FnmQYj_!*&D77Ve957nSp3KWAmuiFQS0Y5$egtfzbGN_K8|?K zxRw{pe27Ad@RJ&IIrYome-NB~S_8`%IIKR*haCjkA8Key5JPJ%=A;+(ReO$4R{L`a z87ec$RAA*(3xe;@C$Mws2brFOFEmI-|7cvW@S6yNvoB~8RaQ8(iqq!~1lAWdtem$GV@irQ@>asRs1zXclvXNM`ODTwQl&h zkf`=IG)~uvxgXrXX7b}lt^IE^ZJOS)k9@J4Su|Z)?z>n{4hd58v0L7fUj>jz-%r$D z&Mab3saNu|2&hW`D*-6QsRPQx@RGUzoxqagdJN0n1;cl&N|?V-z{pM)gCWx;FUJ>k z`-cROoYH7Ov^*ye7U3cF)W(*EkeGUqq?aVHL~GG*bWhwV(Yp}z z`3b0uHYBG_B%zBE2-2`BVwT;@k@clTk?if~6#bC`dT9bly3Ptu7AbZEwh+|i<3h%+ zMHi4)B_K1}mUG{^0J1 z%+3VDh%p6wi*^aQ1iU!`m?3To*mH;y@1&Mkw{5m51c; zv?l?R(VvN$Wd}ByZ$WdLg+maS0trpvc1$F%%ASq}|9ZnY$+eJEEFt(G!yU zp#+jVW2`b5-El2?@zHT`+(-%W^Qr`nbe&W9(c&Y7b}WH5!bn*;#iAkv^l$=bL`ROU zk`@Abbpk=g4r3J?84)IiP9)$))GQ^cLO_osfJS&ev74F`LsvBcHnNiH^~^$MGYPot z8<<7$Mxyu#Zp^JyP9q2>BqXV30&avUPQ4{r@YhZNj_?!Z?Z*P6o4~kC%zDf=KQWmz zR^%->V>-=c#f(V~1TmzPFRt}cUb-)7k4I1i&NqtB+^jXxzd!e9zYVD#$v<9hYP+UuCG>F#^`)@*m zQtHy(F;x&nqdoG>G-141V_G&u1;^gTD`4KD!DO%#UbZW6h8oW3n_~GsU4iiq4MUD= zN|oeYxdQ548q{cZc!`-YNW1St45=k0t}C=e<-_}69Uq6_eih-Id9U=kl2vo@tanLJ zzoyl0wC))#T>B-M_iHe6+)iW4xfNDO>^C*QJhhAm(1^X?($J)PnZ}-so)OXqG$iR4 zrXt~eL85{m(tuu?v=nl>Y03|=<9`IoM+QSWe})5D(7jldxgeD1kqg-{m6fwppzxYh z0r+vPT5^=k;?7=uDlk8Zm`-z-qj)ADU1loaKCQ8+EQY7oTii`5aQ{%lm9A7Kr(vHW z-F!~ND^zp3M^te5`2{2Paf7@-{38uf8J^Q+Dew9ez+XV%@pVqOa|yUFYJ5sp(&)yG zESQ?&^`HKinIYjSDb9jh9Ii2f_3AkoDrp;)92E%is|YDOpR#2n8__+9$8{$7uW5Bw zR)+GBaMvpd{u>&+vZ9p(o{v5V*8f_=9#1;X!8bwTI~s}cq!V5rDd4}S!H-u16C3W~ zJ(RLV{y-z4Y`D##01T(dsT?Wz?-1YVf(yTlQAp@V8vElZAUq-`;D4;azc9BV3M&7o zQ5mljNLPd8zZiG7#-~2Yu8|!273Pqjmub+KB%O~~aP%E$2w#e2ogI@h5E9za*mXvx z;l3Ra)**tldy1oIDTi0VIrj##1_*4URu#n^N&mF43SAA2tCc#~sq(<%lHZuWZ56QM9p!Ag|JxQzkiSuJnAd8Uii?(tiPzC_<0HIn8eV>BDq|DD?pPSCGGGwwP7PLZ_)}^xE`SpvxLE_u zulbniCO&V~AoFWU_8h|7t>Gzwu9SKU<4JIPG&m*3BUf_p?lylv7IZA9GSQ||vD?#F z+=OutV#vO$3^odcdeN9^$%K1ItJHzyh>#asUd|=hKdO;Y=3W|^_^o4-{a0zYio2Bu zH-+_M8VO}M)=2P?D(6Nr$^F9`v=Tl|hjuRf67qiaf{~ruy9Dxy1tU9GaS7x{G-So_ zr%QueSsUh}=3Uh=71NiFY46JtjLvAFg%~wgU#g_3*L1=3Hn(LS{bV{pk;7_aNn-rzU@mBi|D_gxM&nFlIT3A z&=~_>@g@oWJsP~?jbw-?9(o|`Uq)=FZzuz825`)?B;0?g@vMX^GoaB@4(#|s!2e1E zF5Xt8>q@x)M#EJm7#R!;7nWduU4xa|2h21+4r|Yvn$C^)-T40=v7}whVJa++x$cE6 z525`Bjk7|C*pnrHzq3$8*@212|D+*i81IL~F=>0*im3L-77A%DP!W)y&>)LQl5a&3 zh@a9BGe$wD-u9vsf%*FyW)Uu-FV4(JeShW!Kn!=_k-q=)LJ?)RCdB`$Au7&OTK|h~ z4iTdKV+~XZUZsIPD0UwH687vrML^lJb69uVvk6RIy1|SR?w2)2b68;)+g_m|fd4`R z&S5QGU_L%CVE=~(s|4Cd3O0VTg~0#1hOeB+7>#cZP|)odrazFhzont)@G@MXSaiDP zLc;$~4gRuZ;6N~MidV6XeIJWEF_8k@fOh#Dix$=i@rQ^gTWtYJhJbcAk{#p;`TuBD z&Ec3>Nk-_hi4pYwRiFp{BDh`PvEh`z8gCg`1Al@wM`!_(L%PpG65Tz3ml)I!)(_72 z;Q|BydCYz9xAwi%#WKNw7y>m-`(YNhE7gN@~5 zu#vsT>l^oX+h@+gWfD{VEaVQ>!?B0go-OxH1`kr;sua480xsSFXs=so4ufU)5(U!M zNvvBF|IFc^7W{c~_{4Df$ziY(yfEekI6u2B{8c~bHz3=T@0JbLa^sjxM?WRdm&2y@ zZnZx2e+FKrL9Tg_GELrJne~PmzmEw-hsik0!RGtv9*#+e$tQ<{V0ER{Zbd2}xf|g> zG4%UDAdUsEk!iy@{Q3p_^#=G;KlsEjY!HKBd1DS3@4?+4Mu~%d1LVKqV58~bEng3{ z8jtR8RQsT(t&b0%9!>`9q=x4`*ItZfZ$oL25BLCJX$)l76Kb5};_v&Cmj?%P8J_L(rKK E0CL0QWB>pF delta 640 zcmdm}Hd~FQfn}<|MwV!n$ptLplRH`J86!7yvg$LkmSu=$luWi|)8j47V9a37P|Z-w zQ1=#^T)~!Q+nvE4J0-)Zho>MlFC{ZCJ-(tSHEqh|DVmK_YNu#qux3CMF!Zox=H+DO zrJ_hpmS>k|oH^N^-I8@W(2gmS4LM{c&t#WiTr&9pm)7Jv?9#lGptgws<@#hS`r!Bj+ha)#1=;f5uiYcWTDj6`BiUM789L1#|k;5R77;G+u zxuJ*G3K(=&3B{?2MadZnQ@}1p_~1L54a#Ef1q9vXUgK7yq8N) zdTS3G#5xoohymUE6U7H0k*^>TR;JY)0YccY@B00OTvy=>Px# 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 0000000000000000000000000000000000000000..18627287cc834f907f8aef60af434ed99e89b77e GIT binary patch literal 9658 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8a4eb6a0df3626202519cf93a73f6d1b81b78891 GIT binary patch literal 7230 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 J5xSmh{|^oMmVf{N literal 0 HcmV?d00001 diff --git a/docs/build/doctrees/models/models.doctree b/docs/build/doctrees/models/models.doctree index 4399f5f94986b6d7eb80f12ddc0859f687b93f9d..7c28f2540e72d4c2547f52362edbf5ef017af22e 100644 GIT binary patch delta 139 zcmca5zD$C(fn};R&qmf>W>>!+x!nAe)SP1dw4%(^yp;H&)WXu#;*$8xyyX1c%)Im| zZBuZn$uBKQ&j+fR+|O*rk`bSgxcMlv1T*7;&G*=5Gs{$Elo$7K<|XE)mQ)s`7Eh_2 Tl2PSXoFNaS%QsKua%BVnbYL~d delta 64 zcmZ1`aZ8-Ffo1A>?v1Ry%#&|18*dh5kzi(=vssIMHnU7wMsaZuXI^4%YDr~5YVnlX TDH&yc#ToKIx_I+LE>}hX+58uI diff --git a/docs/build/doctrees/models/user_overview_stats.doctree b/docs/build/doctrees/models/user_overview_stats.doctree index d860168a571f5b4ea1330666660c21c74ec66532..74d20289fc29851cbfc8a3b7898b4eb3fef0aa39 100644 GIT binary patch delta 43 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 Nk^fD$v0C-u{|4)^vPl2{ delta 23200 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 r`m;{2!tfL{!qLxW4rjD1<$_5L& diff --git a/docs/build/doctrees/utils/downloaders.doctree b/docs/build/doctrees/utils/downloaders.doctree new file mode 100644 index 0000000000000000000000000000000000000000..5723287905d67c532d78e56a41344686d0dfe81d GIT binary patch literal 16184 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 Ypn{}I@r)`K7PZeJl*|A{#ypn(2Wt+^VgLXD literal 0 HcmV?d00001 diff --git a/docs/build/doctrees/utils/episode_list.doctree b/docs/build/doctrees/utils/episode_list.doctree new file mode 100644 index 0000000000000000000000000000000000000000..297ea2d39e5ea80917a70a5ae83c0a84a68a61ca GIT binary patch literal 16482 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
  • Sakurajima
  • Models
  • +
  • Utils
  • @@ -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 58047aadbecdb13cac25d9546d244c01a9a985a5..6ae09e6a723a02032b4a67b4f04e76a35379c723 100644 GIT binary patch delta 2179 zcmV-}2z>Xo59bk(dw;IfHWWHYNBvQ4si~&Bf4jtC|-tBf?PnHWpTpT9l;Ki6P7U|oaCn%PkCcA!CjtPC*)n3nEwwL=lb{s=6g$ZhDL2YOEsx)v&Ul45h9x*NIXwplm_KDp*lc z#6fO*#}ewUvws{>P-s~wqY5>%Im%OIa6nKaV=tU()YT?e7|C6gV;Uh_XJ-YSr;Uev*Vr05Wky;BQX+j^?04ZLWKZXQ3WUv ztdz5&03oqFJ~IkIDk#ch$(n}&z^ggRDmtL5aN?z-nSbCoQ=_z>KQYcFe{cuZVWH!g z<+xy6IbI?Jg!`D_gj6FxbFpR0R&%GZAld1pZuw|PI1T$2IGp3kqhJ@ORMt1E;Oxhy zQK51*Oht51QC!^o9;J58pB}46T6emOL~VKAoEWLW|Fx?Ax{rpt+|MeonH;s%V^TjI zBqb+@Qh#E2A!TJwB|0^suScC`^uvo}hNqBigDhp^`@xk%)kh1C%+~+K?C=dI5|Eq{ zP@FdDb3@JpCO4ifGIu3YP^LZlap&qHA$O`SDsrcqBP4gWHd@+`ts*BVlr7%bgEc#| zjn=p`^~#iDliS*htdlq5g(`_`3^N>9S-XUjqJQ%ufM9mXtFJ z<3XMkG*=G{7z=M=R-|%n0t4c{R*!?;Ng_7M|5xN;*Ba5Q?$_&X>0siwwD4`zC65Cq zVSgJN5Gp;xYiH6n!tsPx{>1l<8km1i3M5^HI^+oC-ma%R7{Rr3`x+{E*`*ze=a6Gz24R?Zc9(Y#5=Zrv%# zB%<-tC(oZ0!|j)zbe{cw6Pe#`)0;njy@^ciOnnNRl@&t7r$WZ=iPtWOGk?2ywPHY ziLJVetS#vz6J3ZrryN~Y^nYus;NR+Zy<_s73OP*q=Hz79a>Qy^Sd)H&OWLzhXgOQ% z2(Go9NfW$-tcu2;*D;9g9E^q=y>!=m%O`9!9F*w#!MUJVN}@!%vqy~!zAlT@7a#W*t*iA@QuOLv=68RN z$lU`mfHxBLN2YeN-+z}>k}v1#3l^Yl)^h!~T-jxQ!v65r{qE|Ch}|;XQTXd&$MMJ; zUS1x0qtfkBmv=3A{1E5dO~B#N=lJjt_otO6Hvw4Z8jS$UsCy&#W9xyYvm4EY6*i+O z*v#(tSK9cIjbJM&#yPMW_U27i;FEKkhM4t@I#p*HQv4$nGJkzR(E-!&(bKT|Yh5mU zTm20BVkt|iLW2@tF-@3?F(nK8=Kc52zams+%dE;=su#LH1;g{jL)PrvaaiZef#LfO z`l|C!K!lU8NCeu&d>gHg5yH_+gFt`KMuXBF#Rq7{^{H%eg*6=Cy2NSbTyFt;Z~32^J#hMffD>4z)pi zrJ^zL#lxfS!gb76ZnZJ>gR2YGK0YY-l8XV<3d2646u~^|m;+`pNjL~(f{`PgR~~xUu<^#7YW?rLM&resM_By~{s%@! F%3)bROmP4J delta 1871 zcmV-V2eA0(5w#DHdw-7ICK$)}ehOFZcATWC+9)V{A6~*DpP~0zNGN)g+ z7aqO|AA*mgR?+>}8jw&VMEGjf-r6uiX&Q5!usp{_iX>r0(|;K(GnQ_KrI=!gV?=3O zFiFlNX^6rdIm9{8&ghA8qIfw@3332ku#6Gmq`1VWAUSqLK{4VaR44$hEi7;#1w~G#WkyBWknGOdF(w)sHe^f zL_widsmvkN%75mlNLdbp8kuR~Orx&Ww8ntG0U?oe9amLwj8f2-KW z1gWH`h$U-Z1^}<&D68mzs>6wQj%I@6T+Pz^`iXHN`G3_Nc;?DVJI+~-OU9MsB|<>B zj|omlHS=>9Tc&I^cbW^5T`ua8kEVpvwC{k!HLg4gc5zBo^RP5&CYD2HSSHlGNst$ zw)P^Mw&PfU$%G;m8cDM(Ut%lcEn5)t4zfIVa>3=^*Z)8RZE2J8s;R(3&+aej=0&{0)9G|^) zyoSdo727|0lQ1r6cXWSJ;RPBiYJMy~V8t{O`o*pWXv+w~)0bLg({ zHSXI{W0mW4)TkX-QKdv)eJLz@DSt=)npL|WZgT`{+4p*3Q=L|G#|r!3-uJF+_Oa)h zsx_43Wja#()p zN$2(Vhsb<)%xHf2_7IuInfesSlodk6r$Wx2h}R*Aj6HkhxA~b@3GWlBDt{ge_FTA2 zw4Y=cONd7wTD0fxaqSFOIH>_S`bh=p=jOu74tAL3BGcVe?I$HJ(5g7!hVBOZ zOa0u`#Qmb2iYcR*eEYK;lh_s3BpBe5_Iv|c&Xyg3Yb_@W(2^^12WUy_o*T5-LRJAS zXWcc07PDnE;9BMc$64z4fqzle*{j~@?qNnK%}HVFxf{JOpKH0(7j2Id{qA_Y;M+*k z8@>rMz0vJ^^ZhFkIE@ zigk0z6sS~_TpT8}Dj0%N>?(ZY6kQM1%UH!d3pSq~{@HnO>%5;PaDO{>qD?OjN_73? z)G1!-ddL)i3587GQgp&Je0UZ1e{Jpyf3ANF?zg2Zsc#zG@d?v}DeEO!cpt)l|NJXL zb#Il|KC#{m{wf%Ln>1w2zF7*JNGvdX8>(+Q|9mu@e1RHhHv(+5`|Jxx8xDcKNLPc> zmGOq-qTpa$P_KcE27fgfOc&A28x1n)uuZ&!VZENF#l6rpYzqH^d3~R$)o^t8qQM)W z-Ad84ZKrnItob?6lo)9m&MJ_?VeL|2Igi%BjK|iY=KMRD9C2uQgY(d^KQsxnXCs#8 zTB~Rb zV|jh(FWlyAy))RF$idZxW*@FHQ!{7O`u@)ihC`VA%Y%V^4Z42?F;Z&(*4^4(|9~zy J_#fiBEQ}aEvb6vJ 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..75a19a7 --- /dev/null +++ b/docs/source/utils/downloaders.rst @@ -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/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 From b815dba1be79d9ac7fb65727e1dae293652cf2a4 Mon Sep 17 00:00:00 2001 From: veselym Date: Sat, 1 Aug 2020 07:50:53 +0200 Subject: [PATCH 02/23] Fixed chunk decryption in downloader --- Sakurajima/api.py | 1 - Sakurajima/models/base_models.py | 4 +++- Sakurajima/utils/downloader.py | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Sakurajima/api.py b/Sakurajima/api.py index b6d2d95..7d9bd95 100644 --- a/Sakurajima/api.py +++ b/Sakurajima/api.py @@ -339,7 +339,6 @@ 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"]) def get_user_chronicle(self, user_id, page=1): diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 57770b5..bab38dc 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 @@ -24,6 +24,7 @@ class Anime(object): Use the get_episodes method to get a list of available episodes""" def __init__(self, data_dict: dict, network, api_url: str): + print(data_dict) self.__network = network self.__API_URL = api_url self.data_dict = data_dict @@ -387,6 +388,7 @@ def get_m3u8(self, quality: str) -> M3U8: REFERER = self.__generate_referer() self.__network.headers.update({"REFERER": REFERER, "ORIGIN": "https://aniwatch.me"}) aniwatch_episode = self.get_aniwatch_episode() + print(aniwatch_episode.stream.sources) res = self.__network.get(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index c4195c8..b0df82b 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -1,5 +1,6 @@ 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 @@ -84,7 +85,7 @@ def download(self): for chunk_number, segment in enumerate(self.m3u8.data["segments"]): file_name = f"chunks\/{self.file_name}-{chunk_number}.chunk.ts" - ChunkDownloader(self.__network, segment, file_name).download() + ChunkDownloader(self.__network, segment, file_name, chunk_number).download() self.progress_bar.next() self.progress_tracker.update_chunks_done(chunk_number) if self.on_progress: @@ -110,7 +111,7 @@ class ChunkDownloader(object): """ The object that actually downloads a single chunk. """ - def __init__(self, network, segment, file_name): + def __init__(self, network, segment, file_name, chunk_number): """ :param network: The Sakurajima :class:`Network` object that is used to make network requests. :type network: :class:`Network` @@ -122,6 +123,7 @@ def __init__(self, network, segment, file_name): self.__network = network self.segment = segment self.file_name = file_name + self.chunk_number = chunk_number def download(self): """Starts downloading the chunk. @@ -130,6 +132,7 @@ def download(self): 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) @@ -139,10 +142,15 @@ def download(self): def get_decrypt_key(self, uri): res = self.__network.get(uri) - return res.content + key = [] + for byte in res.content: + key.append(byte) + key[13] = key[13] - 1 + return bytearray(key) def decrypt_chunk(self, chunk, key): - decryptor = AES.new(key, AES.MODE_CBC) + iv=bytearray([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,self.chunk_number+1]) + decryptor = AES.new(key, AES.MODE_CBC, iv) return decryptor.decrypt(chunk) @@ -229,7 +237,7 @@ def reset_threads(self): self.threads = [] def assign_target(self, network, segment, file_name, chunk_number): - ChunkDownloader(network, segment, file_name).download() + ChunkDownloader(network, segment, file_name, chunk_number).download() with self.__lock: self.progress_tracker.update_chunks_done(chunk_number) self.progress_bar.next() From 4562466fec753970ee5de9e67fcc128992463dd3 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sat, 1 Aug 2020 12:40:55 +0530 Subject: [PATCH 03/23] added some more docs --- Sakurajima/models/base_models.py | 4 ++-- docs/source/utils/downloaders.rst | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 57770b5..6f6f2d4 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -44,7 +44,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: @@ -320,7 +320,7 @@ def __init__(self, data_dict, network, api_url, anime_id, anime_title=None): """The description of the episode.""" self.thumbnail = data_dict.get("thumbnail", None) """The URL to the thumbnail for the episode.""" - self.added = datetime.datetime.utcfromtimestamp(data_dict.get("added", None)) + self.added = datetime.utcfromtimestamp(data_dict.get("added", None)) """The date when the episode was added.""" self.filler = data_dict.get("filler", None) """Is set to 1 if the episode is filler else 0""" diff --git a/docs/source/utils/downloaders.rst b/docs/source/utils/downloaders.rst index 75a19a7..fc44538 100644 --- a/docs/source/utils/downloaders.rst +++ b/docs/source/utils/downloaders.rst @@ -4,8 +4,7 @@ Downloaders .. module:: Sakurajima.utils.downloader .. autoclass:: Downloader - :members: - :autoclass_content: both + :members: .. autoclass:: ChunkDownloader :members: From 1a84b3c6428a8e7898f825320ac76b4f7cdcdc92 Mon Sep 17 00:00:00 2001 From: veselym Date: Sat, 1 Aug 2020 09:20:35 +0200 Subject: [PATCH 04/23] Added a function for generating the IV for video decryption --- Sakurajima/models/base_models.py | 2 -- Sakurajima/utils/downloader.py | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index bab38dc..0175edb 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -24,7 +24,6 @@ class Anime(object): Use the get_episodes method to get a list of available episodes""" def __init__(self, data_dict: dict, network, api_url: str): - print(data_dict) self.__network = network self.__API_URL = api_url self.data_dict = data_dict @@ -388,7 +387,6 @@ def get_m3u8(self, quality: str) -> M3U8: REFERER = self.__generate_referer() self.__network.headers.update({"REFERER": REFERER, "ORIGIN": "https://aniwatch.me"}) aniwatch_episode = self.get_aniwatch_episode() - print(aniwatch_episode.stream.sources) res = self.__network.get(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index b0df82b..1155a8e 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -139,6 +139,12 @@ def download(self): videofile.write(decrypted_chunk) else: videofile.write(chunk) + + def create_initialization_vector(self, chunk): + iv = [0 for _ in range(0, 16)] + for i in range(12, 16): + iv[i] = chunk >> 8 * (15 - i) & 255 + return bytearray(iv) def get_decrypt_key(self, uri): res = self.__network.get(uri) @@ -149,8 +155,7 @@ def get_decrypt_key(self, uri): return bytearray(key) def decrypt_chunk(self, chunk, key): - iv=bytearray([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,self.chunk_number+1]) - decryptor = AES.new(key, AES.MODE_CBC, iv) + decryptor = AES.new(key, AES.MODE_CBC, self.create_initialization_vector(self.chunk_number + 1)) return decryptor.decrypt(chunk) From e1613cf85bce140d8bfef2d2ca62d4efdf2e56a0 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sat, 1 Aug 2020 16:22:46 +0530 Subject: [PATCH 05/23] Requests can now be made without including user session. --- Sakurajima/models/base_models.py | 6 ++-- Sakurajima/utils/downloader.py | 5 +++- Sakurajima/utils/network.py | 47 +++++++++++++++++++++++--------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index ae4f834..0e66208 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -320,7 +320,7 @@ def __init__(self, data_dict, network, api_url, anime_id, anime_title=None): """The description of the episode.""" self.thumbnail = data_dict.get("thumbnail", None) """The URL to the thumbnail for the episode.""" - self.added = datetime.utcfromtimestamp(data_dict.get("added", None)) + self.added = datetime.datetime.utcfromtimestamp(data_dict.get("added", None)) """The date when the episode was added.""" self.filler = data_dict.get("filler", None) """Is set to 1 if the episode is filler else 0""" @@ -385,9 +385,9 @@ def get_m3u8(self, quality: str) -> M3U8: return self.__m3u8 else: REFERER = self.__generate_referer() - self.__network.headers.update({"REFERER": REFERER, "ORIGIN": "https://aniwatch.me"}) + headers = {"ORIGIN": "https://aniwatch.me"} aniwatch_episode = self.get_aniwatch_episode() - res = self.__network.get(aniwatch_episode.stream.sources[quality]) + res = self.__network.get_with_user_session(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index 1155a8e..478a2bb 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -119,6 +119,9 @@ def __init__(self, network, segment, file_name, chunk_number): :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 @@ -147,7 +150,7 @@ def create_initialization_vector(self, chunk): return bytearray(iv) def get_decrypt_key(self, uri): - res = self.__network.get(uri) + res = self.__network.get_with_user_session(uri) key = [] for byte in res.content: key.append(byte) diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 1fd3785..0d692f3 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -5,19 +5,15 @@ 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} if username is not None and user_id is not None and user_id is not None: - headers["x-auth"] = auth_token session_token = ( '{"userid":' + str(user_id) @@ -27,11 +23,27 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi + str(auth_token) + '","remember_login":true}' ) - cookies["SESSION"] = session_token - headers["COOKIE"] = f"SESSION={session_token}; XSRF-TOKEN={xsrf_token};" + headers = { + "REFERER": "https://aniwatch.me/", + "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", + "COOKIE": f"SESSION={session_token}; XSRF-TOKEN={xsrf_token};", + "X-AUTH": auth_token + } + + cookies = { + "SESSION": session_token, + "XSRF-TOKEN": xsrf_token + } + self.session.headers.update(headers) self.session.cookies.update(cookies) - + self.userless_session.headers.update( + { + "USER_AGENT": headers["USER-AGENT"], + "REFERER": headers["REFERER"] + } + ) def __repr__(self): return "" @@ -43,10 +55,19 @@ def post(self, data): self.session.close() raise e - def get(self, uri): + def get_with_user_session(self, uri, headers = None): 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): + print(uri) + try: + res = self.userless_session.get(uri, headers = None) + print(res.request.headers) + return res + except Exception as e: + raise(e) \ No newline at end of file From 81fc0c1d4fbd6c407eb3e655867f75187dd7b2fd Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sat, 1 Aug 2020 22:07:15 +0530 Subject: [PATCH 06/23] Rudimentary implementation of the new decryption technique. (Probably broken) --- Sakurajima/utils/decrypter_provider.py | 33 ++++++++++++++ Sakurajima/utils/downloader.py | 59 ++++++++++++++------------ 2 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 Sakurajima/utils/decrypter_provider.py diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py new file mode 100644 index 0000000..93b156e --- /dev/null +++ b/Sakurajima/utils/decrypter_provider.py @@ -0,0 +1,33 @@ +from Crypto.Cipher import AES + +class DecrypterProvider(object): + + def __init__(self, network, m3u8): + self.__network = network + self.m3u8 = m3u8 + self.key = None + + def get_key(self): + if self.key == None: + uri = self.m3u8.data["keys"][1]["uri"] + key1 = bytearray(self.__network.get_with_user_session(uri)) + key2 = key1 + while key1 == key2: + key2 = bytearray(self.__network.get_with_user_session(uri)) + final_key = [] + for index, byte in enumerate(key1): + smaller = min(byte, key2[index]) + final_key.append(smaller) + self.key = bytearray(final_key) + return self.key + + @staticmethod + def create_initialization_vector(chunk_number): + iv = [0 for _ in range(0, 16)] + for i in range(12, 16): + iv[i] = chunk >> 8 * (15 - i) & 255 + return bytearray(iv) + + def get_decryptor(self, chunk_number): + 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 478a2bb..eda1eca 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -5,7 +5,7 @@ from threading import Thread, Lock from progress.bar import IncrementalBar from Sakurajima.utils.progress_tracker import ProgressTracker - +from Sakurajima.utils.decrypter_provider import DecrypterProvider class Downloader(object): """ @@ -69,11 +69,19 @@ 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"], start = 1): + 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") @@ -83,9 +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"]): + 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, chunk_number).download() + decryter_provider = DecrypterProvider(self__network, self.m3u8) + 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: @@ -111,7 +128,7 @@ class ChunkDownloader(object): """ The object that actually downloads a single chunk. """ - def __init__(self, network, segment, file_name, chunk_number): + 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` @@ -126,7 +143,8 @@ def __init__(self, network, segment, file_name, chunk_number): self.__network = network self.segment = segment self.file_name = file_name - self.chunk_number = chunk_number + self.chunk_number = chunk_number, + self.decrypter_provider = decrypter_provider def download(self): """Starts downloading the chunk. @@ -137,29 +155,14 @@ def download(self): 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 create_initialization_vector(self, chunk): - iv = [0 for _ in range(0, 16)] - for i in range(12, 16): - iv[i] = chunk >> 8 * (15 - i) & 255 - return bytearray(iv) - - def get_decrypt_key(self, uri): - res = self.__network.get_with_user_session(uri) - key = [] - for byte in res.content: - key.append(byte) - key[13] = key[13] - 1 - return bytearray(key) - - def decrypt_chunk(self, chunk, key): - decryptor = AES.new(key, AES.MODE_CBC, self.create_initialization_vector(self.chunk_number + 1)) - 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): From b7f81886d48bb3a2fd7758a0864063e22c426a71 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sat, 1 Aug 2020 22:33:10 +0530 Subject: [PATCH 07/23] Changed a for loop to be slightly more clear. --- Sakurajima/utils/decrypter_provider.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py index 93b156e..082122f 100644 --- a/Sakurajima/utils/decrypter_provider.py +++ b/Sakurajima/utils/decrypter_provider.py @@ -7,7 +7,7 @@ def __init__(self, network, m3u8): self.m3u8 = m3u8 self.key = None - def get_key(self): + def get_key(self) -> bytearray: if self.key == None: uri = self.m3u8.data["keys"][1]["uri"] key1 = bytearray(self.__network.get_with_user_session(uri)) @@ -15,19 +15,19 @@ def get_key(self): while key1 == key2: key2 = bytearray(self.__network.get_with_user_session(uri)) final_key = [] - for index, byte in enumerate(key1): - smaller = min(byte, key2[index]) + for index in range(len(key1)): + smaller = min(key1[index], key2[index]) final_key.append(smaller) self.key = bytearray(final_key) return self.key @staticmethod - def create_initialization_vector(chunk_number): + def create_initialization_vector(chunk_number) -> bytearray: iv = [0 for _ in range(0, 16)] for i in range(12, 16): iv[i] = chunk >> 8 * (15 - i) & 255 return bytearray(iv) - def get_decryptor(self, chunk_number): + def get_decryptor(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 From f1c00e6bf5bfb53089a3e8193cbb097ca26d8d17 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sat, 1 Aug 2020 22:48:23 +0530 Subject: [PATCH 08/23] Fixed chunk numbers wrongly starting from 1. --- Sakurajima/utils/downloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index eda1eca..eea30ab 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -72,7 +72,7 @@ def download(self): 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"], start = 1): + for chunk_number, chunk in enumerate(self.m3u8.data["segments"]): chunk_tuple_list.append((chunk_number, chunk)) if not self.include_intro: From 5070f6c45db00e828cd8d642213bc9df5960b358 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sun, 2 Aug 2020 11:00:43 +0530 Subject: [PATCH 09/23] Fixed an error that caused unnecessay network requests. --- Sakurajima/utils/downloader.py | 3 +-- Sakurajima/utils/network.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index eea30ab..74fe3d8 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -90,12 +90,11 @@ def download(self): self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() - + 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" - decryter_provider = DecrypterProvider(self__network, self.m3u8) ChunkDownloader( self.__network, chunk_tuple[1], # The segment data diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 0d692f3..b691e9d 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -66,7 +66,7 @@ def get_with_user_session(self, uri, headers = None): def get(self, uri, headers = None): print(uri) try: - res = self.userless_session.get(uri, headers = None) + res = self.userless_session.get(uri, headers = headers) print(res.request.headers) return res except Exception as e: From a1a560e7102b8050e708de670e321c5dac545d34 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sun, 2 Aug 2020 18:29:52 +0530 Subject: [PATCH 10/23] Fixed a typo in the headers and removed some debug statements. --- Sakurajima/models/base_models.py | 2 +- Sakurajima/utils/decrypter_provider.py | 4 ++-- Sakurajima/utils/network.py | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 0e66208..fdbfaf7 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -387,7 +387,7 @@ def get_m3u8(self, quality: str) -> M3U8: REFERER = self.__generate_referer() headers = {"ORIGIN": "https://aniwatch.me"} aniwatch_episode = self.get_aniwatch_episode() - res = self.__network.get_with_user_session(aniwatch_episode.stream.sources[quality]) + res = self.__network.get(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py index 082122f..3c41e5b 100644 --- a/Sakurajima/utils/decrypter_provider.py +++ b/Sakurajima/utils/decrypter_provider.py @@ -10,10 +10,10 @@ def __init__(self, network, m3u8): def get_key(self) -> bytearray: if self.key == None: uri = self.m3u8.data["keys"][1]["uri"] - key1 = bytearray(self.__network.get_with_user_session(uri)) + key1 = bytearray(self.__network.get(uri)) key2 = key1 while key1 == key2: - key2 = bytearray(self.__network.get_with_user_session(uri)) + key2 = bytearray(self.__network.get(uri)) final_key = [] for index in range(len(key1)): smaller = min(key1[index], key2[index]) diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index b691e9d..57b8294 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -40,7 +40,7 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi self.session.cookies.update(cookies) self.userless_session.headers.update( { - "USER_AGENT": headers["USER-AGENT"], + "USER-AGENT": headers["USER-AGENT"], "REFERER": headers["REFERER"] } ) @@ -64,10 +64,8 @@ def get_with_user_session(self, uri, headers = None): raise e def get(self, uri, headers = None): - print(uri) try: res = self.userless_session.get(uri, headers = headers) - print(res.request.headers) return res except Exception as e: raise(e) \ No newline at end of file From 04b503147dc194c16b13a192789a582b3a3cee18 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Sun, 2 Aug 2020 19:37:35 +0530 Subject: [PATCH 11/23] Changed the way we get decryption keys. --- Sakurajima/utils/decrypter_provider.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py index 3c41e5b..33e6fb0 100644 --- a/Sakurajima/utils/decrypter_provider.py +++ b/Sakurajima/utils/decrypter_provider.py @@ -6,14 +6,16 @@ def __init__(self, network, m3u8): self.__network = network self.m3u8 = m3u8 self.key = None + self.uri = self.m3u8.data["keys"][1]["uri"] - def get_key(self) -> bytearray: + def get_key_by_comparison(self) -> bytearray: if self.key == None: - uri = self.m3u8.data["keys"][1]["uri"] - key1 = bytearray(self.__network.get(uri)) + key1 = bytearray(self.__network.get(self.uri).content) key2 = key1 - while key1 == key2: - key2 = bytearray(self.__network.get(uri)) + 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]) @@ -21,6 +23,11 @@ def get_key(self) -> bytearray: 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)] From e92f3c9ded77115c19ce1f2eeb295e687618753a Mon Sep 17 00:00:00 2001 From: veselym Date: Sun, 2 Aug 2020 16:29:41 +0200 Subject: [PATCH 12/23] Fixed many typos to get downloading working --- .vscode/settings.json | 2 +- Sakurajima/models/base_models.py | 2 +- Sakurajima/utils/decrypter_provider.py | 4 ++-- Sakurajima/utils/downloader.py | 4 ++-- Sakurajima/utils/network.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ee19851..5b34ad9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "restructuredtext.confPath": "${workspaceFolder}\\docs\\source", - "python.pythonPath": "C:\\Python\\python.exe" + "python.pythonPath": "C:\\Python38\\python.exe" } \ No newline at end of file diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index fdbfaf7..0e66208 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -387,7 +387,7 @@ def get_m3u8(self, quality: str) -> M3U8: REFERER = self.__generate_referer() headers = {"ORIGIN": "https://aniwatch.me"} aniwatch_episode = self.get_aniwatch_episode() - res = self.__network.get(aniwatch_episode.stream.sources[quality]) + res = self.__network.get_with_user_session(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py index 33e6fb0..65d537f 100644 --- a/Sakurajima/utils/decrypter_provider.py +++ b/Sakurajima/utils/decrypter_provider.py @@ -32,9 +32,9 @@ def get_key(self) -> bytearray: def create_initialization_vector(chunk_number) -> bytearray: iv = [0 for _ in range(0, 16)] for i in range(12, 16): - iv[i] = chunk >> 8 * (15 - i) & 255 + iv[i] = chunk_number[0] >> 8 * (15 - i) & 255 return bytearray(iv) - def get_decryptor(self, chunk_number) -> AES: + 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 74fe3d8..98a4235 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -90,7 +90,7 @@ def download(self): self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() - decryter_provider = DecrypterProvider(self__network, self.m3u8) + 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. @@ -143,7 +143,7 @@ def __init__(self, network, segment, file_name, chunk_number, decrypt_provider: self.segment = segment self.file_name = file_name self.chunk_number = chunk_number, - self.decrypter_provider = decrypter_provider + self.decrypter_provider = decrypt_provider def download(self): """Starts downloading the chunk. diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 57b8294..c2a94d6 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -1,6 +1,6 @@ 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): @@ -14,7 +14,7 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi self.cookies = self.session.cookies # Expose session cookies xsrf_token = Misc().generate_xsrf_token() if username is not None and user_id is not None and user_id is not None: - session_token = ( + session_token = urllib.parse.quote( '{"userid":' + str(user_id) + ',"username":"' From dd21b0f4db8899119feb8b1eb04ad6a7ee9b5603 Mon Sep 17 00:00:00 2001 From: veselym Date: Sun, 2 Aug 2020 16:40:37 +0200 Subject: [PATCH 13/23] Fixed multi threaded downloads --- Sakurajima/utils/downloader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index 98a4235..fd397b5 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -246,8 +246,8 @@ def start_threads(self): def reset_threads(self): self.threads = [] - def assign_target(self, network, segment, file_name, chunk_number): - ChunkDownloader(network, segment, file_name, chunk_number).download() + def assign_target(self, network, segment, file_name, chunk_number, decrypter_provider): + ChunkDownloader(network, segment, file_name, chunk_number, decrypter_provider).download() with self.__lock: self.progress_tracker.update_chunks_done(chunk_number) self.progress_bar.next() @@ -258,13 +258,14 @@ def download(self): stateful_segment_list = StatefulSegmentList(self.m3u8.data["segments"]) self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() + decrypter_provider = DecrypterProvider(self.__network, self.m3u8) 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),) + Thread(target=self.assign_target, args=(self.__network, segment, file_name, chunk_number, decrypter_provider),) ) self.start_threads() self.reset_threads() From 09818bbb04df0233ab0ed2b0ce8ce2b6574b8893 Mon Sep 17 00:00:00 2001 From: DhanrajHira Date: Mon, 3 Aug 2020 00:00:06 +0530 Subject: [PATCH 14/23] Better multi-thread downloader implementation. --- .vscode/settings.json | 2 +- Sakurajima/utils/decrypter_provider.py | 6 +- Sakurajima/utils/downloader.py | 111 ++++++++++++++----------- 3 files changed, 68 insertions(+), 51 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b34ad9..ee19851 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "restructuredtext.confPath": "${workspaceFolder}\\docs\\source", - "python.pythonPath": "C:\\Python38\\python.exe" + "python.pythonPath": "C:\\Python\\python.exe" } \ No newline at end of file diff --git a/Sakurajima/utils/decrypter_provider.py b/Sakurajima/utils/decrypter_provider.py index 65d537f..a29f70d 100644 --- a/Sakurajima/utils/decrypter_provider.py +++ b/Sakurajima/utils/decrypter_provider.py @@ -2,11 +2,15 @@ class DecrypterProvider(object): - def __init__(self, network, m3u8): + 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: diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index 98a4235..c4e9d15 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -6,6 +6,7 @@ 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): """ @@ -204,23 +205,12 @@ def __init__( 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: @@ -237,42 +227,67 @@ 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, chunk_number).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): """Runs the downloader and starts downloading the video file. """ - stateful_segment_list = StatefulSegmentList(self.m3u8.data["segments"]) + + 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): @@ -288,14 +303,12 @@ def remove_chunks(self): """ 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 From 5e1c7a69b3c00b1ab0707ec3923d69178dfab158 Mon Sep 17 00:00:00 2001 From: Dhanraj Hira Date: Mon, 3 Aug 2020 00:39:55 +0530 Subject: [PATCH 15/23] Possibly clean up? --- Sakurajima/utils/downloader.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/Sakurajima/utils/downloader.py b/Sakurajima/utils/downloader.py index cd8c264..c4e9d15 100644 --- a/Sakurajima/utils/downloader.py +++ b/Sakurajima/utils/downloader.py @@ -230,7 +230,6 @@ def init_tracker(self): def assign_segments(self, segment): -<<<<<<< HEAD ChunkDownloader( segment.network, segment.segment, @@ -238,10 +237,6 @@ def assign_segments(self, segment): segment.chunk_number, segment.decrypter_provider ).download() -======= - def assign_target(self, network, segment, file_name, chunk_number, decrypter_provider): - ChunkDownloader(network, segment, file_name, chunk_number, decrypter_provider).download() ->>>>>>> dd21b0f4db8899119feb8b1eb04ad6a7ee9b5603 with self.__lock: self.progress_tracker.update_chunks_done(segment.chunk_number) self.progress_bar.next() @@ -267,7 +262,6 @@ def download(self): self.total_chunks = len(chunk_tuple_list) self.progress_bar = IncrementalBar("Downloading", max=self.total_chunks) self.init_tracker() -<<<<<<< HEAD segment_wrapper_list = [] @@ -294,24 +288,6 @@ def download(self): for future in futures: # This loop servers to run the generator. pass -======= - decrypter_provider = DecrypterProvider(self.__network, self.m3u8) - 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, decrypter_provider),) - ) - self.start_threads() - self.reset_threads() - except IndexError: - if self.threads != []: - self.start_threads() - self.reset_threads() - break ->>>>>>> dd21b0f4db8899119feb8b1eb04ad6a7ee9b5603 self.progress_bar.finish() def merge(self): From 3fed5350272712794a659216463a950ab125cc45 Mon Sep 17 00:00:00 2001 From: Dhanraj Hira Date: Tue, 4 Aug 2020 22:41:21 +0530 Subject: [PATCH 16/23] Now the requests to get m3u8 file includes better referer, also getting the m3u8 file for an episode toggles mark as watched. --- Sakurajima/models/base_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 0e66208..18f242d 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -385,8 +385,9 @@ def get_m3u8(self, quality: str) -> M3U8: return self.__m3u8 else: REFERER = self.__generate_referer() - headers = {"ORIGIN": "https://aniwatch.me"} + headers = {"ORIGIN": "https://aniwatch.me", "REFERER": REFERER} aniwatch_episode = self.get_aniwatch_episode() + self.toggle_mark_as_watched() res = self.__network.get_with_user_session(aniwatch_episode.stream.sources[quality]) self.__m3u8 = M3U8(res.text) return self.__m3u8 From 3cc3520e0a6f46215caf8ebe1d193b79782b8b13 Mon Sep 17 00:00:00 2001 From: veselym Date: Wed, 5 Aug 2020 10:51:37 +0200 Subject: [PATCH 17/23] +add+adxpath --- .gitignore | 3 ++- Sakurajima/api.py | 39 ++++++++++++++++---------------- Sakurajima/models/base_models.py | 32 +++++++++++++------------- Sakurajima/utils/network.py | 6 ++++- 4 files changed, 43 insertions(+), 37 deletions(-) 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/Sakurajima/api.py b/Sakurajima/api.py index 7d9bd95..9e583f6 100644 --- a/Sakurajima/api.py +++ b/Sakurajima/api.py @@ -120,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"] ] ) @@ -134,7 +134,8 @@ 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,) + d = self.network.post(data, f"/anime/{anime_id}") + return Anime(d["anime"], network=self.network, api_url=self.API_URL,) def get_recommendations(self, anime_id: int): """Gets a list of recommendations for an anime. @@ -152,7 +153,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): @@ -186,7 +187,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 @@ -196,7 +197,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 @@ -206,7 +207,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" @@ -215,7 +216,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. @@ -224,7 +225,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. @@ -241,7 +242,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] @@ -256,7 +257,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. @@ -267,7 +268,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. @@ -278,12 +279,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". @@ -294,7 +295,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. @@ -323,7 +324,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, @@ -339,7 +340,7 @@ def get_user_overview(self, user_id): "action": "getOverview", "profile_id": str(user_id), } - 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. @@ -359,7 +360,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): @@ -377,7 +378,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): @@ -395,7 +396,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 = { diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 18f242d..c4bae84 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -93,7 +93,7 @@ def get_episodes(self): 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"] + for data_dict in self.__network.post(data, f"/anime/{self.anime_id}")["episodes"] ] ) return self.__episodes @@ -112,7 +112,7 @@ def get_relations(self): "action": "getRelation", "relation_id": self.relation_id, } - return Relation(self.__network.post(data)["relation"]) + return Relation(self.__network.post(data, f"/anime/{self.anime_id}")["relation"]) def get_recommendations(self): """Gets the recommendations for the anime. @@ -128,7 +128,7 @@ def get_recommendations(self): } 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/{self.anime_id}")["entries"] ] def get_chronicle(self, page=1): @@ -149,7 +149,7 @@ def get_chronicle(self, page=1): } return [ ChronicleEntry(data_dict, self.__network, self.__API_URL) - for data_dict in self.__network.post(data)["chronicle"] + for data_dict in self.__network.post(data, f"/anime/{self.anime_id}")["chronicle"] ] def mark_as_completed(self): @@ -163,7 +163,7 @@ def mark_as_completed(self): "action": "markAsCompleted", "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_plan_to_watch(self): """Marks the anime as "plan to watch" on the user's aniwatch anime list. @@ -176,7 +176,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 +189,7 @@ def mark_as_on_hold(self): "action": "markAsOnHold", "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_dropped(self): """Marks the anime as "dropped" on the user's aniwatch anime list. @@ -202,7 +202,7 @@ def mark_as_dropped(self): "action": "markAsDropped", "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_watching(self): """Marks the anime as "watching" on the user's aniwatch anime list @@ -215,7 +215,7 @@ def mark_as_watching(self): "action": "markAsWatching", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + return self.__network.post(data, f"/anime/{self.anime_id}")["success"] def remove_from_list(self): """Removes the anime from the user's aniwatch anime list. @@ -228,7 +228,7 @@ def remove_from_list(self): "action": "removeAnime", "detail_id": str(self.anime_id), } - return self.__network.post(data)["success"] + return self.__network.post(data, f"/anime/{self.anime_id}")["success"] def rate(self, rating: int): """Set the user's rating for the anime on aniwatch. @@ -246,7 +246,7 @@ def rate(self, rating: int): "detail_id": str(self.anime_id), "rating": rating, } - return self.__network.post(data)["success"] + return self.__network.post(data, f"/anime/{self.anime_id}")["success"] def get_media(self): """Gets the anime's associated media from aniwatch.me @@ -259,7 +259,7 @@ def get_media(self): "action": "getMedia", "detail_id": str(self.anime_id), } - return Media(self.__network.post(data), self.__network, self.anime_id,) + return Media(self.__network.post(data, f"/anime/{self.anime_id}"), self.__network, self.anime_id,) def get_complete_object(self): """Gets the current anime object but with complete attributes. Sometimes, the Anime @@ -275,7 +275,7 @@ def get_complete_object(self): "action": "getAnime", "detail_id": str(self.anime_id), } - data_dict = self.__network.post(data)["anime"] + data_dict = self.__network.post(data, f"/anime/{self.anime_id}")["anime"] return Anime(data_dict, self.__network, api_url=self.__API_URL,) def add_recommendation(self, recommended_anime_id: int): @@ -292,7 +292,7 @@ def add_recommendation(self, recommended_anime_id: int): "detail_id": str(self.anime_id), "recommendation": str(recommended_anime_id), } - return self.__network.post(data) + return self.__network.post(data, f"/anime/{self.anime_id}") def get_dict(self): """Gets the JSON response in the form of a dictionary that was used to @@ -368,7 +368,7 @@ 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) + self.__aniwatch_episode = AniWatchEpisode(self.__network.post(data, f"/anime/{self.anime_id}/{self.number}"), self.ep_id) return self.__aniwatch_episode def get_m3u8(self, quality: str) -> M3U8: @@ -591,7 +591,7 @@ def toggle_mark_as_watched(self): "detail_id": str(self.anime_id), "episode_id": self.ep_id, } - return self.__network.post(data)["success"] + return self.__network.post(data, f"/anime/{self.anime_id}/{self.number}")["success"] def __repr__(self): return f"" diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index c2a94d6..a9fcbe3 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -47,8 +47,12 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi def __repr__(self): return "" - def post(self, data): + def post(self, data, path=None): + if path is None: + print("This endpoint doesn't have a path parameter") + return try: + self.headers['X-PATH'] = path res = self.session.post(self.API_URL, json=data) return res.json() except Exception as e: From 123672f56492174abda41b1215d3ebb5b64c3242 Mon Sep 17 00:00:00 2001 From: Dhanraj Hira Date: Wed, 5 Aug 2020 17:22:04 +0530 Subject: [PATCH 18/23] Better "X-PATH" and "REFERER" implementation. --- Sakurajima/api.py | 24 +++- Sakurajima/errors.py | 3 + Sakurajima/models/base_models.py | 239 ++++++++++++++----------------- Sakurajima/utils/network.py | 8 +- 4 files changed, 136 insertions(+), 138 deletions(-) create mode 100644 Sakurajima/errors.py diff --git a/Sakurajima/api.py b/Sakurajima/api.py index 7d9bd95..764a9a5 100644 --- a/Sakurajima/api.py +++ b/Sakurajima/api.py @@ -21,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: @@ -134,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. @@ -832,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 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 18f242d..bd1bee8 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -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 @@ -72,6 +73,13 @@ def __init__(self, data_dict: dict, network, api_url: str): self.score_rank = data_dict.get("score_rank", None) self.__episodes = None + @staticmethod + def __generate_default_headers(): + 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 +90,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 +127,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 +148,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 +176,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 +199,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. @@ -189,7 +228,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 +243,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 +258,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 +273,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 +293,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 +303,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 +328,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 +351,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 +398,12 @@ 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) + @staticmethod + def __generate_default_headers(): + 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 +425,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,30 +444,14 @@ def get_m3u8(self, quality: str) -> M3U8: if self.__m3u8: return self.__m3u8 else: - REFERER = self.__generate_referer() - headers = {"ORIGIN": "https://aniwatch.me", "REFERER": REFERER} - aniwatch_episode = self.get_aniwatch_episode() + headers = self.__generate_default_headers() self.toggle_mark_as_watched() - res = self.__network.get_with_user_session(aniwatch_episode.stream.sources[quality]) + 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 - 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) - def download( self, quality: str, @@ -498,77 +542,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. @@ -577,7 +550,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 @@ -591,7 +564,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/utils/network.py b/Sakurajima/utils/network.py index c2a94d6..485bb1a 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -24,6 +24,7 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi + '","remember_login":true}' ) headers = { + "ORIGIN": "https://aniwatch.me/", "REFERER": "https://aniwatch.me/", "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", @@ -41,21 +42,22 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi 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_with_user_session(self, uri, headers = None): + def get_with_user_session(self, uri, headers): try: res = self.session.get(uri, headers = headers) return res From c2d1648e9cdce71c5806ff6b8006c9b36762ddcc Mon Sep 17 00:00:00 2001 From: veselym Date: Wed, 5 Aug 2020 17:32:48 +0200 Subject: [PATCH 19/23] Added ANIWATCH_CHAT_SETTINGS to network requests --- Sakurajima/utils/network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 485bb1a..8ef9026 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -23,16 +23,18 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi + str(auth_token) + '","remember_login":true}' ) + 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": 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={xsrf_token};", + "COOKIE": f"SESSION={session_token}; XSRF-TOKEN={xsrf_token}; ANIWATCH_CHAT_SETTINGS={chat_cookie};", "X-AUTH": auth_token } cookies = { + "ANIWATCH_CHAT_SETTINGS": chat_cookie. "SESSION": session_token, "XSRF-TOKEN": xsrf_token } From 0235da0d53fa0b1a419f50d4793543edf1175fcf Mon Sep 17 00:00:00 2001 From: veselym Date: Wed, 5 Aug 2020 17:40:31 +0200 Subject: [PATCH 20/23] Made xsrf token accessible outside of network.__init__ --- Sakurajima/utils/network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index 8ef9026..b31f621 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -12,7 +12,7 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi self.session.proxies = proxies self.headers = self.session.headers # Expose session headers self.cookies = self.session.cookies # Expose session cookies - xsrf_token = Misc().generate_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: session_token = urllib.parse.quote( '{"userid":' @@ -27,16 +27,16 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi headers = { "ORIGIN": "https://aniwatch.me/", "REFERER": "https://aniwatch.me/", - "X-XSRF-TOKEN": xsrf_token, + "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={xsrf_token}; ANIWATCH_CHAT_SETTINGS={chat_cookie};", + "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. + "ANIWATCH_CHAT_SETTINGS": chat_cookie, "SESSION": session_token, - "XSRF-TOKEN": xsrf_token + "XSRF-TOKEN": self.xsrf_token } self.session.headers.update(headers) From 21f97f222e094076f945146629327e99d7a84b08 Mon Sep 17 00:00:00 2001 From: veselym Date: Wed, 5 Aug 2020 17:42:11 +0200 Subject: [PATCH 21/23] Disclaimer --- README.md | 4 ++++ 1 file changed, 4 insertions(+) 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. From 7106fcb514f77d774836b9581edd04b9ed72a478 Mon Sep 17 00:00:00 2001 From: veselym Date: Thu, 6 Aug 2020 08:06:44 +0200 Subject: [PATCH 22/23] Fixed some typos --- Sakurajima/models/base_models.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Sakurajima/models/base_models.py b/Sakurajima/models/base_models.py index 2c5c652..dfd53bb 100644 --- a/Sakurajima/models/base_models.py +++ b/Sakurajima/models/base_models.py @@ -73,8 +73,7 @@ def __init__(self, data_dict: dict, network, api_url: str): self.score_rank = data_dict.get("score_rank", None) self.__episodes = None - @staticmethod - def __generate_default_headers(): + def __generate_default_headers(self): return { "X-PATH": f"/anime/{self.anime_id}", "REFERER": f"https://aniwatch.me/anime/{self.anime_id}" @@ -99,7 +98,7 @@ def get_episodes(self): "detail_id": str(self.anime_id), } headers = self.__generate_default_headers() - json = self.network.post(data, headers) + json = self.__network.post(data, headers) if json.get("success", True) != True: error = json["error"] @@ -398,8 +397,7 @@ def __init__(self, data_dict, network, api_url, anime_id, anime_title=None): self.__aniwatch_episode = None self.__m3u8 = None - @staticmethod - def __generate_default_headers(): + 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}" @@ -444,13 +442,16 @@ def get_m3u8(self, quality: str) -> M3U8: if self.__m3u8: return self.__m3u8 else: - 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 + 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, From 79eabd3bab48e1d1b6372c54da6f851bec0f719c Mon Sep 17 00:00:00 2001 From: Marek Vesely Date: Tue, 25 Aug 2020 18:14:39 +0200 Subject: [PATCH 23/23] Minor fixes, updated setup.py --- Sakurajima/api.py | 2 +- Sakurajima/utils/network.py | 2 +- setup.py | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sakurajima/api.py b/Sakurajima/api.py index 9d5942c..e752949 100644 --- a/Sakurajima/api.py +++ b/Sakurajima/api.py @@ -846,7 +846,7 @@ def search(self, query: str): "REFERER": f"https://aniwatch.me/search" } json = self.network.post(data, headers) - if json.get("success", True) != True: + if type(json) == dict and json.get("success", True) != True: error = json["error"] raise AniwatchError(error) else: diff --git a/Sakurajima/utils/network.py b/Sakurajima/utils/network.py index b31f621..637c6f4 100644 --- a/Sakurajima/utils/network.py +++ b/Sakurajima/utils/network.py @@ -19,7 +19,7 @@ def __init__(self, username: str, user_id: str, auth_token: str, proxies, endpoi + 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}' ) diff --git a/setup.py b/setup.py index e5ca162..8a9b884 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,11 @@ setup( name="sakurajima", - version="0.2.0", + version="0.2.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,8 +19,9 @@ ], python_requires=">=3.6", install_requires=[ - "requests==2.23.0", - "pycryptodome==3.9.7", - "m3u8==0.6.0", + "requests>=2.23.0", + "pycryptodome>=3.9.7", + "m3u8>=0.6.0", + "pathvalidate>=2.3.0" ], )