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

224 lines
6.2 KiB
Go

package chapter_downloader
import (
"github.com/goccy/go-json"
"github.com/rs/zerolog"
"seanime/internal/database/db"
"seanime/internal/database/models"
"seanime/internal/events"
hibikemanga "seanime/internal/extension/hibike/manga"
"seanime/internal/util"
"sync"
"time"
)
const (
QueueStatusNotStarted QueueStatus = "not_started"
QueueStatusDownloading QueueStatus = "downloading"
QueueStatusErrored QueueStatus = "errored"
)
type (
// Queue is used to manage the download queue.
// If feeds the downloader with the next item in the queue.
Queue struct {
logger *zerolog.Logger
mu sync.Mutex
db *db.Database
current *QueueInfo
runCh chan *QueueInfo // Channel to tell downloader to run the next item
active bool
wsEventManager events.WSEventManagerInterface
}
QueueStatus string
// QueueInfo stores details about the download progress of a chapter.
QueueInfo struct {
DownloadID
Pages []*hibikemanga.ChapterPage
DownloadedUrls []string
Status QueueStatus
}
)
func NewQueue(db *db.Database, logger *zerolog.Logger, wsEventManager events.WSEventManagerInterface, runCh chan *QueueInfo) *Queue {
return &Queue{
logger: logger,
db: db,
runCh: runCh,
wsEventManager: wsEventManager,
}
}
// Add adds a chapter to the download queue.
// It tells the queue to download the next item if possible.
func (q *Queue) Add(id DownloadID, pages []*hibikemanga.ChapterPage, runNext bool) error {
q.mu.Lock()
defer q.mu.Unlock()
marshalled, err := json.Marshal(pages)
if err != nil {
q.logger.Error().Err(err).Msgf("Failed to marshal pages for id %v", id)
return err
}
err = q.db.InsertChapterDownloadQueueItem(&models.ChapterDownloadQueueItem{
BaseModel: models.BaseModel{},
Provider: id.Provider,
MediaID: id.MediaId,
ChapterNumber: id.ChapterNumber,
ChapterID: id.ChapterId,
PageData: marshalled,
Status: string(QueueStatusNotStarted),
})
if err != nil {
q.logger.Error().Err(err).Msgf("Failed to insert chapter download queue item for id %v", id)
return err
}
q.logger.Info().Msgf("chapter downloader: Added chapter to download queue: %s", id.ChapterId)
q.wsEventManager.SendEvent(events.ChapterDownloadQueueUpdated, nil)
if runNext && q.active {
// Tells queue to run next if possible
go q.runNext()
}
return nil
}
func (q *Queue) HasCompleted(queueInfo *QueueInfo) {
q.mu.Lock()
defer q.mu.Unlock()
if queueInfo.Status == QueueStatusErrored {
q.logger.Warn().Msgf("chapter downloader: Errored %s", queueInfo.DownloadID.ChapterId)
// Update the status of the current item in the database.
_ = q.db.UpdateChapterDownloadQueueItemStatus(q.current.DownloadID.Provider, q.current.DownloadID.MediaId, q.current.DownloadID.ChapterId, string(QueueStatusErrored))
} else {
q.logger.Debug().Msgf("chapter downloader: Dequeueing %s", queueInfo.DownloadID.ChapterId)
// Dequeue the item from the database.
_, err := q.db.DequeueChapterDownloadQueueItem()
if err != nil {
q.logger.Error().Err(err).Msgf("Failed to dequeue chapter download queue item for id %v", queueInfo.DownloadID)
return
}
}
q.wsEventManager.SendEvent(events.ChapterDownloadQueueUpdated, nil)
q.wsEventManager.SendEvent(events.RefreshedMangaDownloadData, nil)
// Reset current item
q.current = nil
if q.active {
// Tells queue to run next if possible
q.runNext()
}
}
// Run activates the queue and invokes runNext
func (q *Queue) Run() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.active {
q.logger.Debug().Msg("chapter downloader: Starting queue")
}
q.active = true
// Tells queue to run next if possible
q.runNext()
}
// Stop deactivates the queue
func (q *Queue) Stop() {
q.mu.Lock()
defer q.mu.Unlock()
if q.active {
q.logger.Debug().Msg("chapter downloader: Stopping queue")
}
q.active = false
}
// runNext runs the next item in the queue.
// - Checks if there is a current item, if so, it returns.
// - If nothing is running, it gets the next item (QueueInfo) from the database, sets it as current and sends it to the downloader.
func (q *Queue) runNext() {
q.logger.Debug().Msg("chapter downloader: Processing next item in queue")
// Catch panic in runNext, so it doesn't bubble up and stop goroutines.
defer util.HandlePanicInModuleThen("internal/manga/downloader/runNext", func() {
q.logger.Error().Msg("chapter downloader: Panic in 'runNext'")
})
if q.current != nil {
q.logger.Debug().Msg("chapter downloader: Current item is not nil")
return
}
q.logger.Debug().Msg("chapter downloader: Checking next item in queue")
// Get next item from the database.
next, _ := q.db.GetNextChapterDownloadQueueItem()
if next == nil {
q.logger.Debug().Msg("chapter downloader: No next item in queue")
return
}
id := DownloadID{
Provider: next.Provider,
MediaId: next.MediaID,
ChapterId: next.ChapterID,
ChapterNumber: next.ChapterNumber,
}
q.logger.Debug().Msgf("chapter downloader: Preparing next item in queue: %s", id.ChapterId)
q.wsEventManager.SendEvent(events.ChapterDownloadQueueUpdated, nil)
// Update status
_ = q.db.UpdateChapterDownloadQueueItemStatus(id.Provider, id.MediaId, id.ChapterId, string(QueueStatusDownloading))
// Set the current item.
q.current = &QueueInfo{
DownloadID: id,
DownloadedUrls: make([]string, 0),
Status: QueueStatusDownloading,
}
// Unmarshal the page data.
err := json.Unmarshal(next.PageData, &q.current.Pages)
if err != nil {
q.logger.Error().Err(err).Msgf("Failed to unmarshal pages for id %v", id)
_ = q.db.UpdateChapterDownloadQueueItemStatus(id.Provider, id.MediaId, id.ChapterId, string(QueueStatusNotStarted))
return
}
// TODO: This is a temporary fix to prevent the downloader from running too fast.
time.Sleep(5 * time.Second)
q.logger.Info().Msgf("chapter downloader: Running next item in queue: %s", id.ChapterId)
// Tell Downloader to run
q.runCh <- q.current
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (q *Queue) GetCurrent() (qi *QueueInfo, ok bool) {
q.mu.Lock()
defer q.mu.Unlock()
if q.current == nil {
return nil, false
}
return q.current, true
}