Code formatting and updated flamecomic to use changed api
This commit is contained in:
+176
-176
@@ -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
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user