Files
seanime-docker/seanime-2.9.10/internal/manga/chapter_page_container.go
2025-09-20 14:08:38 +01:00

241 lines
6.9 KiB
Go

package manga
import (
"errors"
"fmt"
"seanime/internal/extension"
manga_providers "seanime/internal/manga/providers"
"seanime/internal/util"
"sync"
hibikemanga "seanime/internal/extension/hibike/manga"
)
type (
// PageContainer is used to display the list of pages from a chapter in the client.
// It is cached in the file cache bucket with a key of the format: {provider}${mediaId}${chapterId}
PageContainer struct {
MediaId int `json:"mediaId"`
Provider string `json:"provider"`
ChapterId string `json:"chapterId"`
Pages []*hibikemanga.ChapterPage `json:"pages"`
PageDimensions map[int]*PageDimension `json:"pageDimensions"` // Indexed by page number
IsDownloaded bool `json:"isDownloaded"` // TODO remove
}
// PageDimension is used to store the dimensions of a page.
// It is used by the client for 'Double Page' mode.
PageDimension struct {
Width int `json:"width"`
Height int `json:"height"`
}
)
// GetMangaPageContainer returns the PageContainer for a manga chapter based on the provider.
func (r *Repository) GetMangaPageContainer(
provider string,
mediaId int,
chapterId string,
doublePage bool,
isOffline *bool,
) (ret *PageContainer, err error) {
defer util.HandlePanicInModuleWithError("manga/GetMangaPageContainer", &err)
// +---------------------+
// | Downloads |
// +---------------------+
providerExtension, ok := extension.GetExtension[extension.MangaProviderExtension](r.providerExtensionBank, provider)
if !ok {
r.logger.Error().Str("provider", provider).Msg("manga: Provider not found")
return nil, errors.New("manga: Provider not found")
}
_, isLocalProvider := providerExtension.GetProvider().(*manga_providers.Local)
if *isOffline && !isLocalProvider {
ret, err = r.getDownloadedMangaPageContainer(provider, mediaId, chapterId)
if err != nil {
return nil, err
}
return ret, nil
}
if !isLocalProvider {
ret, _ = r.getDownloadedMangaPageContainer(provider, mediaId, chapterId)
if ret != nil {
return ret, nil
}
}
// +---------------------+
// | Get Pages |
// +---------------------+
// PageContainer key
pageContainerKey := fmt.Sprintf("%s$%d$%s", provider, mediaId, chapterId)
r.logger.Trace().
Str("provider", provider).
Int("mediaId", mediaId).
Str("key", pageContainerKey).
Str("chapterId", chapterId).
Msgf("manga: Getting pages")
// +---------------------+
// | Cache |
// +---------------------+
var container *PageContainer
// PageContainer bucket
// e.g., manga_comick_pages_123
// -> { "comick$123$10010": PageContainer }, { "comick$123$10011": PageContainer }
pageBucket := r.getFcProviderBucket(provider, mediaId, bucketTypePage)
// Check if the container is in the cache
if found, _ := r.fileCacher.Get(pageBucket, pageContainerKey, &container); found && !isLocalProvider {
// Hydrate page dimensions
pageDimensions, _ := r.getPageDimensions(doublePage, provider, mediaId, chapterId, container.Pages)
container.PageDimensions = pageDimensions
r.logger.Debug().Str("key", pageContainerKey).Msg("manga: Page Container Cache HIT")
return container, nil
}
// +---------------------+
// | Fetch pages |
// +---------------------+
// Search for the chapter in the cache
containerBucket := r.getFcProviderBucket(provider, mediaId, bucketTypeChapter)
chapterContainerKey := getMangaChapterContainerCacheKey(provider, mediaId)
var chapterContainer *ChapterContainer
if found, _ := r.fileCacher.Get(containerBucket, chapterContainerKey, &chapterContainer); !found {
r.logger.Error().Msg("manga: Chapter Container not found")
return nil, ErrNoChapters
}
// Get the chapter from the container
var chapter *hibikemanga.ChapterDetails
for _, c := range chapterContainer.Chapters {
if c.ID == chapterId {
chapter = c
break
}
}
if chapter == nil {
r.logger.Error().Msg("manga: Chapter not found")
return nil, ErrChapterNotFound
}
// Get the chapter pages
var pages []*hibikemanga.ChapterPage
pages, err = providerExtension.GetProvider().FindChapterPages(chapter.ID)
if err != nil {
r.logger.Error().Err(err).Msg("manga: Could not get chapter pages")
return nil, err
}
if pages == nil || len(pages) == 0 {
r.logger.Error().Msg("manga: No pages found")
return nil, fmt.Errorf("manga: No pages found")
}
// Overwrite provider just in case
for _, page := range pages {
page.Provider = provider
}
pageDimensions, _ := r.getPageDimensions(doublePage, provider, mediaId, chapterId, pages)
container = &PageContainer{
MediaId: mediaId,
Provider: provider,
ChapterId: chapterId,
Pages: pages,
PageDimensions: pageDimensions,
IsDownloaded: false,
}
// Set cache only if not local provider
if !isLocalProvider {
err = r.fileCacher.Set(pageBucket, pageContainerKey, container)
if err != nil {
r.logger.Warn().Err(err).Msg("manga: Failed to populate cache")
}
}
r.logger.Debug().Str("key", pageContainerKey).Msg("manga: Retrieved pages")
return container, nil
}
func (r *Repository) getPageDimensions(enabled bool, provider string, mediaId int, chapterId string, pages []*hibikemanga.ChapterPage) (ret map[int]*PageDimension, err error) {
defer util.HandlePanicInModuleWithError("manga/getPageDimensions", &err)
if !enabled {
return nil, nil
}
// e.g. comick$123$10010
key := fmt.Sprintf("%s$%d$%s", provider, mediaId, chapterId)
// Page dimensions bucket
// e.g., manga_comick_page-dimensions_123
// -> { "comick$123$10010": PageDimensions }, { "comick$123$10011": PageDimensions }
dimensionBucket := r.getFcProviderBucket(provider, mediaId, bucketTypePageDimensions)
if found, _ := r.fileCacher.Get(dimensionBucket, key, &ret); found {
r.logger.Debug().Str("key", key).Msg("manga: Page Dimensions Cache HIT")
return
}
r.logger.Trace().Str("key", key).Msg("manga: Getting page dimensions")
// Get the page dimensions
pageDimensions := make(map[int]*PageDimension)
mu := sync.Mutex{}
wg := sync.WaitGroup{}
for _, page := range pages {
wg.Add(1)
go func(page *hibikemanga.ChapterPage) {
defer wg.Done()
var buf []byte
if page.Buf != nil {
buf = page.Buf
} else {
buf, err = manga_providers.GetImageByProxy(page.URL, page.Headers)
if err != nil {
return
}
}
width, height, err := getImageNaturalSizeB(buf)
if err != nil {
//r.logger.Warn().Err(err).Int("index", page.Index).Msg("manga: failed to get image size")
return
}
mu.Lock()
// DEVNOTE: Index by page index
pageDimensions[page.Index] = &PageDimension{
Width: width,
Height: height,
}
mu.Unlock()
}(page)
}
wg.Wait()
_ = r.fileCacher.Set(dimensionBucket, key, pageDimensions)
r.logger.Info().Str("bucket", dimensionBucket.Name()).Msg("manga: Retrieved page dimensions")
return pageDimensions, nil
}