├── tests ├── __init__.py └── test_animeworld.py ├── .gitignore ├── requirements.txt ├── docs ├── static │ ├── img │ │ └── AnimeWorld-API.png │ ├── server.txt │ └── example.py ├── api-reference │ ├── exceptions.md │ └── developer-interface.md ├── index.it.md ├── index.en.md ├── community │ ├── code-of-conduct.md │ ├── contributing.en.md │ └── contributing.it.md └── usage │ ├── quickstart.en.md │ ├── quickstart.it.md │ ├── advanced.en.md │ └── advanced.it.md ├── setup.py ├── animeworld ├── servers │ ├── __init__.py │ ├── AnimeWorld_Server.py │ ├── YouTube.py │ ├── Streamtape.py │ └── Server.py ├── __init__.py ├── exceptions.py ├── utility.py ├── episodio.py └── anime.py ├── .vscode └── settings.json ├── .github ├── FUNDING.yml └── workflows │ ├── deploy-mkdocs.yml │ └── python-publish.yml ├── LICENSE ├── pyproject.toml ├── mkdocs.yml ├── README.it.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bat 2 | animeworld.egg-info/ 3 | build/ 4 | dist/ 5 | *.pyc 6 | *.key 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | httpx[http2] 3 | youtube_dl 4 | beautifulsoup4 5 | certifi==2025.1.31 -------------------------------------------------------------------------------- /docs/static/img/AnimeWorld-API.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainKronos/AnimeWorld-API/HEAD/docs/static/img/AnimeWorld-API.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | 4 | setuptools.setup( 5 | version=os.environ["RELEASE_VERSION"].replace("v","",1) 6 | ) -------------------------------------------------------------------------------- /animeworld/servers/__init__.py: -------------------------------------------------------------------------------- 1 | from .AnimeWorld_Server import AnimeWorld_Server 2 | from .Streamtape import Streamtape 3 | from .YouTube import YouTube -------------------------------------------------------------------------------- /docs/static/server.txt: -------------------------------------------------------------------------------- 1 | ||Server|| 2 | |:---|:---|:---| 3 | |✔️ AnimeWorld_Server|❌ StreamHide|❌ Doodstream| 4 | |✔️ Streamtape|❌ FileMoon|❌ StreamSB| 5 | |✔️ YouTube|❌ Streamtape|❌ Streamlare| -------------------------------------------------------------------------------- /animeworld/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AnimeWorld-API 3 | """ 4 | from .utility import find, SES 5 | from .anime import Anime 6 | from .episodio import Episodio 7 | from .servers.Server import Server 8 | from .exceptions import ServerNotSupported, AnimeNotAvailable, Error404, DeprecatedLibrary -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestArgs": [ 3 | "-v", 4 | "-s", 5 | "./tests", 6 | "-p", 7 | "test_*.py" 8 | ], 9 | "python.testing.pytestEnabled": false, 10 | "python.testing.unittestEnabled": true 11 | } 12 | 13 | // python -m unittest discover -v -s ./tests -p test_*.py -------------------------------------------------------------------------------- /docs/api-reference/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | ## ::: animeworld.exceptions.ServerNotSupported 4 | 5 | ## ::: animeworld.exceptions.AnimeNotAvailable 6 | 7 | ## ::: animeworld.exceptions.Error404 8 | 9 | ## ::: animeworld.exceptions.DeprecatedLibrary 10 | 11 | ## ::: animeworld.exceptions.HardStoppedDownload 12 | 13 | -------------------------------------------------------------------------------- /docs/api-reference/developer-interface.md: -------------------------------------------------------------------------------- 1 | --- 2 | search: 3 | boost: 2 4 | --- 5 | 6 | # Developer Interface 7 | 8 | ## Globals 9 | 10 | ### ::: animeworld.SES 11 | 12 | ## Funzioni 13 | 14 | ### ::: animeworld.find 15 | 16 | ## Classi 17 | 18 | ### ::: animeworld.Anime 19 | 20 | ### ::: animeworld.Episodio 21 | options: 22 | merge_init_into_class: false 23 | 24 | ### ::: animeworld.Server 25 | options: 26 | merge_init_into_class: false 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MainKronos 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-Fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: ### 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Krónos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "animeworld" 7 | dynamic = ["version"] 8 | description = "AnimeWorld UNOFFICIAL API" 9 | dependencies = [ 10 | "httpx", 11 | "httpx[http2]", 12 | "youtube_dl", 13 | "beautifulsoup4" 14 | ] 15 | readme = {file = "README.md", content-type = "text/markdown"} 16 | requires-python = ">= 3.11" 17 | license = {file = "LICENSE"} 18 | keywords = ["anime", "animeworld", "animeworld-api", "api"] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Intended Audience :: Developers", 22 | "Topic :: Internet :: WWW/HTTP :: Indexing/Search", 23 | "Topic :: Software Development :: Libraries", 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License" 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://mainkronos.github.io/AnimeWorld-API/" 30 | Documentation = "https://mainkronos.github.io/AnimeWorld-API/" 31 | Repository = "https://github.com/MainKronos/AnimeWorld-API.git" 32 | Issues = "https://github.com/MainKronos/AnimeWorld-API/issues" 33 | -------------------------------------------------------------------------------- /animeworld/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modulo per le eccezioni. 3 | """ 4 | 5 | 6 | class ServerNotSupported(Exception): 7 | """Il server da dove si tenta di scaricare l'episodio non è supportato.""" 8 | def __init__(self, server): 9 | self.server = server 10 | self.message = f"Il server {server} non è supportato." 11 | super().__init__(self.message) 12 | 13 | class AnimeNotAvailable(Exception): 14 | """L'anime non è ancora disponibile.""" 15 | def __init__(self, animeName=''): 16 | self.anime = animeName 17 | self.message = f"L'anime '{animeName}' non è ancora disponibile." 18 | super().__init__(self.message) 19 | 20 | class Error404(Exception): 21 | """Il link porta ad una pagina inesistente.""" 22 | def __init__(self, link): 23 | self.link = link 24 | self.message = f"Il link '{link}' porta ad una pagina inesistente." 25 | super().__init__(self.message) 26 | 27 | class DeprecatedLibrary(Exception): 28 | """Libreria deprecata a causa di un cambiamento della struttura del sito.""" 29 | def __init__(self, file, funName, line): 30 | self.funName = funName 31 | self.line = line 32 | self.message = f"Il sito è cambiato, di conseguenza la libreria è DEPRECATA. -> [File {file} in {funName} - {line}]" 33 | super().__init__(self.message) 34 | 35 | class HardStoppedDownload(Exception): 36 | """Il file in download è stato forzatamente interrotto.""" 37 | def __init__(self, file:str): 38 | self.file = file 39 | self.message = f"Il file in download ({file}) è stato forzatamente interrotto." 40 | super().__init__(self.message) -------------------------------------------------------------------------------- /.github/workflows/deploy-mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy MkDocs 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | # Build job 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | - name: Setup Pages 29 | uses: actions/configure-pages@v5 30 | - uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.x 33 | - name: Setup dependencies 34 | run: pip install --upgrade pip && pip install mkdocs mkdocs-material mkdocstrings mkdocstrings[python] black mkdocs-git-revision-date-localized-plugin mkdocs-static-i18n[material] 35 | - name: Build with MkDocs 36 | run: | 37 | mkdocs build -c -d _site 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: ./_site 42 | # Deployment job 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | runs-on: ubuntu-latest 48 | needs: build 49 | steps: 50 | - name: Deploy to GitHub Pages 51 | id: deployment 52 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /docs/index.it.md: -------------------------------------------------------------------------------- 1 |

2 | AnimeWorld-API 3 |

4 | 5 | --- 6 | 7 | # Welcome to AnimeWorld-API wiki! 8 | 9 | AnimeWorld-API is an UNOFFICIAL library for AnimeWorld (Italian anime site). 10 | 11 | --- 12 | 13 | Installa la libreria tramite pip: 14 | ```bash 15 | $ pip install animeworld 16 | ``` 17 | 18 | Adesso puoi iniziare a cercare anime: 19 | ```python 20 | >>> import animeworld as aw 21 | >>> res = aw.find("No game no life") 22 | >>> res 23 | {'name': 'No Game no Life', 'link': 'https://www.animeworld.ac/play/no-game-no-life.IJUH1E', ...} 24 | ``` 25 | 26 | E a scaricare episodi: 27 | ```python 28 | >>> import animeworld as aw 29 | >>> # https://www.animeworld.ac/play/danmachi-3.Ydt8- 30 | >>> anime = aw.Anime(link="/play/danmachi-3.Ydt8-") 31 | >>> for episodio in anime.getEpisodes(): 32 | ... print("Episodio Numero: ", episodio.number) 33 | ... if(episodio.download()): 34 | ... print("Download completato") 35 | ``` 36 | 37 | ## Documentazione 38 | 39 | Per una panoramica di tutte le nozioni di base, vai alla sezione [QuickStart](usage/quickstart.md) 40 | 41 | Per argomenti più avanzati, vedere la sezione [Advanced Usage](usage/advanced.md) 42 | 43 | La sezione [API Reference](api-reference/developer-interface.md) fornisce un riferimento API completo. 44 | 45 | Se vuoi contribuire al progetto, vai alla sezione [Contributing](community/contributing.md) 46 | 47 | ## Dipendenze 48 | 49 | - [`httpx`](https://github.com/encode/httpx) - A next generation HTTP client for Python. 50 | 51 | - [`youtube_dl`](https://github.com/ytdl-org/youtube-dl) - Command-line program to download videos from YouTube.com and other video sites. 52 | 53 | - [`beautifulsoup4`](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - Beautiful Soup is a Python library designed for quick turnaround projects like screen-scraping. -------------------------------------------------------------------------------- /docs/index.en.md: -------------------------------------------------------------------------------- 1 |

2 | AnimeWorld-API 3 |

4 | 5 | --- 6 | 7 | # Welcome to AnimeWorld-API wiki! 8 | 9 | AnimeWorld-API is an UNOFFICIAL library for AnimeWorld (Italian anime site). 10 | 11 | --- 12 | 13 | ## Installation 14 | Install the library using pip: 15 | ```bash 16 | $ pip install animeworld 17 | ``` 18 | 19 | Now you can start searching for anime: 20 | ```python 21 | >>> import animeworld as aw 22 | >>> res = aw.find("No game no life") 23 | >>> res 24 | {'name': 'No Game no Life', 'link': 'https://www.animeworld.ac/play/no-game-no-life.IJUH1E', ...} 25 | ``` 26 | 27 | And download episodes: 28 | ```python 29 | >>> import animeworld as aw 30 | >>> # https://www.animeworld.ac/play/danmachi-3.Ydt8- 31 | >>> anime = aw.Anime(link="/play/danmachi-3.Ydt8-") 32 | >>> for episode in anime.getEpisodes(): 33 | ... print("Episode Number: ", episode.number) 34 | ... if(episode.download()): 35 | ... print("Download completed") 36 | ``` 37 | 38 | ## Documentation 39 | 40 | For an overview of all the basics, go to the [QuickStart](usage/quickstart.md) section. 41 | 42 | For more advanced topics, see the [Advanced Usage](usage/advanced.md) section. 43 | 44 | The [API Reference](api-reference/developer-interface.md) section provides a complete API reference. 45 | 46 | If you want to contribute to the project, visit the [Contributing](community/contributing.md) section. 47 | 48 | ## Dependencies 49 | 50 | - [`httpx`](https://github.com/encode/httpx) - A next-generation HTTP client for Python. 51 | 52 | - [`youtube_dl`](https://github.com/ytdl-org/youtube-dl) - Command-line program to download videos from YouTube.com and other video sites. 53 | 54 | - [`beautifulsoup4`](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - Beautiful Soup is a Python library designed for quick turnaround projects like screen-scraping. -------------------------------------------------------------------------------- /docs/static/example.py: -------------------------------------------------------------------------------- 1 | import animeworld as aw 2 | from datetime import datetime 3 | 4 | 5 | def my_hook(d): 6 | """ 7 | Stampa una ProgressBar con tutte le informazioni di download. 8 | """ 9 | if d['status'] == 'downloading': 10 | out = "{filename}:\n[{bar}][{percentage:^6.1%}]\n{downloaded_bytes}/{total_bytes} in {elapsed:%H:%M:%S} (ETA: {eta:%H:%M:%S})\x1B[3A" 11 | 12 | width = 70 # grandezza progressbar 13 | 14 | d['elapsed'] = datetime.utcfromtimestamp(d['elapsed']) 15 | d['eta'] = datetime.utcfromtimestamp(d['eta']) 16 | d['bar'] = '#'*int(width*d['percentage']) + ' '*(width-int(width*d['percentage'])) 17 | 18 | print(out.format(**d)) 19 | 20 | elif d['status'] == 'finished': 21 | print('\n\n\n') 22 | 23 | 24 | try: 25 | # Imposto il base_url per la sessione 26 | aw.SES.base_url = "https://www.animeworld.ac" 27 | 28 | anime = aw.Anime(link="/play/tokyo-revengers-seiya-kessen-hen.tzgly") 29 | 30 | print("Titolo:", anime.getName()) # Titolo dell'anime 31 | 32 | print("\n----------------------------------\n") 33 | 34 | print("Trama:", anime.getTrama()) # Trama 35 | 36 | print("\n----------------------------------\n") 37 | 38 | print("Info:") 39 | info = anime.getInfo() 40 | for x in info: print(f"{x}: {info[x]}") # Informazioni presenti su Animeworld riguardanti l'anime 41 | 42 | print("\n----------------------------------\n") 43 | 44 | print("Episodi:") 45 | try: 46 | episodi = anime.getEpisodes() 47 | except aw.AnimeNotAvailable as error: 48 | print("Errore:", error) 49 | else: 50 | for x in episodi: 51 | print(f"\n-> Ep. {x.number}") 52 | for k in x.links: 53 | print(f"\t{k.name} - {k.link}") 54 | 55 | if x.number == '1': 56 | print("\n\tFile info: {\n\t\t" + "\n\t\t".join("{}: {}".format(k, v) for k, v in x.fileInfo().items()) + "\n\t}") 57 | x.download(hook=my_hook) 58 | break 59 | except (aw.DeprecatedLibrary, aw.Error404, aw.ServerNotSupported) as error: 60 | print(error) 61 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Publish to PyPI 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | inputs: 11 | version: 12 | description: 'Versione' 13 | required: true 14 | 15 | jobs: 16 | 17 | build: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | steps: 22 | - name: Check Out Repo 23 | uses: actions/checkout@v4 24 | - name: Set env 25 | if: github.event_name != 'workflow_dispatch' 26 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 27 | - name: Set env 28 | if: github.event_name == 'workflow_dispatch' 29 | run: echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.x' 34 | cache: 'pip' # caching pip dependencies 35 | - name: Install dependencies 36 | run: | 37 | python3 -m pip install build --user 38 | - name: Build a binary wheel and a source tarball 39 | run: python3 -m build 40 | - name: Store the distribution packages 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: python-package-distributions 44 | path: dist/ 45 | 46 | deploy: 47 | runs-on: ubuntu-latest 48 | needs: build 49 | environment: 50 | name: pypi 51 | url: https://pypi.org/project/animeworld/ 52 | permissions: 53 | id-token: write 54 | steps: 55 | - name: Download all the dists 56 | uses: actions/download-artifact@v4 57 | with: 58 | name: python-package-distributions 59 | path: dist/ 60 | - name: Publish distribution 📦 to PyPI 61 | uses: pypa/gh-action-pypi-publish@release/v1 62 | -------------------------------------------------------------------------------- /animeworld/servers/AnimeWorld_Server.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import re 3 | 4 | from .Server import * 5 | from ..utility import HealthCheck 6 | 7 | class AnimeWorld_Server(Server): 8 | @HealthCheck 9 | def fileLink(self) -> str: 10 | """ 11 | Recupera il link diretto per il download del file dell'episodio. 12 | 13 | Returns: 14 | Link diretto. 15 | 16 | Example: 17 | ```py 18 | return str # Link del file 19 | ``` 20 | """ 21 | 22 | return self.link.replace('download-file.php?id=', '') 23 | 24 | def fileInfo(self) -> Dict[str,str]: 25 | """ 26 | Recupera le informazioni del file dell'episodio. 27 | 28 | Returns: 29 | Informazioni file episodio. 30 | 31 | Example: 32 | ```py 33 | return { 34 | "content_type": str, # Tipo del file, es. video/mp4 35 | "total_bytes": int, # Byte totali del file 36 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 37 | "server_name": str, # Nome del server 38 | "server_id": int, # ID del server 39 | "url": str # url dell'episodio 40 | } 41 | ``` 42 | """ 43 | 44 | return self._fileInfoIn() 45 | 46 | def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: 47 | """ 48 | Scarica l'episodio. 49 | 50 | Args: 51 | title: Nome con cui verrà nominato il file scaricato. 52 | folder: Posizione in cui verrà spostato il file scaricato. 53 | 54 | Other parameters: 55 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 56 | - `total_bytes`: Byte totali da scaricare. 57 | - `downloaded_bytes`: Byte attualmente scaricati. 58 | - `percentage`: Percentuale del progresso di download. 59 | - `speed`: Velocità di download (byte/s) 60 | - `elapsed`: Tempo trascorso dall'inizio del download. 61 | - `eta`: Tempo stimato rimanente per fine del download. 62 | - `status`: 'downloading' | 'finished' | 'aborted' 63 | - `filename`: Nome del file in download. 64 | 65 | opt: Lista per delle opzioni aggiuntive.\n 66 | - `'abort'`: Ferma forzatamente il download. 67 | 68 | Returns: 69 | Nome del file scaricato. 70 | 71 | Raises: 72 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 73 | 74 | Example: 75 | ```py 76 | return str # File scaricato 77 | ``` 78 | """ 79 | if title is None: title = self._defTitle 80 | else: title = self._sanitize(title) 81 | return self._downloadIn(title,folder,hook=hook,opt=opt) 82 | -------------------------------------------------------------------------------- /animeworld/servers/YouTube.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import re 3 | 4 | from .Server import * 5 | from ..utility import HealthCheck 6 | 7 | class YouTube(Server): 8 | @HealthCheck 9 | def fileLink(self) -> str: 10 | """ 11 | Recupera il link diretto per il download del file dell'episodio. 12 | 13 | Returns: 14 | Link diretto. 15 | 16 | Example: 17 | ```py 18 | return str # Link del file 19 | ``` 20 | """ 21 | 22 | anime_id = self.link.split("/")[-1] 23 | external_link = str(SES.build_url(f"/api/episode/serverPlayerAnimeWorld?id={anime_id}")) 24 | 25 | sb_get = SES.get(self.link) 26 | sb_get.raise_for_status() 27 | 28 | sb_get = SES.get(external_link) 29 | soupeddata = BeautifulSoup(sb_get.content, "html.parser") 30 | sb_get.raise_for_status() 31 | 32 | yutubelink_raw = re.search(r'"(https:\/\/www\.youtube\.com\/embed\/.+)"\);', soupeddata.prettify()).group(1) 33 | 34 | return yutubelink_raw.replace('embed/', 'watch?v=') 35 | 36 | def fileInfo(self) -> Dict[str,str]: 37 | """ 38 | Recupera le informazioni del file dell'episodio. 39 | 40 | Returns: 41 | Informazioni file episodio. 42 | 43 | Example: 44 | ```py 45 | return { 46 | "content_type": str, # Tipo del file, es. video/mp4 47 | "total_bytes": int, # Byte totali del file 48 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 49 | "server_name": str, # Nome del server 50 | "server_id": int, # ID del server 51 | "url": str # url dell'episodio 52 | } 53 | ``` 54 | """ 55 | 56 | return self._fileInfoEx() 57 | 58 | def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: 59 | """ 60 | Scarica l'episodio. 61 | 62 | Args: 63 | title: Nome con cui verrà nominato il file scaricato. 64 | folder: Posizione in cui verrà spostato il file scaricato. 65 | 66 | Other parameters: 67 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 68 | - `total_bytes`: Byte totali da scaricare. 69 | - `downloaded_bytes`: Byte attualmente scaricati. 70 | - `percentage`: Percentuale del progresso di download. 71 | - `speed`: Velocità di download (byte/s) 72 | - `elapsed`: Tempo trascorso dall'inizio del download. 73 | - `eta`: Tempo stimato rimanente per fine del download. 74 | - `status`: 'downloading' | 'finished' | 'aborted' 75 | - `filename`: Nome del file in download. 76 | 77 | opt: Lista per delle opzioni aggiuntive.\n 78 | - `'abort'`: Ferma forzatamente il download. 79 | 80 | Returns: 81 | Nome del file scaricato. 82 | 83 | Raises: 84 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 85 | 86 | Example: 87 | ```py 88 | return str # File scaricato 89 | ``` 90 | """ 91 | if title is None: title = self._defTitle 92 | else: title = self._sanitize(title) 93 | return self._dowloadEx(title,folder,hook=hook,opt=opt) -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: AnimeWorld-API 2 | site_description: UNOFFICIAL library for AnimeWorld 3 | site_url: https://github.com/MainKronos/AnimeWorld-API 4 | 5 | theme: 6 | name: 'material' 7 | language: en 8 | palette: 9 | - scheme: 'default' 10 | media: '(prefers-color-scheme: light)' 11 | toggle: 12 | icon: 'material/lightbulb' 13 | name: "Switch to dark mode" 14 | - scheme: 'slate' 15 | media: '(prefers-color-scheme: dark)' 16 | primary: 'blue' 17 | toggle: 18 | icon: 'material/lightbulb-outline' 19 | name: 'Switch to light mode' 20 | icon: 21 | repo: fontawesome/brands/github 22 | logo: static/img/logo.svg 23 | favicon: static/img/logo.svg 24 | features: 25 | - navigation.sections 26 | - navigation.tracking 27 | - search.suggest 28 | - content.code.annotate 29 | - content.code.copy 30 | 31 | repo_name: MainKronos/AnimeWorld-API 32 | repo_url: https://github.com/MainKronos/AnimeWorld-API 33 | edit_uri: "" 34 | 35 | nav: 36 | - Introduction: 'index.md' 37 | - Usage: 38 | - QuickStart: 'usage/quickstart.md' 39 | - Advanced Usage: 'usage/advanced.md' 40 | - API Reference: 41 | - Developer Interface: 'api-reference/developer-interface.md' 42 | - Exceptions: 'api-reference/exceptions.md' 43 | - Community: 44 | - Contributing: 'community/contributing.md' 45 | - Code of Conduct: 'community/code-of-conduct.md' 46 | 47 | markdown_extensions: 48 | - pymdownx.highlight: 49 | anchor_linenums: true 50 | line_spans: __span 51 | pygments_lang_class: true 52 | - pymdownx.inlinehilite 53 | - pymdownx.snippets: 54 | base_path: ['docs'] 55 | check_paths: true 56 | - admonition 57 | - pymdownx.details 58 | - pymdownx.superfences 59 | 60 | plugins: 61 | - i18n: 62 | docs_structure: suffix 63 | fallback_to_default: true 64 | languages: 65 | - locale: en 66 | default: true 67 | name: English 68 | build: true 69 | - locale: it 70 | name: Italiano 71 | build: true 72 | - autorefs: 73 | resolve_closest: true 74 | - git-revision-date-localized: 75 | type: date 76 | locale: en 77 | - search: 78 | lang: 79 | - en 80 | - it 81 | - mkdocstrings: 82 | default_handler: python 83 | handlers: 84 | python: 85 | paths: [.] 86 | options: 87 | docstring_style: google 88 | # show_source: false 89 | 90 | show_root_heading: true 91 | show_root_full_path: false 92 | # show_category_heading: false 93 | # show_object_full_path: false 94 | allow_inspection: true 95 | group_by_category: true 96 | 97 | show_root_toc_entry: false 98 | # signature_crossrefs: true ---- 99 | # show_symbol_type_toc: true ---- 100 | 101 | # docstring_section_style: list 102 | line_length: 80 103 | separate_signature: true 104 | 105 | annotations_path: brief 106 | 107 | show_signature: true 108 | # show_signature_annotations: true 109 | 110 | show_submodules: false 111 | show_bases: false 112 | 113 | merge_init_into_class: true 114 | filters: ["!^_"] 115 | 116 | 117 | watch: 118 | - animeworld -------------------------------------------------------------------------------- /README.it.md: -------------------------------------------------------------------------------- 1 | [![AnimeWorld](https://github.com/MainKronos/AnimeWorld-API/blob/master/docs/static/img/AnimeWorld-API.png)](https://mainkronos.github.io/AnimeWorld-API/) 2 | # AnimeWorld-API 3 | 4 | [![Version](https://img.shields.io/pypi/v/animeworld)](https://github.com/MainKronos/AnimeWorld-API/releases/latest) 5 | ![Activity](https://img.shields.io/github/commit-activity/w/MainKronos/AnimeWorld-API) 6 | [![Publish to PyPI](https://github.com/MainKronos/AnimeWorld-API/workflows/Publish%20to%20PyPI/badge.svg)](https://pypi.org/project/animeworld/) 7 | [![Deploy MkDocs](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml/badge.svg)](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml) 8 | 9 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/animeworld) 10 | ![PyPI - Downloads](https://img.shields.io/pypi/dw/animeworld) 11 | ![PyPI - Downloads](https://img.shields.io/pypi/dd/animeworld) 12 | 13 | [![Static Badge](https://img.shields.io/badge/lang-english-%239FA8DA)](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.md) 14 | [![Static Badge](https://img.shields.io/badge/lang-italian-%239FA8DA)](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.it.md) 15 | 16 | 17 | AnimeWorld-API is an unofficial library for [AnimeWorld](https://www.animeworld.ac/) (Italian anime site). 18 | 19 | ## Installazione 20 | Questa libreria richiede [Python 3.11](https://www.python.org/) o superiore. 21 | 22 | È Possibile installarare la libreria tramite pip: 23 | ```shell script 24 | pip install animeworld 25 | ``` 26 | 27 | ## Utilizzo 28 | Per ricercare un anime per nome nel sito di animeWolrd è possibile usare la funzione find(). 29 | ```python 30 | import animeworld as aw 31 | 32 | res = aw.find("No game no life") 33 | print(res) 34 | ``` 35 | La funzione estituirà un dizionario contentente per chiave il nome dell'anime e per valore il link della pagina di animeworld. 36 | ```python 37 | {'name': 'No Game no Life', 'link': 'https://www.animeworld.ac/play/no-game-no-life.IJUH1E', ...} 38 | ``` 39 | È Possibile anche scaricare gli episodi di un anime. 40 | ```python 41 | import animeworld as aw 42 | 43 | anime = aw.Anime(link="https://www.animeworld.ac/play/danmachi-3.Ydt8-") 44 | for episodio in anime.getEpisodes(): 45 | print("Episodio Numero: ", episodio.number) 46 | 47 | if(episodio.download()): 48 | print("scaricato") 49 | else: 50 | print("errore") 51 | 52 | if episodio.number == '1': break 53 | ``` 54 | ``` 55 | Episodio Numero: 1 56 | scaricato 57 | ``` 58 | 59 | ## Documentazione 60 | 61 | La documentazione completa è disponibile qui: [Documentazione](https://mainkronos.github.io/AnimeWorld-API/) 62 | 63 | Per una panoramica di tutte le nozioni di base, vai alla sezione [QuickStart](https://mainkronos.github.io/AnimeWorld-API/usage/quickstart) 64 | 65 | Per argomenti più avanzati, vedere la sezione [Advanced Usage](https://mainkronos.github.io/AnimeWorld-API/usage/advanced) 66 | 67 | La sezione [API Reference](https://mainkronos.github.io/AnimeWorld-API/api-reference/developer-interface) fornisce un riferimento API completo. 68 | 69 | Se vuoi contribuire al progetto, vai alla sezione [Contributing](https://mainkronos.github.io/AnimeWorld-API/community/contributing) 70 | 71 | ## Star History 72 | 73 | [![Star History Chart](https://api.star-history.com/svg?repos=MainKronos/AnimeWorld-API&type=Date)](https://star-history.com/#MainKronos/AnimeWorld-API&Date) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![AnimeWorld](https://github.com/MainKronos/AnimeWorld-API/blob/master/docs/static/img/AnimeWorld-API.png)](https://mainkronos.github.io/AnimeWorld-API/) 2 | # AnimeWorld-API 3 | 4 | [![Version](https://img.shields.io/pypi/v/animeworld)](https://github.com/MainKronos/AnimeWorld-API/releases/latest) 5 | ![Activity](https://img.shields.io/github/commit-activity/w/MainKronos/AnimeWorld-API) 6 | [![Publish to PyPI](https://github.com/MainKronos/AnimeWorld-API/workflows/Publish%20to%20PyPI/badge.svg)](https://pypi.org/project/animeworld/) 7 | [![Deploy MkDocs](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml/badge.svg)](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml) 8 | 9 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/animeworld) 10 | ![PyPI - Downloads](https://img.shields.io/pypi/dw/animeworld) 11 | ![PyPI - Downloads](https://img.shields.io/pypi/dd/animeworld) 12 | 13 | [![Static Badge](https://img.shields.io/badge/lang-english-%239FA8DA)](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.md) 14 | [![Static Badge](https://img.shields.io/badge/lang-italian-%239FA8DA)](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.it.md) 15 | 16 | 17 | AnimeWorld-API is an unofficial library for [AnimeWorld](https://www.animeworld.ac/) (Italian anime site). 18 | 19 | ## Installation 20 | This library requires [Python 3.11](https://www.python.org/) or later. 21 | 22 | You can install the library using pip: 23 | ```shell script 24 | pip install animeworld 25 | ``` 26 | 27 | ## Usage 28 | To search for an anime by name on the AnimeWorld site, you can use the `find()` function. 29 | ```python 30 | import animeworld as aw 31 | 32 | res = aw.find("No game no life") 33 | print(res) 34 | ``` 35 | The function will return a dictionary with the anime name as the key and the link to the anime world page as the value. 36 | ```python 37 | {'name': 'No Game no Life', 'link': 'https://www.animeworld.ac/play/no-game-no-life.IJUH1E', ...} 38 | ``` 39 | You can also download episodes of an anime. 40 | ```python 41 | import animeworld as aw 42 | 43 | anime = aw.Anime(link="https://www.animeworld.ac/play/danmachi-3.Ydt8-") 44 | for episode in anime.getEpisodes(): 45 | print("Episode Number: ", episode.number) 46 | 47 | if(episode.download()): 48 | print("Downloaded") 49 | else: 50 | print("Error") 51 | 52 | if episode.number == '1': break 53 | ``` 54 | ``` 55 | Episode Number: 1 56 | Downloaded 57 | ``` 58 | 59 | ## Documentation 60 | 61 | The complete documentation is available here: [Documentation](https://mainkronos.github.io/AnimeWorld-API/) 62 | 63 | For an overview of all the basics, go to the [QuickStart](https://mainkronos.github.io/AnimeWorld-API/usage/quickstart) section. 64 | 65 | For more advanced topics, see the [Advanced Usage](https://mainkronos.github.io/AnimeWorld-API/usage/advanced) section. 66 | 67 | The [API Reference](https://mainkronos.github.io/AnimeWorld-API/api-reference/developer-interface) section provides a complete API reference. 68 | 69 | If you want to contribute to the project, visit the [Contributing](https://mainkronos.github.io/AnimeWorld-API/community/contributing) section. 70 | 71 | ## Star History 72 | 73 | [![Star History Chart](https://api.star-history.com/svg?repos=MainKronos/AnimeWorld-API&type=Date)](https://star-history.com/#MainKronos/AnimeWorld-API&Date) 74 | -------------------------------------------------------------------------------- /animeworld/servers/Streamtape.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import re 3 | 4 | from .Server import * 5 | from ..utility import HealthCheck 6 | 7 | class Streamtape(Server): 8 | @HealthCheck 9 | def fileLink(self) -> str: 10 | """ 11 | Recupera il link diretto per il download del file dell'episodio. 12 | 13 | Returns: 14 | Link diretto. 15 | 16 | Example: 17 | ```py 18 | return str # Link del file 19 | ``` 20 | """ 21 | 22 | sb_get = SES.get(self.link, follow_redirects=True) 23 | 24 | if sb_get.status_code == 200: 25 | soupeddata = BeautifulSoup(sb_get.content, "html.parser") 26 | 27 | raw_link = re.search(r"document\.getElementById\('ideoooolink'\)\.innerHTML = (\".*'\))", soupeddata.prettify()).group(1) 28 | 29 | raw_link = raw_link.replace('"', '').replace("'", "").replace('+', '') 30 | 31 | raw_link_part2 = re.search(r"\((.*?)\)", raw_link).group(1)[4:] 32 | raw_link_part1 = re.sub(r"\(.*?\)",'', raw_link) 33 | 34 | mp4_link = 'http:/' + (raw_link_part1 + raw_link_part2).replace(' ', '') 35 | 36 | return mp4_link 37 | 38 | def fileInfo(self) -> Dict[str,str]: 39 | """ 40 | Recupera le informazioni del file dell'episodio. 41 | 42 | Returns: 43 | Informazioni file episodio. 44 | 45 | Example: 46 | ```py 47 | return { 48 | "content_type": str, # Tipo del file, es. video/mp4 49 | "total_bytes": int, # Byte totali del file 50 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 51 | "server_name": str, # Nome del server 52 | "server_id": int, # ID del server 53 | "url": str # url dell'episodio 54 | } 55 | ``` 56 | """ 57 | 58 | return self._fileInfoIn() 59 | 60 | def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: 61 | """ 62 | Scarica l'episodio. 63 | 64 | Args: 65 | title: Nome con cui verrà nominato il file scaricato. 66 | folder: Posizione in cui verrà spostato il file scaricato. 67 | 68 | Other parameters: 69 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 70 | - `total_bytes`: Byte totali da scaricare. 71 | - `downloaded_bytes`: Byte attualmente scaricati. 72 | - `percentage`: Percentuale del progresso di download. 73 | - `speed`: Velocità di download (byte/s) 74 | - `elapsed`: Tempo trascorso dall'inizio del download. 75 | - `eta`: Tempo stimato rimanente per fine del download. 76 | - `status`: 'downloading' | 'finished' | 'aborted' 77 | - `filename`: Nome del file in download. 78 | 79 | opt: Lista per delle opzioni aggiuntive.\n 80 | - `'abort'`: Ferma forzatamente il download. 81 | 82 | Returns: 83 | Nome del file scaricato. 84 | 85 | Raises: 86 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 87 | 88 | Example: 89 | ```py 90 | return str # File scaricato 91 | ``` 92 | """ 93 | if title is None: title = self._defTitle 94 | else: title = self._sanitize(title) 95 | return self._downloadIn(title,folder,hook=hook,opt=opt) -------------------------------------------------------------------------------- /docs/community/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We expect contributors to our projects and online spaces to follow [the Python Software Foundation’s Code of Conduct](https://www.python.org/psf/conduct/). 4 | 5 | The Python community is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences great successes and continued growth. When you're working with members of the community, this Code of Conduct will help steer your interactions and keep Python a positive, successful, and growing community. 6 | 7 | ## Our Community 8 | 9 | Members of the Python community are **open, considerate, and respectful**. Behaviours that reinforce these values contribute to a positive environment, and include: 10 | 11 | * **Being open.** Members of the community are open to collaboration, whether it's on PEPs, patches, problems, or otherwise. 12 | * **Focusing on what is best for the community.** We're respectful of the processes set forth in the community, and we work within them. 13 | * **Acknowledging time and effort.** We're respectful of the volunteer efforts that permeate the Python community. We're thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community. 14 | * **Being respectful of differing viewpoints and experiences.** We're receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts. 15 | * **Showing empathy towards other community members.** We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views. 16 | * **Being considerate.** Members of the community are considerate of their peers -- other Python users. 17 | * **Being respectful.** We're respectful of others, their positions, their skills, their commitments, and their efforts. 18 | * **Gracefully accepting constructive criticism.** When we disagree, we are courteous in raising our issues. 19 | * **Using welcoming and inclusive language.** We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. 20 | 21 | ## Our Standards 22 | 23 | Every member of our community has the right to have their identity respected. The Python community is dedicated to providing a positive experience for everyone, regardless of age, gender identity and expression, sexual orientation, disability, physical appearance, body size, ethnicity, nationality, race, or religion (or lack thereof), education, or socio-economic status. 24 | 25 | ## Inappropriate Behavior 26 | 27 | Examples of unacceptable behavior by participants include: 28 | 29 | * Harassment of any participants in any form 30 | * Deliberate intimidation, stalking, or following 31 | * Logging or taking screenshots of online activity for harassment purposes 32 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 33 | * Violent threats or language directed against another person 34 | * Incitement of violence or harassment towards any individual, including encouraging a person to commit suicide or to engage in self-harm 35 | * Creating additional online accounts in order to harass another person or circumvent a ban 36 | * Sexual language and imagery in online communities or in any conference venue, including talks 37 | * Insults, put downs, or jokes that are based upon stereotypes, that are exclusionary, or that hold others up for ridicule 38 | * Excessive swearing 39 | * Unwelcome sexual attention or advances 40 | * Unwelcome physical contact, including simulated physical contact (eg, textual descriptions like "hug" or "backrub") without consent or after a request to stop 41 | * Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others 42 | * Sustained disruption of online community discussions, in-person presentations, or other in-person events 43 | * Continued one-on-one communication after requests to cease 44 | * Other conduct that is inappropriate for a professional audience including people of many different backgrounds 45 | 46 | Community members asked to stop any inappropriate behavior are expected to comply immediately. 47 | 48 | ## Enforcement 49 | 50 | We take Code of Conduct violations seriously, and will act to ensure our spaces are welcoming, inclusive, and professional environments to communicate in. -------------------------------------------------------------------------------- /docs/community/contributing.en.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Server 4 | 5 | This section explains how to add the ability to download an episode from another unsupported server. 6 | 7 | --8<-- "static/server.txt" 8 | 9 | To add a new server, follow these steps: 10 | 11 | 1. Create a .py file with the server's name and place it in the [servers](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/servers) folder (e.g., `NewServer.py`). 12 | 13 | 1. Use this template for the class of the new server: 14 | ```py title="NewServer.py" linenums="1" 15 | from .Server import * 16 | 17 | class NewServer(Server): 18 | def fileLink(self): # Mandatory 19 | """ 20 | Retrieves the direct link for downloading the episode file. 21 | 22 | Returns: 23 | Direct link. 24 | 25 | Example: 26 | ```py 27 | return str # File link 28 | ``` 29 | """ 30 | pass # TODO: to be completed 31 | 32 | def fileInfo(self) -> Dict[str, str]: # Optional 33 | """ 34 | Retrieves information about the episode file. 35 | 36 | Returns: 37 | Episode file information. 38 | 39 | Example: 40 | ```py 41 | return { 42 | "content_type": str, # File type, e.g., video/mp4 43 | "total_bytes": int, # Total file bytes 44 | "last_modified": datetime, # Date and time of the last update to the episode on the server 45 | "server_name": str, # Server name 46 | "server_id": int, # Server ID 47 | "url": str # Episode URL 48 | } 49 | ``` 50 | """ 51 | pass # TODO: to be completed 52 | 53 | def download(self, title: Optional[str] = None, folder: str = '', *, hook: Callable[[Dict], None] = lambda *args: None, opt: List[str] = []) -> Optional[str]: # Mandatory 54 | """ 55 | Downloads the episode. 56 | 57 | Args: 58 | title: Name to be given to the downloaded file. 59 | folder: Location where the downloaded file will be moved. 60 | 61 | Other parameters: 62 | hook: Function called multiple times during the download; the function receives a dictionary with the following keys:\n 63 | - `total_bytes`: Total bytes to download. 64 | - `downloaded_bytes`: Currently downloaded bytes. 65 | - `percentage`: Download progress percentage. 66 | - `speed`: Download speed (bytes/s). 67 | - `elapsed`: Time elapsed since the start of the download. 68 | - `eta`: Estimated remaining time for the download to finish. 69 | - `status`: 'downloading' | 'finished' | 'aborted' 70 | - `filename`: Downloaded file name. 71 | 72 | opt: List for additional options.\n 73 | - `'abort'`: Forcefully stops the download. 74 | 75 | Returns: 76 | Downloaded file name. 77 | 78 | Raises: 79 | HardStoppedDownload: The downloading file has been forcibly interrupted. 80 | 81 | Example: 82 | ```py 83 | return str # Downloaded file 84 | ``` 85 | """ 86 | if title is None: 87 | title = self._defTitle 88 | else: 89 | title = self._sanitize(title) 90 | 91 | pass 92 | 93 | # TODO: to be completed, select one of the 2 methods: 94 | # #NOTE: to be used if the file can be downloaded simply with httpx: 95 | # return self._downloadIn(title,folder,hook=hook,opt=opt) 96 | # 97 | # #NOTE: to be used if the file must be downloaded using the youtube_dl library 98 | # return self._dowloadEx(title,folder,hook=hook,opt=opt) 99 | ``` 100 | 101 | 1. Complete the various functions (those marked as `Optional` can also be left incomplete), also taking inspiration from the various servers loaded in the folder. 102 | 103 | 1. Add the line `from .NewServer import NewServer` to the file [servers/__init__.py](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/servers/__init__.py). 104 | 105 | 1. Modify the file [episodio.py](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/episodio.py) by adding the server's name among the imports ([Line 12](https://github.com/MainKronos/AnimeWorld-API/blob/master/animeworld/episodio.py#L12)) and modifying the function [__setServer](https://github.com/MainKronos/AnimeWorld-API/blob/master/animeworld/episodio.py). 106 | 107 | If everything works correctly, open a [pull request](https://github.com/MainKronos/AnimeWorld-API/pulls). -------------------------------------------------------------------------------- /docs/community/contributing.it.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Server 4 | 5 | Questa sezione spiega come aggiungere alla libreria la possibilità di scaricare un episodio da un altro server non ancora supportato. 6 | 7 | --8<-- "static/server.txt" 8 | 9 | Per aggiungere un nuovo server basta seguire questi passi: 10 | 11 | 1. Creare un file .py con il nome del server e metterlo nella cartella [servers](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/servers). (es `NuovoServer.py`) 12 | 13 | 1. Usa questo template per la classe del nuovo server: 14 | ```py title="NuovoServer.py" linenums="1" 15 | from .Server import * 16 | 17 | class NuovoServer(Server): 18 | def fileLink(self): # Obbligatoria 19 | """ 20 | Recupera il link diretto per il download del file dell'episodio. 21 | 22 | Returns: 23 | Link diretto. 24 | 25 | Example: 26 | ```py 27 | return str # Link del file 28 | ``` 29 | """ 30 | pass #TODO: da completare 31 | 32 | def fileInfo(self) -> Dict[str,str]: # Opzionale 33 | """ 34 | Recupera le informazioni del file dell'episodio. 35 | 36 | Returns: 37 | Informazioni file episodio. 38 | 39 | Example: 40 | ```py 41 | return { 42 | "content_type": str, # Tipo del file, es. video/mp4 43 | "total_bytes": int, # Byte totali del file 44 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 45 | "server_name": str, # Nome del server 46 | "server_id": int, # ID del server 47 | "url": str # url dell'episodio 48 | } 49 | ``` 50 | """ 51 | pass #TODO: da completare 52 | 53 | def download(self, title: Optional[str]=None, folder: str='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: # Obbligatoria 54 | """ 55 | Scarica l'episodio. 56 | 57 | Args: 58 | title: Nome con cui verrà nominato il file scaricato. 59 | folder: Posizione in cui verrà spostato il file scaricato. 60 | 61 | Other parameters: 62 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 63 | - `total_bytes`: Byte totali da scaricare. 64 | - `downloaded_bytes`: Byte attualmente scaricati. 65 | - `percentage`: Percentuale del progresso di download. 66 | - `speed`: Velocità di download (byte/s) 67 | - `elapsed`: Tempo trascorso dall'inizio del download. 68 | - `eta`: Tempo stimato rimanente per fine del download. 69 | - `status`: 'downloading' | 'finished' | 'aborted' 70 | - `filename`: Nome del file in download. 71 | 72 | opt: Lista per delle opzioni aggiuntive.\n 73 | - `'abort'`: Ferma forzatamente il download. 74 | 75 | Returns: 76 | Nome del file scaricato. 77 | 78 | Raises: 79 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 80 | 81 | Example: 82 | ```py 83 | return str # File scaricato 84 | ``` 85 | """ 86 | if title is None: title = self._defTitle 87 | else: title = self._sanitize(title) 88 | 89 | pass 90 | 91 | #TODO: da completare, selezionare uno dei 2 metodi: 92 | # #NOTE: da usare se il file puó essere scaricato semplicemente con httpx: 93 | # return self._downloadIn(title,folder,hook=hook,opt=opt) 94 | # 95 | # #NOTE: da usare se il file deve essere scaricato usando la libreria youtube_dl 96 | # return self._dowloadEx(title,folder,hook=hook,opt=opt) 97 | ``` 98 | 99 | 1. Completare le varie funzioni (quelle segnate con `Opzionale` possono anche non essere completate), prendendo anche spunto dai vari server caricati nella cartella. 100 | 101 | 1. Aggiungi la linea `from .NuovoServer import NuovoServer` al file [servers/__init__.py](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/servers/__init__.py). 102 | 103 | 1. Modificare il file [episodio.py](https://github.com/MainKronos/AnimeWorld-API/tree/master/animeworld/episodio.py) aggiungendo il nome del server tra gli import ([Linea 12](https://github.com/MainKronos/AnimeWorld-API/blob/master/animeworld/episodio.py#L12)) e modificare la funzione [__setServer](https://github.com/MainKronos/AnimeWorld-API/blob/master/animeworld/episodio.py). 104 | 105 | Se tutto funziona correttamente apri una richiesta di [pull](https://github.com/MainKronos/AnimeWorld-API/pulls). -------------------------------------------------------------------------------- /docs/usage/quickstart.en.md: -------------------------------------------------------------------------------- 1 | # QuickStart 2 | 3 | First, let's import the library: 4 | 5 | ```python linenums="1" 6 | import animeworld as aw 7 | ``` 8 | 9 | ## Session (Optional) 10 | 11 | It may be necessary to change the `base_url` of all links if the AnimeWorld site changes from `https://www.animeworld.ac` to something else. 12 | 13 | To do this, you need to modify the session's `base_url` [(SES)][animeworld.SES]: 14 | 15 | ```python linenums="2" 16 | aw.SES.base_url = "https://www.animeworld.ac" 17 | ``` 18 | 19 | ## Find 20 | 21 | Now let's try searching for an anime: 22 | 23 | ```python linenums="3" 24 | res = aw.find("Sword Art Online") 25 | print(res) 26 | ``` 27 | ??? Example "Output" 28 | 29 | ```python 30 | [ 31 | { 32 | "id": 1717, 33 | "name": "Sword Art Online", 34 | "jtitle": "Sword Art Online", 35 | "studio": "A-1 Pictures", 36 | "release": "July 5, 2014", 37 | "episodes": 25, 38 | "state": "1", 39 | "story": 'Kazuto "Kirito" Kirigaya, a programming genius, enters an interactive virtual reality with multiple players (a "massively multiplayer online" or "MMO") called "Sword Art Online." The problem is that once inside, players can only leave by winning the game—because "game over" means certain death.', 40 | "categories": [...], 41 | "image": "https://img.animeworld.ac/locandine/36343l.jpg", 42 | "durationEpisodes": "23", 43 | "link": "https://www.animeworld.ac/play/sword-art-online.N0onT", 44 | "createdAt": "2020-08-02T15:42:44.000Z", 45 | "language": "jp", 46 | "year": "2012", 47 | "dub": False, 48 | "season": "summer", 49 | "totViews": 461576, 50 | "dayViews": 204, 51 | "weekViews": 459, 52 | "monthViews": 6416, 53 | "malId": 11757, 54 | "anilistId": 11757, 55 | "mangaworldId": None, 56 | "malVote": 7.35, 57 | "trailer": "https://www.youtube.com/embed/6ohYYtxfDCg?enablejsapi=1&wmode=opaque", 58 | }, 59 | ... 60 | ] 61 | ``` 62 | 63 | The [find][animeworld.find] function returns a list of dictionaries, one for each anime found. Each dictionary contains a lot of information, including the name, number of episodes, release date, link, etc. 64 | 65 | ## Anime 66 | 67 | The [Anime][animeworld.Anime] class is the core object of this library. To instantiate it, you need to pass the anime's link, which can be obtained directly from the [AnimeWorld](https://www.animeworld.ac/) website or from the previously mentioned [find][animeworld.find] function. 68 | 69 | ```py linenums="5" 70 | # https://www.animeworld.ac/play/sword-art-online.N0onT 71 | anime = aw.Anime("/play/sword-art-online.N0onT") 72 | ``` 73 | 74 | !!! warning 75 | You can pass the full link to the `Anime` class, like this: 76 | ```py 77 | anime = aw.Anime("https://www.animeworld.ac/play/sword-art-online.N0onT") 78 | ``` 79 | However, the `base_url` (`https://www.animeworld.ac`) will be replaced with the one defined in the Session [(SES)][animeworld.SES]. 80 | 81 | !!! warning 82 | If the provided link points to a [404](https://www.animeworld.ac/404) page, the exception [Error404][animeworld.exceptions.Error404] will be raised. 83 | 84 | With this class, you can retrieve information about the anime: 85 | 86 | ```py linenums="7" 87 | # Title 88 | print("Title:", anime.getName()) 89 | print("----------------------------------\n") 90 | 91 | # Synopsis 92 | print("Synopsis:", anime.getTrama()) 93 | print("----------------------------------\n") 94 | 95 | # Cover image 96 | print("Cover: ", anime.getCover()) 97 | print("----------------------------------\n") 98 | 99 | # General information 100 | info = anime.getInfo() 101 | print("General Information:\n", "\n".join( 102 | [f"{x}: {info[x]}" for x in info] 103 | )) 104 | print("----------------------------------\n") 105 | ``` 106 | 107 | ??? Example "Output" 108 | 109 | ``` 110 | Title: Sword Art Online 111 | ---------------------------------- 112 | 113 | Synopsis: Kazuto "Kirito" Kirigaya, a programming genius, enters an interactive virtual reality with multiple players (a "massively multiplayer online" or "MMO") called "Sword Art Online." The problem is that once inside, players can only leave by winning the game—because "game over" means certain death. 114 | ---------------------------------- 115 | 116 | General Information: 117 | Category: Anime 118 | Audio: Japanese 119 | Release Date: July 8, 2012 120 | Season: Summer 2012 121 | Studio: A-1 Pictures 122 | Genre: ['Adventure', 'Action', 'Fantasy', 'Game', 'Romance', 'Drama'] 123 | Score: 8.36 / 10 124 | Duration: 23 min/ep 125 | Episodes: 25 126 | Status: Completed 127 | Views: 461,733 128 | ---------------------------------- 129 | ``` 130 | 131 | Most importantly, you can download the episodes: 132 | 133 | !!! Quote inline end "" 134 | 135 | !!! Info 136 | If no argument is passed to the `getEpisodes()` method, **ALL** episodes of the anime will be retrieved. 137 | 138 | !!! Warning 139 | `ep.number` is a `str` attribute. More information [here][animeworld.Episodio]. 140 | 141 | 142 | 143 | ```py linenums="20" 144 | # Get a list of episodes 145 | # that interest me 146 | episodes = anime.getEpisodes([1, 2, 4]) 147 | 148 | 149 | # And download them 150 | for ep in episodes: 151 | 152 | print(f"Downloading episode {ep.number}.") 153 | 154 | # One at a time... 155 | ep.download() 156 | 157 | print(f"Download completed.") 158 | ``` 159 | 160 | ??? Example "Output" 161 | 162 | ``` 163 | Downloading episode 1. 164 | Download completed. 165 | Downloading episode 2. 166 | Download completed. 167 | Downloading episode 4. 168 | Download completed. 169 | ``` -------------------------------------------------------------------------------- /docs/usage/quickstart.it.md: -------------------------------------------------------------------------------- 1 | # QuickStart 2 | 3 | Per prima cosa importiamo la libreria: 4 | 5 | ```python linenums="1" 6 | import animeworld as aw 7 | ``` 8 | 9 | ## Sessione (Opzionale) 10 | 11 | Potrebbe essere necessario cambiare il base_url di tutti i link se il sito di animeworld cambia da `https://www.animeworld.ac` a qualcos'altro. 12 | 13 | Per fare ciò è necessario cambiare il base_url della sessione [(SES)][animeworld.SES] 14 | 15 | ```python linenums="2" 16 | aw.SES.base_url = "https://www.animeworld.ac" 17 | ``` 18 | 19 | ## Find 20 | 21 | Adesso proviamo a cercare un anime: 22 | 23 | ```python linenums="3" 24 | res = aw.find("Sword Art Online") 25 | print(res) 26 | ``` 27 | ??? Example "Output" 28 | 29 | ```python 30 | [ 31 | { 32 | "id": 1717, 33 | "name": "Sword Art Online", 34 | "jtitle": "Sword Art Online", 35 | "studio": "A-1 Pictures", 36 | "release": "05 Luglio 2014", 37 | "episodes": 25, 38 | "state": "1", 39 | "story": 'Kazuto "Kirito" Kirigaya, un genio della programmazione, entra in una realtà virtuale interattiva con pluralità di giocatori (una realtà "massively multi-player online" o "MMO") denominata "Sword Art Online". Il problema sta nel fatto che, una volta entrati, se ne può uscire solo vincitori, completando il gioco, perché il game over equivale a morte certa del giocatore.', 40 | "categories": [...], 41 | "image": "https://img.animeworld.ac/locandine/36343l.jpg", 42 | "durationEpisodes": "23", 43 | "link": "https://www.animeworld.ac/play/sword-art-online.N0onT", 44 | "createdAt": "2020-08-02T15:42:44.000Z", 45 | "language": "jp", 46 | "year": "2012", 47 | "dub": False, 48 | "season": "summer", 49 | "totViews": 461576, 50 | "dayViews": 204, 51 | "weekViews": 459, 52 | "monthViews": 6416, 53 | "malId": 11757, 54 | "anilistId": 11757, 55 | "mangaworldId": None, 56 | "malVote": 7.35, 57 | "trailer": "https://www.youtube.com/embed/6ohYYtxfDCg?enablejsapi=1&wmode=opaque", 58 | }, 59 | ... 60 | ] 61 | ``` 62 | 63 | La funzione [find][animeworld.find] restituisce una lista di dizionari, uno per ogni anime trovato. Ogni dizionario contiene molte informazioni, tra cui: il nome, il numero di episodi, la data di uscita, il link, ecc. 64 | 65 | ## Anime 66 | 67 | La classe [Anime][animeworld.Anime] è l'oggetto che stà alla base di questa libreria. Per istanziarla è necessario passare il link dell'anime, ottenuto direttamente dal sito di [AnimeWorld](https://www.animeworld.ac/) o dalla funzione [find][animeworld.find] vista prima. 68 | 69 | ```py linenums="5" 70 | # https://www.animeworld.ac/play/sword-art-online.N0onT 71 | anime = aw.Anime("/play/sword-art-online.N0onT") 72 | ``` 73 | 74 | !!! warning 75 | È possibile passare il link completo alla classe Anime come ad esempio: 76 | ```py 77 | anime = aw.Anime("https://www.animeworld.ac/play/sword-art-online.N0onT") 78 | ``` 79 | Ma il base_url (https://www.animeworld.ac) verrà rimpiazzato con quello definito nella Sessione [(SES)][animeworld.SES]. 80 | 81 | !!! warning 82 | Se il link passato punta ad una pagina [404](https://www.animeworld.ac/404) verrannà sollevata l'eccezione [Error404][animeworld.exceptions.Error404]. 83 | 84 | Con questa classe è possibile ottenere informazioni sull'anime: 85 | 86 | ```py linenums="7" 87 | # Il titolo 88 | print("Titolo:", anime.getName()) 89 | print("----------------------------------\n") 90 | 91 | # La trama 92 | print("Trama:", anime.getTrama()) 93 | print("----------------------------------\n") 94 | 95 | # La locandina 96 | print("Cover: ", anime.getCover()) 97 | print("----------------------------------\n") 98 | 99 | # Informazioni generali 100 | info = anime.getInfo() 101 | print("Informazioni generali:\n", "\n".join( 102 | [f"{x}: {info[x]}" for x in info] 103 | )) 104 | print("----------------------------------\n") 105 | ``` 106 | 107 | ??? Example "Output" 108 | 109 | ``` 110 | Titolo: Sword Art Online 111 | ---------------------------------- 112 | 113 | Trama: Kazuto "Kirito" Kirigaya, un genio della programmazione, entra in una realtà virtuale interattiva con pluralità di giocatori (una realtà "massively multi-player online" o "MMO") denominata "Sword Art Online". Il problema sta nel fatto che, una volta entrati, se ne può uscire solo vincitori, completando il gioco, perché il game over equivale a morte certa del giocatore. 114 | ---------------------------------- 115 | 116 | Informazioni generali: 117 | Categoria: Anime 118 | Audio: Giapponese 119 | Data di Uscita: 08 Luglio 2012 120 | Stagione: Estate 2012 121 | Studio: A-1 Pictures 122 | Genere: ['Avventura', 'Azione', 'Fantasy', 'Gioco', 'Romantico', 'Sentimentale'] 123 | Voto: 8.36 / 10 124 | Durata: 23 min/ep 125 | Episodi: 25 126 | Stato: Finito 127 | Visualizzazioni: 461.733 128 | ---------------------------------- 129 | ``` 130 | 131 | Ma soprattutto scaricare gli episodi: 132 | 133 | !!! Quote inline end "" 134 | 135 | !!! Info 136 | Se al metodo `getEpisodes()` non viene passato nessun argomento, verranno ottenuti **TUTTI** gli episodi dell'anime. 137 | 138 | !!! Warning 139 | `ep.number` è un attributo di tipo `str`, maggiori informazioni [qui][animeworld.Episodio]. 140 | 141 | 142 | 143 | ```py linenums="20" 144 | # Ottengo una lista di Episodi 145 | # che mi interessano 146 | episodi = anime.getEpisodes([1, 2, 4]) 147 | 148 | 149 | # E li scarico 150 | for ep in episodi: 151 | 152 | print(f"Scarico l'episodio {ep.number}.") 153 | 154 | # Uno alla volta... 155 | ep.download() 156 | 157 | print(f"Download completato.") 158 | ``` 159 | 160 | ??? Example "Output" 161 | 162 | ``` 163 | Scarico l'episodio 1. 164 | Download completato. 165 | Scarico l'episodio 2. 166 | Download completato. 167 | Scarico l'episodio 4. 168 | Download completato. 169 | ``` -------------------------------------------------------------------------------- /animeworld/utility.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modulo per delle funzioni di utilità. 3 | """ 4 | import httpx 5 | from bs4 import BeautifulSoup 6 | import inspect 7 | from typing import * 8 | import re 9 | 10 | from .exceptions import DeprecatedLibrary 11 | 12 | class MySession(httpx.Client): 13 | """ 14 | Sessione httpx. 15 | """ 16 | 17 | def __init__(self, *args, **kwargs) -> None: 18 | super().__init__(*args, **kwargs) 19 | self.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'}) 20 | self.fixCookie() 21 | 22 | def build_url(self, url: httpx.URL | str) -> httpx.URL: 23 | """ 24 | Unisce l'url con il base_url. 25 | """ 26 | return self._merge_url(url) 27 | 28 | def fixCookie(self): 29 | """Aggiunge il csrf_token all'headers.""" 30 | 31 | csrf_token = re.compile(br'') 32 | cookie = re.compile(br'document\.cookie\s*?=\s*?"(.+?)=(.+?)(\s*?;\s*?path=.+?)?"\s*?;') 33 | for _ in range(2): # numero di tentativi 34 | res = self.get("/", follow_redirects=True) 35 | 36 | m = cookie.search(res.content) 37 | if m: 38 | self.cookies.update({m.group(1).decode('utf-8'): m.group(2).decode('utf-8')}) 39 | continue 40 | 41 | m = csrf_token.search(res.content) 42 | if m: 43 | self.headers.update({'csrf-token': m.group(1).decode('utf-8')}) 44 | break 45 | 46 | 47 | def HealthCheck(fun): 48 | """ 49 | Controlla se la libreria è deprecata 50 | """ 51 | def wrapper(*args, **kwargs): 52 | try: 53 | attempt = False # tentativo utilizzato 54 | while True: 55 | try: 56 | return fun(*args, **kwargs) 57 | except Exception as e: 58 | if not attempt: 59 | SES.fixCookie() 60 | else: 61 | raise e 62 | attempt = True 63 | 64 | except AttributeError: 65 | frame = inspect.trace()[-1] 66 | funName = frame[3] 67 | errLine = frame[2] 68 | filename = frame[1] 69 | raise DeprecatedLibrary(filename, funName, errLine) 70 | return wrapper 71 | 72 | @HealthCheck 73 | def find(keyword: str) -> List[Dict]: 74 | """ 75 | Ricerca un anime tramite le API interne di Animeworld. 76 | 77 | Args: 78 | keyword: Il nome dell'anime o una porzione di esso. 79 | 80 | Returns: 81 | Informazioni riguardanti l'anime. 82 | 83 | Raises: 84 | DeprecatedLibrary: Cambiamento del sito Animeworld. 85 | 86 | Example: 87 | ```py 88 | [ 89 | { 90 | "id": int, # ID interno di AnimeWorld 91 | "name": str, # Nome dell'anime 92 | "jtitle": str, # Nome giapponese (con caratteri latini) 93 | "studio": str, # Studio dell'anime 94 | "release": str | None, # Giorno, Mese e Anno della release dell'anime. Se non disponibile ("??" al posto della data), ritorna None. 95 | "episodes": int, # Numero di episodi 96 | "state": str, # Es. "0", "1", ... 97 | "story": str, # Trama dell'anime 98 | "categories": List[dict], # Es. [{"id": int, "name": str, "slug": str, "description": str}] 99 | "image": str, # Link dell'immagine di copertina 100 | "durationEpisodes": str, # Durata episodio 101 | "link": str, # Link dell'anime 102 | "createdAt": str, # Es. "2021-10-24T18:29:34.000Z" 103 | "language": str, # Es. "jp 104 | "year": str, # Anno di rilascio dell'anime 105 | "dub": bool, # Se è doppiato o meno 106 | "season": str, # Es. "winter" 107 | "totViews": int, # Numero totale di visite alla pagina AnimeWolrd 108 | "dayViews": int, # Numero giornaliero di visite alla pagina AnimeWolrd 109 | "weekViews": int, # Numero settimanale di visite alla pagina AnimeWolrd 110 | "monthViews": int, # Numero mensile di visite alla pagina AnimeWolrd 111 | "malId": int, # ID di MyAnimeList dell'anime 112 | "anilistId": int, # ID di AniList dell'anime 113 | "mangaworldId": int, # ID di MangaWorld dell'anime 114 | "malVote": float, # Valutazione di MyanimeList 115 | "trailer": str # Link del trailer dell'anime 116 | } 117 | ] 118 | ``` 119 | """ 120 | 121 | res = SES.post("/api/search/v2?", params = {"keyword": keyword}, follow_redirects=True) 122 | 123 | data = res.json() 124 | if "error" in data: return [] 125 | data = data["animes"] 126 | 127 | for elem in data: 128 | for k in elem: 129 | if elem[k] == "??": 130 | elem[k] = None 131 | 132 | data.sort(key=lambda a: a["dub"]) 133 | 134 | return [ 135 | { 136 | "id": elem["id"], 137 | "name": elem["name"], 138 | "jtitle": elem["jtitle"], 139 | "studio": elem["studio"], 140 | "release": elem["release"], 141 | "episodes": int(elem["episodes"]) if elem["episodes"] is not None else None, 142 | "state": elem["state"], 143 | "story": elem["story"], 144 | "categories": elem["categories"], 145 | "image": elem["image"], 146 | "durationEpisodes": elem["durationEpisodes"], 147 | "link": str(SES.build_url(f"/play/{elem['link']}.{elem['identifier']}")) if elem['link'] is not None or elem['identifier'] is not None else None, 148 | "createdAt": elem["createdAt"], 149 | "language": elem["language"], 150 | "year": elem["year"], 151 | "dub": elem["dub"] != "0" if elem["dub"] is not None else None, 152 | "season": elem["season"], 153 | "totViews": elem["totViews"], 154 | "dayViews": elem["dayViews"], 155 | "weekViews": elem["weekViews"], 156 | "monthViews": elem["monthViews"], 157 | "malId": elem["malId"], 158 | "anilistId": elem["anilistId"], 159 | "mangaworldId": elem["mangaworldId"], 160 | "malVote": elem["malVote"], 161 | "trailer": elem["trailer"] 162 | } for elem in data 163 | ] 164 | 165 | SES = MySession(http2=True, base_url="https://www.animeworld.ac/") # sessione contenente Cookie e headers 166 | """Sessione httpx. 167 | 168 | Example: 169 | ```py 170 | # Per cabiare il base_url: 171 | SES.base_url = "https://www.animeworld.ac" 172 | ``` 173 | """ 174 | -------------------------------------------------------------------------------- /tests/test_animeworld.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import unittest 4 | import random, io, time 5 | from threading import Thread 6 | 7 | import animeworld as aw 8 | from animeworld.servers import AnimeWorld_Server, Streamtape 9 | 10 | class TestGeneral(unittest.TestCase): 11 | @classmethod 12 | def setUpClass(cls) -> None: 13 | """Inizializza la classe.""" 14 | 15 | # Correggo il base url 16 | aw.SES.base_url = "https://www.animeworld.ac" 17 | 18 | def test_find(self): 19 | """ 20 | Testa la funzione find. 21 | """ 22 | res = aw.find("Sabikui Bisco 2") 23 | 24 | self.assertIsInstance(res, list) 25 | self.assertGreater(len(res), 0) 26 | 27 | anime = random.choice(res) 28 | 29 | self.assertIsInstance(anime, dict) 30 | 31 | self.assertIn("id", anime) 32 | self.assertIn("name", anime) 33 | self.assertIn("jtitle", anime) 34 | self.assertIn("studio", anime) 35 | self.assertIn("release", anime) 36 | self.assertIn("episodes", anime) 37 | self.assertIn("state", anime) 38 | self.assertIn("story", anime) 39 | self.assertIn("categories", anime) 40 | self.assertIn("image", anime) 41 | self.assertIn("durationEpisodes", anime) 42 | self.assertIn("link", anime) 43 | self.assertIn("createdAt", anime) 44 | self.assertIn("language", anime) 45 | self.assertIn("year", anime) 46 | self.assertIn("dub", anime) 47 | self.assertIn("season", anime) 48 | self.assertIn("totViews", anime) 49 | self.assertIn("dayViews", anime) 50 | self.assertIn("weekViews", anime) 51 | self.assertIn("monthViews", anime) 52 | self.assertIn("malId", anime) 53 | self.assertIn("anilistId", anime) 54 | self.assertIn("mangaworldId", anime) 55 | self.assertIn("malVote", anime) 56 | self.assertIn("trailer", anime) 57 | 58 | class TestExceptions(unittest.TestCase): 59 | def test_Error404(self) -> None: 60 | """ 61 | Testa il corretto riconoscimento di una pagina 404. 62 | """ 63 | with self.assertRaises(aw.Error404): 64 | aw.Anime("/play/ttt") 65 | 66 | # @unittest.skip("Link da aggiornare con uno non ancora disponibile.") 67 | def test_AnimeNotAvailable(self) -> None: 68 | """ 69 | Testa il corretto riconoscimento di un anime non ancora disponibile. 70 | """ 71 | res = aw.Anime("/play/sabikui-bisco-2.6CCbU") 72 | 73 | with self.assertRaises(aw.AnimeNotAvailable): 74 | res.getEpisodes() 75 | 76 | @unittest.skip("Link da aggiornare con uno che abbia un server non supportato.") 77 | def test_ServerNotSupported(self) -> None: 78 | """ 79 | Testa il corretto riconoscimento di un server non supportato. 80 | """ 81 | ep = random.choice(aw.Anime("/play/fullmetal-alchemist-brotherhood.4vGGQ").getEpisodes()) 82 | 83 | server = [s for s in ep.links if s.Nid == 2][0] 84 | 85 | with self.assertRaises(aw.ServerNotSupported): 86 | server.fileLink() 87 | 88 | 89 | class TestAnimeWorld(unittest.TestCase): 90 | @classmethod 91 | def setUpClass(cls) -> None: 92 | """Inizializza la classe Anime.""" 93 | 94 | # Correggo il base url 95 | aw.SES.base_url = "https://www.animeworld.ac" 96 | 97 | # Inizializzo l'anime 98 | cls.anime = aw.Anime("/play/fullmetal-alchemist-brotherhood.4vGGQ") 99 | 100 | def test_anime(self): 101 | """ 102 | Testa l'ottenimento delle informazioni relative all'anime. 103 | """ 104 | self.assertIsInstance(self.anime.getName(), str) 105 | self.assertIsInstance(self.anime.getTrama(), str) 106 | self.assertIsInstance(self.anime.getCover(), str) 107 | 108 | info = self.anime.getInfo() 109 | self.assertIsInstance(info, dict) 110 | self.assertIn('Categoria', info) 111 | self.assertIn('Audio', info) 112 | self.assertIn('Data di Uscita', info) 113 | self.assertIn('Stagione', info) 114 | self.assertIn('Studio', info) 115 | self.assertIn('Genere', info) 116 | self.assertIn('Voto', info) 117 | self.assertIn('Durata', info) 118 | self.assertIn('Episodi', info) 119 | self.assertIn('Stato', info) 120 | self.assertIn('Visualizzazioni', info) 121 | 122 | def test_episodio(self): 123 | ep = random.choice(self.anime.getEpisodes()) 124 | 125 | self.assertIsInstance(ep, aw.Episodio) 126 | self.assertIsInstance(ep.number, str) 127 | self.assertIsInstance(ep.links, list) 128 | 129 | class TestServer(unittest.TestCase): 130 | @classmethod 131 | def setUpClass(cls) -> None: 132 | """Sceglie un episodio per i test.""" 133 | 134 | # Correggo il base url 135 | aw.SES.base_url = "https://www.animeworld.ac" 136 | 137 | # Inizializzo l'episodio 138 | cls.episodio = random.choice(aw.Anime("/play/fullmetal-alchemist-brotherhood.4vGGQ").getEpisodes()) 139 | 140 | @staticmethod 141 | def stopDownload(opt:list): 142 | time.sleep(1) 143 | opt.append("abort") 144 | 145 | def test_AnimeWorld_Server(self) -> None: 146 | 147 | servers = [e for e in self.episodio.links if isinstance(e, AnimeWorld_Server)] 148 | 149 | if len(servers) == 0: 150 | self.skipTest('Il server AnimeWorld_Server non esiste in questo episodio.') 151 | return 152 | 153 | server = servers[0] 154 | 155 | self.assertEqual(server.Nid, 9) 156 | self.assertEqual(server.name, "AnimeWorld Server") 157 | self.assertIsInstance(server.fileLink(), str) 158 | 159 | info = server.fileInfo() 160 | self.assertIsInstance(info, dict) 161 | self.assertIn("content_type", info) 162 | self.assertIn("total_bytes", info) 163 | self.assertIn("last_modified", info) 164 | self.assertIn("server_name", info) 165 | self.assertIn("server_id", info) 166 | self.assertIn("url", info) 167 | 168 | with self.subTest('Animeworld_Server Download'): 169 | buf = io.BytesIO() 170 | opt = [] 171 | Thread(target=self.stopDownload, args=(opt,)).start() 172 | self.assertIsNone(server.download(folder=buf, opt=opt)) 173 | buf.close() 174 | 175 | def test_Streamtape(self) -> None: 176 | servers = [e for e in self.episodio.links if isinstance(e, Streamtape)] 177 | 178 | if len(servers) == 0: 179 | self.skipTest('Il server Streamtape non esiste in questo episodio.') 180 | return 181 | 182 | server = servers[0] 183 | 184 | self.assertEqual(server.Nid, 8) 185 | self.assertEqual(server.name, "Streamtape") 186 | 187 | 188 | self.assertIsInstance(server.fileLink(), str) 189 | 190 | info = server.fileInfo() 191 | self.assertIsInstance(info, dict) 192 | self.assertIn("content_type", info) 193 | self.assertIn("total_bytes", info) 194 | self.assertIn("last_modified", info) 195 | self.assertIn("server_name", info) 196 | self.assertIn("server_id", info) 197 | self.assertIn("url", info) 198 | 199 | with self.subTest('Streamtape Download'): 200 | buf = io.BytesIO() 201 | opt = [] 202 | Thread(target=self.stopDownload, args=(opt,)).start() 203 | self.assertIsNone(server.download(folder=buf, opt=opt)) 204 | buf.close() 205 | 206 | if __name__ == '__main__': 207 | unittest.main(verbosity=2) -------------------------------------------------------------------------------- /docs/usage/advanced.en.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | ## Exceptions 4 | 5 | The library raises several exceptions, the main ones being: [`AnimeNotAvailable`][animeworld.exceptions.AnimeNotAvailable], [`Error404`][animeworld.exceptions.Error404], and [`DeprecatedLibrary`][animeworld.exceptions.DeprecatedLibrary].
For more information, consult the [documentation](../api-reference/exceptions.md). 6 | 7 | ### DeprecatedLibrary 8 | 9 | The [`DeprecatedLibrary`][animeworld.exceptions.DeprecatedLibrary] exception is raised when a change on the [AnimeWorld](https://www.animeworld.ac/) site is detected that is no longer supported by the library. 10 | 11 | This exception can be raised practically by any method of the library, so it is recommended to handle it globally. 12 | 13 | ```py linenums="1" hl_lines="9" 14 | try: 15 | res = aw.find("...") 16 | anime = aw.Anime("/play/...") 17 | episodes = anime.getEpisodes() 18 | 19 | for x in episodes: 20 | x.download() 21 | 22 | except aw.DeprecatedLibrary as e: 23 | # Exception handling 24 | print("Open an issue on GitHub :(") 25 | ``` 26 | 27 | ### Error404 28 | 29 | The [`Error404`][animeworld.exceptions.Error404] exception is raised when the URL passed during the creation of the Anime object points to a [404](https://www.animeworld.ac/404) page. 30 | 31 | Since this exception is only raised by the [`Anime`][animeworld.Anime] class, it is recommended to handle it only if you are indeed instantiating an object of that class. 32 | 33 | ```py linenums="1" hl_lines="7" 34 | try: 35 | res = aw.find("...") 36 | 37 | try: 38 | anime = aw.Anime("/play/...") 39 | 40 | except aw.Error404 as e: 41 | # Exception handling 42 | print("Anime not found :(") 43 | 44 | else: 45 | episodes = anime.getEpisodes() 46 | for x in episodes: 47 | x.download() 48 | 49 | except aw.DeprecatedLibrary as e: 50 | # Exception handling 51 | print("Open an issue on GitHub :(") 52 | ``` 53 | 54 | ### AnimeNotAvailable 55 | 56 | The [`AnimeNotAvailable`][animeworld.exceptions.AnimeNotAvailable] exception is raised when the anime page exists, but the episodes are not yet available. This happens, for example, when a new season starts. 57 | 58 | The exception occurs only when calling the [`getEpisodes`][animeworld.Anime.getEpisodes] method. 59 | 60 | ```py linenums="1" hl_lines="15" 61 | try: 62 | res = aw.find("...") 63 | 64 | try: 65 | anime = aw.Anime("/play/...") 66 | 67 | except aw.Error404 as e: 68 | # Exception handling 69 | print("Anime not found :(") 70 | 71 | else: 72 | try: 73 | episodes = anime.getEpisodes() 74 | 75 | except aw.AnimeNotAvailable as e: 76 | # Exception handling 77 | print("Anime not yet available :(") 78 | 79 | else: 80 | for x in episodes: 81 | x.download() 82 | 83 | except aw.DeprecatedLibrary as e: 84 | # Exception handling 85 | print("Open an issue on GitHub :(") 86 | ``` 87 | 88 | ## Server 89 | 90 | To download an episode, you can manually select the server from which to download the video. To do this, first obtain the list of servers using the [`Episodio.links`][animeworld.Episodio.links] attribute and then choose one of the supported ones. 91 | 92 | !!! Warning 93 | I do not recommend using this method to download an episode; it is much simpler and safer to use the [`Episodio.download`][animeworld.Episodio.download] method because: 94 | 95 | 1. The fastest server is always chosen at the beginning. 96 | 2. If an unsupported server is chosen, the [ServerNotSupported][animeworld.exceptions.ServerNotSupported] exception will be raised. 97 | 98 | ```py linenums="1" 99 | anime = aw.Anime("...") 100 | 101 | # Choose the first episode 102 | episode = anime.getEpisodes()[0] 103 | 104 | # Get the list of servers 105 | servers = episode.links 106 | 107 | # Choose the AnimeWorld_Server server, for example 108 | server = [x for x in servers if isinstance(x.name, AnimeWorld_Server)][0] 109 | 110 | # Download the video 111 | server.download() 112 | ``` 113 | 114 | ### Supported Servers 115 | 116 | The supported servers are listed below; if you want to contribute to add others, you can take a look at the [Contributing](../community/contributing.md) section. 117 | 118 | --8<-- "static/server.txt" 119 | 120 | ## Download 121 | 122 | !!! Warning inline end 123 | If there are any disallowed characters in the file name (`#%&{}<>*?/$!'":@+\``|=`), they will be automatically removed. To obtain the actual file name written to disk, you can get it from the return of the [`Episodio.download`][animeworld.Episodio.download] method. 124 | 125 | To obtain an episode, i recommend using the [`Episodio.download`][animeworld.Episodio.download] method, which retrieves the video using the fastest available server at the time of the download. 126 | 127 | You can set the file name using the `title` parameter and the destination folder using the `folder` parameter. 128 | 129 | ### hook 130 | 131 | The `hook` parameter is more interesting; this is a reference to a function that will be called every time a video chunk is downloaded (~524 KB). This is useful for displaying the download progress on the screen. The function must have a single parameter of type `Dict[str, Any]`. 132 | 133 | ```py 134 | def my_hook(data): 135 | print(data) 136 | 137 | episodi = anime.getEpisodes() 138 | for x in episodi: 139 | x.download(hook=my_hook) 140 | ``` 141 | 142 | An example of a possible dictionary passed to the hook function is as follows: 143 | 144 | ```py 145 | { 146 | "total_bytes": 234127340, # Total size of the video in bytes 147 | "downloaded_bytes": 524288, # Downloaded size in bytes 148 | "percentage": 0.0022393283928310126, # Downloaded percentage [0, 1] 149 | "speed": 3048288.673006227, # Download speed in bytes/s 150 | "elapsed": 0.17199420928955078, # Elapsed time in seconds 151 | "filename": "1 - AnimeWorld Server.mp4", # File name 152 | "eta": 76.63416331551707, # Estimated remaining time in seconds 153 | "status": "downloading", # Download status ('downloading' | 'finished' | 'aborted') 154 | } 155 | ``` 156 | 157 | ### opt 158 | 159 | It is also possible to forcefully stop the download using the `opt` parameter. This parameter is a list of strings, each string representing an option. Currently, the only possible option is `abort`, which stops the download. 160 | 161 | If the string `abort` appears in opt during the download, the download is stopped, and the partially downloaded file is deleted. 162 | 163 | An example of using the `opt` parameter is as follows: 164 | 165 | ```py linenums="1" 166 | import animeworld as aw 167 | import time 168 | from threading import Thread 169 | 170 | anime = aw.Anime("...") 171 | episode = anime.getEpisodes()[0] 172 | 173 | # Define the function for the thread 174 | def handle_download(options_list): 175 | time.sleep(5) 176 | options_list.append("abort") 177 | 178 | 179 | opt = [] # Array for dynamic options 180 | t = Thread(target=handle_download, args=(opt,)) # Create the thread 181 | 182 | t.start() # Start the thread 183 | 184 | episode.download(opt=opt) # Start the download 185 | ``` 186 | 187 | In this example, the download is stopped after 5 seconds. 188 | 189 | ### I/O Buffer 190 | 191 | You can download an episode using a file descriptor directly instead of a string for the directory. Just pass an [IOBase](https://docs.python.org/3/library/io.html#i-o-base-classes) type to the `folder` parameter. 192 | 193 | ```py linenums="1" 194 | import animeworld as aw 195 | import io 196 | 197 | anime = aw.Anime("...") 198 | episode = anime.getEpisodes()[0] 199 | 200 | buffer = io.BytesIO() # Allocate an in-memory buffer 201 | 202 | episode.download(folder=buffer) # Start the download 203 | ``` 204 | 205 | In this example, the downloaded episode is written to memory without being saved as a file. 206 | 207 | --- 208 | 209 | ## Complete Example 210 | 211 | ```py title="example.py" linenums="1" 212 | --8<-- "static/example.py" 213 | ``` -------------------------------------------------------------------------------- /animeworld/episodio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modulo contenente la struttura a classe degli episodi. 3 | """ 4 | import httpx 5 | from bs4 import BeautifulSoup 6 | from typing import * 7 | import time 8 | import io 9 | 10 | from .utility import SES 11 | from .exceptions import ServerNotSupported, HardStoppedDownload 12 | from .servers import AnimeWorld_Server, YouTube, Streamtape 13 | from .servers.Server import Server 14 | 15 | class Episodio: 16 | """ 17 | Attributes: 18 | number: Numero dell'episodio. 19 | links: Lista dei server in cui è hostato l'episodio. 20 | 21 | Warning: 22 | L'attributo `number` è di tipo `str` perchè è possibile che capitino episodi con un numero composto (es. `5.5`, `268-269`), è un caso molto raro ma possibile. 23 | """ 24 | 25 | def __init__(self, number: str, link: str, legacy: List[Dict] = []): 26 | """ 27 | Args: 28 | number: Numero dell'episodio. 29 | link: Link dell'endpoint dell'episodio. 30 | legacy: Lista di tutti i link dei server in cui sono hostati gli episodi. 31 | """ 32 | self.number:str = number 33 | self.__link = link 34 | self.__legacy = legacy 35 | 36 | @property 37 | def links(self) -> List[Server]: # lista dei provider dove sono hostati gli ep 38 | """ 39 | Ottiene la lista dei server in cui è hostato l'episodio. 40 | 41 | Returns: 42 | Lista di oggetti Server. 43 | 44 | Example: 45 | ```py 46 | return [ 47 | Server, # Classe Server 48 | ... 49 | ] 50 | ``` 51 | """ 52 | tmp = [] # tutti i links 53 | res = SES.post(self.__link, timeout=(3, 27), follow_redirects=True) 54 | data = res.json() 55 | 56 | for provID in data["links"]: 57 | key = [x for x in data["links"][provID].keys() if x != 'server'][0] 58 | tmp.append({ 59 | "id": int(provID), 60 | "name": data["links"][provID]["server"]["name"], 61 | "link": data["links"][provID][key]["link"] 62 | }) 63 | 64 | for prov in self.__legacy: 65 | if str(prov['id']) in data["links"].keys(): continue 66 | 67 | tmp.append(prov) 68 | 69 | return self.__setServer(tmp, self.number) 70 | 71 | def fileInfo(self) -> Dict[str,str]: 72 | """ 73 | Recupera le informazioni del file dell'episodio. 74 | 75 | Returns: 76 | Informazioni file episodio. 77 | 78 | Example: 79 | ```py 80 | return { 81 | "content_type": str, # Tipo del file, es. video/mp4 82 | "total_bytes": int, # Byte totali del file 83 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 84 | "server_name": str, # Nome del server 85 | "server_id": int, # ID del server 86 | "url": str # url dell'episodio 87 | } 88 | ``` 89 | """ 90 | 91 | info = "" 92 | err = None 93 | for server in self.links: 94 | try: 95 | info = server.fileInfo() 96 | except ServerNotSupported: 97 | pass 98 | except httpx.HTTPError as exc: 99 | err = exc 100 | else: 101 | return info 102 | 103 | raise err 104 | 105 | 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 106 | """ 107 | Scarica l'episodio dal server più veloce. 108 | 109 | Args: 110 | title: Nome con cui verrà nominato il file scaricato. 111 | folder: Posizione in cui verrà spostato il file scaricato. 112 | 113 | Other parameters: 114 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 115 | - `total_bytes`: Byte totali da scaricare. 116 | - `downloaded_bytes`: Byte attualmente scaricati. 117 | - `percentage`: Percentuale del progresso di download. 118 | - `speed`: Velocità di download (byte/s) 119 | - `elapsed`: Tempo trascorso dall'inizio del download. 120 | - `eta`: Tempo stimato rimanente per fine del download. 121 | - `status`: 'downloading' | 'finished' | 'aborted' 122 | - `filename`: Nome del file in download. 123 | 124 | opt: Lista per delle opzioni aggiuntive.\n 125 | - `'abort'`: Ferma forzatamente il download. 126 | 127 | Returns: 128 | Nome del file scaricato. 129 | 130 | Raises: 131 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 132 | 133 | Example: 134 | ```py 135 | return str # File scaricato 136 | ``` 137 | """ 138 | 139 | return self.__choiceBestServer().download(title,folder,hook=hook,opt=opt) 140 | 141 | # Private 142 | def __setServer(self, links: List[Dict], numero: str) -> List[Server]: # Per ogni link li posizioni nelle rispettive classi 143 | """ 144 | Costruisce la rispettiva classe Server per ogni link passato. 145 | 146 | Args: 147 | links: Dizionario ('id', 'name', 'link') contenente le informazioni del Server in cui è hostato l'episodio. 148 | numero: Numero dell'episodio. 149 | 150 | Returns: 151 | Lista di oggetti Server. 152 | 153 | Example: 154 | ```py 155 | return [ 156 | Server, # Classe Server 157 | ... 158 | ] 159 | ``` 160 | """ 161 | ret: List[Server] = [] # lista dei server 162 | for prov in links: 163 | if prov["id"] == 4: 164 | ret.append(YouTube(prov["link"], prov["id"], prov["name"], numero)) 165 | elif prov["id"] == 9: 166 | ret.append(AnimeWorld_Server(prov["link"], prov["id"], prov["name"], numero)) 167 | elif prov["id"] == 8: 168 | ret.append(Streamtape(prov["link"], prov["id"], prov["name"], numero)) 169 | else: 170 | ret.append(Server(prov["link"], prov["id"], prov["name"], numero)) 171 | ret.sort(key=self.__sortServer) 172 | return ret 173 | 174 | # Private 175 | def __sortServer(self, elem): 176 | """ 177 | Ordina i server per importanza. 178 | """ 179 | if isinstance(elem, YouTube): return 0 180 | elif isinstance(elem, AnimeWorld_Server): return 1 181 | elif isinstance(elem, Streamtape): return 2 182 | else: return 4 183 | 184 | def __choiceBestServer(self) -> Server: 185 | """ 186 | Sceglie il server più veloce per il download dell'episodio. 187 | 188 | Returns: 189 | Il Server più veloce. 190 | """ 191 | servers = self.links 192 | 193 | speed_test = [{ 194 | "server": x, 195 | "bytes": -1 196 | } for x in servers] 197 | 198 | max_time = 0.5 # numero di secondi massimo 199 | 200 | for test in speed_test: 201 | try: 202 | start = time.perf_counter() 203 | link = test["server"].fileLink() 204 | if not link: continue 205 | with SES.stream("GET", link, timeout=0.9, follow_redirects=True) as r: 206 | for chunk in r.iter_bytes(chunk_size = 2048): 207 | if time.perf_counter() - start > max_time: break 208 | test["bytes"] += len(chunk) 209 | except (ServerNotSupported, httpx.HTTPError): 210 | continue 211 | 212 | speed_test = [x for x in speed_test if x["bytes"] != -1] # tolgo tutti i server che hanno generato un eccezione 213 | if len(speed_test) == 0: return servers[0] # ritorno al caso standard 214 | 215 | return max(speed_test, key=lambda x: x["bytes"])["server"] # restituisco il server che ha scaricato più byte in `max_time` secondi -------------------------------------------------------------------------------- /docs/usage/advanced.it.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | ## Eccezioni 4 | 5 | La libreria solleva diverse eccezioni, le principali sono: [`AnimeNotAvailable`][animeworld.exceptions.AnimeNotAvailable], [`Error404`][animeworld.exceptions.Error404] e [`DeprecatedLibrary`][animeworld.exceptions.DeprecatedLibrary].
Per maggiori informazioni consultare la [documentazione](../api-reference/exceptions.md). 6 | 7 | ### DeprecatedLibrary 8 | 9 | L'eccezione [`DeprecatedLibrary`][animeworld.exceptions.DeprecatedLibrary] viene sollevata quando viene rivelato un mutamento del sito di [AnimeWorld](https://www.animeworld.ac/) non piú supportato dalla libreria. 10 | 11 | Questa eccezione può essere sollevata praticamente da qualsiasi metodo della libreria, quindi è consigliato gestirla in modo globale. 12 | 13 | ```py linenums="1" hl_lines="9" 14 | try: 15 | res = aw.find("...") 16 | anime = aw.Anime("/play/...") 17 | episodi = anime.getEpisodes() 18 | 19 | for x in episodi: 20 | x.download() 21 | 22 | except aw.DeprecatedLibrary as e: 23 | # Gestione dell'eccezione 24 | print("Aprire un issue su GitHub :(") 25 | ``` 26 | 27 | ### Error404 28 | 29 | L'eccezione [`Error404`][animeworld.exceptions.Error404] viene sollevata quando l'URL passato alla creazione dell'oggetto Anime punta ad una pagina [404](https://www.animeworld.ac/404). 30 | 31 | Visto che questa eccezione viene sollevata solo dalla classe [`Anime`][animeworld.Anime], è consigliato gestirla solo se effettivamente si sta istanziando un oggetto di quella classe. 32 | 33 | ```py linenums="1" hl_lines="7" 34 | try: 35 | res = aw.find("...") 36 | 37 | try: 38 | anime = aw.Anime("/play/...") 39 | 40 | except aw.Error404 as e: 41 | # Gestione dell'eccezione 42 | print("Anime non trovato :(") 43 | 44 | else: 45 | episodi = anime.getEpisodes() 46 | for x in episodi: 47 | x.download() 48 | 49 | except aw.DeprecatedLibrary as e: 50 | # Gestione dell'eccezione 51 | print("Aprire un issue su GitHub :(") 52 | ``` 53 | 54 | ### AnimeNotAvailable 55 | 56 | L'eccezione [`AnimeNotAvailable`][animeworld.exceptions.AnimeNotAvailable] viene sollevata quando la pagina dell'anime esiste ma gli episodi non sono ancora disponibili. Questo accade ad esempio quando inizia una nuova stagione. 57 | 58 | L'eccezione si verifica soltanto alla chiamata del metodo [`getEpisodes`][animeworld.Anime.getEpisodes]. 59 | 60 | ```py linenums="1" hl_lines="15" 61 | try: 62 | res = aw.find("...") 63 | 64 | try: 65 | anime = aw.Anime("/play/...") 66 | 67 | except aw.Error404 as e: 68 | # Gestione dell'eccezione 69 | print("Anime non trovato :(") 70 | 71 | else: 72 | try: 73 | episodi = anime.getEpisodes() 74 | 75 | except aw.AnimeNotAvailable as e: 76 | # Gestione dell'eccezione 77 | print("Anime non ancora disponibile :(") 78 | 79 | else: 80 | for x in episodi: 81 | x.download() 82 | 83 | except aw.DeprecatedLibrary as e: 84 | # Gestione dell'eccezione 85 | print("Aprire un issue su GitHub :(") 86 | ``` 87 | 88 | ## Server 89 | 90 | Per scarica un episodio è possibile selezione manualmente il server da cui scaricare il video. Per farlo è ottenere prima la lista dei server utilizzando l'attributo [`Episodio.links`][animeworld.Episodio.links] e poi sceglierne uno tra quelli supportati. 91 | 92 | !!! Warning 93 | Non consiglio di utilizzare questo metodo per scaricare un episodio, è molto piú semplice e sicuro utilizzare il metodo [`Episodio.download`][animeworld.Episodio.download], perchè: 94 | 95 | 1. Viene scelto sempre il server più veloce al momento del download. 96 | 2. Se si sceglie un server non supportato, verrà sollevata l'eccezione [ServerNotSupported][animeworld.exceptions.ServerNotSupported]. 97 | 98 | ```py linenums="1" 99 | anime = aw.Anime("...") 100 | 101 | # Scelgo il primo episodio 102 | episodio = anime.getEpisodes()[0] 103 | 104 | # Ottengo la lista dei server 105 | servers = episodio.links 106 | 107 | # Scelgo il server AnimeWorld_Server ad esempio 108 | server = [x for x in servers if isinstance(x.name, AnimeWorld_Server)][0] 109 | 110 | # Scarico il video 111 | server.download() 112 | ``` 113 | 114 | ### Server supportati 115 | 116 | I server supportati sono indicati di seguito, se vuoi contribuire per aggiungerne altri puoi dare un occhiata alla sezione [Contributing](../community/contributing.md). 117 | 118 | --8<-- "static/server.txt" 119 | 120 | ## Download 121 | 122 | !!! Warning inline end 123 | Se ci sono dei caratteri non ammessi nel nome del file (`#%&{}<>*?/$!'":@+\``|=`), questi verranno rimossi automaticamente. Per ottenere il nome del file effettivamente scritto su disco è possibile ottenerlo dal ritorno del metodo [`Episodio.download`][animeworld.Episodio.download]. 124 | 125 | 126 | Per ottenere un episodio, consiglio di utilizzare il metodo [`Episodio.download`][animeworld.Episodio.download], il quale recupera il video utilizzando il server più veloce disponibile al momento del download. 127 | 128 | È possibile impostare il nome del file utilizzando il parametro `title` e la cartella di destinazione utilizzando il parametro `folder`. 129 | 130 | ### hook 131 | 132 | Il parametro `hook` è più interessante, questo è un riferimento ad una funzione che poi verrà chiamata ogni volta che viene scaricato un chunk del video (~524 Kb). Questo è utile per mostrare a schermo il progresso del download. La funzione deve avere un singolo parametro di tipo `Dict[str, Any]`. 133 | 134 | ```py 135 | def my_hook(data): 136 | print(data) 137 | 138 | episodi = anime.getEpisodes() 139 | for x in episodi: 140 | x.download(hook=my_hook) 141 | ``` 142 | 143 | Un esempio di un possibile dizionario passato alla funzione hook è il seguente: 144 | 145 | ```py 146 | { 147 | "total_bytes": 234127340, # Dimensione totale del video in byte 148 | "downloaded_bytes": 524288, # Dimensione scaricata in byte 149 | "percentage": 0.0022393283928310126, # Percentuale scaricata [0, 1] 150 | "speed": 3048288.673006227, # Velocità di download in byte/s 151 | "elapsed": 0.17199420928955078, # Tempo trascorso in secondi 152 | "filename": "1 - AnimeWorld Server.mp4", # Nome del file 153 | "eta": 76.63416331551707, # Tempo rimanente stimato in secondi 154 | "status": "downloading", # Stato del download ('downloading' | 'finished' | 'aborted') 155 | } 156 | ``` 157 | 158 | ### opt 159 | 160 | È anche possibile fermare forzatamente il download utilizzando il parametro `opt`. Questo parametro è una lista di stringhe, ogni stringa rappresenta un'opzione. Attualmente l'unica opzione possibile è `abort`, che ferma il download. 161 | 162 | Se in opt compare, durante il download, la stringa `abort` allora il download viene fermato e il file parzialmente scaricato viene eliminato. 163 | 164 | Un esempio di utilizzo del parametro `opt` è il seguente: 165 | 166 | ```py linenums="1" 167 | import animeworld as aw 168 | import time 169 | from threading import Thread 170 | 171 | anime = aw.Anime("...") 172 | episodio = anime.getEpisodes()[0] 173 | 174 | # Definisco la funzione per il thread 175 | def gestioneDownload(lista_opzioni): 176 | time.sleep(5) 177 | lista_opzioni.append("abort") 178 | 179 | 180 | opt = [] # Array per le opzioni dinamiche 181 | t = Thread(target=gestioneDownload, args=(opt,)) # Creo il thread 182 | 183 | t.start() # Avvio il thread 184 | 185 | episodio.download(opt=opt) # Avvio il download 186 | ``` 187 | 188 | In questo esempio il download viene fermato dopo 5 secondi. 189 | 190 | ### I/O Buffer 191 | 192 | È 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). 193 | 194 | ```py linenums="1" 195 | import animeworld as aw 196 | import io 197 | 198 | anime = aw.Anime("...") 199 | episodio = anime.getEpisodes()[0] 200 | 201 | buffer = io.BytesIO() # Alloco un buffer in memoria 202 | 203 | episodio.download(folder=buffer) # Avvio il download 204 | ``` 205 | 206 | In questo esempio l'episodio scaricato viene scritto in memoria senza essere salvato come file. 207 | 208 | --- 209 | 210 | ## Esempio completo 211 | 212 | ```py title="example.py" linenums="1" 213 | --8<-- "static/example.py" 214 | ``` 215 | -------------------------------------------------------------------------------- /animeworld/anime.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modulo contenente la struttura a classe dell'anime. 3 | """ 4 | import httpx 5 | from bs4 import BeautifulSoup 6 | import re 7 | import time 8 | from typing import * 9 | 10 | from .utility import HealthCheck, SES 11 | from .exceptions import Error404, AnimeNotAvailable 12 | from .episodio import Episodio 13 | 14 | class Anime: 15 | """ 16 | Attributes: 17 | link: Link dell'anime. 18 | html: Pagina web di Animeworld dell'anime. 19 | """ 20 | 21 | def __init__(self, link: str): 22 | """ 23 | Args: 24 | link: Link dell'anime. 25 | 26 | Raises: 27 | DeprecatedLibrary: Cambiamento del sito Animeworld. 28 | Error404: È una pagina 404. 29 | """ 30 | 31 | self.link:str = httpx.URL(link).path 32 | self.html:bytes = self.__getHTML().content 33 | self.__check404() 34 | 35 | # Private 36 | def __getHTML(self) -> httpx.Response: 37 | """ 38 | Ottiene la pagina web di Animeworld dell'anime e aggiorna i cookies. 39 | 40 | Returns: 41 | La risposta di requests. 42 | 43 | Raises: 44 | DeprecatedLibrary: Cambiamento del sito Animeworld. 45 | 46 | Example: 47 | ```py 48 | return Response # Risposta GET 49 | ``` 50 | """ 51 | r = None 52 | retry = 0 53 | while True: 54 | try: 55 | r = SES.get(self.link, timeout=(3, 27), follow_redirects=True) 56 | 57 | except httpx.ReadTimeout as e: 58 | if retry <= 2: 59 | retry +=1 60 | time.sleep(1) # errore 61 | else: 62 | raise e 63 | 64 | else: 65 | break 66 | r.raise_for_status() 67 | return r 68 | 69 | # Private 70 | def __check404(self): 71 | """ 72 | Controlla se la pagina è una pagina 404. 73 | 74 | Raises: 75 | Error404: È una pagina 404. 76 | """ 77 | if self.html.decode("utf-8").find('Errore 404') != -1: raise Error404(self.link) 78 | 79 | # Private 80 | @HealthCheck 81 | def __getServer(self) -> Dict[int, Dict[str, str]]: 82 | """ 83 | Ottiene tutti i server in cui sono hostati gli episodi. 84 | 85 | Raises: 86 | DeprecatedLibrary: Cambiamento del sito Animeworld. 87 | AnimeNotAvailable: L'anime non è ancora disponibile. 88 | 89 | Example: 90 | ``` 91 | return { 92 | int: { # ID del server 93 | name: str # Nome del server 94 | }, 95 | ... 96 | } 97 | ``` 98 | """ 99 | soupeddata = BeautifulSoup(self.html, "html.parser") 100 | block = soupeddata.find("span", { "class" : "servers-tabs" }) 101 | 102 | if block == None: raise AnimeNotAvailable(self.getName()) 103 | 104 | providers = block.find_all("span", { "class" : "server-tab" }) 105 | return { 106 | int(x["data-name"]): { 107 | "name": x.get_text() 108 | } 109 | for x in providers 110 | } 111 | 112 | @HealthCheck 113 | def getTrama(self) -> str: 114 | """ 115 | Ottiene la trama dell'anime. 116 | 117 | Returns: 118 | La trama dell'anime. 119 | 120 | Raises: 121 | DeprecatedLibrary: Cambiamento del sito Animeworld. 122 | 123 | Example: 124 | ```py 125 | return str # Trama anime. 126 | ``` 127 | """ 128 | soupeddata = BeautifulSoup(self.html, "html.parser") 129 | return soupeddata.find("div", { "class" : "desc" }).get_text() 130 | 131 | @HealthCheck 132 | def getCover(self) -> str: 133 | """ 134 | Ottiene l'url dell'immagine di copertina dell'anime. 135 | 136 | Returns: 137 | Url dell'immagine di copertina dell'anime. 138 | 139 | Raises: 140 | DeprecatedLibrary: Cambiamento del sito Animeworld. 141 | 142 | Example: 143 | ```py 144 | return str # Url dell'immagine di copertina dell'anime 145 | ``` 146 | """ 147 | soupeddata = BeautifulSoup(self.html, "html.parser") 148 | return soupeddata.find("div", { "id" : "thumbnail-watch" }).find("img")["src"] 149 | 150 | @HealthCheck 151 | def getInfo(self) -> Dict[str, str]: 152 | """ 153 | Ottiene le informazioni dell'anime. 154 | 155 | Returns: 156 | Informazioni anime. 157 | 158 | Raises: 159 | DeprecatedLibrary: Cambiamento del sito Animeworld. 160 | 161 | Example: 162 | ```py 163 | return { 164 | 'Categoria': str, 165 | 'Audio': str, 166 | 'Data di Uscita': str, 167 | 'Stagione': str, 168 | 'Studio': str, 169 | 'Genere': List[str], 170 | 'Voto': str, 171 | 'Durata': str, 172 | 'Episodi': str, 173 | 'Stato': str, 174 | 'Visualizzazioni': str 175 | } 176 | ``` 177 | """ 178 | soupeddata = BeautifulSoup(self.html, "html.parser") 179 | block = soupeddata.find("div", { "class" : "info" }).find("div", { "class" : "row" }) 180 | 181 | tName = [x.get_text().replace(':', '') for x in block.find_all("dt")] 182 | tInfo = [] 183 | for x in block.find_all("dd"): 184 | txt = x.get_text() 185 | if len(txt.split(',')) > 1: 186 | tInfo.append([x.strip() for x in txt.split(',')]) 187 | else: 188 | tInfo.append(txt.strip()) 189 | 190 | return dict(zip(tName, tInfo)) 191 | 192 | @HealthCheck 193 | def getName(self) -> str: # Nome dell'anime 194 | """ 195 | Ottiene il nome dell'anime. 196 | 197 | Returns: 198 | Nome anime. 199 | 200 | Raises: 201 | DeprecatedLibrary: Cambiamento del sito Animeworld. 202 | 203 | Example: 204 | ```py 205 | return str # Nome dell'anime 206 | ``` 207 | """ 208 | soupeddata = BeautifulSoup(self.html, "html.parser") 209 | return soupeddata.find("h1", { "id" : "anime-title" }).get_text() 210 | 211 | ############# 212 | 213 | @HealthCheck 214 | def getEpisodes(self, nums: Union[List[int], List[str]] = None) -> List[Episodio]: # Ritorna una lista di Episodi 215 | """ 216 | Ottiene tutti gli episodi dell'anime. 217 | 218 | Args: 219 | nums: I numeri degli episodi da ottenere 220 | 221 | Note: 222 | Se `nums` è `None` o `[]` allora il metodo restituisce tutti gli episodi dell'anime. 223 | 224 | Returns: 225 | Lista di oggetti Episodio. 226 | 227 | Raises: 228 | AnimeNotAvailable: L'anime non è ancora disponibile. 229 | DeprecatedLibrary: Cambiamento del sito Animeworld. 230 | 231 | Example: 232 | ```py 233 | return [ 234 | Episodio, # Classe Episodio 235 | ... 236 | ] 237 | ``` 238 | """ 239 | 240 | # Controllo se viene passata una lista di episodi da filtrare 241 | if nums: nums = list(map(str, nums)) 242 | 243 | soupeddata = BeautifulSoup(self.html.decode('utf-8', 'ignore'), "html.parser") 244 | 245 | a_link = soupeddata.select_one('li.episode > a') 246 | if a_link is None: raise AnimeNotAvailable(self.getName()) 247 | 248 | self.link = str(SES.build_url(a_link.get('href'))) 249 | 250 | provLegacy = self.__getServer() # vecchio sistema di cattura server 251 | 252 | raw_eps = {} 253 | for provID in provLegacy: 254 | prov_soup = soupeddata.select_one(f"div[class*='server'][data-name='{provID}']") 255 | 256 | for data in prov_soup.select('li.episode > a'): 257 | epNum = data.get('data-episode-num') 258 | epID = data.get('data-episode-id') 259 | 260 | if epID not in raw_eps: 261 | raw_eps[epID] = { 262 | 'number': epNum, 263 | 'link': str(SES.build_url(f"/api/download/{epID}")), 264 | 'legacy': [{ 265 | "id": int(provID), 266 | "name": provLegacy[provID]["name"], 267 | "link": str(SES.build_url(data.get("href"))) 268 | }] 269 | } 270 | else: 271 | raw_eps[epID]['legacy'].append({ 272 | "id": int(provID), 273 | "name": provLegacy[provID]["name"], 274 | "link": str(SES.build_url(data.get("href"))) 275 | }) 276 | 277 | return [ 278 | Episodio(x['number'], x['link'], x['legacy']) 279 | for x in list(raw_eps.values()) 280 | if not nums or x['number'] in nums 281 | ] -------------------------------------------------------------------------------- /animeworld/servers/Server.py: -------------------------------------------------------------------------------- 1 | import os, time, io 2 | from typing import Dict, List, Optional, Callable, Union 3 | from datetime import datetime 4 | import youtube_dl 5 | 6 | from ..utility import SES 7 | from ..exceptions import ServerNotSupported, HardStoppedDownload 8 | 9 | class Server: 10 | """ 11 | Attributes: 12 | link: Link del server in cui è hostato l'episodio. 13 | Nid: ID del server. 14 | name: Nome del server. 15 | number: Numero dell'episodio. 16 | """ 17 | 18 | def __init__(self, link: str, Nid: int, name: str, number: str): 19 | """ 20 | Args: 21 | link: Link del server in cui è hostato l'episodio. 22 | Nid: ID del server. 23 | name: Nome del server. 24 | number: Numero dell'episodio. 25 | """ 26 | 27 | self.link:str = link 28 | self.Nid:int = Nid 29 | self.name:str = name 30 | self.number:str = number 31 | 32 | # self._HDR = HDR # Protected 33 | self._defTitle = f"{self.number} - {self.name}" # nome del file provvisorio 34 | 35 | def _sanitize(self, title: str) -> str: # Toglie i caratteri illegali per i file 36 | """ 37 | Rimuove i caratteri illegali per il nome del file. 38 | 39 | Args: 40 | title: Nome del file. 41 | 42 | Returns: 43 | Il nome del file sanitizzato. 44 | 45 | Example: 46 | ```py 47 | return str # title sanitizzato 48 | ``` 49 | """ 50 | illegal = ['#','%','&','{','}', '\\','<','>','*','?','/','$','!',"'",'"',':','@','+','`','|','='] 51 | for x in illegal: 52 | title = title.replace(x, '') 53 | return title 54 | 55 | def fileInfo(self) -> dict[str,str]: 56 | """ 57 | Recupera le informazioni del file dell'episodio. 58 | 59 | Returns: 60 | Informazioni file episodio. 61 | 62 | Raises: 63 | ServerNotSupported: Se il server non è supportato. 64 | 65 | Example: 66 | ```py 67 | return { 68 | "content_type": str, # Tipo del file, es. video/mp4 69 | "total_bytes": int, # Byte totali del file 70 | "last_modified": datetime, # Data e ora dell'ultimo aggiornamento effettuato all'episodio sul server 71 | "server_name": str, # Nome del server 72 | "server_id": int, # ID del server 73 | "url": str # url dell'episodio 74 | } 75 | ``` 76 | """ 77 | 78 | raise ServerNotSupported(self.name) 79 | 80 | def fileLink(self) -> str: 81 | """ 82 | Recupera il link diretto per il download del file dell'episodio. 83 | 84 | Returns: 85 | Link diretto. 86 | 87 | Raises: 88 | ServerNotSupported: Se il server non è supportato. 89 | 90 | Example: 91 | ```py 92 | return str # Link del file 93 | ``` 94 | """ 95 | 96 | raise ServerNotSupported(self.name) 97 | 98 | def _fileInfoIn(self) -> Dict[str,str]: 99 | """ 100 | Recupera le informazioni del file dell'episodio usando httpx. 101 | """ 102 | url = self.fileLink() 103 | 104 | r = SES.head(url, follow_redirects=True) 105 | r.raise_for_status() 106 | 107 | return { 108 | "content_type": r.headers['content-type'], 109 | "total_bytes": int(r.headers['Content-Length']), 110 | "last_modified": datetime.strptime(r.headers['Last-Modified'], "%a, %d %b %Y %H:%M:%S %Z"), 111 | "server_name": self.name, 112 | "server_id": self.Nid, 113 | "url": url 114 | } 115 | 116 | def _fileInfoEx(self) -> Dict[str,str]: 117 | """ 118 | Recupera le informazioni del file dell'episodio usando yutube_dl. 119 | """ 120 | 121 | class MyLogger(object): 122 | def debug(self, msg): 123 | pass 124 | def warning(self, msg): 125 | pass 126 | def error(self, msg): 127 | print(msg) 128 | return False 129 | 130 | ydl_opts = { 131 | 'logger': MyLogger() 132 | } 133 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 134 | url = self.fileLink() 135 | info = ydl.extract_info(url, download=False) 136 | return { 137 | "content_type": "unknown", 138 | "total_bytes": -1, 139 | "last_modified": datetime.fromtimestamp(0), 140 | "server_name": self.name, 141 | "server_id": self.Nid, 142 | "url": url 143 | } 144 | 145 | 146 | def download(self, title: Optional[str]=None, folder: Union[str, io.IOBase]='', *, hook: Callable[[Dict], None]=lambda *args:None, opt: List[str]=[]) -> Optional[str]: 147 | """ 148 | Scarica l'episodio dal primo server funzionante della lista links. 149 | 150 | Args: 151 | title: Nome con cui verrà nominato il file scaricato. 152 | folder: Posizione in cui verrà spostato il file scaricato. 153 | 154 | Other parameters: 155 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 156 | - `total_bytes`: Byte totali da scaricare. 157 | - `downloaded_bytes`: Byte attualmente scaricati. 158 | - `percentage`: Percentuale del progresso di download. 159 | - `speed`: Velocità di download (byte/s) 160 | - `elapsed`: Tempo trascorso dall'inizio del download. 161 | - `eta`: Tempo stimato rimanente per fine del download. 162 | - `status`: 'downloading' | 'finished' | 'aborted' 163 | - `filename`: Nome del file in download. 164 | 165 | opt: Lista per delle opzioni aggiuntive.\n 166 | - `'abort'`: Ferma forzatamente il download. 167 | 168 | Returns: 169 | Nome del file scaricato. 170 | 171 | Raises: 172 | ServerNotSupported: Se il server non è supportato. 173 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 174 | 175 | Example: 176 | ```py 177 | return str # File scaricato 178 | ``` 179 | """ 180 | raise ServerNotSupported(self.name) 181 | 182 | # Protected 183 | def _downloadIn(self, title: str, folder: Union[str, io.IOBase], *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: # Scarica l'episodio 184 | 185 | """ 186 | Scarica il file utilizzando httpx. 187 | 188 | Args: 189 | title: Nome con cui verrà nominato il file scaricato. 190 | folder: Posizione in cui verrà spostato il file scaricato. 191 | 192 | Other parameters: 193 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 194 | - `total_bytes`: Byte totali da scaricare. 195 | - `downloaded_bytes`: Byte attualmente scaricati. 196 | - `percentage`: Percentuale del progresso di download. 197 | - `speed`: Velocità di download (byte/s) 198 | - `elapsed`: Tempo trascorso dall'inizio del download. 199 | - `eta`: Tempo stimato rimanente per fine del download. 200 | - `status`: 'downloading' | 'finished' | 'aborted' 201 | - `filename`: Nome del file in download. 202 | 203 | opt: Lista per delle opzioni aggiuntive.\n 204 | - `'abort'`: Ferma forzatamente il download. 205 | 206 | Returns: 207 | Nome del file scaricato. 208 | 209 | Raises: 210 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 211 | 212 | Example: 213 | ```py 214 | return str # File scaricato 215 | ``` 216 | """ 217 | with SES.stream("GET", self.fileLink(), follow_redirects=True) as r: 218 | r.raise_for_status() 219 | ext = r.headers['content-type'].split('/')[-1] 220 | if ext == 'octet-stream': ext = 'mp4' 221 | file = f"{title}.{ext}" 222 | 223 | total_length = int(r.headers.get('content-length')) 224 | current_lenght = 0 225 | start = time.time() 226 | step = time.time() 227 | 228 | fd:io.IOBase = None 229 | if isinstance(folder, io.IOBase): fd = folder 230 | else: fd = open(f"{os.path.join(folder,file)}", 'wb') 231 | 232 | try: 233 | for chunk in r.iter_bytes(chunk_size = 524288): 234 | if chunk: 235 | fd.write(chunk) 236 | fd.flush() 237 | 238 | current_lenght += len(chunk) 239 | 240 | hook({ 241 | 'total_bytes': total_length, 242 | 'downloaded_bytes': current_lenght, 243 | 'percentage': current_lenght/total_length, 244 | 'speed': len(chunk) / (time.time() - step) if (time.time() - step) != 0 else 0, 245 | 'elapsed': time.time() - start, 246 | 'filename': file, 247 | 'eta': ((total_length - current_lenght) / len(chunk)) * (time.time() - step), 248 | 'status': 'downloading' if "abort" not in opt else "aborted" 249 | }) 250 | 251 | if "abort" in opt: raise HardStoppedDownload(file) 252 | 253 | step = time.time() 254 | 255 | else: 256 | hook({ 257 | 'total_bytes': total_length, 258 | 'downloaded_bytes': total_length, 259 | 'percentage': 1, 260 | 'speed': 0, 261 | 'elapsed': time.time() - start, 262 | 'eta': 0, 263 | 'status': 'finished' 264 | }) 265 | 266 | if isinstance(folder, str): fd.close() 267 | else: fd.seek(0) 268 | return file # Se il file è stato scaricato correttamente 269 | except HardStoppedDownload: 270 | if isinstance(folder, str): 271 | fd.close() 272 | os.remove(f"{os.path.join(folder,file)}") 273 | else: fd.seek(0) 274 | return None 275 | 276 | # Protected 277 | def _dowloadEx(self, title: str, folder: Union[str, io.IOBase], *, hook: Callable[[Dict], None], opt: List[str]) -> Optional[str]: 278 | """ 279 | Scarica il file utilizzando yutube_dl. 280 | 281 | Args: 282 | title: Nome con cui verrà nominato il file scaricato. 283 | folder: Posizione in cui verrà spostato il file scaricato. 284 | 285 | Other parameters: 286 | hook: Funzione che viene richiamata varie volte durante il download; la funzione riceve come argomento un dizionario con le seguenti chiavi:\n 287 | - `total_bytes`: Byte totali da scaricare. 288 | - `downloaded_bytes`: Byte attualmente scaricati. 289 | - `percentage`: Percentuale del progresso di download. 290 | - `speed`: Velocità di download (byte/s) 291 | - `elapsed`: Tempo trascorso dall'inizio del download. 292 | - `eta`: Tempo stimato rimanente per fine del download. 293 | - `status`: 'downloading' | 'finished' | 'aborted' 294 | - `filename`: Nome del file in download. 295 | 296 | opt: Lista per delle opzioni aggiuntive.\n 297 | - `'abort'`: Ferma forzatamente il download. 298 | 299 | Returns: 300 | Nome del file scaricato. 301 | 302 | Raises: 303 | HardStoppedDownload: Il file in download è stato forzatamente interrotto. 304 | 305 | Example: 306 | ```py 307 | return str # File scaricato 308 | ``` 309 | """ 310 | 311 | tmp = '' 312 | if isinstance(folder, str): tmp = folder 313 | 314 | class MyLogger(object): 315 | def debug(self, msg): 316 | pass 317 | def warning(self, msg): 318 | pass 319 | def error(self, msg): 320 | print(msg) 321 | return False 322 | 323 | def my_hook(d): 324 | 325 | hook({ 326 | 'total_bytes': int(d['total_bytes_estimate']), 327 | 'downloaded_bytes': int(d['downloaded_bytes']), 328 | 'percentage': int(d['downloaded_bytes'])/int(d['total_bytes_estimate']), 329 | 'speed': float(d['speed']) if d['speed'] is not None else 0, 330 | 'elapsed': float(d['elapsed']), 331 | 'filename': d['filename'], 332 | 'eta': int(d['eta']), 333 | 'status': d['status'] if "abort" not in opt else "aborted" 334 | }) 335 | 336 | if "abort" in opt: raise HardStoppedDownload(d['filename']) 337 | 338 | ydl_opts = { 339 | 'outtmpl': f"{os.path.join(tmp,title)}.%(ext)s", 340 | 'logger': MyLogger(), 341 | 'progress_hooks': [my_hook], 342 | } 343 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 344 | url = self.fileLink() 345 | info = ydl.extract_info(url, download=False) 346 | filename = ydl.prepare_filename(info) 347 | try: 348 | ydl.download([url]) 349 | except HardStoppedDownload: 350 | os.remove(f"{os.path.join(tmp,filename)}") 351 | return None 352 | if isinstance(folder, io.IOBase): 353 | with open(os.path.join(tmp,filename), 'rb') as f: 354 | folder.write(f.read()) 355 | f.seek(0) 356 | os.remove(f"{os.path.join(tmp,filename)}") 357 | return filename --------------------------------------------------------------------------------