Compare commits

..

75 Commits

Author SHA1 Message Date
22cf2592dd 0.2.11 2018-03-16 23:48:58 +08:00
caa0753adb fix bug #13 2018-03-16 23:45:05 +08:00
0e14dd62d5 fix bug #13 2018-03-16 23:42:24 +08:00
7c9693785e fix #14 2018-03-16 23:39:04 +08:00
08ad73b683 fix bug #13 2018-03-16 23:33:16 +08:00
a56d3ca18c fix bug #13 2018-03-16 23:23:25 +08:00
c1975897d2 save downloaded doujinshi as doujinshi name #13 2018-03-16 23:16:26 +08:00
4ed596ff98 download user fav 2018-03-05 21:47:27 +08:00
debf287fb0 download user fav 2018-03-05 21:45:56 +08:00
308c5277b8 Merge pull request #12 from RomaniukVadim/master
Add install for Gentoo
2018-03-03 19:33:23 +08:00
b425c883c7 Add install for Gentoo 2018-03-02 17:18:22 +02:00
7bf9507bd2 0.2.10 2018-01-09 16:05:52 +08:00
5f5245f70f fix bug 2018-01-09 16:02:16 +08:00
45fb35b950 fix bug and add --html 2018-01-01 17:44:55 +08:00
2271b83d93 0.2.8 2017-08-19 00:50:38 +08:00
0ee000edeb sort #10 2017-08-19 00:48:53 +08:00
a47359f411 tiny bug 2017-07-06 15:41:33 +08:00
48c6fadc98 add viewer image 2017-06-18 16:48:54 +08:00
dbc834ea2e 0.2.7 2017-06-18 14:25:00 +08:00
71177ff94e 0.2.6 2017-06-18 14:19:28 +08:00
d1ed9b6980 add html doujinshi viewer 2017-06-18 14:19:07 +08:00
42a09e2c1e fix timeout 2017-03-17 20:19:40 +08:00
e306d50b7e fix bug 2017-03-17 20:14:42 +08:00
043f391d04 fix https error 2016-11-23 22:45:03 +08:00
9549c5f5a2 fix bug 2016-11-23 22:35:56 +08:00
5592b30be4 do not download 404 2016-11-23 22:11:47 +08:00
12f7b2225b 0.2.2 2016-10-19 22:23:02 +08:00
b0e71c9a6c remove windows 2016-10-19 22:16:56 +08:00
ad64a5685a try ... except for reload 2016-10-19 22:09:43 +08:00
6bd0a6b96a add unicode prefix 2016-10-19 22:08:18 +08:00
3a80c233d5 remove darwin 2016-10-19 22:03:38 +08:00
69e0d1d6f1 fix bug of unicode in optparse 2016-10-19 21:21:03 +08:00
c300a2777f Update README.md 2016-10-19 13:14:21 +08:00
d0d7fb7015 0.2.1 2016-10-19 13:06:27 +08:00
4ed91db60a remove emoji windows 2016-10-19 13:04:36 +08:00
4c11288d63 add test 2016-10-19 13:01:46 +08:00
de476aac46 qwq 2016-10-19 13:00:59 +08:00
a3fb75eb11 fix bug in logger 2016-10-19 12:55:09 +08:00
bb5024f1d7 logger on windows 2016-10-19 12:50:30 +08:00
da5b860e5f fix bug in python3 2016-10-19 12:20:14 +08:00
8b63d41cbb fix unicodecodeerror on windows 2016-10-19 12:18:42 +08:00
55d24883be update 2016-10-19 12:07:39 +08:00
8f3bdc73bf unicode literals 2016-10-19 11:07:48 +08:00
cc2f0521b3 urlparse for python3(再犯错我直播吃屎) 2016-10-17 21:56:53 +08:00
795e8b2bb8 urlparse for python3 2016-10-17 21:53:44 +08:00
97b2ba8fd2 urlparse for python3 2016-10-17 21:52:06 +08:00
6858bacd41 urlparse for python3 2016-10-17 21:50:07 +08:00
148b4a1a08 urlparse for python3 2016-10-17 21:48:21 +08:00
3ba8f62fe2 update test 2016-10-17 21:44:53 +08:00
16d3b555c9 update usages 2016-10-17 21:43:40 +08:00
0d185f465d 忘记干啥了.. 2016-10-17 21:26:58 +08:00
3eacd118ed change the way of download 2016-10-17 21:00:28 +08:00
e42f42d7db use https 2016-08-13 20:03:40 +08:00
fd0b53ee36 🐶 2016-08-11 22:37:06 +08:00
35fec2e1f4 take the warning of bs4 off 2016-08-11 22:32:34 +08:00
40e880cf77 modify test case 2016-08-11 22:26:03 +08:00
2f756ecb5b get nhentai url from env 2016-08-11 22:25:10 +08:00
441317c28c use nhentai mirror 2016-08-11 21:10:11 +08:00
8442f00c6c Merge pull request #5 from navrudh/master
Python3 compatablility and new Singleton implementation
2016-08-11 21:07:02 +08:00
43e59b724a Update .travis.yml 2016-08-10 20:05:15 +05:30
5d6a773460 bumping major version due to dependency changes 2016-08-10 16:12:28 +05:30
9fe43dc219 project is now Py3 and Py2 compatible 2016-08-10 16:11:52 +05:30
0f89ff4d63 implemented Py2 & Py3 compatible Singleton 2016-08-10 16:11:10 +05:30
5bb98aa007 Merge pull request #4 from navrudh/patch-1
Padding filenames with width 3
2016-08-10 15:21:43 +08:00
a4ac1c9720 Padding filenames with width 3
Pad the filenames with a width of 3 characters so that image viewers display files in the expected order.
2016-08-10 12:16:15 +05:30
8d25673180 nhentai origin 2016-07-03 17:06:22 +08:00
aab92bbc8e nhentai mirror 2016-07-03 17:02:47 +08:00
2b52e300d4 use nhentai.net as default domain 2016-05-20 09:54:23 +08:00
6e3299a08d pep8 2016-05-18 21:23:07 +08:00
e598c8686a modify constant.py 2016-05-18 21:22:27 +08:00
dd7b2d493e fix bug of return value 2016-05-02 16:03:28 +08:00
3d481dbf13 fix bug of setup 2016-05-02 15:57:17 +08:00
3a52e8a8bc support python2.6 2016-05-02 15:55:14 +08:00
bf8205efbe update README.md 2016-05-02 15:47:44 +08:00
e3980b0696 0.1.4 2016-05-02 15:44:23 +08:00
21 changed files with 585 additions and 152 deletions

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ build
dist/ dist/
*.egg-info *.egg-info
.python-version .python-version
.DS_Store

View File

@ -1,15 +1,17 @@
os: os:
- linux - linux
- os x
language: python language: python
python: python:
- 2.7 - 2.7
- 2.6 - 2.6
- 3.3
- 3.4
- 3.5.2
install: install:
- python setup.py install - python setup.py install
script: script:
- nhentai --search umaru - NHENTAI=https://nhentai.net nhentai --search umaru
- nhentai --ids=152503,146134 -t 10 --download --path=/tmp/ - NHENTAI=https://nhentai.net nhentai --id=152503,146134 -t 10 --output=/tmp/

View File

@ -1,2 +1,3 @@
include README.md include README.md
include requirements.txt include requirements.txt
include nhentai/doujinshi.html

View File

@ -7,45 +7,62 @@ 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) 下载下来的种子速度很慢,而且官方也提供在线观看本子的功能,所以可以利用本脚本下载本子。 由于 [http://nhentai.net](http://nhentai.net) 下载下来的种子速度很慢,而且官方也提供在线观看本子的功能,所以可以利用本脚本下载本子。
### 安装
### Installation
git clone https://github.com/RicterZ/nhentai git clone https://github.com/RicterZ/nhentai
cd nhentai cd nhentai
python setup.py install python setup.py install
### Gentoo
### 用法 layman -fa glicOne
+ 下载指定 id 的本子: sudo emerge net-misc/nhentai
### Usage
下载指定 id 列表的本子:
```bash
nhentai --id=123855,123866
```
nhentai --id=123855 --download 下载某关键词第一页的本子:
```bash
nhentai --search="tomori" --page=1 --download
```
下载用户 favorites 内容:
```bash
nhentai --login "username:password" --download
```
+ 下载指定 id 列表的本子: ### Options
`-t, --thread`:指定下载的线程数,最多为 10 线程。
`--path`:指定下载文件的输出路径,默认为当前目录。
`--timeout`:指定下载图片的超时时间,默认为 30 秒。
`--proxy`:指定下载的代理,例如: http://127.0.0.1:8080/
`--login`nhentai 账号的“用户名:密码”组合
nhentai --ids=123855,123866 --download ### nHentai Mirror
如果想用自建镜像下载 nhentai 的本子,需要搭建 nhentai.net 和 i.nhentai.net 的反向代理。
例如用 h.loli.club 来做反向代理的话,需要 h.loli.club 反代 nhentai.neti.h.loli.club 反带 i.nhentai.net。
+ 下载某关键词第一页的本子(不推荐): 然后利用环境变量来下载:
nhentai --search="tomori" --page=1 --download
`-t, --thread` 指定下载的线程数,最多为 10 线程。
`--path` 指定下载文件的输出路径,默认为当前目录。
`--timeout` 指定下载图片的超时时间,默认为 30 秒。
`--proxy` 指定下载的代理,例如: http://127.0.0.1:8080/
```bash
NHENTAI=http://h.loli.club nhentai --id 123456
```
![](./images/search.png) ![](./images/search.png)
![](./images/download.png) ![](./images/download.png)
![](./images/viewer.png)
### License ### License
MIT MIT
### あなたも変態 ### あなたも変態
![](./images/image.jpg) ![](./images/image.jpg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

After

Width:  |  Height:  |  Size: 189 KiB

0
images/image.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 173 KiB

BIN
images/viewer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

View File

@ -1,3 +1,3 @@
__version__ = '0.1.3' __version__ = '0.2.11'
__author__ = 'Ricter' __author__ = 'Ricter'
__email__ = 'ricterzheng@gmail.com' __email__ = 'ricterzheng@gmail.com'

View File

@ -1,18 +1,26 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function from __future__ import print_function
import sys
from optparse import OptionParser from optparse import OptionParser
from logger import logger
try: try:
from itertools import ifilter as filter from itertools import ifilter as filter
except ImportError: except ImportError:
pass pass
import nhentai.constant as constant
from nhentai.utils import urlparse, generate_html
from nhentai.logger import logger
import constant try:
reload(sys)
sys.setdefaultencoding(sys.stdin.encoding)
except NameError:
# python3
pass
def banner(): def banner():
logger.info('''nHentai: あなたも変態。 いいね? logger.info(u'''nHentai: あなたも変態。 いいね?
_ _ _ _ _ _ _ _
_ __ | | | | ___ _ __ | |_ __ _(_) _ __ | | | | ___ _ __ | |_ __ _(_)
| '_ \| |_| |/ _ \ '_ \| __/ _` | | | '_ \| |_| |/ _ \ '_ \| __/ _` | |
@ -22,51 +30,84 @@ def banner():
def cmd_parser(): def cmd_parser():
parser = OptionParser() parser = OptionParser('\n nhentai --search [keyword] --download'
parser.add_option('--download', dest='is_download', action='store_true', help='download doujinshi or not') '\n NHENTAI=http://h.loli.club nhentai --id [ID ...]'
parser.add_option('--id', type='int', dest='id', action='store', help='doujinshi id of nhentai') '\n\nEnvironment Variable:\n'
parser.add_option('--ids', type='str', dest='ids', action='store', help='doujinshi id set, e.g. 1,2,3') ' NHENTAI nhentai mirror url')
parser.add_option('--search', type='string', dest='keyword', action='store', help='keyword searched') 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, parser.add_option('--page', type='int', dest='page', action='store', default=1,
help='page number of search result') help='page number of search result')
parser.add_option('--path', type='string', dest='saved_path', action='store', default='', parser.add_option('--tags', type='string', dest='tags', action='store', help='download doujinshi by tags')
help='path which save the doujinshi') 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, parser.add_option('--threads', '-t', type='int', dest='threads', action='store', default=5,
help='thread count of download doujinshi') help='thread count of download doujinshi')
parser.add_option('--timeout', type='int', dest='timeout', action='store', default=30, parser.add_option('--timeout', type='int', dest='timeout', action='store', default=30,
help='timeout of download doujinshi') help='timeout of download doujinshi')
parser.add_option('--proxy', type='string', dest='proxy', action='store', default='', parser.add_option('--proxy', type='string', dest='proxy', action='store', default='',
help='use proxy, example: http://127.0.0.1:1080') help='use proxy, example: http://127.0.0.1:1080')
args, _ = parser.parse_args() parser.add_option('--html', dest='html_viewer', action='store_true',
help='generate a html viewer at current directory')
if args.ids: parser.add_option('--login', '-l', type='str', dest='login', action='store',
_ = map(lambda id: id.strip(), args.ids.split(',')) help='username:password pair of nhentai account')
args.ids = set(map(int, filter(lambda id: id.isdigit(), _)))
if args.is_download and not args.id and not args.ids and not args.keyword: try:
logger.critical('Doujinshi id/ids is required for downloading') sys.argv = list(map(lambda x: unicode(x.decode(sys.stdin.encoding)), sys.argv))
parser.print_help() except (NameError, TypeError):
raise SystemExit 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: if args.id:
args.ids = (args.id, ) if not args.ids else args.ids _ = map(lambda id: id.strip(), args.id.split(','))
args.id = set(map(int, filter(lambda id_: id_.isdigit(), _)))
if not args.keyword and not args.ids: 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() parser.print_help()
raise SystemExit exit(1)
if not args.keyword and not args.id and not args.login:
parser.print_help()
exit(1)
if args.threads <= 0: if args.threads <= 0:
args.threads = 1 args.threads = 1
elif args.threads > 10:
logger.critical('Maximum number of used threads is 10') elif args.threads > 15:
raise SystemExit logger.critical('Maximum number of used threads is 15')
exit(1)
if args.proxy: if args.proxy:
import urlparse proxy_url = urlparse(args.proxy)
proxy_url = urlparse.urlparse(args.proxy)
if proxy_url.scheme not in ('http', 'https'): if proxy_url.scheme not in ('http', 'https'):
logger.error('Invalid protocol \'{}\' of proxy, ignored'.format(proxy_url.scheme)) logger.error('Invalid protocol \'{0}\' of proxy, ignored'.format(proxy_url.scheme))
else: else:
constant.PROXY = {proxy_url.scheme: args.proxy} constant.PROXY = {'http': args.proxy, 'https': args.proxy}
return args return args

View File

@ -1,50 +1,66 @@
#!/usr/bin/env python2.7 #!/usr/bin/env python2.7
# coding: utf-8 # coding: utf-8
from __future__ import unicode_literals, print_function
import signal import signal
from cmdline import cmd_parser, banner import platform
from parser import doujinshi_parser, search_parser, print_doujinshi
from doujinshi import Doujinshi from nhentai.cmdline import cmd_parser, banner
from downloader import Downloader from nhentai.parser import doujinshi_parser, search_parser, print_doujinshi, login_parser
from logger import logger 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
def main(): def main():
banner() banner()
logger.info('Using mirror: {0}'.format(BASE_URL))
options = cmd_parser() options = cmd_parser()
doujinshi_ids = [] doujinshi_ids = []
doujinshi_list = [] 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: if options.keyword:
doujinshis = search_parser(options.keyword, options.page) doujinshis = search_parser(options.keyword, options.page)
print_doujinshi(doujinshis) print_doujinshi(doujinshis)
if options.is_download: if options.is_download:
doujinshi_ids = map(lambda d: d['id'], doujinshis) doujinshi_ids = map(lambda d: d['id'], doujinshis)
else: else:
doujinshi_ids = options.ids doujinshi_ids = options.id
if doujinshi_ids: if doujinshi_ids:
for id in doujinshi_ids: for id_ in doujinshi_ids:
doujinshi_info = doujinshi_parser(id) doujinshi_info = doujinshi_parser(id_)
doujinshi_list.append(Doujinshi(**doujinshi_info)) doujinshi_list.append(Doujinshi(**doujinshi_info))
else:
raise SystemExit
if options.is_download: if not options.is_show:
downloader = Downloader(path=options.saved_path, downloader = Downloader(path=options.output_dir,
thread=options.threads, timeout=options.timeout) thread=options.threads, timeout=options.timeout)
for doujinshi in doujinshi_list: for doujinshi in doujinshi_list:
doujinshi.downloader = downloader doujinshi.downloader = downloader
doujinshi.download() doujinshi.download()
else: generate_html(options.output_dir, doujinshi)
map(lambda doujinshi: doujinshi.show(), doujinshi_list)
logger.log(15, u'🍺 All done.') if not platform.system() == 'Windows':
logger.log(15, '🍺 All done.')
else:
logger.log(15, 'All done.')
else:
[doujinshi.show() for doujinshi in doujinshi_list]
def signal_handler(signal, frame): def signal_handler(signal, frame):
logger.error('Ctrl-C signal received. Quit.') logger.error('Ctrl-C signal received. Quit.')
raise SystemExit exit(1)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)

View File

@ -1,6 +1,16 @@
SCHEMA = 'http://' # coding: utf-8
URL = '%snhentai.net' % SCHEMA from __future__ import unicode_literals, print_function
DETAIL_URL = '%s/g' % URL import os
SEARCH_URL = '%s/search/' % URL from nhentai.utils import urlparse
IMAGE_URL = '%si.nhentai.net/galleries' % SCHEMA
BASE_URL = os.getenv('NHENTAI', 'https://nhentai.net')
DETAIL_URL = '%s/g' % BASE_URL
SEARCH_URL = '%s/search/' % BASE_URL
LOGIN_URL = '%s/login/' % BASE_URL
FAV_URL = '%s/favorites/' % BASE_URL
u = urlparse(BASE_URL)
IMAGE_URL = '%s://i.%s/galleries' % (u.scheme, u.hostname)
PROXY = {} PROXY = {}

126
nhentai/doujinshi.html Normal file
View File

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{TITLE}</title>
<style>
html, body {{
background-color: #e8e6e6;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}}
.container img {{
display: block;
width: 100%;
margin: 30px 0;
padding: 10px;
cursor: pointer;
}}
.container {{
height: 100%;
overflow: scroll;
background: #e8e6e6;
width: 200px;
padding: 30px;
float: left;
}}
.image {{
margin-left: 260px;
height: 100%;
background: #222;
text-align: center;
}}
.image img {{
height: 100%;
}}
.i a {{
display: block;
position: absolute;
top: 0;
width: 50%;
height: 100%;
}}
.i {{
position: relative;
height: 100%;
}}
.current {{
background: #BBB;
border-radius: 10px;
}}
</style>
<script>
function cursorfocus(elem) {{
var container = document.getElementsByClassName('container')[0];
container.scrollTop = elem.offsetTop - 500;
}}
function getImage(type) {{
var current = document.getElementsByClassName("current")[0];
current.className = "image-item";
var img_src = type == 1 ? current.getAttribute('attr-next') : current.getAttribute('attr-prev');
if (img_src === "") {{
img_src = current.src;
}}
var img_list = document.getElementsByClassName("image-item");
for (i=0; i<img_list.length; i++) {{
if (img_list[i].src.endsWith(img_src)) {{
img_list[i].className = "image-item current";
cursorfocus(img_list[i]);
break;
}}
}}
var display = document.getElementById("dest");
display.src = img_src;
display.focus();
}}
</script>
</head>
<body>
<div class="container">
{IMAGES}</div>
<div class="image">
<div class="i">
<img src="" id="dest">
<a href="javascript:getImage(-1)" style="left: 0;"></a>
<a href="javascript:getImage(1)" style="left: 50%;"></a>
</div>
</div>
</body>
<script>
var img_list = document.getElementsByClassName("image-item");
var display = document.getElementById("dest");
display.src = img_list[0].src;
for (var i = 0; i < img_list.length; i++) {{
img_list[i].addEventListener('click', function() {{
var current = document.getElementsByClassName("current")[0];
current.className = "image-item";
this.className = "image-item current";
var display = document.getElementById("dest");
display.src = this.src;
display.focus();
}}, false);
}}
document.onkeypress = function(e) {{
if (e.keyCode == 32) {{
getImage(1);
}}
}}
</script>
</html>

View File

@ -1,8 +1,11 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function from __future__ import print_function, unicode_literals
from tabulate import tabulate from tabulate import tabulate
from constant import DETAIL_URL, IMAGE_URL from future.builtins import range
from logger import logger
from nhentai.constant import DETAIL_URL, IMAGE_URL
from nhentai.logger import logger
from nhentai.utils import format_filename
class DoujinshiInfo(dict): class DoujinshiInfo(dict):
@ -28,7 +31,7 @@ class Doujinshi(object):
self.info = DoujinshiInfo(**kwargs) self.info = DoujinshiInfo(**kwargs)
def __repr__(self): def __repr__(self):
return '<Doujinshi: {}>'.format(self.name) return '<Doujinshi: {0}>'.format(self.name)
def show(self): def show(self):
table = [ table = [
@ -41,15 +44,15 @@ class Doujinshi(object):
["URL", self.url], ["URL", self.url],
["Pages", self.pages], ["Pages", self.pages],
] ]
logger.info(u'Print doujinshi information\n{}'.format(tabulate(table))) logger.info(u'Print doujinshi information of {0}\n{1}'.format(self.id, tabulate(table)))
def download(self): def download(self):
logger.info('Start download doujinshi: %s' % self.name) logger.info('Start download doujinshi: %s' % self.name)
if self.downloader: if self.downloader:
download_queue = [] download_queue = []
for i in xrange(1, self.pages + 1): for i in range(1, self.pages + 1):
download_queue.append('%s/%d/%d.%s' % (IMAGE_URL, int(self.img_id), i, self.ext)) download_queue.append('%s/%d/%d.%s' % (IMAGE_URL, int(self.img_id), i, self.ext))
self.downloader.download(download_queue, self.id) self.downloader.download(download_queue, format_filename('%s-%s' % (self.id, self.name[:200])))
else: else:
logger.critical('Downloader has not be loaded') logger.critical('Downloader has not be loaded')

View File

@ -1,73 +1,95 @@
# coding: utf-8 # coding: utf-
from __future__ import unicode_literals, print_function
from future.builtins import str as text
import os import os
import requests import requests
import threadpool import threadpool
from urlparse import urlparse try:
from logger import logger from urllib.parse import urlparse
from parser import request except ImportError:
from urlparse import urlparse
from nhentai.logger import logger
from nhentai.parser import request
from nhentai.utils import Singleton
class Downloader(object): requests.packages.urllib3.disable_warnings()
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance: class NhentaiImageNotExistException(Exception):
cls._instance = super(Downloader, cls).__new__(cls, *args, **kwargs) pass
return cls._instance
class Downloader(Singleton):
def __init__(self, path='', thread=1, timeout=30): def __init__(self, path='', thread=1, timeout=30):
if not isinstance(thread, (int, )) or thread < 1 or thread > 10: if not isinstance(thread, (int, )) or thread < 1 or thread > 15:
raise ValueError('Invalid threads count') raise ValueError('Invalid threads count')
self.path = str(path) self.path = str(path)
self.thread_count = thread self.thread_count = thread
self.threads = [] self.threads = []
self.timeout = timeout self.timeout = timeout
def _download(self, url, folder='', filename='', retried=False): def _download(self, url, folder='', filename='', retried=0):
logger.info('Start downloading: {} ...'.format(url)) logger.info('Start downloading: {0} ...'.format(url))
filename = filename if filename else os.path.basename(urlparse(url).path) filename = filename if filename else os.path.basename(urlparse(url).path)
base_filename, extension = os.path.splitext(filename)
try: try:
with open(os.path.join(folder, filename), "wb") as f: with open(os.path.join(folder, base_filename.zfill(3) + extension), "wb") as f:
response = request('get', url, stream=True, timeout=self.timeout) response = request('get', url, stream=True, timeout=self.timeout)
if response.status_code != 200:
raise NhentaiImageNotExistException
length = response.headers.get('content-length') length = response.headers.get('content-length')
if length is None: if length is None:
f.write(response.content) f.write(response.content)
else: else:
for chunk in response.iter_content(2048): for chunk in response.iter_content(2048):
f.write(chunk) f.write(chunk)
except requests.HTTPError as e:
if not retried: except (requests.HTTPError, requests.Timeout) as e:
logger.error('Error: {}, retrying'.format(str(e))) if retried < 3:
return self._download(url=url, folder=folder, filename=filename, retried=True) logger.warning('Warning: {0}, retrying({1}) ...'.format(str(e), retried))
return 0, self._download(url=url, folder=folder, filename=filename, retried=retried+1)
else: else:
return None return 0, None
except NhentaiImageNotExistException as e:
os.remove(os.path.join(folder, base_filename.zfill(3) + extension))
return -1, url
except Exception as e: except Exception as e:
logger.critical(str(e)) logger.critical(str(e))
return None return 0, None
return url
return 1, url
def _download_callback(self, request, result): def _download_callback(self, request, result):
if not result: result, data = result
logger.critical('Too many errors occurred, quit.') if result == 0:
raise SystemExit logger.warning('fatal errors occurred, ignored')
logger.log(15, '{} download successfully'.format(result)) # exit(1)
elif result == -1:
logger.warning('url {} return status code 404'.format(data))
else:
logger.log(15, '{0} download successfully'.format(data))
def download(self, queue, folder=''): def download(self, queue, folder=''):
if not isinstance(folder, (str, unicode)): if not isinstance(folder, (text)):
folder = str(folder) folder = str(folder)
if self.path: if self.path:
folder = os.path.join(self.path, folder) folder = os.path.join(self.path, folder)
if not os.path.exists(folder): if not os.path.exists(folder):
logger.warn('Path \'{}\' not exist.'.format(folder)) logger.warn('Path \'{0}\' not exist.'.format(folder))
try: try:
os.makedirs(folder) os.makedirs(folder)
except EnvironmentError as e: except EnvironmentError as e:
logger.critical('Error: {}'.format(str(e))) logger.critical('{0}'.format(str(e)))
raise SystemExit exit(1)
else: else:
logger.warn('Path \'{}\' already exist.'.format(folder)) logger.warn('Path \'{0}\' already exist.'.format(folder))
queue = [([url], {'folder': folder}) for url in queue] queue = [([url], {'folder': folder}) for url in queue]

View File

@ -1,13 +1,24 @@
import logging
# #
# Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. Licensed under the new BSD license. # Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. Licensed under the new BSD license.
# #
from __future__ import print_function, unicode_literals
import logging import logging
import os
import re import re
import platform
import sys 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): class ColorizingStreamHandler(logging.StreamHandler):
# color names to indices # color names to indices
color_map = { color_map = {
@ -22,22 +33,13 @@ class ColorizingStreamHandler(logging.StreamHandler):
} }
# levels to (background, foreground, bold/intense) # levels to (background, foreground, bold/intense)
if os.name == 'nt': level_map = {
level_map = { logging.DEBUG: (None, 'blue', False),
logging.DEBUG: (None, 'white', False), logging.INFO: (None, 'green', False),
logging.INFO: (None, 'green', False), logging.WARNING: (None, 'yellow', False),
logging.WARNING: (None, 'yellow', False), logging.ERROR: (None, 'red', False),
logging.ERROR: (None, 'red', False), logging.CRITICAL: ('red', 'white', False)
logging.CRITICAL: ('red', 'white', False) }
}
else:
level_map = {
logging.DEBUG: (None, 'white', False),
logging.INFO: (None, 'green', False),
logging.WARNING: (None, 'yellow', False),
logging.ERROR: (None, 'red', False),
logging.CRITICAL: ('red', 'white', False)
}
csi = '\x1b[' csi = '\x1b['
reset = '\x1b[0m' reset = '\x1b[0m'
disable_coloring = False disable_coloring = False
@ -47,7 +49,29 @@ class ColorizingStreamHandler(logging.StreamHandler):
isatty = getattr(self.stream, 'isatty', None) isatty = getattr(self.stream, 'isatty', None)
return isatty and isatty() and not self.disable_coloring return isatty and isatty() and not self.disable_coloring
if os.name != 'nt': 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): def output_colorized(self, message):
self.stream.write(message) self.stream.write(message)
else: else:
@ -65,8 +89,6 @@ class ColorizingStreamHandler(logging.StreamHandler):
} }
def output_colorized(self, message): def output_colorized(self, message):
import ctypes
parts = self.ansi_esc.split(message) parts = self.ansi_esc.split(message)
write = self.stream.write write = self.stream.write
h = None h = None
@ -75,7 +97,7 @@ class ColorizingStreamHandler(logging.StreamHandler):
if fd is not None: if fd is not None:
fd = fd() fd = fd()
if fd in (1, 2): # stdout or stderr if fd in (1, 2): # stdout or stderr
h = ctypes.windll.kernel32.GetStdHandle(-10 - fd) h = ctypes.windll.kernel32.GetStdHandle(-10 - fd)
while parts: while parts:
@ -97,11 +119,11 @@ class ColorizingStreamHandler(logging.StreamHandler):
elif 30 <= p <= 37: elif 30 <= p <= 37:
color |= self.nt_color_map[p - 30] color |= self.nt_color_map[p - 30]
elif p == 1: elif p == 1:
color |= 0x08 # foreground intensity on color |= 0x08 # foreground intensity on
elif p == 0: # reset to default color elif p == 0: # reset to default color
color = 0x07 color = 0x07
else: else:
pass # error condition ignored pass # error condition ignored
ctypes.windll.kernel32.SetConsoleTextAttribute(h, color) ctypes.windll.kernel32.SetConsoleTextAttribute(h, color)
@ -135,6 +157,7 @@ class ColorizingStreamHandler(logging.StreamHandler):
message = logging.StreamHandler.format(self, record) message = logging.StreamHandler.format(self, record)
return self.colorize(message, record) return self.colorize(message, record)
logging.addLevelName(15, "INFO") logging.addLevelName(15, "INFO")
logger = logging.getLogger('nhentai') logger = logging.getLogger('nhentai')
LOGGER_HANDLER = ColorizingStreamHandler(sys.stdout) LOGGER_HANDLER = ColorizingStreamHandler(sys.stdout)

View File

@ -1,38 +1,101 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function from __future__ import unicode_literals, print_function
import sys
import os
import re import re
import threadpool
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import constant
from logger import logger
from tabulate import tabulate from tabulate import tabulate
import nhentai.constant as constant
from nhentai.logger import logger
def request(method, url, **kwargs): def request(method, url, **kwargs):
if not hasattr(requests, method): if not hasattr(requests, method):
raise AttributeError('\'requests\' object has no attribute \'{}\''.format(method)) raise AttributeError('\'requests\' object has no attribute \'{0}\''.format(method))
return requests.__dict__[method](url, proxies=constant.PROXY, **kwargs) 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')
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')
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(')'))
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).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_): def doujinshi_parser(id_):
if not isinstance(id_, (int,)) and (isinstance(id_, (str,)) and not id_.isdigit()): if not isinstance(id_, (int,)) and (isinstance(id_, (str,)) and not id_.isdigit()):
raise Exception('Doujinshi id({}) is not valid'.format(id_)) raise Exception('Doujinshi id({0}) is not valid'.format(id_))
id_ = int(id_) id_ = int(id_)
logger.log(15, 'Fetching doujinshi information of id {}'.format(id_)) logger.log(15, 'Fetching doujinshi information of id {0}'.format(id_))
doujinshi = dict() doujinshi = dict()
doujinshi['id'] = id_ doujinshi['id'] = id_
url = '{}/{}/'.format(constant.DETAIL_URL, id_) url = '{0}/{1}/'.format(constant.DETAIL_URL, id_)
try: try:
response = request('get', url).content response = request('get', url).content
except Exception as e: except Exception as e:
logger.critical(str(e)) logger.critical(str(e))
sys.exit() exit(1)
html = BeautifulSoup(response) html = BeautifulSoup(response, 'html.parser')
doujinshi_info = html.find('div', attrs={'id': 'info'}) doujinshi_info = html.find('div', attrs={'id': 'info'})
title = doujinshi_info.find('h1').text title = doujinshi_info.find('h1').text
@ -42,10 +105,11 @@ def doujinshi_parser(id_):
doujinshi['subtitle'] = subtitle.text if subtitle else '' doujinshi['subtitle'] = subtitle.text if subtitle else ''
doujinshi_cover = html.find('div', attrs={'id': 'cover'}) doujinshi_cover = html.find('div', attrs={'id': 'cover'})
img_id = re.search('/galleries/([\d]+)/cover\.(jpg|png)$', doujinshi_cover.a.img['src']) img_id = re.search('/galleries/([\d]+)/cover\.(jpg|png)$', doujinshi_cover.a.img.attrs['data-src'])
if not img_id: if not img_id:
logger.critical('Tried yo get image id failed') logger.critical('Tried yo get image id failed')
sys.exit() exit(1)
doujinshi['img_id'] = img_id.group(1) doujinshi['img_id'] = img_id.group(1)
doujinshi['ext'] = img_id.group(2) doujinshi['ext'] = img_id.group(2)
@ -71,16 +135,16 @@ def doujinshi_parser(id_):
def search_parser(keyword, page): def search_parser(keyword, page):
logger.debug('Searching doujinshis of keyword {}'.format(keyword)) logger.debug('Searching doujinshis of keyword {0}'.format(keyword))
result = [] result = []
try: try:
response = request('get', url=constant.SEARCH_URL, params={'q': keyword, 'page': page}).content response = request('get', url=constant.SEARCH_URL, params={'q': keyword, 'page': page}).content
except requests.ConnectionError as e: except requests.ConnectionError as e:
logger.critical(e) logger.critical(e)
logger.warn('If you are in China, please configure the proxy to fu*k GFW.') logger.warn('If you are in China, please configure the proxy to fu*k GFW.')
raise SystemExit exit(1)
html = BeautifulSoup(response) html = BeautifulSoup(response, 'html.parser')
doujinshi_search_result = html.find_all('div', attrs={'class': 'gallery'}) doujinshi_search_result = html.find_all('div', attrs={'class': 'gallery'})
for doujinshi in doujinshi_search_result: for doujinshi in doujinshi_search_result:
doujinshi_container = doujinshi.find('div', attrs={'class': 'caption'}) doujinshi_container = doujinshi.find('div', attrs={'class': 'caption'})
@ -88,16 +152,20 @@ def search_parser(keyword, page):
title = (title[:85] + '..') if len(title) > 85 else title title = (title[:85] + '..') if len(title) > 85 else title
id_ = re.search('/g/(\d+)/', doujinshi.a['href']).group(1) id_ = re.search('/g/(\d+)/', doujinshi.a['href']).group(1)
result.append({'id': id_, 'title': title}) result.append({'id': id_, 'title': title})
if not result:
logger.warn('Not found anything of keyword {}'.format(keyword))
return result return result
def print_doujinshi(doujinshi_list): def print_doujinshi(doujinshi_list):
if not doujinshi_list: if not doujinshi_list:
return return
doujinshi_list = [i.values() for i in doujinshi_list] doujinshi_list = [(i['id'], i['title']) for i in doujinshi_list]
headers = ['id', 'doujinshi'] headers = ['id', 'doujinshi']
logger.info('Search Result\n' + logger.info('Search Result\n' +
tabulate(tabular_data=doujinshi_list, headers=headers, tablefmt='rst')) tabulate(tabular_data=doujinshi_list, headers=headers, tablefmt='rst'))
if __name__ == '__main__': if __name__ == '__main__':
print(doujinshi_parser("32271")) print(doujinshi_parser("32271"))

87
nhentai/utils.py Normal file
View File

@ -0,0 +1,87 @@
# coding: utf-8
from __future__ import unicode_literals, print_function
import os
import string
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 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])))
else:
doujinshi_dir = '.'
file_list = os.listdir(doujinshi_dir)
file_list.sort()
for index, image in enumerate(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
with open(os.path.join(os.path.dirname(__file__), 'doujinshi.html'), 'r') as template:
html = template.read()
if doujinshi_obj is not None:
title = doujinshi_obj.name
else:
title = 'nHentai HTML Viewer'
data = html.format(TITLE=title, IMAGES=image_html)
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 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

View File

@ -2,3 +2,4 @@ requests>=2.5.0
BeautifulSoup4>=4.0.0 BeautifulSoup4>=4.0.0
threadpool>=1.2.7 threadpool>=1.2.7
tabulate>=0.7.5 tabulate>=0.7.5
future>=0.15.2

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[metadata]
description-file = README.md

View File

@ -1,9 +1,20 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import sys
import codecs
from setuptools import setup, find_packages from setuptools import setup, find_packages
from nhentai import __version__, __author__, __email__ from nhentai import __version__, __author__, __email__
with open('requirements.txt') as f: with open('requirements.txt') as f:
requirements = [l for l in f.read().splitlines() if l] requirements = [l for l in f.read().splitlines() if l]
def long_description():
with codecs.open('README.md', 'rb') as f:
if sys.version_info >= (3, 0, 0):
return str(f.read())
setup( setup(
name='nhentai', name='nhentai',
version=__version__, version=__version__,
@ -13,7 +24,9 @@ setup(
author_email=__email__, author_email=__email__,
keywords='nhentai, doujinshi', keywords='nhentai, doujinshi',
description='nhentai.net doujinshis downloader', description='nhentai.net doujinshis downloader',
long_description=long_description(),
url='https://github.com/RicterZ/nhentai', url='https://github.com/RicterZ/nhentai',
download_url='https://github.com/RicterZ/nhentai/tarball/master',
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,