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

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
}