diff --git a/animeworld/episodio.py b/animeworld/episodio.py index 2aebca5..a9a963e 100644 --- a/animeworld/episodio.py +++ b/animeworld/episodio.py @@ -5,6 +5,7 @@ from bs4 import BeautifulSoup from typing import * import time +import io from .utility import SES from .exceptions import ServerNotSupported, HardStoppedDownload @@ -101,7 +102,7 @@ def fileInfo(self) -> Dict[str,str]: raise err - def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: # Scarica l'episodio con il primo link nella lista + def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: # Scarica l'episodio con il primo link nella lista """ Scarica l'episodio dal server più veloce. diff --git a/animeworld/servers/AnimeWorld_Server.py b/animeworld/servers/AnimeWorld_Server.py index 963016e..f7301dc 100644 --- a/animeworld/servers/AnimeWorld_Server.py +++ b/animeworld/servers/AnimeWorld_Server.py @@ -43,7 +43,7 @@ def fileInfo(self) -> Dict[str,str]: return self._fileInfoIn() - def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: + def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: """ Scarica l'episodio. diff --git a/animeworld/servers/Server.py b/animeworld/servers/Server.py index 27968bf..735674b 100644 --- a/animeworld/servers/Server.py +++ b/animeworld/servers/Server.py @@ -1,5 +1,5 @@ -import os, time -from typing import Dict, List, Optional, Callable +import os, time, io +from typing import Dict, List, Optional, Callable, Union from datetime import datetime import youtube_dl @@ -143,7 +143,7 @@ def error(self, msg): } - def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: + def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: """ Scarica l'episodio dal primo server funzionante della lista links. @@ -180,7 +180,7 @@ def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[ raise ServerNotSupported(self.name) # Protected - def _downloadIn(self, title: str, folder: str, *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: # Scarica l'episodio + def _downloadIn(self, title: str, folder: Union[str, io.IOBase], *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: # Scarica l'episodio """ Scarica il file utilizzando httpx. @@ -225,48 +225,56 @@ def _downloadIn(self, title: str, folder: str, *, hook: Callable[[Dict], None], start = time.time() step = time.time() + fd:io.IOBase = None + if isinstance(folder, io.IOBase): fd = folder + else: fd = open(f"{os.path.join(folder,file)}", 'wb') + try: - with open(f"{os.path.join(folder,file)}", 'wb') as f: - for chunk in r.iter_bytes(chunk_size = 524288): - if chunk: - f.write(chunk) - f.flush() - - current_lenght += len(chunk) - - hook({ - 'total_bytes': total_length, - 'downloaded_bytes': current_lenght, - 'percentage': current_lenght/total_length, - 'speed': len(chunk) / (time.time() - step) if (time.time() - step) != 0 else 0, - 'elapsed': time.time() - start, - 'filename': file, - 'eta': ((total_length - current_lenght) / len(chunk)) * (time.time() - step), - 'status': 'downloading' if "abort" not in opt else "aborted" - }) - - if "abort" in opt: raise HardStoppedDownload(file) - - step = time.time() - - else: + for chunk in r.iter_bytes(chunk_size = 524288): + if chunk: + fd.write(chunk) + fd.flush() + + current_lenght += len(chunk) + hook({ 'total_bytes': total_length, - 'downloaded_bytes': total_length, - 'percentage': 1, - 'speed': 0, + 'downloaded_bytes': current_lenght, + 'percentage': current_lenght/total_length, + 'speed': len(chunk) / (time.time() - step) if (time.time() - step) != 0 else 0, 'elapsed': time.time() - start, - 'eta': 0, - 'status': 'finished' + 'filename': file, + 'eta': ((total_length - current_lenght) / len(chunk)) * (time.time() - step), + 'status': 'downloading' if "abort" not in opt else "aborted" }) - return file # Se il file è stato scaricato correttamente + if "abort" in opt: raise HardStoppedDownload(file) + + step = time.time() + + else: + hook({ + 'total_bytes': total_length, + 'downloaded_bytes': total_length, + 'percentage': 1, + 'speed': 0, + 'elapsed': time.time() - start, + 'eta': 0, + 'status': 'finished' + }) + + if isinstance(folder, str): fd.close() + else: fd.seek(0) + return file # Se il file è stato scaricato correttamente except HardStoppedDownload: - os.remove(f"{os.path.join(folder,file)}") + if isinstance(folder, str): + fd.close() + os.remove(f"{os.path.join(folder,file)}") + else: fd.seek(0) return None # Protected - def _dowloadEx(self, title: str, folder: str, *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: + def _dowloadEx(self, title: str, folder: Union[str, io.IOBase], *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: """ Scarica il file utilizzando yutube_dl. @@ -300,6 +308,9 @@ def _dowloadEx(self, title: str, folder: str, *, hook: Callable[[Dict], None], o ``` """ + tmp = '' + if isinstance(folder, str): tmp = folder + class MyLogger(object): def debug(self, msg): pass @@ -325,7 +336,7 @@ def my_hook(d): if "abort" in opt: raise HardStoppedDownload(d['filename']) ydl_opts = { - 'outtmpl': f"{os.path.join(folder,title)}.%(ext)s", + 'outtmpl': f"{os.path.join(tmp,title)}.%(ext)s", 'logger': MyLogger(), 'progress_hooks': [my_hook], } @@ -336,6 +347,11 @@ def my_hook(d): try: ydl.download([url]) except HardStoppedDownload: - os.remove(f"{os.path.join(folder,filename)}") + os.remove(f"{os.path.join(tmp,filename)}") return None + if isinstance(folder, io.IOBase): + with open(os.path.join(tmp,filename), 'rb') as f: + folder.write(f.read()) + f.seek(0) + os.remove(f"{os.path.join(tmp,filename)}") return filename \ No newline at end of file diff --git a/animeworld/servers/Streamtape.py b/animeworld/servers/Streamtape.py index 2fa1e93..dade51e 100644 --- a/animeworld/servers/Streamtape.py +++ b/animeworld/servers/Streamtape.py @@ -57,7 +57,7 @@ def fileInfo(self) -> Dict[str,str]: return self._fileInfoIn() - def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: + def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: """ Scarica l'episodio. diff --git a/animeworld/servers/YouTube.py b/animeworld/servers/YouTube.py index 9e900f9..5764529 100644 --- a/animeworld/servers/YouTube.py +++ b/animeworld/servers/YouTube.py @@ -55,7 +55,7 @@ def fileInfo(self) -> Dict[str,str]: return self._fileInfoEx() - def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: + def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: """ Scarica l'episodio. diff --git a/docs/usage/advanced.md b/docs/usage/advanced.md index dfd432c..3a41c8b 100644 --- a/docs/usage/advanced.md +++ b/docs/usage/advanced.md @@ -179,6 +179,24 @@ episodio.download(opt=opt) # Avvio il download In questo esempio il download viene fermato dopo 5 secondi. +### I/O Buffer + +È possibile scaricare un episodio usando direttamente un descrittore di file invece che una stringa per la directory. Basta passare al parametro `folder` un tipo [IOBase](https://docs.python.org/3/library/io.html#i-o-base-classes). + +```py linenums="1" +import animeworld as aw +import io + +anime = aw.Anime("...") +episodio = anime.getEpisodes()[0] + +buffer = io.BytesIO() # Alloco un buffer in memoria + +episodio.download(folder=buffer) # Avvio il download +``` + +In questo esempio l'episodio scaricato viene scritto in memoria senza essere salvato come file. + --- ## Esempio completo diff --git a/setup.py b/setup.py index 4c45e8f..1585f10 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="animeworld", - version="1.6.3", + version="1.6.4", author="MainKronos", description="AnimeWorld UNOFFICIAL API", long_description=long_description, diff --git a/tests/test_animeworld.py b/tests/test_animeworld.py index 3fe13b2..1610d90 100644 --- a/tests/test_animeworld.py +++ b/tests/test_animeworld.py @@ -1,7 +1,8 @@ import unittest import unittest -import random +import random, io, time +from threading import Thread import animeworld as aw from animeworld.servers import AnimeWorld_Server, Streamtape @@ -102,45 +103,86 @@ def test_anime(self): self.assertIn('Durata', info) self.assertIn('Episodi', info) self.assertIn('Stato', info) - self.assertIn('Visualizzazioni', info) + self.assertIn('Visualizzazioni', info) - def test_servers(self): - """ - Controlli relativi ai server. - """ + def test_episodio(self): ep = random.choice(self.anime.getEpisodes()) - servers = ep.links - - animeworld_server = [e for e in servers if isinstance(e, AnimeWorld_Server)][0] - streamtape_server = [e for e in servers if isinstance(e, Streamtape)][0] - - self.assertEqual(animeworld_server.Nid, 9) - self.assertEqual(animeworld_server.name, "AnimeWorld Server") - - self.assertEqual(streamtape_server.Nid, 8) - self.assertEqual(streamtape_server.name, "Streamtape") - - self.assertIsInstance(animeworld_server.fileLink(), str) - self.assertIsInstance(streamtape_server.fileLink(), str) - - animeworld_info = animeworld_server.fileInfo() - self.assertIsInstance(animeworld_info, dict) - self.assertIn("content_type", animeworld_info) - self.assertIn("total_bytes", animeworld_info) - self.assertIn("last_modified", animeworld_info) - self.assertIn("server_name", animeworld_info) - self.assertIn("server_id", animeworld_info) - self.assertIn("url", animeworld_info) - - streamtape_info = streamtape_server.fileInfo() - self.assertIsInstance(animeworld_info, dict) - self.assertIn("content_type", streamtape_info) - self.assertIn("total_bytes", streamtape_info) - self.assertIn("last_modified", streamtape_info) - self.assertIn("server_name", streamtape_info) - self.assertIn("server_id", streamtape_info) - self.assertIn("url", streamtape_info) + self.assertIsInstance(ep, aw.Episodio) + self.assertIsInstance(ep.number, str) + self.assertIsInstance(ep.links, list) + +class TestServer(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + """Sceglie un episodio per i test.""" + cls.episodio = random.choice(aw.Anime("https://www.animeworld.so/play/fullmetal-alchemist-brotherhood.4vGGQ").getEpisodes()) + + @staticmethod + def stopDownload(opt:list): + time.sleep(1) + opt.append("abort") + + def test_AnimeWorld_Server(self) -> None: + + servers = [e for e in self.episodio.links if isinstance(e, AnimeWorld_Server)] + + if len(servers) == 0: + self.skipTest('Il server AnimeWorld_Server non esiste in questo episodio.') + return + + server = servers[0] + + self.assertEqual(server.Nid, 9) + self.assertEqual(server.name, "AnimeWorld Server") + self.assertIsInstance(server.fileLink(), str) + + info = server.fileInfo() + self.assertIsInstance(info, dict) + self.assertIn("content_type", info) + self.assertIn("total_bytes", info) + self.assertIn("last_modified", info) + self.assertIn("server_name", info) + self.assertIn("server_id", info) + self.assertIn("url", info) + + with self.subTest('Animeworld_Server Download'): + buf = io.BytesIO() + opt = [] + Thread(target=self.stopDownload, args=(opt,)).start() + self.assertIsNone(server.download(folder=buf, opt=opt)) + buf.close() + + def test_Streamtape(self) -> None: + servers = [e for e in self.episodio.links if isinstance(e, Streamtape)] + + if len(servers) == 0: + self.skipTest('Il server Streamtape non esiste in questo episodio.') + return + + server = servers[0] + + self.assertEqual(server.Nid, 8) + self.assertEqual(server.name, "Streamtape") + + + self.assertIsInstance(server.fileLink(), str) + + info = server.fileInfo() + self.assertIsInstance(info, dict) + self.assertIn("content_type", info) + self.assertIn("total_bytes", info) + self.assertIn("last_modified", info) + self.assertIn("server_name", info) + self.assertIn("server_id", info) + self.assertIn("url", info) + + with self.subTest('Streamtape Download'): + buf = io.BytesIO() + opt = [] + Thread(target=self.stopDownload, args=(opt,)).start() + self.assertIsNone(server.download(folder=buf, opt=opt)) + buf.close() if __name__ == '__main__': unittest.main(verbosity=2) \ No newline at end of file