mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 20:26:36 +08:00
优化
This commit is contained in:
@@ -718,6 +718,9 @@ func mergeSearchResults(hits []searchHit, key string) []SearchBook {
|
||||
k := b.Name + "|" + b.Author
|
||||
if existing, ok := merged[k]; ok {
|
||||
existing.Origins = append(existing.Origins, b.Origins...)
|
||||
// 同书多源时补齐缺失字段:legado 是「一源一行」,各源自己显示拿到的
|
||||
// 信息;MeBox 合并成一行,若不补齐,先到的空值会挡掉后面源的有效值。
|
||||
fillMissingSearchBookFields(existing, &b)
|
||||
continue
|
||||
}
|
||||
merged[k] = &b
|
||||
@@ -739,6 +742,29 @@ func mergeSearchResults(hits []searchHit, key string) []SearchBook {
|
||||
return out
|
||||
}
|
||||
|
||||
// fillMissingSearchBookFields 把同书多源结果里另一份的有效字段补进合并结果。
|
||||
//
|
||||
// 同一个聚合源的不同上游、或不同书源,对同一本书的覆盖能力不同:有的能给封面、
|
||||
// 简介、字数、最新章节,有的只给书名。合并成一行时必须补齐,否则先到的空值会把
|
||||
// 后面源的有效值挡掉——典型表现就是「这本书明明有源带封面,列表里却是空白」。
|
||||
func fillMissingSearchBookFields(dst, src *SearchBook) {
|
||||
if dst.CoverURL == "" {
|
||||
dst.CoverURL = src.CoverURL
|
||||
}
|
||||
if dst.Intro == "" {
|
||||
dst.Intro = src.Intro
|
||||
}
|
||||
if dst.Kind == "" {
|
||||
dst.Kind = src.Kind
|
||||
}
|
||||
if dst.WordCount == "" {
|
||||
dst.WordCount = src.WordCount
|
||||
}
|
||||
if dst.LatestChapter == "" {
|
||||
dst.LatestChapter = src.LatestChapter
|
||||
}
|
||||
}
|
||||
|
||||
// searchInSource 单源搜索(对应 WebBook.searchBook)。
|
||||
func (s *ReaderService) searchInSource(ctx context.Context, src *model.ReaderBookSource, bs *BookSource, key string, page int) ([]SearchBook, error) {
|
||||
if bs == nil {
|
||||
@@ -800,7 +826,7 @@ func (s *ReaderService) searchInSource(ctx context.Context, src *model.ReaderBoo
|
||||
WordCount: wordCount,
|
||||
LatestChapter: lastChapter,
|
||||
Intro: intro,
|
||||
CoverURL: cover,
|
||||
CoverURL: normalizeCoverURL(cover),
|
||||
BookURL: bookURL,
|
||||
Origins: []SearchOrigin{{
|
||||
SourceID: src.ID,
|
||||
@@ -835,6 +861,33 @@ func firstNonEmpty(vals ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// normalizeCoverURL 过滤书源规则给出的封面地址,只保留浏览器能真正渲染成图片的
|
||||
// 值,其余一律返回空串(前端据此显示占位图标,而不是破图)。
|
||||
//
|
||||
// 背景:书源在「这本书没有封面」时不一定返回空值。legado 语义下
|
||||
// getString(rule, isUrl=true) 取值为空会回退成 baseUrl,而聚合类书源
|
||||
// (如「光遇聚合」)的搜索请求地址本身就是 data:;base64,... 参数信封,
|
||||
// 于是封面上会落一串 data: 文本,<img> 按 text/plain 处理必然破图。
|
||||
// 允许的相对地址以 "/" 开头,用于应用自身生成的本地书封面
|
||||
// (/api/reader/local/asset?...)。
|
||||
func normalizeCoverURL(raw string) string {
|
||||
u := strings.TrimSpace(raw)
|
||||
if u == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(u)
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"):
|
||||
return u
|
||||
case strings.HasPrefix(lower, "data:image/"):
|
||||
return u
|
||||
case strings.HasPrefix(u, "/"):
|
||||
return u
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 详情 / 目录 / 正文 ─────────────────────────────────────────────────────
|
||||
|
||||
// BookInfo 书籍详情(对应 legado Book 信息页)。
|
||||
@@ -912,7 +965,7 @@ func (s *ReaderService) getBookInfoFrom(ctx context.Context, src *model.ReaderBo
|
||||
info.Intro = v
|
||||
}
|
||||
if v, err := ar.GetString(SPtr(bir.CoverURL), nil, true); err == nil && v != "" {
|
||||
info.CoverURL = v
|
||||
info.CoverURL = normalizeCoverURL(v)
|
||||
}
|
||||
if v, err := ar.GetString(SPtr(bir.TocURL), nil, true); err == nil && v != "" {
|
||||
info.TocURL = v
|
||||
@@ -1300,7 +1353,7 @@ func (s *ReaderService) AddBook(ctx context.Context, userID string, origin Searc
|
||||
BookURL: origin.BookURL,
|
||||
Name: name,
|
||||
Author: author,
|
||||
CoverURL: coverURL,
|
||||
CoverURL: normalizeCoverURL(coverURL),
|
||||
Type: origin.OriginType,
|
||||
}
|
||||
if err := s.repo.CreateBook(ctx, book); err != nil {
|
||||
@@ -1309,6 +1362,67 @@ func (s *ReaderService) AddBook(ctx context.Context, userID string, origin Searc
|
||||
return book, nil
|
||||
}
|
||||
|
||||
// SwitchOrigin 换源:把书架里的书切到另一个书源(对应 legado 的「换源」)。
|
||||
//
|
||||
// 换源要同时处理三件事:
|
||||
// 1. 来源字段整体换成新源(origin / origin_name / book_url / toc_url);
|
||||
// 2. 旧源缓存的目录必须清掉——章节地址只对旧源有效,留着会让阅读器读到错内容;
|
||||
// 3. 阅读进度保留(按章节序号定位,与 legado 的做法一致)。
|
||||
//
|
||||
// 新源详情是「尽力而为」:抓得到就用它的 tocUrl(以及当前封面为空时的封面),
|
||||
// 抓不到就把 tocUrl 留空让目录回落到书籍地址重新解析,不让一次网络抖动挡住换源。
|
||||
func (s *ReaderService) SwitchOrigin(ctx context.Context, userID, bookID string, origin SearchOrigin) (*model.ReaderBook, error) {
|
||||
book, err := s.repo.GetBook(ctx, bookID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if book.UserID != userID {
|
||||
return nil, fmt.Errorf("无权操作他人书架")
|
||||
}
|
||||
if book.LocalPath != "" {
|
||||
return nil, fmt.Errorf("本地导入的书籍没有书源,无法换源")
|
||||
}
|
||||
target := strings.TrimSpace(origin.BookURL)
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("缺少目标书源的书本地址")
|
||||
}
|
||||
if _, _, err := s.loadSourceFlexible(ctx, origin.SourceID, origin.Origin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if book.BookURL == target && book.Origin == origin.Origin {
|
||||
return book, nil // 已经是这个源,重复点击视为成功
|
||||
}
|
||||
|
||||
info, infoErr := s.GetBookInfo(ctx, origin.SourceID, origin.Origin, target)
|
||||
if infoErr != nil && s.log != nil {
|
||||
s.log.Warn("reader: 换源时读取新源详情失败",
|
||||
zap.String("book", book.ID), zap.String("origin", origin.Origin), zap.Error(infoErr))
|
||||
}
|
||||
|
||||
book.Origin = origin.Origin
|
||||
book.OriginName = firstNonEmpty(origin.OriginName, book.OriginName)
|
||||
book.BookURL = target
|
||||
book.TocURL = ""
|
||||
if info != nil {
|
||||
book.TocURL = strings.TrimSpace(info.TocURL)
|
||||
if book.CoverURL == "" {
|
||||
book.CoverURL = normalizeCoverURL(info.CoverURL)
|
||||
}
|
||||
if info.LatestChapter != "" {
|
||||
book.LatestChapterTitle = info.LatestChapter
|
||||
}
|
||||
}
|
||||
// 目录是旧源的缓存,必须清空;章节数一并归零,等新源目录重新预热后再算未读。
|
||||
book.TotalChapterNum = 0
|
||||
if err := s.repo.ReplaceChapters(ctx, book.ID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.UpdateBook(ctx, book); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return book, nil
|
||||
}
|
||||
|
||||
// GetBook 按 ID 取书(媒体代理等使用)。
|
||||
func (s *ReaderService) GetBook(ctx context.Context, id string) (*model.ReaderBook, error) {
|
||||
return s.repo.GetBook(ctx, id)
|
||||
|
||||
Reference in New Issue
Block a user