Update
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -158,3 +158,7 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
*.json
|
||||
*.db
|
||||
exported/
|
||||
|
||||
29
booksnew.py
Normal file
29
booksnew.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from os.path import isdir, join
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
class BooksNew:
|
||||
def __init__(self, path: str):
|
||||
self._path = path
|
||||
self._is_zip = False
|
||||
self._contain_dir_name = False
|
||||
if not isdir(path):
|
||||
self._z = ZipFile(path)
|
||||
self._is_zip = True
|
||||
try:
|
||||
self._z.getinfo("booksnew/")
|
||||
self._contain_dir_name = True
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_chapter(self, book_id: int, chapter_id: int):
|
||||
if self._is_zip:
|
||||
if self._contain_dir_name:
|
||||
path = f'booksnew/{book_id}/{chapter_id}.txt'
|
||||
else:
|
||||
path = f'{book_id}/{chapter_id}.txt'
|
||||
return self._z.read(path).decode()
|
||||
else:
|
||||
with open(join(self._path, str(book_id), f"{chapter_id}.txt"), 'r',
|
||||
encoding='UTF-8') as f:
|
||||
return f.read()
|
||||
74
config.py
Normal file
74
config.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from argparse import Namespace
|
||||
from os.path import exists
|
||||
import json
|
||||
try:
|
||||
from functools import cached_property
|
||||
except Exception:
|
||||
cached_property = property
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, cfg_path: str):
|
||||
self._path = cfg_path
|
||||
self._data = {}
|
||||
self._args = Namespace()
|
||||
if exists(cfg_path):
|
||||
with open(cfg_path, 'r', encoding='UTF-8') as f:
|
||||
self._data = json.load(f)
|
||||
else:
|
||||
self._data['db'] = 'cwm.db'
|
||||
self._data['save_to_config'] = True
|
||||
self._data['export_chapter_template'] = 'exported/<book_id>/<chapter_id>.txt' # noqa: E501
|
||||
self.save()
|
||||
|
||||
def add_args(self, args: Namespace):
|
||||
self._args = args
|
||||
|
||||
def get_arg(self, key: str, default):
|
||||
x = getattr(self._args, key, None)
|
||||
if x is not None:
|
||||
if self.save_to_config:
|
||||
self._data[key] = x
|
||||
return x
|
||||
if key in self._data:
|
||||
return self._data[key]
|
||||
else:
|
||||
return default
|
||||
|
||||
@cached_property
|
||||
def booksnew(self):
|
||||
return self.get_arg('booksnew', None)
|
||||
|
||||
@cached_property
|
||||
def chapter_id(self):
|
||||
return getattr(self._args, 'cid', None)
|
||||
|
||||
@cached_property
|
||||
def cwmdb(self):
|
||||
return self.get_arg('cwmdb', None)
|
||||
|
||||
@cached_property
|
||||
def db(self):
|
||||
return self.get_arg('db', 'cwm.db')
|
||||
|
||||
@cached_property
|
||||
def export_chapter_template(self):
|
||||
return self.get_arg('export_chapter_template', 'exported/<book_id>/<chapter_id>.txt') # noqa: E501
|
||||
|
||||
def get_export_chapter(self, chapter):
|
||||
temp = self.export_chapter_template
|
||||
for k in chapter.keys():
|
||||
temp = temp.replace(f'<{k}>', str(chapter[k]))
|
||||
return temp
|
||||
|
||||
@cached_property
|
||||
def key(self):
|
||||
return self.get_arg('key', None)
|
||||
|
||||
@cached_property
|
||||
def save_to_config(self):
|
||||
return getattr(self._args, 'save_to_config', True)
|
||||
|
||||
def save(self):
|
||||
with open(self._path, 'w', encoding='UTF-8') as f:
|
||||
json.dump(self._data, f, ensure_ascii=False)
|
||||
14
crypto.py
Normal file
14
crypto.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from Crypto.Cipher import AES
|
||||
from hashlib import sha256
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
def decrypt(encrypted: str | bytes,
|
||||
key: str | bytes = 'zG2nSeEfSHfvTCHy5LCcqtBbQehKNLXn') -> bytes:
|
||||
if isinstance(key, str):
|
||||
key = key.encode()
|
||||
ekey = sha256(key).digest()
|
||||
iv = b'\0' * 16
|
||||
aes = AES.new(ekey, AES.MODE_CBC, iv)
|
||||
data = aes.decrypt(b64decode(encrypted))
|
||||
return data[0:len(data) - ord(chr(data[len(data) - 1]))]
|
||||
77
db.py
Normal file
77
db.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import sqlite3
|
||||
from semver import Version
|
||||
from typing import Optional, Set, List
|
||||
|
||||
|
||||
VERSION_TABLE = '''CREATE TABLE version (
|
||||
id TEXT,
|
||||
version TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);'''
|
||||
KEY_TABLE = '''CREATE TABLE key (
|
||||
chapter_id INT,
|
||||
user_id INT,
|
||||
key TEXT,
|
||||
PRIMARY KEY (chapter_id, user_id)
|
||||
);'''
|
||||
|
||||
|
||||
class CwmDb:
|
||||
def __init__(self, db_path):
|
||||
self._db = sqlite3.connect(db_path, check_same_thread=False)
|
||||
self.version = Version(0, 0, 0, 0)
|
||||
if not self.__check_database():
|
||||
self.__create_table()
|
||||
|
||||
def __check_database(self):
|
||||
self.__update_exists_tables()
|
||||
v = self.__read_version()
|
||||
if v is None:
|
||||
return False
|
||||
if v < self.version:
|
||||
self.__update_exists_tables()
|
||||
self.__write_version()
|
||||
return True
|
||||
|
||||
def __create_table(self):
|
||||
if 'version' not in self._exist_tables:
|
||||
self._db.execute(VERSION_TABLE)
|
||||
self.__write_version()
|
||||
if 'key' not in self._exist_tables:
|
||||
self._db.execute(KEY_TABLE)
|
||||
self._db.commit()
|
||||
|
||||
def __write_version(self):
|
||||
self._db.execute('INSERT OR REPLACE INTO version VALUES (?, ?);', [
|
||||
'main', str(self.version)])
|
||||
|
||||
def __read_version(self) -> Optional[Version]:
|
||||
if 'version' not in self._exist_tables:
|
||||
return None
|
||||
cur = self._db.execute(
|
||||
'SELECT version FROM version WHERE id = ?;', ['main'])
|
||||
for i in cur:
|
||||
try:
|
||||
return Version.parse(i[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def __update_exists_tables(self):
|
||||
cur = self._db.execute('SELECT * FROM main.sqlite_master;')
|
||||
self._exist_tables = {}
|
||||
for i in cur:
|
||||
if i[0] == 'table':
|
||||
self._exist_tables[i[1]] = i
|
||||
|
||||
def add_key(self, chapter_id: int, user_id: int, key: str):
|
||||
self._db.execute('INSERT OR REPLACE INTO key VALUES (?, ?, ?);', [
|
||||
chapter_id, user_id, key])
|
||||
|
||||
def get_all_keys_as_origin(self) -> Set[str]:
|
||||
cur = self._db.execute('SELECT chapter_id, user_id FROM key;')
|
||||
return {f'{i[0]}{i[1]}' for i in cur}
|
||||
|
||||
def get_key(self, chapter_id: int) -> List[str]:
|
||||
cur = self._db.execute('SELECT key FROM key WHERE chapter_id = ?;', [
|
||||
chapter_id])
|
||||
return [i[0] for i in cur]
|
||||
48
export.py
Normal file
48
export.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from novelCiwei import NovelCiwei
|
||||
from db import CwmDb
|
||||
from config import Config
|
||||
from key import import_keys
|
||||
from booksnew import BooksNew
|
||||
from crypto import decrypt
|
||||
from os.path import dirname
|
||||
from os import makedirs
|
||||
|
||||
|
||||
key_imported = False
|
||||
|
||||
|
||||
def get_key(db: CwmDb, cfg: Config, chapter_id: int):
|
||||
keys = db.get_key(chapter_id)
|
||||
if len(keys) == 0:
|
||||
if key_imported:
|
||||
raise ValueError('The key is not found.')
|
||||
else:
|
||||
import_keys(cfg.key, db)
|
||||
keys = db.get_key(chapter_id)
|
||||
if len(keys) == 0:
|
||||
raise ValueError('The key is not found.')
|
||||
return keys
|
||||
|
||||
|
||||
def try_decrypt(db: CwmDb, cfg: Config, content, chapter_id: int):
|
||||
keys = get_key(db, cfg, chapter_id)
|
||||
for key in keys:
|
||||
try:
|
||||
return decrypt(content, key).decode()
|
||||
except Exception:
|
||||
pass
|
||||
raise ValueError('Failed to decrypt the content.')
|
||||
|
||||
|
||||
def export_chapter(ncw: NovelCiwei, db: CwmDb, cfg: Config, bn: BooksNew,
|
||||
chapter_id: int):
|
||||
chapter = ncw.get_chapter(chapter_id)
|
||||
book_id = int(chapter['book_id'])
|
||||
raw_content = bn.get_chapter(book_id, chapter_id)
|
||||
content = try_decrypt(db, cfg, raw_content, chapter_id)
|
||||
filename = cfg.get_export_chapter(chapter)
|
||||
d = dirname(filename)
|
||||
makedirs(d, exist_ok=True)
|
||||
with open(filename, 'w', encoding='UTF-8') as f:
|
||||
f.write(chapter['chapter_title'] + '\n')
|
||||
f.write(content)
|
||||
45
key.py
Normal file
45
key.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from os.path import isdir, join
|
||||
from os import listdir
|
||||
from db import CwmDb
|
||||
from zipfile import ZipFile
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
def import_keys(key: str, db: CwmDb):
|
||||
is_zip = False
|
||||
file_list = []
|
||||
contain_dir_name = False
|
||||
if isdir(key):
|
||||
file_list = listdir(key)
|
||||
else:
|
||||
z = ZipFile(key)
|
||||
is_zip = True
|
||||
file_list = z.namelist()
|
||||
if 'Y2hlcy8/' in file_list:
|
||||
contain_dir_name = True
|
||||
file_list = [i[8:] for i in file_list if i.startswith(
|
||||
'Y2hlcy8/') and i != 'Y2hlcy8/']
|
||||
try:
|
||||
count = 0
|
||||
keys = db.get_all_keys_as_origin()
|
||||
for i in file_list:
|
||||
oid = b64decode(i).decode()
|
||||
if oid in keys:
|
||||
continue
|
||||
cid = int(oid[0:9])
|
||||
uid = int(oid[9:])
|
||||
if is_zip:
|
||||
if contain_dir_name:
|
||||
path = 'Y2hlcy8/' + i
|
||||
else:
|
||||
path = i
|
||||
content = z.read(path).decode()
|
||||
else:
|
||||
content = open(join(key, i), 'r', encoding='UTF-8').read()
|
||||
db.add_key(cid, uid, content)
|
||||
count += 1
|
||||
print(f'Imported {count} keys.')
|
||||
finally:
|
||||
db._db.commit()
|
||||
if is_zip:
|
||||
z.close()
|
||||
46
main.py
Normal file
46
main.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from argparse import ArgumentParser
|
||||
from config import Config
|
||||
from db import CwmDb
|
||||
from novelCiwei import NovelCiwei
|
||||
from booksnew import BooksNew
|
||||
|
||||
|
||||
parser = ArgumentParser(description='A tool to export CiWeiMao novel cache.')
|
||||
parser.add_argument('-c', '--config', help='The path of the config file.', default='config.json') # noqa: E501
|
||||
parser.add_argument('-d', '--db', help='The path of the database file.')
|
||||
parser.add_argument('-k', '--key', help='The path to Y2hlcy8 key directory or zip file.') # noqa: E501
|
||||
parser.add_argument('--cwmdb', help='The path to NovelCiwei file.')
|
||||
parser.add_argument('-b', '--booksnew', help='The path to booksnew directory or zip file.') # noqa: E501
|
||||
parser.add_argument('-C', '--cid', '--chapter-id', help='The chapter id.', type=int) # noqa: E501
|
||||
parser.add_argument('--ect', '--export-chapter-template', help='The template of the exported chapter. Available key: <book_id>, <chapter_id> eta.') # noqa: E501
|
||||
parser.add_argument('action', help='The action to do.', choices=['importkey', 'exportchapter']) # noqa: E501
|
||||
|
||||
|
||||
def main(args=None):
|
||||
arg = parser.parse_intermixed_args(args)
|
||||
cfg = Config(arg.config)
|
||||
cfg.add_args(arg)
|
||||
try:
|
||||
db = CwmDb(cfg.db)
|
||||
if arg.action == 'importkey':
|
||||
if cfg.key is None:
|
||||
raise ValueError('The key is not specified.')
|
||||
from key import import_keys
|
||||
import_keys(cfg.key, db)
|
||||
elif arg.action == 'exportchapter':
|
||||
if cfg.cwmdb is None:
|
||||
raise ValueError('The cwmdb is not specified.')
|
||||
ncw = NovelCiwei(cfg.cwmdb)
|
||||
if cfg.booksnew is None:
|
||||
raise ValueError('The booksnew is not specified.')
|
||||
bn = BooksNew(cfg.booksnew)
|
||||
if cfg.chapter_id is None:
|
||||
raise ValueError('The chapter id is not specified.')
|
||||
from export import export_chapter
|
||||
export_chapter(ncw, db, cfg, bn, cfg.chapter_id)
|
||||
finally:
|
||||
cfg.save()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
20
novelCiwei.py
Normal file
20
novelCiwei.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
|
||||
class NovelCiwei:
|
||||
def __init__(self, db_path: str):
|
||||
self._db = sqlite3.connect(db_path, check_same_thread=False)
|
||||
|
||||
def get_book_in_shelf(self, book_id: int):
|
||||
cur = self._db.execute(
|
||||
'SELECT book_info FROM shelf_book_info WHERE book_id = ?;',
|
||||
[str(book_id)])
|
||||
for i in cur:
|
||||
return json.loads(i)
|
||||
|
||||
def get_chapter(self, chapter_id: int):
|
||||
cur = self._db.execute(
|
||||
'SELECT * FROM catalog1 WHERE chapter_id = ?;', [str(chapter_id)])
|
||||
cur.row_factory = sqlite3.Row
|
||||
return cur.fetchone()
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
PyCryptodome
|
||||
semver
|
||||
Reference in New Issue
Block a user