Files

240 lines
6.5 KiB
Go

package app
import (
"context"
"encoding/json"
"net/http"
"regexp"
"strings"
"time"
)
var finishedEpisodeRemark = regexp.MustCompile(`(?:全\s*\d+\s*集|\d+\s*集全|已完结|大结局)`)
func releaseStatusFromRemark(remark string) string {
if strings.Contains(remark, "未完结") || strings.Contains(remark, "更新至") || strings.Contains(remark, "连载") {
return "ongoing"
}
if finishedEpisodeRemark.MatchString(remark) || strings.Contains(remark, "完结") {
return "finished"
}
return "unknown"
}
func dramaReleaseStatus(drama Drama) string {
switch drama.ReleaseStatus {
case "finished", "ongoing", "unknown":
return drama.ReleaseStatus
default:
return releaseStatusFromRemark(drama.Remark)
}
}
func (a *UIApp) removeTaskLocked(id string) {
task := a.tasks[id]
delete(a.tasks, id)
order := a.taskOrder[:0]
groupRemains := false
for _, taskID := range a.taskOrder {
if current := a.tasks[taskID]; current != nil {
order = append(order, taskID)
if task != nil && current.DramaID == task.DramaID {
groupRemains = true
}
}
}
a.taskOrder = order
if task != nil && !groupRemains {
if _, _, err := a.mergedPlaybackTaskLocked(mergedPlaybackPrefix + task.DramaID); err != nil && a.mergeJobs[task.DramaID] == nil {
delete(a.merges, task.DramaID)
}
}
}
func (a *UIApp) startDramaParse(drama Drama, updateOnly bool) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
a.mu.Lock()
if a.parsingCancels == nil {
a.parsingCancels = map[string]context.CancelFunc{}
}
a.parsingCancels[placeholderDramaKey(drama)] = cancel
if a.finishCanceledParseLocked(drama) {
a.mu.Unlock()
cancel()
a.markParsingFinished([]Drama{drama})
return
}
quality := 0
if task := a.tasks[uiTaskID(a.placeholderTask(drama, "parsing").OutPath)]; task != nil {
quality = task.Source.DownloadQuality
}
a.mu.Unlock()
go func() {
defer cancel()
defer a.markParsingFinished([]Drama{drama})
_, _, _ = a.enqueueDramas(ctx, []Drama{drama}, updateOnly, quality)
}()
}
func (a *UIApp) finishCanceledParseLocked(drama Drama) bool {
id := uiTaskID(a.placeholderTask(drama, "parsing").OutPath)
task := a.tasks[id]
if task == nil || !task.CancelRequested && !task.RemoveRequested && !task.PauseRequested {
return false
}
if task.RemoveRequested {
a.removeTaskLocked(id)
} else if task.PauseRequested {
task.Status = uiStatusPaused
task.Phase = "paused"
task.PauseRequested = false
task.CancelRequested = false
task.Error = ""
} else {
task.Status = uiStatusCanceled
task.Phase = "canceled"
task.CancelRequested = false
task.Error = "章节解析已取消"
task.UpdatedAt = time.Now()
}
_ = a.saveStateLocked()
return true
}
func isChapterPlaceholderTask(task *UITask) bool {
return task != nil && (strings.Contains(task.Source.Chapter.ID, ":fetch-failed") || strings.Contains(task.Source.Chapter.ID, ":parsing"))
}
type taskSelection struct {
IDs []string `json:"ids"`
DramaIDs []string `json:"dramaIds"`
}
func readTaskSelection(writer http.ResponseWriter, request *http.Request) (taskSelection, bool) {
var selection taskSelection
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&selection); err != nil {
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "无效的任务选择"})
return selection, false
}
if _, valid := cleanIDList(writer, append(append([]string{}, selection.IDs...), selection.DramaIDs...)); !valid {
return selection, false
}
return selection, true
}
func (selection taskSelection) idsLocked(app *UIApp) []string {
groups := map[string]bool{}
selected := map[string]bool{}
for _, id := range selection.DramaIDs {
groups[strings.TrimSpace(id)] = true
}
for _, id := range selection.IDs {
selected[strings.TrimSpace(id)] = true
}
var ids []string
for _, id := range app.taskOrder {
task := app.tasks[id]
if task != nil && (selected[id] || groups[task.DramaID]) {
ids = append(ids, id)
}
}
return ids
}
func (a *UIApp) handlePause(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
writeJSON(writer, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
selection, ok := readTaskSelection(writer, request)
if !ok {
return
}
a.mu.Lock()
if !a.requireTaskSourcesLocked(writer, request, selection) {
a.mu.Unlock()
return
}
for _, id := range selection.idsLocked(a) {
task := a.tasks[id]
if task == nil || task.RemoveRequested || task.CancelRequested {
continue
}
switch task.Status {
case uiStatusQueued:
task.Status = uiStatusPaused
task.Phase = "paused"
case uiStatusRunning, uiStatusParsing:
task.PauseRequested = true
if cancel := a.runningCancels[id]; cancel != nil {
cancel()
}
if task.Status == uiStatusParsing {
if cancel := a.parsingCancels[task.DramaID]; cancel != nil {
cancel()
}
}
}
task.UpdatedAt = time.Now()
}
_ = a.saveStateLocked()
views := a.taskViewsForSourceLocked(request.Context())
a.mu.Unlock()
writeJSON(writer, http.StatusOK, map[string]any{"data": views})
}
func (a *UIApp) handleResume(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodPost {
writeJSON(writer, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
selection, ok := readTaskSelection(writer, request)
if !ok {
return
}
a.mu.Lock()
if !a.requireTaskSourcesLocked(writer, request, selection) {
a.mu.Unlock()
return
}
if a.parsingInFlight == nil {
a.parsingInFlight = map[string]bool{}
}
var reparses []Drama
for _, id := range selection.idsLocked(a) {
task := a.tasks[id]
if task == nil || task.Status != uiStatusPaused || task.RemoveRequested {
continue
}
if isChapterPlaceholderTask(task) {
drama := dramaFromPlaceholderTask(task)
if a.parsingInFlight[drama.ID] {
continue
}
a.removeTaskLocked(id)
a.parsingInFlight[drama.ID] = true
a.addParsingPlaceholderTaskLocked(drama, task.Source.DownloadQuality)
reparses = append(reparses, drama)
continue
}
task.Status = uiStatusQueued
task.Phase = "queued"
task.Progress = 0
task.DownloadedBytes = 0
task.SpeedBytesPerSecond = 0
task.PauseRequested = false
task.CancelRequested = false
task.UpdatedAt = time.Now()
}
_ = a.saveStateLocked()
a.cond.Broadcast()
views := a.taskViewsForSourceLocked(request.Context())
a.mu.Unlock()
for _, drama := range reparses {
a.startDramaParse(drama, true)
}
writeJSON(writer, http.StatusOK, map[string]any{"data": views})
}