Files
djk/internal/app/ui_drama_refresh.go
T

192 lines
5.5 KiB
Go

package app
import (
"context"
"errors"
"math"
"net/http"
"reflect"
"time"
)
const dramaRefreshRetryDelay = 5 * time.Minute
type dramaRefreshResult struct {
DramaID string `json:"dramaId"`
Drama Drama `json:"drama"`
RetryAfter int `json:"retryAfter"`
Warning string `json:"warning,omitempty"`
}
type dramaRefreshCall struct {
done chan struct{}
err error
retryAt time.Time
}
func (a *UIApp) handleDramaRefresh(w http.ResponseWriter, r *http.Request) {
var input struct {
DramaID string `json:"dramaId"`
}
if !readPlaybackRequest(w, r, &input) || !a.requireDramaSources(w, r, []string{input.DramaID}) {
return
}
source, id, valid := splitProviderDramaID(input.DramaID)
if !valid || len(input.DramaID) > 512 || !supportsSortMetadata(Drama{Source: source}) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "剧集资料更新参数无效"})
return
}
input.DramaID = providerDramaID(source, id)
a.mu.Lock()
drama := a.dramaForRefreshLocked(input.DramaID)
a.mu.Unlock()
if drama.ID == "" {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "剧库中没有该剧集"})
return
}
result, err := a.refreshDramaMetadata(r.Context(), drama)
if r.Context().Err() != nil {
return
}
if err != nil {
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "资料更新繁忙,可稍后重新打开剧集"})
return
}
writeJSON(w, http.StatusOK, result)
}
func (a *UIApp) dramaForRefreshLocked(id string) Drama {
for _, drama := range a.dramas {
if drama.ID == id {
return drama
}
}
return Drama{}
}
func (a *UIApp) dramaRefreshResultLocked(id string, call *dramaRefreshCall) dramaRefreshResult {
result := dramaRefreshResult{DramaID: id, Drama: a.dramaForRefreshLocked(id), RetryAfter: max(1, int(math.Ceil(time.Until(call.retryAt).Seconds())))}
if call.err != nil {
result.Warning = "部分资料暂未更新,已保留原有信息,可稍后再次打开剧集重试"
}
return result
}
func (a *UIApp) refreshDramaMetadata(ctx context.Context, drama Drama) (dramaRefreshResult, error) {
if err := ctx.Err(); err != nil {
return dramaRefreshResult{}, err
}
now := time.Now()
a.mu.Lock()
if call := a.dramaRefreshes[drama.ID]; call != nil && (call.retryAt.IsZero() || now.Before(call.retryAt)) {
a.mu.Unlock()
select {
case <-ctx.Done():
return dramaRefreshResult{}, ctx.Err()
case <-call.done:
if errors.Is(call.err, context.Canceled) && ctx.Err() == nil {
return a.refreshDramaMetadata(ctx, drama)
}
a.mu.Lock()
result := a.dramaRefreshResultLocked(drama.ID, call)
a.mu.Unlock()
return result, nil
}
}
pending := 0
for id, call := range a.dramaRefreshes {
if call.retryAt.IsZero() {
pending++
} else if !now.Before(call.retryAt) {
delete(a.dramaRefreshes, id)
}
}
if pending >= 8 || len(a.dramaRefreshes) >= 256 {
a.mu.Unlock()
return dramaRefreshResult{}, errors.New("剧集资料更新请求过多")
}
if a.dramaRefreshes == nil {
a.dramaRefreshes = make(map[string]*dramaRefreshCall)
a.dramaRefreshSlots = make(chan struct{}, 1)
}
call := &dramaRefreshCall{done: make(chan struct{})}
a.dramaRefreshes[drama.ID] = call
slots := a.dramaRefreshSlots
a.mu.Unlock()
work, cancel := context.WithTimeout(context.WithValue(ctx, backgroundCatalogKey{}, true), 35*time.Second)
defer cancel()
var patch Drama
var err error
select {
case <-work.Done():
err = work.Err()
case slots <- struct{}{}:
patch, err = a.downloader.fetchDramaMetadata(work, drama)
<-slots
}
if patch.ID != "" && patch.ID != drama.ID {
patch = Drama{}
err = errors.New("详情返回了其他剧集")
}
changed := false
a.mu.Lock()
if ctx.Err() == nil && patch.ID == drama.ID {
for index, current := range a.dramas {
if current.ID != drama.ID {
continue
}
if err == nil {
patch.SortMetadata = completedDramaMetadata(current)
}
updated := mergeRefreshedDrama(patch, current, drama)
if !reflect.DeepEqual(updated, current) {
a.dramas[index] = updated
a.libraryDirty = true
a.libraryRevision++
changed = true
}
break
}
}
a.mu.Unlock()
if changed {
a.persistLibrary()
}
if err != nil && ctx.Err() == nil {
a.downloader.recordDiagnostic(diagnosticEvent{Event: "drama.metadata_failed", Source: dramaProvider(drama), DramaID: drama.ID, Message: a.redactError(err)})
}
a.mu.Lock()
call.err, call.retryAt = err, time.Now().Add(dramaRefreshRetryDelay)
if ctx.Err() != nil {
call.err = ctx.Err()
delete(a.dramaRefreshes, drama.ID)
}
result := a.dramaRefreshResultLocked(drama.ID, call)
close(call.done)
a.mu.Unlock()
return result, ctx.Err()
}
func mergeRefreshedDrama(patch, current, previous Drama) Drama {
updated := mergeDramaMetadata(patch, current)
if current.SortMetadata != nil && (previous.SortMetadata == nil || current.SortMetadata.CheckedAt.After(previous.SortMetadata.CheckedAt)) {
updated = mergeDramaMetadata(current, patch)
}
if current.Title != previous.Title || current.Name != previous.Name {
updated.Title, updated.Name = current.Title, current.Name
}
if bestDramaCover(current) != bestDramaCover(previous) {
updated.Cover, updated.CoverURL = current.Cover, current.CoverURL
}
normalizeDramaCover(&updated)
return updated
}
func completedDramaMetadata(drama Drama) *sortMetadataState {
state := sortMetadataState{Version: dramaSortMetadataVersion(drama), CheckedAt: time.Now(), CoverChecked: dramaProvider(drama) == sourceHongguo, VIPChecked: dramaProvider(drama) == sourceHuangdou}
if drama.SortMetadata != nil {
state.Pending = drama.SortMetadata.Pending
}
return &state
}