feat(tuku): CBZ 档案册集成——档案页源/书库列册/解压缓存
- logic/reader/archive_source.c:档案册页源实现 M6 mr_book 契约 (mz_open/mz_count/mz_entry_at/mz_read_alloc),条目自然序 + 图片后缀 + 路径安全(`..`/绝对路径/反斜杠/隐藏段/内嵌 NUL)+ 单页尺寸上限过滤; meta 按需解压只读图片头(不落缓存,结果按页记忆);mr_book_open_auto 统一目录册/档案册开册,翻页与条漫两套消费代码零改动。 - logic/reader/page_cache.c/h:解压页落 MeowData/<pkg>/cache,LRU 64 页或 32MiB,启动清 mrpage_* 孤儿,写入 .part + rename;与缩略图缓存同目录、 前缀隔离;互斥量保护,解码/缩略图线程可并发。 - logic/library.c/h:扫描直接子文件里的 .cbz/.zip 成册(行=文件名+页数, 点入直接阅读);档案探测由调用方注入(logic 不依赖 zip 容器),不注入时 与 M7 行为逐字一致;册间衔接兄弟表=含图目录∪含页档案册,同列自然序。 条目构造/副标题/角标/兄弟判据拆成小函数,便于 M13 书库重写拼接。 - ui/library:档案册行 + 封面缩略图(解压档内首图)、扫描注入探测、缓存 目录统一由 page_cache 推导(一份规则)。 - ui/reader:open_book 走 mr_book_open_auto;跨册衔接纳入档案册。 - build_tuku.sh 补 zip/archive/cache 源与 sysroot zlib 头(zip.c include <zlib.h>);run_tests.sh 纳入 zip+cbz 用例;run_webtoon_tests.sh 的宿主 集成构建补新源与 -lz;.gitignore 同步测试产物。 - test/test_cbz.c(267 项断言)+ make_cbz_samples.py 真实 cbz 夹具: 列档/自然序/解压字节 CRC/路径安全/坏档与 zip 炸弹容错/LRU 与 32MiB 上限/ 启动清孤儿/续读角标/封面首图。
This commit is contained in:
1 parent
19620a7957
commit
562cc44931
16 files changed
+2330
-108
No files matched your search
+146
-43
@@ -12,6 +12,14 @@
|
||||
* - 续读角标:含图册的副标题后缀「· 续读 Pn」(读 logic/reader 的每册 last page);
|
||||
* - 册间衔接:mr_book_adjacent 给出同父目录下按书库自然序的相邻「含图册」,
|
||||
* 供阅读器末页前进/首页后退跨册;mr_library_dir_has_images 是共享的「是不是一本书」判据。
|
||||
*
|
||||
* M11 追加(档案册,仍然不改目录扫描的主体语义):
|
||||
* - 目标目录的**直接子文件**里 .cbz/.zip 也成行(行=文件名+档内页数,has_images=1);
|
||||
* - 档案册探测由调用方注入(mr_lib_archive_fn),logic 层不依赖 zip 容器;
|
||||
* 不注入时所有 *_ex 入口与 M7 行为逐字一致;
|
||||
* - 册间衔接的兄弟表从「含图目录」扩成「含图目录 ∪ 含图片页的档案册」,同列自然序。
|
||||
* 小函数边界(add_dir_entry / add_archive_entry / entry_subtitle / entry_badge /
|
||||
* sibling_is_book)刻意留给 M13 的书库重写复用。
|
||||
*/
|
||||
#include "library.h"
|
||||
#include "logic/reader.h" /* mr_cont_badge:书库行续读角标(M7) */
|
||||
@@ -66,6 +74,15 @@ int mr_path_is_image(const char *name)
|
||||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
|
||||
}
|
||||
|
||||
int mr_path_is_archive(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 0;
|
||||
}
|
||||
|
||||
int mr_natural_cmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
@@ -105,12 +122,22 @@ int mr_natural_cmp(const char *a, const char *b)
|
||||
typedef struct scan_info {
|
||||
int pages; /* 直接图片数 */
|
||||
int subdirs; /* 含图子目录数 */
|
||||
int nav; /* 1=本目录最终含图(可直接看或可导航到图) */
|
||||
int archives; /* 直接档案册数(.cbz/.zip 且探测到图片页) */
|
||||
int nav; /* 1=本目录最终含书(可直接看或可导航到书) */
|
||||
char cover[MR_PATH_MAX]; /* 缩略图源 */
|
||||
char first_name[MR_NAME_MAX]; /* 本目录自然序第一张图片的文件名(比较用) */
|
||||
} scan_info;
|
||||
|
||||
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si)
|
||||
/* 档案册探测(UI 注入;NULL=纯目录书库,行为与 M7 一致)。 */
|
||||
static int archive_pages_at(mr_lib_archive_fn probe, void *ud, const char *path)
|
||||
{
|
||||
if (!probe || !path || !path[0]) return 0;
|
||||
int n = probe(path, ud);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
static void scan_dir(const char *path, int depth, int max_depth, scan_info *si,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
memset(si, 0, sizeof *si);
|
||||
DIR *d = opendir(path);
|
||||
@@ -132,21 +159,25 @@ static void scan_dir(const char *path, int depth, int max_depth, scan_info *si)
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (depth >= max_depth) continue;
|
||||
scan_info child;
|
||||
scan_dir(full, depth + 1, max_depth, &child);
|
||||
scan_dir(full, depth + 1, max_depth, &child, probe, ud);
|
||||
if (!child.nav) continue;
|
||||
si->subdirs++;
|
||||
if (!si->cover[0] && child.cover[0])
|
||||
copy_str(si->cover, (int)sizeof si->cover, child.cover);
|
||||
} else if (S_ISREG(st.st_mode) && mr_path_is_image(name)) {
|
||||
si->pages++;
|
||||
if (!si->first_name[0] || mr_natural_cmp(name, si->first_name) < 0) {
|
||||
copy_str(si->first_name, (int)sizeof si->first_name, name);
|
||||
copy_str(si->cover, (int)sizeof si->cover, full);
|
||||
} else if (S_ISREG(st.st_mode)) {
|
||||
if (mr_path_is_image(name)) {
|
||||
si->pages++;
|
||||
if (!si->first_name[0] || mr_natural_cmp(name, si->first_name) < 0) {
|
||||
copy_str(si->first_name, (int)sizeof si->first_name, name);
|
||||
copy_str(si->cover, (int)sizeof si->cover, full);
|
||||
}
|
||||
} else if (mr_path_is_archive(name)) {
|
||||
if (archive_pages_at(probe, ud, full) > 0) si->archives++;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
si->nav = (si->pages > 0 || si->subdirs > 0);
|
||||
si->nav = (si->pages > 0 || si->subdirs > 0 || si->archives > 0);
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
@@ -179,7 +210,69 @@ static int entry_cmp(const void *pa, const void *pb)
|
||||
return mr_natural_cmp(a->name, b->name);
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
/* 副标题(M11 扩展):直接含图 → pages;只有档案册 → books;否则 folders。 */
|
||||
static void entry_subtitle(mr_library_entry *e, int archive_books)
|
||||
{
|
||||
if (e->has_images)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d page%s", e->pages,
|
||||
e->pages == 1 ? "" : "s");
|
||||
else if (archive_books > 0)
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d book%s", archive_books,
|
||||
archive_books == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "%d folder%s", e->subdirs,
|
||||
e->subdirs == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
|
||||
* 档案册以档案路径为册标识,同一套逻辑复用。
|
||||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||||
static void entry_badge(mr_library_entry *e)
|
||||
{
|
||||
if (!e->has_images) return;
|
||||
char badge[24];
|
||||
if (!mr_cont_badge(e->path, badge, (int)sizeof badge)) return;
|
||||
int used = (int)strlen(e->subtitle);
|
||||
if (used < (int)sizeof e->subtitle - 2)
|
||||
snprintf(e->subtitle + used, sizeof e->subtitle - (size_t)used, " · %s", badge);
|
||||
}
|
||||
|
||||
/* 文件夹册(含可导航中间目录)一行。 */
|
||||
static int add_dir_entry(mr_library *lib, const char *name, const char *full,
|
||||
const scan_info *si)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
copy_str(e.cover, (int)sizeof e.cover, si->cover);
|
||||
e.pages = si->pages;
|
||||
e.subdirs = si->subdirs;
|
||||
e.has_images = (si->pages > 0);
|
||||
entry_subtitle(&e, si->archives);
|
||||
entry_badge(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
/* 档案册(.cbz/.zip)一行:文件即一本书,点入直接阅读。
|
||||
* cover 留空——缩略图由 UI 解压档内首图(mr_zip_cover_path)生成。 */
|
||||
static int add_archive_entry(mr_library *lib, const char *name, const char *full,
|
||||
int pages)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
e.pages = pages;
|
||||
e.subdirs = 0;
|
||||
e.has_images = 1;
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", pages, pages == 1 ? "" : "s");
|
||||
entry_badge(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
int mr_library_scan_ex(mr_library *lib, const char *dir, int max_depth,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
@@ -201,38 +294,22 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
scan_info si;
|
||||
scan_dir(full, 1, max_depth, &si);
|
||||
if (!si.nav) continue;
|
||||
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) continue;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) continue;
|
||||
copy_str(e.cover, (int)sizeof e.cover, si.cover);
|
||||
e.pages = si.pages;
|
||||
e.subdirs = si.subdirs;
|
||||
e.has_images = (si.pages > 0);
|
||||
if (e.has_images)
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d page%s", e.pages,
|
||||
e.pages == 1 ? "" : "s");
|
||||
else
|
||||
snprintf(e.subtitle, sizeof e.subtitle, "%d folder%s", e.subdirs,
|
||||
e.subdirs == 1 ? "" : "s");
|
||||
/* 续读角标(M7):有 last page 记录的册,在副标题后缀「· 续读 Pn」。
|
||||
* config 读失败/无记录时 mr_cont_badge 返回 0,副标题保持原样。 */
|
||||
if (e.has_images) {
|
||||
char badge[24];
|
||||
if (mr_cont_badge(e.path, badge, (int)sizeof badge)) {
|
||||
int used = (int)strlen(e.subtitle);
|
||||
if (used < (int)sizeof e.subtitle - 2)
|
||||
snprintf(e.subtitle + used, sizeof e.subtitle - (size_t)used,
|
||||
" · %s", badge);
|
||||
}
|
||||
int rc = 0;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
scan_info si;
|
||||
scan_dir(full, 1, max_depth, &si, archive_pages, ud);
|
||||
if (!si.nav) continue;
|
||||
rc = add_dir_entry(lib, name, full, &si);
|
||||
} else if (S_ISREG(st.st_mode) && mr_path_is_archive(name)) {
|
||||
int pages = archive_pages_at(archive_pages, ud, full);
|
||||
if (pages <= 0) continue; /* 坏档/无图片页:不当书 */
|
||||
rc = add_archive_entry(lib, name, full, pages);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (lib_add(lib, &e) != 0) {
|
||||
if (rc != 0) {
|
||||
closedir(d);
|
||||
mr_library_free(lib);
|
||||
return -1;
|
||||
@@ -244,6 +321,11 @@ int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
return mr_library_scan_ex(lib, dir, max_depth, NULL, NULL);
|
||||
}
|
||||
|
||||
int mr_path_parent(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
@@ -295,7 +377,23 @@ static int name_ptr_cmp(const void *pa, const void *pb)
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
|
||||
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
|
||||
static int sibling_is_book(const char *full, const struct stat *st,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
|
||||
if (S_ISREG(st->st_mode)) {
|
||||
const char *slash = strrchr(full, '/');
|
||||
const char *name = slash ? slash + 1 : full;
|
||||
if (!mr_path_is_archive(name)) return 0;
|
||||
return archive_pages_at(probe, ud, full) > 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
@@ -329,8 +427,8 @@ int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || S_ISLNK(st.st_mode) || !S_ISDIR(st.st_mode)) continue;
|
||||
if (!mr_library_dir_has_images(full)) continue; /* 只算「一本书」,中间目录跳过 */
|
||||
if (lstat(full, &st) != 0 || S_ISLNK(st.st_mode)) continue;
|
||||
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
|
||||
|
||||
if (n >= cap) {
|
||||
int nc = cap ? cap * 2 : 16;
|
||||
@@ -360,3 +458,8 @@ int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
free(names);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
{
|
||||
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
|
||||
}
|
||||
Reference in new issue
Block a user