初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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 {
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user