Skip to content

Commit 5e7ab98

Browse files
Update version to 1.8.8 and enhance video detection features
- Bump version in README.md, manifest.json, SPECIFICATION.md, main.py, and worker.py to 1.8.8. - Introduce deep manifest interception to detect disguised streams, improving video URL detection capabilities. - Update documentation to reflect new features and changes in functionality. - Implement response content-type detection for HLS manifests regardless of URL extension. - Adjust rate limiting for read and write endpoints to optimize performance. - Add tests for format hint acceptance and rate limit behavior.
1 parent 447588f commit 5e7ab98

7 files changed

Lines changed: 67 additions & 22 deletions

File tree

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
**Languages**: **English** (`README.md`) | **繁體中文** (`README.zh-TW.md`)
1010

11-
> Seamlessly capture web video URLs (M3U8 and MP4) from Chrome and download them to your NAS
11+
> Seamlessly capture web video URLs (M3U8 and MP4) from Chrome and download them to your NAS — even when sites disguise streams with non-standard URLs
1212
1313
> [!IMPORTANT]
1414
> This project does **not** guarantee every video can be downloaded. Some sites use DRM, expiring URLs, anti-hotlinking, IP restrictions, or change their delivery logic at any time.
@@ -39,7 +39,7 @@
3939
## Overview
4040

4141
This system enables you to:
42-
1. 🔍 Detect M3U8 and MP4 video URLs in Chrome
42+
1. 🔍 Detect M3U8 and MP4 video URLs in Chrome (including disguised streams)
4343
2. 📤 Send URLs to your NAS with one click
4444
3. ⬇️ Automatically download and convert to MP4
4545
4. 💾 Store videos on your NAS storage
@@ -72,6 +72,7 @@ Chrome Extension → NAS Docker (API + Worker) → Video Storage
7272

7373
### Chrome Extension
7474
- ✅ Automatic M3U8 and MP4 URL detection
75+
- ✅ Deep manifest interception — detects disguised streams (e.g. `.jpg`-wrapped HLS) via fetch/XHR content inspection
7576
- ✅ One-click send to NAS
7677
- ✅ Side panel interface for easy access
7778
- ✅ Real-time download progress
@@ -280,6 +281,7 @@ You can now:
280281
- ✅ Deploy Docker stack on Synology NAS or any Docker host
281282
- ✅ Download M3U8 video streams to MP4
282283
- ✅ Download MP4 videos directly
284+
- ✅ Detect disguised manifests via JS-level content interception
283285
- ✅ Use Chrome extension for automatic detection (M3U8 & MP4)
284286
- ✅ Forward cookies & headers for authenticated streams
285287
- ✅ Monitor download progress in side panel
@@ -644,6 +646,8 @@ async function detectM3u8Urls(details) {
644646
WebVideo2NAS/
645647
├── chrome-extension/ # Chrome extension source
646648
│ ├── background.js # Background service worker
649+
│ ├── content.js # Content script (ISOLATED world)
650+
│ ├── inject.js # Manifest interceptor (MAIN world)
647651
│ ├── sidepanel.* # Extension side panel UI
648652
│ ├── options/ # Extension options page
649653
│ ├── icons/ # Extension icons
@@ -735,6 +739,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
735739
<details>
736740
<summary><strong>Full Changelog (click to expand)</strong></summary>
737741

742+
### [1.8.8] - 2026-03-11
743+
744+
#### Added
745+
- **Deep manifest interception** (`inject.js`): New MAIN-world content script that patches `fetch()` and `XMLHttpRequest` to inspect the first bytes of every response for `#EXTM3U` signatures — catches HLS manifests served from arbitrary URLs without `.m3u8` extension or correct MIME type (e.g. sites that disguise streams as `.jpg`)
746+
- **Response Content-Type detection**: Identify HLS manifests by `Content-Type` header regardless of URL extension
747+
- `format` hint field in download API — allows the extension to tell the backend the stream type even when the URL has no recognizable extension
748+
749+
#### Changed
750+
- `sendToNAS()` accepts URLs detected by Content-Type or content interception (not just URL pattern)
751+
- NAS API validator uses `model_validator` to allow `format` hint to bypass URL pattern check
752+
753+
#### Fixed
754+
- **Rate limit no longer blocks normal usage**: Read-only endpoints (job list, job status) now use a separate, higher rate-limit bucket so side panel polling no longer starves download requests
755+
738756
### [1.8.6] - 2026-01-13
739757

740758
#### Added
@@ -942,8 +960,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
942960

943961
---
944962

945-
**Version**: 1.8.6
946-
**Last Updated**: 2025-12-16
963+
**Version**: 1.8.8
964+
**Last Updated**: 2026-03-11
947965
**Port**: 52052 (NAS host port → API container :8000)
948966

949967
## Star History

chrome-extension/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "WebVideo2NAS",
4-
"version": "1.8.6",
4+
"version": "1.8.8",
55
"description": "Send web videos (m3u8, mpd, mp4) to your NAS for download",
66
"permissions": [
77
"storage",

chrome-extension/tests/background.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function makeChromeStub() {
2626
webRequest: {
2727
onBeforeRequest: { addListener: () => {} },
2828
onSendHeaders: { addListener: () => {} },
29+
onHeadersReceived: { addListener: () => {} },
2930
},
3031
action: {
3132
setBadgeText: () => {},

docs/SPECIFICATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ This document specifies a complete system for capturing web video URLs (m3u8 str
101101
{
102102
"manifest_version": 3,
103103
"name": "WebVideo2NAS",
104-
"version": "1.8.6",
105-
"description": "Send m3u8 and mp4 videos to your NAS for download",
104+
"version": "1.8.8",
105+
"description": "Send m3u8, mpd, and mp4 videos to your NAS for download",
106106
"permissions": [
107107
"storage",
108108
"contextMenus",

video-downloader/docker/api/main.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
app = FastAPI(
4545
title="WebVideo2NAS API",
4646
description="API for managing web video downloads (M3U8 and MP4)",
47-
version="1.8.6"
47+
version="1.8.8"
4848
)
4949

5050
# CORS middleware
@@ -107,19 +107,22 @@ def _enforce_client_allowlist(request: Request) -> None:
107107
raise HTTPException(status_code=403, detail="Client IP not allowed")
108108

109109

110+
_RATE_LIMIT_MULTIPLIERS = {"read": 6, "write": 1}
111+
110112
def _rate_limit(request: Request, bucket: str) -> None:
111113
if RATE_LIMIT_PER_MINUTE <= 0:
112114
return
115+
multiplier = _RATE_LIMIT_MULTIPLIERS.get(bucket, 1)
116+
limit = RATE_LIMIT_PER_MINUTE * multiplier
113117
client_ip = _get_client_ip(request)
114118
window = int(datetime.utcnow().timestamp() // 60)
115119
key = f"rl:{bucket}:{client_ip}:{window}"
116120
try:
117121
count = redis_client.incr(key)
118122
redis_client.expire(key, 90)
119123
except Exception:
120-
# If Redis is unavailable, skip rate limiting (avoid breaking core API).
121124
return
122-
if count > RATE_LIMIT_PER_MINUTE:
125+
if count > limit:
123126
raise HTTPException(status_code=429, detail="Rate limit exceeded")
124127

125128

@@ -204,30 +207,33 @@ def get_db():
204207
finally:
205208
db.close()
206209

207-
def verify_api_key(request: Request, authorization: Optional[str] = Header(None)):
208-
"""Verify API key from Authorization header"""
210+
def _verify_key_common(request: Request, authorization: Optional[str], bucket: str) -> str:
209211
_enforce_client_allowlist(request)
210-
_rate_limit(request, bucket="auth")
212+
_rate_limit(request, bucket=bucket)
211213
if not API_KEY or API_KEY.strip() == "" or API_KEY.strip() == "change-this-key":
212214
raise HTTPException(status_code=503, detail="Server not configured: API_KEY is not set")
213215
if not authorization:
214216
raise HTTPException(status_code=401, detail="Missing Authorization header")
215-
216-
# Support both "Bearer TOKEN" and just "TOKEN"
217217
token = authorization.replace("Bearer ", "").strip()
218-
219218
if token != API_KEY:
220219
raise HTTPException(status_code=401, detail="Invalid API key")
221-
222220
return token
223221

222+
def verify_api_key(request: Request, authorization: Optional[str] = Header(None)):
223+
"""Verify API key — write-endpoint rate limit (RATE_LIMIT_PER_MINUTE)."""
224+
return _verify_key_common(request, authorization, bucket="write")
225+
226+
def verify_api_key_read(request: Request, authorization: Optional[str] = Header(None)):
227+
"""Verify API key — read-endpoint rate limit (6x write limit)."""
228+
return _verify_key_common(request, authorization, bucket="read")
229+
224230
# Routes
225231
@app.get("/")
226232
async def root():
227233
"""Root endpoint"""
228234
return {
229235
"name": "WebVideo2NAS API",
230-
"version": "1.8.6",
236+
"version": "1.8.8",
231237
"status": "running"
232238
}
233239

@@ -316,7 +322,7 @@ async def list_jobs(
316322
status: Optional[str] = None,
317323
limit: int = 50,
318324
db: Session = Depends(get_db),
319-
api_key: str = Depends(verify_api_key)
325+
api_key: str = Depends(verify_api_key_read)
320326
):
321327
"""List all jobs with optional status filter"""
322328
try:
@@ -363,7 +369,7 @@ async def list_jobs(
363369
async def get_job(
364370
job_id: str,
365371
db: Session = Depends(get_db),
366-
api_key: str = Depends(verify_api_key)
372+
api_key: str = Depends(verify_api_key_read)
367373
):
368374
"""Get details of a specific job"""
369375
try:
@@ -430,7 +436,7 @@ async def delete_job(
430436
@app.get("/api/status", response_model=SystemStatus)
431437
async def get_status(
432438
db: Session = Depends(get_db),
433-
api_key: str = Depends(verify_api_key)
439+
api_key: str = Depends(verify_api_key_read)
434440
):
435441
"""Get system status"""
436442
try:

video-downloader/docker/api/tests/test_api_validation.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,23 @@ def test_download_request_allows_localhost_when_ssrf_guard_disabled(monkeypatch)
4141
api_main = _reload_api_main(monkeypatch, SSRF_GUARD="false")
4242
r = api_main.DownloadRequest(url="http://127.0.0.1/video.mp4")
4343
assert str(r.url).startswith("http://127.0.0.1")
44+
45+
46+
def test_download_request_accepts_format_hint_for_non_standard_url(monkeypatch):
47+
api_main = _reload_api_main(monkeypatch, SSRF_GUARD="false")
48+
r = api_main.DownloadRequest(
49+
url="https://example.com/stream/index.jpg",
50+
format="m3u8",
51+
)
52+
assert r.format == "m3u8"
53+
54+
55+
def test_download_request_rejects_non_standard_url_without_format_hint(monkeypatch):
56+
api_main = _reload_api_main(monkeypatch, SSRF_GUARD="false")
57+
with pytest.raises(Exception):
58+
api_main.DownloadRequest(url="https://example.com/stream/index.jpg")
59+
60+
61+
def test_rate_limit_read_bucket_has_higher_limit(monkeypatch):
62+
api_main = _reload_api_main(monkeypatch, RATE_LIMIT_PER_MINUTE="10")
63+
assert api_main._RATE_LIMIT_MULTIPLIERS["read"] > api_main._RATE_LIMIT_MULTIPLIERS["write"]

video-downloader/docker/worker/worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ def main():
11341134
"""Main entry point"""
11351135
logger.info("="*50)
11361136
logger.info("WebVideo2NAS Worker")
1137-
logger.info("Version: 1.8.6")
1137+
logger.info("Version: 1.8.8")
11381138
logger.info("="*50)
11391139

11401140
# Wait for database to be ready

0 commit comments

Comments
 (0)