Code formatting and updated flamecomic to use changed api

This commit is contained in:
2026-03-12 14:18:31 +01:00 Verified
parent 7591639177
commit 9cd15e33fb
7 changed files with 600 additions and 564 deletions
+176 -176
View File
@@ -1,177 +1,177 @@
manga.json manga.json
config.ini config.ini
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest *.manifest
*.spec *.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/ cover/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff: # Django stuff:
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache .webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# .python-version # .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# UV # UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
#uv.lock #uv.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock #poetry.lock
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock #pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml .pdm.toml
.pdm-python .pdm-python
.pdm-build/ .pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
# Environments # Environments
.env .env
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# mkdocs documentation # mkdocs documentation
/site /site
# mypy # mypy
.mypy_cache/ .mypy_cache/
.dmypy.json .dmypy.json
dmypy.json dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # pytype static type analyzer
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# Ruff stuff: # Ruff stuff:
.ruff_cache/ .ruff_cache/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
+146 -140
View File
@@ -1,140 +1,146 @@
import requests import argparse
from pathlib import Path import configparser
import argparse import json
import configparser from pathlib import Path
import json
import questionary
import questionary import requests
from rich import print from manga import Manga
from rich import print
from sites import parse_provider from sites import parse_provider
from manga import Manga
parser = argparse.ArgumentParser(
parser = argparse.ArgumentParser( prog="Manga Downloader",
prog="Manga Downloader", description="Download all your favourite manga!",
description="Download all your favourite manga!", epilog="Happy Reading!",
epilog="Happy Reading!" )
) parser.add_argument(
parser.add_argument( "--auto",
"--auto", default=False,
default=False, action=argparse.BooleanOptionalAction,
action=argparse.BooleanOptionalAction, help="Run using auto mode (default: %(default)s)",
help="Run using auto mode (default: %(default)s)" )
) parser.add_argument(
parser.add_argument( "--save-dir",
"--save-dir", dest="save_dir",
dest="save_dir", type=Path,
type=Path, help="Set temporary save dir instead of using config value",
help="Set temporary save dir instead of using config value" )
)
args = parser.parse_args()
args = parser.parse_args()
s = requests.session()
s = requests.session() cache_file = Path(__file__).parent.resolve().joinpath("manga.json")
cache_file = Path(__file__).parent.resolve().joinpath("manga.json") config_file = Path(__file__).parent.resolve().joinpath("config.ini")
config_file = Path(__file__).parent.resolve().joinpath("config.ini")
def get_config() -> configparser.ConfigParser:
def get_config() -> configparser.ConfigParser: config = configparser.ConfigParser()
config = configparser.ConfigParser() if config_file.exists():
if config_file.exists(): config.read(config_file)
config.read(config_file)
if not config.has_option("DEFAULT", "savedir"):
if not config.has_option("DEFAULT", "savedir"): path = questionary.path(
path = questionary.path("Enter default save path for downloaded Manga: ", qmark="").ask() "Enter default save path for downloaded Manga: ", qmark=""
config["DEFAULT"] = {"savedir": path} ).ask()
config["DEFAULT"] = {"savedir": path}
with open(config_file, "w") as cf:
config.write(cf) with open(config_file, "w") as cf:
config.write(cf)
return config
return config
def read_from_cache() -> dict:
if cache_file.exists():
return json.loads(cache_file.read_bytes()) def read_from_cache() -> dict:
else: if cache_file.exists():
return {} return json.loads(cache_file.read_bytes())
else:
def write_to_cache(manga:Manga) -> None: return {}
cache = read_from_cache()
series = manga.info.get("series")
cached_chapters = cache.get(series, {}).get("cached",[]) + manga.cached def write_to_cache(manga: Manga) -> None:
cache = read_from_cache()
cache[series] = { series = manga.info.get("series")
"url": manga.site.get_url(), cached_chapters = cache.get(series, {}).get("cached", []) + manga.cached
"cached": list(set(cached_chapters))
} cache[series] = {"url": manga.site.get_url(), "cached": list(set(cached_chapters))}
with open(cache_file, "w") as fp: with open(cache_file, "w") as fp:
json.dump(cache,fp, indent=4) json.dump(cache, fp, indent=4)
def parse_cache(cache:dict) -> list[Manga]:
if not cache: def parse_cache(cache: dict) -> list[Manga]:
print("Cache is empty!") if not cache:
exit() print("Cache is empty!")
else: exit()
manga = list() else:
for entry in cache.values(): manga = list()
manga.append( for entry in cache.values():
Manga(parse_provider(s, entry.get("url")), entry.get("cached")) # type: ignore manga.append(
) Manga(parse_provider(s, entry.get("url")), entry.get("cached")) # type: ignore
return manga )
return manga
def get_cached_manga() -> list[Manga]:
cache = read_from_cache()
return parse_cache(cache) def get_cached_manga() -> list[Manga]:
cache = read_from_cache()
def input_manga() -> list[str]: return parse_cache(cache)
manga = list()
add_more = True
while add_more: def input_manga() -> list[str]:
manga.append( manga = list()
questionary.text( add_more = True
"Input Manga url:", while add_more:
qmark = "" manga.append(questionary.text("Input Manga url:", qmark="").ask())
).ask() add_more = questionary.confirm(
) "Do you want to add more Manga", default=False
add_more = questionary.confirm("Do you want to add more Manga", default=False).ask() ).ask()
return manga return manga
def parse_manga(manga:list[str]) -> list[Manga]:
parsed_manga = list() def parse_manga(manga: list[str]) -> list[Manga]:
for entry in manga: parsed_manga = list()
provider = parse_provider(s, entry) for entry in manga:
if not provider: provider = parse_provider(s, entry)
print(f"Failed to parse: {entry}") if not provider:
else: print(f"Failed to parse: {entry}")
parsed_manga.append(Manga(provider)) else:
return parsed_manga parsed_manga.append(Manga(provider))
return parsed_manga
def choose_manga() -> list[Manga]:
use_cached = questionary.confirm("Do you want to choose from cached Manga", default=True).ask()
def choose_manga() -> list[Manga]:
if use_cached: use_cached = questionary.confirm(
return get_cached_manga() "Do you want to choose from cached Manga", default=True
else: ).ask()
manga = input_manga()
return parse_manga(manga) if use_cached:
return get_cached_manga()
def main(): else:
manga = input_manga()
config = get_config() return parse_manga(manga)
save_dir = args.save_dir if args.save_dir else Path(config["DEFAULT"]["savedir"])
if args.auto: def main():
if not config_file.exists():
print("No config file exists") config = get_config()
exit(1) save_dir = args.save_dir if args.save_dir else Path(config["DEFAULT"]["savedir"])
else:
list_of_manga = get_cached_manga() if args.auto:
else: if not config_file.exists():
list_of_manga = choose_manga() print("No config file exists")
exit(1)
for manga in list_of_manga: else:
manga.choose_chapters(args.auto) list_of_manga = get_cached_manga()
manga.download(save_dir) else:
write_to_cache(manga) list_of_manga = choose_manga()
if __name__ == "__main__": for manga in list_of_manga:
main() manga.choose_chapters(args.auto)
manga.download(save_dir)
write_to_cache(manga)
if __name__ == "__main__":
main()
+97 -84
View File
@@ -1,84 +1,97 @@
import xml.etree.ElementTree as XML import tempfile
from pathlib import Path import xml.etree.ElementTree as XML
import tempfile from pathlib import Path
import questionary import questionary
from rich import print from packager import download_images, zip_files
from rich.progress import Progress, SpinnerColumn, TextColumn, MofNCompleteColumn from rich import print
from rich.progress import MofNCompleteColumn, Progress, SpinnerColumn, TextColumn
from sites import Provider from sites import Provider
from packager import download_images, zip_files
class Manga(): class Manga:
def __init__(self, provider:Provider, cached:list[int] = []) -> None: def __init__(self, provider: Provider, cached: list[int] = []) -> None:
self.site = provider self.site = provider
self.cached = cached self.cached = cached
self.info, self.chapters = self.site.get_mediainfo() self.info, self.chapters = self.site.get_mediainfo()
self.slug = self.slugify(self.info.get("series")) # type: ignore self.slug = self.slugify(self.info.get("series")) # type: ignore
@staticmethod @staticmethod
def slugify(string:str) -> str: def slugify(string: str) -> str:
string = "".join(x for x in string if not x in '<>:"/\\|?*') string = "".join(x for x in string if not x in '<>:"/\\|?*')
return str(string) return str(string)
def _generateComicInfo(self, chapter:dict, dir:Path): def _generateComicInfo(self, chapter: dict, dir: Path):
filename = dir.joinpath("ComicInfo.xml") filename = dir.joinpath("ComicInfo.xml")
ComicInfo = XML.Element("ComicInfo") ComicInfo = XML.Element("ComicInfo")
XML.SubElement(ComicInfo, "Series").text = self.info.get("series") XML.SubElement(ComicInfo, "Series").text = self.info.get("series")
XML.SubElement(ComicInfo, "Writer").text = ",".join(self.info.get("writer")) # type: ignore XML.SubElement(ComicInfo, "Writer").text = ",".join(self.info.get("writer")) # type: ignore
XML.SubElement(ComicInfo, "Penciller").text = ",".join(self.info.get("penciller")) # type: ignore XML.SubElement(ComicInfo, "Penciller").text = ",".join(self.info.get("penciller")) # type: ignore
XML.SubElement(ComicInfo, "Genre").text = ",".join(self.info.get("genre")) # type: ignore XML.SubElement(ComicInfo, "Genre").text = ",".join(self.info.get("genre")) # type: ignore
XML.SubElement(ComicInfo, "Summary").text = self.info.get("summary") XML.SubElement(ComicInfo, "Summary").text = self.info.get("summary")
XML.SubElement(ComicInfo, "Number").text = str(chapter.get("nr")) XML.SubElement(ComicInfo, "Number").text = str(chapter.get("nr"))
XML.SubElement(ComicInfo, "Title").text = chapter.get("title") XML.SubElement(ComicInfo, "Title").text = chapter.get("title")
XML.SubElement(ComicInfo, "LanguageISO").text = self.info.get("languageISO") XML.SubElement(ComicInfo, "LanguageISO").text = self.info.get("languageISO")
XML.SubElement(ComicInfo, "PageCount").text = str(len(chapter.get("images"))) # type: ignore XML.SubElement(ComicInfo, "PageCount").text = str(len(chapter.get("images"))) # type: ignore
XML.SubElement(ComicInfo, "ScanInformation").text = self.info.get("scanInformation") XML.SubElement(ComicInfo, "ScanInformation").text = self.info.get(
"scanInformation"
tree = XML.ElementTree(ComicInfo) )
XML.indent(tree, space="\t", level=0) tree = XML.ElementTree(ComicInfo)
tree.write(str(filename), encoding="utf-8")
XML.indent(tree, space="\t", level=0)
return filename tree.write(str(filename), encoding="utf-8")
@staticmethod return filename
def printMangaInfo(info:dict):
print(info) @staticmethod
def printMangaInfo(info: dict):
def choose_chapters(self, auto): print(info)
choices = [
questionary.Choice( def choose_chapters(self, auto):
f"{chapter.get("nr"):02d}: {chapter.get("title")}", choices = [
chapter, questionary.Choice(
checked = chapter.get("nr") not in self.cached f"{chapter.get('nr'):02d}: {chapter.get('title')}",
) for chapter in self.chapters chapter,
] checked=chapter.get("nr") not in self.cached,
self.chapters = questionary.checkbox( )
"Choose which chapters to download", for chapter in self.chapters
choices ]
).skip_if( self.chapters = (
auto, questionary.checkbox("Choose which chapters to download", choices)
[chapter for chapter in self.chapters if chapter.get("nr") not in self.cached] .skip_if(
).ask() auto,
[
def download(self, save_dir:Path): chapter
with tempfile.TemporaryDirectory(prefix="manga_") as temp_dir: for chapter in self.chapters
if self.chapters: if chapter.get("nr") not in self.cached
with Progress( ],
TextColumn("[progress.description]{task.description}"), )
MofNCompleteColumn(), .ask()
SpinnerColumn() )
) as p:
task = p.add_task(f"[bold]Processing [orange3]{self.info.get("series")}[/]", total=len(self.chapters)) def download(self, save_dir: Path):
while not p.finished: with tempfile.TemporaryDirectory(prefix="manga_") as temp_dir:
for chapter in self.chapters: if self.chapters:
print(f"Downloading [bold blue]Chapter {chapter.get("nr"):02d}[/]...") with Progress(
files = [self._generateComicInfo(chapter, Path(temp_dir))] TextColumn("[progress.description]{task.description}"),
files.extend(download_images(chapter.get("images"), Path(temp_dir))) # type: ignore MofNCompleteColumn(),
zip_files(self.slug, chapter, files, save_dir) SpinnerColumn(),
self.cached.append(chapter.get("nr")) ) as p:
p.update(task, advance=1) task = p.add_task(
else: f"[bold]Processing [orange3]{self.info.get('series')}[/]",
print("No new [orange3]Chapter[/] were found...") total=len(self.chapters),
)
while not p.finished:
for chapter in self.chapters:
print(
f"Downloading [bold blue]Chapter {chapter.get('nr'):02d}[/]..."
)
files = [self._generateComicInfo(chapter, Path(temp_dir))]
files.extend(download_images(chapter.get("images"), Path(temp_dir))) # type: ignore
zip_files(self.slug, chapter, files, save_dir)
self.cached.append(chapter.get("nr"))
p.update(task, advance=1)
else:
print("No new [orange3]Chapter[/] were found...")
+58 -50
View File
@@ -1,50 +1,58 @@
from pathlib import Path import asyncio
import asyncio import zipfile
import aiohttp from pathlib import Path
import aiofiles
from aiohttp import ClientError import aiofiles
import zipfile import aiohttp
from aiohttp import ClientError
async def download_image(session:aiohttp.ClientSession, url:str, temp_dir:Path, retries=3, timeout=10):
filename = temp_dir.joinpath(Path(url).name) async def download_image(
session: aiohttp.ClientSession, url: str, temp_dir: Path, retries=3, timeout=10
for attempt in range(1, retries + 1): ):
try:
async with session.get(url, timeout=timeout) as response: # type: ignore filename = temp_dir.joinpath(Path(url).name)
if response.status == 200:
async with aiofiles.open(filename, "wb") as f: for attempt in range(1, retries + 1):
await f.write(await response.read()) try:
return filename async with session.get(url, timeout=timeout) as response: # type: ignore
else: if response.status == 200:
raise ClientError(f"Bad status {response.status} for {url}") async with aiofiles.open(filename, "wb") as f:
except (asyncio.TimeoutError, ClientError, aiohttp.ClientConnectorError) as e: await f.write(await response.read())
if attempt < retries: return filename
await asyncio.sleep(2 ** attempt) else:
else: raise ClientError(f"Bad status {response.status} for {url}")
print(f"Failed to download {url}: {e}") except (asyncio.TimeoutError, ClientError, aiohttp.ClientConnectorError) as e:
return None if attempt < retries:
await asyncio.sleep(2**attempt)
async def _download_images(urls, temp_dir:Path, concurrency=5): else:
semaphore = asyncio.Semaphore(concurrency) print(f"Failed to download {url}: {e}")
return None
async with aiohttp.ClientSession() as session:
async def bounded_download(url):
async with semaphore: async def _download_images(urls, temp_dir: Path, concurrency=5):
return await download_image(session, url, temp_dir) semaphore = asyncio.Semaphore(concurrency)
tasks = [bounded_download(url) for url in urls] async with aiohttp.ClientSession() as session:
return await asyncio.gather(*tasks)
async def bounded_download(url):
def download_images(images:list[str], temp_dir:Path): async with semaphore:
return asyncio.run(_download_images(images, temp_dir)) return await download_image(session, url, temp_dir)
def zip_files(slug, chapter:dict, files:list[Path], save_dir:Path): tasks = [bounded_download(url) for url in urls]
save_dir.joinpath(slug).mkdir(exist_ok=True) return await asyncio.gather(*tasks)
filename = save_dir.joinpath(slug).joinpath(f"{slug} - Ch. {chapter.get("nr")}.cbz") # type: ignore
with zipfile.ZipFile(filename, "w") as zf:
for file in files: def download_images(images: list[str], temp_dir: Path):
zf.write(file, file.name) return asyncio.run(_download_images(images, temp_dir))
zf.close()
return filename def zip_files(slug, chapter: dict, files: list[Path], save_dir: Path):
save_dir.joinpath(slug).mkdir(exist_ok=True)
filename = save_dir.joinpath(slug).joinpath(f"{slug} - Ch. {chapter.get('nr')}.cbz") # type: ignore
with zipfile.ZipFile(filename, "w") as zf:
for file in files:
zf.write(file, file.name)
zf.close()
return filename
+12 -12
View File
@@ -1,12 +1,12 @@
from .flamecomics import FlameComics from .flamecomics import FlameComics
from .provider import Provider from .provider import Provider
PROVIDERS = [ PROVIDERS = [
FlameComics, FlameComics,
] ]
def parse_provider(session, url) -> Provider | None:
for site in PROVIDERS: def parse_provider(session, url) -> Provider | None:
if site.domain() in url: for site in PROVIDERS:
return site(session, url) if site.domain() in url:
return site(session, url)
+94 -85
View File
@@ -1,85 +1,94 @@
import requests import json
import json import re
from bs4 import BeautifulSoup as bs
import re import iso639
import iso639 import requests
from .provider import Provider from bs4 import BeautifulSoup as bs
CDN_URL = "https://cdn.flamecomics.xyz" from .provider import Provider
IMAGE_URL = CDN_URL + "/uploads/images/series/{id}/{token}/{file}"
INFO_URL = "https://flamecomics.xyz/series/{id}" CDN_URL = "https://cdn.flamecomics.xyz"
IMAGE_URL = CDN_URL + "/uploads/images/series/{id}/{token}/{file}"
class FlameComics(Provider): INFO_URL = "https://flamecomics.xyz/series/{id}"
CHAPTER_URL = "https://flamecomics.xyz/series/{id}/{token}"
def __init__(self, session:requests.Session, url) -> None:
self.s = session
self.id = self.parse_url(url) class FlameComics(Provider):
def parse_url(self, url): def __init__(self, session: requests.Session, url) -> None:
parse = re.search(r"series\/(?P<id>\d+)", url) self.s = session
return parse.group("id") # type: ignore self.id = self.parse_url(url)
@staticmethod def parse_url(self, url):
def parse_info(payload:dict) -> dict: parse = re.search(r"series\/(?P<id>\d+)", url)
return { return parse.group("id") # type: ignore
"series": payload["series"].get("title"),
"writer": payload["series"].get("author"), @staticmethod
"penciller": payload["series"].get("artist"), def parse_info(payload: dict) -> dict:
"genre": payload["series"].get("tags"), return {
"summary": bs(payload["series"].get("description"), "html.parser").get_text(), "series": payload["series"].get("title"),
"languageISO": iso639.Language.from_name(payload["series"].get("language")).part1, "writer": payload["series"].get("author"),
"scanInformation": "Reaper_Scans & Flame Comics", "penciller": payload["series"].get("artist"),
} "genre": payload["series"].get("tags"),
"summary": bs(
def generate_image_urls(self, raw_images:dict[str,dict], token:str) -> list[str]: payload["series"].get("description"), "html.parser"
images = list() ).get_text(),
for raw in raw_images.values(): "languageISO": iso639.Language.from_name(
images.append( payload["series"].get("language")
IMAGE_URL.format(id = self.id, token = token, file = raw.get("name")) ).part1,
) "scanInformation": "Reaper_Scans & Flame Comics",
return images[1:] }
def parse_chapters(self, payload:list[dict]) -> list[dict]: def generate_image_urls(self, raw_images: dict[str, dict], token: str) -> list[str]:
chapters = list() images = list()
for c in payload: for raw in raw_images.values():
nbr = int(float(c.get("chapter", 0.0))) images.append(
title = c.get("title") IMAGE_URL.format(id=self.id, token=token, file=raw.get("name"))
if not title: )
title = f"Chapter {nbr}" return images[1:]
images = self.generate_image_urls(c.get("images"), c.get("token")) # type: ignore
def parse_chapters(self, payload: list[dict]) -> list[dict]:
data = { chapters = list()
"nr": nbr, for c in payload:
"title": title, nbr = int(float(c.get("chapter", 0.0)))
"images": images title = c.get("title")
} if not title:
title = f"Chapter {nbr}"
chapters.append(data) r = self.s.get(CHAPTER_URL.format(id=self.id, token=c.get("token")))
chapter_page = bs(r.content, "html.parser")
return chapters chapter_payload = self.get_page_props(chapter_page).get("chapter")
images = self.generate_image_urls(chapter_payload.get("images"), chapter_payload.get("token")) # type: ignore
def get_info(self) -> tuple[dict, list]:
r = self.s.get(INFO_URL.format(id = self.id)) data = {"nr": nbr, "title": title, "images": images}
series_page = bs(r.content, "html.parser")
payload = json.loads( chapters.append(data)
series_page.find(
"script", return chapters
attrs = {"id": "__NEXT_DATA__"}
).text # type: ignore @staticmethod
).get("props").get("pageProps") def get_page_props(page: bs) -> dict:
return (
info = self.parse_info(payload) json.loads(
chapters = self.parse_chapters(payload["chapters"]) page.find("script", attrs={"id": "__NEXT_DATA__"}).text # type: ignore
return info, chapters )
.get("props")
def get_mediainfo(self) -> tuple[dict, list]: .get("pageProps")
return self.get_info() )
def get_url(self) -> str: def get_info(self) -> tuple[dict, list]:
return INFO_URL.format(id = self.id) r = self.s.get(INFO_URL.format(id=self.id))
series_page = bs(r.content, "html.parser")
@staticmethod payload = self.get_page_props(series_page)
def domain() -> str: info = self.parse_info(payload)
return "flamecomics.xyz" chapters = self.parse_chapters(payload["chapters"])
return info, chapters
def get_mediainfo(self) -> tuple[dict, list]:
return self.get_info()
def get_url(self) -> str:
return INFO_URL.format(id=self.id)
@staticmethod
def domain() -> str:
return "flamecomics.xyz"
+17 -17
View File
@@ -1,17 +1,17 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
class Provider(ABC):
class Provider(ABC):
@staticmethod
@abstractmethod @staticmethod
def domain() -> str: @abstractmethod
pass def domain() -> str:
pass
@abstractmethod
def get_mediainfo(self) -> tuple[dict, list]: @abstractmethod
pass def get_mediainfo(self) -> tuple[dict, list]:
pass
@abstractmethod
def get_url(self) -> str: @abstractmethod
pass def get_url(self) -> str:
pass