--include README.md
--include requirements.txt
- include nhentai/doujinshi.html
-include nhentai/viewer/index.html
-include nhentai/viewer/styles.css
-include nhentai/viewer/scripts.js
++include README.md\r
++include requirements.txt\r
++include nhentai/viewer/index.html\r
++include nhentai/viewer/styles.css\r
++include nhentai/viewer/scripts.js\r
--nhentai
--=======
-- _ _ _ _
-- _ __ | | | | ___ _ __ | |_ __ _(_)
-- | '_ \| |_| |/ _ \ '_ \| __/ _` | |
-- | | | | _ | __/ | | | || (_| | |
-- |_| |_|_| |_|\___|_| |_|\__\__,_|_|
--
--あなたも変態。 いいね?
--[![Build Status](https://travis-ci.org/RicterZ/nhentai.svg?branch=master)](https://travis-ci.org/RicterZ/nhentai)
--
--🎉🎉 nhentai 现在支持 Windows 啦!
--
--由于 [http://nhentai.net](http://nhentai.net) 下载下来的种子速度很慢,而且官方也提供在线观看本子的功能,所以可以利用本脚本下载本子。
--
--### Installation
--
-- git clone https://github.com/RicterZ/nhentai
-- cd nhentai
-- python setup.py install
--
--### Gentoo
--
-- layman -fa glicOne
-- sudo emerge net-misc/nhentai
--
--### Usage
--下载指定 id 列表的本子:
--```bash
--nhentai --id=123855,123866
--```
--
--下载某关键词第一页的本子:
--```bash
--nhentai --search="tomori" --page=1 --download
--```
--
--下载用户 favorites 内容:
--```bash
--nhentai --login "username:password" --download
--```
--
--### Options
--
--`-t, --thread`:指定下载的线程数,最多为 10 线程。
--`--path`:指定下载文件的输出路径,默认为当前目录。
--`--timeout`:指定下载图片的超时时间,默认为 30 秒。
--`--proxy`:指定下载的代理,例如: http://127.0.0.1:8080/
--`--login`:nhentai 账号的“用户名:密码”组合
-`--nohtml`:nhentai Don't generate HTML
-`--cbz`:nhentai Generate Comic Book CBZ file
--
--### nHentai Mirror
--如果想用自建镜像下载 nhentai 的本子,需要搭建 nhentai.net 和 i.nhentai.net 的反向代理。
--例如用 h.loli.club 来做反向代理的话,需要 h.loli.club 反代 nhentai.net,i.h.loli.club 反带 i.nhentai.net。
--然后利用环境变量来下载:
--
--```bash
--NHENTAI=http://h.loli.club nhentai --id 123456
--```
--
--![](./images/search.png)
--![](./images/download.png)
--![](./images/viewer.png)
--
--### License
--MIT
--
--### あなたも変態
++nhentai\r
++=======\r
++ _ _ _ _\r
++ _ __ | | | | ___ _ __ | |_ __ _(_)\r
++ | '_ \| |_| |/ _ \ '_ \| __/ _` | |\r
++ | | | | _ | __/ | | | || (_| | |\r
++ |_| |_|_| |_|\___|_| |_|\__\__,_|_|\r
++\r
++あなたも変態。 いいね? \r
++[![Build Status](https://travis-ci.org/RicterZ/nhentai.svg?branch=master)](https://travis-ci.org/RicterZ/nhentai) \r
++\r
++🎉🎉 nhentai 现在支持 Windows 啦!\r
++\r
++由于 [http://nhentai.net](http://nhentai.net) 下载下来的种子速度很慢,而且官方也提供在线观看本子的功能,所以可以利用本脚本下载本子。\r
++\r
++### Installation\r
++\r
++ git clone https://github.com/RicterZ/nhentai\r
++ cd nhentai\r
++ python setup.py install\r
++ \r
++### Gentoo\r
++\r
++ layman -fa glicOne\r
++ sudo emerge net-misc/nhentai\r
++\r
++### Usage\r
++下载指定 id 列表的本子:\r
++```bash\r
++nhentai --id=123855,123866\r
++```\r
++\r
++下载某关键词第一页的本子:\r
++```bash\r
++nhentai --search="tomori" --page=1 --download\r
++```\r
++\r
++下载用户 favorites 内容:\r
++```bash\r
++nhentai --login "username:password" --download\r
++```\r
++\r
++### Options\r
++\r
++`-t, --thread`:指定下载的线程数,最多为 10 线程。 \r
++`--path`:指定下载文件的输出路径,默认为当前目录。 \r
++`--timeout`:指定下载图片的超时时间,默认为 30 秒。 \r
++`--proxy`:指定下载的代理,例如: http://127.0.0.1:8080/\r
++`--login`:nhentai 账号的“用户名:密码”组合\r
++`--nohtml`:nhentai Don't generate HTML\r
++`--cbz`:nhentai Generate Comic Book CBZ file\r
++\r
++### nHentai Mirror\r
++如果想用自建镜像下载 nhentai 的本子,需要搭建 nhentai.net 和 i.nhentai.net 的反向代理。 \r
++例如用 h.loli.club 来做反向代理的话,需要 h.loli.club 反代 nhentai.net,i.h.loli.club 反带 i.nhentai.net。 \r
++然后利用环境变量来下载: \r
++\r
++```bash\r
++NHENTAI=http://h.loli.club nhentai --id 123456\r
++```\r
++\r
++![](./images/search.png) \r
++![](./images/download.png) \r
++![](./images/viewer.png) \r
++\r
++### License \r
++MIT\r
++\r
++### あなたも変態\r
![](./images/image.jpg)
- __version__ = '0.2.12'
- __author__ = 'Ricter'
-__version__ = '0.2.13'
-__author__ = 'RicterZ'
--__email__ = 'ricterzheng@gmail.com'
++__version__ = '0.2.14'\r
++__author__ = 'RicterZ'\r
++__email__ = 'ricterzheng@gmail.com'\r
--# coding: utf-8
--from __future__ import print_function
--import sys
--from optparse import OptionParser
-from nhentai import __version__
--try:
-- from itertools import ifilter as filter
--except ImportError:
-- pass
--
--import nhentai.constant as constant
--from nhentai.utils import urlparse, generate_html
--from nhentai.logger import logger
--
--try:
-- reload(sys)
-- sys.setdefaultencoding(sys.stdin.encoding)
--except NameError:
-- # python3
-- pass
--
--
--def banner():
- logger.info(u'''nHentai: あなたも変態。 いいね?
- logger.info(u'''nHentai ver %s: あなたも変態。 いいね?
-- _ _ _ _
-- _ __ | | | | ___ _ __ | |_ __ _(_)
--| '_ \| |_| |/ _ \ '_ \| __/ _` | |
--| | | | _ | __/ | | | || (_| | |
--|_| |_|_| |_|\___|_| |_|\__\__,_|_|
- ''')
-''' % __version__)
--
--
--def cmd_parser():
-- parser = OptionParser('\n nhentai --search [keyword] --download'
-- '\n NHENTAI=http://h.loli.club nhentai --id [ID ...]'
-- '\n\nEnvironment Variable:\n'
-- ' NHENTAI nhentai mirror url')
-- parser.add_option('--download', dest='is_download', action='store_true',
-- help='download doujinshi (for search result)')
-- parser.add_option('--show-info', dest='is_show', action='store_true', help='just show the doujinshi information')
-- parser.add_option('--id', type='string', dest='id', action='store', help='doujinshi ids set, e.g. 1,2,3')
-- parser.add_option('--search', type='string', dest='keyword', action='store', help='search doujinshi by keyword')
-- parser.add_option('--page', type='int', dest='page', action='store', default=1,
-- help='page number of search result')
-- parser.add_option('--tags', type='string', dest='tags', action='store', help='download doujinshi by tags')
-- parser.add_option('--output', type='string', dest='output_dir', action='store', default='',
-- help='output dir')
-- parser.add_option('--threads', '-t', type='int', dest='threads', action='store', default=5,
-- help='thread count of download doujinshi')
-- parser.add_option('--timeout', type='int', dest='timeout', action='store', default=30,
-- help='timeout of download doujinshi')
-- parser.add_option('--proxy', type='string', dest='proxy', action='store', default='',
-- help='use proxy, example: http://127.0.0.1:1080')
-- parser.add_option('--html', dest='html_viewer', action='store_true',
-- help='generate a html viewer at current directory')
--
-- parser.add_option('--login', '-l', type='str', dest='login', action='store',
-- help='username:password pair of nhentai account')
-
- parser.add_option('--nohtml', dest='is_nohtml', action='store_true',
- help='Don\'t generate HTML')
--
- parser.add_option('--cbz', dest='is_cbz', action='store_true',
- help='Generate Comic Book CBZ File')
-
-- try:
-- sys.argv = list(map(lambda x: unicode(x.decode(sys.stdin.encoding)), sys.argv))
-- except (NameError, TypeError):
-- pass
-- except UnicodeDecodeError:
-- exit(0)
--
-- args, _ = parser.parse_args(sys.argv[1:])
--
-- if args.html_viewer:
-- generate_html()
-- exit(0)
--
-- if args.login:
-- try:
-- _, _ = args.login.split(':', 1)
-- except ValueError:
-- logger.error('Invalid `username:password` pair.')
-- exit(1)
--
-- if not args.is_download:
-- logger.warning('YOU DO NOT SPECIFY `--download` OPTION !!!')
--
-- if args.tags:
-- logger.warning('`--tags` is under construction')
-- exit(1)
--
-- if args.id:
-- _ = map(lambda id: id.strip(), args.id.split(','))
-- args.id = set(map(int, filter(lambda id_: id_.isdigit(), _)))
--
-- if (args.is_download or args.is_show) and not args.id and not args.keyword and not args.login:
-- logger.critical('Doujinshi id(s) are required for downloading')
-- parser.print_help()
-- exit(1)
--
-- if not args.keyword and not args.id and not args.login:
-- parser.print_help()
-- exit(1)
--
-- if args.threads <= 0:
-- args.threads = 1
--
-- elif args.threads > 15:
-- logger.critical('Maximum number of used threads is 15')
-- exit(1)
--
-- if args.proxy:
-- proxy_url = urlparse(args.proxy)
-- if proxy_url.scheme not in ('http', 'https'):
-- logger.error('Invalid protocol \'{0}\' of proxy, ignored'.format(proxy_url.scheme))
-- else:
-- constant.PROXY = {'http': args.proxy, 'https': args.proxy}
--
-- return args
++# coding: utf-8\r
++from __future__ import print_function\r
++import sys\r
++from optparse import OptionParser\r
++from nhentai import __version__\r
++try:\r
++ from itertools import ifilter as filter\r
++except ImportError:\r
++ pass\r
++\r
++import nhentai.constant as constant\r
++from nhentai.utils import urlparse, generate_html\r
++from nhentai.logger import logger\r
++\r
++try:\r
++ reload(sys)\r
++ sys.setdefaultencoding(sys.stdin.encoding)\r
++except NameError:\r
++ # python3\r
++ pass\r
++\r
++\r
++def banner():\r
++ logger.info(u'''nHentai ver %s: あなたも変態。 いいね?\r
++ _ _ _ _\r
++ _ __ | | | | ___ _ __ | |_ __ _(_)\r
++| '_ \| |_| |/ _ \ '_ \| __/ _` | |\r
++| | | | _ | __/ | | | || (_| | |\r
++|_| |_|_| |_|\___|_| |_|\__\__,_|_|\r
++''' % __version__)\r
++\r
++\r
++def cmd_parser():\r
++ parser = OptionParser('\n nhentai --search [keyword] --download'\r
++ '\n NHENTAI=http://h.loli.club nhentai --id [ID ...]'\r
++ '\n\nEnvironment Variable:\n'\r
++ ' NHENTAI nhentai mirror url')\r
++ parser.add_option('--download', dest='is_download', action='store_true',\r
++ help='download doujinshi (for search result)')\r
++ parser.add_option('--show-info', dest='is_show', action='store_true', help='just show the doujinshi information')\r
++ parser.add_option('--id', type='string', dest='id', action='store', help='doujinshi ids set, e.g. 1,2,3')\r
++ parser.add_option('--search', type='string', dest='keyword', action='store', help='search doujinshi by keyword')\r
++ parser.add_option('--page', type='int', dest='page', action='store', default=1,\r
++ help='page number of search result')\r
++ parser.add_option('--tags', type='string', dest='tags', action='store', help='download doujinshi by tags')\r
++ parser.add_option('--output', type='string', dest='output_dir', action='store', default='',\r
++ help='output dir')\r
++ parser.add_option('--threads', '-t', type='int', dest='threads', action='store', default=5,\r
++ help='thread count of download doujinshi')\r
++ parser.add_option('--timeout', type='int', dest='timeout', action='store', default=30,\r
++ help='timeout of download doujinshi')\r
++ parser.add_option('--proxy', type='string', dest='proxy', action='store', default='',\r
++ help='use proxy, example: http://127.0.0.1:1080')\r
++ parser.add_option('--html', dest='html_viewer', action='store_true',\r
++ help='generate a html viewer at current directory')\r
++\r
++ parser.add_option('--login', '-l', type='str', dest='login', action='store',\r
++ help='username:password pair of nhentai account')\r
++\r
++ parser.add_option('--nohtml', dest='is_nohtml', action='store_true',\r
++ help='Don\'t generate HTML')\r
++\r
++ parser.add_option('--cbz', dest='is_cbz', action='store_true',\r
++ help='Generate Comic Book CBZ File') \r
++ \r
++ try:\r
++ sys.argv = list(map(lambda x: unicode(x.decode(sys.stdin.encoding)), sys.argv))\r
++ except (NameError, TypeError):\r
++ pass\r
++ except UnicodeDecodeError:\r
++ exit(0)\r
++\r
++ args, _ = parser.parse_args(sys.argv[1:])\r
++\r
++ if args.html_viewer:\r
++ generate_html()\r
++ exit(0)\r
++\r
++ if args.login:\r
++ try:\r
++ _, _ = args.login.split(':', 1)\r
++ except ValueError:\r
++ logger.error('Invalid `username:password` pair.')\r
++ exit(1)\r
++\r
++ if not args.is_download:\r
++ logger.warning('YOU DO NOT SPECIFY `--download` OPTION !!!')\r
++\r
++ if args.tags:\r
++ logger.warning('`--tags` is under construction')\r
++ exit(1)\r
++\r
++ if args.id:\r
++ _ = map(lambda id: id.strip(), args.id.split(','))\r
++ args.id = set(map(int, filter(lambda id_: id_.isdigit(), _)))\r
++\r
++ if (args.is_download or args.is_show) and not args.id and not args.keyword and not args.login:\r
++ logger.critical('Doujinshi id(s) are required for downloading')\r
++ parser.print_help()\r
++ exit(1)\r
++\r
++ if not args.keyword and not args.id and not args.login:\r
++ parser.print_help()\r
++ exit(1)\r
++\r
++ if args.threads <= 0:\r
++ args.threads = 1\r
++\r
++ elif args.threads > 15:\r
++ logger.critical('Maximum number of used threads is 15')\r
++ exit(1)\r
++\r
++ if args.proxy:\r
++ proxy_url = urlparse(args.proxy)\r
++ if proxy_url.scheme not in ('http', 'https'):\r
++ logger.error('Invalid protocol \'{0}\' of proxy, ignored'.format(proxy_url.scheme))\r
++ else:\r
++ constant.PROXY = {'http': args.proxy, 'https': args.proxy}\r
++\r
++ return args\r
--#!/usr/bin/env python2.7
--# coding: utf-8
--from __future__ import unicode_literals, print_function
--import signal
--import platform
--
--from nhentai.cmdline import cmd_parser, banner
--from nhentai.parser import doujinshi_parser, search_parser, print_doujinshi, login_parser
--from nhentai.doujinshi import Doujinshi
--from nhentai.downloader import Downloader
--from nhentai.logger import logger
--from nhentai.constant import BASE_URL
- from nhentai.utils import generate_html
-from nhentai.utils import generate_html, generate_cbz
--
--
--def main():
-- banner()
-- logger.info('Using mirror: {0}'.format(BASE_URL))
-- options = cmd_parser()
--
-- doujinshi_ids = []
-- doujinshi_list = []
--
-- if options.login:
-- username, password = options.login.split(':', 1)
-- logger.info('Login to nhentai use credential \'%s:%s\'' % (username, '*' * len(password)))
-- for doujinshi_info in login_parser(username=username, password=password):
-- doujinshi_list.append(Doujinshi(**doujinshi_info))
--
-- if options.keyword:
-- doujinshis = search_parser(options.keyword, options.page)
-- print_doujinshi(doujinshis)
-- if options.is_download:
-- doujinshi_ids = map(lambda d: d['id'], doujinshis)
-- else:
-- doujinshi_ids = options.id
--
-- if doujinshi_ids:
-- for id_ in doujinshi_ids:
-- doujinshi_info = doujinshi_parser(id_)
-- doujinshi_list.append(Doujinshi(**doujinshi_info))
--
-- if not options.is_show:
-- downloader = Downloader(path=options.output_dir,
-- thread=options.threads, timeout=options.timeout)
--
-- for doujinshi in doujinshi_list:
-- doujinshi.downloader = downloader
-- doujinshi.download()
- generate_html(options.output_dir, doujinshi)
- if not options.is_nohtml and not options.is_cbz:
- generate_html(options.output_dir, doujinshi)
- elif options.is_cbz:
- generate_cbz(options.output_dir, doujinshi)
--
-- if not platform.system() == 'Windows':
- logger.log(15, '🍺 All done.')
- logger.log(15, '🍻 All done.')
-- else:
-- logger.log(15, 'All done.')
--
-- else:
-- [doujinshi.show() for doujinshi in doujinshi_list]
--
--
--def signal_handler(signal, frame):
-- logger.error('Ctrl-C signal received. Quit.')
-- exit(1)
--
--
--signal.signal(signal.SIGINT, signal_handler)
--
--if __name__ == '__main__':
-- main()
++#!/usr/bin/env python2.7\r
++# coding: utf-8\r
++from __future__ import unicode_literals, print_function\r
++import signal\r
++import platform\r
++\r
++from nhentai.cmdline import cmd_parser, banner\r
++from nhentai.parser import doujinshi_parser, search_parser, print_doujinshi, login_parser\r
++from nhentai.doujinshi import Doujinshi\r
++from nhentai.downloader import Downloader\r
++from nhentai.logger import logger\r
++from nhentai.constant import BASE_URL\r
++from nhentai.utils import generate_html, generate_cbz\r
++\r
++\r
++def main():\r
++ banner()\r
++ logger.info('Using mirror: {0}'.format(BASE_URL))\r
++ options = cmd_parser()\r
++\r
++ doujinshi_ids = []\r
++ doujinshi_list = []\r
++\r
++ if options.login:\r
++ username, password = options.login.split(':', 1)\r
++ logger.info('Login to nhentai use credential \'%s:%s\'' % (username, '*' * len(password)))\r
++ for doujinshi_info in login_parser(username=username, password=password):\r
++ doujinshi_list.append(Doujinshi(**doujinshi_info))\r
++\r
++ if options.keyword:\r
++ doujinshis = search_parser(options.keyword, options.page)\r
++ print_doujinshi(doujinshis)\r
++ if options.is_download:\r
++ doujinshi_ids = map(lambda d: d['id'], doujinshis)\r
++ else:\r
++ doujinshi_ids = options.id\r
++\r
++ if doujinshi_ids:\r
++ for id_ in doujinshi_ids:\r
++ doujinshi_info = doujinshi_parser(id_)\r
++ doujinshi_list.append(Doujinshi(**doujinshi_info))\r
++\r
++ if not options.is_show:\r
++ downloader = Downloader(path=options.output_dir,\r
++ thread=options.threads, timeout=options.timeout)\r
++\r
++ for doujinshi in doujinshi_list:\r
++ doujinshi.downloader = downloader\r
++ doujinshi.download()\r
++ if not options.is_nohtml and not options.is_cbz:\r
++ generate_html(options.output_dir, doujinshi)\r
++ elif options.is_cbz:\r
++ generate_cbz(options.output_dir, doujinshi)\r
++\r
++ if not platform.system() == 'Windows':\r
++ logger.log(15, '🍻 All done.')\r
++ else:\r
++ logger.log(15, 'All done.')\r
++\r
++ else:\r
++ [doujinshi.show() for doujinshi in doujinshi_list]\r
++\r
++\r
++def signal_handler(signal, frame):\r
++ logger.error('Ctrl-C signal received. Quit.')\r
++ exit(1)\r
++\r
++\r
++signal.signal(signal.SIGINT, signal_handler)\r
++\r
++if __name__ == '__main__':\r
++ main()\r
--# coding: utf-8
--from __future__ import unicode_literals, print_function
--
--import os
--import re
--import threadpool
--import requests
--from bs4 import BeautifulSoup
--from tabulate import tabulate
--
--import nhentai.constant as constant
--from nhentai.logger import logger
--
--
--def request(method, url, **kwargs):
-- if not hasattr(requests, method):
-- raise AttributeError('\'requests\' object has no attribute \'{0}\''.format(method))
--
-- return requests.__dict__[method](url, proxies=constant.PROXY, verify=False, **kwargs)
--
--
--def login_parser(username, password):
-- s = requests.Session()
-- s.proxies = constant.PROXY
-- s.verify = False
-- s.headers.update({'Referer': constant.LOGIN_URL})
--
-- s.get(constant.LOGIN_URL)
-- content = s.get(constant.LOGIN_URL).content
- html = BeautifulSoup(content, 'html.parser')
- html = BeautifulSoup(content, 'html.parser').encode("ascii")
-- csrf_token_elem = html.find('input', attrs={'name': 'csrfmiddlewaretoken'})
--
-- if not csrf_token_elem:
-- raise Exception('Cannot find csrf token to login')
-- csrf_token = csrf_token_elem.attrs['value']
--
-- login_dict = {
-- 'csrfmiddlewaretoken': csrf_token,
-- 'username_or_email': username,
-- 'password': password,
-- }
-- resp = s.post(constant.LOGIN_URL, data=login_dict)
-- if 'Invalid username (or email) or password' in resp.text:
-- logger.error('Login failed, please check your username and password')
-- exit(1)
--
- html = BeautifulSoup(s.get(constant.FAV_URL).content, 'html.parser')
- html = BeautifulSoup(s.get(constant.FAV_URL).content, 'html.parser').encode("ascii")
-- count = html.find('span', attrs={'class': 'count'})
-- if not count:
-- logger.error('Cannot get count of your favorites, maybe login failed.')
--
-- count = int(count.text.strip('(').strip(')'))
- if count == 0:
- logger.warning('No favorites found')
- return []
- pages = int(count / 25)
-
- if pages:
- pages += 1 if count % (25 * pages) else 0
- else:
- pages = 1
-
- pages = count / 25
- pages += 1 if count % (25 * pages) else 0
-- logger.info('Your have %d favorites in %d pages.' % (count, pages))
--
-- if os.getenv('DEBUG'):
-- pages = 1
--
-- ret = []
-- doujinshi_id = re.compile('data-id="([\d]+)"')
--
-- def _callback(request, result):
-- ret.append(result)
--
-- thread_pool = threadpool.ThreadPool(5)
--
-- for page in range(1, pages+1):
-- try:
-- logger.info('Getting doujinshi id of page %d' % page)
- resp = s.get(constant.FAV_URL + '?page=%d' % page).text
- resp = s.get(constant.FAV_URL + '?page=%d' % page).content
-- ids = doujinshi_id.findall(resp)
-- requests_ = threadpool.makeRequests(doujinshi_parser, ids, _callback)
-- [thread_pool.putRequest(req) for req in requests_]
-- thread_pool.wait()
-- except Exception as e:
-- logger.error('Error: %s, continue', str(e))
--
-- return ret
--
--
--def doujinshi_parser(id_):
-- if not isinstance(id_, (int,)) and (isinstance(id_, (str,)) and not id_.isdigit()):
-- raise Exception('Doujinshi id({0}) is not valid'.format(id_))
--
-- id_ = int(id_)
-- logger.log(15, 'Fetching doujinshi information of id {0}'.format(id_))
-- doujinshi = dict()
-- doujinshi['id'] = id_
-- url = '{0}/{1}'.format(constant.DETAIL_URL, id_)
--
-- try:
-- response = request('get', url).json()
-- except Exception as e:
-- logger.critical(str(e))
-- exit(1)
--
-- doujinshi['name'] = response['title']['english']
-- doujinshi['subtitle'] = response['title']['japanese']
-- doujinshi['img_id'] = response['media_id']
-- doujinshi['ext'] = ''.join(map(lambda s: s['t'], response['images']['pages']))
-- doujinshi['pages'] = len(response['images']['pages'])
--
-- # gain information of the doujinshi
-- needed_fields = ['character', 'artist', 'language']
-- for tag in response['tags']:
-- tag_type = tag['type']
-- if tag_type in needed_fields:
-- if tag_type not in doujinshi:
-- doujinshi[tag_type] = tag['name']
-- else:
-- doujinshi[tag_type] += tag['name']
--
-- return doujinshi
--
--
--def search_parser(keyword, page):
-- logger.debug('Searching doujinshis of keyword {0}'.format(keyword))
-- result = []
-- try:
-- response = request('get', url=constant.SEARCH_URL, params={'query': keyword, 'page': page}).json()
-- if 'result' not in response:
-- raise Exception('No result in response')
-- except requests.ConnectionError as e:
-- logger.critical(e)
-- logger.warn('If you are in China, please configure the proxy to fu*k GFW.')
-- exit(1)
--
-- for row in response['result']:
-- title = row['title']['english']
-- title = title[:85] + '..' if len(title) > 85 else title
-- result.append({'id': row['id'], 'title': title})
--
-- if not result:
-- logger.warn('Not found anything of keyword {}'.format(keyword))
--
-- return result
--
--
--def print_doujinshi(doujinshi_list):
-- if not doujinshi_list:
-- return
-- doujinshi_list = [(i['id'], i['title']) for i in doujinshi_list]
-- headers = ['id', 'doujinshi']
-- logger.info('Search Result\n' +
-- tabulate(tabular_data=doujinshi_list, headers=headers, tablefmt='rst'))
--
--
--if __name__ == '__main__':
-- print(doujinshi_parser("32271"))
++# coding: utf-8\r
++from __future__ import unicode_literals, print_function\r
++\r
++import os\r
++import re\r
++import threadpool\r
++import requests\r
++from bs4 import BeautifulSoup\r
++from tabulate import tabulate\r
++\r
++import nhentai.constant as constant\r
++from nhentai.logger import logger\r
++\r
++\r
++def request(method, url, **kwargs):\r
++ if not hasattr(requests, method):\r
++ raise AttributeError('\'requests\' object has no attribute \'{0}\''.format(method))\r
++\r
++ return requests.__dict__[method](url, proxies=constant.PROXY, verify=False, **kwargs)\r
++\r
++\r
++def login_parser(username, password):\r
++ s = requests.Session()\r
++ s.proxies = constant.PROXY\r
++ s.verify = False\r
++ s.headers.update({'Referer': constant.LOGIN_URL})\r
++\r
++ s.get(constant.LOGIN_URL)\r
++ content = s.get(constant.LOGIN_URL).content\r
++ html = BeautifulSoup(content, 'html.parser').encode("ascii")\r
++ csrf_token_elem = html.find('input', attrs={'name': 'csrfmiddlewaretoken'})\r
++\r
++ if not csrf_token_elem:\r
++ raise Exception('Cannot find csrf token to login')\r
++ csrf_token = csrf_token_elem.attrs['value']\r
++\r
++ login_dict = {\r
++ 'csrfmiddlewaretoken': csrf_token,\r
++ 'username_or_email': username,\r
++ 'password': password,\r
++ }\r
++ resp = s.post(constant.LOGIN_URL, data=login_dict)\r
++ if 'Invalid username (or email) or password' in resp.text:\r
++ logger.error('Login failed, please check your username and password')\r
++ exit(1)\r
++\r
++ html = BeautifulSoup(s.get(constant.FAV_URL).content, 'html.parser').encode("ascii")\r
++ count = html.find('span', attrs={'class': 'count'})\r
++ if not count:\r
++ logger.error('Cannot get count of your favorites, maybe login failed.')\r
++\r
++ count = int(count.text.strip('(').strip(')'))\r
++ if count == 0:\r
++ logger.warning('No favorites found')\r
++ return []\r
++ pages = int(count / 25)\r
++\r
++ if pages:\r
++ pages += 1 if count % (25 * pages) else 0\r
++ else:\r
++ pages = 1\r
++\r
++ logger.info('Your have %d favorites in %d pages.' % (count, pages))\r
++\r
++ if os.getenv('DEBUG'):\r
++ pages = 1\r
++\r
++ ret = []\r
++ doujinshi_id = re.compile('data-id="([\d]+)"')\r
++\r
++ def _callback(request, result):\r
++ ret.append(result)\r
++\r
++ thread_pool = threadpool.ThreadPool(5)\r
++\r
++ for page in range(1, pages+1):\r
++ try:\r
++ logger.info('Getting doujinshi id of page %d' % page)\r
++ resp = s.get(constant.FAV_URL + '?page=%d' % page).text\r
++ ids = doujinshi_id.findall(resp)\r
++ requests_ = threadpool.makeRequests(doujinshi_parser, ids, _callback)\r
++ [thread_pool.putRequest(req) for req in requests_]\r
++ thread_pool.wait()\r
++ except Exception as e:\r
++ logger.error('Error: %s, continue', str(e))\r
++\r
++ return ret\r
++\r
++\r
++def doujinshi_parser(id_):\r
++ if not isinstance(id_, (int,)) and (isinstance(id_, (str,)) and not id_.isdigit()):\r
++ raise Exception('Doujinshi id({0}) is not valid'.format(id_))\r
++\r
++ id_ = int(id_)\r
++ logger.log(15, 'Fetching doujinshi information of id {0}'.format(id_))\r
++ doujinshi = dict()\r
++ doujinshi['id'] = id_\r
++ url = '{0}/{1}'.format(constant.DETAIL_URL, id_)\r
++\r
++ try:\r
++ response = request('get', url).json()\r
++ except Exception as e:\r
++ logger.critical(str(e))\r
++ exit(1)\r
++\r
++ doujinshi['name'] = response['title']['english']\r
++ doujinshi['subtitle'] = response['title']['japanese']\r
++ doujinshi['img_id'] = response['media_id']\r
++ doujinshi['ext'] = ''.join(map(lambda s: s['t'], response['images']['pages']))\r
++ doujinshi['pages'] = len(response['images']['pages'])\r
++\r
++ # gain information of the doujinshi\r
++ needed_fields = ['character', 'artist', 'language']\r
++ for tag in response['tags']:\r
++ tag_type = tag['type']\r
++ if tag_type in needed_fields:\r
++ if tag_type not in doujinshi:\r
++ doujinshi[tag_type] = tag['name']\r
++ else:\r
++ doujinshi[tag_type] += tag['name']\r
++\r
++ return doujinshi\r
++\r
++\r
++def search_parser(keyword, page):\r
++ logger.debug('Searching doujinshis of keyword {0}'.format(keyword))\r
++ result = []\r
++ try:\r
++ response = request('get', url=constant.SEARCH_URL, params={'query': keyword, 'page': page}).json()\r
++ if 'result' not in response:\r
++ raise Exception('No result in response')\r
++ except requests.ConnectionError as e:\r
++ logger.critical(e)\r
++ logger.warn('If you are in China, please configure the proxy to fu*k GFW.')\r
++ exit(1)\r
++\r
++ for row in response['result']:\r
++ title = row['title']['english']\r
++ title = title[:85] + '..' if len(title) > 85 else title\r
++ result.append({'id': row['id'], 'title': title})\r
++\r
++ if not result:\r
++ logger.warn('Not found anything of keyword {}'.format(keyword))\r
++\r
++ return result\r
++\r
++\r
++def print_doujinshi(doujinshi_list):\r
++ if not doujinshi_list:\r
++ return\r
++ doujinshi_list = [(i['id'], i['title']) for i in doujinshi_list]\r
++ headers = ['id', 'doujinshi']\r
++ logger.info('Search Result\n' +\r
++ tabulate(tabular_data=doujinshi_list, headers=headers, tablefmt='rst'))\r
++\r
++\r
++if __name__ == '__main__':\r
++ print(doujinshi_parser("32271"))\r
--# coding: utf-8
--from __future__ import unicode_literals, print_function
--
--import os
--import string
-import zipfile
-import shutil
--from nhentai.logger import logger
--
--
--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
--
--
--def urlparse(url):
-- try:
-- from urlparse import urlparse
-- except ImportError:
-- from urllib.parse import urlparse
--
-- return urlparse(url)
-
-def readfile(path):
- loc = os.path.dirname(__file__)
--
- with open(os.path.join(loc, path), 'r') as file:
- return file.read()
--
--def generate_html(output_dir='.', doujinshi_obj=None):
-- image_html = ''
- previous = ''
--
-- if doujinshi_obj is not None:
-- doujinshi_dir = os.path.join(output_dir, format_filename('%s-%s' % (doujinshi_obj.id,
- doujinshi_obj.name[:200])))
- str(doujinshi_obj.name[:200]))))
-- else:
-- doujinshi_dir = '.'
--
-- file_list = os.listdir(doujinshi_dir)
-- file_list.sort()
--
- for index, image in enumerate(file_list):
- for image in file_list:
-- if not os.path.splitext(image)[1] in ('.jpg', '.png'):
-- continue
-
- try:
- next_ = file_list[file_list.index(image) + 1]
- except IndexError:
- next_ = ''
--
- image_html += '<img src="{0}" class="image-item {1}" attr-prev="{2}" attr-next="{3}">\n'\
- .format(image, 'current' if index == 0 else '', previous, next_)
- previous = image
- image_html += '<img src="{0}" class="image-item"/>\n'\
- .format(image)
--
- with open(os.path.join(os.path.dirname(__file__), 'doujinshi.html'), 'r') as template:
- html = template.read()
- html = readfile('viewer/index.html')
- css = readfile('viewer/styles.css')
- js = readfile('viewer/scripts.js')
--
-- if doujinshi_obj is not None:
-- title = doujinshi_obj.name
-- else:
-- title = 'nHentai HTML Viewer'
--
- data = html.format(TITLE=title, IMAGES=image_html)
- data = html.format(TITLE=title, IMAGES=image_html, SCRIPTS=js, STYLES=css)
-- with open(os.path.join(doujinshi_dir, 'index.html'), 'w') as f:
-- f.write(data)
--
-- logger.log(15, 'HTML Viewer has been write to \'{0}\''.format(os.path.join(doujinshi_dir, 'index.html')))
-
-
-def generate_cbz(output_dir='.', doujinshi_obj=None):
- if doujinshi_obj is not None:
- doujinshi_dir = os.path.join(output_dir, format_filename('%s-%s' % (doujinshi_obj.id,
- str(doujinshi_obj.name[:200]))))
- cbz_filename = os.path.join(output_dir, format_filename('%s-%s.cbz' % (doujinshi_obj.id,
- str(doujinshi_obj.name[:200]))))
- else:
- cbz_filename = './doujinshi.cbz'
- doujinshi_dir = '.'
-
- file_list = os.listdir(doujinshi_dir)
- file_list.sort()
-
- with zipfile.ZipFile(cbz_filename, 'w') as cbz_pf:
- for image in file_list:
- image_path = os.path.join(doujinshi_dir, image)
- cbz_pf.write(image_path, image)
-
- shutil.rmtree(doujinshi_dir, ignore_errors=True)
- logger.log(15, 'Comic Book CBZ file has been write to \'{0}\''.format(doujinshi_dir))
-
-
-
-
-
--
--
--def format_filename(s):
-- """Take a string and return a valid filename constructed from the string.
--Uses a whitelist approach: any characters not present in valid_chars are
--removed. Also spaces are replaced with underscores.
--
--Note: this method may produce invalid filenames such as ``, `.` or `..`
--When I use this method I prepend a date string like '2009_01_15_19_46_32_'
--and append a file extension like '.txt', so I avoid the potential of using
--an invalid filename.
--
--"""
-- valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
-- filename = ''.join(c for c in s if c in valid_chars)
-- filename = filename.replace(' ', '_') # I don't like spaces in filenames.
-- return filename
++# coding: utf-8\r
++from __future__ import unicode_literals, print_function\r
++\r
++import os\r
++import string\r
++import zipfile\r
++import shutil\r
++from nhentai.logger import logger\r
++\r
++\r
++class _Singleton(type):\r
++ """ A metaclass that creates a Singleton base class when called. """\r
++ _instances = {}\r
++\r
++ def __call__(cls, *args, **kwargs):\r
++ if cls not in cls._instances:\r
++ cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)\r
++ return cls._instances[cls]\r
++\r
++\r
++class Singleton(_Singleton(str('SingletonMeta'), (object,), {})):\r
++ pass\r
++\r
++\r
++def urlparse(url):\r
++ try:\r
++ from urlparse import urlparse\r
++ except ImportError:\r
++ from urllib.parse import urlparse\r
++\r
++ return urlparse(url)\r
++\r
++def readfile(path):\r
++ loc = os.path.dirname(__file__)\r
++\r
++ with open(os.path.join(loc, path), 'r') as file:\r
++ return file.read()\r
++\r
++def generate_html(output_dir='.', doujinshi_obj=None):\r
++ image_html = ''\r
++\r
++ if doujinshi_obj is not None:\r
++ doujinshi_dir = os.path.join(output_dir, format_filename('%s-%s' % (doujinshi_obj.id,\r
++ str(doujinshi_obj.name[:200]))))\r
++ else:\r
++ doujinshi_dir = '.'\r
++\r
++ file_list = os.listdir(doujinshi_dir)\r
++ file_list.sort()\r
++\r
++ for image in file_list:\r
++ if not os.path.splitext(image)[1] in ('.jpg', '.png'):\r
++ continue\r
++\r
++ image_html += '<img src="{0}" class="image-item"/>\n'\\r
++ .format(image)\r
++\r
++ html = readfile('viewer/index.html')\r
++ css = readfile('viewer/styles.css')\r
++ js = readfile('viewer/scripts.js')\r
++\r
++ if doujinshi_obj is not None:\r
++ title = doujinshi_obj.name\r
++ else:\r
++ title = 'nHentai HTML Viewer'\r
++\r
++ data = html.format(TITLE=title, IMAGES=image_html, SCRIPTS=js, STYLES=css)\r
++ with open(os.path.join(doujinshi_dir, 'index.html'), 'w') as f:\r
++ f.write(data)\r
++\r
++ logger.log(15, 'HTML Viewer has been write to \'{0}\''.format(os.path.join(doujinshi_dir, 'index.html')))\r
++\r
++\r
++def generate_cbz(output_dir='.', doujinshi_obj=None):\r
++ if doujinshi_obj is not None:\r
++ doujinshi_dir = os.path.join(output_dir, format_filename('%s-%s' % (doujinshi_obj.id,\r
++ str(doujinshi_obj.name[:200])))) \r
++ cbz_filename = os.path.join(output_dir, format_filename('%s-%s.cbz' % (doujinshi_obj.id,\r
++ str(doujinshi_obj.name[:200]))))\r
++ else:\r
++ cbz_filename = './doujinshi.cbz'\r
++ doujinshi_dir = '.'\r
++\r
++ file_list = os.listdir(doujinshi_dir)\r
++ file_list.sort()\r
++ \r
++ with zipfile.ZipFile(cbz_filename, 'w') as cbz_pf:\r
++ for image in file_list:\r
++ image_path = os.path.join(doujinshi_dir, image)\r
++ cbz_pf.write(image_path, image)\r
++ \r
++ shutil.rmtree(doujinshi_dir, ignore_errors=True)\r
++ logger.log(15, 'Comic Book CBZ file has been write to \'{0}\''.format(doujinshi_dir))\r
++\r
++\r
++\r
++\r
++\r
++\r
++\r
++def format_filename(s):\r
++ """Take a string and return a valid filename constructed from the string.\r
++Uses a whitelist approach: any characters not present in valid_chars are\r
++removed. Also spaces are replaced with underscores.\r
++\r
++Note: this method may produce invalid filenames such as ``, `.` or `..`\r
++When I use this method I prepend a date string like '2009_01_15_19_46_32_'\r
++and append a file extension like '.txt', so I avoid the potential of using\r
++an invalid filename.\r
++\r
++"""\r
++ valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)\r
++ filename = ''.join(c for c in s if c in valid_chars)\r
++ filename = filename.replace(' ', '_') # I don't like spaces in filenames.\r
++ return filename\r
--- /dev/null
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="UTF-8">
- <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>
++<!DOCTYPE html>\r
++<html>\r
++<head>\r
++ <meta charset="UTF-8">\r
++ <title>{TITLE}</title>\r
++ <style>\r
++{STYLES}\r
++ </style>\r
++</head>\r
++<body>\r
++\r
++<nav id="list">\r
++{IMAGES}</nav>\r
++\r
++<div id="image-container">\r
++ <span id="page-num"></span>\r
++ <div id="dest"></div>\r
++</div>\r
++\r
++<script>\r
++{SCRIPTS}\r
++</script>\r
++</body>\r
+ </html>
--- /dev/null
-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}")`;
-
- 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 'arrowleft':
- case 'a':
- changePage(currentPage - 1);
- break;
-
- // Next Image
- case ' ':
- case 'enter':
- case 'arrowright':
- case 'd':
- changePage(currentPage + 1);
- break;
- }
++const pages = Array.from(document.querySelectorAll('img.image-item'));\r
++let currentPage = 0;\r
++\r
++function changePage(pageNum) {\r
++ const previous = pages[currentPage];\r
++ const current = pages[pageNum];\r
++\r
++ if (current == null) {\r
++ return;\r
++ }\r
++ \r
++ previous.classList.remove('current');\r
++ current.classList.add('current');\r
++\r
++ currentPage = pageNum;\r
++\r
++ const display = document.getElementById('dest');\r
++ display.style.backgroundImage = `url("${current.src}")`;\r
++\r
++ document.getElementById('page-num')\r
++ .innerText = [\r
++ (pageNum + 1).toLocaleString(),\r
++ pages.length.toLocaleString()\r
++ ].join('\u200a/\u200a');\r
++}\r
++\r
++changePage(0);\r
++\r
++document.getElementById('list').onclick = event => {\r
++ if (pages.includes(event.target)) {\r
++ changePage(pages.indexOf(event.target));\r
++ }\r
++};\r
++\r
++document.getElementById('image-container').onclick = event => {\r
++ const width = document.getElementById('image-container').clientWidth;\r
++ const clickPos = event.clientX / width;\r
++\r
++ if (clickPos < 0.5) {\r
++ changePage(currentPage - 1);\r
++ } else {\r
++ changePage(currentPage + 1);\r
++ }\r
++};\r
++\r
++document.onkeypress = event => {\r
++ switch (event.key.toLowerCase()) {\r
++ // Previous Image\r
++ case 'arrowleft':\r
++ case 'a':\r
++ changePage(currentPage - 1);\r
++ break;\r
++\r
++ // Next Image\r
++ case ' ':\r
++ case 'enter':\r
++ case 'arrowright':\r
++ case 'd':\r
++ changePage(currentPage + 1);\r
++ break;\r
++ }\r
+ };
--- /dev/null
-*, *::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: 100%;
- overflow: auto;
- 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: 100vh;
- background: #222;
- color: #fff;
- text-align: center;
- cursor: pointer;
- -webkit-user-select: none;
- user-select: none;
- position: relative;
-}
-
-#image-container #dest {
- height: 100%;
- width: 100%;
- background-size: contain;
- background-repeat: no-repeat;
- background-position: center;
-}
-
-#image-container #page-num {
- position: absolute;
- font-size: 18pt;
- 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;
++*, *::after, *::before {\r
++ box-sizing: border-box;\r
++}\r
++\r
++img {\r
++ vertical-align: middle;\r
++}\r
++\r
++html, body {\r
++ display: flex;\r
++ background-color: #e8e6e6;\r
++ height: 100%;\r
++ width: 100%;\r
++ padding: 0;\r
++ margin: 0;\r
++ font-family: sans-serif;\r
++}\r
++\r
++#list {\r
++ height: 100%;\r
++ overflow: auto;\r
++ width: 260px;\r
++ text-align: center;\r
++}\r
++\r
++#list img {\r
++ width: 200px;\r
++ padding: 10px;\r
++ border-radius: 10px;\r
++ margin: 15px 0;\r
++ cursor: pointer;\r
++}\r
++\r
++#list img.current {\r
++ background: #0003;\r
++}\r
++\r
++#image-container {\r
++ flex: auto;\r
++ height: 100vh;\r
++ background: #222;\r
++ color: #fff;\r
++ text-align: center;\r
++ cursor: pointer;\r
++ -webkit-user-select: none;\r
++ user-select: none;\r
++ position: relative;\r
++}\r
++\r
++#image-container #dest {\r
++ height: 100%;\r
++ width: 100%;\r
++ background-size: contain;\r
++ background-repeat: no-repeat;\r
++ background-position: center;\r
++}\r
++\r
++#image-container #page-num {\r
++ position: absolute;\r
++ font-size: 18pt;\r
++ left: 10px;\r
++ bottom: 5px;\r
++ font-weight: bold;\r
++ opacity: 0.75;\r
++ text-shadow: /* Duplicate the same shadow to make it very strong */\r
++ 0 0 2px #222,\r
++ 0 0 2px #222,\r
++ 0 0 2px #222;\r
+ }