]> git.lizzy.rs Git - nhentai.git/commitdiff
Merge branch 'master' of github.com:RicterZ/nhentai
authorRicter Z <ricterzheng@gmail.com>
Sat, 11 Aug 2018 01:19:31 +0000 (09:19 +0800)
committerRicter Z <ricterzheng@gmail.com>
Sat, 11 Aug 2018 01:19:31 +0000 (09:19 +0800)
1  2 
MANIFEST.in
README.md
nhentai/__init__.py
nhentai/cmdline.py
nhentai/command.py
nhentai/parser.py
nhentai/utils.py
nhentai/viewer/index.html
nhentai/viewer/scripts.js
nhentai/viewer/styles.css

diff --cc MANIFEST.in
index edb93481d3a08835c7036842584eb9259832c79e,87eea1751c070a787ce7a4bc5ef8460183dcc34c..60ce4b827060db85cce989a64fa066d3cbfbdf1b
@@@ -1,3 -1,5 +1,5 @@@
--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
diff --cc README.md
index c7f7d8e9e6a3c54dcd2963f37ca441ffd4b699f2,e7d04d492a1acd712d77ea2f14436b09aeb29168..6b348f7e7d2f6b26382100c7eb1be5e23b9347b7
+++ b/README.md
@@@ -1,68 -1,70 +1,70 @@@
--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)
index 9acc19b37d9d190dbd6545549296bf6131d52da4,67105c6ab4f555408a92fb75cdcc05dff0e4a7cd..51111f21d4c993117c453c227b15b38453cbfc6e
@@@ -1,3 -1,3 +1,3 @@@
- __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
index 7995e4fe63809ac3ef33c60c0e8393205b299e8a,d3453d20eb953e19db4d716fb4161ed1cd282513..deea5b2f894a477cf9e91efd5b737b633d200f27
--# 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
index 840b076d11ac6f7701dd35040c5e5f25c7b92df1,5f08cef4f3cffb4e4138c9b02c9a37893fb368af..cfbf2ade42b6aae9da286ada0653f6d8f7f2a55e
@@@ -1,69 -1,72 +1,72 @@@
--#!/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
index bef57309a8a7ece4281d2931c254e9aad7fe2c4f,25e2d1e71e3f4d1b2a7d90a8acdb62909350af7a..f405f7be86c1375cbba60faed3e0f8ca80cb4611
--# 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
index 21e47f150864a89db101dbd229f4a1cd4e737498,3cddd501ad4ae0186ef02c2536cdc8d7c2c75479..d83ce0ffb478cd9f0afcb3cc9db093364d92836f
--# 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
index 0000000000000000000000000000000000000000,e7cb006a5e21a2297076943fe7e2dbfd4bb09418..6d344e3c83ea51a06e3fabd3a8c32bd4b16a9666
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,24 +1,24 @@@
 -<!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>
index 0000000000000000000000000000000000000000,721f45a97d44deca61180fc90be232243b863a86..09f976e3bc56094db8b4817deda49ec99da94d2b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,62 +1,62 @@@
 -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
+ };
index 0000000000000000000000000000000000000000,4de6fd92f1f0b16a76a235134016d177d44190af..f9830dc3a8f66da026f1a5194387dc0fbd0a5fa1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,69 +1,69 @@@
 -*, *::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
+ }