301 lines
7.7 KiB
Markdown
301 lines
7.7 KiB
Markdown
# Extract AllAnime Data Extraction Logic Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Extract data parsing and stream detection logic from `allanime_client.go` into a new file `allanime_extractor.go` to improve maintainability and separate concerns.
|
|
|
|
**Architecture:** Move non-network utility functions and helper structs related to parsing AllAnime responses and URL decoding/detection into a dedicated extractor file within the same package.
|
|
|
|
**Tech Stack:** Go (Standard Library)
|
|
|
|
---
|
|
|
|
### Task 1: Create allanime_extractor.go
|
|
|
|
**Files:**
|
|
- Create: `api/playback/allanime_extractor.go`
|
|
|
|
- [ ] **Step 1: Create the file with moved logic**
|
|
|
|
```go
|
|
package playback
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type sourceReference struct {
|
|
URL string
|
|
Name string
|
|
}
|
|
|
|
func (c *allAnimeClient) extractSourceURLsFromData(ctx context.Context, data map[string]any) []StreamSource {
|
|
episodeData, ok := data["episode"].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
sourceURLs, ok := episodeData["sourceUrls"].([]any)
|
|
if !ok || len(sourceURLs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
references := buildSourceReferences(sourceURLs)
|
|
if len(references) == 0 {
|
|
return nil
|
|
}
|
|
|
|
out := make([]StreamSource, 0, len(references))
|
|
for _, ref := range references {
|
|
target := strings.TrimSpace(ref.URL)
|
|
if target == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
|
sourceType := detectStreamType(target)
|
|
if sourceType == "unknown" {
|
|
sourceType = detectEmbedType(target)
|
|
}
|
|
|
|
out = append(out, buildStreamSource(target, sourceType, ref.Name))
|
|
continue
|
|
}
|
|
|
|
decoded := decodeSourceURL(target)
|
|
if decoded == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(decoded, "http://") || strings.HasPrefix(decoded, "https://") {
|
|
sourceType := detectStreamType(decoded)
|
|
if sourceType == "unknown" {
|
|
sourceType = detectEmbedType(decoded)
|
|
}
|
|
|
|
out = append(out, buildStreamSource(decoded, sourceType, ref.Name))
|
|
continue
|
|
}
|
|
|
|
if !strings.HasPrefix(decoded, "/") {
|
|
decoded = "/" + decoded
|
|
}
|
|
|
|
extracted, err := c.extractor.ExtractVideoLinks(ctx, decoded)
|
|
if err != nil {
|
|
log.Printf("source extraction failed for %s: %v", decoded, err)
|
|
continue
|
|
}
|
|
|
|
out = append(out, extracted...)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func buildStreamSource(url, sourceType, provider string) StreamSource {
|
|
return StreamSource{
|
|
URL: url,
|
|
Provider: provider,
|
|
Type: sourceType,
|
|
Referer: allAnimeReferer,
|
|
}
|
|
}
|
|
|
|
func buildSourceReferences(rawSourceURLs []any) []sourceReference {
|
|
priorityOrder := []string{"default", "yt-mp4", "s-mp4", "luf-mp4"}
|
|
prioritySet := map[string]struct{}{"default": {}, "yt-mp4": {}, "s-mp4": {}, "luf-mp4": {}}
|
|
|
|
prioritized := make(map[string]sourceReference)
|
|
fallback := make([]sourceReference, 0, len(rawSourceURLs))
|
|
seen := make(map[string]struct{})
|
|
|
|
for _, source := range rawSourceURLs {
|
|
item, ok := source.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
sourceURL, _ := item["sourceUrl"].(string)
|
|
sourceName, _ := item["sourceName"].(string)
|
|
sourceURL = strings.TrimSpace(sourceURL)
|
|
sourceName = strings.TrimSpace(sourceName)
|
|
if sourceURL == "" {
|
|
continue
|
|
}
|
|
|
|
if _, exists := seen[sourceURL]; exists {
|
|
continue
|
|
}
|
|
seen[sourceURL] = struct{}{}
|
|
|
|
ref := sourceReference{URL: sourceURL, Name: sourceName}
|
|
normalized := strings.ToLower(sourceName)
|
|
if _, prioritizedProvider := prioritySet[normalized]; prioritizedProvider {
|
|
if _, exists := prioritized[normalized]; !exists {
|
|
prioritized[normalized] = ref
|
|
}
|
|
continue
|
|
}
|
|
|
|
fallback = append(fallback, ref)
|
|
}
|
|
|
|
ordered := make([]sourceReference, 0, len(prioritized)+len(fallback))
|
|
for _, provider := range priorityOrder {
|
|
if ref, ok := prioritized[provider]; ok {
|
|
ordered = append(ordered, ref)
|
|
}
|
|
}
|
|
|
|
ordered = append(ordered, fallback...)
|
|
return ordered
|
|
}
|
|
|
|
func extractEpisodeData(data map[string]any) (map[string]any, error) {
|
|
episodeData, ok := data["episode"].(map[string]any)
|
|
if ok && episodeData != nil {
|
|
return episodeData, nil
|
|
}
|
|
|
|
toBeParsed, ok := data["tobeparsed"].(string)
|
|
if !ok || strings.TrimSpace(toBeParsed) == "" {
|
|
return nil, fmt.Errorf("episode not found")
|
|
}
|
|
|
|
decoded, err := decryptTobeparsed(toBeParsed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode episode payload: %w", err)
|
|
}
|
|
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal(decoded, &parsed); err != nil {
|
|
return nil, fmt.Errorf("parse decoded payload: %w", err)
|
|
}
|
|
|
|
episodeData, ok = parsed["episode"].(map[string]any)
|
|
if !ok || episodeData == nil {
|
|
return nil, fmt.Errorf("decoded payload missing episode")
|
|
}
|
|
|
|
return episodeData, nil
|
|
}
|
|
|
|
func decodeSourceURL(encoded string) string {
|
|
if encoded == "" {
|
|
return ""
|
|
}
|
|
|
|
encoded = strings.TrimPrefix(encoded, "--")
|
|
|
|
substitutions := map[string]string{
|
|
"79": "A", "7a": "B", "7b": "C", "7c": "D", "7d": "E",
|
|
"7e": "F", "7f": "G", "70": "H", "71": "I", "72": "J",
|
|
"73": "K", "74": "L", "75": "M", "76": "N", "77": "O",
|
|
"68": "P", "69": "Q", "6a": "R", "6b": "S", "6c": "T",
|
|
"6d": "U", "6e": "V", "6f": "W", "60": "X", "61": "Y",
|
|
"62": "Z",
|
|
"59": "a", "5a": "b", "5b": "c", "5c": "d", "5d": "e",
|
|
"5e": "f", "5f": "g", "50": "h", "51": "i", "52": "j",
|
|
"53": "k", "54": "l", "55": "m", "56": "n", "57": "o",
|
|
"48": "p", "49": "q", "4a": "r", "4b": "s", "4c": "t",
|
|
"4d": "u", "4e": "v", "4f": "w", "40": "x", "41": "y",
|
|
"42": "z",
|
|
"08": "0", "09": "1", "0a": "2", "0b": "3", "0c": "4",
|
|
"0d": "5", "0e": "6", "0f": "7", "00": "8", "01": "9",
|
|
"15": "-", "16": ".", "67": "_", "46": "~", "02": ":",
|
|
"17": "/", "07": "?", "1b": "#", "63": "[", "65": "]",
|
|
"78": "@", "19": "!", "1c": "$", "1e": "&", "10": "(",
|
|
"11": ")", "12": "*", "13": "+", "14": ",", "03": ";",
|
|
"05": "=", "1d": "%",
|
|
}
|
|
|
|
var result strings.Builder
|
|
for idx := 0; idx < len(encoded); {
|
|
if idx+2 <= len(encoded) {
|
|
pair := encoded[idx : idx+2]
|
|
if sub, ok := substitutions[pair]; ok {
|
|
result.WriteString(sub)
|
|
idx += 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
result.WriteByte(encoded[idx])
|
|
idx++
|
|
}
|
|
|
|
decoded := result.String()
|
|
if strings.Contains(decoded, "/clock") && !strings.Contains(decoded, "/clock.json") {
|
|
decoded = strings.Replace(decoded, "/clock", "/clock.json", 1)
|
|
}
|
|
|
|
return decoded
|
|
}
|
|
|
|
func detectStreamType(sourceURL string) string {
|
|
lower := strings.ToLower(sourceURL)
|
|
if strings.Contains(lower, ".m3u8") || strings.Contains(lower, "master.m3u8") {
|
|
return "m3u8"
|
|
}
|
|
|
|
if strings.Contains(lower, ".mp4") {
|
|
return "mp4"
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
func detectEmbedType(rawURL string) string {
|
|
lower := strings.ToLower(rawURL)
|
|
embedHosts := []string{"streamwish", "streamsb", "mp4upload", "ok.ru", "gogoplay", "streamlare"}
|
|
for _, host := range embedHosts {
|
|
if strings.Contains(lower, host) {
|
|
return "embed"
|
|
}
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
func getMapKeys(m map[string]any) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add api/playback/allanime_extractor.go
|
|
git commit -m "feat: add allanime_extractor.go with moved logic"
|
|
```
|
|
|
|
### Task 2: Update allanime_client.go
|
|
|
|
**Files:**
|
|
- Modify: `api/playback/allanime_client.go`
|
|
|
|
- [ ] **Step 1: Remove moved logic from allanime_client.go**
|
|
Remove: `extractSourceURLsFromData`, `buildStreamSource`, `sourceReference` struct, `buildSourceReferences`, `extractEpisodeData`, `decodeSourceURL`, `detectStreamType`, `detectEmbedType`, `getMapKeys`.
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `go build ./api/playback/...`
|
|
Expected: PASS
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add api/playback/allanime_client.go
|
|
git commit -m "refactor: remove moved logic from allanime_client.go"
|
|
```
|