From 7dffd231bb48719aba3ea773c2be96b353cfa1ec Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 8 Mar 2024 13:50:04 +0800 Subject: [PATCH] Update --- .gitignore | 4 +++ booksnew.py | 29 ++++++++++++++++++ config.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ crypto.py | 14 +++++++++ db.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ export.py | 48 ++++++++++++++++++++++++++++++ key.py | 45 ++++++++++++++++++++++++++++ main.py | 46 +++++++++++++++++++++++++++++ novelCiwei.py | 20 +++++++++++++ requirements.txt | 2 ++ 10 files changed, 359 insertions(+) create mode 100644 booksnew.py create mode 100644 config.py create mode 100644 crypto.py create mode 100644 db.py create mode 100644 export.py create mode 100644 key.py create mode 100644 main.py create mode 100644 novelCiwei.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 68bc17f..076249d 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/booksnew.py b/booksnew.py new file mode 100644 index 0000000..23de89e --- /dev/null +++ b/booksnew.py @@ -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() diff --git a/config.py b/config.py new file mode 100644 index 0000000..6765a50 --- /dev/null +++ b/config.py @@ -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//.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//.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) diff --git a/crypto.py b/crypto.py new file mode 100644 index 0000000..9888e9c --- /dev/null +++ b/crypto.py @@ -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]))] diff --git a/db.py b/db.py new file mode 100644 index 0000000..c45e497 --- /dev/null +++ b/db.py @@ -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] diff --git a/export.py b/export.py new file mode 100644 index 0000000..b6ec35c --- /dev/null +++ b/export.py @@ -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) diff --git a/key.py b/key.py new file mode 100644 index 0000000..30687a7 --- /dev/null +++ b/key.py @@ -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() diff --git a/main.py b/main.py new file mode 100644 index 0000000..1a3b4d7 --- /dev/null +++ b/main.py @@ -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: , 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() diff --git a/novelCiwei.py b/novelCiwei.py new file mode 100644 index 0000000..3a5c1a2 --- /dev/null +++ b/novelCiwei.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e218f03 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyCryptodome +semver