├── 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 |
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 |
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 | [](https://mainkronos.github.io/AnimeWorld-API/)
2 | # AnimeWorld-API
3 |
4 | [](https://github.com/MainKronos/AnimeWorld-API/releases/latest)
5 | 
6 | [](https://pypi.org/project/animeworld/)
7 | [](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml)
8 |
9 | 
10 | 
11 | 
12 |
13 | [](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.md)
14 | [](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 | [](https://star-history.com/#MainKronos/AnimeWorld-API&Date)
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://mainkronos.github.io/AnimeWorld-API/)
2 | # AnimeWorld-API
3 |
4 | [](https://github.com/MainKronos/AnimeWorld-API/releases/latest)
5 | 
6 | [](https://pypi.org/project/animeworld/)
7 | [](https://github.com/MainKronos/AnimeWorld-API/actions/workflows/deploy-mkdocs.yml)
8 |
9 | 
10 | 
11 | 
12 |
13 | [](https://github.com/MainKronos/AnimeWorld-API/blob/master/README.md)
14 | [](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 | [](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
--------------------------------------------------------------------------------