Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ venv/
# [date]full_name(id) report folders
\[*\]*\(*\)/
*.egg-info
mloader_downloads/
mloader_downloads/
build
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Mangaplus Downloader

[![Latest Github release](https://img.shields.io/github/tag/hurlenko/mloader.svg)](https://github.com/hurlenko/mloader/releases/latest)
![Python](https://img.shields.io/badge/python-v3.6+-blue.svg)
![Python](https://img.shields.io/badge/python-v3.12+-blue.svg)
![License](https://img.shields.io/badge/license-GPLv3-blue.svg)

## **mloader** - download manga from mangaplus.shueisha.co.jp

## 🚩 Table of Contents

- [Installation](#-installation)
- [Usage](#-usage)
- [Command line interface](#%EF%B8%8F-command-line-interface)
- [Installation](#-installation)
- [Usage](#-usage)
- [Command line interface](#%EF%B8%8F-command-line-interface)

## 💾 Installation

Expand All @@ -30,7 +30,7 @@ You can use `--title` and `--chapter` command line argument to download by title

You can download individual chapters or full title (but only available chapters).

Chapters can be saved as `CBZ` archives (default) or separate images by passing the `--raw` parameter.
Chapters can be saved in different formats (check the `--help` output for the available formats).

## 🖥️ Command line interface

Expand All @@ -45,20 +45,16 @@ Options:
--version Show the version and exit.
-o, --out <directory> Save directory (not a file) [default:
mloader_downloads]
-r, --raw Save raw images [default: False]
-f, --format [raw|cbz|pdf] Output format [default: cbz]
-q, --quality [super_high|high|low]
Image quality [default: super_high]
-s, --split Split combined images [default: False]
-s, --split Split combined images
-c, --chapter INTEGER Chapter id
-t, --title INTEGER Title id
-b, --begin INTEGER RANGE Minimal chapter to try to download
[default: 0;x>=0]
-e, --end INTEGER RANGE Maximal chapter to try to download [x>=1]
-l, --last Download only the last chapter for title
[default: False]
--chapter-title Include chapter titles in filenames
[default: False]
--chapter-subdir Save raw images in sub directory by chapter
[default: False]
--help Show this message and exit.
```
```
4 changes: 4 additions & 0 deletions mloader/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
APP_VER=97
OS=ios
OS_VER=18.1
SECRET=f40080bcb01a9a963912f46688d411a3
234 changes: 7 additions & 227 deletions mloader/__main__.py
Original file line number Diff line number Diff line change
@@ -1,231 +1,11 @@
import logging
import re
import sys
from functools import partial
from typing import Optional, Set
# mloader/__main__.py

import click

from mloader import __version__ as about
from mloader.exporter import RawExporter, CBZExporter
from mloader.loader import MangaLoader

log = logging.getLogger()


def setup_logging():
for logger in ("requests", "urllib3"):
logging.getLogger(logger).setLevel(logging.WARNING)
handlers = [logging.StreamHandler(sys.stdout)]
logging.basicConfig(
handlers=handlers,
format=(
"{asctime:^} | {levelname: ^8} | "
"{filename: ^14} {lineno: <4} | {message}"
),
style="{",
datefmt="%d.%m.%Y %H:%M:%S",
level=logging.INFO,
)


setup_logging()


def validate_urls(ctx: click.Context, param, value):
if not value:
return value

res = {"viewer": set(), "titles": set()}
for url in value:
match = re.search(r"(\w+)/(\d+)", url)
if not match:
raise click.BadParameter(f"Invalid url: {url}")
try:
res[match.group(1)].add(int(match.group(2)))
except (ValueError, KeyError):
raise click.BadParameter(f"Invalid url: {url}")

ctx.params.setdefault("titles", set()).update(res["titles"])
ctx.params.setdefault("chapters", set()).update(res["viewer"])


def validate_ids(ctx: click.Context, param, value):
if not value:
return value

assert param.name in ("chapter", "title")

ctx.params.setdefault(f"{param.name}s", set()).update(value)


EPILOG = f"""
Examples:

{click.style('• download manga chapter 1 as CBZ archive', fg="green")}

$ mloader https://mangaplus.shueisha.co.jp/viewer/1

{click.style('• download all chapters for manga title 2 and save '
'to current directory', fg="green")}

$ mloader https://mangaplus.shueisha.co.jp/titles/2 -o .

{click.style('• download chapter 1 AND all available chapters from '
'title 2 (can be two different manga) in low quality and save as '
'separate images', fg="green")}

$ mloader https://mangaplus.shueisha.co.jp/viewer/1
https://mangaplus.shueisha.co.jp/titles/2 -r -q low
"""


@click.command(
help=about.__description__,
epilog=EPILOG,
)
@click.version_option(
about.__version__,
prog_name=about.__title__,
message="%(prog)s by Hurlenko, version %(version)s\n"
f"Check {about.__url__} for more info",
)
@click.option(
"--out",
"-o",
"out_dir",
type=click.Path(exists=False, writable=True),
metavar="<directory>",
default="mloader_downloads",
show_default=True,
help="Save directory (not a file)",
envvar="MLOADER_EXTRACT_OUT_DIR",
)
@click.option(
"--raw",
"-r",
is_flag=True,
default=False,
show_default=True,
help="Save raw images",
envvar="MLOADER_RAW",
)
@click.option(
"--quality",
"-q",
default="super_high",
type=click.Choice(["super_high", "high", "low"]),
show_default=True,
help="Image quality",
envvar="MLOADER_QUALITY",
)
@click.option(
"--split",
"-s",
is_flag=True,
default=False,
show_default=True,
help="Split combined images",
envvar="MLOADER_SPLIT",
)
@click.option(
"--chapter",
"-c",
type=click.INT,
multiple=True,
help="Chapter id",
expose_value=False,
callback=validate_ids,
)
@click.option(
"--title",
"-t",
type=click.INT,
multiple=True,
help="Title id",
expose_value=False,
callback=validate_ids,
)
@click.option(
"--begin",
"-b",
type=click.IntRange(min=0),
default=0,
show_default=True,
help="Minimal chapter to try to download",
)
@click.option(
"--end",
"-e",
type=click.IntRange(min=1),
help="Maximal chapter to try to download",
)
@click.option(
"--last",
"-l",
is_flag=True,
default=False,
show_default=True,
help="Download only the last chapter for title",
)
@click.option(
"--chapter-title",
is_flag=True,
default=False,
show_default=True,
help="Include chapter titles in filenames",
)
@click.option(
"--chapter-subdir",
is_flag=True,
default=False,
show_default=True,
help="Save raw images in sub directory by chapter",
)
@click.argument("urls", nargs=-1, callback=validate_urls, expose_value=False)
@click.pass_context
def main(
ctx: click.Context,
out_dir: str,
raw: bool,
quality: str,
split: bool,
begin: int,
end: int,
last: bool,
chapter_title: bool,
chapter_subdir: bool,
chapters: Optional[Set[int]] = None,
titles: Optional[Set[int]] = None,
):
click.echo(click.style(about.__doc__, fg="blue"))
if not any((chapters, titles)):
click.echo(ctx.get_help())
return
end = end or float("inf")
log.info("Started export")

exporter = RawExporter if raw else CBZExporter
exporter = partial(
exporter,
destination=out_dir,
add_chapter_title=chapter_title,
add_chapter_subdir=chapter_subdir,
)

loader = MangaLoader(exporter, quality, split)
try:
loader.download(
title_ids=titles,
chapter_ids=chapters,
min_chapter=begin,
max_chapter=end,
last_chapter=last,
)
except Exception:
log.exception("Failed to download manga")
log.info("SUCCESS")
# Import the logging setup early so that it applies to all loggers.
from mloader.cli.config import setup_logging
setup_logging() # This ensures all logging settings are in place.

# Now import the main CLI command.
from mloader.cli.main import main

if __name__ == "__main__":
main(prog_name=about.__title__)
main()
5 changes: 3 additions & 2 deletions mloader/__version__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
__intro__ = r"""
_ _
_ __ ___ | | ___ __ _ __| | ___ _ __
| '_ ` _ \| |/ _ \ / _` |/ _` |/ _ \ '__|
| | | | | | | (_) | (_| | (_| | __/ |
|_| |_| |_|_|\___/ \__,_|\__,_|\___|_|

"""

__title__ = "mloader"
__description__ = "Command-line tool to download manga from mangaplus"
__url__ = "https://github.com/hurlenko/mloader"
__version__ = "1.1.12"
__version__ = "1.2.0"
__license__ = "GPLv3"
Empty file added mloader/cli/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions mloader/cli/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging
import sys


def setup_logging():
"""
Configure logging for the application.

Sets third-party loggers (e.g., 'requests', 'urllib3') to WARNING and configures the
root logger to output logs to stdout with a custom format.
"""
for logger_name in ("requests", "urllib3"):
logging.getLogger(logger_name).setLevel(logging.WARNING)

stream_handler = logging.StreamHandler(sys.stdout)
logging.basicConfig(
handlers=[stream_handler],
format=(
"{asctime:^} | {levelname: ^8} | {filename: ^14} {lineno: <4} | {message}"
),
style="{",
datefmt="%d.%m.%Y %H:%M:%S",
level=logging.INFO,
)


# Immediately configure logging upon module import.
setup_logging()


def get_logger(name: str = None) -> logging.Logger:
"""
Retrieve a logger instance with the given name.

Parameters:
name (str, optional): The name of the logger. Defaults to None for the root logger.

Returns:
logging.Logger: The configured logger.
"""
return logging.getLogger(name)
3 changes: 3 additions & 0 deletions mloader/cli/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .main import main

__all__ = ["main"]
Loading