package app import ( "context" "crypto/sha256" "errors" "fmt" "net/http" "sort" "strconv" "strings" "time" ) var hongguoAppGenres = []struct { key string scene string name string }{ {key: "short_play", scene: "default", name: "真人剧"}, {key: "comic_series", scene: "comic_series", name: "漫剧"}, {key: "ai_series", scene: "ai_series", name: "AI剧"}, } type libraryMoreKey struct{} type libraryUpdateKey struct{} type libraryKnownKey struct{} func withKnownHongguoDramas(ctx context.Context, dramas []Drama) context.Context { known := make(map[string]bool, len(dramas)) for _, drama := range dramas { known[drama.ID] = true } ctx = context.WithValue(ctx, libraryRowsKey{}, append([]Drama(nil), dramas...)) return context.WithValue(ctx, libraryKnownKey{}, known) } func (downloader *Downloader) fetchHongguoAppCatalog(ctx context.Context) ([]Drama, error) { client := downloader.hongguoClient() client.catalogMu.Lock() defer client.catalogMu.Unlock() more, _ := ctx.Value(libraryMoreKey{}).(bool) update, _ := ctx.Value(libraryUpdateKey{}).(bool) known, _ := ctx.Value(libraryKnownKey{}).(map[string]bool) state := downloader.hongguoCatalogSnapshot() pageLimit := downloader.cfg.MaxPagesPerSort if pageLimit < 1 { pageLimit = defaultConfig().MaxPagesPerSort } if pageLimit > 200 { pageLimit = 200 } headLimit := min(pageLimit, 3) roundLimit := pageLimit if update { roundLimit += headLimit } type feedScan struct { cursor hongguoCatalogCursor tail hongguoCatalogCursor head bool done bool pages int } scans := make([]feedScan, len(hongguoAppGenres)) for index, genre := range hongguoAppGenres { cursor := state.Feeds[genre.key] if !more && cursor.Initialized { scans[index].head = true scans[index].tail = cursor } else { scans[index].cursor = cursor scans[index].done = cursor.Exhausted } } seen := map[string]int{} var dramas []Drama var failures []error for round := 0; round < roundLimit; round++ { active := false for index, genre := range hongguoAppGenres { scan := &scans[index] if scan.done || !scan.head && scan.pages >= pageLimit { continue } if err := ctx.Err(); err != nil { return dramas, err } active = true cursor := scan.cursor if time.Since(cursor.UpdatedAt) > 30*time.Minute { cursor.SessionID = "" } payload := map[string]any{ "req_scene": genre.scene, "offset": cursor.Offset, "limit": 18, "req_type": "only_content", "need_selector_panel": false, "client_req_type": 3, "session_id": cursor.SessionID, "filter_ids": "", "select_items": map[string]any{ "genre": []string{genre.key}, "sort": []string{"online_time"}, "gender": []string{}, "category_dim_theme": []string{}, "category_dim_role": []string{}, "category_dim_epoch": []string{}, "online_time": []string{}, "creation_status": []string{}, }, } if cursor.Offset > 0 { payload["client_req_type"] = 2 } var items []Drama var nextCursor hongguoCatalogCursor var pageErr error newItems := 0 for attempt := 0; attempt < 2; attempt++ { result, err := downloader.hongguoAppRequest(ctx, http.MethodPost, "/reading/distribution/category/landpage/v/", nil, payload) retryable := err == nil || cursor.SessionID != "" items = nil if err == nil { items, nextCursor, err = parseHongguoCatalogPage(result, cursor, genre.name) } newItems = 0 for _, drama := range items { if !known[drama.ID] { newItems++ } if position, exists := seen[drama.ID]; exists { dramas[position] = mergeDramaMetadata(drama, dramas[position]) } else { seen[drama.ID] = len(dramas) dramas = append(dramas, drama) } } pageErr = err if err == nil { break } reportLibraryProgress(ctx, sourceHongguo, items, nil, false) var backoff *requestBackoff if attempt > 0 || !retryable || errors.As(err, &backoff) || ctx.Err() != nil { break } payload["session_id"] = "" } if err := ctx.Err(); err != nil { return dramas, err } if pageErr != nil { failures = append(failures, fmt.Errorf("%s: %w", genre.name, pageErr)) scan.done = true continue } cursor = nextCursor scan.pages++ scan.cursor = cursor scan.done = cursor.Exhausted checkpoint := cursor if scan.head && newItems == 0 && !cursor.Exhausted { scan.done = true if scan.tail.Exhausted || scan.tail.Offset >= cursor.Offset { checkpoint = scan.tail } } if scan.head && (scan.done || scan.pages >= headLimit) { scan.done = true if update && !checkpoint.Exhausted { scan.cursor = checkpoint scan.head, scan.done, scan.pages = false, false, 0 } } client.mu.Lock() client.state.Feeds[genre.key] = checkpoint client.mu.Unlock() reportLibraryProgress(ctx, sourceHongguo, items, nil, false) } if !active { break } } sort.SliceStable(dramas, func(left, right int) bool { return dramas[left].DisplayTitle() < dramas[right].DisplayTitle() }) return dramas, errors.Join(failures...) } func parseHongguoCatalogPage(result map[string]any, cursor hongguoCatalogCursor, category string) ([]Drama, hongguoCatalogCursor, error) { data := nestedMap(result, "data") rows, valid := data["video_data"].([]any) if !valid { return nil, cursor, errors.New("App 分类数据格式异常") } items := make([]Drama, 0, len(rows)) ids := make([]string, 0, len(rows)) seen := map[string]bool{} for _, row := range rows { drama := hongguoDramaFromAny(row, category) if drama.ID != "" { items = append(items, drama) if !seen[drama.ID] { seen[drama.ID] = true ids = append(ids, drama.ID) } } } if len(rows) > 0 && len(items) == 0 { return items, cursor, errors.New("App 分类未返回可识别的剧集") } next, parseErr := strconv.Atoi(mapString(data, "next_offset")) hasMore, paginationOK := data["has_more"].(bool) if !paginationOK || hasMore && parseErr != nil { return items, cursor, errors.New("App 分页标记无效,已保留上次位置") } if parseErr != nil { next = cursor.Offset + len(rows) } lastID, signature := "", "" if len(items) > 0 { lastID = items[len(items)-1].ID sort.Strings(ids) signature = fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(ids, "\n")))) } if hasMore && (len(items) == 0 || next <= cursor.Offset || next > 1_000_000 || signature == cursor.PageSignature) { return items, cursor, errors.New("App 分页未前进,已保留上次位置") } cursor.Exhausted = !hasMore cursor.Initialized = true cursor.Offset = next cursor.SessionID = mapString(data, "session_id") cursor.LastID = lastID cursor.PageSignature = signature cursor.UpdatedAt = time.Now() return items, cursor, nil }