From f99b30bf43f202da7db5fddc6b75be4a8d953153 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 13 Jun 2026 22:24:06 +0200 Subject: [PATCH] extract: add stream source resolution --- integrations/playback/allanime/sources.go | 297 ++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 integrations/playback/allanime/sources.go diff --git a/integrations/playback/allanime/sources.go b/integrations/playback/allanime/sources.go new file mode 100644 index 0000000..d93ee8d --- /dev/null +++ b/integrations/playback/allanime/sources.go @@ -0,0 +1,297 @@ +package allanime + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" +) + +const episodeQueryHash = "d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec" + +type sourceReference struct { + URL string + Name string +} + +func (c *AllAnimeProvider) GetEpisodeSources(ctx context.Context, showID string, episode string, mode string) ([]StreamSource, error) { + episodeQuery := `query($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) { + episode(showId: $showId, translationType: $translationType, episodeString: $episodeString) { + sourceUrls + } + }` + + result, err := c.graphqlRequestWithHash(ctx, showID, episode, mode) + if err == nil { + sources := c.extractSourceURLsFromData(ctx, result) + if len(sources) > 0 { + return sources, nil + } + } + + result, err = c.graphqlRequest(ctx, episodeQuery, map[string]any{ + "showId": showID, + "translationType": mode, + "episodeString": episode, + }) + if err != nil { + return nil, err + } + + data, ok := result["data"].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid source response") + } + + rawSourceURLs, ok := data["episode"].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid episode response") + } + + sourceURLs, ok := rawSourceURLs["sourceUrls"].([]any) + if !ok || len(sourceURLs) == 0 { + return nil, fmt.Errorf("no source urls") + } + + references := buildSourceReferences(sourceURLs) + if len(references) == 0 { + return nil, fmt.Errorf("no source references") + } + + out := c.resolveSourceReferences(ctx, references) + + if len(out) == 0 { + return nil, fmt.Errorf("no playable sources extracted") + } + + return out, nil +} + +func (c *AllAnimeProvider) 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 + } + + return c.resolveSourceReferences(ctx, references) +} + +func (c *AllAnimeProvider) resolveSourceReferences(ctx context.Context, references []sourceReference) []StreamSource { + out := make([]StreamSource, 0, len(references)) + for _, ref := range references { + if source, ok := resolveDirectSource(ref); ok { + out = append(out, source) + continue + } + + extracted := c.resolveExtractedSources(ctx, ref) + out = append(out, extracted...) + } + + return out +} + +func resolveDirectSource(ref sourceReference) (StreamSource, bool) { + target := strings.TrimSpace(ref.URL) + if target == "" { + return StreamSource{}, false + } + + if isHTTPURL(target) { + return buildStreamSource(target, detectSourceType(target), ref.Name), true + } + + decoded := decodeSourceURL(target) + if !isHTTPURL(decoded) { + return StreamSource{}, false + } + + return buildStreamSource(decoded, detectSourceType(decoded), ref.Name), true +} + +func (c *AllAnimeProvider) resolveExtractedSources(ctx context.Context, ref sourceReference) []StreamSource { + decoded := decodeSourceURL(strings.TrimSpace(ref.URL)) + if decoded == "" { + return nil + } + + if !strings.HasPrefix(decoded, "/") { + decoded = "/" + decoded + } + + extracted, err := c.extractor.ExtractVideoLinks(ctx, decoded) + if err != nil { + return nil + } + + return extracted +} + +func detectSourceType(sourceURL string) string { + sourceType := detectStreamType(sourceURL) + if sourceType != "unknown" { + return sourceType + } + + return detectEmbedType(sourceURL) +} + +func isHTTPURL(value string) bool { + return strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") +} + +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 (c *AllAnimeProvider) graphqlRequestWithHash(ctx context.Context, showID, episode, mode string) (map[string]any, error) { + req, err := newEpisodeHashRequest(ctx, showID, episode, mode) + if err != nil { + return nil, fmt.Errorf("create GET request: %w", err) + } + + req.Header.Set("User-Agent", defaultUserAgent) + req.Header.Set("Accept", "*/*") + req.Header.Set("Accept-Language", "en-US,en;q=0.5") + req.Header.Set("Accept-Encoding", "identity") + req.Header.Set("Referer", allAnimeReferer) + req.Header.Set("Origin", allAnimeOrigin) + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Site", "cross-site") + + statusCode, respBody, err := executeAndReadResponse(c.utlsClient, req, "execute GET request", "read response") + if err != nil { + return nil, err + } + + if statusCode != http.StatusOK { + return nil, fmt.Errorf("GET status %d: %s", statusCode, string(respBody)) + } + + parsed, err := parseGraphQLResponse(respBody, "decode response") + if err != nil { + return nil, err + } + + data, ok := parsed["data"].(map[string]any) + if !ok { + return nil, fmt.Errorf("no data in response") + } + + decrypted, err := responseFromTobeparsed(data) + if err != nil { + return nil, err + } + if decrypted != nil { + return decrypted, nil + } + + if hasEpisodeSourceURLs(data) { + return parsed, nil + } + + return nil, fmt.Errorf("no usable data in response") +} + +func newEpisodeHashRequest(ctx context.Context, showID, episode, mode string) (*http.Request, error) { + varsJSON := fmt.Sprintf(`{"showId":"%s","translationType":"%s","episodeString":"%s"}`, showID, strings.ToLower(mode), episode) + extJSON := fmt.Sprintf(`{"persistedQuery":{"version":1,"sha256Hash":"%s"}}`, episodeQueryHash) + + params := url.Values{} + params.Set("variables", varsJSON) + params.Set("extensions", extJSON) + + return http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api?%s", allAnimeBaseURL, params.Encode()), nil) +} + +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" +}