mirror of
https://github.com/RicterZ/nhentai.git
synced 2026-04-08 10:40:22 +02:00
Initial commit: doujinshi-dl generic plugin framework
History reset as part of DMCA compliance. The project has been refactored into a generic, site-agnostic download framework. Site-specific logic lives in separate plugin packages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
venv
|
||||||
|
*.egg-info
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
images
|
||||||
|
LICENSE
|
||||||
|
.travis.yml
|
||||||
|
.idea
|
||||||
27
.github/workflows/docker-image.yml
vendored
Normal file
27
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Docker Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build the Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ricterz/doujinshi-dl:latest
|
||||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
*.py[cod]
|
||||||
|
.idea/
|
||||||
|
build
|
||||||
|
dist/
|
||||||
|
*.egg-info
|
||||||
|
.python-version
|
||||||
|
.DS_Store
|
||||||
|
output/
|
||||||
|
venv/
|
||||||
|
.vscode/
|
||||||
|
test-output
|
||||||
|
*.whl
|
||||||
80
CLAUDE.md
Normal file
80
CLAUDE.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
`doujinshi-dl` is a Python CLI tool for downloading doujinshi from mirror sites. Entry point: `nhentai.command:main`. PyPI distribution name: `doujinshi-dl`, Python package name: `nhentai`.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install in editable mode
|
||||||
|
pip install --no-cache-dir .
|
||||||
|
|
||||||
|
# With nhentai plugin
|
||||||
|
pip install -e . -e ../doujinshi-dl-nhentai/
|
||||||
|
|
||||||
|
# Or with Poetry
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
doujinshi-dl --id <ID>
|
||||||
|
doujinshi-dl --search "keyword" [--download]
|
||||||
|
doujinshi-dl --favorites --download
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
All tests are integration tests that make real HTTP requests and require environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DDL_COOKIE="<cookie>"
|
||||||
|
export DDL_UA="<user-agent>"
|
||||||
|
export DOUJINSHI_DL_URL="<mirror-url>"
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
python -m unittest discover tests/
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
python -m unittest tests.test_parser
|
||||||
|
|
||||||
|
# Run a single test case
|
||||||
|
python -m unittest tests.test_parser.TestParser.test_search
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The pipeline flows through these modules in sequence:
|
||||||
|
|
||||||
|
1. **`cmdline.py`** — Parses CLI arguments; loads/saves config from `~/.doujinshi-dl/config.json`
|
||||||
|
2. **`parser.py`** — Scrapes mirror site via BeautifulSoup and HTTP; functions: `doujinshi_parser`, `search_parser`, `favorites_parser`
|
||||||
|
3. **`doujinshi.py`** — `Doujinshi` model holding metadata and building the download queue; folder-name format tokens: `%i` (ID), `%t` (title), `%a` (artist), etc.
|
||||||
|
4. **`downloader.py`** — Async image downloading via `httpx` + `asyncio`; `Downloader` writes files directly, `CompressedDownloader` writes `.zip`
|
||||||
|
5. **`utils.py`** — HTTP helpers (`request()`, `async_request()`), HTML generation, PDF/CBZ creation, SQLite history DB
|
||||||
|
6. **`serializer.py`** — Writes `metadata.json`, `ComicInfo.xml`, `info.txt` alongside downloaded images
|
||||||
|
7. **`command.py`** — Orchestrates the full pipeline; the `main()` entry point
|
||||||
|
|
||||||
|
## Key Constants & Environment Variables
|
||||||
|
|
||||||
|
All URLs, paths, and defaults live in **`constant.py`** (plugin package).
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `DOUJINSHI_DL_URL` | Mirror base URL (required to run) |
|
||||||
|
| `DEBUG` | Enable debug logging; `DEBUG=NODOWNLOAD` skips actual downloads |
|
||||||
|
| `DDL_COOKIE` | Cookie for authentication (also used by tests) |
|
||||||
|
| `DDL_UA` | User-agent string (also used by tests) |
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
- **Config:** `~/.doujinshi-dl/config.json` (cookie, user-agent, proxy, language, template)
|
||||||
|
- **History DB:** `~/.doujinshi-dl/history.sqlite3` (SQLite, tracks downloaded IDs to avoid re-downloading)
|
||||||
|
- On Linux, `$XDG_DATA_HOME` is respected for these paths
|
||||||
|
|
||||||
|
## Viewer Templates
|
||||||
|
|
||||||
|
`doujinshi_dl/viewer/` contains bundled HTML viewer templates (`default/`, `minimal/`) used by `generate_html()` in `utils.py` to produce a local browsing interface after download.
|
||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM python:3
|
||||||
|
|
||||||
|
WORKDIR /usr/src/doujinshi-dl
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
|
WORKDIR /output
|
||||||
|
ENTRYPOINT ["doujinshi-dl"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Ricter Zheng
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
229
README.rst
Normal file
229
README.rst
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
doujinshi-dl
|
||||||
|
============
|
||||||
|
|
||||||
|
あなたも変態。 いいね?
|
||||||
|
|
||||||
|
|pypi| |version| |license|
|
||||||
|
|
||||||
|
|
||||||
|
doujinshi-dl is a CLI tool for downloading doujinshi from mirror sites.
|
||||||
|
|
||||||
|
===================
|
||||||
|
Manual Installation
|
||||||
|
===================
|
||||||
|
From Github:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
git clone https://github.com/RicterZ/doujinshi-dl
|
||||||
|
cd doujinshi-dl
|
||||||
|
pip install --no-cache-dir .
|
||||||
|
|
||||||
|
Build Docker container:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
git clone https://github.com/RicterZ/doujinshi-dl
|
||||||
|
cd doujinshi-dl
|
||||||
|
docker build -t doujinshi-dl:latest .
|
||||||
|
docker run --rm -it -v ~/Downloads/doujinshi:/output doujinshi-dl --id 123855
|
||||||
|
|
||||||
|
==================
|
||||||
|
Installation
|
||||||
|
==================
|
||||||
|
From PyPI with pip:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
pip install doujinshi-dl
|
||||||
|
|
||||||
|
Install a plugin to connect to a mirror site:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
pip install doujinshi-dl-<plugin-name>
|
||||||
|
|
||||||
|
For a self-contained installation, use `pipx <https://github.com/pipxproject/pipx/>`_:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
pipx install doujinshi-dl
|
||||||
|
|
||||||
|
=====
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
**⚠️IMPORTANT⚠️**: To bypass Cloudflare, you should use ``--cookie`` and ``--useragent`` options to store your cookie and user-agent.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --useragent "USER AGENT of YOUR BROWSER"
|
||||||
|
doujinshi-dl --cookie "YOUR COOKIE"
|
||||||
|
|
||||||
|
**NOTE:**
|
||||||
|
|
||||||
|
- The format of the cookie is ``"csrftoken=TOKEN; sessionid=ID; cf_clearance=CLOUDFLARE"``
|
||||||
|
- ``cf_clearance`` cookie and useragent must be set if you encounter "blocked by cloudflare captcha" error. Make sure you use the same IP and useragent as when you got it
|
||||||
|
|
||||||
|
| To get csrftoken and sessionid, first login to your account in a web browser, then:
|
||||||
|
| (Chrome) |ve| |ld| More tools |ld| Developer tools |ld| Application |ld| Storage |ld| Cookies |ld| your mirror URL
|
||||||
|
| (Firefox) |hv| |ld| Web Developer |ld| Web Developer Tools |ld| Storage |ld| Cookies |ld| your mirror URL
|
||||||
|
|
|
||||||
|
|
||||||
|
.. |hv| unicode:: U+2630 .. https://www.compart.com/en/unicode/U+2630
|
||||||
|
.. |ve| unicode:: U+22EE .. https://www.compart.com/en/unicode/U+22EE
|
||||||
|
.. |ld| unicode:: U+2014 .. https://www.compart.com/en/unicode/U+2014
|
||||||
|
|
||||||
|
*The default download folder will be the path where you run the command (%cd% or $PWD).*
|
||||||
|
|
||||||
|
Download specified doujinshi:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --id 123855 123866 123877
|
||||||
|
|
||||||
|
Download doujinshi with ids specified in a file (doujinshi ids split by line):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --file=doujinshi.txt
|
||||||
|
|
||||||
|
Set search default language:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --language=english
|
||||||
|
|
||||||
|
Search a keyword and download the first page:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --search="tomori" --page=1 --download
|
||||||
|
# you also can download by tags and multiple keywords
|
||||||
|
doujinshi-dl --search="tag:lolicon, artist:henreader, tag:full color"
|
||||||
|
doujinshi-dl --search="lolicon, henreader, full color"
|
||||||
|
|
||||||
|
Download your favorites with delay:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --favorites --download --delay 1 --page 3-5,7
|
||||||
|
|
||||||
|
Format output doujinshi folder name:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doujinshi-dl --id 261100 --format '[%i]%s'
|
||||||
|
# for Windows
|
||||||
|
doujinshi-dl --id 261100 --format "[%%i]%%s"
|
||||||
|
|
||||||
|
Supported doujinshi folder formatter:
|
||||||
|
|
||||||
|
- %i: Doujinshi id
|
||||||
|
- %f: Doujinshi favorite count
|
||||||
|
- %t: Doujinshi name
|
||||||
|
- %s: Doujinshi subtitle (translated name)
|
||||||
|
- %a: Doujinshi authors' name
|
||||||
|
- %g: Doujinshi groups name
|
||||||
|
- %p: Doujinshi pretty name
|
||||||
|
- %ag: Doujinshi authors name or groups name
|
||||||
|
|
||||||
|
Note: for Windows operation system, please use double "%", such as "%%i".
|
||||||
|
|
||||||
|
Other options:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
doujinshi-dl --search [keyword] --download
|
||||||
|
DOUJINSHI_DL_URL=https://mirror-url/ doujinshi-dl --id [ID ...]
|
||||||
|
doujinshi-dl --file [filename]
|
||||||
|
|
||||||
|
Environment Variable:
|
||||||
|
DOUJINSHI_DL_URL mirror url
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--download, -D download doujinshi (for search results)
|
||||||
|
--no-download skip downloading (for search results)
|
||||||
|
--show, -S just show the doujinshi information
|
||||||
|
--id ID [ID ...] doujinshi ids set, e.g. 167680 167681 167682
|
||||||
|
--search, -s KEYWORD search doujinshi by keyword
|
||||||
|
--favorites, -F list or download your favorites
|
||||||
|
--artist, -a ARTIST list doujinshi by artist name
|
||||||
|
--page-all all search results
|
||||||
|
--page, --page-range PAGE
|
||||||
|
page number of search results. e.g. 1,2-5,14
|
||||||
|
--sorting, --sort {recent,popular,popular-today,popular-week,date}
|
||||||
|
sorting of doujinshi (recent / popular / popular-[today|week])
|
||||||
|
--output, -o OUTPUT_DIR
|
||||||
|
output dir
|
||||||
|
--threads, -t THREADS
|
||||||
|
thread count for downloading doujinshi
|
||||||
|
--timeout, -T TIMEOUT
|
||||||
|
timeout for downloading doujinshi
|
||||||
|
--delay, -d DELAY slow down between downloading every doujinshi
|
||||||
|
--retry RETRY retry times when downloading failed
|
||||||
|
--exit-on-fail exit on fail to prevent generating incomplete files
|
||||||
|
--proxy PROXY store a proxy, for example: -p "http://127.0.0.1:1080"
|
||||||
|
--file, -f FILE read gallery IDs from file.
|
||||||
|
--format NAME_FORMAT format the saved folder name
|
||||||
|
--no-filename-padding
|
||||||
|
no padding in the images filename, such as '001.jpg'
|
||||||
|
--html [HTML_VIEWER] generate an HTML viewer in the specified directory, or
|
||||||
|
scan all subfolders within the entire directory to
|
||||||
|
generate the HTML viewer. By default, current working
|
||||||
|
directory is used.
|
||||||
|
--no-html don't generate HTML after downloading
|
||||||
|
--gen-main generate a main viewer contain all the doujin in the folder
|
||||||
|
--cbz, -C generate Comic Book CBZ File
|
||||||
|
--pdf, -P generate PDF file
|
||||||
|
--meta generate a metadata file in doujinshi format
|
||||||
|
--update-meta update the metadata file of a doujinshi, update CBZ
|
||||||
|
metadata if exists
|
||||||
|
--rm-origin-dir remove downloaded doujinshi dir when generated CBZ or PDF file
|
||||||
|
--move-to-folder remove files in doujinshi dir then move new file to
|
||||||
|
folder when generated CBZ or PDF file
|
||||||
|
--regenerate regenerate the cbz or pdf file if exists
|
||||||
|
--zip package into a single zip file
|
||||||
|
--cookie COOKIE set cookie to bypass Cloudflare captcha
|
||||||
|
--useragent, --user-agent USERAGENT
|
||||||
|
set useragent to bypass Cloudflare captcha
|
||||||
|
--language LANGUAGE set default language to parse doujinshis
|
||||||
|
--clean-language set DEFAULT as language to parse doujinshis
|
||||||
|
--save-download-history
|
||||||
|
save downloaded doujinshis, whose will be skipped if
|
||||||
|
you re-download them
|
||||||
|
--clean-download-history
|
||||||
|
clean download history
|
||||||
|
--template VIEWER_TEMPLATE
|
||||||
|
set viewer template
|
||||||
|
--legacy use legacy searching method
|
||||||
|
|
||||||
|
======
|
||||||
|
Mirror
|
||||||
|
======
|
||||||
|
To use a mirror, set the ``DOUJINSHI_DL_URL`` environment variable to your mirror's base URL.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
DOUJINSHI_DL_URL=https://your-mirror.example.com doujinshi-dl --id 123456
|
||||||
|
|
||||||
|
.. image:: https://github.com/RicterZ/doujinshi-dl/raw/master/images/search.png
|
||||||
|
:alt: search
|
||||||
|
:align: center
|
||||||
|
.. image:: https://github.com/RicterZ/doujinshi-dl/raw/master/images/download.png
|
||||||
|
:alt: download
|
||||||
|
:align: center
|
||||||
|
.. image:: https://github.com/RicterZ/doujinshi-dl/raw/master/images/viewer.png
|
||||||
|
:alt: viewer
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
|
||||||
|
.. |license| image:: https://img.shields.io/github/license/ricterz/nhentai.svg
|
||||||
|
:target: https://github.com/RicterZ/nhentai/blob/master/LICENSE
|
||||||
|
|
||||||
|
.. |pypi| image:: https://img.shields.io/pypi/v/doujinshi-dl.svg
|
||||||
|
:target: https://pypi.org/project/doujinshi-dl/
|
||||||
|
|
||||||
|
.. |version| image:: https://img.shields.io/badge/python-3.8%2B-blue.svg
|
||||||
|
:target: https://pypi.org/project/doujinshi-dl/
|
||||||
3
doujinshi_dl/__init__.py
Normal file
3
doujinshi_dl/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__version__ = '2.0.5'
|
||||||
|
__author__ = 'RicterZ'
|
||||||
|
__email__ = 'ricterzheng@gmail.com'
|
||||||
290
doujinshi_dl/cmdline.py
Normal file
290
doujinshi_dl/cmdline.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from doujinshi_dl import __version__
|
||||||
|
from doujinshi_dl.utils import generate_html, generate_main_html, DB, EXTENSIONS
|
||||||
|
from doujinshi_dl.logger import logger
|
||||||
|
from doujinshi_dl.constant import PATH_SEPARATOR
|
||||||
|
|
||||||
|
|
||||||
|
_plugin_const_cache = None
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_const():
|
||||||
|
"""Return the active plugin's constant module without hard-coding plugin names."""
|
||||||
|
global _plugin_const_cache
|
||||||
|
if _plugin_const_cache is not None:
|
||||||
|
return _plugin_const_cache
|
||||||
|
from doujinshi_dl.core.registry import get_first_plugin
|
||||||
|
import importlib
|
||||||
|
plugin = get_first_plugin()
|
||||||
|
pkg = type(plugin).__module__.split('.')[0] # top-level package name of the plugin
|
||||||
|
_plugin_const_cache = importlib.import_module(f'{pkg}.constant')
|
||||||
|
return _plugin_const_cache
|
||||||
|
|
||||||
|
|
||||||
|
def banner():
|
||||||
|
logger.debug(f'doujinshi-dl ver {__version__}: あなたも変態。 いいね?')
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
c = _plugin_const()
|
||||||
|
if not os.path.exists(c.PLUGIN_CONFIG_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(c.PLUGIN_CONFIG_FILE, 'r') as f:
|
||||||
|
c.CONFIG.update(json.load(f))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error('Failed to load config file.')
|
||||||
|
write_config()
|
||||||
|
|
||||||
|
|
||||||
|
def write_config():
|
||||||
|
c = _plugin_const()
|
||||||
|
if not os.path.exists(c.PLUGIN_HOME):
|
||||||
|
os.mkdir(c.PLUGIN_HOME)
|
||||||
|
|
||||||
|
with open(c.PLUGIN_CONFIG_FILE, 'w') as f:
|
||||||
|
f.write(json.dumps(c.CONFIG))
|
||||||
|
|
||||||
|
|
||||||
|
def callback(option, _opt_str, _value, parser):
|
||||||
|
if option == '--id':
|
||||||
|
pass
|
||||||
|
value = []
|
||||||
|
|
||||||
|
for arg in parser.rargs:
|
||||||
|
if arg.isdigit():
|
||||||
|
value.append(int(arg))
|
||||||
|
elif arg.startswith('-'):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.warning(f'Ignore invalid id {arg}')
|
||||||
|
|
||||||
|
setattr(parser.values, option.dest, value)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_parser():
|
||||||
|
load_config()
|
||||||
|
c = _plugin_const()
|
||||||
|
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description='\n doujinshi-dl --search [keyword] --download'
|
||||||
|
'\n DOUJINSHI_DL_URL=https://mirror-url/ doujinshi-dl --id [ID ...]'
|
||||||
|
'\n doujinshi-dl --file [filename]'
|
||||||
|
'\n\nEnvironment Variable:\n'
|
||||||
|
' DOUJINSHI_DL_URL mirror url'
|
||||||
|
)
|
||||||
|
|
||||||
|
# operation options
|
||||||
|
parser.add_argument('--download', '-D', dest='is_download', action='store_true',
|
||||||
|
help='download doujinshi (for search results)')
|
||||||
|
parser.add_argument('--no-download', dest='no_download', action='store_true', default=False,
|
||||||
|
help='download doujinshi (for search results)')
|
||||||
|
parser.add_argument('--show', '-S', dest='is_show', action='store_true',
|
||||||
|
help='just show the doujinshi information')
|
||||||
|
|
||||||
|
# doujinshi options
|
||||||
|
parser.add_argument('--id', dest='id', nargs='+', type=int,
|
||||||
|
help='doujinshi ids set, e.g. 167680 167681 167682')
|
||||||
|
parser.add_argument('--search', '-s', type=str, dest='keyword',
|
||||||
|
help='search doujinshi by keyword')
|
||||||
|
parser.add_argument('--favorites', '-F', action='store_true', dest='favorites',
|
||||||
|
help='list or download your favorites')
|
||||||
|
parser.add_argument('--artist', '-a', type=str, dest='artist',
|
||||||
|
help='list doujinshi by artist name')
|
||||||
|
|
||||||
|
# page options
|
||||||
|
parser.add_argument('--page-all', dest='page_all', action='store_true', default=False,
|
||||||
|
help='all search results')
|
||||||
|
parser.add_argument('--page', '--page-range', type=str, dest='page',
|
||||||
|
help='page number of search results. e.g. 1,2-5,14')
|
||||||
|
parser.add_argument('--sorting', '--sort', dest='sorting', type=str, default='popular',
|
||||||
|
help='sorting of doujinshi (recent / popular / popular-[today|week])',
|
||||||
|
choices=['recent', 'popular', 'popular-today', 'popular-week', 'date'])
|
||||||
|
|
||||||
|
# download options
|
||||||
|
parser.add_argument('--output', '-o', type=str, dest='output_dir', default='.',
|
||||||
|
help='output dir')
|
||||||
|
parser.add_argument('--threads', '-t', type=int, dest='threads', default=5,
|
||||||
|
help='thread count for downloading doujinshi')
|
||||||
|
parser.add_argument('--timeout', '-T', type=int, dest='timeout', default=30,
|
||||||
|
help='timeout for downloading doujinshi')
|
||||||
|
parser.add_argument('--delay', '-d', type=int, dest='delay', default=0,
|
||||||
|
help='slow down between downloading every doujinshi')
|
||||||
|
parser.add_argument('--retry', type=int, dest='retry', default=3,
|
||||||
|
help='retry times when downloading failed')
|
||||||
|
parser.add_argument('--exit-on-fail', dest='exit_on_fail', action='store_true', default=False,
|
||||||
|
help='exit on fail to prevent generating incomplete files')
|
||||||
|
parser.add_argument('--proxy', type=str, dest='proxy',
|
||||||
|
help='store a proxy, for example: -p "http://127.0.0.1:1080"')
|
||||||
|
parser.add_argument('--file', '-f', type=str, dest='file',
|
||||||
|
help='read gallery IDs from file.')
|
||||||
|
parser.add_argument('--format', type=str, dest='name_format', default='[%i][%a][%t]',
|
||||||
|
help='format the saved folder name')
|
||||||
|
|
||||||
|
parser.add_argument('--no-filename-padding', action='store_true', dest='no_filename_padding',
|
||||||
|
default=False, help='no padding in the images filename, such as \'001.jpg\'')
|
||||||
|
|
||||||
|
# generate options
|
||||||
|
parser.add_argument('--html', dest='html_viewer', type=str, nargs='?', const='.',
|
||||||
|
help='generate an HTML viewer in the specified directory, or scan all subfolders '
|
||||||
|
'within the entire directory to generate the HTML viewer. By default, current '
|
||||||
|
'working directory is used.')
|
||||||
|
parser.add_argument('--no-html', dest='is_nohtml', action='store_true',
|
||||||
|
help='don\'t generate HTML after downloading')
|
||||||
|
parser.add_argument('--gen-main', dest='main_viewer', action='store_true',
|
||||||
|
help='generate a main viewer contain all the doujin in the folder')
|
||||||
|
parser.add_argument('--cbz', '-C', dest='is_cbz', action='store_true',
|
||||||
|
help='generate Comic Book CBZ File')
|
||||||
|
parser.add_argument('--pdf', '-P', dest='is_pdf', action='store_true',
|
||||||
|
help='generate PDF file')
|
||||||
|
|
||||||
|
parser.add_argument('--meta', dest='generate_metadata', action='store_true', default=False,
|
||||||
|
help='generate a metadata file in doujinshi format')
|
||||||
|
parser.add_argument('--update-meta', dest='update_metadata', action='store_true', default=False,
|
||||||
|
help='update the metadata file of a doujinshi, update CBZ metadata if exists')
|
||||||
|
|
||||||
|
parser.add_argument('--rm-origin-dir', dest='rm_origin_dir', action='store_true', default=False,
|
||||||
|
help='remove downloaded doujinshi dir when generated CBZ or PDF file')
|
||||||
|
parser.add_argument('--move-to-folder', dest='move_to_folder', action='store_true', default=False,
|
||||||
|
help='remove files in doujinshi dir then move new file to folder when generated CBZ or PDF file')
|
||||||
|
|
||||||
|
parser.add_argument('--regenerate', dest='regenerate', action='store_true', default=False,
|
||||||
|
help='regenerate the cbz or pdf file if exists')
|
||||||
|
parser.add_argument('--zip', action='store_true', help='Package into a single zip file')
|
||||||
|
|
||||||
|
# site options
|
||||||
|
parser.add_argument('--cookie', type=str, dest='cookie',
|
||||||
|
help='set cookie to bypass Cloudflare captcha')
|
||||||
|
parser.add_argument('--useragent', '--user-agent', type=str, dest='useragent',
|
||||||
|
help='set useragent to bypass Cloudflare captcha')
|
||||||
|
parser.add_argument('--language', type=str, dest='language',
|
||||||
|
help='set default language to parse doujinshis')
|
||||||
|
parser.add_argument('--clean-language', dest='clean_language', action='store_true', default=False,
|
||||||
|
help='set DEFAULT as language to parse doujinshis')
|
||||||
|
parser.add_argument('--save-download-history', dest='is_save_download_history', action='store_true',
|
||||||
|
default=False, help='save downloaded doujinshis, whose will be skipped if you re-download them')
|
||||||
|
parser.add_argument('--clean-download-history', action='store_true', default=False, dest='clean_download_history',
|
||||||
|
help='clean download history')
|
||||||
|
parser.add_argument('--template', dest='viewer_template', type=str, default='',
|
||||||
|
help='set viewer template')
|
||||||
|
parser.add_argument('--legacy', dest='legacy', action='store_true', default=False,
|
||||||
|
help='use legacy searching method')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.html_viewer:
|
||||||
|
if not os.path.exists(args.html_viewer):
|
||||||
|
logger.error(f'Path \'{args.html_viewer}\' not exists')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(args.html_viewer):
|
||||||
|
if not dirs:
|
||||||
|
generate_html(output_dir=args.html_viewer, template=c.CONFIG['template'])
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
for dir_name in dirs:
|
||||||
|
# it will scan the entire subdirectories
|
||||||
|
doujinshi_dir = os.path.join(root, dir_name)
|
||||||
|
items = set(map(lambda s: os.path.splitext(s)[1], os.listdir(doujinshi_dir)))
|
||||||
|
|
||||||
|
# skip directory without any images
|
||||||
|
if items & set(EXTENSIONS):
|
||||||
|
generate_html(output_dir=doujinshi_dir, template=c.CONFIG['template'])
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.main_viewer and not args.id and not args.keyword and not args.favorites:
|
||||||
|
generate_main_html()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.clean_download_history:
|
||||||
|
with DB() as db:
|
||||||
|
db.clean_all()
|
||||||
|
|
||||||
|
logger.info('Download history cleaned.')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# --- set config ---
|
||||||
|
if args.cookie is not None:
|
||||||
|
c.CONFIG['cookie'] = args.cookie.strip()
|
||||||
|
write_config()
|
||||||
|
logger.info('Cookie saved.')
|
||||||
|
|
||||||
|
if args.useragent is not None:
|
||||||
|
c.CONFIG['useragent'] = args.useragent.strip()
|
||||||
|
write_config()
|
||||||
|
logger.info('User-Agent saved.')
|
||||||
|
|
||||||
|
if args.language is not None:
|
||||||
|
c.CONFIG['language'] = args.language
|
||||||
|
write_config()
|
||||||
|
logger.info(f'Default language now set to "{args.language}"')
|
||||||
|
# TODO: search without language
|
||||||
|
|
||||||
|
if any([args.cookie, args.useragent, args.language]):
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.proxy is not None:
|
||||||
|
proxy_url = urlparse(args.proxy)
|
||||||
|
if not args.proxy == '' and proxy_url.scheme not in ('http', 'https', 'socks5', 'socks5h',
|
||||||
|
'socks4', 'socks4a'):
|
||||||
|
logger.error(f'Invalid protocol "{proxy_url.scheme}" of proxy, ignored')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
c.CONFIG['proxy'] = args.proxy
|
||||||
|
logger.info(f'Proxy now set to "{args.proxy}"')
|
||||||
|
write_config()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if args.viewer_template is not None:
|
||||||
|
if not args.viewer_template:
|
||||||
|
args.viewer_template = 'default'
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.join(os.path.dirname(__file__),
|
||||||
|
f'viewer/{args.viewer_template}/index.html')):
|
||||||
|
logger.error(f'Template "{args.viewer_template}" does not exists')
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
c.CONFIG['template'] = args.viewer_template
|
||||||
|
write_config()
|
||||||
|
|
||||||
|
# --- end set config ---
|
||||||
|
|
||||||
|
if args.favorites:
|
||||||
|
if not c.CONFIG['cookie']:
|
||||||
|
logger.warning('Cookie has not been set, please use `doujinshi-dl --cookie \'COOKIE\'` to set it.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.file:
|
||||||
|
with open(args.file, 'r') as f:
|
||||||
|
_ = [i.strip() for i in f.readlines()]
|
||||||
|
args.id = set(int(i) for i in _ if i.isdigit())
|
||||||
|
|
||||||
|
if (args.is_download or args.is_show) and not args.id and not args.keyword and not args.favorites and not args.artist:
|
||||||
|
logger.critical('Doujinshi id(s) are required for downloading')
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not args.keyword and not args.id and not args.favorites and not args.artist:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.threads <= 0:
|
||||||
|
args.threads = 1
|
||||||
|
|
||||||
|
elif args.threads > 15:
|
||||||
|
logger.critical('Maximum number of used threads is 15')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return args
|
||||||
197
doujinshi_dl/command.py
Normal file
197
doujinshi_dl/command.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import platform
|
||||||
|
import urllib3.exceptions
|
||||||
|
|
||||||
|
from doujinshi_dl.cmdline import cmd_parser, banner, write_config
|
||||||
|
from doujinshi_dl.core.registry import get_first_plugin
|
||||||
|
from doujinshi_dl.core import config as core_config
|
||||||
|
from doujinshi_dl.downloader import Downloader, CompressedDownloader
|
||||||
|
from doujinshi_dl.logger import logger
|
||||||
|
from doujinshi_dl.utils import (
|
||||||
|
generate_html, generate_doc, generate_main_html,
|
||||||
|
paging, signal_handler, DB, move_to_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
banner()
|
||||||
|
|
||||||
|
if sys.version_info < (3, 0, 0):
|
||||||
|
logger.error('doujinshi-dl requires Python 3.x')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
plugin = get_first_plugin()
|
||||||
|
parser = plugin.create_parser()
|
||||||
|
serializer = plugin.create_serializer()
|
||||||
|
|
||||||
|
options = cmd_parser()
|
||||||
|
|
||||||
|
# Let the plugin configure its own CONFIG and register runtime values
|
||||||
|
# (this also sets core_config 'base_url' from env or plugin default)
|
||||||
|
plugin.configure(options)
|
||||||
|
|
||||||
|
# Read common config values registered by the plugin
|
||||||
|
plugin_config = core_config.get('plugin_config', {})
|
||||||
|
base_url = core_config.get('base_url', os.getenv('DOUJINSHI_DL_URL', ''))
|
||||||
|
if not base_url:
|
||||||
|
logger.error('No target URL configured. Set DOUJINSHI_DL_URL or install a plugin that provides a default URL.')
|
||||||
|
sys.exit(1)
|
||||||
|
logger.info(f'Using mirror: {base_url}')
|
||||||
|
|
||||||
|
# CONFIG['proxy'] may have been updated after cmd_parser()
|
||||||
|
proxy = plugin_config.get('proxy', '')
|
||||||
|
if proxy:
|
||||||
|
if isinstance(proxy, dict):
|
||||||
|
proxy = proxy.get('http', '')
|
||||||
|
plugin_config['proxy'] = proxy
|
||||||
|
logger.warning(f'Update proxy config to: {proxy}')
|
||||||
|
write_config()
|
||||||
|
logger.info(f'Using proxy: {proxy}')
|
||||||
|
|
||||||
|
if not plugin_config.get('template'):
|
||||||
|
plugin_config['template'] = 'default'
|
||||||
|
|
||||||
|
template = plugin_config.get('template', 'default')
|
||||||
|
language = plugin_config.get('language', '')
|
||||||
|
logger.info(f'Using viewer template "{template}"')
|
||||||
|
|
||||||
|
# Check authentication
|
||||||
|
plugin.check_auth()
|
||||||
|
|
||||||
|
doujinshis = []
|
||||||
|
doujinshi_ids = []
|
||||||
|
|
||||||
|
page_list = paging(options.page)
|
||||||
|
|
||||||
|
if options.favorites:
|
||||||
|
if not options.is_download:
|
||||||
|
logger.warning('You do not specify --download option')
|
||||||
|
|
||||||
|
doujinshis = parser.favorites(page=page_list) if options.page else parser.favorites()
|
||||||
|
|
||||||
|
elif options.keyword:
|
||||||
|
if language:
|
||||||
|
logger.info(f'Using default language: {language}')
|
||||||
|
options.keyword += f' language:{language}'
|
||||||
|
|
||||||
|
doujinshis = parser.search(
|
||||||
|
options.keyword,
|
||||||
|
sorting=options.sorting,
|
||||||
|
page=page_list,
|
||||||
|
legacy=options.legacy,
|
||||||
|
is_page_all=options.page_all,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif options.artist:
|
||||||
|
doujinshis = parser.search(
|
||||||
|
options.artist,
|
||||||
|
sorting=options.sorting,
|
||||||
|
page=page_list,
|
||||||
|
is_page_all=options.page_all,
|
||||||
|
type_='ARTIST',
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not doujinshi_ids:
|
||||||
|
doujinshi_ids = options.id
|
||||||
|
|
||||||
|
plugin.print_results(doujinshis)
|
||||||
|
if options.is_download and doujinshis:
|
||||||
|
doujinshi_ids = [i['id'] for i in doujinshis]
|
||||||
|
|
||||||
|
if options.is_save_download_history:
|
||||||
|
with DB() as db:
|
||||||
|
data = set(map(int, db.get_all()))
|
||||||
|
|
||||||
|
doujinshi_ids = list(set(map(int, doujinshi_ids)) - set(data))
|
||||||
|
logger.info(f'New doujinshis account: {len(doujinshi_ids)}')
|
||||||
|
|
||||||
|
if options.zip:
|
||||||
|
options.is_nohtml = True
|
||||||
|
|
||||||
|
if not options.is_show:
|
||||||
|
downloader = (CompressedDownloader if options.zip else Downloader)(
|
||||||
|
path=options.output_dir,
|
||||||
|
threads=options.threads,
|
||||||
|
timeout=options.timeout,
|
||||||
|
delay=options.delay,
|
||||||
|
exit_on_fail=options.exit_on_fail,
|
||||||
|
no_filename_padding=options.no_filename_padding,
|
||||||
|
)
|
||||||
|
|
||||||
|
for doujinshi_id in doujinshi_ids:
|
||||||
|
meta = parser.fetch(str(doujinshi_id))
|
||||||
|
if not meta:
|
||||||
|
continue
|
||||||
|
|
||||||
|
doujinshi_model = plugin.create_model(meta, name_format=options.name_format)
|
||||||
|
doujinshi = doujinshi_model.doujinshi
|
||||||
|
doujinshi.downloader = downloader
|
||||||
|
|
||||||
|
if doujinshi.check_if_need_download(options):
|
||||||
|
doujinshi.download()
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f'Skip download doujinshi because a PDF/CBZ file exists of doujinshi {doujinshi.name}'
|
||||||
|
)
|
||||||
|
|
||||||
|
doujinshi_dir = os.path.join(options.output_dir, doujinshi.filename)
|
||||||
|
|
||||||
|
if options.generate_metadata:
|
||||||
|
serializer.write_all(meta, doujinshi_dir)
|
||||||
|
logger.log(16, f'Metadata files have been written to "{doujinshi_dir}"')
|
||||||
|
|
||||||
|
if options.is_save_download_history:
|
||||||
|
with DB() as db:
|
||||||
|
db.add_one(doujinshi.id)
|
||||||
|
|
||||||
|
if not options.is_nohtml:
|
||||||
|
generate_html(options.output_dir, doujinshi, template=template)
|
||||||
|
|
||||||
|
if options.is_cbz:
|
||||||
|
# Write ComicInfo.xml metadata before packaging
|
||||||
|
serializer.write_all(meta, doujinshi_dir)
|
||||||
|
generate_doc('cbz', options.output_dir, doujinshi, options.regenerate)
|
||||||
|
|
||||||
|
if options.is_pdf:
|
||||||
|
generate_doc('pdf', options.output_dir, doujinshi, options.regenerate)
|
||||||
|
|
||||||
|
if options.move_to_folder:
|
||||||
|
if options.is_cbz:
|
||||||
|
move_to_folder(options.output_dir, doujinshi, 'cbz')
|
||||||
|
if options.is_pdf:
|
||||||
|
move_to_folder(options.output_dir, doujinshi, 'pdf')
|
||||||
|
|
||||||
|
if options.rm_origin_dir:
|
||||||
|
if options.move_to_folder:
|
||||||
|
logger.critical('You specified both --move-to-folder and --rm-origin-dir options, '
|
||||||
|
'you will not get anything :(')
|
||||||
|
shutil.rmtree(os.path.join(options.output_dir, doujinshi.filename), ignore_errors=True)
|
||||||
|
|
||||||
|
if options.main_viewer:
|
||||||
|
generate_main_html(options.output_dir)
|
||||||
|
serializer.finalize(options.output_dir)
|
||||||
|
|
||||||
|
if not platform.system() == 'Windows':
|
||||||
|
logger.log(16, '🍻 All done.')
|
||||||
|
else:
|
||||||
|
logger.log(16, 'All done.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
for doujinshi_id in doujinshi_ids:
|
||||||
|
meta = parser.fetch(str(doujinshi_id))
|
||||||
|
if not meta:
|
||||||
|
continue
|
||||||
|
doujinshi_model = plugin.create_model(meta, name_format=options.name_format)
|
||||||
|
doujinshi = doujinshi_model.doujinshi
|
||||||
|
doujinshi.show()
|
||||||
|
|
||||||
|
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
9
doujinshi_dl/constant.py
Normal file
9
doujinshi_dl/constant.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""Main-package constants.
|
||||||
|
|
||||||
|
Only the constants that the main package itself needs are defined here.
|
||||||
|
Plugin-specific constants live in the respective plugin package.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
PATH_SEPARATOR = os.path.sep
|
||||||
1
doujinshi_dl/core/__init__.py
Normal file
1
doujinshi_dl/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# coding: utf-8
|
||||||
16
doujinshi_dl/core/config.py
Normal file
16
doujinshi_dl/core/config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""Runtime configuration store for the main package.
|
||||||
|
|
||||||
|
Plugins write their paths and settings here so that generic utilities
|
||||||
|
(e.g. db.py) can read them without hard-coding any plugin name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_runtime: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def set(key: str, value) -> None:
|
||||||
|
_runtime[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def get(key: str, default=None):
|
||||||
|
return _runtime.get(key, default)
|
||||||
214
doujinshi_dl/core/downloader.py
Normal file
214
doujinshi_dl/core/downloader.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# coding: utf-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import urllib3.exceptions
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from doujinshi_dl.core.logger import logger
|
||||||
|
from doujinshi_dl.core.utils.db import Singleton
|
||||||
|
from doujinshi_dl.core import config as core_config
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_request(method, url, timeout=30, proxy=None):
|
||||||
|
"""Minimal async HTTP helper using httpx directly."""
|
||||||
|
# httpx >=0.28 uses `proxy` (str), older versions used `proxies` (dict)
|
||||||
|
client_kwargs = {'verify': False}
|
||||||
|
if proxy:
|
||||||
|
client_kwargs['proxy'] = proxy
|
||||||
|
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||||
|
headers = {}
|
||||||
|
cookie = core_config.get('plugin_config', {}).get('cookie', '')
|
||||||
|
useragent = core_config.get('plugin_config', {}).get('useragent', '')
|
||||||
|
if cookie:
|
||||||
|
headers['Cookie'] = cookie
|
||||||
|
if useragent:
|
||||||
|
headers['User-Agent'] = useragent
|
||||||
|
return await client.request(method, url, timeout=timeout, headers=headers, follow_redirects=True)
|
||||||
|
|
||||||
|
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
def download_callback(result):
|
||||||
|
result, data = result
|
||||||
|
if result == 0:
|
||||||
|
logger.warning('fatal errors occurred, ignored')
|
||||||
|
elif result == -1:
|
||||||
|
logger.warning(f'url {data} return status code 404')
|
||||||
|
elif result == -2:
|
||||||
|
logger.warning('Ctrl-C pressed, exiting sub processes ...')
|
||||||
|
elif result == -3:
|
||||||
|
# workers won't be run, just pass
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.log(16, f'{data} downloaded successfully')
|
||||||
|
|
||||||
|
|
||||||
|
class Downloader(Singleton):
|
||||||
|
def __init__(self, path='', threads=5, timeout=30, delay=0, exit_on_fail=False,
|
||||||
|
no_filename_padding=False):
|
||||||
|
self.threads = threads
|
||||||
|
self.path = str(path)
|
||||||
|
self.timeout = timeout
|
||||||
|
self.delay = delay
|
||||||
|
self.exit_on_fail = exit_on_fail
|
||||||
|
self.folder = None
|
||||||
|
self.semaphore = None
|
||||||
|
self.no_filename_padding = no_filename_padding
|
||||||
|
|
||||||
|
async def fiber(self, tasks):
|
||||||
|
self.semaphore = asyncio.Semaphore(self.threads)
|
||||||
|
for completed_task in asyncio.as_completed(tasks):
|
||||||
|
try:
|
||||||
|
result = await completed_task
|
||||||
|
if result[0] > 0:
|
||||||
|
logger.info(f'{result[1]} download completed')
|
||||||
|
else:
|
||||||
|
raise Exception(f'{result[1]} download failed, return value {result[0]}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'An error occurred: {e}')
|
||||||
|
if self.exit_on_fail:
|
||||||
|
raise Exception('User intends to exit on fail')
|
||||||
|
|
||||||
|
async def _semaphore_download(self, *args, **kwargs):
|
||||||
|
async with self.semaphore:
|
||||||
|
return await self.download(*args, **kwargs)
|
||||||
|
|
||||||
|
async def download(self, url, folder='', filename='', retried=0, proxy=None, length=0):
|
||||||
|
logger.info(f'Starting to download {url} ...')
|
||||||
|
|
||||||
|
if self.delay:
|
||||||
|
await asyncio.sleep(self.delay)
|
||||||
|
|
||||||
|
filename = filename if filename else os.path.basename(urlparse(url).path)
|
||||||
|
base_filename, extension = os.path.splitext(filename)
|
||||||
|
|
||||||
|
if not self.no_filename_padding:
|
||||||
|
filename = base_filename.zfill(length) + extension
|
||||||
|
else:
|
||||||
|
filename = base_filename + extension
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await _async_request('GET', url, timeout=self.timeout, proxy=proxy)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
path = urlparse(url).path
|
||||||
|
image_url_mirrors = core_config.get('image_url_mirrors', [])
|
||||||
|
for mirror in image_url_mirrors:
|
||||||
|
logger.info(f"Try mirror: {mirror}{path}")
|
||||||
|
mirror_url = f'{mirror}{path}'
|
||||||
|
response = await _async_request('GET', mirror_url, timeout=self.timeout, proxy=proxy)
|
||||||
|
if response.status_code == 200:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not await self.save(filename, response):
|
||||||
|
logger.error(f'Can not download image {url}')
|
||||||
|
return -1, url
|
||||||
|
|
||||||
|
except (httpx.HTTPStatusError, httpx.TimeoutException, httpx.ConnectError) as e:
|
||||||
|
retry_times = core_config.get('retry_times', 3)
|
||||||
|
if retried < retry_times:
|
||||||
|
logger.warning(f'Download {filename} failed, retrying({retried + 1}) times...')
|
||||||
|
return await self.download(
|
||||||
|
url=url,
|
||||||
|
folder=folder,
|
||||||
|
filename=filename,
|
||||||
|
retried=retried + 1,
|
||||||
|
proxy=proxy,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(f'Download {filename} failed with {retry_times} times retried, skipped')
|
||||||
|
return -2, url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(f"Exception type: {type(e)}")
|
||||||
|
traceback.print_stack()
|
||||||
|
logger.critical(str(e))
|
||||||
|
return -9, url
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return -4, url
|
||||||
|
|
||||||
|
return 1, url
|
||||||
|
|
||||||
|
async def save(self, filename, response) -> bool:
|
||||||
|
if response is None:
|
||||||
|
logger.error('Error: Response is None')
|
||||||
|
return False
|
||||||
|
save_file_path = os.path.join(self.folder, filename)
|
||||||
|
with open(save_file_path, 'wb') as f:
|
||||||
|
if response is not None:
|
||||||
|
length = response.headers.get('content-length')
|
||||||
|
if length is None:
|
||||||
|
f.write(response.content)
|
||||||
|
else:
|
||||||
|
async for chunk in response.aiter_bytes(2048):
|
||||||
|
f.write(chunk)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_storage_object(self, folder:str):
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
try:
|
||||||
|
os.makedirs(folder)
|
||||||
|
except EnvironmentError as e:
|
||||||
|
logger.critical(str(e))
|
||||||
|
self.folder:str = folder
|
||||||
|
self.close = lambda: None # Only available in class CompressedDownloader
|
||||||
|
|
||||||
|
def start_download(self, queue, folder='') -> bool:
|
||||||
|
if not isinstance(folder, (str,)):
|
||||||
|
folder = str(folder)
|
||||||
|
|
||||||
|
if self.path:
|
||||||
|
folder = os.path.join(self.path, folder)
|
||||||
|
|
||||||
|
logger.info(f'Doujinshi will be saved at "{folder}"')
|
||||||
|
self.create_storage_object(folder)
|
||||||
|
|
||||||
|
if os.getenv('DEBUG', None) == 'NODOWNLOAD':
|
||||||
|
# Assuming we want to continue with rest of process.
|
||||||
|
return True
|
||||||
|
|
||||||
|
digit_length = len(str(len(queue)))
|
||||||
|
logger.info(f'Total download pages: {len(queue)}')
|
||||||
|
coroutines = [
|
||||||
|
self._semaphore_download(url, filename=os.path.basename(urlparse(url).path), length=digit_length)
|
||||||
|
for url in queue
|
||||||
|
]
|
||||||
|
|
||||||
|
# Prevent coroutines infection
|
||||||
|
asyncio.run(self.fiber(coroutines))
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
class CompressedDownloader(Downloader):
|
||||||
|
def create_storage_object(self, folder):
|
||||||
|
filename = f'{folder}.zip'
|
||||||
|
print(filename)
|
||||||
|
self.zipfile = zipfile.ZipFile(filename,'w')
|
||||||
|
self.close = lambda: self.zipfile.close()
|
||||||
|
|
||||||
|
async def save(self, filename, response) -> bool:
|
||||||
|
if response is None:
|
||||||
|
logger.error('Error: Response is None')
|
||||||
|
return False
|
||||||
|
|
||||||
|
image_data = io.BytesIO()
|
||||||
|
length = response.headers.get('content-length')
|
||||||
|
if length is None:
|
||||||
|
content = await response.read()
|
||||||
|
image_data.write(content)
|
||||||
|
else:
|
||||||
|
async for chunk in response.aiter_bytes(2048):
|
||||||
|
image_data.write(chunk)
|
||||||
|
|
||||||
|
image_data.seek(0)
|
||||||
|
self.zipfile.writestr(filename, image_data.read())
|
||||||
|
return True
|
||||||
179
doujinshi_dl/core/logger.py
Normal file
179
doujinshi_dl/core/logger.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. Licensed under the new BSD license.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
import ctypes
|
||||||
|
import ctypes.wintypes
|
||||||
|
|
||||||
|
# Reference: https://gist.github.com/vsajip/758430
|
||||||
|
# https://github.com/ipython/ipython/issues/4252
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms686047%28v=vs.85%29.aspx
|
||||||
|
ctypes.windll.kernel32.SetConsoleTextAttribute.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD]
|
||||||
|
ctypes.windll.kernel32.SetConsoleTextAttribute.restype = ctypes.wintypes.BOOL
|
||||||
|
|
||||||
|
|
||||||
|
class ColorizingStreamHandler(logging.StreamHandler):
|
||||||
|
# color names to indices
|
||||||
|
color_map = {
|
||||||
|
'black': 0,
|
||||||
|
'red': 1,
|
||||||
|
'green': 2,
|
||||||
|
'yellow': 3,
|
||||||
|
'blue': 4,
|
||||||
|
'magenta': 5,
|
||||||
|
'cyan': 6,
|
||||||
|
'white': 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
# levels to (background, foreground, bold/intense)
|
||||||
|
level_map = {
|
||||||
|
logging.DEBUG: (None, 'blue', False),
|
||||||
|
logging.INFO: (None, 'white', False),
|
||||||
|
logging.WARNING: (None, 'yellow', False),
|
||||||
|
logging.ERROR: (None, 'red', False),
|
||||||
|
logging.CRITICAL: ('red', 'white', False)
|
||||||
|
}
|
||||||
|
csi = '\x1b['
|
||||||
|
reset = '\x1b[0m'
|
||||||
|
disable_coloring = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_tty(self):
|
||||||
|
isatty = getattr(self.stream, 'isatty', None)
|
||||||
|
return isatty and isatty() and not self.disable_coloring
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
try:
|
||||||
|
message = self.format(record)
|
||||||
|
stream = self.stream
|
||||||
|
|
||||||
|
if not self.is_tty:
|
||||||
|
if message and message[0] == "\r":
|
||||||
|
message = message[1:]
|
||||||
|
stream.write(message)
|
||||||
|
else:
|
||||||
|
self.output_colorized(message)
|
||||||
|
stream.write(getattr(self, 'terminator', '\n'))
|
||||||
|
|
||||||
|
self.flush()
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
self.handleError(record)
|
||||||
|
|
||||||
|
|
||||||
|
if not platform.system() == 'Windows':
|
||||||
|
def output_colorized(self, message):
|
||||||
|
self.stream.write(message)
|
||||||
|
else:
|
||||||
|
ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m')
|
||||||
|
|
||||||
|
nt_color_map = {
|
||||||
|
0: 0x00, # black
|
||||||
|
1: 0x04, # red
|
||||||
|
2: 0x02, # green
|
||||||
|
3: 0x06, # yellow
|
||||||
|
4: 0x01, # blue
|
||||||
|
5: 0x05, # magenta
|
||||||
|
6: 0x03, # cyan
|
||||||
|
7: 0x07, # white
|
||||||
|
}
|
||||||
|
|
||||||
|
def output_colorized(self, message):
|
||||||
|
parts = self.ansi_esc.split(message)
|
||||||
|
write = self.stream.write
|
||||||
|
h = None
|
||||||
|
fd = getattr(self.stream, 'fileno', None)
|
||||||
|
|
||||||
|
if fd is not None:
|
||||||
|
fd = fd()
|
||||||
|
|
||||||
|
if fd in (1, 2): # stdout or stderr
|
||||||
|
h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
|
||||||
|
|
||||||
|
while parts:
|
||||||
|
text = parts.pop(0)
|
||||||
|
|
||||||
|
if text:
|
||||||
|
if sys.version_info < (3, 0, 0):
|
||||||
|
write(text.encode('utf-8'))
|
||||||
|
else:
|
||||||
|
write(text)
|
||||||
|
|
||||||
|
if parts:
|
||||||
|
params = parts.pop(0)
|
||||||
|
|
||||||
|
if h is not None:
|
||||||
|
params = [int(p) for p in params.split(';')]
|
||||||
|
color = 0
|
||||||
|
|
||||||
|
for p in params:
|
||||||
|
if 40 <= p <= 47:
|
||||||
|
color |= self.nt_color_map[p - 40] << 4
|
||||||
|
elif 30 <= p <= 37:
|
||||||
|
color |= self.nt_color_map[p - 30]
|
||||||
|
elif p == 1:
|
||||||
|
color |= 0x08 # foreground intensity on
|
||||||
|
elif p == 0: # reset to default color
|
||||||
|
color = 0x07
|
||||||
|
else:
|
||||||
|
pass # error condition ignored
|
||||||
|
|
||||||
|
ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
|
||||||
|
|
||||||
|
def colorize(self, message, record):
|
||||||
|
if record.levelno in self.level_map and self.is_tty:
|
||||||
|
bg, fg, bold = self.level_map[record.levelno]
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if bg in self.color_map:
|
||||||
|
params.append(str(self.color_map[bg] + 40))
|
||||||
|
|
||||||
|
if fg in self.color_map:
|
||||||
|
params.append(str(self.color_map[fg] + 30))
|
||||||
|
|
||||||
|
if bold:
|
||||||
|
params.append('1')
|
||||||
|
|
||||||
|
if params and message:
|
||||||
|
if message.lstrip() != message:
|
||||||
|
prefix = re.search(r"\s+", message).group(0)
|
||||||
|
message = message[len(prefix):]
|
||||||
|
else:
|
||||||
|
prefix = ""
|
||||||
|
|
||||||
|
message = "%s%s" % (prefix, ''.join((self.csi, ';'.join(params),
|
||||||
|
'm', message, self.reset)))
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
message = logging.StreamHandler.format(self, record)
|
||||||
|
return self.colorize(message, record)
|
||||||
|
|
||||||
|
|
||||||
|
logging.addLevelName(16, "SUCCESS")
|
||||||
|
logger = logging.getLogger('doujinshi_dl')
|
||||||
|
LOGGER_HANDLER = ColorizingStreamHandler(sys.stdout)
|
||||||
|
FORMATTER = logging.Formatter("\r[%(asctime)s] %(funcName)s: %(message)s", "%H:%M:%S")
|
||||||
|
LOGGER_HANDLER.setFormatter(FORMATTER)
|
||||||
|
LOGGER_HANDLER.level_map[logging.getLevelName("SUCCESS")] = (None, "green", False)
|
||||||
|
logger.addHandler(LOGGER_HANDLER)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger.log(16, 'doujinshi-dl')
|
||||||
|
logger.info('info')
|
||||||
|
logger.warning('warning')
|
||||||
|
logger.debug('debug')
|
||||||
|
logger.error('error')
|
||||||
|
logger.critical('critical')
|
||||||
77
doujinshi_dl/core/plugin.py
Normal file
77
doujinshi_dl/core/plugin.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict, Any, Iterator, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GalleryMeta:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
pretty_name: str
|
||||||
|
img_id: str
|
||||||
|
ext: list
|
||||||
|
pages: int
|
||||||
|
info: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
extra: Dict[str, Any] = field(default_factory=dict) # plugin-private data
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'pretty_name': self.pretty_name,
|
||||||
|
'img_id': self.img_id,
|
||||||
|
'ext': self.ext,
|
||||||
|
'pages': self.pages,
|
||||||
|
}
|
||||||
|
d.update(self.info)
|
||||||
|
d.update(self.extra)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class BaseParser(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def fetch(self, gallery_id: str) -> GalleryMeta: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def search(self, keyword: str, sorting: str = 'date', page=None, **kwargs) -> List[Dict]: ...
|
||||||
|
|
||||||
|
def favorites(self, page=None) -> List[Dict]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def configure(self, args): ...
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def iter_tasks(self) -> Iterator[Tuple[str, str]]: ...
|
||||||
|
# yields (url, filename) tuples
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSerializer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def write_all(self, meta: GalleryMeta, output_dir: str): ...
|
||||||
|
|
||||||
|
def finalize(self, output_dir: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BasePlugin(ABC):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_parser(self) -> BaseParser: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_model(self, meta: GalleryMeta, name_format: str = '[%i][%a][%t]') -> BaseModel: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_serializer(self) -> BaseSerializer: ...
|
||||||
|
|
||||||
|
def register_args(self, argparser): pass
|
||||||
|
|
||||||
|
def check_auth(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def print_results(self, results) -> None:
|
||||||
|
pass
|
||||||
28
doujinshi_dl/core/registry.py
Normal file
28
doujinshi_dl/core/registry.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from doujinshi_dl.core.plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin(name: str) -> 'BasePlugin':
|
||||||
|
from importlib.metadata import entry_points
|
||||||
|
eps = entry_points(group='doujinshi_dl.plugins')
|
||||||
|
for ep in eps:
|
||||||
|
if ep.name == name:
|
||||||
|
return ep.load()
|
||||||
|
raise KeyError(
|
||||||
|
f"Plugin {name!r} not found. "
|
||||||
|
f"Install it with: pip install doujinshi-dl-{name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_plugin() -> 'BasePlugin':
|
||||||
|
from importlib.metadata import entry_points
|
||||||
|
eps = list(entry_points(group='doujinshi_dl.plugins'))
|
||||||
|
if not eps:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No doujinshi-dl plugin installed. "
|
||||||
|
"Install a plugin from PyPI, e.g.: pip install doujinshi-dl-<name>"
|
||||||
|
)
|
||||||
|
return eps[0].load()
|
||||||
5
doujinshi_dl/core/utils/__init__.py
Normal file
5
doujinshi_dl/core/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from doujinshi_dl.core.utils.db import Singleton, DB
|
||||||
|
from doujinshi_dl.core.utils.fs import format_filename, generate_cbz, move_to_folder, parse_doujinshi_obj, EXTENSIONS
|
||||||
|
from doujinshi_dl.core.utils.html import generate_html, generate_main_html
|
||||||
|
from doujinshi_dl.core.utils.http import async_request
|
||||||
50
doujinshi_dl/core/utils/db.py
Normal file
50
doujinshi_dl/core/utils/db.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""DB and Singleton utilities."""
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class _Singleton(type):
|
||||||
|
""" A metaclass that creates a Singleton base class when called. """
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
if cls not in cls._instances:
|
||||||
|
cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton(_Singleton(str('SingletonMeta'), (object,), {})):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DB(object):
|
||||||
|
conn = None
|
||||||
|
cur = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
from doujinshi_dl.core import config
|
||||||
|
history_path = config.get(
|
||||||
|
'history_path',
|
||||||
|
os.path.expanduser('~/.doujinshi-dl/history.sqlite3'),
|
||||||
|
)
|
||||||
|
self.conn = sqlite3.connect(history_path)
|
||||||
|
self.cur = self.conn.cursor()
|
||||||
|
self.cur.execute('CREATE TABLE IF NOT EXISTS download_history (id text)')
|
||||||
|
self.conn.commit()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
def clean_all(self):
|
||||||
|
self.cur.execute('DELETE FROM download_history WHERE 1')
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def add_one(self, data):
|
||||||
|
self.cur.execute('INSERT INTO download_history VALUES (?)', [data])
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
data = self.cur.execute('SELECT id FROM download_history')
|
||||||
|
return [i[0] for i in data]
|
||||||
98
doujinshi_dl/core/utils/fs.py
Normal file
98
doujinshi_dl/core/utils/fs.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""Filesystem utilities: filename formatting, CBZ generation, folder management."""
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from doujinshi_dl.core.logger import logger
|
||||||
|
from doujinshi_dl.constant import PATH_SEPARATOR
|
||||||
|
|
||||||
|
MAX_FIELD_LENGTH = 100
|
||||||
|
EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif', '.webp')
|
||||||
|
|
||||||
|
|
||||||
|
def format_filename(s, length=MAX_FIELD_LENGTH, _truncate_only=False):
|
||||||
|
"""
|
||||||
|
It used to be a whitelist approach allowed only alphabet and a part of symbols.
|
||||||
|
but most doujinshi's names include Japanese 2-byte characters and these was rejected.
|
||||||
|
so it is using blacklist approach now.
|
||||||
|
if filename include forbidden characters ('/:,;*?"<>|) ,it replaces space character(" ").
|
||||||
|
"""
|
||||||
|
if not _truncate_only:
|
||||||
|
ban_chars = '\\\'/:,;*?"<>|\t\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b'
|
||||||
|
filename = s.translate(str.maketrans(ban_chars, ' ' * len(ban_chars))).strip()
|
||||||
|
filename = ' '.join(filename.split())
|
||||||
|
|
||||||
|
while filename.endswith('.'):
|
||||||
|
filename = filename[:-1]
|
||||||
|
else:
|
||||||
|
filename = s
|
||||||
|
|
||||||
|
# limit `length` chars
|
||||||
|
if len(filename) >= length:
|
||||||
|
filename = filename[:length - 1] + u'…'
|
||||||
|
|
||||||
|
# Remove [] from filename
|
||||||
|
filename = filename.replace('[]', '').strip()
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def parse_doujinshi_obj(
|
||||||
|
output_dir: str,
|
||||||
|
doujinshi_obj=None,
|
||||||
|
file_type: str = ''
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
filename = f'.{PATH_SEPARATOR}doujinshi.{file_type}'
|
||||||
|
if doujinshi_obj is not None:
|
||||||
|
doujinshi_dir = os.path.join(output_dir, doujinshi_obj.filename)
|
||||||
|
_filename = f'{doujinshi_obj.filename}.{file_type}'
|
||||||
|
|
||||||
|
if file_type == 'pdf':
|
||||||
|
_filename = _filename.replace('/', '-')
|
||||||
|
|
||||||
|
filename = os.path.join(output_dir, _filename)
|
||||||
|
else:
|
||||||
|
if file_type == 'html':
|
||||||
|
return output_dir, 'index.html'
|
||||||
|
|
||||||
|
doujinshi_dir = f'.{PATH_SEPARATOR}'
|
||||||
|
|
||||||
|
if not os.path.exists(doujinshi_dir):
|
||||||
|
os.makedirs(doujinshi_dir)
|
||||||
|
|
||||||
|
return doujinshi_dir, filename
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cbz(doujinshi_dir, filename):
|
||||||
|
file_list = os.listdir(doujinshi_dir)
|
||||||
|
file_list.sort()
|
||||||
|
|
||||||
|
logger.info(f'Writing CBZ file to path: {filename}')
|
||||||
|
with zipfile.ZipFile(filename, 'w') as cbz_pf:
|
||||||
|
for image in file_list:
|
||||||
|
image_path = os.path.join(doujinshi_dir, image)
|
||||||
|
cbz_pf.write(image_path, image)
|
||||||
|
|
||||||
|
logger.log(16, f'Comic Book CBZ file has been written to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
|
def move_to_folder(output_dir='.', doujinshi_obj=None, file_type=None):
|
||||||
|
if not file_type:
|
||||||
|
raise RuntimeError('no file_type specified')
|
||||||
|
|
||||||
|
doujinshi_dir, filename = parse_doujinshi_obj(output_dir, doujinshi_obj, file_type)
|
||||||
|
|
||||||
|
for fn in os.listdir(doujinshi_dir):
|
||||||
|
file_path = os.path.join(doujinshi_dir, fn)
|
||||||
|
_, ext = os.path.splitext(file_path)
|
||||||
|
if ext in ['.pdf', '.cbz']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting file: {e}")
|
||||||
|
|
||||||
|
shutil.move(filename, os.path.join(doujinshi_dir, os.path.basename(filename)))
|
||||||
118
doujinshi_dl/core/utils/html.py
Normal file
118
doujinshi_dl/core/utils/html.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""HTML viewer generation utilities (generic, no site-specific references)."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from doujinshi_dl.core.logger import logger
|
||||||
|
from doujinshi_dl.core.utils.fs import EXTENSIONS, parse_doujinshi_obj
|
||||||
|
from doujinshi_dl.constant import PATH_SEPARATOR
|
||||||
|
|
||||||
|
|
||||||
|
def _readfile(path):
|
||||||
|
loc = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # doujinshi_dl/
|
||||||
|
|
||||||
|
with open(os.path.join(loc, path), 'r') as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html(output_dir='.', doujinshi_obj=None, template='default'):
|
||||||
|
doujinshi_dir, filename = parse_doujinshi_obj(output_dir, doujinshi_obj, 'html')
|
||||||
|
image_html = ''
|
||||||
|
|
||||||
|
if not os.path.exists(doujinshi_dir):
|
||||||
|
logger.warning(f'Path "{doujinshi_dir}" does not exist, creating.')
|
||||||
|
try:
|
||||||
|
os.makedirs(doujinshi_dir)
|
||||||
|
except EnvironmentError as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
file_list = os.listdir(doujinshi_dir)
|
||||||
|
file_list.sort()
|
||||||
|
|
||||||
|
for image in file_list:
|
||||||
|
if not os.path.splitext(image)[1] in EXTENSIONS:
|
||||||
|
continue
|
||||||
|
image_html += f'<img src="{image}" class="image-item"/>\n'
|
||||||
|
|
||||||
|
html = _readfile(f'viewer/{template}/index.html')
|
||||||
|
css = _readfile(f'viewer/{template}/styles.css')
|
||||||
|
js = _readfile(f'viewer/{template}/scripts.js')
|
||||||
|
|
||||||
|
if doujinshi_obj is not None:
|
||||||
|
name = doujinshi_obj.name
|
||||||
|
else:
|
||||||
|
metadata_path = os.path.join(doujinshi_dir, "metadata.json")
|
||||||
|
if os.path.exists(metadata_path):
|
||||||
|
with open(metadata_path, 'r') as file:
|
||||||
|
doujinshi_info = json.loads(file.read())
|
||||||
|
name = doujinshi_info.get("title")
|
||||||
|
else:
|
||||||
|
name = 'Doujinshi HTML Viewer'
|
||||||
|
|
||||||
|
data = html.format(TITLE=name, IMAGES=image_html, SCRIPTS=js, STYLES=css)
|
||||||
|
try:
|
||||||
|
with open(os.path.join(doujinshi_dir, 'index.html'), 'wb') as f:
|
||||||
|
f.write(data.encode('utf-8'))
|
||||||
|
|
||||||
|
logger.log(16, f'HTML Viewer has been written to "{os.path.join(doujinshi_dir, "index.html")}"')
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Writing HTML Viewer failed ({e})')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_main_html(output_dir=f'.{PATH_SEPARATOR}'):
|
||||||
|
"""
|
||||||
|
Generate a main html to show all the contained doujinshi.
|
||||||
|
With a link to their `index.html`.
|
||||||
|
Default output folder will be the CLI path.
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
image_html = ''
|
||||||
|
|
||||||
|
main = _readfile('viewer/main.html')
|
||||||
|
css = _readfile('viewer/main.css')
|
||||||
|
js = _readfile('viewer/main.js')
|
||||||
|
|
||||||
|
element = '\n\
|
||||||
|
<div class="gallery-favorite">\n\
|
||||||
|
<div class="gallery">\n\
|
||||||
|
<a href="./{FOLDER}/index.html" class="cover" style="padding:0 0 141.6% 0"><img\n\
|
||||||
|
src="./{FOLDER}/{IMAGE}" />\n\
|
||||||
|
<div class="caption">{TITLE}</div>\n\
|
||||||
|
</a>\n\
|
||||||
|
</div>\n\
|
||||||
|
</div>\n'
|
||||||
|
|
||||||
|
os.chdir(output_dir)
|
||||||
|
doujinshi_dirs = next(os.walk('.'))[1]
|
||||||
|
|
||||||
|
for folder in doujinshi_dirs:
|
||||||
|
files = os.listdir(folder)
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
if 'index.html' in files:
|
||||||
|
logger.info(f'Add doujinshi "{folder}"')
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
image = files[0] # 001.jpg or 001.png
|
||||||
|
if folder is not None:
|
||||||
|
title = folder.replace('_', ' ')
|
||||||
|
else:
|
||||||
|
title = 'Doujinshi HTML Viewer'
|
||||||
|
|
||||||
|
image_html += element.format(FOLDER=urllib.parse.quote(folder), IMAGE=image, TITLE=title)
|
||||||
|
if image_html == '':
|
||||||
|
logger.warning('No index.html found, --gen-main paused.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = main.format(STYLES=css, SCRIPTS=js, PICTURE=image_html)
|
||||||
|
with open('./main.html', 'wb') as f:
|
||||||
|
f.write(data.encode('utf-8'))
|
||||||
|
pkg_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
shutil.copy(os.path.join(pkg_dir, 'viewer/logo.png'), './')
|
||||||
|
output_dir = output_dir[:-1] if output_dir.endswith('/') else output_dir
|
||||||
|
logger.log(16, f'Main Viewer has been written to "{output_dir}/main.html"')
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f'Writing Main Viewer failed ({e})')
|
||||||
34
doujinshi_dl/core/utils/http.py
Normal file
34
doujinshi_dl/core/utils/http.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
"""Generic async HTTP request helper (no site-specific headers injected here)."""
|
||||||
|
import httpx
|
||||||
|
import urllib3.exceptions
|
||||||
|
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_request(method, url, proxy=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Thin async HTTP client wrapper.
|
||||||
|
|
||||||
|
Header injection (Cookie, User-Agent, Referer) is done by callers that
|
||||||
|
have access to site-specific configuration; this helper stays generic.
|
||||||
|
"""
|
||||||
|
from doujinshi_dl import constant
|
||||||
|
|
||||||
|
headers = kwargs.pop('headers', {})
|
||||||
|
|
||||||
|
if proxy is None:
|
||||||
|
proxy = constant.CONFIG.get('proxy', '')
|
||||||
|
|
||||||
|
if isinstance(proxy, str) and not proxy:
|
||||||
|
proxy = None
|
||||||
|
|
||||||
|
# Remove 'timeout' from kwargs to avoid duplicate keyword argument since
|
||||||
|
# httpx.AsyncClient accepts it as a constructor arg or request arg.
|
||||||
|
timeout = kwargs.pop('timeout', 30)
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(headers=headers, verify=False, proxy=proxy,
|
||||||
|
timeout=timeout) as client:
|
||||||
|
response = await client.request(method, url, **kwargs)
|
||||||
|
|
||||||
|
return response
|
||||||
5
doujinshi_dl/downloader.py
Normal file
5
doujinshi_dl/downloader.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# Compatibility shim — re-exports from new location.
|
||||||
|
# Preserves backward compatibility for: from doujinshi_dl.downloader import Downloader, CompressedDownloader
|
||||||
|
from doujinshi_dl.core.downloader import * # noqa: F401, F403
|
||||||
|
from doujinshi_dl.core.downloader import Downloader, CompressedDownloader, download_callback # noqa: F401
|
||||||
5
doujinshi_dl/logger.py
Normal file
5
doujinshi_dl/logger.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# Compatibility shim — re-exports from new location.
|
||||||
|
# Preserves backward compatibility for: from doujinshi_dl.logger import logger
|
||||||
|
from doujinshi_dl.core.logger import * # noqa: F401, F403
|
||||||
|
from doujinshi_dl.core.logger import logger # noqa: F401
|
||||||
77
doujinshi_dl/utils.py
Normal file
77
doujinshi_dl/utils.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# Utility helpers for the main package.
|
||||||
|
# No plugin-specific imports.
|
||||||
|
|
||||||
|
# Generic filesystem / HTML utilities
|
||||||
|
from doujinshi_dl.core.utils.fs import ( # noqa: F401
|
||||||
|
format_filename, parse_doujinshi_obj, generate_cbz, move_to_folder,
|
||||||
|
EXTENSIONS, MAX_FIELD_LENGTH,
|
||||||
|
)
|
||||||
|
from doujinshi_dl.core.utils.html import generate_html, generate_main_html # noqa: F401
|
||||||
|
from doujinshi_dl.core.utils.db import Singleton, DB # noqa: F401
|
||||||
|
|
||||||
|
# Signal handler and paging helper (kept inline — they have no site-specific code)
|
||||||
|
import sys
|
||||||
|
from doujinshi_dl.core.logger import logger # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(_signal, _frame):
|
||||||
|
logger.error('Ctrl-C signal received. Stopping...')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def paging(page_string):
|
||||||
|
# 1,3-5,14 -> [1, 3, 4, 5, 14]
|
||||||
|
if not page_string:
|
||||||
|
return [1]
|
||||||
|
|
||||||
|
page_list = []
|
||||||
|
for i in page_string.split(','):
|
||||||
|
if '-' in i:
|
||||||
|
start, end = i.split('-')
|
||||||
|
if not (start.isdigit() and end.isdigit()):
|
||||||
|
raise Exception('Invalid page number')
|
||||||
|
page_list.extend(list(range(int(start), int(end) + 1)))
|
||||||
|
else:
|
||||||
|
if not i.isdigit():
|
||||||
|
raise Exception('Invalid page number')
|
||||||
|
page_list.append(int(i))
|
||||||
|
|
||||||
|
return page_list
|
||||||
|
|
||||||
|
|
||||||
|
def generate_doc(file_type='', output_dir='.', doujinshi_obj=None, regenerate=False):
|
||||||
|
"""Generate a CBZ or PDF document from a downloaded doujinshi directory.
|
||||||
|
|
||||||
|
For CBZ, any metadata files (ComicInfo.xml, etc.) should be written to the
|
||||||
|
directory *before* calling this function.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
doujinshi_dir, filename = parse_doujinshi_obj(output_dir, doujinshi_obj, file_type)
|
||||||
|
|
||||||
|
if os.path.exists(f'{doujinshi_dir}.{file_type}') and not regenerate:
|
||||||
|
logger.info(f'Skipped {file_type} file generation: {doujinshi_dir}.{file_type} already exists')
|
||||||
|
return
|
||||||
|
|
||||||
|
if file_type == 'cbz':
|
||||||
|
generate_cbz(doujinshi_dir, filename)
|
||||||
|
|
||||||
|
elif file_type == 'pdf':
|
||||||
|
try:
|
||||||
|
import img2pdf
|
||||||
|
|
||||||
|
file_list = [f for f in os.listdir(doujinshi_dir) if f.lower().endswith(EXTENSIONS)]
|
||||||
|
file_list.sort()
|
||||||
|
|
||||||
|
logger.info(f'Writing PDF file to path: {filename}')
|
||||||
|
with open(filename, 'wb') as pdf_f:
|
||||||
|
full_path_list = [os.path.join(doujinshi_dir, image) for image in file_list]
|
||||||
|
pdf_f.write(img2pdf.convert(full_path_list, rotation=img2pdf.Rotation.ifvalid))
|
||||||
|
|
||||||
|
logger.log(16, f'PDF file has been written to "{filename}"')
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
logger.error("Please install img2pdf package by using pip.")
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid file type')
|
||||||
25
doujinshi_dl/viewer/default/index.html
Normal file
25
doujinshi_dl/viewer/default/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>{TITLE}</title>
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav id="list">
|
||||||
|
{IMAGES}</nav>
|
||||||
|
|
||||||
|
<div id="image-container">
|
||||||
|
<span id="page-num"></span>
|
||||||
|
<div id="dest"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
87
doujinshi_dl/viewer/default/scripts.js
Normal file
87
doujinshi_dl/viewer/default/scripts.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const pages = Array.from(document.querySelectorAll('img.image-item'));
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
function changePage(pageNum) {
|
||||||
|
const previous = pages[currentPage];
|
||||||
|
const current = pages[pageNum];
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous.classList.remove('current');
|
||||||
|
current.classList.add('current');
|
||||||
|
|
||||||
|
currentPage = pageNum;
|
||||||
|
|
||||||
|
const display = document.getElementById('dest');
|
||||||
|
display.style.backgroundImage = `url("${current.src}")`;
|
||||||
|
|
||||||
|
scroll(0,0)
|
||||||
|
|
||||||
|
document.getElementById('page-num')
|
||||||
|
.innerText = [
|
||||||
|
(pageNum + 1).toLocaleString(),
|
||||||
|
pages.length.toLocaleString()
|
||||||
|
].join('\u200a/\u200a');
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(0);
|
||||||
|
|
||||||
|
document.getElementById('list').onclick = event => {
|
||||||
|
if (pages.includes(event.target)) {
|
||||||
|
changePage(pages.indexOf(event.target));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('image-container').onclick = event => {
|
||||||
|
const width = document.getElementById('image-container').clientWidth;
|
||||||
|
const clickPos = event.clientX / width;
|
||||||
|
|
||||||
|
if (clickPos < 0.5) {
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
} else {
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeypress = event => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
// Previous Image
|
||||||
|
case 'w':
|
||||||
|
scrollBy(0, -40);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
// Return to previous page
|
||||||
|
case 'q':
|
||||||
|
window.history.go(-1);
|
||||||
|
break;
|
||||||
|
// Next Image
|
||||||
|
case ' ':
|
||||||
|
case 's':
|
||||||
|
scrollBy(0, 40);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}// remove arrow cause it won't work
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeydown = event =>{
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: //left
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 38: //up
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 39: //right
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
case 40: //down
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
70
doujinshi_dl/viewer/default/styles.css
Normal file
70
doujinshi_dl/viewer/default/styles.css
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
*, *::after, *::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
background-color: #e8e6e6;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list {
|
||||||
|
height: 2000px;
|
||||||
|
overflow: scroll;
|
||||||
|
width: 260px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img {
|
||||||
|
width: 200px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img.current {
|
||||||
|
background: #0003;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
flex: auto;
|
||||||
|
height: 2000px;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #dest {
|
||||||
|
height: 2000px;
|
||||||
|
width: 100%;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #page-num {
|
||||||
|
position: static;
|
||||||
|
font-size: 14pt;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.75;
|
||||||
|
text-shadow: /* Duplicate the same shadow to make it very strong */
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222;
|
||||||
|
}
|
||||||
BIN
doujinshi_dl/viewer/logo.png
Normal file
BIN
doujinshi_dl/viewer/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
331
doujinshi_dl/viewer/main.css
Normal file
331
doujinshi_dl/viewer/main.css
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
-webkit-text-decoration-skip: objects
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
*,:after,:before {
|
||||||
|
box-sizing: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body,html {
|
||||||
|
font-family: 'Noto Sans',sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #34495e;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #34495e
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin-left: 15rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
width: 100% - 15rem;
|
||||||
|
max-width: 1500px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery img,.gallery-favorite img,.thumb-container img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 980px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:19%;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 979px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:24%;
|
||||||
|
margin: 2px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 772px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:32%;
|
||||||
|
margin: 1.5px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:49%;
|
||||||
|
margin: .5px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery a,.gallery-favorite a {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery a img,.gallery-favorite a img {
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
line-height: 15px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 34px;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #34495e
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery:hover .caption {
|
||||||
|
max-height: 100%;
|
||||||
|
box-shadow: 0 10px 20px rgba(100,100,100,.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-favorite .gallery {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav {
|
||||||
|
height: 100%;
|
||||||
|
width: 15rem;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding-top: 20px;
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Old versions of Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav a {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 5px 0px 5px 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #0d0d0d;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sidenav img {
|
||||||
|
width:100%;
|
||||||
|
padding: 0px 5px 0px 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav a:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #EC2754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #eee;
|
||||||
|
color: #444;
|
||||||
|
padding: 10px 0px 5px 8px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: 0.4s;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion.active{
|
||||||
|
background-color:#ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #eee;
|
||||||
|
color: #444;
|
||||||
|
padding: 8px 8px 5px 9px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn a{
|
||||||
|
font-weight: normal;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: crosshair
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display:block;
|
||||||
|
padding: 0px 0px 0px 0px;
|
||||||
|
background-color: #eee;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.2s ease-out;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search{
|
||||||
|
background-color: #eee;
|
||||||
|
padding-right:40px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 5px;
|
||||||
|
height:43px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.search input{
|
||||||
|
border-top-right-radius:10px;
|
||||||
|
padding-top:0;
|
||||||
|
padding-bottom:0;
|
||||||
|
font-size:1em;
|
||||||
|
width:100%;
|
||||||
|
height:38px;
|
||||||
|
vertical-align:top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn{
|
||||||
|
border-top-left-radius:10px;
|
||||||
|
color:#fff;
|
||||||
|
font-size:100%;
|
||||||
|
padding: 8px;
|
||||||
|
width:38px;
|
||||||
|
background-color:#ed2553;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags{
|
||||||
|
text-align:left;
|
||||||
|
display: flex;
|
||||||
|
width:15rem;
|
||||||
|
justify-content: start;
|
||||||
|
margin: 2px 2px 2px 0px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2{
|
||||||
|
font-weight:700;
|
||||||
|
padding-right:0.5rem;
|
||||||
|
padding-left:0.5rem;
|
||||||
|
color:#fff;
|
||||||
|
border:0;
|
||||||
|
font-size:100%;
|
||||||
|
height:1.25rem;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin:0.15rem;
|
||||||
|
transition: all 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#parody{
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#character{
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#tag{
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#artist{
|
||||||
|
background-color: fuchsia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#group{
|
||||||
|
background-color: teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2.hover{
|
||||||
|
filter: saturate(20%)
|
||||||
|
}
|
||||||
|
|
||||||
|
input,input:focus{
|
||||||
|
border:none;
|
||||||
|
outline:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black,html.theme-black body {
|
||||||
|
color: #d9d9d9;
|
||||||
|
background-color: #0d0d0d
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black #thumbnail-container,html.theme-black .container {
|
||||||
|
background-color: #1f1f1f
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black .gallery:hover .caption {
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black .caption {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #d9d9d9
|
||||||
|
}
|
||||||
51
doujinshi_dl/viewer/main.html
Normal file
51
doujinshi_dl/viewer/main.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class=" theme-black">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="theme-color" content="#1f1f1f" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>Doujinshi Viewer</title>
|
||||||
|
<script type="text/javascript" src="data.js"></script>
|
||||||
|
<!-- <link rel="stylesheet" href="./main.css"> -->
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<nav class="sidenav">
|
||||||
|
<img src="logo.png">
|
||||||
|
<h1>Doujinshi Viewer</h1>
|
||||||
|
<button class="accordion">Language</button>
|
||||||
|
<div class="options" id="language">
|
||||||
|
<a>English</a>
|
||||||
|
<a>Japanese</a>
|
||||||
|
<a>Chinese</a>
|
||||||
|
</div>
|
||||||
|
<button class="accordion">Category</button>
|
||||||
|
<div class="options" id ="category">
|
||||||
|
<a>Doujinshi</a>
|
||||||
|
<a>Manga</a>
|
||||||
|
</div>
|
||||||
|
<button class="nav-btn hidden">Filters</button>
|
||||||
|
<div class="search">
|
||||||
|
<input autocomplete="off" type="search" id="tagfilter" name="q" value="" autocapitalize="none" required="">
|
||||||
|
<svg class="btn" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"/></svg>
|
||||||
|
<div id="tags">
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="container" id="favcontainer">
|
||||||
|
|
||||||
|
{PICTURE}
|
||||||
|
|
||||||
|
</div> <!-- container -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
177
doujinshi_dl/viewer/main.js
Normal file
177
doujinshi_dl/viewer/main.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
//------------------------------------navbar script------------------------------------
|
||||||
|
var menu = document.getElementsByClassName("accordion");
|
||||||
|
for (var i = 0; i < menu.length; i++) {
|
||||||
|
menu[i].addEventListener("click", function() {
|
||||||
|
var panel = this.nextElementSibling;
|
||||||
|
if (panel.style.maxHeight) {
|
||||||
|
this.classList.toggle("active");
|
||||||
|
panel.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||||
|
this.classList.toggle("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var language = document.getElementById("language").children;
|
||||||
|
for (var i = 0; i < language.length; i++){
|
||||||
|
language[i].addEventListener("click", function() {
|
||||||
|
toggler = document.getElementById("language")
|
||||||
|
toggler.style.maxHeight = null;
|
||||||
|
document.getElementsByClassName("accordion")[0].classList.toggle("active");
|
||||||
|
filter_maker(this.innerText, "language");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var category = document.getElementById("category").children;
|
||||||
|
for (var i = 0; i < category.length; i++){
|
||||||
|
category[i].addEventListener("click", function() {
|
||||||
|
document.getElementById("category").style.maxHeight = null;
|
||||||
|
document.getElementsByClassName("accordion")[1].classList.toggle("active");
|
||||||
|
filter_maker(this.innerText, "category");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
//----------------------------------Tags Script--------------------------------------
|
||||||
|
tag_maker(tags);
|
||||||
|
|
||||||
|
var tag = document.getElementsByClassName("btn-2");
|
||||||
|
for (var i = 0; i < tag.length; i++){
|
||||||
|
tag[i].addEventListener("click", function() {
|
||||||
|
filter_maker(this.innerText, this.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = document.getElementById("tagfilter");
|
||||||
|
input.addEventListener("input", function() {
|
||||||
|
var tags = document.querySelectorAll(".btn-2");
|
||||||
|
if (this.value.length > 0) {
|
||||||
|
for (var i = 0; i < tags.length; i++) {
|
||||||
|
var tag = tags[i];
|
||||||
|
var nome = tag.innerText;
|
||||||
|
var exp = new RegExp(this.value, "i");;
|
||||||
|
if (exp.test(nome)) {
|
||||||
|
tag.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tag.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < tags.length; i++) {
|
||||||
|
var tag = tags[i];
|
||||||
|
tag.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener('keypress', function (e) {
|
||||||
|
enter_search(e, this.value);
|
||||||
|
});
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
//------------------------------------Functions--------------------------------------
|
||||||
|
function enter_search(e, input){
|
||||||
|
var count = 0;
|
||||||
|
var key = e.which || e.keyCode;
|
||||||
|
if (key === 13 && input.length > 0) {
|
||||||
|
var all_tags = document.getElementById("tags").children;
|
||||||
|
for(i = 0; i < all_tags.length; i++){
|
||||||
|
if (!all_tags[i].classList.contains("hidden")){
|
||||||
|
count++;
|
||||||
|
var tag_name = all_tags[i].innerText;
|
||||||
|
var tag_id = all_tags[i].id;
|
||||||
|
if (count>1){break}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 1){
|
||||||
|
filter_maker(tag_name, tag_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function filter_maker(text, class_value){
|
||||||
|
var check = filter_checker(text);
|
||||||
|
var nav_btn = document.getElementsByClassName("nav-btn")[0];
|
||||||
|
if (nav_btn.classList.contains("hidden")){
|
||||||
|
nav_btn.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
if (check == true){
|
||||||
|
var node = document.createElement("a");
|
||||||
|
var textnode = document.createTextNode(text);
|
||||||
|
node.appendChild(textnode);
|
||||||
|
node.classList.add(class_value);
|
||||||
|
nav_btn.appendChild(node);
|
||||||
|
filter_searcher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter_searcher(){
|
||||||
|
var verifier = null;
|
||||||
|
var tags_filter = [];
|
||||||
|
var doujinshi_id = [];
|
||||||
|
var filter_tag = document.getElementsByClassName("nav-btn")[0].children;
|
||||||
|
filter_tag[filter_tag.length-1].addEventListener("click", function() {
|
||||||
|
this.remove();
|
||||||
|
try{
|
||||||
|
filter_searcher();
|
||||||
|
}
|
||||||
|
catch{
|
||||||
|
var gallery = document.getElementsByClassName("gallery-favorite");
|
||||||
|
for (var i = 0; i < gallery.length; i++){
|
||||||
|
gallery[i].classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (var i=0; i < filter_tag.length; i++){
|
||||||
|
var fclass = filter_tag[i].className;
|
||||||
|
var fname = filter_tag[i].innerText.toLowerCase();
|
||||||
|
tags_filter.push([fclass, fname])
|
||||||
|
}
|
||||||
|
for (var i=0; i < data.length; i++){
|
||||||
|
for (var j=0; j < tags_filter.length; j++){
|
||||||
|
try{
|
||||||
|
if(data[i][tags_filter[j][0]].includes(tags_filter[j][1])){
|
||||||
|
verifier = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
verifier = false;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch{
|
||||||
|
verifier = false;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifier){doujinshi_id.push(data[i].Folder.replace("_", " "));}
|
||||||
|
}
|
||||||
|
var gallery = document.getElementsByClassName("gallery-favorite");
|
||||||
|
for (var i = 0; i < gallery.length; i++){
|
||||||
|
gtext = gallery [i].children[0].children[0].children[1].innerText;
|
||||||
|
if(doujinshi_id.includes(gtext)){
|
||||||
|
gallery[i].classList.remove("hidden");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
gallery[i].classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter_checker(text){
|
||||||
|
var filter_tags = document.getElementsByClassName("nav-btn")[0].children;
|
||||||
|
if (filter_tags == null){return true;}
|
||||||
|
for (var i=0; i < filter_tags.length; i++){
|
||||||
|
if (filter_tags[i].innerText == text){return false;}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag_maker(data){
|
||||||
|
for (i in data){
|
||||||
|
for (j in data[i]){
|
||||||
|
var node = document.createElement("button");
|
||||||
|
var textnode = document.createTextNode(data[i][j]);
|
||||||
|
node.appendChild(textnode);
|
||||||
|
node.classList.add("btn-2");
|
||||||
|
node.setAttribute('id', i);
|
||||||
|
node.classList.add("hidden");
|
||||||
|
document.getElementById("tags").appendChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
doujinshi_dl/viewer/minimal/index.html
Normal file
25
doujinshi_dl/viewer/minimal/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>{TITLE}</title>
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav id="list" hidden=true>
|
||||||
|
{IMAGES}</nav>
|
||||||
|
|
||||||
|
<div id="image-container">
|
||||||
|
<div id="dest"></div>
|
||||||
|
<span id="page-num"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
doujinshi_dl/viewer/minimal/scripts.js
Normal file
79
doujinshi_dl/viewer/minimal/scripts.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const pages = Array.from(document.querySelectorAll('img.image-item'));
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
function changePage(pageNum) {
|
||||||
|
const previous = pages[currentPage];
|
||||||
|
const current = pages[pageNum];
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous.classList.remove('current');
|
||||||
|
current.classList.add('current');
|
||||||
|
|
||||||
|
currentPage = pageNum;
|
||||||
|
|
||||||
|
const display = document.getElementById('dest');
|
||||||
|
display.style.backgroundImage = `url("${current.src}")`;
|
||||||
|
|
||||||
|
scroll(0,0)
|
||||||
|
|
||||||
|
document.getElementById('page-num')
|
||||||
|
.innerText = [
|
||||||
|
(pageNum + 1).toLocaleString(),
|
||||||
|
pages.length.toLocaleString()
|
||||||
|
].join('\u200a/\u200a');
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(0);
|
||||||
|
|
||||||
|
document.getElementById('image-container').onclick = event => {
|
||||||
|
const width = document.getElementById('image-container').clientWidth;
|
||||||
|
const clickPos = event.clientX / width;
|
||||||
|
|
||||||
|
if (clickPos < 0.5) {
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
} else {
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeypress = event => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
// Previous Image
|
||||||
|
case 'w':
|
||||||
|
scrollBy(0, -40);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
// Return to previous page
|
||||||
|
case 'q':
|
||||||
|
window.history.go(-1);
|
||||||
|
break;
|
||||||
|
// Next Image
|
||||||
|
case ' ':
|
||||||
|
case 's':
|
||||||
|
scrollBy(0, 40);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}// remove arrow cause it won't work
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeydown = event =>{
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: //left
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 38: //up
|
||||||
|
break;
|
||||||
|
case 39: //right
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
case 40: //down
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
75
doujinshi_dl/viewer/minimal/styles.css
Normal file
75
doujinshi_dl/viewer/minimal/styles.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
*, *::after, *::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
background-color: #e8e6e6;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list {
|
||||||
|
height: 2000px;
|
||||||
|
overflow: scroll;
|
||||||
|
width: 260px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img {
|
||||||
|
width: 200px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img.current {
|
||||||
|
background: #0003;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
flex: auto;
|
||||||
|
height: 100%;
|
||||||
|
background: rgb(0, 0, 0);
|
||||||
|
color: rgb(100, 100, 100);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #dest {
|
||||||
|
height: 2000px;
|
||||||
|
width: 100%;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: top;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #page-num {
|
||||||
|
position: static;
|
||||||
|
font-size: 9pt;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-shadow: /* Duplicate the same shadow to make it very strong */
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222;
|
||||||
|
}
|
||||||
25
doujinshi_dl/viewer/viewer/default/index.html
Normal file
25
doujinshi_dl/viewer/viewer/default/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>{TITLE}</title>
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav id="list">
|
||||||
|
{IMAGES}</nav>
|
||||||
|
|
||||||
|
<div id="image-container">
|
||||||
|
<span id="page-num"></span>
|
||||||
|
<div id="dest"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
87
doujinshi_dl/viewer/viewer/default/scripts.js
Normal file
87
doujinshi_dl/viewer/viewer/default/scripts.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const pages = Array.from(document.querySelectorAll('img.image-item'));
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
function changePage(pageNum) {
|
||||||
|
const previous = pages[currentPage];
|
||||||
|
const current = pages[pageNum];
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous.classList.remove('current');
|
||||||
|
current.classList.add('current');
|
||||||
|
|
||||||
|
currentPage = pageNum;
|
||||||
|
|
||||||
|
const display = document.getElementById('dest');
|
||||||
|
display.style.backgroundImage = `url("${current.src}")`;
|
||||||
|
|
||||||
|
scroll(0,0)
|
||||||
|
|
||||||
|
document.getElementById('page-num')
|
||||||
|
.innerText = [
|
||||||
|
(pageNum + 1).toLocaleString(),
|
||||||
|
pages.length.toLocaleString()
|
||||||
|
].join('\u200a/\u200a');
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(0);
|
||||||
|
|
||||||
|
document.getElementById('list').onclick = event => {
|
||||||
|
if (pages.includes(event.target)) {
|
||||||
|
changePage(pages.indexOf(event.target));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('image-container').onclick = event => {
|
||||||
|
const width = document.getElementById('image-container').clientWidth;
|
||||||
|
const clickPos = event.clientX / width;
|
||||||
|
|
||||||
|
if (clickPos < 0.5) {
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
} else {
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeypress = event => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
// Previous Image
|
||||||
|
case 'w':
|
||||||
|
scrollBy(0, -40);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
// Return to previous page
|
||||||
|
case 'q':
|
||||||
|
window.history.go(-1);
|
||||||
|
break;
|
||||||
|
// Next Image
|
||||||
|
case ' ':
|
||||||
|
case 's':
|
||||||
|
scrollBy(0, 40);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}// remove arrow cause it won't work
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeydown = event =>{
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: //left
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 38: //up
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 39: //right
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
case 40: //down
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
70
doujinshi_dl/viewer/viewer/default/styles.css
Normal file
70
doujinshi_dl/viewer/viewer/default/styles.css
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
*, *::after, *::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
background-color: #e8e6e6;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list {
|
||||||
|
height: 2000px;
|
||||||
|
overflow: scroll;
|
||||||
|
width: 260px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img {
|
||||||
|
width: 200px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img.current {
|
||||||
|
background: #0003;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
flex: auto;
|
||||||
|
height: 2000px;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #dest {
|
||||||
|
height: 2000px;
|
||||||
|
width: 100%;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #page-num {
|
||||||
|
position: static;
|
||||||
|
font-size: 14pt;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.75;
|
||||||
|
text-shadow: /* Duplicate the same shadow to make it very strong */
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222;
|
||||||
|
}
|
||||||
BIN
doujinshi_dl/viewer/viewer/logo.png
Normal file
BIN
doujinshi_dl/viewer/viewer/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
331
doujinshi_dl/viewer/viewer/main.css
Normal file
331
doujinshi_dl/viewer/viewer/main.css
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
-webkit-text-decoration-skip: objects
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
*,:after,:before {
|
||||||
|
box-sizing: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body,html {
|
||||||
|
font-family: 'Noto Sans',sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #34495e;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #34495e
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin-left: 15rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
width: 100% - 15rem;
|
||||||
|
max-width: 1500px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery img,.gallery-favorite img,.thumb-container img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 980px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:19%;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 979px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:24%;
|
||||||
|
margin: 2px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 772px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:32%;
|
||||||
|
margin: 1.5px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.gallery,.gallery-favorite,.thumb-container {
|
||||||
|
width:49%;
|
||||||
|
margin: .5px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery a,.gallery-favorite a {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery a img,.gallery-favorite a img {
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
line-height: 15px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 34px;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #34495e
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 3em
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery:hover .caption {
|
||||||
|
max-height: 100%;
|
||||||
|
box-shadow: 0 10px 20px rgba(100,100,100,.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-favorite .gallery {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav {
|
||||||
|
height: 100%;
|
||||||
|
width: 15rem;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding-top: 20px;
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Old versions of Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav a {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 5px 0px 5px 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #0d0d0d;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.sidenav img {
|
||||||
|
width:100%;
|
||||||
|
padding: 0px 5px 0px 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav a:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #EC2754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #eee;
|
||||||
|
color: #444;
|
||||||
|
padding: 10px 0px 5px 8px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: 0.4s;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion.active{
|
||||||
|
background-color:#ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #eee;
|
||||||
|
color: #444;
|
||||||
|
padding: 8px 8px 5px 9px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn a{
|
||||||
|
font-weight: normal;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: crosshair
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display:block;
|
||||||
|
padding: 0px 0px 0px 0px;
|
||||||
|
background-color: #eee;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.2s ease-out;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search{
|
||||||
|
background-color: #eee;
|
||||||
|
padding-right:40px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 5px;
|
||||||
|
height:43px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.search input{
|
||||||
|
border-top-right-radius:10px;
|
||||||
|
padding-top:0;
|
||||||
|
padding-bottom:0;
|
||||||
|
font-size:1em;
|
||||||
|
width:100%;
|
||||||
|
height:38px;
|
||||||
|
vertical-align:top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn{
|
||||||
|
border-top-left-radius:10px;
|
||||||
|
color:#fff;
|
||||||
|
font-size:100%;
|
||||||
|
padding: 8px;
|
||||||
|
width:38px;
|
||||||
|
background-color:#ed2553;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags{
|
||||||
|
text-align:left;
|
||||||
|
display: flex;
|
||||||
|
width:15rem;
|
||||||
|
justify-content: start;
|
||||||
|
margin: 2px 2px 2px 0px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2{
|
||||||
|
font-weight:700;
|
||||||
|
padding-right:0.5rem;
|
||||||
|
padding-left:0.5rem;
|
||||||
|
color:#fff;
|
||||||
|
border:0;
|
||||||
|
font-size:100%;
|
||||||
|
height:1.25rem;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin:0.15rem;
|
||||||
|
transition: all 1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#parody{
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#character{
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#tag{
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#artist{
|
||||||
|
background-color: fuchsia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2#group{
|
||||||
|
background-color: teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-2.hover{
|
||||||
|
filter: saturate(20%)
|
||||||
|
}
|
||||||
|
|
||||||
|
input,input:focus{
|
||||||
|
border:none;
|
||||||
|
outline:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black,html.theme-black body {
|
||||||
|
color: #d9d9d9;
|
||||||
|
background-color: #0d0d0d
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black #thumbnail-container,html.theme-black .container {
|
||||||
|
background-color: #1f1f1f
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black .gallery:hover .caption {
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
html.theme-black .caption {
|
||||||
|
background-color: #404040;
|
||||||
|
color: #d9d9d9
|
||||||
|
}
|
||||||
51
doujinshi_dl/viewer/viewer/main.html
Normal file
51
doujinshi_dl/viewer/viewer/main.html
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class=" theme-black">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="theme-color" content="#1f1f1f" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>Doujinshi Viewer</title>
|
||||||
|
<script type="text/javascript" src="data.js"></script>
|
||||||
|
<!-- <link rel="stylesheet" href="./main.css"> -->
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<nav class="sidenav">
|
||||||
|
<img src="logo.png">
|
||||||
|
<h1>Doujinshi Viewer</h1>
|
||||||
|
<button class="accordion">Language</button>
|
||||||
|
<div class="options" id="language">
|
||||||
|
<a>English</a>
|
||||||
|
<a>Japanese</a>
|
||||||
|
<a>Chinese</a>
|
||||||
|
</div>
|
||||||
|
<button class="accordion">Category</button>
|
||||||
|
<div class="options" id ="category">
|
||||||
|
<a>Doujinshi</a>
|
||||||
|
<a>Manga</a>
|
||||||
|
</div>
|
||||||
|
<button class="nav-btn hidden">Filters</button>
|
||||||
|
<div class="search">
|
||||||
|
<input autocomplete="off" type="search" id="tagfilter" name="q" value="" autocapitalize="none" required="">
|
||||||
|
<svg class="btn" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="white" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"/></svg>
|
||||||
|
<div id="tags">
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="container" id="favcontainer">
|
||||||
|
|
||||||
|
{PICTURE}
|
||||||
|
|
||||||
|
</div> <!-- container -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
177
doujinshi_dl/viewer/viewer/main.js
Normal file
177
doujinshi_dl/viewer/viewer/main.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
//------------------------------------navbar script------------------------------------
|
||||||
|
var menu = document.getElementsByClassName("accordion");
|
||||||
|
for (var i = 0; i < menu.length; i++) {
|
||||||
|
menu[i].addEventListener("click", function() {
|
||||||
|
var panel = this.nextElementSibling;
|
||||||
|
if (panel.style.maxHeight) {
|
||||||
|
this.classList.toggle("active");
|
||||||
|
panel.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
panel.style.maxHeight = panel.scrollHeight + "px";
|
||||||
|
this.classList.toggle("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var language = document.getElementById("language").children;
|
||||||
|
for (var i = 0; i < language.length; i++){
|
||||||
|
language[i].addEventListener("click", function() {
|
||||||
|
toggler = document.getElementById("language")
|
||||||
|
toggler.style.maxHeight = null;
|
||||||
|
document.getElementsByClassName("accordion")[0].classList.toggle("active");
|
||||||
|
filter_maker(this.innerText, "language");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var category = document.getElementById("category").children;
|
||||||
|
for (var i = 0; i < category.length; i++){
|
||||||
|
category[i].addEventListener("click", function() {
|
||||||
|
document.getElementById("category").style.maxHeight = null;
|
||||||
|
document.getElementsByClassName("accordion")[1].classList.toggle("active");
|
||||||
|
filter_maker(this.innerText, "category");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
//----------------------------------Tags Script--------------------------------------
|
||||||
|
tag_maker(tags);
|
||||||
|
|
||||||
|
var tag = document.getElementsByClassName("btn-2");
|
||||||
|
for (var i = 0; i < tag.length; i++){
|
||||||
|
tag[i].addEventListener("click", function() {
|
||||||
|
filter_maker(this.innerText, this.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = document.getElementById("tagfilter");
|
||||||
|
input.addEventListener("input", function() {
|
||||||
|
var tags = document.querySelectorAll(".btn-2");
|
||||||
|
if (this.value.length > 0) {
|
||||||
|
for (var i = 0; i < tags.length; i++) {
|
||||||
|
var tag = tags[i];
|
||||||
|
var nome = tag.innerText;
|
||||||
|
var exp = new RegExp(this.value, "i");;
|
||||||
|
if (exp.test(nome)) {
|
||||||
|
tag.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tag.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < tags.length; i++) {
|
||||||
|
var tag = tags[i];
|
||||||
|
tag.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener('keypress', function (e) {
|
||||||
|
enter_search(e, this.value);
|
||||||
|
});
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
//------------------------------------Functions--------------------------------------
|
||||||
|
function enter_search(e, input){
|
||||||
|
var count = 0;
|
||||||
|
var key = e.which || e.keyCode;
|
||||||
|
if (key === 13 && input.length > 0) {
|
||||||
|
var all_tags = document.getElementById("tags").children;
|
||||||
|
for(i = 0; i < all_tags.length; i++){
|
||||||
|
if (!all_tags[i].classList.contains("hidden")){
|
||||||
|
count++;
|
||||||
|
var tag_name = all_tags[i].innerText;
|
||||||
|
var tag_id = all_tags[i].id;
|
||||||
|
if (count>1){break}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == 1){
|
||||||
|
filter_maker(tag_name, tag_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function filter_maker(text, class_value){
|
||||||
|
var check = filter_checker(text);
|
||||||
|
var nav_btn = document.getElementsByClassName("nav-btn")[0];
|
||||||
|
if (nav_btn.classList.contains("hidden")){
|
||||||
|
nav_btn.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
if (check == true){
|
||||||
|
var node = document.createElement("a");
|
||||||
|
var textnode = document.createTextNode(text);
|
||||||
|
node.appendChild(textnode);
|
||||||
|
node.classList.add(class_value);
|
||||||
|
nav_btn.appendChild(node);
|
||||||
|
filter_searcher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter_searcher(){
|
||||||
|
var verifier = null;
|
||||||
|
var tags_filter = [];
|
||||||
|
var doujinshi_id = [];
|
||||||
|
var filter_tag = document.getElementsByClassName("nav-btn")[0].children;
|
||||||
|
filter_tag[filter_tag.length-1].addEventListener("click", function() {
|
||||||
|
this.remove();
|
||||||
|
try{
|
||||||
|
filter_searcher();
|
||||||
|
}
|
||||||
|
catch{
|
||||||
|
var gallery = document.getElementsByClassName("gallery-favorite");
|
||||||
|
for (var i = 0; i < gallery.length; i++){
|
||||||
|
gallery[i].classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (var i=0; i < filter_tag.length; i++){
|
||||||
|
var fclass = filter_tag[i].className;
|
||||||
|
var fname = filter_tag[i].innerText.toLowerCase();
|
||||||
|
tags_filter.push([fclass, fname])
|
||||||
|
}
|
||||||
|
for (var i=0; i < data.length; i++){
|
||||||
|
for (var j=0; j < tags_filter.length; j++){
|
||||||
|
try{
|
||||||
|
if(data[i][tags_filter[j][0]].includes(tags_filter[j][1])){
|
||||||
|
verifier = true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
verifier = false;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch{
|
||||||
|
verifier = false;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verifier){doujinshi_id.push(data[i].Folder.replace("_", " "));}
|
||||||
|
}
|
||||||
|
var gallery = document.getElementsByClassName("gallery-favorite");
|
||||||
|
for (var i = 0; i < gallery.length; i++){
|
||||||
|
gtext = gallery [i].children[0].children[0].children[1].innerText;
|
||||||
|
if(doujinshi_id.includes(gtext)){
|
||||||
|
gallery[i].classList.remove("hidden");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
gallery[i].classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter_checker(text){
|
||||||
|
var filter_tags = document.getElementsByClassName("nav-btn")[0].children;
|
||||||
|
if (filter_tags == null){return true;}
|
||||||
|
for (var i=0; i < filter_tags.length; i++){
|
||||||
|
if (filter_tags[i].innerText == text){return false;}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag_maker(data){
|
||||||
|
for (i in data){
|
||||||
|
for (j in data[i]){
|
||||||
|
var node = document.createElement("button");
|
||||||
|
var textnode = document.createTextNode(data[i][j]);
|
||||||
|
node.appendChild(textnode);
|
||||||
|
node.classList.add("btn-2");
|
||||||
|
node.setAttribute('id', i);
|
||||||
|
node.classList.add("hidden");
|
||||||
|
document.getElementById("tags").appendChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
doujinshi_dl/viewer/viewer/minimal/index.html
Normal file
25
doujinshi_dl/viewer/viewer/minimal/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, viewport-fit=cover" />
|
||||||
|
<title>{TITLE}</title>
|
||||||
|
<style>
|
||||||
|
{STYLES}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav id="list" hidden=true>
|
||||||
|
{IMAGES}</nav>
|
||||||
|
|
||||||
|
<div id="image-container">
|
||||||
|
<div id="dest"></div>
|
||||||
|
<span id="page-num"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{SCRIPTS}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
doujinshi_dl/viewer/viewer/minimal/scripts.js
Normal file
79
doujinshi_dl/viewer/viewer/minimal/scripts.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const pages = Array.from(document.querySelectorAll('img.image-item'));
|
||||||
|
let currentPage = 0;
|
||||||
|
|
||||||
|
function changePage(pageNum) {
|
||||||
|
const previous = pages[currentPage];
|
||||||
|
const current = pages[pageNum];
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous.classList.remove('current');
|
||||||
|
current.classList.add('current');
|
||||||
|
|
||||||
|
currentPage = pageNum;
|
||||||
|
|
||||||
|
const display = document.getElementById('dest');
|
||||||
|
display.style.backgroundImage = `url("${current.src}")`;
|
||||||
|
|
||||||
|
scroll(0,0)
|
||||||
|
|
||||||
|
document.getElementById('page-num')
|
||||||
|
.innerText = [
|
||||||
|
(pageNum + 1).toLocaleString(),
|
||||||
|
pages.length.toLocaleString()
|
||||||
|
].join('\u200a/\u200a');
|
||||||
|
}
|
||||||
|
|
||||||
|
changePage(0);
|
||||||
|
|
||||||
|
document.getElementById('image-container').onclick = event => {
|
||||||
|
const width = document.getElementById('image-container').clientWidth;
|
||||||
|
const clickPos = event.clientX / width;
|
||||||
|
|
||||||
|
if (clickPos < 0.5) {
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
} else {
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeypress = event => {
|
||||||
|
switch (event.key.toLowerCase()) {
|
||||||
|
// Previous Image
|
||||||
|
case 'w':
|
||||||
|
scrollBy(0, -40);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
// Return to previous page
|
||||||
|
case 'q':
|
||||||
|
window.history.go(-1);
|
||||||
|
break;
|
||||||
|
// Next Image
|
||||||
|
case ' ':
|
||||||
|
case 's':
|
||||||
|
scrollBy(0, 40);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
}// remove arrow cause it won't work
|
||||||
|
};
|
||||||
|
|
||||||
|
document.onkeydown = event =>{
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: //left
|
||||||
|
changePage(currentPage - 1);
|
||||||
|
break;
|
||||||
|
case 38: //up
|
||||||
|
break;
|
||||||
|
case 39: //right
|
||||||
|
changePage(currentPage + 1);
|
||||||
|
break;
|
||||||
|
case 40: //down
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
75
doujinshi_dl/viewer/viewer/minimal/styles.css
Normal file
75
doujinshi_dl/viewer/viewer/minimal/styles.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
*, *::after, *::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
background-color: #e8e6e6;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list {
|
||||||
|
height: 2000px;
|
||||||
|
overflow: scroll;
|
||||||
|
width: 260px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img {
|
||||||
|
width: 200px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list img.current {
|
||||||
|
background: #0003;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container {
|
||||||
|
flex: auto;
|
||||||
|
height: 100%;
|
||||||
|
background: rgb(0, 0, 0);
|
||||||
|
color: rgb(100, 100, 100);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #dest {
|
||||||
|
height: 2000px;
|
||||||
|
width: 100%;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: top;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-container #page-num {
|
||||||
|
position: static;
|
||||||
|
font-size: 9pt;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-shadow: /* Duplicate the same shadow to make it very strong */
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222,
|
||||||
|
0 0 2px #222;
|
||||||
|
}
|
||||||
BIN
images/download.png
Normal file
BIN
images/download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
images/search.png
Normal file
BIN
images/search.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 991 KiB |
BIN
images/usage.png
Normal file
BIN
images/usage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 679 KiB |
BIN
images/viewer.png
Normal file
BIN
images/viewer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
370
poetry.lock
generated
Normal file
370
poetry.lock
generated
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.5.2"
|
||||||
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"},
|
||||||
|
{file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||||
|
idna = ">=2.8"
|
||||||
|
sniffio = ">=1.1"
|
||||||
|
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||||
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
|
||||||
|
trio = ["trio (>=0.26.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.12.3"
|
||||||
|
description = "Screen-scraping library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.0"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||||
|
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
soupsieve = ">1.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cchardet = ["cchardet"]
|
||||||
|
chardet = ["chardet"]
|
||||||
|
charset-normalizer = ["charset-normalizer"]
|
||||||
|
html5lib = ["html5lib"]
|
||||||
|
lxml = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2024.12.14"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
|
||||||
|
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardet"
|
||||||
|
version = "5.2.0"
|
||||||
|
description = "Universal encoding detector for Python 3"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
|
||||||
|
{file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.1"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
|
||||||
|
{file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
|
||||||
|
{file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
|
||||||
|
{file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.2.2"
|
||||||
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "python_version < \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||||
|
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||||
|
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||||
|
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.16"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
trio = ["trio (>=0.22.0,<1.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||||
|
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
certifi = "*"
|
||||||
|
httpcore = "==1.*"
|
||||||
|
idna = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.10"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||||
|
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iso8601"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "Simple module to parse ISO 8601 dates"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2,<4.0"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"},
|
||||||
|
{file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.3"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||||
|
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = ">=2,<4"
|
||||||
|
idna = ">=2.5,<4"
|
||||||
|
urllib3 = ">=1.21.1,<3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
description = "Sniff out which async library your code is running under"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||||
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.6"
|
||||||
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
|
||||||
|
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tabulate"
|
||||||
|
version = "0.9.0"
|
||||||
|
description = "Pretty-print tabular data"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
|
||||||
|
{file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
widechars = ["wcwidth"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.12.2"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "python_version < \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||||
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.20"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"},
|
||||||
|
{file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""]
|
||||||
|
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.1"
|
||||||
|
python-versions = "^3.8"
|
||||||
|
content-hash = "b17c2cdd4b140f2ab8081bca7d94630e821fa2e882ac768b1bd8cf3ec58726ce"
|
||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "doujinshi-dl"
|
||||||
|
version = "2.0.5"
|
||||||
|
description = "doujinshi downloader"
|
||||||
|
authors = ["Ricter Z <ricterzheng@gmail.com>"]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.rst"
|
||||||
|
homepage = "https://github.com/RicterZ/doujinshi-dl"
|
||||||
|
repository = "https://github.com/RicterZ/doujinshi-dl"
|
||||||
|
packages = [{include = "doujinshi_dl"}]
|
||||||
|
include = ["doujinshi_dl/viewer/**"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
requests = ">=2.28"
|
||||||
|
soupsieve = ">=2.3"
|
||||||
|
beautifulsoup4 = ">=4.11"
|
||||||
|
tabulate = ">=0.9"
|
||||||
|
iso8601 = ">=1.0"
|
||||||
|
httpx = ">=0.23"
|
||||||
|
chardet = ">=4.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
doujinshi-dl = 'doujinshi_dl.command:main'
|
||||||
29
qodana.yaml
Executable file
29
qodana.yaml
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||||
|
linter: jetbrains/qodana-python:2024.3
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Reference in New Issue
Block a user