├── .gitignore ├── .idea ├── Wenku8ToEpub-Online.iml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── LICENSE ├── NovelList.txt ├── Procfile ├── Procfile.windows ├── README.md ├── __pycache__ ├── base_logger.cpython-36.pyc ├── database.cpython-36.pyc ├── error_report.cpython-36.pyc ├── manager.cpython-36.pyc └── wenku8toepub.cpython-36.pyc ├── app.json ├── async_test.py ├── base_logger.py ├── database.py ├── dmzj2epub.py ├── dmzj_novel_data.json ├── dmzj_novel_data_full.json ├── ebooklib ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-36.pyc │ ├── epub.cpython-36.pyc │ └── utils.cpython-36.pyc ├── epub.py ├── plugins │ ├── __init__.py │ ├── base.py │ ├── booktype.py │ ├── sourcecode.py │ ├── standard.py │ └── tidyhtml.py └── utils.py ├── error_report.py ├── errors.txt ├── headers.txt ├── images ├── 1.jpg ├── 2.jpg └── 3.png ├── make.bat ├── manage.py ├── manager.py ├── opds ├── .gitignore ├── Config.py ├── Const.py ├── Procfile ├── Procfile.windows ├── README.MD ├── app.json ├── config.yaml ├── filesystem.py ├── generate.py ├── index.wsgi ├── metadata.json ├── opdscore.py ├── opdsserver.py ├── requirements.txt ├── runtime.txt ├── static │ ├── book.png │ ├── bookdetail.xsl │ ├── booklist.xsl │ ├── bootstrap-responsive.min.css │ ├── bootstrap.min.css │ ├── jquery.min.js │ ├── logo.png │ └── test.html ├── test_mine.py └── utils.py ├── progress.txt ├── refresh.py ├── requirements.txt ├── restart.sh ├── runtime.txt ├── server.py ├── static ├── board.json ├── extra.js ├── favicon.ico ├── theme.js └── wenku8.js ├── templates ├── forms.html └── index.html ├── wenku8toepub.bkp.py ├── wenku8toepub.py ├── wk8local.py └── xiaoice.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # test module 个人习惯,测试目录去掉 10 | # /test/ 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | .idea/ 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | .dmypy.json 117 | dmypy.json 118 | 119 | # Pyre type checker 120 | .pyre/ 121 | 122 | # downloaded 123 | static/*.epub -------------------------------------------------------------------------------- /.idea/Wenku8ToEpub-Online.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LanceLiang2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python server.py 2 | -------------------------------------------------------------------------------- /Procfile.windows: -------------------------------------------------------------------------------- 1 | web: python server.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 使用的模块的说明 2 | 3 | ``` 4 | 把www.wenku8.net的轻小说在线转换成epub格式。wenku8.net没有版权的小说则下载TXT文件然后转换为epub文件。 5 | 6 | wk2epub [-h] [-t] [-m] [-b] [list] 7 | 8 | list 一个数字列表,中间用空格隔开 9 | 10 | -t 只获取文字,忽略图片。 11 | 但是图像远程连接仍然保留在文中。 12 | 此开关默认关闭,即默认获取图片。 13 | 14 | -m 多线程模式。 15 | 该开关已默认打开。 16 | 17 | -i 显示该书信息。 18 | 19 | -b 把生成的epub文件直接从stdio返回。 20 | 此时list长度应为1。 21 | 调试用。 22 | 23 | -h 显示本帮助。 24 | 25 | 调用示例: 26 | wk2epub -t 1 1213 27 | 28 | 关于: 29 | https://github.com/LanceLiang2018/Wenku8ToEpub 30 | 31 | 版本: 32 | 2020/3/8 1:45 AM 33 | ``` 34 | 35 | ### 文件下载方式 36 | 37 | #### 方式1 38 | 39 | [书名形式](https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/小说标题.epub) 40 | 41 | https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/{{小说标题}}.epub 42 | 43 | 小说标题以显示在wenku8网站上的为准,例如 44 | 45 | TIGER×DRAGON!(龙与虎) 46 | 47 | 示例: 48 | 49 | [文学少女](https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/文学少女.epub) 50 | 51 | #### 方式2 52 | 53 | ~~[ID形式](https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/小说ID.html)~~(废弃。) 54 | 55 | 注意等待静态HTML跳转 56 | 57 | ## 更新:服务器版 58 | 59 | - 从缓存中获取。存在此书则直接重定向到下载链接。 60 | 61 | https://wenku8.herokuapp.com/get/书本id 62 | 63 | - 更新CDN缓存。更新完成后就会重定向到下载链接,请耐心等候。小书5s,大书30s以上。(Heroku在30s没有响应时会报错。) 64 | 65 | https://wenku8.herokuapp.com/cache/书本id 66 | 67 | - 直接获取下载。获取最新的章节,但是不更新CDN。这个链接可以获取图片。好吧这个速度够慢的... 68 | 69 | https://wenku8.herokuapp.com/no_cache/书本id 70 | 71 | ## 更新:美化UI和功能 72 | 73 | [主站](http://wenku8.herokuapp.com):http://wenku8.herokuapp.com 74 | 75 | 免费托管于heroku。可以https,但是会造成iframe引用源站的图片没法显示。 76 | 77 | 在这里提出issues或者在网站内反馈。 78 | 79 | **TODO**: 80 | 81 | - [x] MDUI配置 82 | - [x] 书籍信息显示 83 | - [x] 下载过程实时反馈 84 | - [x] 防止内存泄露 85 | 86 | **效果展示** 87 | 88 | ![图片1](https://github.com/LanceLiang2018/Wenku8ToEpub-Online/raw/master/images/1.jpg) 89 | 90 | ![图片2](https://github.com/LanceLiang2018/Wenku8ToEpub-Online/raw/master/images/2.jpg) 91 | 92 | ![图片3](https://github.com/LanceLiang2018/Wenku8ToEpub-Online/raw/master/images/3.png) -------------------------------------------------------------------------------- /__pycache__/base_logger.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/__pycache__/base_logger.cpython-36.pyc -------------------------------------------------------------------------------- /__pycache__/database.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/__pycache__/database.cpython-36.pyc -------------------------------------------------------------------------------- /__pycache__/error_report.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/__pycache__/error_report.cpython-36.pyc -------------------------------------------------------------------------------- /__pycache__/manager.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/__pycache__/manager.cpython-36.pyc -------------------------------------------------------------------------------- /__pycache__/wenku8toepub.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/__pycache__/wenku8toepub.cpython-36.pyc -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Start on Heroku: Python", 3 | "description": "A barebones Python app, which can easily be deployed to Heroku.", 4 | "image": "heroku/python", 5 | "repository": "https://github.com/heroku/python-getting-started", 6 | "keywords": ["python", "django" ], 7 | "addons": [ "heroku-postgresql" ], 8 | "env": { 9 | "SECRET_KEY": { 10 | "description": "The secret key for the Django application.", 11 | "generator": "secret" 12 | } 13 | }, 14 | "environments": { 15 | "test": { 16 | "scripts": { 17 | "test-setup": "python manage.py collectstatic --noinput", 18 | "test": "python manage.py test" 19 | } 20 | } 21 | }, 22 | "stack": "heroku-22" 23 | } 24 | -------------------------------------------------------------------------------- /async_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import json 4 | 5 | 6 | async def chapter(bid, vid, sem): 7 | async with sem: 8 | t = 1 + random.random() 9 | await asyncio.sleep(t) 10 | print('done', t) 11 | return ('%s %s' % (bid, vid)).encode() 12 | 13 | 14 | async def volume(vid, chapters, sem_limit=10): 15 | sem = asyncio.Semaphore(sem_limit) 16 | chapter_data = [None for _ in range(len(chapters))] 17 | for i in range(len(chapters)): 18 | chapter_data[i] = chapter(chapters[i]['chapter_id'], vid, sem) 19 | res = await asyncio.wait(chapter_data) 20 | print(list(res[0])[0].result()) 21 | return chapter_data 22 | 23 | 24 | if __name__ == '__main__': 25 | test_js = '[{"volume_id":10728,"id":10728,"volume_name":"\u7b2c\u4e00\u5377","volume_order":10,"chapters":[{"chapter_id":104897,"chapter_name":"\u8f6c\u8f7d\u4fe1\u606f","chapter_order":1},{"chapter_id":104892,"chapter_name":"\u7b2c1\u8bdd \u4e24\u540dJK","chapter_order":10},{"chapter_id":104893,"chapter_name":"\u7b2c2\u8bdd \u5c31\u5bdd\u524d\u7684JK","chapter_order":20},{"chapter_id":104894,"chapter_name":"\u7b2c3\u8bdd \u5bb6\u52a1\u4e0eJK","chapter_order":30},{"chapter_id":104895,"chapter_name":"\u7b2c4\u8bdd \u8d2d\u7269\u4e0eJK","chapter_order":40},{"chapter_id":104896,"chapter_name":"\u7b2c5\u8bdd \u7535\u8111\u4e0eJK","chapter_order":50},{"chapter_id":104898,"chapter_name":"\u7b2c6\u8bdd \u610f\u5916\u4e0eJK","chapter_order":60},{"chapter_id":104899,"chapter_name":"\u7b2c7\u8bdd \u8840\u7f18\u4e0eJK","chapter_order":70},{"chapter_id":104900,"chapter_name":"\u7b2c8\u8bdd \u4f11\u606f\u65f6\u95f4\u4e0eJK","chapter_order":80},{"chapter_id":104901,"chapter_name":"\u7b2c9\u8bdd \u540d\u5b57\u4e0eJK","chapter_order":90},{"chapter_id":104902,"chapter_name":"\u7b2c10\u8bdd \u98df\u5802\u4e0e\u6211","chapter_order":100},{"chapter_id":104903,"chapter_name":"\u7b2c11\u8bdd \u517c\u804c\u4e0eJK","chapter_order":110},{"chapter_id":104904,"chapter_name":"\u7b2c12\u8bdd \u9752\u6885\u7af9\u9a6c\u4e0e\u6211","chapter_order":120},{"chapter_id":104905,"chapter_name":"\u7b2c13\u8bdd \u4f11\u606f\u65f6\u95f4\u4e0eJK\u2461","chapter_order":130},{"chapter_id":104906,"chapter_name":"\u7b2c14\u8bdd \u611f\u5192\u4e0eJK","chapter_order":140},{"chapter_id":104907,"chapter_name":"\u7b2c15\u8bdd \u88ad\u51fb\u4e0eJK","chapter_order":150},{"chapter_id":104908,"chapter_name":"\u7b2c16\u8bdd \u714e\u86cb\u4e0eJK","chapter_order":160},{"chapter_id":104909,"chapter_name":"\u540e\u8bb0","chapter_order":170},{"chapter_id":104910,"chapter_name":"\u63d2\u753b","chapter_order":180}]}]' 26 | test_data = json.loads(test_js) 27 | # volume_data = [None for _ in range(len(test_data))] 28 | # sem = None 29 | for i in range(len(test_data)): 30 | v = test_data[i] 31 | # volume_data[i] = volume(v['volume_id'], v['chapters'], sem) 32 | # asyncio.run(volume_data[i]) 33 | asyncio.run(volume(v['volume_id'], v['chapters'])) 34 | -------------------------------------------------------------------------------- /base_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from colorlog import ColoredFormatter 3 | 4 | 5 | def getLogger(name=__name__): 6 | logger_base = logging.getLogger(name) 7 | logger_base.setLevel(logging.DEBUG) 8 | stream_handler = logging.StreamHandler() 9 | 10 | color_formatter = ColoredFormatter('%(log_color)s[%(module)-15s][%(funcName)-20s][%(levelname)-8s] %(message)s') 11 | 12 | # formatter = logging.Formatter('[%(module)-15s][%(funcName)-7s][%(levelname)-8s] %(message)s') 13 | stream_handler.setFormatter(color_formatter) 14 | 15 | logger_base.addHandler(stream_handler) 16 | 17 | return logger_base 18 | 19 | 20 | if __name__ == '__main__': 21 | logger = getLogger(__name__) 22 | logger.debug('debug message') 23 | logger.info('info message') 24 | logger.warn('warn message') 25 | logger.error('error message') 26 | logger.critical('critical message') -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import pymongo 4 | 5 | 6 | ''' 7 | DATA: 8 | { 9 | username: ..., 10 | email: ..., 11 | message: ... 12 | } 13 | ''' 14 | 15 | 16 | class DataBase: 17 | def __init__(self): 18 | self.client = None 19 | self.db = None 20 | self.col = None 21 | self.connect_init() 22 | 23 | def connect_init(self): 24 | # 下面这个是哪个数据库来着??? 25 | # self.client = pymongo.MongoClient("mongodb+srv://LanceLiang:1352040930database@lanceliang-lktmq.azure." 26 | # "mongodb.net/test?retryWrites=true&w=majority") 27 | self.client = pymongo.MongoClient("mongodb+srv://lanceliang:1352040930database@lanceliang-9kkx3.azure." 28 | "mongodb.net/test?retryWrites=true&w=majority") 29 | # self.client = pymongo.MongoClient() 30 | self.db = self.client.wenku8_comments 31 | self.col = self.db.wenku8_comments 32 | 33 | def db_init(self): 34 | collection_names = self.db.list_collection_names() 35 | if 'wenku8_comments' in collection_names: 36 | self.db.drop_collection('wenku8_comments') 37 | self.col = self.db.wenku8_comments 38 | 39 | def put_comment(self, username: str, email: str, message: str, head: str): 40 | self.col.insert_one({'username': username, 'email': email, 'message': message, 'head': head}) 41 | 42 | def get_comments(self, count=5000, show_email=True): 43 | result = list(self.col.find({}, {'username': 1, 'email': 1, 'message': 1, 'head': 1, '_id': 0}).limit(count)) 44 | if not show_email: 45 | for i in range(len(result)): 46 | result[i]['email'] = '' 47 | return result 48 | 49 | def find_email(self, username: str): 50 | data = list(self.col.find({'username': username}, {'username': 1, 'email': 1, 'message': 1, '_id': 0})) 51 | if len(data) == 0: 52 | return '' 53 | return data[-1]['email'] 54 | 55 | def error_report(self, error): 56 | self.db.wenku8_bugs.insert_one({'time': time.asctime(), 'error': error}) 57 | 58 | 59 | if __name__ == '__main__': 60 | _db = DataBase() 61 | _db.db_init() 62 | _db.put_comment('lance', 'lanceliang2018@163.com', 'messagefsiafjaiso') 63 | print(_db.get_comments(show_email=True)) 64 | -------------------------------------------------------------------------------- /dmzj2epub.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import bs4 3 | from bs4 import BeautifulSoup as Soup 4 | from ebooklib import epub 5 | import os 6 | import json 7 | import sys 8 | import getopt 9 | from base_logger import getLogger 10 | import threading 11 | import io 12 | import copy 13 | import re 14 | import asyncio 15 | 16 | 17 | class MLogger: 18 | def __init__(self): 19 | self.data = io.StringIO() 20 | 21 | def write(self, content: str): 22 | self.data.write(content + '\n') 23 | print(content) 24 | 25 | def read_all(self): 26 | data2 = copy.deepcopy(self.data) 27 | data2.seek(0) 28 | d = data2.read() 29 | return d 30 | 31 | def info(self, message): 32 | self.write(message) 33 | 34 | def error(self, message): 35 | self.write(message) 36 | 37 | def warning(self, message): 38 | self.write(message) 39 | 40 | def warn(self, message): 41 | self.write(message) 42 | 43 | def critical(self, message): 44 | self.write(message) 45 | 46 | def debug(self, message): 47 | self.write(message) 48 | 49 | 50 | class Dmzj2Epub: 51 | def __init__(self, logger=None): 52 | self.novel_data_file = 'dmzj_novel_data_full.json' 53 | # self.novel_data_file = 'dmzj_novel_data.json' 54 | self.api_novel = 'http://v2.api.dmzj.com/novel/%d.json' 55 | self.api_chapter = 'http://v2.api.dmzj.com/novel/chapter/%d.json' 56 | # 'http://v2.api.dmzj.com/novel/download/%d_%d_%d.txt'%(BookId,volume_id,chapter_id) 57 | self.api_download = 'http://v2.api.dmzj.com/novel/download/%d_%d_%d.txt' 58 | 59 | self.limit_sem_img = 10 60 | self.limit_sem_chapter = 10 61 | self.limit_sem_volume = 10 62 | 63 | self.sumi = 0 64 | self.book = None 65 | 66 | if logger is None: 67 | self.logger = getLogger() 68 | else: 69 | self.logger = logger 70 | 71 | if not os.path.exists(self.novel_data_file): 72 | raise FileNotFoundError('Can not find ' + self.novel_data_file) 73 | with open(self.novel_data_file, 'r', encoding='utf8') as f: 74 | self.novel_data = json.load(f) 75 | 76 | def search(self, key: str): 77 | results = [] 78 | if len(key) == 0: 79 | return None 80 | for d in self.novel_data: 81 | if key in d['name'] or key in d['authors']: 82 | # if key in d['title'] or key in d['author']: 83 | results.append(d) 84 | return results 85 | 86 | def info(self, bid: int): 87 | # for d in self.novel_data: 88 | # if bid == d['id']: 89 | # return d 90 | # return None 91 | response = requests.get(self.api_novel % bid).content 92 | info = json.loads(response) 93 | if type(info) is list: 94 | return None 95 | return info 96 | 97 | def get_volumes_chapters(self, bid: int): 98 | response = json.loads(requests.get(self.api_chapter % bid).content) 99 | return response 100 | 101 | async def download_img(self, url, sem): 102 | async with sem: 103 | filename = os.path.basename(url) 104 | data = requests.get(url).content 105 | file_type = filename.split('.')[-1] 106 | item_img = epub.EpubItem(file_name="images/%s" % filename, 107 | media_type="image/%s" % file_type, content=data) 108 | self.book.add_item(item_img) 109 | self.logger.info('<-Done image: ' + url) 110 | 111 | async def download_chapter(self, bid: int, volume_id: int, chapter_id: int, sem, fetch_image: bool = False): 112 | async with sem: 113 | content = requests.get(self.api_download % (bid, volume_id, chapter_id)).content 114 | if fetch_image: 115 | text = content.decode('utf8', errors='ignore') 116 | imgs = re.findall('https://xs.dmzj.com/img/[0-9]+/[0-9]+/[a-fA-F0-9]{32,32}.jpg', text) 117 | # self.logger.debug(str(imgs)) 118 | tasks_imgs = [] 119 | msem = asyncio.Semaphore(self.limit_sem_img) 120 | for img in imgs: 121 | tasks_imgs.append(self.download_img(img, msem)) 122 | filename = os.path.basename(img) 123 | text = text.replace(img, 'images/%s' % filename) 124 | if len(tasks_imgs) > 0: 125 | await asyncio.wait(tasks_imgs) 126 | content = text.encode() 127 | return { 128 | 'chapter_id': chapter_id, 129 | 'content': content 130 | } 131 | 132 | async def download_book(self, 133 | bid: int, 134 | fetch_image: bool = False): 135 | self.book = epub.EpubBook() 136 | self.sumi = 0 137 | book_info = self.info(bid) 138 | if book_info is None: 139 | return None 140 | title = book_info['name'] 141 | author = book_info['authors'] 142 | cover_url = book_info['cover'] 143 | self.logger.info('#' * 15 + '开始下载' + '#' * 15) 144 | self.logger.info('标题: ' + title + " 作者: " + author) 145 | self.book.set_identifier("%s, %s" % (title, author)) 146 | self.book.set_title(title) 147 | self.book.add_author(author) 148 | data_cover = requests.get(cover_url).content 149 | self.book.set_cover('cover.jpg', data_cover) 150 | 151 | toc = [] 152 | spine = [] 153 | 154 | volume_chapters = self.get_volumes_chapters(bid) 155 | for volume in volume_chapters: 156 | self.logger.info('volume: ' + volume['volume_name']) 157 | # 先增加卷 158 | toc.append((epub.Section(volume['volume_name']), [])) 159 | page_volume = epub.EpubHtml(title=volume['volume_name'], file_name='%s.html' % self.sumi) 160 | self.sumi = self.sumi + 1 161 | page_volume.set_content(("

%s


" % volume['volume_name']).encode()) 162 | self.book.add_item(page_volume) 163 | tasks_chapters = [] 164 | sem = asyncio.Semaphore(self.limit_sem_chapter) 165 | for chapter in volume['chapters']: 166 | tasks_chapters.append(self.download_chapter(bid, volume['volume_id'], chapter['chapter_id'], sem, fetch_image=fetch_image)) 167 | result_chapters = [] 168 | result_tasks = list((await asyncio.wait(tasks_chapters))[0]) 169 | for task in result_tasks: 170 | result_chapters.append(task.result()) 171 | result_chapters.sort(key=lambda x: x['chapter_id'], reverse=False) 172 | # print(result_chapters) 173 | for i in range(len(result_chapters)): 174 | chapter = volume['chapters'][i] 175 | self.logger.info(' chapter: ' + chapter['chapter_name']) 176 | chapter_content = result_chapters[i]['content'] 177 | page = epub.EpubHtml(title=chapter['chapter_name'], file_name='%s.xhtml' % self.sumi) 178 | self.sumi = self.sumi + 1 179 | page.set_content(chapter_content) 180 | self.book.add_item(page) 181 | toc[-1][1].append(page) 182 | spine.append(page) 183 | 184 | self.book.toc = toc 185 | self.book.spine = spine 186 | self.book.add_item(epub.EpubNcx()) 187 | self.book.add_item(epub.EpubNav()) 188 | 189 | stream = io.BytesIO() 190 | epub.write_epub(stream, self.book) 191 | return stream.getvalue() 192 | 193 | 194 | if __name__ == '__main__': 195 | _de = Dmzj2Epub() 196 | # print(_de.search('入间人间')) 197 | # print(_de.info(6)) 198 | # print(_de.get_chapters(6)) 199 | _info = _de.info(1) 200 | print(_info) 201 | _data = asyncio.run(_de.download_book(1, fetch_image=True)) 202 | with open('%s - %s.epub' % (_info['name'], _info['authors']), 'wb') as f: 203 | f.write(_data) -------------------------------------------------------------------------------- /ebooklib/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | # Version of ebook library 18 | 19 | VERSION = (0, 17, 1) 20 | 21 | # LIST OF POSSIBLE ITEMS 22 | ITEM_UNKNOWN = 0 23 | ITEM_IMAGE = 1 24 | ITEM_STYLE = 2 25 | ITEM_SCRIPT = 3 26 | ITEM_NAVIGATION = 4 27 | ITEM_VECTOR = 5 28 | ITEM_FONT = 6 29 | ITEM_VIDEO = 7 30 | ITEM_AUDIO = 8 31 | ITEM_DOCUMENT = 9 32 | ITEM_COVER = 10 33 | ITEM_SMIL = 11 34 | 35 | # EXTENSION MAPPER 36 | EXTENSIONS = {ITEM_IMAGE: ['.jpg', '.jpeg', '.gif', '.tiff', '.tif', '.png'], 37 | ITEM_STYLE: ['.css'], 38 | ITEM_VECTOR: ['.svg'], 39 | ITEM_FONT: ['.otf', '.woff', '.ttf'], 40 | ITEM_SCRIPT: ['.js'], 41 | ITEM_NAVIGATION: ['.ncx'], 42 | ITEM_VIDEO: ['.mov', '.mp4', '.avi'], 43 | ITEM_AUDIO: ['.mp3', '.ogg'], 44 | ITEM_COVER: ['.jpg', '.jpeg', '.png'], 45 | ITEM_SMIL: ['.smil'] 46 | } 47 | -------------------------------------------------------------------------------- /ebooklib/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/ebooklib/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /ebooklib/__pycache__/epub.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/ebooklib/__pycache__/epub.cpython-36.pyc -------------------------------------------------------------------------------- /ebooklib/__pycache__/utils.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/ebooklib/__pycache__/utils.cpython-36.pyc -------------------------------------------------------------------------------- /ebooklib/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/ebooklib/plugins/__init__.py -------------------------------------------------------------------------------- /ebooklib/plugins/base.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | 18 | class BasePlugin(object): 19 | def before_write(self, book): 20 | "Processing before save" 21 | return True 22 | 23 | def after_write(self, book): 24 | "Processing after save" 25 | return True 26 | 27 | def before_read(self, book): 28 | "Processing before save" 29 | return True 30 | 31 | def after_read(self, book): 32 | "Processing after save" 33 | return True 34 | 35 | def item_after_read(self, book, item): 36 | "Process general item after read." 37 | return True 38 | 39 | def item_before_write(self, book, item): 40 | "Process general item before write." 41 | return True 42 | 43 | def html_after_read(self, book, chapter): 44 | "Processing HTML before read." 45 | return True 46 | 47 | def html_before_write(self, book, chapter): 48 | "Processing HTML before save." 49 | return True 50 | -------------------------------------------------------------------------------- /ebooklib/plugins/booktype.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | from ebooklib.plugins.base import BasePlugin 18 | from ebooklib.utils import parse_html_string 19 | 20 | class BooktypeLinks(BasePlugin): 21 | NAME = 'Booktype Links' 22 | 23 | def __init__(self, booktype_book): 24 | self.booktype_book = booktype_book 25 | 26 | def html_before_write(self, book, chapter): 27 | from lxml import etree 28 | 29 | try: 30 | from urlparse import urlparse, urljoin 31 | except ImportError: 32 | from urllib.parse import urlparse, urljoin 33 | 34 | try: 35 | tree = parse_html_string(chapter.content) 36 | except: 37 | return 38 | 39 | root = tree.getroottree() 40 | 41 | if len(root.find('body')) != 0: 42 | body = tree.find('body') 43 | 44 | # should also be aware to handle 45 | # ../chapter/ 46 | # ../chapter/#reference 47 | # ../chapter#reference 48 | 49 | for _link in body.xpath('//a'): 50 | # This is just temporary for the footnotes 51 | if _link.get('href', '').find('InsertNoteID') != -1: 52 | _ln = _link.get('href', '') 53 | i = _ln.find('#') 54 | _link.set('href', _ln[i:]) 55 | 56 | continue 57 | 58 | _u = urlparse(_link.get('href', '')) 59 | 60 | # Let us care only for internal links at the moment 61 | if _u.scheme == '': 62 | if _u.path != '': 63 | _link.set('href', '%s.xhtml' % _u.path) 64 | 65 | if _u.fragment != '': 66 | _link.set('href', urljoin(_link.get('href'), '#%s' % _u.fragment)) 67 | 68 | if _link.get('name') != None: 69 | _link.set('id', _link.get('name')) 70 | etree.strip_attributes(_link, 'name') 71 | 72 | chapter.content = etree.tostring(tree, pretty_print=True, encoding='utf-8') 73 | 74 | 75 | 76 | 77 | class BooktypeFootnotes(BasePlugin): 78 | NAME = 'Booktype Footnotes' 79 | 80 | def __init__(self, booktype_book): 81 | self.booktype_book = booktype_book 82 | 83 | def html_before_write(self, book, chapter): 84 | from lxml import etree 85 | 86 | from ebooklib import epub 87 | 88 | try: 89 | tree = parse_html_string(chapter.content) 90 | except: 91 | return 92 | 93 | root = tree.getroottree() 94 | 95 | if len(root.find('body')) != 0: 96 | body = tree.find('body') 97 | 98 | # 1 99 | #
  1. prvi footnote ^
  2. 100 | 101 | # 1

    102 | # 103 | for footnote in body.xpath('//span[@class="InsertNoteMarker"]'): 104 | footnote_id = footnote.get('id')[:-8] 105 | a = footnote.getchildren()[0].getchildren()[0] 106 | 107 | footnote_text = body.xpath('//li[@id="%s"]' % footnote_id)[0] 108 | 109 | a.attrib['{%s}type' % epub.NAMESPACES['EPUB']] = 'noteref' 110 | ftn = etree.SubElement(body, 'aside', {'id': footnote_id}) 111 | ftn.attrib['{%s}type' % epub.NAMESPACES['EPUB']] = 'footnote' 112 | ftn_p = etree.SubElement(ftn, 'p') 113 | ftn_p.text = footnote_text.text 114 | 115 | old_footnote = body.xpath('//ol[@id="InsertNote_NoteList"]') 116 | if len(old_footnote) > 0: 117 | body.remove(old_footnote[0]) 118 | 119 | chapter.content = etree.tostring(tree, pretty_print=True, encoding='utf-8') 120 | -------------------------------------------------------------------------------- /ebooklib/plugins/sourcecode.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | from ebooklib.plugins.base import BasePlugin 18 | from ebooklib.utils import parse_html_string 19 | 20 | class SourceHighlighter(BasePlugin): 21 | def __init__(self): 22 | pass 23 | 24 | def html_before_write(self, book, chapter): 25 | from lxml import etree, html 26 | 27 | from pygments import highlight 28 | from pygments.formatters import HtmlFormatter 29 | 30 | from ebooklib import epub 31 | 32 | try: 33 | tree = parse_html_string(chapter.content) 34 | except: 35 | return 36 | 37 | root = tree.getroottree() 38 | 39 | had_source = False 40 | 41 | if len(root.find('body')) != 0: 42 | body = tree.find('body') 43 | # check for embeded source 44 | for source in body.xpath('//pre[contains(@class,"source-")]'): 45 | css_class = source.get('class') 46 | 47 | source_text = (source.text or '') + ''.join([html.tostring(child) for child in source.iterchildren()]) 48 | 49 | if 'source-python' in css_class: 50 | from pygments.lexers import PythonLexer 51 | 52 | # _text = highlight(source_text, PythonLexer(), HtmlFormatter(linenos="inline")) 53 | _text = highlight(source_text, PythonLexer(), HtmlFormatter()) 54 | 55 | if 'source-css' in css_class: 56 | from pygments.lexers import CssLexer 57 | 58 | _text = highlight(source_text, CssLexer(), HtmlFormatter()) 59 | 60 | _parent = source.getparent() 61 | _parent.replace(source, etree.XML(_text)) 62 | 63 | had_source = True 64 | 65 | if had_source: 66 | chapter.add_link(href="style/code.css", rel="stylesheet", type="text/css") 67 | chapter.content = etree.tostring(tree, pretty_print=True, encoding='utf-8') 68 | 69 | -------------------------------------------------------------------------------- /ebooklib/plugins/standard.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | import six 18 | 19 | from ebooklib.plugins.base import BasePlugin 20 | from ebooklib.utils import parse_html_string 21 | 22 | # TODO: 23 | # - should also look for the _required_ elements 24 | # http://www.w3.org/html/wg/drafts/html/master/tabular-data.html#the-table-element 25 | 26 | ATTRIBUTES_GLOBAL = ['accesskey', 'class', 'contenteditable', 'contextmenu', 'dir', 'draggable', 27 | 'dropzone', 'hidden', 'id', 'inert', 'itemid', 'itemprop', 'itemref', 28 | 'itemscope', 'itemtype', 'lang', 'spellcheck', 'style', 'tabindex', 29 | 'title', 'translate', 'epub:type'] 30 | 31 | # Remove for now from here 32 | DEPRECATED_TAGS = ['acronym', 'applet', 'basefont', 'big', 'center', 'dir', 'font', 'frame', 33 | 'frameset', 'isindex', 'noframes', 's', 'strike', 'tt'] 34 | 35 | 36 | def leave_only(item, tag_list): 37 | for _attr in six.iterkeys(item.attrib): 38 | if _attr not in tag_list: 39 | del item.attrib[_attr] 40 | 41 | 42 | class SyntaxPlugin(BasePlugin): 43 | NAME = 'Check HTML syntax' 44 | 45 | def html_before_write(self, book, chapter): 46 | from lxml import etree 47 | 48 | try: 49 | tree = parse_html_string(chapter.content) 50 | except: 51 | return 52 | 53 | root = tree.getroottree() 54 | 55 | # delete deprecated tags 56 | # i should really have a list of allowed tags 57 | for tag in DEPRECATED_TAGS: 58 | etree.strip_tags(root, tag) 59 | 60 | head = tree.find('head') 61 | 62 | if head is not None and len(head) != 0: 63 | 64 | for _item in head: 65 | if _item.tag == 'base': 66 | leave_only(_item, ATTRIBUTES_GLOBAL + ['href', 'target']) 67 | elif _item.tag == 'link': 68 | leave_only(_item, ATTRIBUTES_GLOBAL + ['href', 'crossorigin', 'rel', 'media', 'hreflang', 'type', 'sizes']) 69 | elif _item.tag == 'title': 70 | if _item.text == '': 71 | head.remove(_item) 72 | elif _item.tag == 'meta': 73 | leave_only(_item, ATTRIBUTES_GLOBAL + ['name', 'http-equiv', 'content', 'charset']) 74 | # just remove for now, but really should not be like this 75 | head.remove(_item) 76 | elif _item.tag == 'script': 77 | leave_only(_item, ATTRIBUTES_GLOBAL + ['src', 'type', 'charset', 'async', 'defer', 'crossorigin']) 78 | elif _item.tag == 'source': 79 | leave_only(_item, ATTRIBUTES_GLOBAL + ['src', 'type', 'media']) 80 | elif _item.tag == 'style': 81 | leave_only(_item, ATTRIBUTES_GLOBAL + ['media', 'type', 'scoped']) 82 | else: 83 | leave_only(_item, ATTRIBUTES_GLOBAL) 84 | 85 | 86 | if len(root.find('body')) != 0: 87 | body = tree.find('body') 88 | 89 | for _item in body.iter(): 90 | # it is not 91 | # 92 | 93 | if _item.tag == 'a': 94 | leave_only(_item, ATTRIBUTES_GLOBAL + ['href', 'target', 'download', 'rel', 'hreflang', 'type']) 95 | elif _item.tag == 'area': 96 | leave_only(_item, ATTRIBUTES_GLOBAL + ['alt', 'coords', 'shape', 'href', 'target', 'download', 'rel', 'hreflang', 'type']) 97 | elif _item.tag == 'audio': 98 | leave_only(_item, ATTRIBUTES_GLOBAL + ['src', 'crossorigin', 'preload', 'autoplay', 'mediagroup', 'loop', 'muted', 'controls']) 99 | elif _item.tag == 'blockquote': 100 | leave_only(_item, ATTRIBUTES_GLOBAL + ['cite']) 101 | elif _item.tag == 'button': 102 | leave_only(_item, ATTRIBUTES_GLOBAL + ['autofocus', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 103 | 'formtarget', 'name', 'type', 'value', 'menu']) 104 | elif _item.tag == 'canvas': 105 | leave_only(_item, ATTRIBUTES_GLOBAL + ['width', 'height']) 106 | elif _item.tag == 'canvas': 107 | leave_only(_item, ATTRIBUTES_GLOBAL + ['width', 'height']) 108 | elif _item.tag == 'del': 109 | leave_only(_item, ATTRIBUTES_GLOBAL + ['cite', 'datetime']) 110 | elif _item.tag == 'details': 111 | leave_only(_item, ATTRIBUTES_GLOBAL + ['open']) 112 | elif _item.tag == 'embed': 113 | leave_only(_item, ATTRIBUTES_GLOBAL + ['src', 'type', 'width', 'height']) 114 | elif _item.tag == 'fieldset': 115 | leave_only(_item, ATTRIBUTES_GLOBAL + ['disable', 'form', 'name']) 116 | elif _item.tag == 'details': 117 | leave_only(_item, ATTRIBUTES_GLOBAL + ['accept-charset', 'action', 'autocomplete', 'enctype', 'method', 'name', 'novalidate', 'target']) 118 | elif _item.tag == 'iframe': 119 | leave_only(_item, ATTRIBUTES_GLOBAL + ['src', 'srcdoc', 'name', 'sandbox', 'seamless', 'allowfullscreen', 'width', 'height']) 120 | elif _item.tag == 'img': 121 | _src = _item.get('src', '').lower() 122 | if _src.startswith('http://') or _src.startswith('https://'): 123 | if 'remote-resources' not in chapter.properties: 124 | chapter.properties.append('remote-resources') 125 | # THIS DOES NOT WORK, ONLY VIDEO AND AUDIO FILES CAN BE REMOTE RESOURCES 126 | # THAT MEANS I SHOULD ALSO CATCH 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | import six 18 | import subprocess 19 | 20 | from ebooklib.plugins.base import BasePlugin 21 | from ebooklib.utils import parse_html_string 22 | 23 | # Recommend usage of 24 | # - https://github.com/w3c/tidy-html5 25 | 26 | def tidy_cleanup(content, **extra): 27 | cmd = [] 28 | 29 | for k, v in six.iteritems(extra): 30 | 31 | if v: 32 | cmd.append('--%s' % k) 33 | cmd.append(v) 34 | else: 35 | cmd.append('-%s' % k) 36 | 37 | # must parse all other extra arguments 38 | try: 39 | p = subprocess.Popen(['tidy']+cmd, shell=False, 40 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, 41 | stderr=subprocess.PIPE, close_fds=True) 42 | except OSError: 43 | return (3, None) 44 | 45 | p.stdin.write(content) 46 | 47 | (cont, p_err) = p.communicate() 48 | 49 | # 0 - all ok 50 | # 1 - there were warnings 51 | # 2 - there were errors 52 | # 3 - exception 53 | 54 | return (p.returncode, cont) 55 | 56 | 57 | class TidyPlugin(BasePlugin): 58 | NAME = 'Tidy HTML' 59 | OPTIONS = {'char-encoding': 'utf8', 60 | 'tidy-mark': 'no' 61 | } 62 | 63 | def __init__(self, extra = {}): 64 | self.options = dict(self.OPTIONS) 65 | self.options.update(extra) 66 | 67 | def html_before_write(self, book, chapter): 68 | if not chapter.content: 69 | return None 70 | 71 | (_, chapter.content) = tidy_cleanup(chapter.content, **self.options) 72 | 73 | return chapter.content 74 | 75 | def html_after_read(self, book, chapter): 76 | if not chapter.content: 77 | return None 78 | 79 | (_, chapter.content) = tidy_cleanup(chapter.content, **self.options) 80 | 81 | return chapter.content 82 | 83 | -------------------------------------------------------------------------------- /ebooklib/utils.py: -------------------------------------------------------------------------------- 1 | # This file is part of EbookLib. 2 | # Copyright (c) 2013 Aleksandar Erkalovic 3 | # 4 | # EbookLib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # EbookLib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with EbookLib. If not, see . 16 | 17 | import io 18 | import mimetypes 19 | 20 | from lxml import etree 21 | 22 | 23 | mimetype_initialised = False 24 | 25 | 26 | def debug(obj): 27 | import pprint 28 | 29 | pp = pprint.PrettyPrinter(indent=4) 30 | pp.pprint(obj) 31 | 32 | 33 | def parse_string(s): 34 | try: 35 | tree = etree.parse(io.BytesIO(s.encode('utf-8'))) 36 | except: 37 | tree = etree.parse(io.BytesIO(s)) 38 | 39 | return tree 40 | 41 | 42 | def parse_html_string(s): 43 | from lxml import html 44 | 45 | utf8_parser = html.HTMLParser(encoding='utf-8') 46 | 47 | html_tree = html.document_fromstring(s, parser=utf8_parser) 48 | 49 | return html_tree 50 | 51 | 52 | def guess_type(extenstion): 53 | global mimetype_initialised 54 | 55 | if not mimetype_initialised: 56 | mimetypes.init() 57 | mimetypes.add_type('application/xhtml+xml', '.xhtml') 58 | mimetype_initialised = True 59 | 60 | return mimetypes.guess_type(extenstion) 61 | 62 | 63 | def create_pagebreak(pageref, label=None, html=True): 64 | from ebooklib.epub import NAMESPACES 65 | 66 | pageref_attributes = { 67 | '{%s}type' % NAMESPACES['EPUB']: 'pagebreak', 68 | 'title': u'{}'.format(pageref), 69 | 'id': u'{}'.format(pageref), 70 | } 71 | 72 | pageref_elem = etree.Element('span', pageref_attributes, nsmap={'epub': NAMESPACES['EPUB']}) 73 | 74 | if label: 75 | pageref_elem.text = label 76 | 77 | if html: 78 | return etree.tostring(pageref_elem, encoding='unicode') 79 | 80 | return pageref_elem 81 | 82 | 83 | def get_headers(elem): 84 | for n in range(1, 7): 85 | headers = elem.xpath('./h{}'.format(n)) 86 | 87 | if len(headers) > 0: 88 | text = headers[0].text_content().strip() 89 | if len(text) > 0: 90 | return text 91 | return None 92 | 93 | 94 | def get_pages(item): 95 | body = parse_html_string(item.get_body_content()) 96 | pages = [] 97 | 98 | for elem in body.iter(): 99 | if 'epub:type' in elem.attrib: 100 | if elem.get('id') is not None: 101 | _text = None 102 | 103 | if elem.text is not None and elem.text.strip() != '': 104 | _text = elem.text.strip() 105 | 106 | if _text is None: 107 | _text = elem.get('aria-label') 108 | 109 | if _text is None: 110 | _text = get_headers(elem) 111 | 112 | pages.append((item.get_name(), elem.get('id'), _text or elem.get('id'))) 113 | 114 | return pages 115 | 116 | 117 | def get_pages_for_items(items): 118 | pages_from_docs = [get_pages(item) for item in items] 119 | 120 | return [item for pages in pages_from_docs for item in pages] 121 | -------------------------------------------------------------------------------- /error_report.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import smtplib 4 | from email.mime.text import MIMEText 5 | from email.utils import formataddr 6 | 7 | 8 | def send_report(report): 9 | my_sender = 'LanceLiang2018@163.com' # 发件人邮箱账号 10 | my_pass = '1352040930smtp' # 发件人邮箱密码 11 | # my_user = '1352040930@qq.com' # 收件人邮箱账号 12 | try: 13 | if type(report) is dict: 14 | report = json.dumps(report) 15 | msg = MIMEText(str(report), 'plain', 'utf-8') 16 | msg['From'] = formataddr(["Programe errors", my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号 17 | msg['To'] = formataddr(['Lance Liang', my_sender]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号 18 | msg['Subject'] = "wk8local程序的新bug report" # 邮件的主题,也可以说是标题 19 | 20 | server = smtplib.SMTP_SSL("smtp.163.com", 465) # 发件人邮箱中的SMTP服务器,端口是465 21 | server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码 22 | server.sendmail(my_sender, [my_sender, ], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件 23 | server.quit() # 关闭连接 24 | except Exception as e: 25 | print('错误信息邮件发送失败!', e) 26 | print('请将程序窗口截图手动发送到 LanceLiang2018@163.com 以协助程序开发。') 27 | print('...如果您不想发也没关系QAQ...') 28 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 29 | input() 30 | exit(1) 31 | 32 | 33 | def form_report(e): 34 | report = { 35 | 'string': str(e), 36 | 'file': e.__traceback__.tb_frame.f_globals['__file__'], 37 | 'line': e.__traceback__.tb_lineno 38 | } 39 | return report 40 | 41 | try: 42 | from database import DataBase 43 | _db = DataBase() 44 | except Exception as _e: 45 | print("产生了无法预知的错误") 46 | print("错误内容如下:") 47 | print('初始化远程数据库时出现错误(wk8local.py)') 48 | _error = form_report(_e) 49 | print(_error['string']) 50 | print('文件', _error['file']) 51 | print('行号', _error['line']) 52 | print('尝试发送bug报告邮件...') 53 | send_report(_error) 54 | print('发送bug报告邮件完成,请关闭窗口。') 55 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 56 | input() 57 | exit(1) 58 | 59 | 60 | def report_it(e, _exit=False): 61 | print("产生了无法预知的错误") 62 | print("错误内容如下:") 63 | error = form_report(e) 64 | print(error['string']) 65 | print('文件', error['file']) 66 | print('行号', error['line']) 67 | print('正在尝试反馈错误...') 68 | print('尝试发送bug报告邮件...') 69 | send_report(error) 70 | print('发送bug报告邮件成功') 71 | try: 72 | print('尝试把bug发送到远程数据库...') 73 | _db.error_report(error) 74 | except Exception as e2: 75 | print('把bug发送到远程数据库失败') 76 | send_report(e2) 77 | print('发送bug报告完成,请关闭窗口。') 78 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True' and _exit: 79 | input() 80 | exit(1) -------------------------------------------------------------------------------- /errors.txt: -------------------------------------------------------------------------------- 1 | 11 2 | 14 3 | 2 4 | -------------------------------------------------------------------------------- /headers.txt: -------------------------------------------------------------------------------- 1 | Host: m.weibo.cn 2 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0 3 | Accept: application/json, text/plain, */* 4 | Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 5 | Accept-Encoding: gzip, deflate, br 6 | Content-Type: application/x-www-form-urlencoded 7 | X-Requested-With: XMLHttpRequest 8 | MWeibo-Pwa: 1 9 | X-XSRF-TOKEN: f572a8 10 | Origin: https://m.weibo.cn 11 | Referer: https://m.weibo.cn/message/chat?uid=5175429989&name=msgbox 12 | Connection: keep-alive 13 | Cookie: ALF=1587999867; _T_WM=58616718015; SCF=Ahy1D6if_emkN6pc6So_HAY-k_9h-LMxAeGhK7JS3utF_LHmzXNOh78wNJcWP5mbaZ_rVMF1VS1W298GHOAOFjA.; SUHB=0nl-HcWaEtaWnm; SUB=_2A25ze5xeDeRhGeNG7lIU-SjPwj2IHXVQhyQWrDV6PUJbkdAKLWH9kW1NS14NzQhs_rKjFfwH3KlvjD3w4V-NYQ4N; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9Wh2-Tz-6ovrrTzGYGhUmmqO5JpX5K-hUgL.Fo-RSK5f1Kq01K22dJLoI7v-dc44PXpDqg4rP0z7eK-t; SSOLoginState=1585441806; XSRF-TOKEN=f572a8; WEIBOCN_FROM=1110006030; MLOGIN=1; M_WEIBOCN_PARAMS=uicode%3D20000174 14 | Pragma: no-cache 15 | Cache-Control: no-cache 16 | TE: Trailers -------------------------------------------------------------------------------- /images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/images/1.jpg -------------------------------------------------------------------------------- /images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/images/2.jpg -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/images/3.png -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pyinstaller wk8local.py 3 | md dist\wk8local\static 4 | copy /Y static\* dist\wk8local\static\ 5 | md dist\wk8local\templates 6 | copy /Y templates\* dist\wk8local\templates\ 7 | copy dmzj_novel_data_full.json dist\wk8local\ 8 | copy dmzj_novel_data.json dist\wk8local\ 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import * 3 | from manager import * 4 | import io 5 | # import urllib.parse 6 | import threading 7 | import re 8 | import time 9 | import requests 10 | import hashlib 11 | import smtplib 12 | from email.mime.text import MIMEText 13 | from email.utils import formataddr 14 | from database import DataBase 15 | import error_report 16 | import xiaoice 17 | 18 | 19 | db = DataBase() 20 | app = Flask(__name__) 21 | threads = [] 22 | my_email = 'LanceLiang2018@163.com' 23 | my_password = '1352040930wenku8' 24 | 25 | 26 | def get_icon(email): 27 | return'https://s.gravatar.com/avatar/' + hashlib.md5(email.lower().encode()).hexdigest() + '?s=34' 28 | 29 | 30 | def has_file(target): 31 | r = requests.get(target, stream=True) 32 | if int(r.status_code) == 200: 33 | return True 34 | return False 35 | 36 | 37 | def file_size(target): 38 | r = requests.get(target, stream=True) 39 | if int(r.status_code) == 200: 40 | return int(r.headers['Content-Length']) 41 | return 0 42 | 43 | 44 | def local_check(book_id): 45 | wk = Wenku8ToEpub() 46 | filename_ = wk.id2name(book_id) + '.epub' 47 | info = wk.book_info(book_id) 48 | if info is None: 49 | return '1' # 需要更新 50 | # 检查上次上传时间 51 | last_time = v2_check_time(filename_) 52 | if last_time is None: 53 | return '1' 54 | last_time = last_time[:10] 55 | if last_time > info['update']: 56 | return '0' 57 | return '1' # 需要更新 58 | 59 | 60 | def send_email(user, email, message): 61 | # print(user, message) 62 | my_sender = 'LanceLiang2018@163.com' # 发件人邮箱账号 63 | my_pass = '1352040930smtp' # 发件人邮箱密码 64 | # my_user = '1352040930@qq.com' # 收件人邮箱账号 65 | try: 66 | # print('try to send:', user) 67 | msg = MIMEText(message, 'plain', 'utf-8') 68 | msg['From'] = formataddr(["USER:%s" % user, my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号 69 | msg['To'] = formataddr(['Lance Liang', my_sender]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号 70 | msg['Subject'] = "来自 %s(%s) 的新消息" % (user, email) # 邮件的主题,也可以说是标题 71 | 72 | server = smtplib.SMTP_SSL("smtp.163.com", 465) # 发件人邮箱中的SMTP服务器,端口是465 73 | server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码 74 | server.sendmail(my_sender, [my_sender, ], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件 75 | server.quit() # 关闭连接 76 | except Exception as e: 77 | print(e) 78 | error_report.report_it(e) 79 | 80 | 81 | def send_email_2(user, email, message): 82 | my_sender = 'LanceLiang2018@163.com' # 发件人邮箱账号 83 | my_pass = '1352040930smtp' # 发件人邮箱密码 84 | # my_user = '1352040930@qq.com' # 收件人邮箱账号 85 | try: 86 | # print('try to send:', user) 87 | msg = MIMEText(message, 'plain', 'utf-8') 88 | msg['From'] = formataddr(["Lance Liang", my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号 89 | msg['To'] = formataddr(['%s' % user, email]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号 90 | msg['Subject'] = "Re:您在wenku8.herokuapp.com的反馈" # 邮件的主题,也可以说是标题 91 | 92 | server = smtplib.SMTP_SSL("smtp.163.com", 465) # 发件人邮箱中的SMTP服务器,端口是465 93 | server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码 94 | server.sendmail(my_sender, [email, ], msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件 95 | server.quit() # 关闭连接 96 | except Exception as e: 97 | print(e) 98 | error_report.report_it(e) 99 | 100 | 101 | @app.route('/', methods=['GET']) 102 | def index(): 103 | # return '' \ 104 | # 'https://github.com/LanceLiang2018/Wenku8ToEpub-Online' 105 | local = False 106 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 107 | local = True 108 | urls = make_urls() 109 | return render_template('index.html', local=local, urls=urls) 110 | 111 | 112 | @app.route('/bookinfo/', methods=['GET']) 113 | def get_bookinfo(book_id: int): 114 | wk = Wenku8ToEpub() 115 | filename_ = wk.id2name(book_id) + '.epub' 116 | info = wk.book_info(book_id) 117 | if info is None: 118 | return json.dumps({}) 119 | # 检查上次上传时间 120 | last_time = v2_check_time(filename_) 121 | info['update_time'] = last_time 122 | return json.dumps(info) 123 | 124 | 125 | @app.route('/bookinfo_dmzj/', methods=['GET']) 126 | def get_dmzj_bookinfo(book_id: int): 127 | de = Dmzj2Epub() 128 | info = de.info(book_id) 129 | filename_ = 'dmzj_%s.epub' % info['name'] 130 | if info is None: 131 | return json.dumps({}) 132 | # 检查上次上传时间 133 | last_time = v2_check_time(filename_) 134 | info['update_time'] = last_time 135 | return json.dumps(info) 136 | 137 | 138 | @app.route('/v2/check/', methods=['GET']) 139 | def v2_check(book_id): 140 | book_id = str(book_id) 141 | if book_id.startswith('dmzj_'): 142 | # 没有动态获取,直接返回“需要” 143 | de = Dmzj2Epub() 144 | try: 145 | book_id = int(book_id.split('dmzj_')[-1]) 146 | except ValueError: 147 | return '1' 148 | info = de.info(book_id) 149 | if info is None: 150 | return '1' 151 | # 检查上次上传时间 152 | filename_ = 'dmzj_%s.epub' % info['name'] 153 | last_time = v2_check_time(filename_) 154 | if last_time is None: 155 | return '1' 156 | last_time = last_time[:10] 157 | update = list(time.localtime(info['last_update_time'])) 158 | update_time = '%s-%s-%s' % (update[0], update[1], update[2]) 159 | if last_time > update_time: 160 | return '0' 161 | return '1' # 需要更新 162 | try: 163 | book_id = int(book_id) 164 | except ValueError: 165 | return '1' 166 | wk = Wenku8ToEpub() 167 | filename_ = wk.id2name(book_id) + '.epub' 168 | info = wk.book_info(book_id) 169 | if info is None: 170 | return '1' # 需要更新 171 | # 检查上次上传时间 172 | last_time = v2_check_time(filename_) 173 | if last_time is None: 174 | return '1' 175 | last_time = last_time[:10] 176 | if last_time > info['update']: 177 | return '0' 178 | return '1' # 需要更新 179 | 180 | 181 | @app.route('/v2/search/', methods=['GET']) 182 | def v2_search(key: str): 183 | wk = Wenku8ToEpub() 184 | results = wk.search(key) 185 | return json.dumps(results) 186 | 187 | 188 | @app.route('/v2_dmzj/search/', methods=['GET']) 189 | def v2_dmzj_search(key: str): 190 | de = Dmzj2Epub() 191 | results = de.search(key) 192 | return json.dumps(results) 193 | 194 | 195 | @app.route('/v2/name/') 196 | def v2_jump_by_name(book_name): 197 | filename = "%s.epub" % book_name 198 | target = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 199 | if has_file(target): 200 | return target 201 | return '' 202 | 203 | 204 | @app.route('/v2/cache/') 205 | def v2_cache(book_id: int, image=False): 206 | wk = Wenku8ToEpub() 207 | filename_ = wk.id2name(book_id) 208 | if filename_ == '': 209 | return '1' 210 | for t in threads: 211 | if t['bid'] == book_id: 212 | return '2' 213 | mlogger = MLogger() 214 | th = threading.Thread(target=v2_work, args=(book_id, None, mlogger, image)) 215 | th.setDaemon(True) 216 | th.start() 217 | # filename = "%s.epub" % filename_ 218 | # url = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 219 | threads.append({ 220 | 'bid': book_id, 221 | 'th': th, 222 | 'messages': mlogger, 223 | # 'result': url 224 | }) 225 | # url = work(book_id) 226 | return '0' 227 | 228 | 229 | @app.route('/v2_dmzj/cache/') 230 | def v2_dmzj_cache(book_id: int, image=False): 231 | de = Dmzj2Epub() 232 | info = de.info(book_id) 233 | if info is None: 234 | return '1' 235 | for t in threads: 236 | if t['bid'] == book_id: 237 | return '2' 238 | mlogger = MLogger() 239 | th = threading.Thread(target=v2_dmzj_work, args=(book_id, None, mlogger, image)) 240 | th.setDaemon(True) 241 | th.start() 242 | # filename = "%s.epub" % filename_ 243 | # url = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 244 | threads.append({ 245 | 'bid': 'dmzj_' + str(book_id), 246 | 'th': th, 247 | 'messages': mlogger, 248 | # 'result': url 249 | }) 250 | # url = work(book_id) 251 | return '0' 252 | 253 | 254 | @app.route('/v2/cache_img/') 255 | def v2_cache_img(book_id: int): 256 | return v2_cache(book_id, image=True) 257 | 258 | 259 | @app.route('/v2_dmzj/cache_img/') 260 | def v2_dmzj_cache_img(book_id: int): 261 | return v2_dmzj_cache(book_id, image=True) 262 | 263 | 264 | @app.route('/v2/cache_status/') 265 | def v2_cache_status(book_id): 266 | book_id = str(book_id) 267 | for t in threads: 268 | if t['bid'] == book_id: 269 | if t['th'].isAlive(): 270 | return '0' 271 | else: 272 | # url = t['result'] 273 | threads.remove(t) 274 | url = th_results.get(str(book_id)) 275 | if url is None: 276 | return '1' 277 | return url 278 | return '1' 279 | 280 | 281 | @app.route('/v2/cache_logs/') 282 | def v2_cache_logs(book_id): 283 | book_id = str(book_id) 284 | for t in threads: 285 | if t['bid'] == book_id: 286 | data = t['messages'].read_all() 287 | return data 288 | return '' 289 | 290 | 291 | @app.route('/v2/get/') 292 | def v2_get(book_id: int): 293 | wk = Wenku8ToEpub() 294 | filename_ = wk.id2name(book_id) 295 | if filename_ == '': 296 | return '' 297 | filename = "%s.epub" % filename_ 298 | filename = urllib.parse.quote(filename) 299 | target = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 300 | if has_file(target): 301 | return target 302 | return '' 303 | 304 | 305 | @app.route('/v2_dmzj/get/') 306 | def v2_dmzj_get(book_id: int): 307 | de = Dmzj2Epub() 308 | info = de.info(book_id) 309 | if info is None: 310 | return '' 311 | filename_ = 'dmzj_%s' % info['name'] 312 | filename = "%s.epub" % filename_ 313 | filename = urllib.parse.quote(filename) 314 | target = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 315 | if has_file(target): 316 | return target 317 | return '' 318 | 319 | 320 | @app.route('/v2/comments', methods=['GET']) 321 | def v2_comments(): 322 | data = db.get_comments(show_email=False) 323 | return json.dumps(data) 324 | 325 | 326 | @app.route('/v2/feedback', methods=['POST']) 327 | def v2_feedback(): 328 | form = dict(request.form) 329 | message = form.get('message', '') 330 | user = form.get('user', '') 331 | email = form.get('email', '') 332 | password = form.get('password', '') 333 | head = get_icon(email) 334 | logger.info(str((user, email, message, password))) 335 | if len(password) > 0: 336 | if password == my_password: 337 | # 老子是管理员,给别人发消息,user是名字。 338 | target_email = db.find_email(user) 339 | if '' == target_email: 340 | return '邮箱查找失败' 341 | send_email_2(user, target_email, message) 342 | db.put_comment('Lance->@%s' % user, my_email, message, head) 343 | return '管理员操作成功' 344 | else: 345 | return '管理员密码错误' 346 | else: 347 | send_email(user, email, message) 348 | db.put_comment(user, email, message, head) 349 | pass 350 | return '' 351 | 352 | 353 | @app.route('/v2/visitors') 354 | def v2_visitors(): 355 | api = 'https://api.baidu.com/json/tongji/v1/ReportService/getData' 356 | r = requests.post(api) 357 | return '0' 358 | 359 | 360 | @app.route('/cache/') 361 | def cache(book_id: int): 362 | wk = Wenku8ToEpub() 363 | filename_ = wk.id2name(book_id) 364 | if filename_ == '': 365 | return '没有这个小说!' 366 | url = work(book_id) 367 | return redirect(url) 368 | 369 | 370 | @app.route('/cache_img/') 371 | def cache_img(book_id: int): 372 | wk = Wenku8ToEpub() 373 | filename_ = wk.id2name(book_id) 374 | if filename_ == '': 375 | return '没有这个小说!' 376 | url = work4(book_id) 377 | return redirect(url) 378 | 379 | 380 | @app.route('/no_cache/') 381 | def no_cache(book_id: int): 382 | wk = Wenku8ToEpub() 383 | filename_ = wk.id2name(book_id) 384 | if filename_ == '': 385 | return '没有这个小说!' 386 | 387 | data = work3(book_id) 388 | fp = io.BytesIO(data) 389 | 390 | # urlencode方案 391 | # filename_ = urllib.parse.urlencode({'': filename_})[1:] + '.epub' 392 | # latin-1 方案 393 | 394 | filename_ = ("%s.epub" % filename_).encode().decode('latin-1') 395 | response = make_response(send_file(fp, attachment_filename="%s" % filename_)) 396 | response.headers["Content-Disposition"] = "attachment; filename=%s;" % filename_ 397 | return response 398 | 399 | # url = work3(book_id) 400 | # return redirect(url) 401 | 402 | 403 | @app.route('/get/') 404 | def get(book_id: int): 405 | wk = Wenku8ToEpub() 406 | filename_ = wk.id2name(book_id) 407 | if filename_ == '': 408 | return '没有这个小说!' 409 | filename = "%s.epub" % filename_ 410 | return redirect('https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename) 411 | 412 | 413 | @app.route('/name/') 414 | def jump_by_name(book_name: str): 415 | filename = "%s.epub" % book_name 416 | return redirect('https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename) 417 | 418 | 419 | @app.route('/search') 420 | def search(): 421 | args0 = dict(request.args) 422 | args = {} 423 | for arg in args0: 424 | v = args0[arg] 425 | if type(v) is list: 426 | args[arg] = v[0] 427 | else: 428 | args[arg] = v 429 | # print(args) 430 | method = args['method'] 431 | search_key = args['search_key'] 432 | bid = None 433 | if method == 'id': 434 | try: 435 | bid = int(search_key) 436 | except ValueError: 437 | return "ID输入错误!" 438 | return redirect('/get/%s' % bid) 439 | elif method == 'name': 440 | return redirect('/name/%s' % search_key) 441 | elif method == 'cache': 442 | return redirect('/cache/%s' % search_key) 443 | elif method == 'cache_img': 444 | return redirect('/cache_img/%s' % search_key) 445 | else: 446 | return '参数不正确' 447 | 448 | 449 | @app.route('/favicon.ico', methods=['GET']) 450 | def favicon(): 451 | return redirect('/static/favicon.ico') 452 | 453 | 454 | @app.route('/baidu_verify_kBBfcDGnTX.html', methods=['GET']) 455 | def baidu_verify(): 456 | return 'kBBfcDGnTX' 457 | 458 | 459 | @app.route('/chat/', methods=['GET', 'POST']) 460 | def server_chat(text): 461 | try: 462 | # 先下载 463 | headers_data = requests.get('https://cdn-1254016670.cos.ap-chengdu.myqcloud.com/headers.txt').content 464 | with open('headers.txt', 'wb') as f: 465 | f.write(headers_data) 466 | response = xiaoice.chat(text) 467 | # 然后上传 468 | with open('headers.txt', 'rb') as f: 469 | response1 = client2.put_object( 470 | Bucket=bucket2, 471 | Body=f.read(), 472 | # Key=filename_md5, 473 | Key="headers.txt", 474 | StorageClass='STANDARD', 475 | EnableMD5=False 476 | ) 477 | logger.info(str(response1)) 478 | except Exception as e: 479 | result = { 480 | 'data': '', 481 | 'other': str(e), 482 | 'code': 1 483 | } 484 | return 'foo(' + json.dumps(result) + ')' 485 | result = { 486 | 'data': response, 487 | 'other': response1, 488 | 'code': 0 489 | } 490 | return 'foo(' + json.dumps(result) + ')' 491 | 492 | 493 | if __name__ == '__main__': 494 | # os.environ['WENKU8_LOCAL'] = "True" 495 | app.run("0.0.0.0", port=int(os.environ.get('PORT', '8000')), debug=False) 496 | 497 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | from qcloud_cos import CosConfig 2 | from qcloud_cos import CosS3Client 3 | from qcloud_cos import CosClientError 4 | import sys 5 | import getopt 6 | import json 7 | import base_logger 8 | from tqdm import * 9 | from wenku8toepub import Wenku8ToEpub, lock, MLogger, logger 10 | from dmzj2epub import Dmzj2Epub 11 | import requests 12 | import threading 13 | import urllib.parse 14 | import os 15 | import io 16 | import asyncio 17 | 18 | # logger = base_logger.getLogger() 19 | th_results = {} 20 | 21 | # 向服务器请求密码 22 | logger.info('正在获取密码...') 23 | password = '1352040930' 24 | 25 | import base64 26 | password_data = json.loads(base64.b64decode("ewogICAgImNvZGUiOiAwLAogICAgImlkIjogIkFLSUQyc1RxenZYN05QQ3JIUlAxUmVjS24wMG1KYmZVT01RRSIsCiAgICAia2V5IjogImlCT001WW1rNUM1anZzWjBEQXJJVE85ZXV1ZkNhbWtUIgp9").decode()) 27 | if not password_data['code'] == 0: 28 | logger.error('密码无效!进入只读模式!') 29 | logger.info('密码正确!') 30 | 31 | secret_id = password_data['id'] 32 | secret_key = password_data['key'] 33 | region = 'ap-guangzhou' 34 | region2 = 'ap-chengdu' 35 | 36 | # NO提高超时时间 37 | # config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Timeout=120) 38 | config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) 39 | config2 = CosConfig(Region=region2, SecretId=secret_id, SecretKey=secret_key) 40 | # 2. 获取客户端对象 41 | # NO增大重试次数 42 | # client = CosS3Client(config, retry=5) 43 | client = CosS3Client(config) 44 | client2 = CosS3Client(config2) 45 | 46 | bucket = 'light-novel-1254016670' 47 | bucket2 = 'cdn-1254016670' 48 | 49 | 50 | str_jump = '''''' 51 | 52 | 53 | def work2(book_id: int, filename: str = None): 54 | wk = Wenku8ToEpub() 55 | if filename is None: 56 | filename_ = wk.id2name(book_id) 57 | if filename == '': 58 | return 59 | filename = "%s.epub" % filename_ 60 | response = client.put_object( 61 | Bucket=bucket, 62 | Body=(str_jump % filename).encode('gbk'), 63 | # Body=(str_jump % ("https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/" + urllib.quote(filename))).encode('utf-8'), 64 | # Key=filename_md5, 65 | Key="%s.html" % (book_id, ), 66 | StorageClass='STANDARD', 67 | EnableMD5=False 68 | ) 69 | logger.info("%s OK." % filename) 70 | 71 | 72 | def work(book_id: int, filename: str = None): 73 | wk = Wenku8ToEpub() 74 | if filename is None: 75 | filename_ = wk.id2name(book_id) 76 | if filename == '': 77 | return 78 | filename = "%s.epub" % filename_ 79 | data = wk.get_book(book_id, bin_mode=True, fetch_image=False) 80 | response1 = client.put_object( 81 | Bucket=bucket, 82 | Body=data, 83 | # Key=filename_md5, 84 | Key="%s" % (filename, ), 85 | StorageClass='STANDARD', 86 | EnableMD5=False 87 | ) 88 | # response2 = client.put_object( 89 | # Bucket=bucket, 90 | # Body=(str_jump % filename).encode('gbk'), 91 | # # Key=filename_md5, 92 | # Key="%s.html" % (book_id, ), 93 | # StorageClass='STANDARD', 94 | # EnableMD5=False 95 | # ) 96 | # logger.info("%s OK. %s %s" % (filename, str(response1), str(response2))) 97 | logger.info("%s OK. %s" % (filename, str(response1))) 98 | return 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 99 | 100 | 101 | def work3(book_id: int, filename: str = None): 102 | wk = Wenku8ToEpub() 103 | if filename is None: 104 | filename_ = wk.id2name(book_id) 105 | if filename == '': 106 | return 107 | filename = "%s.epub" % filename_ 108 | data = wk.get_book(book_id, bin_mode=True, fetch_image=True) 109 | # response1 = client.put_object( 110 | # Bucket=bucket, 111 | # Body=data, 112 | # # Key=filename_md5, 113 | # Key="%s" % (filename, ), 114 | # StorageClass='STANDARD', 115 | # EnableMD5=False 116 | # ) 117 | # response2 = client.put_object( 118 | # Bucket=bucket, 119 | # Body=(str_jump % filename).encode('gbk'), 120 | # # Key=filename_md5, 121 | # Key="%s.html" % (book_id, ), 122 | # StorageClass='STANDARD', 123 | # EnableMD5=False 124 | # ) 125 | # logger.info("%s OK. %s %s" % (filename, str(response1), str(response2))) 126 | # return 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 127 | logger.info("%s OK。(No Cache.)" % (filename,)) 128 | return data 129 | 130 | 131 | def work4(book_id: int, filename: str = None): 132 | wk = Wenku8ToEpub() 133 | if filename is None: 134 | filename_ = wk.id2name(book_id) 135 | if filename == '': 136 | return 137 | filename = "%s.epub" % filename_ 138 | data = wk.get_book(book_id, bin_mode=True, fetch_image=True) 139 | response1 = client.put_object( 140 | Bucket=bucket, 141 | Body=data, 142 | # Key=filename_md5, 143 | Key="%s" % (filename, ), 144 | StorageClass='STANDARD', 145 | EnableMD5=False 146 | ) 147 | response2 = client.put_object( 148 | Bucket=bucket, 149 | Body=(str_jump % filename).encode('gbk'), 150 | # Key=filename_md5, 151 | Key="%s.html" % (book_id, ), 152 | StorageClass='STANDARD', 153 | EnableMD5=False 154 | ) 155 | logger.info("%s OK. %s %s" % (filename, str(response1), str(response2))) 156 | return 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 157 | # logger.info("%s OK。(No Cache.)" % (filename,)) 158 | # return data 159 | 160 | 161 | def my_upload_file(key, data): 162 | # 最后尝试 163 | data.seek(0) 164 | client.upload_file_from_buffer( 165 | Bucket=bucket, 166 | Body=data, 167 | # Key=filename_md5, 168 | Key=key, 169 | StorageClass='STANDARD', 170 | # PartSize=1, 171 | # MAXThread=10 172 | ) 173 | 174 | 175 | def v2_work(book_id: int, filename: str = None, mlogger=None, image=False): 176 | wk = Wenku8ToEpub() 177 | if filename is None: 178 | filename_ = wk.id2name(book_id) 179 | if filename == '': 180 | return 181 | filename = "%s.epub" % filename_ 182 | # 设置最大图像规模为3MB 183 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 184 | image_size = None 185 | else: 186 | image_size = 3 * 1024 * 1024 187 | data = wk.get_book(book_id, bin_mode=True, fetch_image=image, mlogger=mlogger, image_size=image_size) 188 | mlogger.info('小说获取完毕,准备上传到腾讯云...') 189 | try: 190 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 191 | response1 = client.put_object( 192 | Bucket=bucket, 193 | Body=data, 194 | # Key=filename_md5, 195 | Key="%s" % (filename,), 196 | StorageClass='STANDARD', 197 | EnableMD5=False 198 | ) 199 | else: 200 | raise CosClientError("腾讯云上传取消。") 201 | # 小心内存过大 202 | except Exception as e: 203 | mlogger.warn("%s 腾讯云上传错误,准备直接返回临时下载链接..." % str(e)) 204 | # 保存到本地 205 | with open('static/%s' % filename, 'wb') as f: 206 | f.write(data) 207 | filename = urllib.parse.quote(filename) 208 | url = '/static/%s' % filename 209 | lock.acquire() 210 | th_results[str(book_id)] = url 211 | lock.release() 212 | # 再开个线程再次尝试上传 213 | # threading.Thread(target=my_upload_file, args=("%s" % (filename,), bio)).start() 214 | return url 215 | mlogger.info("%s OK. %s" % (filename, str(response1))) 216 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 217 | with open('static/%s' % filename, 'wb') as f: 218 | f.write(data) 219 | filename = urllib.parse.quote(filename) 220 | url = '/static/%s' % filename 221 | else: 222 | filename = urllib.parse.quote(filename) 223 | url = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 224 | lock.acquire() 225 | th_results[str(book_id)] = url 226 | lock.release() 227 | return url 228 | 229 | 230 | def v2_dmzj_work(book_id: int, filename: str = None, mlogger=None, image=False): 231 | de = Dmzj2Epub(logger=mlogger) 232 | if filename is None: 233 | info = de.info(book_id) 234 | if info is None: 235 | return 236 | filename_ = info['name'] 237 | filename = "dmzj_%s.epub" % filename_ 238 | # 设置最大图像规模为3MB 239 | if os.environ.get('WENKU8_LOCAL', 'False') != 'True': 240 | image = False 241 | data = asyncio.run(de.download_book(book_id, fetch_image=image)) 242 | mlogger.info('小说获取完毕,准备上传到腾讯云...') 243 | try: 244 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 245 | response1 = client.put_object( 246 | Bucket=bucket, 247 | Body=data, 248 | # Key=filename_md5, 249 | Key="%s" % (filename,), 250 | StorageClass='STANDARD', 251 | EnableMD5=False 252 | ) 253 | else: 254 | raise CosClientError("腾讯云上传取消。") 255 | # 小心内存过大 256 | except Exception as e: 257 | mlogger.warn("%s 腾讯云上传错误,准备直接返回临时下载链接..." % str(e)) 258 | # 保存到本地 259 | with open('static/%s' % filename, 'wb') as f: 260 | f.write(data) 261 | filename = urllib.parse.quote(filename) 262 | url = '/static/%s' % filename 263 | lock.acquire() 264 | th_results['dmzj_' + str(book_id)] = url 265 | lock.release() 266 | # 再开个线程再次尝试上传 267 | # threading.Thread(target=my_upload_file, args=("%s" % (filename,), bio)).start() 268 | return url 269 | mlogger.info("%s OK. %s" % (filename, str(response1))) 270 | if os.environ.get('WENKU8_LOCAL', 'False') == 'True': 271 | with open('static/%s' % filename, 'wb') as f: 272 | f.write(data) 273 | filename = urllib.parse.quote(filename) 274 | url = '/static/%s' % filename 275 | else: 276 | filename = urllib.parse.quote(filename) 277 | url = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com/%s' % filename 278 | lock.acquire() 279 | th_results['dmzj_' + str(book_id)] = url 280 | lock.release() 281 | return url 282 | 283 | 284 | def v2_check_time(key): 285 | response = client.list_objects( 286 | Bucket=bucket, 287 | Prefix=key 288 | ) 289 | if 'Contents' not in response or len(response['Contents']) == 0: 290 | return None 291 | return response['Contents'][0]['LastModified'] 292 | 293 | 294 | def make_urls(): 295 | method = 'GET' 296 | # 30分钟有效 297 | expired = 30 * 60 298 | req = { 299 | 'static-1254016670': ['wk8local.exe', 'wenku8toepub.exe', 'Wenku8下载_1.1.apk', 300 | '网易云音乐下载器_1.2.apk', '方寸之间_2.31.apk'] 301 | } 302 | urls = [] 303 | for r in req: 304 | for k in req[r]: 305 | urls.append(client2.get_presigned_download_url( 306 | Bucket=r, 307 | Key=k, 308 | Expired=expired 309 | )) 310 | # 更换为百度云链接 311 | urls[0] = 'https://pan.baidu.com/s/1FljnyZQK2VdeZIl-kd90lw' 312 | urls[1] = 'https://pan.baidu.com/s/1FljnyZQK2VdeZIl-kd90lw' 313 | return urls 314 | 315 | 316 | if __name__ == '__main__': 317 | opts, args = getopt.getopt(sys.argv[1:], '-s:-e:-b', []) 318 | start = 1 319 | end = 3000 320 | for name, val in opts: 321 | if name == '-s': 322 | try: 323 | start = int(val) 324 | except ValueError as e: 325 | logger.error(str(e)) 326 | sys.exit() 327 | if name == '-e': 328 | try: 329 | end = int(val) 330 | except ValueError as e: 331 | logger.error(str(e)) 332 | sys.exit() 333 | if name == '-b': 334 | for _book_id in trange(start, end + 1, 1): 335 | try: 336 | work2(_book_id) 337 | except Exception as e: 338 | logger.critical(str(e)) 339 | sys.exit() 340 | 341 | for _book_id in trange(start, end + 1, 1): 342 | try: 343 | work(_book_id) 344 | except Exception as e: 345 | logger.critical(str(e)) -------------------------------------------------------------------------------- /opds/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | .pyc -------------------------------------------------------------------------------- /opds/Config.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | 3 | __author__ = 'lei' 4 | 5 | # ############################# 6 | # root for opds server website 7 | # SITE_URL = "http://10.10.113.237:5000" 8 | # SITE_URL = "http://opds.cockybook.com" 9 | SITE_URL = '/opds' 10 | # SITE_URL = 'http://192.168.43.203:10086' 11 | # SITE_URL = 'https://light-opds.herokuapp.com' 12 | SITE_TITLE = "Light Novels OPDS Site" 13 | SITE_EMAIL = "LanceLiang2018@163.com" 14 | SITE_BOOK_LIST = SITE_URL + "/list" 15 | 16 | # for local filesyste 17 | base = "/home/lance/Books" 18 | 19 | # Used In opdscore.py 20 | # filesyste_type = 'LocalFileSystem' 21 | # filesyste_type = 'QiniuFileSystem' 22 | filesyste_type = 'TencentFileSystem' 23 | # filesyste_type = 'LocalMetadataFileSystem' 24 | 25 | # download URL is SITE_BOOK_DONWLOAD/$path/$filename.$postfix 26 | # SITE_BOOK_DONWLOAD = 'http://7sbqcs.com1.z0.glb.clouddn.com' 27 | if filesyste_type == 'TencentFileSystem': 28 | SITE_BOOK_DONWLOAD = 'https://light-novel-1254016670.cos.ap-guangzhou.myqcloud.com' 29 | else: 30 | SITE_BOOK_DONWLOAD = 'http://192.168.43.203:10086/static/Books' 31 | 32 | 33 | description = u""" 34 | OPDS 标准核心功能是支持 EPUB 标准和基于 Atom XML 的目录格式. 35 | 可以使用阅读器进行在线书库添加,比如FBReader、静读天下(Moon+ Reader)、Aldiko、Stanza等等. 36 | 添加地址为: %s 37 | (轻小说书源提供&修改代码by LanceLiang2018@163.com) 38 | """ % SITE_URL 39 | -------------------------------------------------------------------------------- /opds/Const.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | __author__ = 'lei' 3 | 4 | id = "id" 5 | title = "title" 6 | updated = "updated" 7 | icon = "icon" 8 | author = "author" 9 | link = "link" 10 | description = "description" 11 | search = 'search' 12 | 13 | ##################### 14 | 15 | entry = "entry" 16 | entry_title = "title" 17 | entry_link = "link" 18 | entry_updated = "updated" 19 | entry_id = "id" 20 | entry_type = "type" 21 | entry_content = "content" 22 | 23 | ############## 24 | book_type_pdf = "application/pdf" 25 | book_type_epub = "application/epub+zip" 26 | book_type_mobi = "application/x-mobipocket-ebook" 27 | ##book page 28 | book_type_picture = "image/jpeg;image/png" 29 | ##html open 30 | book_type_html = "text/html" 31 | book_type_text = "text/plain" 32 | book_type_content = "text" 33 | book_type_entry_catalog = "application/atom+xml;type=entry;profile=opds-catalog" 34 | 35 | ######book_link_type 36 | book_link_rel_subsection = "subsection" 37 | book_link_rel_image = "http://opds-spec.org/image" 38 | book_link_rel__image_thumbnail = "http://opds-spec.org/image/thumbnail" 39 | book_link_rel__acquisition = "http://opds-spec.org/acquisition" 40 | -------------------------------------------------------------------------------- /opds/Procfile: -------------------------------------------------------------------------------- 1 | web: python opdsserver.py 2 | -------------------------------------------------------------------------------- /opds/Procfile.windows: -------------------------------------------------------------------------------- 1 | web: python opdsserver.py 2 | -------------------------------------------------------------------------------- /opds/README.MD: -------------------------------------------------------------------------------- 1 | #cockybook 2 | 演示地址: [http://opds.cockybook.com/][2] 3 | 4 | OPDS 标准核心功能是支持 EPUB 标准和基于 Atom XML 的目录格式. 可以使用阅读器进行在线书库添加,比如FBReader、静读天下(Moon+ Reader)、Aldiko、Stanza等等 5 | 6 | ##cockybook简介 7 | cockybook是python开发的一个opds server 的简易书籍共享服务。 8 | 使用python Flask实现。 9 | 他的数据源可以是本地存储,也可以是云存储如百度网盘、七牛云存储。当然你可以很方便的自定义实现自己的数据源接口。 10 | 11 | ##What is OPDS? 12 | OPDS全称是Open Publication Distribution System开放式出版发布系统,使用 Atom 格式,意在为电子书在线目录建立一个公开标准。OPDS 将 RSS 信息源,替换为电子书目录,包括链接到书籍封面和简短摘要的可选链接。使用 OPDS,用户无需到处点击链接,通过电子书应用程序,只需订阅并搜索这些目录,然后就可以将电子书下载到 ebook 阅读器中,不需要再使用浏览器或其他应用程序。 13 | OPDS 标准核心功能是支持 EPUB 标准和基于 Atom XML 的目录格式。 14 | 15 | ##OPDS协议 16 | 17 | 可以参考博客:[http://www.cockybook.com/?p=159][3] 18 | 19 | ##入口 20 | `opdsserver.py` 21 | 可以直接启动`opdsserver.py` 22 | 这里你可以找到: 23 | 24 | ``` 25 | if __name__ == "__main__": 26 | 27 | logging.basicConfig(level=logging.DEBUG, 28 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 29 | ) 30 | app.debug = True 31 | app.run(host='0.0.0.0') 32 | ``` 33 | 34 | ##程序结构 35 | 文件系统接口,你好可以自由扩展。 36 | `opdscore.py` 中主要类: 37 | 这个类描述需要实现的主要接口。 38 | ``` 39 | class OpdsProtocol: 40 | def listBooks(self, path): 41 | return ("No Realized") 42 | pass 43 | def dowloadBook(self, path): 44 | return ("No Realized") 45 | pass 46 | def showhtml(self): 47 | return ("No Realized") 48 | pass 49 | ``` 50 | 51 | `filesystem.py` 52 | 你可以在这个类中实现你的数据源文件操作。 53 | 目前实现的有LocalFileSystem、QiniuFileSystem。 54 | 55 | ``` 56 | class LocalMetadataFileSystem(FileSystem): 57 | class QiniuFileSystem(FileSystem): 58 | ``` 59 | 60 | 61 | ##部署 62 | 1. 目前可以在[sinaapp][1]中直接部署。index.wsgi 已经写好。 63 | 2. 可以直接运行`opdsserver.py`进行发布。 64 | 65 | 66 | [1]: http://sinaapp.com/ 67 | [2]: http://opds.cockybook.com/ 68 | [3]: http://www.cockybook.com/?p=159 -------------------------------------------------------------------------------- /opds/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Start on Heroku: Python", 3 | "description": "A barebones Python app, which can easily be deployed to Heroku.", 4 | "image": "heroku/python", 5 | "repository": "https://github.com/heroku/python-getting-started", 6 | "keywords": ["python", "django" ], 7 | "addons": [ "heroku-postgresql" ], 8 | "env": { 9 | "SECRET_KEY": { 10 | "description": "The secret key for the Django application.", 11 | "generator": "secret" 12 | } 13 | }, 14 | "environments": { 15 | "test": { 16 | "scripts": { 17 | "test-setup": "python manage.py collectstatic --noinput", 18 | "test": "python manage.py test" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /opds/config.yaml: -------------------------------------------------------------------------------- 1 | name: opdscockybook 2 | version: 1 -------------------------------------------------------------------------------- /opds/filesystem.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | import logging 3 | 4 | import os 5 | # import urllib2 6 | import requests 7 | import opds.Config as Config 8 | import json 9 | 10 | from opds.utils import connect_path, getFile 11 | bookdata = {} 12 | 13 | from qcloud_cos import CosConfig 14 | from qcloud_cos import CosS3Client 15 | 16 | 17 | __author__ = 'lei' 18 | __author2__ = 'Lance' 19 | 20 | 21 | # base="/home/cocky" 22 | class FileSystem: 23 | def outErr(self): 24 | logging.error("No Realyzed") 25 | 26 | def exists(self, path): 27 | self.outErr() 28 | pass 29 | 30 | def isfile(self, path): 31 | self.outErr() 32 | pass 33 | 34 | def listdir(self, path): 35 | self.outErr() 36 | return [] 37 | pass 38 | 39 | def getdownloadurl(self, path, name): 40 | self.outErr() 41 | return "" 42 | 43 | 44 | class LocalFileSystem(FileSystem): 45 | """ 46 | config the #Config.base 47 | """ 48 | 49 | def __init__(self): 50 | self.path = '' 51 | 52 | def exists(self, path): 53 | if path is None: 54 | path = self.path 55 | return os.path.exists(connect_path(Config.base, path)) 56 | 57 | def isfile(self, path): 58 | if path is None: 59 | path = self.path 60 | # print('isf', connect_path(Config.base, path)) 61 | return os.path.isfile(connect_path(Config.base, path)) 62 | 63 | def listdir(self, path): 64 | if path is None: 65 | path = self.path 66 | # print('listdir', os.listdir(connect_path(Config.base, path))) 67 | return os.listdir(connect_path(Config.base, path)) 68 | 69 | def getdownloadurl(self, path, name): 70 | # print('down url:', connect_path(connect_path(Config.SITE_BOOK_DONWLOAD, path), name)) 71 | # 这里有问题。已经修改 72 | return [connect_path(connect_path(Config.SITE_BOOK_DONWLOAD, path), name), ] 73 | 74 | 75 | class LocalMetadataFileSystem(FileSystem): 76 | # q = Auth(Config.access_key, Config.secret_key) 77 | 78 | # bucket = BucketManager(q) 79 | def __init__(self): 80 | ff = open('metadata.json', 'r') 81 | 82 | self.book_trees = json.load(ff) 83 | 84 | def exists(self, path): 85 | files = getFile(self.book_trees, self.getTruePaths(path)) 86 | return files != None 87 | 88 | def isfile(self, path): 89 | if path is None: 90 | return False 91 | # ???为啥放_-_ 92 | if path.find('_-_') == -1: 93 | return False 94 | else: 95 | return True 96 | 97 | def listdir(self, path): 98 | paths = self.getTruePaths(path) 99 | 100 | if len(paths) != 0: 101 | return getFile(self.book_trees, paths) 102 | else: 103 | return self.book_trees 104 | 105 | def getTruePaths(self, tmp): 106 | """ 107 | :param tmp: 108 | :return: 109 | """ 110 | paths = tmp.split('/') 111 | paths = [p for p in paths if p != ''] 112 | return paths 113 | 114 | def getdownloadurl(self, path, name): 115 | tmp = connect_path(path, name) 116 | 117 | files = getFile(self.book_trees, self.getTruePaths(tmp)) 118 | 119 | return [connect_path(Config.SITE_BOOK_DONWLOAD, connect_path(path, ee)) for ee in files] 120 | 121 | 122 | class QiniuFileSystem(FileSystem): 123 | # q = Auth(Config.access_key, Config.secret_key) 124 | 125 | # bucket = BucketManager(q) 126 | def __init__(self): 127 | # resp=urllib2.urlopen(connect_path(Config.SITE_BOOK_DONWLOAD,'metadata.json')) 128 | resp = requests.get(connect_path(Config.SITE_BOOK_DONWLOAD, 'metadata.json')) 129 | if resp.status_code == 200: 130 | self.book_trees = json.loads(resp.text) 131 | 132 | def outErr(self): 133 | logging.error("No Realyzed") 134 | 135 | def exists(self, path): 136 | files = getFile(self.book_trees, self.getTruePaths(path)) 137 | # logging.info(len(files)!=0) 138 | return len(files) != 0 139 | 140 | def isfile(self, path): 141 | if path.find('_-_') == -1: 142 | return False 143 | else: 144 | return True 145 | 146 | def listdir(self, path): 147 | paths = self.getTruePaths(path) 148 | 149 | if len(paths) != 0: 150 | return getFile(self.book_trees, paths) 151 | else: 152 | return self.book_trees 153 | 154 | def getTruePaths(self, tmp): 155 | """ 156 | :param tmp: 157 | :return: 158 | """ 159 | paths = tmp.split('/') 160 | paths = [p for p in paths if p != ''] 161 | return paths 162 | 163 | def getdownloadurl(self, path, name): 164 | tmp = connect_path(path, name) 165 | 166 | files = getFile(self.book_trees, self.getTruePaths(tmp)) 167 | 168 | return [connect_path(Config.SITE_BOOK_DONWLOAD, connect_path(path, ee)) for ee in files] 169 | 170 | 171 | class TencentFileSystem(FileSystem): 172 | 173 | def __init__(self): 174 | # 向服务器请求密码 175 | logging.info('正在获取密码...') 176 | password = '1352040930' 177 | 178 | import base64 179 | password_data = json.loads(base64.b64decode("ewogICAgImNvZGUiOiAwLAogICAgImlkIjogIkFLSUQyc1RxenZYN05QQ3JIUlAxUmVjS24wMG1KYmZVT01RRSIsCiAgICAia2V5IjogImlCT001WW1rNUM1anZzWjBEQXJJVE85ZXV1ZkNhbWtUIgp9").decode()) 180 | if not password_data['code'] == 0: 181 | logging.error('密码无效!进入只读模式!') 182 | logging.info('密码正确!') 183 | 184 | secret_id = password_data['id'] 185 | secret_key = password_data['key'] 186 | region = 'ap-guangzhou' 187 | config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) 188 | # 2. 获取客户端对象 189 | self.client = CosS3Client(config) 190 | self.bucket = 'light-novel-1254016670' 191 | self.booklist = [] 192 | self.bookdata = [] 193 | 194 | def outErr(self): 195 | logging.error("Tencent File System Error...") 196 | 197 | def exists(self, path): 198 | # resp = self.client.list_objects(Bucket=self.bucket, 199 | # Prefix=path, 200 | # MaxKeys=1) 201 | # data = dict(resp) 202 | # if 'Contents' not in data: 203 | # return False 204 | return True 205 | 206 | def isfile(self, path): 207 | if path == '/': 208 | return False 209 | # resp = self.client.list_objects(Bucket=self.bucket, 210 | # Prefix=path, 211 | # MaxKeys=1) 212 | # data = dict(resp) 213 | # if 'Contents' not in data: 214 | # return False 215 | return True 216 | 217 | def listdir(self, path, page=4): 218 | if path is None or len(path) == 0: 219 | path = '' 220 | elif path[0] == '/': 221 | path = path[1:] 222 | last_marker = '' 223 | # page = 1.2.3.4... 224 | data = None 225 | self.booklist = [] 226 | self.bookdata = [] 227 | 228 | while page > 0: 229 | resp = self.client.list_objects(Bucket=self.bucket, 230 | Prefix=path, 231 | MaxKeys=1000, 232 | Marker=last_marker, 233 | ) 234 | data = dict(resp) 235 | # print(data) 236 | # 最后一页 237 | if 'NextMarker' not in data: 238 | break 239 | last_marker = data['NextMarker'] 240 | page -= 1 241 | 242 | if 'Contents' not in data: 243 | return self.booklist 244 | for book in data['Contents']: 245 | key, last_modified, e_tag, size = book['Key'], book['LastModified'], book['ETag'], book['Size'] 246 | self.booklist.append(key) 247 | self.bookdata.append({ 248 | 'key': key, 249 | 'last_modified': last_modified, 250 | 'e_tag': e_tag, 251 | 'size': size 252 | }) 253 | 254 | if data is None: 255 | return [] 256 | global bookdata 257 | bookdata = {} 258 | for d in self.bookdata: 259 | bookdata[d['key']] = d 260 | return self.booklist 261 | 262 | def getTruePaths(self, tmp): 263 | return '' 264 | 265 | def getdownloadurl(self, path, name): 266 | urls = [] 267 | # for book in self.booklist: 268 | # urls.append(Config.SITE_BOOK_DONWLOAD + book) 269 | urls.append(Config.SITE_BOOK_DONWLOAD + path + '/' + name) 270 | return urls 271 | 272 | 273 | if __name__ == '__main__': 274 | _fs = TencentFileSystem() 275 | print(_fs.listdir('/')) 276 | -------------------------------------------------------------------------------- /opds/generate.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | import os, json, sys 3 | 4 | 5 | def getTree(path): 6 | rs = {} 7 | for filename in os.listdir(path): 8 | 9 | print("filename :", filename) 10 | tmpname = os.path.join(path, filename) 11 | if os.path.isdir(tmpname): 12 | rs[filename] = getTree(tmpname) 13 | else: 14 | justname = filename[:filename.rfind('.'):] 15 | 16 | # if rs.has_key(justname): 17 | if justname in rs: 18 | rs[justname].append(filename) 19 | else: 20 | rs[justname] = [filename] 21 | return rs 22 | 23 | 24 | def writeMetadata(rsjson): 25 | ff = open("metadata.json", mode='w') 26 | 27 | ff.write(json.dumps(rsjson, indent=4, encoding='gbk').encode('utf8')) 28 | ff.close() 29 | 30 | 31 | def generateMetadataXml(): 32 | rsjson = getTree('.') 33 | # if rsjson.has_key('generate'): 34 | if 'generate' in rsjson: 35 | rsjson.pop('generate') 36 | 37 | # if rsjson.has_key('metadata'): 38 | if 'metadata' in rsjson: 39 | rsjson.pop('metadata') 40 | 41 | writeMetadata(rsjson) 42 | 43 | 44 | def getFile(jjson, paths): 45 | if len(paths) == 1: 46 | if paths[0] == '': 47 | return jjson 48 | # elif jjson.has_key(paths[0]): 49 | elif paths[0] in jjson: 50 | 51 | return jjson[paths[0]] 52 | else: 53 | print('Jjson', json.dumps(jjson)) 54 | print('No this Key:', paths[0]) 55 | return None 56 | elif len(paths) > 1: 57 | return getFile(jjson[paths[0]], paths[1:]) 58 | 59 | 60 | if __name__ == '__main__': 61 | generateMetadataXml() 62 | 63 | # rsjson=getTree(".") 64 | # print json.dumps(rsjson,encoding='gbk') 65 | # print getFile(rsjson, '/佛学'.split('/')[1:]) 66 | 67 | pass 68 | -------------------------------------------------------------------------------- /opds/index.wsgi: -------------------------------------------------------------------------------- 1 | import sae 2 | from opdsserver import app 3 | 4 | application = sae.create_wsgi_app(app) 5 | -------------------------------------------------------------------------------- /opds/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "\u4f5b\u5b66": { 3 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e": [ 4 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e.jpg", 5 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e.mobi", 6 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e.opf", 7 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e.pdf", 8 | "\u836f\u5e08\u7ecf\u7684\u6d4e\u4e16\u89c2_-_\u5357\u6000\u747e.txt" 9 | ], 10 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e": [ 11 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e.jpg", 12 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e.mobi", 13 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e.opf", 14 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e.pdf", 15 | "\u4e00\u4e2a\u5b66\u4f5b\u8005\u7684\u57fa\u672c\u4fe1\u5ff5\u2014\u2014\u534e\u4e25\u7ecf\u666e\u8d24\u884c\u613f\u54c1\u8bb2\u5f55_-_\u5357\u6000\u747e.txt" 16 | ] 17 | }, 18 | "\u6570\u5b66": { 19 | "\u7edf\u8ba1\u5b66\u4e60\u65b9\u6cd5_-_\u674e\u822a": [ 20 | "\u7edf\u8ba1\u5b66\u4e60\u65b9\u6cd5_-_\u674e\u822a.epub", 21 | "\u7edf\u8ba1\u5b66\u4e60\u65b9\u6cd5_-_\u674e\u822a.jpg", 22 | "\u7edf\u8ba1\u5b66\u4e60\u65b9\u6cd5_-_\u674e\u822a.mobi", 23 | "\u7edf\u8ba1\u5b66\u4e60\u65b9\u6cd5_-_\u674e\u822a.opf" 24 | ] 25 | }, 26 | "\u5fc3\u7406\u5b66": { 27 | "\u4e4c\u5408\u4e4b\u4f17\u2014\u5927\u4f17\u5fc3\u7406\u7814\u7a76_-_\u53e4\u65af\u5854\u592b\u00b7\u52d2\u5e9e": [ 28 | "\u4e4c\u5408\u4e4b\u4f17\u2014\u5927\u4f17\u5fc3\u7406\u7814\u7a76_-_\u53e4\u65af\u5854\u592b\u00b7\u52d2\u5e9e.jpg", 29 | "\u4e4c\u5408\u4e4b\u4f17\u2014\u5927\u4f17\u5fc3\u7406\u7814\u7a76_-_\u53e4\u65af\u5854\u592b\u00b7\u52d2\u5e9e.mobi", 30 | "\u4e4c\u5408\u4e4b\u4f17\u2014\u5927\u4f17\u5fc3\u7406\u7814\u7a76_-_\u53e4\u65af\u5854\u592b\u00b7\u52d2\u5e9e.opf" 31 | ] 32 | }, 33 | "IT\u8bfb\u7269": { 34 | "\u9ed1\u5ba2\u4e0e\u753b\u5bb6_-_\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6": [ 35 | "\u9ed1\u5ba2\u4e0e\u753b\u5bb6_-_\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6.epub", 36 | "\u9ed1\u5ba2\u4e0e\u753b\u5bb6_-_\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6.jpg", 37 | "\u9ed1\u5ba2\u4e0e\u753b\u5bb6_-_\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6.mobi", 38 | "\u9ed1\u5ba2\u4e0e\u753b\u5bb6_-_\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6.opf" 39 | ], 40 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14": [ 41 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14.epub", 42 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14.jpg", 43 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14.mobi", 44 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14.opf", 45 | "\u5f02\u7c7b-\u8001\u7f57\u63a8\u8350_-_\u9a6c\u5c14\u79d1\u59c6\u00b7\u683c\u62c9\u5fb7\u5a01\u5c14.pdf" 46 | ], 47 | "\u7985\u4e0e\u6469\u6258\u8f66\u7ef4\u4fee\u827a\u672f_-_\u7f57\u4f2f\u7279\u00b7M\u00b7\u6ce2\u897f\u683c": [ 48 | "\u7985\u4e0e\u6469\u6258\u8f66\u7ef4\u4fee\u827a\u672f_-_\u7f57\u4f2f\u7279\u00b7M\u00b7\u6ce2\u897f\u683c.jpg", 49 | "\u7985\u4e0e\u6469\u6258\u8f66\u7ef4\u4fee\u827a\u672f_-_\u7f57\u4f2f\u7279\u00b7M\u00b7\u6ce2\u897f\u683c.mobi", 50 | "\u7985\u4e0e\u6469\u6258\u8f66\u7ef4\u4fee\u827a\u672f_-_\u7f57\u4f2f\u7279\u00b7M\u00b7\u6ce2\u897f\u683c.opf" 51 | ] 52 | }, 53 | "\u8f6f\u8003": { 54 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com": [ 55 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com.epub", 56 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com.jpg", 57 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com.mobi", 58 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com.opf", 59 | "\u7cfb\u7edf\u96c6\u6210\u9879\u76ee\u7ba1\u7406\u5de5\u7a0b\u5e08_-_wwww.cockybook.com.pdf" 60 | ] 61 | }, 62 | "\u79d1\u6280": { 63 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang": [ 64 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang.epub", 65 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang.jpg", 66 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang.mobi", 67 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang.opf", 68 | "Protocol_\u534f\u8bae\u68ee\u6797_-_Tengfei_Zhang.pdf" 69 | ], 70 | "Flask_0.10.1_\u6587\u6863_-_Armin_Ronacher": [ 71 | "Flask_0.10.1_\u6587\u6863_-_Armin_Ronacher.jpg", 72 | "Flask_0.10.1_\u6587\u6863_-_Armin_Ronacher.mobi", 73 | "Flask_0.10.1_\u6587\u6863_-_Armin_Ronacher.opf" 74 | ], 75 | "\u4e16\u754c\u662f\u6570\u5b57\u7684_-_\u7a00\u9177\u5ba2(www.ckook.com)": [ 76 | "\u4e16\u754c\u662f\u6570\u5b57\u7684_-_\u7a00\u9177\u5ba2(www.ckook.com).jpg", 77 | "\u4e16\u754c\u662f\u6570\u5b57\u7684_-_\u7a00\u9177\u5ba2(www.ckook.com).mobi", 78 | "\u4e16\u754c\u662f\u6570\u5b57\u7684_-_\u7a00\u9177\u5ba2(www.ckook.com).opf", 79 | "\u4e16\u754c\u662f\u6570\u5b57\u7684_-_\u7a00\u9177\u5ba2(www.ckook.com).pdf" 80 | ] 81 | } 82 | } -------------------------------------------------------------------------------- /opds/opdscore.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | import logging 3 | import os 4 | from xml.dom.minidom import Document, Text, Element 5 | import datetime 6 | from flask import g 7 | import opds.Config as Config 8 | import opds.Const as Const 9 | from opds.filesystem import LocalFileSystem, QiniuFileSystem, LocalMetadataFileSystem, TencentFileSystem 10 | import opds.utils as utils 11 | 12 | __author__ = 'lei' 13 | if Config.filesyste_type == 'LocalFileSystem': 14 | fs = LocalFileSystem() 15 | elif Config.filesyste_type == 'LocalMetadataFileSystem': 16 | fs = LocalMetadataFileSystem() 17 | elif Config.filesyste_type == 'TencentFileSystem': 18 | fs = TencentFileSystem() 19 | else: 20 | fs = QiniuFileSystem() 21 | 22 | 23 | def setfeedNS(feed): 24 | feed.setAttribute("xmlns:app", "http://www.w3.org/2007/app") 25 | feed.setAttribute("xmlns:opds", "http://opds-spec.org/2010/catalog") 26 | feed.setAttribute("xmlns:opds", Config.SITE_URL) 27 | feed.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") 28 | # feed.setAttribute("xmlns", "http://www.w3.org/2005/Atom") 29 | feed.setAttribute("xmlns:dcterms", "http://purl.org/dc/terms/") 30 | feed.setAttribute("xmlns:thr", "http://purl.org/syndication/thread/1.0") 31 | feed.setAttribute("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/") 32 | 33 | 34 | def getCreateDate(file_path): 35 | # return datetime.datetime.now(os.path.getctime(file_path)).strftime("%Y-%m-%dT%I:%M:%SZ") 36 | return datetime.datetime.now().strftime("%Y-%m-%dT%I:%M:%SZ") 37 | 38 | 39 | def create_entry(isFile, path, name): 40 | ''' 41 | create filesystem return object 42 | :param isFile: 43 | :param path: 44 | :param name: 45 | :return: 46 | ''' 47 | entry = Entry() 48 | if not isFile: 49 | entry.id = utils.connect_path(utils.connect_path(Config.SITE_BOOK_LIST, path), name) 50 | entry.links = [] 51 | entry.links.append(Link(entry.id, _get_book_entry_rel(name), name, _get_book_entry_type(name))) 52 | else: 53 | entry.id = utils.connect_path(utils.connect_path(Config.SITE_BOOK_LIST, path), name) 54 | # TODO add Another Links 55 | links = fs.getdownloadurl(path, name) 56 | # name=os.path.basename(path) 57 | entry.links = [] 58 | if links != None: 59 | for link in links: 60 | entry.links.append(Link(link, _get_book_entry_rel(link), name, _get_book_entry_type(link))) 61 | entry.content = name 62 | entry.title = name 63 | entry.updated = utils.getUpdateTime(name) 64 | return entry 65 | 66 | 67 | def create__single_entry(isFile, path, name): 68 | ''' 69 | create filesystem return object for file request 70 | :param isFile: 71 | :param path: 72 | :param name: 73 | :return: 74 | ''' 75 | entry = Entry() 76 | if not isFile: 77 | entry.id = utils.connect_path(utils.connect_path(Config.SITE_BOOK_LIST, path), name) 78 | entry.links = [] 79 | entry.links.append(Link(entry.id, _get_book_entry_rel(name), name, _get_book_entry_type(name))) 80 | else: 81 | entry.id = utils.connect_path(utils.connect_path(Config.SITE_BOOK_LIST, path), name) 82 | # TODO add Another Links 83 | links = fs.getdownloadurl(os.path.dirname(path), name) 84 | entry.links = [] 85 | if links != None: 86 | for link in links: 87 | entry.links.append(Link(link, _get_book_entry_rel(link), name, _get_book_entry_type(link))) 88 | entry.content = name 89 | entry.title = name 90 | entry.updated = utils.getNow() 91 | return entry 92 | 93 | 94 | def _get_book_entry_type(name): 95 | """ 96 | get link type 97 | """ 98 | if name.endswith(".pdf"): 99 | return Const.book_type_pdf 100 | elif name.endswith(".epub"): 101 | return Const.book_type_epub 102 | elif name.endswith(".jpg"): 103 | return Const.book_type_picture 104 | elif name.endswith(".mobi"): 105 | return Const.book_type_mobi 106 | elif name.endswith(".txt"): 107 | return Const.book_type_text 108 | elif name.find('.') != -1: 109 | return Const.book_type_content 110 | else: 111 | # No subifx 112 | return Const.book_type_entry_catalog 113 | 114 | 115 | def _get_book_entry_rel(name): 116 | """ 117 | get link type 118 | """ 119 | if name.endswith(".pdf"): 120 | return Const.book_link_rel__acquisition 121 | elif name.endswith(".epub"): 122 | return Const.book_link_rel__acquisition 123 | elif name.endswith(".jpg"): 124 | return Const.book_link_rel_image 125 | elif name.endswith(".mobi"): 126 | return Const.book_link_rel__acquisition 127 | elif name.endswith(".txt"): 128 | return Const.book_link_rel__acquisition 129 | elif name.find('.') != -1: 130 | return Const.book_link_rel_subsection 131 | else: 132 | # No subifx 133 | return Const.book_link_rel_subsection 134 | 135 | 136 | class FeedDoc: 137 | def __init__(self, doc, path=None): 138 | """ 139 | Root Element 140 | :param doc: Document() 141 | :return: 142 | """ 143 | self.doc = doc 144 | # xml-stylesheet 145 | if fs.isfile(path): 146 | self.doc.appendChild(self.doc.createProcessingInstruction("xml-stylesheet", 147 | "type=\"text/xsl\" " 148 | "href=\"%s/static/bookdetail.xsl\"" % 149 | Config.SITE_URL)) 150 | else: 151 | self.doc.appendChild(self.doc.createProcessingInstruction("xml-stylesheet", 152 | "type=\"text/xsl\" " 153 | "href=\"%s/static/booklist.xsl\"" % 154 | Config.SITE_URL)) 155 | # feed 156 | self.feed = self.doc.createElement("feed") 157 | setfeedNS(self.feed) 158 | self.addNode(self.feed, Const.id, Config.SITE_URL) 159 | self.addNode(self.feed, Const.author, Config.SITE_EMAIL) 160 | self.addNode(self.feed, Const.title, Config.SITE_TITLE) 161 | self.addNode(self.feed, Const.updated, utils.getNow()) 162 | self.addNode(self.feed, Const.description, Config.description) 163 | # def createLink(self, entry, href, rel, title, type): 164 | self.createLink(self.feed, Config.SITE_URL, "Home", "Home", 165 | "application/atom+xml; profile=opds-catalog; kind=navigation") 166 | # self.createLink(self.feed, 'search.xml', Const.search, "Search", 167 | # "application/opensearchdescription+xml") 168 | 169 | 170 | self.doc.appendChild(self.feed) 171 | pass 172 | 173 | def addNode(self, element, key, value, link=None): 174 | """ 175 | add A node to element 176 | :param element: 177 | :param key: 178 | :param value: can be str & Element 179 | :param link: if is link ,this field is Not None. 180 | :return: 181 | """ 182 | if isinstance(value, Element): 183 | element.appendChild(value) 184 | else: 185 | node = self.doc.createElement(key) 186 | node.appendChild(self.doc.createTextNode(value)) 187 | element.appendChild(node) 188 | 189 | def toString(self): 190 | # return self.doc.toxml("utf-8") 191 | return self.doc.toprettyxml(encoding='utf-8') 192 | 193 | def createEntry(self, entry): 194 | entryNode = self.doc.createElement(Const.entry) 195 | 196 | self.addNode(entryNode, Const.entry_title, entry.title) 197 | self.addNode(entryNode, Const.entry_updated, entry.updated) 198 | self.addNode(entryNode, Const.entry_id, entry.id) 199 | self.addNode(entryNode, Const.entry_content, entry.content) 200 | for link in entry.links: 201 | self.createLink(entryNode, link.href, link.rel, link.title, link.type) 202 | self.feed.appendChild(entryNode) 203 | 204 | def createLink(self, entry, href, rel, title, type): 205 | link = self.doc.createElement(Const.link) 206 | link.setAttribute("href", href) 207 | link.setAttribute("rel", rel) 208 | link.setAttribute("title", title) 209 | link.setAttribute("type", type) 210 | text = self.doc.createTextNode(href) 211 | link.appendChild(text) 212 | entry.appendChild(link) 213 | return link 214 | 215 | 216 | class Entry: 217 | def __init__(self, title=None, updated=None, id=None, content=None, links=[]): 218 | self.links = links 219 | self.content = content 220 | self.id = id 221 | self.updated = updated 222 | self.title = title 223 | 224 | 225 | class Link: 226 | """ 227 | Link Entity 228 | """ 229 | 230 | def __init__(self, href, rel, title, type): 231 | self.href = href 232 | self.rel = rel 233 | self.title = title 234 | self.type = type 235 | 236 | 237 | class OpdsProtocol: 238 | """ 239 | All Opds File System Must Realized this Class 240 | """ 241 | 242 | def listBooks(self, path): 243 | """ 244 | :return: {entiry ...} 245 | """ 246 | rslist = [] 247 | 248 | # not exist! 249 | 250 | if path != '/' and not fs.exists(path): 251 | logging.info("dest Path [%s] is Not Exist." % path) 252 | return rslist 253 | 254 | if fs.isfile(path): 255 | logging.info("dest Path [%s] is a File Not Right." % path) 256 | g.book_process = "detail" 257 | rslist.append(create__single_entry(True, path, os.path.basename(path))) 258 | return rslist 259 | 260 | bookmap = {} 261 | 262 | for name in fs.listdir(path): 263 | try: 264 | name = name.decode("utf-8") 265 | except Exception: 266 | try: 267 | name = name.decode("gbk") 268 | 269 | except Exception as e: 270 | pass 271 | 272 | file_path = utils.connect_path(path, name) 273 | 274 | rslist.append(create_entry(fs.isfile(file_path), path, name)) 275 | 276 | return rslist 277 | 278 | def dowloadBook(self, path): 279 | """ 280 | file 281 | :param path: 282 | :return: file 283 | """ 284 | 285 | return utils.connect_path(Config.base, path) 286 | 287 | def showhtml(self): 288 | return ("No Realized") 289 | pass 290 | -------------------------------------------------------------------------------- /opds/opdsserver.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | 3 | from xml.dom.minidom import Document 4 | from flask import Flask, send_file, make_response, g 5 | import opds.Const as Const 6 | from opds.opdscore import FeedDoc, Link, OpdsProtocol, Entry 7 | import os 8 | 9 | import opds.Config as Config 10 | 11 | import opds.utils as utils 12 | 13 | import logging 14 | 15 | __author__ = 'lei' 16 | 17 | app = Flask(__name__) 18 | 19 | 20 | @app.route("/static/") 21 | def css(stcpath): 22 | return app.send_static_file(stcpath) 23 | 24 | 25 | @app.route("/") 26 | def root(): 27 | d = Document() 28 | f = FeedDoc(d) 29 | entry = Entry() 30 | entry.id = Config.SITE_BOOK_LIST 31 | entry.content = "all Books List By Type" 32 | entry.title = "Book List" 33 | 34 | entry.updated = utils.getNow() 35 | # TODO add Another Links 36 | entry.links = [Link(entry.id, Const.book_link_rel_subsection, "Book List", Const.book_type_entry_catalog)] 37 | f.createEntry(entry) 38 | resp = make_response(f.toString()) 39 | resp.headers['Content-Type'] = 'application/xml; profile=opds-catalog; kind=navigation' 40 | # print(f.toString()) 41 | 42 | return resp 43 | 44 | 45 | @app.route('/list') 46 | @app.route('/list/') 47 | def listbooks(path="/"): 48 | feed = FeedDoc(Document(), path) 49 | 50 | # TODO add *** to feed.toString() 51 | l = getOpdsProtocol().listBooks(path) 52 | # print(l) 53 | 54 | for entry in l: 55 | feed.createEntry(entry) 56 | 57 | resp = make_response(feed.toString()) 58 | resp.headers['Content-Type'] = 'text/xml; profile=opds-catalog; kind=navigation' 59 | # print(feed.toString().decode()) 60 | return resp 61 | 62 | 63 | @app.route('/download/') 64 | def download(path): 65 | """ 66 | download book 67 | """ 68 | filePath = getOpdsProtocol().dowloadBook(path) 69 | return send_file(filePath) 70 | 71 | 72 | @app.route('/show/') 73 | def showhtml(path): 74 | return "show file:" + path 75 | 76 | 77 | def getOpdsProtocol(): 78 | return OpdsProtocol() 79 | 80 | 81 | if __name__ == "__main__": 82 | logging.basicConfig(level=logging.DEBUG, 83 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 84 | ) 85 | # app.debug = False 86 | # app.run(host='0.0.0.0', port=10086) 87 | app.run("0.0.0.0", port=int(os.environ.get('PORT', '5000')), debug=False) -------------------------------------------------------------------------------- /opds/requirements.txt: -------------------------------------------------------------------------------- 1 | cos-python-sdk-v5 2 | flask 3 | requests 4 | -------------------------------------------------------------------------------- /opds/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.0 -------------------------------------------------------------------------------- /opds/static/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/opds/static/book.png -------------------------------------------------------------------------------- /opds/static/bookdetail.xsl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <xsl:value-of select="feed/title"/> 10 | 11 | 12 | 13 | 14 | 15 | 44 | 45 | 46 | 47 | 48 |
    49 |
    50 |
    51 |
    52 | 53 | opds logo 55 | 56 |
    57 |
    58 |
    59 |

    60 | 61 |

    62 |
    63 |
    64 |
    65 |

    66 | 67 |

    68 | 69 |
    70 | 75 |
    76 | 77 |
    78 |
    79 | 80 | 81 | 82 | 83 | 84 |
    85 |
    86 |

    87 | 89 | 90 | 91 |

    92 |

    93 | 94 |

    95 | 96 | 97 | 98 |
    99 | 下载链接: 100 | 133 |
    134 |
    135 |
    136 |
    137 |
    138 |
    139 |
    140 | 141 | 142 | 154 | 155 |
    156 | 157 | 160 | 161 | 162 |
    -------------------------------------------------------------------------------- /opds/static/booklist.xsl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <xsl:value-of select="feed/title"/> 10 | 11 | 12 | 13 | 14 | 15 | 44 | 45 | 46 | 47 | 48 |
    49 |
    50 |
    51 |
    52 | 53 | opds logo 55 | 56 |
    57 |
    58 |
    59 |

    60 | 61 |

    62 |
    63 |
    64 |
    65 |

    66 | 67 |

    68 | 69 |
    70 | 75 |
    76 | 77 |
    78 |
    79 | 80 | 81 | 82 | 83 | 84 |
    85 |
    86 |

    87 | 89 | 90 | 91 |

    92 |

    93 | 94 |

    95 | 96 | 97 | 98 |
    99 | 124 |
    125 |
    126 |
    127 |
    128 |
    129 |
    130 |
    131 | 132 | 133 | 134 |
    135 | 136 | 139 | 140 | 141 |
    -------------------------------------------------------------------------------- /opds/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/opds/static/logo.png -------------------------------------------------------------------------------- /opds/static/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <xsl:value-of select="feed/title"/> 5 | 6 | 7 | 8 | 9 | 10 | 40 | 41 | 42 | 43 | 44 |
    45 |
    46 |
    47 |
    48 | 49 | opds logo 51 | 52 |
    53 |
    54 |
    55 |

    56 | 57 |

    58 |
    59 |
    60 |
    61 |

    62 | 63 |

    64 | 65 |
    66 | 71 |
    72 | 73 |
    74 |
    75 | 76 | 77 | 78 | 79 | 80 |
    81 |
    82 |

    83 | 85 | 86 | 87 |

    88 |

    89 | 90 |

    91 | 92 | 93 | 94 |
    95 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 | 128 | 129 | -------------------------------------------------------------------------------- /opds/test_mine.py: -------------------------------------------------------------------------------- 1 | from filesystem import LocalMetadataFileSystem 2 | import unittest 3 | 4 | __author__ = 'lei' 5 | 6 | fs = LocalMetadataFileSystem() 7 | 8 | 9 | class MineTest(unittest.TestCase): 10 | 11 | def test_exists(self): 12 | self.assertTrue(fs.exists(u'\u79d1\u6280')) 13 | 14 | 15 | if __name__ == "__main__": 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /opds/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import opds.filesystem as filesystem 5 | 6 | __author__ = 'lei' 7 | 8 | 9 | # #connect path 10 | # 不是你这啥用处 11 | def connect_path(base, name): 12 | if base is None or name is None: 13 | # print(base, name) 14 | return None 15 | # if name.startswith('/'): 16 | if len(name) == 0: 17 | return base 18 | if name[0] == '/': 19 | name = name[1:] 20 | 21 | if base[-1] == '/': 22 | return base + name 23 | else: 24 | return base + '/' + name 25 | 26 | 27 | def getNow(): 28 | return datetime.datetime.now().strftime("%Y-%m-%dT%I:%M:%SZ") 29 | 30 | 31 | def getUpdateTime(name, default=None): 32 | if default is None: 33 | default = getNow() 34 | result = filesystem.bookdata.get(name, default) 35 | if type(result) is dict: 36 | result = result['last_modified'] 37 | return result 38 | 39 | 40 | def getFile(jjson, paths): 41 | ''' 42 | get json object 43 | :param jjson: json object 44 | :param paths: json path 45 | :return: json object 46 | ''' 47 | try: 48 | if len(paths) == 1: 49 | if paths[0] == '': 50 | return jjson 51 | # elif jjson.has_key(paths[0]): 52 | elif paths[0] in jjson: 53 | 54 | return jjson[paths[0]] 55 | else: 56 | logging.warn('Jjson', json.dumps(jjson)) 57 | logging.warn('No this Key:', paths[0]) 58 | return None 59 | elif len(paths) > 1: 60 | return getFile(jjson[paths[0]], paths[1:]) 61 | except AttributeError as e: 62 | logging.error(e) 63 | return None 64 | -------------------------------------------------------------------------------- /progress.txt: -------------------------------------------------------------------------------- 1 | 2 -------------------------------------------------------------------------------- /refresh.py: -------------------------------------------------------------------------------- 1 | import io 2 | from wenku8toepub import * 3 | from tqdm import trange 4 | from manage import * 5 | 6 | 7 | # 刷新一遍缓存 8 | path = 'books/' 9 | file = 'progress.txt' 10 | errors = 'errors.txt' 11 | if not os.path.exists(path): 12 | os.mkdir(path) 13 | if not os.path.exists(file): 14 | with open(file, 'w') as f: 15 | f.write('1') 16 | if not os.path.exists(errors): 17 | with open(errors, 'w') as f: 18 | f.write('') 19 | 20 | 21 | def max_bid(): 22 | # url_main = 'https://www.wenku8.net/index.php' 23 | # soup = Soup(requests.get(url_main).content, 'html.parser') 24 | # print(soup.find_all(attrs={'class': ''})) 25 | return 2706 26 | 27 | 28 | def main(): 29 | with open(file, 'r') as f: 30 | now = int(f.read()) 31 | # get max: 32 | mbid = max_bid() 33 | for bid in trange(now, mbid): 34 | wk = Wenku8ToEpub() 35 | wk.login() 36 | title = wk.id2name(bid) 37 | filename = "%s.epub" % title 38 | try: 39 | # 先判断一波:是否需要下载? 40 | # 没版权的都更新一遍。 41 | has_copyright = wk.copyright(bid) 42 | # 最新版本的跳过。 43 | if local_check(bid) == '0' and has_copyright: 44 | logger.debug('BID %s最新版本而且有版权,跳过。' % bid) 45 | continue 46 | # 之后再手动上传。 47 | data = wk.get_book(bid, savepath=path, fetch_image=False, bin_mode=True) 48 | if data is None: 49 | continue 50 | with open(os.path.join(path, "%s.epub" % title), 'wb') as f: 51 | f.write(data) 52 | client.put_object( 53 | Bucket=bucket, 54 | Body=data, 55 | Key=filename, 56 | ) 57 | except Exception: 58 | try: 59 | logger.warn('错误:', bid, '尝试备用方案') 60 | with open(errors, 'a') as p: 61 | p.write(str(bid) + '\n') 62 | data = wk.txt2epub(bid) 63 | if data is None: 64 | continue 65 | with open(os.path.join(path, "%s.epub" % title), 'wb') as f: 66 | f.write(data) 67 | client.put_object( 68 | Bucket=bucket, 69 | Body=data, 70 | Key=filename, 71 | ) 72 | except Exception as e: 73 | logger.error(e) 74 | finally: 75 | with open(file, 'w') as f: 76 | f.write(str(bid)) 77 | try: 78 | # client.put_object_from_local_file( 79 | # Bucket=bucket, 80 | # Key=filename, 81 | # LocalFilePath=os.path.join(path, filename) 82 | # ) 83 | pass 84 | except Exception as e: 85 | logger.error(e) 86 | 87 | 88 | if __name__ == '__main__': 89 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | pymongo 3 | requests 4 | cos-python-sdk-v5 5 | colorlog 6 | tqdm 7 | bs4 8 | lxml 9 | six 10 | dnspython 11 | flask_cors 12 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PID=$(ps aux | grep "server.py" | head -n 1 | awk '{print $2}') 3 | echo $PID 4 | for id in $PID 5 | do 6 | kill -9 $id 7 | echo "process $id killed" 8 | done 9 | WENKU8_LOCAL=True python server.py & 10 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.10 -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import * 3 | from flask_cors import * 4 | from werkzeug.middleware.dispatcher import DispatcherMiddleware 5 | from werkzeug.serving import run_simple 6 | 7 | from manage import app 8 | from opds.opdsserver import app as app_opds 9 | 10 | # app = Flask(__name__) 11 | 12 | CORS(app, supports_credentials=True) 13 | 14 | dm = DispatcherMiddleware(app, 15 | { 16 | '/opds': app_opds 17 | } 18 | ) 19 | 20 | 21 | if __name__ == '__main__': 22 | # app.run("0.0.0.0", port=int(os.environ.get('PORT', '8000')), debug=False) 23 | run_simple('0.0.0.0', int(os.environ.get('PORT', '8000')), dm) -------------------------------------------------------------------------------- /static/board.json: -------------------------------------------------------------------------------- 1 | { 2 | "notice": "更新:迁移了服务器。", 3 | "instructions": "[2023-03-19] 暂时迁移到:https://wenku8.chiro.work。服务可能更加稳定,不过暂时无法返回下载状态,如果点击按钮之后没有反应可以过一会直接点击下载按钮。
    使用流程
    先搜索书名,然后到出现的书中选择“下载”。
    暂时因为程序和资金原因在在线版删除了上传缓存功能,直接从服务器在线获取下载链接。请尽量使用迅雷等下载器进行多线程下载。
    本地程序用户能够上传文件到腾讯云储存,能加速本站下载以及减轻主站的访问压力。在本地版(wk8local)下载过的带图小说可以直接从网页端下载,推荐下载wk8local使用。
    本网站运行需要资金,如果你觉得本网站好用,不妨捐助(先打开侧边栏)个几块钱,让这个网站能够长久为大家服务。
    2020/3/16更新:
    把主站小说全部缓存了一遍,包括已经被标记为“无版权”的小说。“无版权”小说由主站txt文件转换而来,不提供图片下载。
    增加了一些说明,简化了一些操作。
    使用腾讯云临时链接对静态内容防止盗链,减少流量。
    感谢捐赠。目前运营成本大约15元/月。
    2020/3/17更新:
    修复了特殊文件名造成的无法下载的问题。
    2020/3/22更新:
    添加了留言和反馈功能;增加了相关链接。
    本地版本ver5004发布。
    2020/3/28更新:
    可以和小冰聊天啦!
    本地版本ver5005发布。
    2020/4/10更新:
    增加了TXT文件下载。
    本地版本ver5006发布。
    2020/4/15更新:
    增加了动漫之家的书源,同样支持下载图片和缓存到腾讯云。书总量x2.5。
    本地版本ver5007发布。
    动漫之家书源异步下载√
    本地版本ver5008发布,使用py3.7.7,主程序体积减小到11MB。
    2020/5/1更新:
    紧急修复一个wenku8书没法下载的BUG。
    ", 4 | "local_latest": 5009 5 | } -------------------------------------------------------------------------------- /static/extra.js: -------------------------------------------------------------------------------- 1 | console.log('EXTRA JAVASCRIPT LOADED!') 2 | 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiro2001/Wenku8ToEpub-Online/53fe93f9093df3722bf336efcf8e9aa6e136b6b4/static/favicon.ico -------------------------------------------------------------------------------- /static/theme.js: -------------------------------------------------------------------------------- 1 | function getCookieByArray(name){ 2 | var cookies = document.cookie.split(';'); 3 | var c; 4 | for(var i=0; i { 26 | $.ajax({url: '/static/board.json'}).then(d => { 27 | console.log("news:", d); 28 | $('#wenku8-board').text(d.notice); 29 | $('#wenku8-instructions').html(d.instructions); 30 | }); 31 | } 32 | 33 | function wenku8Fun1() { 34 | var text = $('#wenku8-fun1-text').val(); 35 | if (text.startsWith('dmzj_')) { 36 | var bid = text.slice(5, text.length); 37 | if (!(myIsNaN(bid) && bid.length <= 5)) { 38 | // 不是id 39 | mdui.snackbar('输入错误!请输入ID号!'); 40 | return false; 41 | } 42 | wenku8_progress.show(); 43 | $.ajax({ 44 | url: '/bookinfo_dmzj/' + bid 45 | }).then((d) => { 46 | wenku8_progress.hide(); 47 | d = JSON.parse(d); 48 | $('#wenku8-book-card').fadeIn('slow'); 49 | $('#wenku8-bookinfo-name').text(d.name); 50 | $('#wenku8-bookinfo-id').text('dmzj_' + d.id); 51 | $('#wenku8-bookinfo-author').text(d.authors); 52 | $('#wenku8-bookinfo-brief').text(d.introduction); 53 | $('#wenku8-bookinfo-time').text(d.update_time); 54 | $('#wenku8-bookinfo-copyright').text('√'); 55 | $('#wenku8-bookinfo-cover').empty(); 56 | $('#wenku8-bookinfo-cover').append($('')); 57 | $('#wenku8-bookinfo-cover').append($('
    ')); 58 | $('#wenku8-bookinfo-cover').append($('封面链接')); 59 | }) 60 | return; 61 | } 62 | if (!(myIsNaN(text) && text.length <= 5)) { 63 | // 不是id 64 | mdui.snackbar('输入错误!请输入ID号!'); 65 | return false; 66 | } 67 | var bid = text; 68 | wenku8_progress.show(); 69 | $.ajax({ 70 | url: '/bookinfo/' + bid 71 | }).then((d) => { 72 | wenku8_progress.hide(); 73 | // console.log(d); 74 | // console.log('ajax: bid:', bid, d); 75 | d = JSON.parse(d); 76 | $('#wenku8-book-card').fadeIn('slow'); 77 | $('#wenku8-bookinfo-name').text(d.name); 78 | $('#wenku8-bookinfo-id').text(d.id); 79 | $('#wenku8-bookinfo-author').text(d.author); 80 | $('#wenku8-bookinfo-brief').text(d.brief); 81 | $('#wenku8-bookinfo-time').text(d.update_time); 82 | if (d.copyright == false) { 83 | $('#wenku8-bookinfo-copyright').text('无版权,可下载'); 84 | } else { 85 | $('#wenku8-bookinfo-copyright').text('有版权'); 86 | } 87 | // $('#wenku8-bookinfo-cover').attr('src', d.cover); 88 | $('#wenku8-bookinfo-cover').empty(); 89 | $('#wenku8-bookinfo-cover').append($('