192 lines
5.5 KiB
Go
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
|
|
}
|