324 lines
9.8 KiB
Go
324 lines
9.8 KiB
Go
package manga
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/extension"
|
|
hibikemanga "seanime/internal/extension/hibike/manga"
|
|
"seanime/internal/hook"
|
|
chapter_downloader "seanime/internal/manga/downloader"
|
|
manga_providers "seanime/internal/manga/providers"
|
|
"slices"
|
|
|
|
"github.com/goccy/go-json"
|
|
)
|
|
|
|
// GetDownloadedMangaChapterContainers retrieves downloaded chapter containers for a specific manga ID.
|
|
// It filters the complete set of downloaded chapters to return only those matching the provided manga ID.
|
|
func (r *Repository) GetDownloadedMangaChapterContainers(mId int, mangaCollection *anilist.MangaCollection) (ret []*ChapterContainer, err error) {
|
|
|
|
containers, err := r.GetDownloadedChapterContainers(mangaCollection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, container := range containers {
|
|
if container.MediaId == mId {
|
|
ret = append(ret, container)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// GetDownloadedChapterContainers retrieves all downloaded manga chapter containers.
|
|
// It scans the download directory for chapter folders, matches them with manga collection entries,
|
|
// and collects chapter details from file cache or provider API when necessary.
|
|
//
|
|
// Ideally, the provider API should never be called assuming the chapter details are cached.
|
|
func (r *Repository) GetDownloadedChapterContainers(mangaCollection *anilist.MangaCollection) (ret []*ChapterContainer, err error) {
|
|
ret = make([]*ChapterContainer, 0)
|
|
|
|
// Trigger hook event
|
|
reqEvent := &MangaDownloadedChapterContainersRequestedEvent{
|
|
MangaCollection: mangaCollection,
|
|
ChapterContainers: ret,
|
|
}
|
|
err = hook.GlobalHookManager.OnMangaDownloadedChapterContainersRequested().Trigger(reqEvent)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Exception occurred while triggering hook event")
|
|
return nil, fmt.Errorf("manga: Error in hook, %w", err)
|
|
}
|
|
mangaCollection = reqEvent.MangaCollection
|
|
|
|
// Default prevented, return the chapter containers
|
|
if reqEvent.DefaultPrevented {
|
|
ret = reqEvent.ChapterContainers
|
|
if ret == nil {
|
|
return nil, fmt.Errorf("manga: No chapter containers returned by hook event")
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Read download directory
|
|
files, err := os.ReadDir(r.downloadDir)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Failed to read download directory")
|
|
return nil, err
|
|
}
|
|
|
|
// Get all chapter directories
|
|
// e.g. manga_comick_123_10010_13
|
|
chapterDirs := make([]string, 0)
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
_, ok := chapter_downloader.ParseChapterDirName(file.Name())
|
|
if !ok {
|
|
continue
|
|
}
|
|
chapterDirs = append(chapterDirs, file.Name())
|
|
}
|
|
}
|
|
|
|
if len(chapterDirs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Now that we have all the chapter directories, we can get the chapter containers
|
|
|
|
keys := make([]*chapter_downloader.DownloadID, 0)
|
|
for _, dir := range chapterDirs {
|
|
downloadId, ok := chapter_downloader.ParseChapterDirName(dir)
|
|
if !ok {
|
|
continue
|
|
}
|
|
keys = append(keys, &downloadId)
|
|
}
|
|
|
|
providerAndMediaIdPairs := make(map[struct {
|
|
provider string
|
|
mediaId int
|
|
}]bool)
|
|
|
|
for _, key := range keys {
|
|
providerAndMediaIdPairs[struct {
|
|
provider string
|
|
mediaId int
|
|
}{
|
|
provider: key.Provider,
|
|
mediaId: key.MediaId,
|
|
}] = true
|
|
}
|
|
|
|
// Get the chapter containers
|
|
for pair := range providerAndMediaIdPairs {
|
|
provider := pair.provider
|
|
mediaId := pair.mediaId
|
|
|
|
// Get the manga from the collection
|
|
mangaEntry, ok := mangaCollection.GetListEntryFromMangaId(mediaId)
|
|
if !ok {
|
|
r.logger.Warn().Int("mediaId", mediaId).Msg("manga: [GetDownloadedChapterContainers] Manga not found in collection")
|
|
continue
|
|
}
|
|
|
|
// Get the list of chapters for the manga
|
|
// Check the permanent file cache
|
|
container, found := r.getChapterContainerFromPermanentFilecache(provider, mediaId)
|
|
if !found {
|
|
// Check the temporary file cache
|
|
container, found = r.getChapterContainerFromFilecache(provider, mediaId)
|
|
if !found {
|
|
// Get the chapters from the provider
|
|
// This stays here for backwards compatibility, but ideally the method should not require an internet connection
|
|
// so this will fail if the chapters were not cached & with no internet
|
|
opts := GetMangaChapterContainerOptions{
|
|
Provider: provider,
|
|
MediaId: mediaId,
|
|
Titles: mangaEntry.GetMedia().GetAllTitles(),
|
|
Year: mangaEntry.GetMedia().GetStartYearSafe(),
|
|
}
|
|
container, err = r.GetMangaChapterContainer(&opts)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Int("mediaId", mediaId).Msg("manga: [GetDownloadedChapterContainers] Failed to retrieve cached list of manga chapters")
|
|
continue
|
|
}
|
|
// Cache the chapter container in the permanent bucket
|
|
go func() {
|
|
chapterContainerKey := getMangaChapterContainerCacheKey(provider, mediaId)
|
|
chapterContainer, found := r.getChapterContainerFromFilecache(provider, mediaId)
|
|
if found {
|
|
// Store the chapter container in the permanent bucket
|
|
permBucket := getPermanentChapterContainerCacheBucket(provider, mediaId)
|
|
_ = r.fileCacher.SetPerm(permBucket, chapterContainerKey, chapterContainer)
|
|
}
|
|
}()
|
|
}
|
|
} else {
|
|
r.logger.Trace().Int("mediaId", mediaId).Msg("manga: Found chapter container in permanent bucket")
|
|
}
|
|
|
|
downloadedContainer := &ChapterContainer{
|
|
MediaId: container.MediaId,
|
|
Provider: container.Provider,
|
|
Chapters: make([]*hibikemanga.ChapterDetails, 0),
|
|
}
|
|
|
|
// Now that we have the container, we'll filter out the chapters that are not downloaded
|
|
// Go through each chapter and check if it's downloaded
|
|
for _, chapter := range container.Chapters {
|
|
// For each chapter, check if the chapter directory exists
|
|
for _, dir := range chapterDirs {
|
|
if dir == chapter_downloader.FormatChapterDirName(provider, mediaId, chapter.ID, chapter.Chapter) {
|
|
downloadedContainer.Chapters = append(downloadedContainer.Chapters, chapter)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(downloadedContainer.Chapters) == 0 {
|
|
continue
|
|
}
|
|
|
|
ret = append(ret, downloadedContainer)
|
|
}
|
|
|
|
// Add chapter containers from local provider
|
|
localProviderB, ok := extension.GetExtension[extension.MangaProviderExtension](r.providerExtensionBank, manga_providers.LocalProvider)
|
|
if ok {
|
|
_, ok := localProviderB.GetProvider().(*manga_providers.Local)
|
|
if ok {
|
|
for _, list := range mangaCollection.MediaListCollection.GetLists() {
|
|
for _, entry := range list.GetEntries() {
|
|
media := entry.GetMedia()
|
|
opts := GetMangaChapterContainerOptions{
|
|
Provider: manga_providers.LocalProvider,
|
|
MediaId: media.GetID(),
|
|
Titles: media.GetAllTitles(),
|
|
Year: media.GetStartYearSafe(),
|
|
}
|
|
container, err := r.GetMangaChapterContainer(&opts)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ret = append(ret, container)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event
|
|
ev := &MangaDownloadedChapterContainersEvent{
|
|
ChapterContainers: ret,
|
|
}
|
|
err = hook.GlobalHookManager.OnMangaDownloadedChapterContainers().Trigger(ev)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Exception occurred while triggering hook event")
|
|
return nil, fmt.Errorf("manga: Error in hook, %w", err)
|
|
}
|
|
ret = ev.ChapterContainers
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// getDownloadedMangaPageContainer retrieves page information for a downloaded manga chapter.
|
|
// It reads the chapter directory and parses the registry file to build a PageContainer
|
|
// with details about each downloaded page including dimensions and file paths.
|
|
func (r *Repository) getDownloadedMangaPageContainer(
|
|
provider string,
|
|
mediaId int,
|
|
chapterId string,
|
|
) (*PageContainer, error) {
|
|
|
|
// Check if the chapter is downloaded
|
|
found := false
|
|
|
|
// Read download directory
|
|
files, err := os.ReadDir(r.downloadDir)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Failed to read download directory")
|
|
return nil, err
|
|
}
|
|
|
|
chapterDir := "" // e.g. manga_comick_123_10010_13
|
|
for _, file := range files {
|
|
if file.IsDir() {
|
|
|
|
downloadId, ok := chapter_downloader.ParseChapterDirName(file.Name())
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if downloadId.Provider == provider &&
|
|
downloadId.MediaId == mediaId &&
|
|
downloadId.ChapterId == chapterId {
|
|
found = true
|
|
chapterDir = file.Name()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, ErrChapterNotDownloaded
|
|
}
|
|
|
|
r.logger.Debug().Msg("manga: Found downloaded chapter directory")
|
|
|
|
// Open registry file
|
|
registryFile, err := os.Open(filepath.Join(r.downloadDir, chapterDir, "registry.json"))
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Failed to open registry file")
|
|
return nil, err
|
|
}
|
|
defer registryFile.Close()
|
|
|
|
r.logger.Debug().Str("chapterId", chapterId).Msg("manga: Reading registry file")
|
|
|
|
// Read registry file
|
|
var pageRegistry *chapter_downloader.Registry
|
|
err = json.NewDecoder(registryFile).Decode(&pageRegistry)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("manga: Failed to decode registry file")
|
|
return nil, err
|
|
}
|
|
|
|
pageList := make([]*hibikemanga.ChapterPage, 0)
|
|
pageDimensions := make(map[int]*PageDimension)
|
|
|
|
// Get the downloaded pages
|
|
for pageIndex, pageInfo := range *pageRegistry {
|
|
pageList = append(pageList, &hibikemanga.ChapterPage{
|
|
Index: pageIndex,
|
|
URL: filepath.Join(chapterDir, pageInfo.Filename),
|
|
Provider: provider,
|
|
})
|
|
pageDimensions[pageIndex] = &PageDimension{
|
|
Width: pageInfo.Width,
|
|
Height: pageInfo.Height,
|
|
}
|
|
}
|
|
|
|
slices.SortStableFunc(pageList, func(i, j *hibikemanga.ChapterPage) int {
|
|
return cmp.Compare(i.Index, j.Index)
|
|
})
|
|
|
|
container := &PageContainer{
|
|
MediaId: mediaId,
|
|
Provider: provider,
|
|
ChapterId: chapterId,
|
|
Pages: pageList,
|
|
PageDimensions: pageDimensions,
|
|
IsDownloaded: true,
|
|
}
|
|
|
|
r.logger.Debug().Str("chapterId", chapterId).Msg("manga: Found downloaded chapter")
|
|
|
|
return container, nil
|
|
}
|