Skip to content

refactor: update m3u8 source fetching logic#164

Closed
fwy13 wants to merge 9 commits into
anime-vsub:mainfrom
fwy13:main
Closed

refactor: update m3u8 source fetching logic#164
fwy13 wants to merge 9 commits into
anime-vsub:mainfrom
fwy13:main

Conversation

@fwy13

@fwy13 fwy13 commented Apr 10, 2026

Copy link
Copy Markdown

Changes:

  • src/apis/runs/ajax/player-link.ts: Use a POST request to fetch /ajax/player from the server, then decrypt it using the new decryptM3u8 function to retrieve the original source.
  • src/constants.ts: Add the cdn_google variable to store the Google Storage CDN address.
  • src/logic/decrypt-hls-animevsub.ts: Replace the old WASM decryption with the new AES-GCM decryption method.

Added:

  • src/logic/get-source-m3u8.ts: Add the getSourceM3u8 function to extract the source, IV, and salt via the iframe player.

fwy13 and others added 8 commits September 11, 2025 16:18
If I write the original data fetched from fetchFile it won't read the ts segment to combine into mp4, so I split the image and ts segment. Then write the ts segment and it works.
Add IEND_IMAGE constant for MP4 conversion.
Moved the IEND_IMAGE constant to the end of the file for better organization.
Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com>
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@codesandbox

codesandbox Bot commented Apr 10, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Refactor m3u8 source fetching with iframe extraction and AES-GCM decryption

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Refactor m3u8 source fetching to use iframe-based extraction
• Replace WASM decryption with native AES-GCM Web Crypto API
• Add new getSourceM3u8 function for iframe player integration
• Switch from direct fetch to POST request via post helper
• Add Google Storage CDN constant for URL resolution
Diagram
flowchart LR
  A["PlayerLink Request"] --> B["POST /ajax/player"]
  B --> C["Check playTech"]
  C -->|iframe| D["getSourceM3u8"]
  D --> E["Extract id & token from HTML"]
  E --> F["Fetch m3u8 from Google CDN"]
  F --> G["Check if encrypted"]
  G -->|encrypted| H["decryptM3u8 with AES-GCM"]
  G -->|not encrypted| I["Use as-is"]
  H --> J["Return config with m3u8"]
  I --> J
Loading

Grey Divider

File Changes

1. src/apis/runs/ajax/player-link.ts ✨ Enhancement +42/-53

Switch to iframe-based m3u8 fetching with POST requests

• Switched from direct fetch to post helper function for /ajax/player endpoint
• Added support for iframe playTech type in PlayerLinkReturn interface
• Implemented iframe-based m3u8 extraction using new getSourceM3u8 function
• Replaced old decryption initialization with direct decryptM3u8 call passing headers
• Simplified link processing logic to handle iframe sources with base64 encoding
• Removed addProtocolUrl helper and C_URL constant usage

src/apis/runs/ajax/player-link.ts


2. src/constants.ts ⚙️ Configuration changes +2/-0

Add Google Storage CDN constant

• Added cdn_google constant storing Google Storage CDN URL
• Used for resolving m3u8 segment URLs in decryption logic

src/constants.ts


3. src/logic/decrypt-hls-animevsub.ts 🐞 Bug fix +150/-1

Replace WASM decryption with Web Crypto AES-GCM

• Replaced WASM-based decryption with native Web Crypto API implementation
• Added base64ToUint8Array helper for base64 decoding with padding
• Implemented decrypt function using HMAC-SHA256 and AES-GCM algorithms
• Refactored decryptM3u8 to parse encrypted tokens from m3u8 playlist
• Added header extraction for encryption metadata (edge_tag, proxyDigest, cacheNode, requestTrace)
• Implemented URL resolution with Google CDN and parameter injection

src/logic/decrypt-hls-animevsub.ts


View more (1)
4. src/logic/get-source-m3u8.ts ✨ Enhancement +31/-0

Add iframe m3u8 extraction function

• New file implementing iframe-based m3u8 source extraction
• Fetches HTML from iframe link and extracts id and avsToken via regex
• Constructs Google CDN playlist URL with extracted parameters
• Returns m3u8 content and response headers for decryption

src/logic/get-source-m3u8.ts


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (3) ☼ Reliability (1)

Grey Divider


Action required

1. Wrong getSourceM3u8 argument 🐞
Description
In PlayerLink, getSourceM3u8 is called with config.link, but the codebase models link as an
array of {file,label,...} objects, not a URL string, so the request URL becomes "[object Object]"
and iframe playback breaks. This is masked by JSON.parse(data) returning any, so TypeScript
won’t protect against it.
Code

src/apis/runs/ajax/player-link.ts[R67-69]

+    if (config.playTech === "iframe") {
+      const { m3u8: encryptedM3u8, headers } = await getSourceM3u8(config.link)

-            item.label = "HD"
-            item.preload = "auto"
-            item.type = "hls"
-          }
Evidence
getSourceM3u8 explicitly requires a link: string and interpolates it into the GET URL; passing a
non-string (like the link array shape used elsewhere) will stringify to "[object Object]". Both
the local PlayerLinkReturn and shared ConfigPlayer interface define link as an array of link
objects, supporting that config.link is expected to be an array in normal responses.

src/apis/runs/ajax/player-link.ts[10-36]
src/apis/runs/ajax/player-link.ts[67-83]
src/logic/get-source-m3u8.ts[3-9]
src/pages/phim/_season.interface.ts[30-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PlayerLink` calls `getSourceM3u8(config.link)` but `getSourceM3u8` expects a URL string.

### Issue Context
In this codebase, `link` is modeled as an array of link objects (`{ file, label, ... }[]`). Passing that array/object into a string template will yield `"[object Object]"`, producing an invalid GET URL.

### Fix Focus Areas
- src/apis/runs/ajax/player-link.ts[67-69]
- src/logic/get-source-m3u8.ts[3-9]
- src/pages/phim/_season.interface.ts[30-54]

### Suggested fix
Decide the actual server response shape for `playTech === "iframe"` and:
- If `config.link` is an array: pass the iframe URL as `config.link[0].file` (or whichever element contains the iframe/player URL).
- If `config.link` is actually a string only in iframe mode: update typings accordingly (e.g., a discriminated union for `playTech`) and ensure downstream code always receives the normalized array after you rewrite `config.link`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Blank m3u8 returned 🐞
Description
In iframe mode, finalM3u8 is initialized to an empty string and only set when _c= is detected;
otherwise the returned HLS data URI contains an empty playlist. This causes playback/download to
fail whenever the fetched playlist is already unencrypted or uses a different encryption marker.
Code

src/apis/runs/ajax/player-link.ts[R70-83]

+      const isEncrypted = encryptedM3u8.match(/[?&]_c=[0-9]+/)
+      let finalM3u8 = ""

-          switch (
-            (item.label as typeof item.label | undefined)?.toUpperCase() as
-              | Uppercase<Exclude<typeof item.label, undefined>>
-              | undefined
-          ) {
-            case "HD":
-              if (item.preload) item.label = "FHD|HD"
-              break
-            case undefined:
-              item.label = "HD"
-              break
-          }
-          item.qualityCode = getQualityByLabel(item.label)
-          item.type ??= "mp4"
-        })
-      )
+      if (isEncrypted) {
+        finalM3u8 = await decryptM3u8(encryptedM3u8, headers)
+      }
+      config.link = [
+        {
+          file: `data:application/vnd.apple.mpegurl;base64,${btoa(unescape(encodeURIComponent(finalM3u8)))}`,
+          label: "FHD|HD",
+          preload: "auto",
+          type: "hls"
+        }
+      ]
Evidence
The code always builds the returned data:application/vnd.apple.mpegurl;base64,... from
finalM3u8, but finalM3u8 remains "" when isEncrypted is falsy. There is no fallback to use
the fetched playlist text (encryptedM3u8) in the non-encrypted case.

src/apis/runs/ajax/player-link.ts[70-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When the fetched playlist is not encrypted (or not matched by the `_c=` regex), `finalM3u8` remains empty but is still used to create the `data:` URL.

### Issue Context
This makes iframe playback return an empty playlist.

### Fix Focus Areas
- src/apis/runs/ajax/player-link.ts[70-83]

### Suggested fix
Set `finalM3u8` to the fetched playlist by default, and only replace it with decrypted content when needed:
- `let finalM3u8 = encryptedM3u8`
- `if (isEncrypted) finalM3u8 = await decryptM3u8(encryptedM3u8, headers)`
Also consider renaming `encryptedM3u8` to `rawM3u8` to avoid confusion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. cdn_google not imported 🐞
Description
cdn_google is referenced in decrypt-hls-animevsub.ts and get-source-m3u8.ts but is never
imported, causing a runtime ReferenceError (and typically a TS compile error). This breaks
playlist URL rewriting and playlist fetching.
Code

src/logic/decrypt-hls-animevsub.ts[R130-134]

+    if (finalUrl.startsWith("/")) {
+      finalUrl = cdn_google + finalUrl
+    } else if (!finalUrl.startsWith("http")) {
+      finalUrl = cdn_google + "/" + finalUrl
+    }
Evidence
Both modules directly use cdn_google, but neither contains an import for it. The constant is
defined/exported in src/constants.ts, so it must be imported to be in scope.

src/logic/decrypt-hls-animevsub.ts[123-134]
src/logic/get-source-m3u8.ts[1-24]
src/constants.ts[50-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`cdn_google` is used without being imported in two files, making it undefined at runtime.

### Issue Context
`cdn_google` is exported from `src/constants.ts`.

### Fix Focus Areas
- src/logic/decrypt-hls-animevsub.ts[123-134]
- src/logic/get-source-m3u8.ts[1-24]
- src/constants.ts[50-64]

### Suggested fix
Add:
- `import { cdn_google } from "src/constants"`
into both `decrypt-hls-animevsub.ts` and `get-source-m3u8.ts`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Null id/token request 🐞
Description
getSourceM3u8 continues even when it fails to extract id or avsToken, producing a request to
/playlist/null/playlist.m3u8?token=null. This will fail downstream with less actionable errors
than a clear parse failure.
Code

src/logic/get-source-m3u8.ts[R12-24]

+  const idRegex = /const\s+id\s*=\s*["']([^"']+)["']/i
+  const tokenRegex = /const\s+avsToken\s*=\s*["']([^"']+)["']/i
+
+  const idMatch = html.match(idRegex)
+  const tokenMatch = html.match(tokenRegex)
+
+  const id = idMatch ? idMatch[1] : null
+  const avsToken = tokenMatch ? tokenMatch[1] : null
+
+  const m3u8Response = await get({
+    url: `${cdn_google}/playlist/${id}/playlist.m3u8?token=${avsToken}#animevsub-vsubo`,
+    responseType: "text"
+  })
Evidence
The regex matches are optional, but the resulting id/avsToken values are used unconditionally to
build the playlist URL. There is no guard/throw between parsing and the GET request.

src/logic/get-source-m3u8.ts[12-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
If HTML parsing fails, `id` and/or `avsToken` becomes `null` but the code still builds a playlist URL using those values.

### Issue Context
This generates invalid URLs and hides the true root cause (parsing failure).

### Fix Focus Areas
- src/logic/get-source-m3u8.ts[12-24]

### Suggested fix
After extracting matches:
- If `!id` or `!avsToken`, throw a descriptive error (include a small snippet or markers to help debugging).
- Use `encodeURIComponent(avsToken)` when constructing the query string to avoid malformed URLs if the token contains special characters.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@codacy-production

codacy-production Bot commented Apr 10, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 21 complexity · 0 duplication

Metric Results
Complexity 21
Duplication 0

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

Comment on lines +67 to 69
if (config.playTech === "iframe") {
const { m3u8: encryptedM3u8, headers } = await getSourceM3u8(config.link)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Wrong getsourcem3u8 argument 🐞 Bug ≡ Correctness

In PlayerLink, getSourceM3u8 is called with config.link, but the codebase models link as an
array of {file,label,...} objects, not a URL string, so the request URL becomes "[object Object]"
and iframe playback breaks. This is masked by JSON.parse(data) returning any, so TypeScript
won’t protect against it.
Agent Prompt
### Issue description
`PlayerLink` calls `getSourceM3u8(config.link)` but `getSourceM3u8` expects a URL string.

### Issue Context
In this codebase, `link` is modeled as an array of link objects (`{ file, label, ... }[]`). Passing that array/object into a string template will yield `"[object Object]"`, producing an invalid GET URL.

### Fix Focus Areas
- src/apis/runs/ajax/player-link.ts[67-69]
- src/logic/get-source-m3u8.ts[3-9]
- src/pages/phim/_season.interface.ts[30-54]

### Suggested fix
Decide the actual server response shape for `playTech === "iframe"` and:
- If `config.link` is an array: pass the iframe URL as `config.link[0].file` (or whichever element contains the iframe/player URL).
- If `config.link` is actually a string only in iframe mode: update typings accordingly (e.g., a discriminated union for `playTech`) and ensure downstream code always receives the normalized array after you rewrite `config.link`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +70 to +83
const isEncrypted = encryptedM3u8.match(/[?&]_c=[0-9]+/)
let finalM3u8 = ""

switch (
(item.label as typeof item.label | undefined)?.toUpperCase() as
| Uppercase<Exclude<typeof item.label, undefined>>
| undefined
) {
case "HD":
if (item.preload) item.label = "FHD|HD"
break
case undefined:
item.label = "HD"
break
}
item.qualityCode = getQualityByLabel(item.label)
item.type ??= "mp4"
})
)
if (isEncrypted) {
finalM3u8 = await decryptM3u8(encryptedM3u8, headers)
}
config.link = [
{
file: `data:application/vnd.apple.mpegurl;base64,${btoa(unescape(encodeURIComponent(finalM3u8)))}`,
label: "FHD|HD",
preload: "auto",
type: "hls"
}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Blank m3u8 returned 🐞 Bug ≡ Correctness

In iframe mode, finalM3u8 is initialized to an empty string and only set when _c= is detected;
otherwise the returned HLS data URI contains an empty playlist. This causes playback/download to
fail whenever the fetched playlist is already unencrypted or uses a different encryption marker.
Agent Prompt
### Issue description
When the fetched playlist is not encrypted (or not matched by the `_c=` regex), `finalM3u8` remains empty but is still used to create the `data:` URL.

### Issue Context
This makes iframe playback return an empty playlist.

### Fix Focus Areas
- src/apis/runs/ajax/player-link.ts[70-83]

### Suggested fix
Set `finalM3u8` to the fetched playlist by default, and only replace it with decrypted content when needed:
- `let finalM3u8 = encryptedM3u8`
- `if (isEncrypted) finalM3u8 = await decryptM3u8(encryptedM3u8, headers)`
Also consider renaming `encryptedM3u8` to `rawM3u8` to avoid confusion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +130 to +134
if (finalUrl.startsWith("/")) {
finalUrl = cdn_google + finalUrl
} else if (!finalUrl.startsWith("http")) {
finalUrl = cdn_google + "/" + finalUrl
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Cdn_google not imported 🐞 Bug ≡ Correctness

cdn_google is referenced in decrypt-hls-animevsub.ts and get-source-m3u8.ts but is never
imported, causing a runtime ReferenceError (and typically a TS compile error). This breaks
playlist URL rewriting and playlist fetching.
Agent Prompt
### Issue description
`cdn_google` is used without being imported in two files, making it undefined at runtime.

### Issue Context
`cdn_google` is exported from `src/constants.ts`.

### Fix Focus Areas
- src/logic/decrypt-hls-animevsub.ts[123-134]
- src/logic/get-source-m3u8.ts[1-24]
- src/constants.ts[50-64]

### Suggested fix
Add:
- `import { cdn_google } from "src/constants"`
into both `decrypt-hls-animevsub.ts` and `get-source-m3u8.ts`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +12 to +24
const idRegex = /const\s+id\s*=\s*["']([^"']+)["']/i
const tokenRegex = /const\s+avsToken\s*=\s*["']([^"']+)["']/i

const idMatch = html.match(idRegex)
const tokenMatch = html.match(tokenRegex)

const id = idMatch ? idMatch[1] : null
const avsToken = tokenMatch ? tokenMatch[1] : null

const m3u8Response = await get({
url: `${cdn_google}/playlist/${id}/playlist.m3u8?token=${avsToken}#animevsub-vsubo`,
responseType: "text"
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Null id/token request 🐞 Bug ☼ Reliability

getSourceM3u8 continues even when it fails to extract id or avsToken, producing a request to
/playlist/null/playlist.m3u8?token=null. This will fail downstream with less actionable errors
than a clear parse failure.
Agent Prompt
### Issue description
If HTML parsing fails, `id` and/or `avsToken` becomes `null` but the code still builds a playlist URL using those values.

### Issue Context
This generates invalid URLs and hides the true root cause (parsing failure).

### Fix Focus Areas
- src/logic/get-source-m3u8.ts[12-24]

### Suggested fix
After extracting matches:
- If `!id` or `!avsToken`, throw a descriptive error (include a small snippet or markers to help debugging).
- Use `encodeURIComponent(avsToken)` when constructing the query string to avoid malformed URLs if the token contains special characters.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@fwy13 fwy13 mentioned this pull request Apr 11, 2026
@tachibana-shin

Copy link
Copy Markdown
Member

Can you create a pull request just for comments? I already have a Rust decoder and I don't want the decoders to appear on public repositories.

@fwy13

fwy13 commented Apr 12, 2026

Copy link
Copy Markdown
Author

I have created a new PR #166 containing only the comments, so I will close this one. Thank you for your guidance

@fwy13 fwy13 closed this Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants