From e3d82389e4a3ebf7d237aca1ea62fe77d7bfb35a Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 13 Jun 2026 22:24:10 +0200 Subject: [PATCH] trim: keep only entrypoint in client.go --- integrations/playback/allanime/client.go | 766 ----------------------- 1 file changed, 766 deletions(-) diff --git a/integrations/playback/allanime/client.go b/integrations/playback/allanime/client.go index 62ffe1b..3b8b0b0 100644 --- a/integrations/playback/allanime/client.go +++ b/integrations/playback/allanime/client.go @@ -1,22 +1,14 @@ -// Package allanime provides an integration with the AllAnime API for episode playback. package allanime import ( "bytes" "context" - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "io" "mal/internal/domain" - "mal/pkg" netutil "mal/pkg/net" "net/http" - "net/url" - "strconv" "strings" "time" ) @@ -28,22 +20,6 @@ const ( defaultUserAgent = netutil.Firefox121 ) -var ( - aesKeys = []string{"Xot36i3lK3:v1", "SimtVuagFbGR2K7P"} -) - -type searchResult struct { - ID string - MalID string - Name string -} - -type AvailableEpisodes struct { - Sub []string - Dub []string - Raw []string -} - type AllAnimeProvider struct { httpClient *http.Client utlsClient *http.Client @@ -67,103 +43,17 @@ func (c *AllAnimeProvider) Name() string { return "AllAnime" } -const searchQuery = `query( - $search: SearchInput - $translationType: VaildTranslationTypeEnumType - $limit: Int = 40 - $page: Int = 1 - $countryOrigin: VaildCountryOriginEnumType = ALL -) { - shows( - search: $search - limit: $limit - page: $page - translationType: $translationType - countryOrigin: $countryOrigin - ) { - edges { - _id - malId - name - } - } -}` - -func (c *AllAnimeProvider) Search(ctx context.Context, query string, mode string) ([]searchResult, error) { - type searchData struct { - Shows struct { - Edges []struct { - ID string `json:"_id"` - MalID string `json:"malId"` - Name string `json:"name"` - } `json:"edges"` - } `json:"shows"` - } - - type searchInput struct { - AllowAdult bool `json:"allowAdult"` - AllowUnknown bool `json:"allowUnknown"` - Query string `json:"query"` - } - - type searchVariables struct { - Search searchInput `json:"search"` - TranslationType string `json:"translationType"` - } - - vars := searchVariables{ - Search: searchInput{ - AllowAdult: false, - AllowUnknown: false, - Query: query, - }, - TranslationType: mode, - } - - data, err := graphql.Post[searchData](ctx, c.httpClient, allAnimeBaseURL+"/api", searchQuery, vars, graphql.PostOptions{ - Headers: map[string]string{ - "Referer": allAnimeReferer, - "User-Agent": defaultUserAgent, - }, - BodyMax: netutil.MiB2, - }) - if err != nil { - return nil, err - } - - out := make([]searchResult, 0, len(data.Shows.Edges)) - for _, edge := range data.Shows.Edges { - id := edge.ID - malID := edge.MalID - name := edge.Name - if unquoted, err := strconv.Unquote("\"" + name + "\""); err == nil { - name = unquoted - } - name = strings.TrimSpace(name) - - if id == "" { - continue - } - - out = append(out, searchResult{ID: id, MalID: malID, Name: name}) - } - - return out, nil -} - func (c *AllAnimeProvider) GetStreams(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string) (*domain.StreamResult, error) { showID := c.resolveShowIDWithFallback(ctx, animeID, titleCandidates, mode) if showID == "" { return nil, fmt.Errorf("allanime: show not found for malID %d", animeID) } - // 2. Get sources sources, err := c.GetEpisodeSources(ctx, showID, episode, mode) if err != nil || len(sources) == 0 { return nil, fmt.Errorf("allanime: no sources for show %s", showID) } - // 3. Return the first usable source primary := sources[0] result := &domain.StreamResult{ @@ -181,95 +71,6 @@ func (c *AllAnimeProvider) GetStreams(ctx context.Context, animeID int, titleCan return result, nil } -func (c *AllAnimeProvider) resolveShowIDWithFallback(ctx context.Context, animeID int, titleCandidates []string, mode string) string { - targetMalIDStr := strconv.Itoa(animeID) - firstAvailableShowID := "" - - for _, title := range titleCandidates { - searchResults, err := c.Search(ctx, title, mode) - if err != nil || len(searchResults) == 0 { - continue - } - if showID := exactMatchShowID(searchResults, targetMalIDStr); showID != "" { - return showID - } - if firstAvailableShowID == "" { - firstAvailableShowID = searchResults[0].ID - } - } - - return firstAvailableShowID -} - -func exactMatchShowID(searchResults []searchResult, targetMalID string) string { - for _, res := range searchResults { - if res.MalID == targetMalID { - return res.ID - } - } - - return "" -} - -func (c *AllAnimeProvider) GetEpisodeAvailability(ctx context.Context, animeID int, titleCandidates []string) (domain.EpisodeAvailability, error) { - showID, err := c.ResolveEpisodeProviderID(ctx, animeID, titleCandidates) - if err != nil { - return domain.EpisodeAvailability{}, err - } - return c.GetEpisodeAvailabilityByProviderID(ctx, showID) -} - -func (c *AllAnimeProvider) ResolveEpisodeProviderID(ctx context.Context, animeID int, titleCandidates []string) (string, error) { - for _, mode := range []string{"sub", "dub"} { - showID, err := c.resolveShowIDStrict(ctx, animeID, titleCandidates, mode) - if err == nil { - return showID, nil - } - } - return "", fmt.Errorf("allanime: no exact mal id match for %d", animeID) -} - -func (c *AllAnimeProvider) GetEpisodeAvailabilityByProviderID(ctx context.Context, showID string) (domain.EpisodeAvailability, error) { - available, err := c.GetAvailableEpisodes(ctx, showID) - if err != nil { - return domain.EpisodeAvailability{}, err - } - - sub := parseEpisodeNumbers(append(available.Sub, available.Raw...)) - dub := parseEpisodeNumbers(available.Dub) - return domain.EpisodeAvailability{Sub: sub, Dub: dub}, nil -} - -func (c *AllAnimeProvider) resolveShowIDStrict(ctx context.Context, animeID int, titleCandidates []string, mode string) (string, error) { - targetMalIDStr := strconv.Itoa(animeID) - for _, title := range titleCandidates { - searchResults, err := c.Search(ctx, title, mode) - if err != nil { - continue - } - for _, res := range searchResults { - if res.MalID == targetMalIDStr { - return res.ID, nil - } - } - } - return "", fmt.Errorf("allanime: no exact mal id match for %d in %s search", animeID, mode) -} - -func parseEpisodeNumbers(raw []string) []int { - seen := make(map[int]bool, len(raw)) - out := make([]int, 0, len(raw)) - for _, value := range raw { - n, err := strconv.Atoi(strings.TrimSpace(value)) - if err != nil || n <= 0 || seen[n] { - continue - } - seen[n] = true - out = append(out, n) - } - return out -} - func (c *AllAnimeProvider) graphqlRequest(ctx context.Context, query string, variables map[string]any) (map[string]any, error) { if mode, ok := variables["translationType"].(string); ok { variables["translationType"] = strings.ToLower(mode) @@ -315,322 +116,6 @@ func (c *AllAnimeProvider) graphqlRequest(ctx context.Context, query string, var return parsed, nil } -const episodeQueryHash = "d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec" - -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 parseGraphQLResponse(respBody []byte, decodeErrPrefix string) (map[string]any, error) { - var parsed map[string]any - if err := json.Unmarshal(respBody, &parsed); err != nil { - return nil, fmt.Errorf("%s: %w", decodeErrPrefix, err) - } - - if errs, ok := parsed["errors"].([]any); ok && len(errs) > 0 { - return nil, fmt.Errorf("graphql error: %v", errs[0]) - } - - return parsed, nil -} - -func responseFromTobeparsed(data map[string]any) (map[string]any, error) { - toBeParsed := firstNonEmptyString( - nestedString(data, "tobeparsed"), - nestedString(data, "episode", "tobeparsed"), - ) - if toBeParsed == "" { - return nil, nil - } - - decrypted, err := decryptTobeparsed(toBeParsed) - if err != nil { - return nil, fmt.Errorf("decrypt tobeparsed: %w", err) - } - - parsed, err := parseGraphQLResponse(decrypted, "unmarshal decrypted") - if err != nil { - return nil, err - } - - sourceURLs := firstNonEmptySlice( - nestedSlice(parsed, "sourceUrls"), - nestedSlice(parsed, "episode", "sourceUrls"), - ) - if len(sourceURLs) == 0 { - return nil, nil - } - - return map[string]any{ - "episode": map[string]any{ - "sourceUrls": sourceURLs, - }, - }, nil -} - -func hasEpisodeSourceURLs(data map[string]any) bool { - return len(nestedSlice(data, "episode", "sourceUrls")) > 0 -} - -func firstNonEmptyString(values ...string) string { - for _, value := range values { - if value != "" { - return value - } - } - - return "" -} - -func firstNonEmptySlice(values ...[]any) []any { - for _, value := range values { - if len(value) > 0 { - return value - } - } - - return nil -} - -func nestedString(data map[string]any, path ...string) string { - value, ok := nestedValue(data, path...) - if !ok { - return "" - } - - str, ok := value.(string) - if !ok { - return "" - } - - return str -} - -func nestedSlice(data map[string]any, path ...string) []any { - value, ok := nestedValue(data, path...) - if !ok { - return nil - } - - slice, ok := value.([]any) - if !ok { - return nil - } - - return slice -} - -func nestedValue(data map[string]any, path ...string) (any, bool) { - var current any = data - for _, key := range path { - currentMap, ok := current.(map[string]any) - if !ok { - return nil, false - } - - current, ok = currentMap[key] - if !ok { - return nil, false - } - } - - return current, true -} - -// GetEpisodeSources fetches stream URLs for a given show, episode, and mode (dub/sub). -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 executeAndReadResponse(client *http.Client, req *http.Request, executeErrPrefix string, readErrPrefix string) (int, []byte, error) { resp, err := client.Do(req) if err != nil { @@ -645,254 +130,3 @@ func executeAndReadResponse(client *http.Client, req *http.Request, executeErrPr return resp.StatusCode, body, nil } - -func buildStreamSource(url, sourceType, provider string) StreamSource { - return StreamSource{ - URL: url, - Provider: provider, - Type: sourceType, - Referer: allAnimeReferer, - } -} - -type sourceReference struct { - URL string - Name string -} - -// buildSourceReferences orders source URLs by provider priority, deduplicating entries. -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) - // separate prioritized providers from fallback - if _, prioritizedProvider := prioritySet[normalized]; prioritizedProvider { - if _, exists := prioritized[normalized]; !exists { - prioritized[normalized] = ref - } - continue - } - - fallback = append(fallback, ref) - } - - // output: prioritized in order, then fallback - 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 decryptTobeparsed(encoded string) ([]byte, error) { - raw, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return nil, fmt.Errorf("base64 decode failed: %w", err) - } - - if len(raw) < 29 { - return nil, fmt.Errorf("encrypted payload too short") - } - - version := raw[0] - iv := raw[1:13] - cipherText := raw[13 : len(raw)-16] - - for _, keyStr := range aesKeys { - key := sha256.Sum256([]byte(keyStr)) - - block, err := aes.NewCipher(key[:]) - if err != nil { - continue - } - - if version == 1 { - plainText := tryDecryptCTR(block, iv, cipherText) - if json.Valid(plainText) { - return plainText, nil - } - } - - gcm, err := cipher.NewGCM(block) - if err == nil { - tag := raw[len(raw)-16:] - combined := append(append([]byte{}, cipherText...), tag...) - plainText, openErr := gcm.Open(nil, iv, combined, nil) - if openErr == nil && json.Valid(plainText) { - return plainText, nil - } - } - } - - return nil, fmt.Errorf("decryption failed") -} - -func tryDecryptCTR(block cipher.Block, iv []byte, cipherText []byte) []byte { - ctrIV := append([]byte{}, iv...) - ctrIV = append(ctrIV, 0x00, 0x00, 0x00, 0x02) - ctr := cipher.NewCTR(block, ctrIV) - plainText := make([]byte, len(cipherText)) - ctr.XORKeyStream(plainText, cipherText) - return plainText -} - -func stringSliceFromAny(value any) []string { - items, ok := value.([]any) - if !ok { - return nil - } - - values := make([]string, 0, len(items)) - for _, item := range items { - str, ok := item.(string) - if !ok { - continue - } - - values = append(values, str) - } - - return values -} - -// GetAvailableEpisodes returns the count of sub/dub/raw episodes available for a show. -func (c *AllAnimeProvider) GetAvailableEpisodes(ctx context.Context, showID string) (AvailableEpisodes, error) { - graphqlQuery := `query($showId: String!) { - show(_id: $showId) { - availableEpisodesDetail - lastEpisodeInfo - } - }` - - result, err := c.graphqlRequest(ctx, graphqlQuery, map[string]any{"showId": showID}) - if err != nil { - return AvailableEpisodes{}, err - } - - data, ok := result["data"].(map[string]any) - if !ok { - return AvailableEpisodes{}, fmt.Errorf("invalid response") - } - - show, ok := data["show"].(map[string]any) - if !ok || show == nil { - return AvailableEpisodes{}, fmt.Errorf("show not found") - } - - detail, ok := show["availableEpisodesDetail"].(map[string]any) - if !ok { - return AvailableEpisodes{}, fmt.Errorf("invalid detail") - } - - return AvailableEpisodes{ - Sub: stringSliceFromAny(detail["sub"]), - Dub: stringSliceFromAny(detail["dub"]), - Raw: stringSliceFromAny(detail["raw"]), - }, 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" -}