├── requirements.txt ├── animeflv ├── exception.py ├── __init__.py └── animeflv.py ├── setup.py ├── .gitignore ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ ├── python-publish.yml │ └── python-package.yml ├── README.md └── tests.py /requirements.txt: -------------------------------------------------------------------------------- 1 | cloudscraper 2 | lxml 3 | beautifulsoup4 -------------------------------------------------------------------------------- /animeflv/exception.py: -------------------------------------------------------------------------------- 1 | class AnimeFLVParseError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | if __name__ == "__main__": 4 | setuptools.setup(name="animeflv") -------------------------------------------------------------------------------- /animeflv/__init__.py: -------------------------------------------------------------------------------- 1 | from .animeflv import AnimeFLV, EpisodeFormat, EpisodeInfo, AnimeInfo 2 | 3 | __version__ = "0.3.1" 4 | __title__ = "animeflv" 5 | __author__ = "Jorge Alejandro Jiménez Luna" 6 | __license__ = "MIT" 7 | __copyright__ = "Copyright 2022 Jorge Alejandro Jiménez Luna" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # VSCode 6 | .vscode 7 | 8 | # C extensions 9 | *.so 10 | 11 | # PyInstaller 12 | # Usually these files are written by a python script from a template 13 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 14 | *.manifest 15 | *.spec 16 | 17 | # Installer logs 18 | pip-log.txt 19 | pip-delete-this-directory.txt 20 | 21 | # Unit test / coverage reports 22 | htmlcov/ 23 | .tox/ 24 | .coverage 25 | .coverage.* 26 | .cache 27 | .pytest_cache 28 | nosetests.xml 29 | coverage.xml 30 | *,cover 31 | .coveralls.yml 32 | 33 | # Translations 34 | *.mo 35 | *.pot 36 | 37 | # Django stuff: 38 | *.log 39 | 40 | # Sphinx documentation 41 | docs/_build/ 42 | 43 | # PyBuilder 44 | target/ 45 | .idea/ 46 | 47 | # Sublime Text 2 48 | *.sublime* 49 | 50 | # Virtualenv 51 | .venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jorge Alejandro Jiménez Luna 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"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "animeflv" 7 | license = { text = "MIT" } 8 | description = "AnimeFLV is a python custom API for https://animeflv.net website" 9 | requires-python = ">=3.5" 10 | keywords = ["animeflv", "anime", "manga"] 11 | authors = [{ name = "Jorge Alejandro Jiménez Luna", email = "jorgeajimenezl17@gmail.com" }] 12 | urls = { homepage = "https://github.com/jorgeajimenezl/animeflv-api" } 13 | readme = "README.md" 14 | classifiers = [ 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3.5", 20 | "Programming Language :: Python :: 3.6", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | ] 27 | dynamic = ["version", "dependencies"] 28 | 29 | [tool.setuptools.dynamic] 30 | version = { attr = "animeflv.__version__" } 31 | dependencies = { file = ["requirements.txt"] } -------------------------------------------------------------------------------- /.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://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with unittest 39 | run: | 40 | python -m unittest 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnimeFLV API 2 | 3 | [![Build Status](https://github.com/jorgeajimenezl/animeflv-api/actions/workflows/python-package.yml/badge.svg)](https://github.com/jorgeajimenezl/animeflv-api/actions/workflows/python-package.yml) 4 | [![Upload Python Package](https://github.com/jorgeajimenezl/animeflv-api/actions/workflows/python-publish.yml/badge.svg)](https://github.com/jorgeajimenezl/animeflv-api/actions/workflows/python-publish.yml) 5 | 6 | > AnimeFLV is a python custom API for [animeflv.net](https://animeflv.net) a Spanish anime content website. 7 | 8 | ## Installation 9 | 10 | For install with pip: 11 | 12 | ```bash 13 | pip install animeflv 14 | ``` 15 | 16 | Install from source: 17 | 18 | ```bash 19 | git clone https://github.com/jorgeajimenezl/animeflv-api.git 20 | cd animeflv 21 | pip install -r requirements.txt 22 | pip install . 23 | ``` 24 | 25 | ## API Documentation 26 | 27 | #### [Read this](https://github.com/jorgeajimenezl/animeflv-api/wiki) | [Watch videos](https://youtube.com) 28 | 29 | #### Create animeflv api instance 30 | 31 | ```python 32 | >>> from animeflv import AnimeFLV 33 | >>> with AnimeFLV() as api: 34 | >>> # Do anything with api object 35 | >>> ... 36 | ``` 37 | 38 | #### Features 39 | 40 | - [X] Get download links by episodes 41 | - [X] Search 42 | - [X] Get Video Servers 43 | - [X] Get Anime Info 44 | - [X] Get new releases (animes and episodes) 45 | 46 | ## License 47 | 48 | [MIT](./LICENSE) 49 | 50 | ## Authors 51 | 52 | - [Jorge Alejandro Jiménez Luna](https://github.com/jorgeajimenezl) 53 | - [Jimmy Angel Pérez Díaz](https://github.com/JimScope) 54 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | from typing import Any 4 | from cloudscraper.exceptions import CloudflareChallengeError 5 | from animeflv import AnimeFLV, EpisodeInfo, AnimeInfo 6 | 7 | 8 | def wrap_request(func, *args, count: int = 5, expected: Any): 9 | """ 10 | Wraps a request sent by the module to test if it works correctly, tries `count` times sleeps 11 | 5 seconds if an error is encountered. 12 | 13 | If `CloudflareChallengeError` is encountered, the expected result will be returned 14 | to make it possible for automated tests to pass 15 | 16 | :param *args: args to call the function with. 17 | :param count: amount of tries 18 | :param expected: example for a valid return, this is used when cloudscraper complains 19 | :rtype: Any 20 | """ 21 | notes = [] 22 | 23 | for _ in range(count): 24 | try: 25 | res = func(*args) 26 | if isinstance(res, list) and len(res) < 1: 27 | raise ValueError() # Raise ValueError to retry test when empty array is returned 28 | return res 29 | except CloudflareChallengeError: 30 | return expected 31 | except Exception as exc: 32 | notes.append(exc) 33 | time.sleep(5) 34 | raise Exception(notes) 35 | 36 | 37 | class AnimeFLVTest(unittest.TestCase): 38 | def test_search(self): 39 | with AnimeFLV() as api: 40 | res = wrap_request(api.search, "Nanatsu", expected=[AnimeInfo(0, "")]) 41 | 42 | self.assertGreater(len(res), 0) 43 | self.assertTrue(isinstance(res, list)) 44 | 45 | item = res[0] 46 | self.assertTrue(isinstance(item, AnimeInfo)) 47 | 48 | def test_list(self): 49 | with AnimeFLV() as api: 50 | res = wrap_request(api.list, 1, expected=[AnimeInfo(0, "")]) 51 | 52 | self.assertGreater(len(res), 0) 53 | self.assertTrue(isinstance(res, list)) 54 | 55 | item = res[0] 56 | self.assertTrue(isinstance(item, AnimeInfo)) 57 | 58 | def test_get_video_servers(self): 59 | with AnimeFLV() as api: 60 | res = wrap_request(api.get_video_servers, "nanatsu-no-taizai", 1, expected=["Lorem Ipsum"]) 61 | 62 | self.assertGreater(len(res), 0) 63 | self.assertTrue(isinstance(res, list)) 64 | 65 | def test_get_anime_info(self): 66 | with AnimeFLV() as api: 67 | res = wrap_request(api.get_anime_info, "nanatsu-no-taizai", expected=AnimeInfo(0, "")) 68 | 69 | self.assertTrue(isinstance(res, AnimeInfo)) 70 | 71 | def test_get_latest_episodes(self): 72 | with AnimeFLV() as api: 73 | res = wrap_request(api.get_latest_episodes, expected=[EpisodeInfo(0, "")]) 74 | 75 | self.assertGreater(len(res), 0) 76 | self.assertTrue(isinstance(res, list)) 77 | 78 | item = res[0] 79 | self.assertTrue(isinstance(item, EpisodeInfo)) 80 | 81 | def test_get_latest_animes(self): 82 | with AnimeFLV() as api: 83 | res = wrap_request(api.get_latest_animes, expected=[AnimeInfo(0, "")]) 84 | 85 | self.assertGreater(len(res), 0) 86 | self.assertTrue(isinstance(res, list)) 87 | 88 | item = res[0] 89 | self.assertTrue(isinstance(item, AnimeInfo)) 90 | -------------------------------------------------------------------------------- /animeflv/animeflv.py: -------------------------------------------------------------------------------- 1 | import cloudscraper 2 | import json, re 3 | 4 | from typing import Dict, List, Optional, Tuple, Type, Union 5 | from types import TracebackType 6 | from bs4 import BeautifulSoup, Tag, ResultSet 7 | from urllib.parse import unquote, urlencode 8 | from enum import Flag, auto 9 | from .exception import AnimeFLVParseError 10 | from dataclasses import dataclass 11 | 12 | 13 | def removeprefix(str: str, prefix: str) -> str: 14 | """ 15 | Remove the prefix of a given string if it contains that 16 | prefix for compatability with Python >3.9 17 | 18 | :param _str: string to remove prefix from. 19 | :param episode: prefix to remove from the string. 20 | :rtype: str 21 | """ 22 | 23 | if type(str) is type(prefix): 24 | if str.startswith(prefix): 25 | return str[len(prefix) :] 26 | else: 27 | return str[:] 28 | 29 | 30 | def parse_table(table: Tag): 31 | columns = list([x.string for x in table.thead.tr.find_all("th")]) 32 | rows = [] 33 | 34 | for row in table.tbody.find_all("tr"): 35 | values = row.find_all("td") 36 | 37 | if len(values) != len(columns): 38 | raise AnimeFLVParseError("Don't match values size with columns size") 39 | 40 | rows.append({h: x for h, x in zip(columns, values)}) 41 | 42 | return rows 43 | 44 | 45 | BASE_URL = "https://animeflv.net" 46 | BROWSE_URL = "https://animeflv.net/browse" 47 | ANIME_VIDEO_URL = "https://animeflv.net/ver/" 48 | ANIME_URL = "https://animeflv.net/anime/" 49 | BASE_EPISODE_IMG_URL = "https://cdn.animeflv.net/screenshots/" 50 | 51 | 52 | @dataclass 53 | class EpisodeInfo: 54 | id: Union[str, int] 55 | anime: str 56 | image_preview: Optional[str] = None 57 | 58 | 59 | @dataclass 60 | class AnimeInfo: 61 | id: Union[str, int] 62 | title: str 63 | poster: Optional[str] = None 64 | banner: Optional[str] = None 65 | synopsis: Optional[str] = None 66 | rating: Optional[str] = None 67 | genres: Optional[List[str]] = None 68 | debut: Optional[str] = None 69 | type: Optional[str] = None 70 | episodes: Optional[List[EpisodeInfo]] = None 71 | 72 | 73 | @dataclass 74 | class DownloadLinkInfo: 75 | server: str 76 | url: str 77 | 78 | 79 | class EpisodeFormat(Flag): 80 | Subtitled = auto() 81 | Dubbed = auto() 82 | 83 | 84 | class AnimeFLV(object): 85 | def __init__(self, *args, **kwargs): 86 | session = kwargs.get("session", None) 87 | self._scraper = cloudscraper.create_scraper(session) 88 | 89 | def close(self) -> None: 90 | self._scraper.close() 91 | 92 | def __enter__(self) -> "AnimeFLV": 93 | return self 94 | 95 | def __exit__( 96 | self, 97 | exc_type: Optional[Type[BaseException]], 98 | exc_val: Optional[BaseException], 99 | exc_tb: Optional[TracebackType], 100 | ) -> None: 101 | self.close() 102 | 103 | def get_links( 104 | self, 105 | id: str, 106 | episode: Union[str, int], 107 | format: EpisodeFormat = EpisodeFormat.Subtitled, 108 | **kwargs, 109 | ) -> List[DownloadLinkInfo]: 110 | """ 111 | Get download links of specific episode. 112 | Return a list of dictionaries like: 113 | [ 114 | { 115 | "server": "...", 116 | "url": "..." 117 | }, 118 | ... 119 | ] 120 | 121 | :param id: Anime id, like as 'nanatsu-no-taizai'. 122 | :param episode: Episode id, like as '1'. 123 | :param **kwargs: Optional arguments for filter output (see doc). 124 | :rtype: list 125 | """ 126 | response = self._scraper.get(f"{ANIME_VIDEO_URL}{id}-{episode}") 127 | soup = BeautifulSoup(response.text, "lxml") 128 | table = soup.find("table", attrs={"class": "RTbl"}) 129 | 130 | try: 131 | rows = parse_table(table) 132 | ret = [] 133 | 134 | for row in rows: 135 | if ( 136 | row["FORMATO"].string == "SUB" 137 | and EpisodeFormat.Subtitled in format 138 | or row["FORMATO"].string == "LAT" 139 | and EpisodeFormat.Dubbed in format 140 | ): 141 | ret.append( 142 | DownloadLinkInfo( 143 | server=row["SERVIDOR"].string, 144 | url=re.sub( 145 | r"^http[s]?://ouo.io/[A-Za-z0-9]+/[A-Za-z0-9]+\?[A-Za-z0-9]+=", 146 | "", 147 | unquote(row["DESCARGAR"].a["href"]), 148 | ), 149 | ) 150 | ) 151 | 152 | return ret 153 | except Exception as exc: 154 | raise AnimeFLVParseError(exc) 155 | 156 | def list(self, page: int = None) -> List[Dict[str, str]]: 157 | """ 158 | Shortcut for search(query=None) 159 | """ 160 | 161 | return self.search(page=page) 162 | 163 | def search(self, query: str = None, page: int = None) -> List[AnimeInfo]: 164 | """ 165 | Search in animeflv.net by query. 166 | :param query: Query information like: 'Nanatsu no Taizai'. 167 | :param page: Page of the information return. 168 | :rtype: list[AnimeInfo] 169 | """ 170 | 171 | if page is not None and not isinstance(page, int): 172 | raise TypeError 173 | 174 | params = dict() 175 | if query is not None: 176 | params["q"] = query 177 | if page is not None: 178 | params["page"] = page 179 | params = urlencode(params) 180 | 181 | url = f"{BROWSE_URL}" 182 | if params != "": 183 | url += f"?{params}" 184 | 185 | response = self._scraper.get(url) 186 | soup = BeautifulSoup(response.text, "lxml") 187 | 188 | elements = soup.select("div.Container ul.ListAnimes li article") 189 | 190 | if elements is None: 191 | raise AnimeFLVParseError("Unable to get list of animes") 192 | 193 | return self._process_anime_list_info(elements) 194 | 195 | def get_video_servers( 196 | self, 197 | id: str, 198 | episode: int, 199 | format: EpisodeFormat = EpisodeFormat.Subtitled, 200 | **kwargs, 201 | ) -> List[Dict[str, str]]: 202 | """ 203 | Get in video servers, this work only using the iframe element. 204 | Return a list of dictionaries. 205 | 206 | :param id: Anime id, like as 'nanatsu-no-taizai'. 207 | :param episode: Episode id, like as '1'. 208 | :rtype: list 209 | """ 210 | 211 | response = self._scraper.get(f"{ANIME_VIDEO_URL}{id}-{episode}") 212 | soup = BeautifulSoup(response.text, "lxml") 213 | scripts = soup.find_all("script") 214 | 215 | servers = [] 216 | 217 | for script in scripts: 218 | content = str(script) 219 | if "var videos = {" in content: 220 | videos = content.split("var videos = ")[1].split(";")[0] 221 | data = json.loads(videos) 222 | 223 | if "SUB" in data and EpisodeFormat.Subtitled in format: 224 | servers.append(data["SUB"]) 225 | if "LAT" in data and EpisodeFormat.Dubbed in format: 226 | servers.append(data["LAT"]) 227 | 228 | return servers 229 | 230 | def get_latest_episodes(self) -> List[EpisodeInfo]: 231 | """ 232 | Get a list of new episodes released (possibly this last week). 233 | Return a list 234 | 235 | :rtype: list 236 | """ 237 | 238 | response = self._scraper.get(BASE_URL) 239 | soup = BeautifulSoup(response.text, "lxml") 240 | 241 | elements = soup.select("ul.ListEpisodios li a") 242 | ret = [] 243 | 244 | for element in elements: 245 | try: 246 | anime, _, id = element["href"].rpartition("-") 247 | 248 | ret.append( 249 | EpisodeInfo( 250 | id=id, 251 | anime=removeprefix(anime, "/ver/"), 252 | image_preview=f"{BASE_URL}{element.select_one('span.Image img').get('src')}", 253 | ) 254 | ) 255 | except Exception as exc: 256 | raise AnimeFLVParseError(exc) 257 | 258 | return ret 259 | 260 | def get_latest_animes(self) -> List[AnimeInfo]: 261 | """ 262 | Get a list of new animes released. 263 | Return a list 264 | 265 | :rtype: list 266 | """ 267 | 268 | response = self._scraper.get(BASE_URL) 269 | soup = BeautifulSoup(response.text, "lxml") 270 | 271 | elements = soup.select("ul.ListAnimes li article") 272 | 273 | if elements is None: 274 | raise AnimeFLVParseError("Unable to get list of animes") 275 | 276 | return self._process_anime_list_info(elements) 277 | 278 | def get_anime_info(self, id: str) -> AnimeInfo: 279 | """ 280 | Get information about specific anime. 281 | Return a dictionary. 282 | 283 | :param id: Anime id, like as 'nanatsu-no-taizai'. 284 | :rtype: dict 285 | """ 286 | response = self._scraper.get(f"{ANIME_URL}/{id}") 287 | soup = BeautifulSoup(response.text, "lxml") 288 | 289 | synopsis = soup.select_one( 290 | "body div div div div div main section div.Description p" 291 | ).string 292 | 293 | information = { 294 | "title": soup.select_one( 295 | "body div.Wrapper div.Body div div.Ficha.fchlt div.Container h1.Title" 296 | ).string, 297 | "poster": BASE_URL 298 | + "/" 299 | + soup.select_one( 300 | "body div div div div div aside div.AnimeCover div.Image figure img" 301 | ).get("src", ""), 302 | "synopsis": synopsis.strip() if synopsis else None, 303 | "rating": soup.select_one( 304 | "body div div div.Ficha.fchlt div.Container div.vtshr div.Votes span#votes_prmd" 305 | ).string, 306 | "debut": soup.select_one( 307 | "body div.Wrapper div.Body div div.Container div.BX.Row.BFluid.Sp20 aside.SidebarA.BFixed p.AnmStts" 308 | ).string, 309 | "type": soup.select_one( 310 | "body div.Wrapper div.Body div div.Ficha.fchlt div.Container span.Type" 311 | ).string, 312 | } 313 | information["banner"] = ( 314 | information["poster"].replace("covers", "banners").strip() 315 | ) 316 | genres = [] 317 | 318 | for element in soup.select("main.Main section.WdgtCn nav.Nvgnrs a"): 319 | if "=" in element["href"]: 320 | genres.append(element["href"].split("=")[1]) 321 | 322 | info_ids = [] 323 | episodes_data = [] 324 | episodes = [] 325 | 326 | try: 327 | for script in soup.find_all("script"): 328 | contents = str(script) 329 | 330 | if "var anime_info = [" in contents: 331 | anime_info = contents.split("var anime_info = ")[1].split(";")[0] 332 | info_ids.append(json.loads(anime_info)) 333 | 334 | if "var episodes = [" in contents: 335 | data = contents.split("var episodes = ")[1].split(";")[0] 336 | episodes_data.extend(json.loads(data)) 337 | 338 | AnimeThumbnailsId = info_ids[0][0] 339 | animeId = info_ids[0][2] 340 | # nextEpisodeDate = info_ids[0][3] if len(info_ids[0]) > 4 else None 341 | 342 | for episode, _ in episodes_data: 343 | episodes.append( 344 | EpisodeInfo( 345 | id=episode, 346 | anime=id, 347 | image_preview=f"{BASE_EPISODE_IMG_URL}{AnimeThumbnailsId}/{episode}/th_3.jpg", 348 | ) 349 | ) 350 | 351 | except Exception as exc: 352 | raise AnimeFLVParseError(exc) 353 | 354 | return AnimeInfo( 355 | id=id, 356 | episodes=episodes, 357 | genres=genres, 358 | **information, 359 | ) 360 | 361 | def _process_anime_list_info(self, elements: ResultSet) -> List[AnimeInfo]: 362 | ret = [] 363 | 364 | for element in elements: 365 | try: 366 | ret.append( 367 | AnimeInfo( 368 | id=removeprefix( 369 | element.select_one("div.Description a.Button")["href"][1:], 370 | "anime/", 371 | ), 372 | title=element.select_one("a h3").string, 373 | poster=( 374 | element.select_one("a div.Image figure img").get( 375 | "src", None 376 | ) 377 | or element.select_one("a div.Image figure img")["data-cfsrc"] 378 | ), 379 | banner=( 380 | element.select_one("a div.Image figure img").get( 381 | "src", None 382 | ) 383 | or element.select_one("a div.Image figure img")["data-cfsrc"] 384 | ) 385 | .replace("covers", "banners") 386 | .strip(), 387 | type=element.select_one("div.Description p span.Type").string, 388 | synopsis=( 389 | element.select("div.Description p")[1].string.strip() 390 | if element.select("div.Description p")[1].string 391 | else None 392 | ), 393 | rating=element.select_one("div.Description p span.Vts").string, 394 | debut=( 395 | element.select_one("a span.Estreno").string.lower() 396 | if element.select_one("a span.Estreno") 397 | else None 398 | ), 399 | ) 400 | ) 401 | except Exception as exc: 402 | raise AnimeFLVParseError(exc) 403 | 404 | return ret 405 | --------------------------------------------------------------------------------