- comics: reads pages straight out of the archive on demand. No extraction pass, so a 200-page CBZ is not materialised for a reader who views three pages. Pages are read into memory and served via http.ServeContent, which gives Range for free. - natural page ordering: page2 sorts before page10. Lexical ordering shuffles a numbered archive, which is the difference between a readable book and a scrambled one. - skips __MACOSX and dotfiles; rejects archives with no images. - cover is the first page. - media.Item gained ModTime (from the backing file) so page responses carry a Last-Modified and support conditional GETs. - web: ComicReader with a bounded prefetch window (two pages each way), page jump, keyboard nav, and progress stored as a page number. Verified: 3-page CBZ served in natural order, out-of-range 404, cover from page 1, progress round-trip. svelte-check 0/0.
52 lines
1.1 KiB
Go
52 lines
1.1 KiB
Go
package comics
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/mithral/vellum/internal/media"
|
|
)
|
|
|
|
func init() {
|
|
media.RegisterCoverExtractor(Kind, ExtractCover)
|
|
}
|
|
|
|
// ExtractCover writes the first page of the archive into the cache and returns
|
|
// its path. A comic's first page is its cover by convention.
|
|
func ExtractCover(ctx context.Context, db *sql.DB, item media.Item, absPath, cacheDir string) (string, error) {
|
|
pages, err := ListPages(absPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
dir, err := media.CoverCacheDir(cacheDir, Kind)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ext := strings.ToLower(path.Ext(pages[0].Name))
|
|
if ext == "" {
|
|
ext = ".jpg"
|
|
}
|
|
outPath := filepath.Join(dir, fmt.Sprintf("%d%s", item.ID, ext))
|
|
|
|
data, err := ReadPage(absPath, pages[0].Name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tmp := outPath + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0o640); err != nil {
|
|
return "", fmt.Errorf("comics: write cover: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, outPath); err != nil {
|
|
_ = os.Remove(tmp)
|
|
return "", err
|
|
}
|
|
return outPath, nil
|
|
}
|