This commit is contained in:
2024-03-08 13:50:04 +08:00
parent bdb44e45c9
commit 7dffd231bb
10 changed files with 359 additions and 0 deletions

4
.gitignore vendored
View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
PyCryptodome
semver