224 lines
6.2 KiB
Go
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
|
|
}
|