mhdzumair commited on
Commit
64eeb8f
·
1 Parent(s): 4b4f6af

Add AD speed test & improve speed test UI & refactoring

Browse files
mediaflow_proxy/main.py CHANGED
@@ -1,19 +1,17 @@
1
  import logging
2
- import uuid
3
  from importlib import resources
4
 
5
- from fastapi import FastAPI, Depends, Security, HTTPException, BackgroundTasks
6
  from fastapi.security import APIKeyQuery, APIKeyHeader
7
  from starlette.middleware.cors import CORSMiddleware
8
- from starlette.responses import RedirectResponse, JSONResponse
9
  from starlette.staticfiles import StaticFiles
10
 
11
  from mediaflow_proxy.configs import settings
12
- from mediaflow_proxy.routes import proxy_router, extractor_router
13
  from mediaflow_proxy.schemas import GenerateUrlRequest
14
  from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware
15
  from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url
16
- from mediaflow_proxy.utils.rd_speedtest import run_speedtest, prune_task, results
17
 
18
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
19
  app = FastAPI()
@@ -51,31 +49,16 @@ async def health_check():
51
  return {"status": "healthy"}
52
 
53
 
54
- @app.get("/speedtest")
55
- async def trigger_speedtest(background_tasks: BackgroundTasks, api_password: str = Depends(verify_api_key)):
56
- # Generate a random UUID as task_id
57
- task_id = str(uuid.uuid4()) # Generate unique task ID
58
- background_tasks.add_task(run_speedtest, task_id)
59
-
60
- # Schedule the task to be pruned after 1 hour
61
- background_tasks.add_task(prune_task, task_id)
62
-
63
- return RedirectResponse(url=f"/speedtest_progress.html?task_id={task_id}")
64
-
65
-
66
- @app.get("/speedtest/results/{task_id}", response_class=JSONResponse)
67
- async def get_speedtest_result(task_id: str):
68
- if task_id in results:
69
- return results[task_id]
70
- else:
71
- return {"message": "Speedtest is still running, please wait or the task may have expired."}
72
-
73
-
74
  @app.get("/favicon.ico")
75
  async def get_favicon():
76
  return RedirectResponse(url="/logo.png")
77
 
78
 
 
 
 
 
 
79
  @app.post("/generate_encrypted_or_encoded_url")
80
  async def generate_encrypted_or_encoded_url(request: GenerateUrlRequest):
81
  if "api_password" not in request.query_params:
@@ -97,6 +80,7 @@ async def generate_encrypted_or_encoded_url(request: GenerateUrlRequest):
97
 
98
  app.include_router(proxy_router, prefix="/proxy", tags=["proxy"], dependencies=[Depends(verify_api_key)])
99
  app.include_router(extractor_router, prefix="/extractor", tags=["extractors"], dependencies=[Depends(verify_api_key)])
 
100
 
101
  static_path = resources.files("mediaflow_proxy").joinpath("static")
102
  app.mount("/", StaticFiles(directory=str(static_path), html=True), name="static")
 
1
  import logging
 
2
  from importlib import resources
3
 
4
+ from fastapi import FastAPI, Depends, Security, HTTPException
5
  from fastapi.security import APIKeyQuery, APIKeyHeader
6
  from starlette.middleware.cors import CORSMiddleware
7
+ from starlette.responses import RedirectResponse
8
  from starlette.staticfiles import StaticFiles
9
 
10
  from mediaflow_proxy.configs import settings
11
+ from mediaflow_proxy.routes import proxy_router, extractor_router, speedtest_router
12
  from mediaflow_proxy.schemas import GenerateUrlRequest
13
  from mediaflow_proxy.utils.crypto_utils import EncryptionHandler, EncryptionMiddleware
14
  from mediaflow_proxy.utils.http_utils import encode_mediaflow_proxy_url
 
15
 
16
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
17
  app = FastAPI()
 
49
  return {"status": "healthy"}
50
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  @app.get("/favicon.ico")
53
  async def get_favicon():
54
  return RedirectResponse(url="/logo.png")
55
 
56
 
57
+ @app.get("/speedtest")
58
+ async def show_speedtest_page():
59
+ return RedirectResponse(url="/speedtest.html")
60
+
61
+
62
  @app.post("/generate_encrypted_or_encoded_url")
63
  async def generate_encrypted_or_encoded_url(request: GenerateUrlRequest):
64
  if "api_password" not in request.query_params:
 
80
 
81
  app.include_router(proxy_router, prefix="/proxy", tags=["proxy"], dependencies=[Depends(verify_api_key)])
82
  app.include_router(extractor_router, prefix="/extractor", tags=["extractors"], dependencies=[Depends(verify_api_key)])
83
+ app.include_router(speedtest_router, prefix="/speedtest", tags=["speedtest"], dependencies=[Depends(verify_api_key)])
84
 
85
  static_path = resources.files("mediaflow_proxy").joinpath("static")
86
  app.mount("/", StaticFiles(directory=str(static_path), html=True), name="static")
mediaflow_proxy/routes/__init__.py CHANGED
@@ -1,2 +1,5 @@
1
  from .proxy import proxy_router
2
  from .extractor import extractor_router
 
 
 
 
1
  from .proxy import proxy_router
2
  from .extractor import extractor_router
3
+ from .speedtest import speedtest_router
4
+
5
+ __all__ = ["proxy_router", "extractor_router", "speedtest_router"]
mediaflow_proxy/routes/speedtest.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+
3
+ from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
4
+ from fastapi.responses import RedirectResponse
5
+
6
+ from mediaflow_proxy.speedtest.service import SpeedTestService, SpeedTestProvider
7
+
8
+ speedtest_router = APIRouter()
9
+
10
+ # Initialize service
11
+ speedtest_service = SpeedTestService()
12
+
13
+
14
+ @speedtest_router.get("/", summary="Show speed test interface")
15
+ async def show_speedtest_page():
16
+ """Return the speed test HTML interface."""
17
+ return RedirectResponse(url="/speedtest.html")
18
+
19
+
20
+ @speedtest_router.post("/start", summary="Start a new speed test", response_model=dict)
21
+ async def start_speedtest(background_tasks: BackgroundTasks, provider: SpeedTestProvider, request: Request):
22
+ """Start a new speed test for the specified provider."""
23
+ task_id = str(uuid.uuid4())
24
+ api_key = request.headers.get("api_key")
25
+
26
+ # Create and initialize the task
27
+ await speedtest_service.create_test(task_id, provider, api_key)
28
+
29
+ # Schedule the speed test
30
+ background_tasks.add_task(speedtest_service.run_speedtest, task_id, provider, api_key)
31
+
32
+ return {"task_id": task_id}
33
+
34
+
35
+ @speedtest_router.get("/results/{task_id}", summary="Get speed test results")
36
+ async def get_speedtest_results(task_id: str):
37
+ """Get the results or current status of a speed test."""
38
+ task = await speedtest_service.get_test_results(task_id)
39
+
40
+ if not task:
41
+ raise HTTPException(status_code=404, detail="Speed test task not found or expired")
42
+
43
+ return task.dict()
mediaflow_proxy/speedtest/__init__.py ADDED
File without changes
mediaflow_proxy/speedtest/models.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from enum import Enum
3
+ from typing import Dict, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SpeedTestProvider(str, Enum):
9
+ REAL_DEBRID = "real_debrid"
10
+ ALL_DEBRID = "all_debrid"
11
+
12
+
13
+ class ServerInfo(BaseModel):
14
+ url: str
15
+ name: str
16
+
17
+
18
+ class UserInfo(BaseModel):
19
+ ip: Optional[str] = None
20
+ isp: Optional[str] = None
21
+ country: Optional[str] = None
22
+
23
+
24
+ class SpeedTestResult(BaseModel):
25
+ speed_mbps: float = Field(..., description="Speed in Mbps")
26
+ duration: float = Field(..., description="Test duration in seconds")
27
+ data_transferred: int = Field(..., description="Data transferred in bytes")
28
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
29
+
30
+
31
+ class LocationResult(BaseModel):
32
+ result: Optional[SpeedTestResult] = None
33
+ error: Optional[str] = None
34
+ server_name: str
35
+ server_url: str
36
+
37
+
38
+ class SpeedTestTask(BaseModel):
39
+ task_id: str
40
+ provider: SpeedTestProvider
41
+ results: Dict[str, LocationResult] = {}
42
+ started_at: datetime
43
+ completed_at: Optional[datetime] = None
44
+ status: str = "running"
45
+ user_info: Optional[UserInfo] = None
46
+ current_location: Optional[str] = None
mediaflow_proxy/speedtest/providers/all_debrid.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import Dict, Tuple, Optional
3
+
4
+ from mediaflow_proxy.configs import settings
5
+ from mediaflow_proxy.speedtest.models import ServerInfo, UserInfo
6
+ from mediaflow_proxy.speedtest.providers.base import BaseSpeedTestProvider, SpeedTestProviderConfig
7
+ from mediaflow_proxy.utils.http_utils import request_with_retry
8
+
9
+
10
+ class SpeedTestError(Exception):
11
+ pass
12
+
13
+
14
+ class AllDebridSpeedTest(BaseSpeedTestProvider):
15
+ """AllDebrid speed test provider implementation."""
16
+
17
+ def __init__(self, api_key: str):
18
+ self.api_key = api_key
19
+ self.servers: Dict[str, ServerInfo] = {}
20
+
21
+ async def get_test_urls(self) -> Tuple[Dict[str, str], Optional[UserInfo]]:
22
+ response = await request_with_retry(
23
+ "GET",
24
+ "https://alldebrid.com/internalapi/v4/speedtest",
25
+ headers={"User-Agent": settings.user_agent},
26
+ params={"agent": "service", "version": "1.0-363869a7", "apikey": self.api_key},
27
+ )
28
+
29
+ if response.status_code != 200:
30
+ raise SpeedTestError("Failed to fetch AllDebrid servers")
31
+
32
+ data = response.json()
33
+ if data["status"] != "success":
34
+ raise SpeedTestError("AllDebrid API returned error")
35
+
36
+ # Create UserInfo
37
+ user_info = UserInfo(ip=data["data"]["ip"], isp=data["data"]["isp"], country=data["data"]["country"])
38
+
39
+ # Store server info
40
+ self.servers = {server["name"]: ServerInfo(**server) for server in data["data"]["servers"]}
41
+
42
+ # Generate URLs with random number
43
+ random_number = f"{random.uniform(1, 2):.24f}".replace(".", "")
44
+ urls = {name: f"{server.url}/speedtest/{random_number}" for name, server in self.servers.items()}
45
+
46
+ return urls, user_info
47
+
48
+ async def get_config(self) -> SpeedTestProviderConfig:
49
+ urls, _ = await self.get_test_urls()
50
+ return SpeedTestProviderConfig(test_duration=10, test_urls=urls)
mediaflow_proxy/speedtest/providers/base.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Tuple, Optional
3
+ from pydantic import BaseModel
4
+
5
+ from mediaflow_proxy.speedtest.models import UserInfo
6
+
7
+
8
+ class SpeedTestProviderConfig(BaseModel):
9
+ test_duration: int = 10 # seconds
10
+ test_urls: Dict[str, str]
11
+
12
+
13
+ class BaseSpeedTestProvider(ABC):
14
+ """Base class for speed test providers."""
15
+
16
+ @abstractmethod
17
+ async def get_test_urls(self) -> Tuple[Dict[str, str], Optional[UserInfo]]:
18
+ """Get list of test URLs for the provider and optional user info."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def get_config(self) -> SpeedTestProviderConfig:
23
+ """Get provider-specific configuration."""
24
+ pass
mediaflow_proxy/speedtest/providers/real_debrid.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Tuple, Optional
2
+ import random
3
+
4
+ from mediaflow_proxy.speedtest.models import UserInfo
5
+ from mediaflow_proxy.speedtest.providers.base import BaseSpeedTestProvider, SpeedTestProviderConfig
6
+
7
+
8
+ class RealDebridSpeedTest(BaseSpeedTestProvider):
9
+ """RealDebrid speed test provider implementation."""
10
+
11
+ async def get_test_urls(self) -> Tuple[Dict[str, str], Optional[UserInfo]]:
12
+ urls = {
13
+ "AMS": "https://45.download.real-debrid.com/speedtest/testDefault.rar/",
14
+ "RBX": "https://rbx.download.real-debrid.com/speedtest/test.rar/",
15
+ "LON1": "https://lon1.download.real-debrid.com/speedtest/test.rar/",
16
+ "HKG1": "https://hkg1.download.real-debrid.com/speedtest/test.rar/",
17
+ "SGP1": "https://sgp1.download.real-debrid.com/speedtest/test.rar/",
18
+ "SGPO1": "https://sgpo1.download.real-debrid.com/speedtest/test.rar/",
19
+ "TYO1": "https://tyo1.download.real-debrid.com/speedtest/test.rar/",
20
+ "LAX1": "https://lax1.download.real-debrid.com/speedtest/test.rar/",
21
+ "TLV1": "https://tlv1.download.real-debrid.com/speedtest/test.rar/",
22
+ "MUM1": "https://mum1.download.real-debrid.com/speedtest/test.rar/",
23
+ "JKT1": "https://jkt1.download.real-debrid.com/speedtest/test.rar/",
24
+ "Cloudflare": "https://45.download.real-debrid.cloud/speedtest/testCloudflare.rar/",
25
+ }
26
+ # Add random number to prevent caching
27
+ urls = {location: f"{base_url}{random.uniform(0, 1):.16f}" for location, base_url in urls.items()}
28
+ return urls, None
29
+
30
+ async def get_config(self) -> SpeedTestProviderConfig:
31
+ urls, _ = await self.get_test_urls()
32
+ return SpeedTestProviderConfig(test_duration=10, test_urls=urls)
mediaflow_proxy/speedtest/service.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import time
3
+ from datetime import datetime
4
+ from typing import Dict, Optional, Type
5
+
6
+ from cachetools import TTLCache
7
+ from httpx import AsyncClient
8
+
9
+ from mediaflow_proxy.utils.http_utils import Streamer
10
+ from .models import SpeedTestTask, LocationResult, SpeedTestResult, SpeedTestProvider
11
+ from .providers.all_debrid import AllDebridSpeedTest
12
+ from .providers.base import BaseSpeedTestProvider
13
+ from .providers.real_debrid import RealDebridSpeedTest
14
+ from ..configs import settings
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SpeedTestService:
20
+ """Service for managing speed tests across different providers."""
21
+
22
+ def __init__(self):
23
+ # Cache for speed test results (1 hour TTL)
24
+ self._cache: TTLCache[str, SpeedTestTask] = TTLCache(maxsize=100, ttl=3600)
25
+
26
+ # Provider mapping
27
+ self._providers: Dict[SpeedTestProvider, Type[BaseSpeedTestProvider]] = {
28
+ SpeedTestProvider.REAL_DEBRID: RealDebridSpeedTest,
29
+ SpeedTestProvider.ALL_DEBRID: AllDebridSpeedTest,
30
+ }
31
+
32
+ def _get_provider(self, provider: SpeedTestProvider, api_key: Optional[str] = None) -> BaseSpeedTestProvider:
33
+ """Get the appropriate provider implementation."""
34
+ provider_class = self._providers.get(provider)
35
+ if not provider_class:
36
+ raise ValueError(f"Unsupported provider: {provider}")
37
+
38
+ if provider == SpeedTestProvider.ALL_DEBRID and not api_key:
39
+ raise ValueError("API key required for AllDebrid")
40
+
41
+ return provider_class(api_key) if provider == SpeedTestProvider.ALL_DEBRID else provider_class()
42
+
43
+ async def create_test(
44
+ self, task_id: str, provider: SpeedTestProvider, api_key: Optional[str] = None
45
+ ) -> SpeedTestTask:
46
+ """Create a new speed test task."""
47
+ provider_impl = self._get_provider(provider, api_key)
48
+
49
+ # Get initial URLs and user info
50
+ urls, user_info = await provider_impl.get_test_urls()
51
+
52
+ task = SpeedTestTask(task_id=task_id, provider=provider, started_at=datetime.utcnow(), user_info=user_info)
53
+
54
+ self._cache[task_id] = task
55
+ return task
56
+
57
+ async def get_test_results(self, task_id: str) -> Optional[SpeedTestTask]:
58
+ """Get results for a specific task."""
59
+ return self._cache.get(task_id)
60
+
61
+ async def run_speedtest(self, task_id: str, provider: SpeedTestProvider, api_key: Optional[str] = None):
62
+ """Run the speed test with real-time updates."""
63
+ try:
64
+ task = self._cache.get(task_id)
65
+ if not task:
66
+ raise ValueError(f"Task {task_id} not found")
67
+
68
+ provider_impl = self._get_provider(provider, api_key)
69
+ config = await provider_impl.get_config()
70
+
71
+ async with AsyncClient(follow_redirects=True, timeout=10, proxy=settings.proxy_url) as client:
72
+ streamer = Streamer(client)
73
+
74
+ for location, url in config.test_urls.items():
75
+ try:
76
+ task.current_location = location
77
+ result = await self._test_location(location, url, streamer, config.test_duration, provider_impl)
78
+ task.results[location] = result
79
+ self._cache[task_id] = task
80
+ except Exception as e:
81
+ logger.error(f"Error testing {location}: {str(e)}")
82
+ task.results[location] = LocationResult(
83
+ error=str(e), server_name=location, server_url=config.test_urls[location]
84
+ )
85
+ self._cache[task_id] = task
86
+
87
+ # Mark task as completed
88
+ task.completed_at = datetime.utcnow()
89
+ task.status = "completed"
90
+ task.current_location = None
91
+ self._cache[task_id] = task
92
+
93
+ except Exception as e:
94
+ logger.error(f"Error in speed test task {task_id}: {str(e)}")
95
+ if task := self._cache.get(task_id):
96
+ task.status = "failed"
97
+ self._cache[task_id] = task
98
+
99
+ async def _test_location(
100
+ self, location: str, url: str, streamer: Streamer, test_duration: int, provider: BaseSpeedTestProvider
101
+ ) -> LocationResult:
102
+ """Test speed for a specific location."""
103
+ try:
104
+ start_time = time.time()
105
+ total_bytes = 0
106
+
107
+ async for chunk in streamer.stream_content(url, headers={}):
108
+ if time.time() - start_time >= test_duration:
109
+ break
110
+ total_bytes += len(chunk)
111
+
112
+ duration = time.time() - start_time
113
+ speed_mbps = (total_bytes * 8) / (duration * 1_000_000)
114
+
115
+ # Get server info if available (for AllDebrid)
116
+ server_info = getattr(provider, "servers", {}).get(location)
117
+ server_url = server_info.url if server_info else url
118
+
119
+ return LocationResult(
120
+ result=SpeedTestResult(
121
+ speed_mbps=round(speed_mbps, 2), duration=round(duration, 2), data_transferred=total_bytes
122
+ ),
123
+ server_name=location,
124
+ server_url=server_url,
125
+ )
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error testing {location}: {str(e)}")
129
+ raise # Re-raise to be handled by run_speedtest
mediaflow_proxy/static/speedtest.html ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Debrid Speed Test</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ theme: {
12
+ extend: {
13
+ animation: {
14
+ 'progress': 'progress 180s linear forwards',
15
+ },
16
+ keyframes: {
17
+ progress: {
18
+ '0%': {width: '0%'},
19
+ '100%': {width: '100%'}
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+ <style>
27
+ .provider-card {
28
+ transition: all 0.3s ease;
29
+ }
30
+
31
+ .provider-card:hover {
32
+ transform: translateY(-5px);
33
+ }
34
+
35
+ @keyframes slideIn {
36
+ from {
37
+ transform: translateY(20px);
38
+ opacity: 0;
39
+ }
40
+ to {
41
+ transform: translateY(0);
42
+ opacity: 1;
43
+ }
44
+ }
45
+
46
+ .slide-in {
47
+ animation: slideIn 0.3s ease-out forwards;
48
+ }
49
+ </style>
50
+ </head>
51
+ <body class="bg-gray-100 dark:bg-gray-900 min-h-full">
52
+ <!-- Theme Toggle -->
53
+ <div class="fixed top-4 right-4 z-50">
54
+ <button id="themeToggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
55
+ <svg id="sunIcon" class="w-6 h-6 text-yellow-500 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
57
+ d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
58
+ </svg>
59
+ <svg id="moonIcon" class="w-6 h-6 text-gray-700 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
61
+ </svg>
62
+ </button>
63
+ </div>
64
+
65
+ <main class="container mx-auto px-4 py-8">
66
+ <!-- Views Container -->
67
+ <div id="views-container">
68
+ <!-- API Password View -->
69
+ <div id="passwordView" class="space-y-8">
70
+ <h1 class="text-3xl font-bold text-center text-gray-800 dark:text-white mb-8">
71
+ Enter API Password
72
+ </h1>
73
+
74
+ <div class="max-w-md mx-auto">
75
+ <form id="passwordForm" class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 space-y-4">
76
+ <div class="space-y-2">
77
+ <label for="apiPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
78
+ API Password
79
+ </label>
80
+ <input
81
+ type="password"
82
+ id="apiPassword"
83
+ class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
84
+ required
85
+ >
86
+ </div>
87
+ <div class="flex items-center space-x-2">
88
+ <input
89
+ type="checkbox"
90
+ id="rememberPassword"
91
+ class="rounded border-gray-300 dark:border-gray-600"
92
+ >
93
+ <label for="rememberPassword" class="text-sm text-gray-600 dark:text-gray-400">
94
+ Remember password
95
+ </label>
96
+ </div>
97
+ <button
98
+ type="submit"
99
+ class="w-full px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
100
+ >
101
+ Continue
102
+ </button>
103
+ </form>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Provider Selection View -->
108
+ <div id="selectionView" class="space-y-8 hidden">
109
+ <h1 class="text-3xl font-bold text-center text-gray-800 dark:text-white mb-8">
110
+ Select Debrid Service for Speed Test
111
+ </h1>
112
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
113
+ <!-- Real-Debrid Card -->
114
+ <button onclick="startTest('real_debrid')" class="provider-card bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-left hover:shadow-xl transition-shadow">
115
+ <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">Real-Debrid</h2>
116
+ <p class="text-gray-600 dark:text-gray-300">Test speeds across multiple Real-Debrid servers worldwide</p>
117
+ </button>
118
+
119
+ <!-- AllDebrid Card -->
120
+ <button onclick="showAllDebridSetup()" class="provider-card bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-left hover:shadow-xl transition-shadow">
121
+ <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">AllDebrid</h2>
122
+ <p class="text-gray-600 dark:text-gray-300">Measure download speeds from AllDebrid servers</p>
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- AllDebrid Setup View -->
128
+ <div id="allDebridSetupView" class="max-w-md mx-auto space-y-6 hidden">
129
+ <h2 class="text-2xl font-bold text-center text-gray-800 dark:text-white mb-8">
130
+ AllDebrid Setup
131
+ </h2>
132
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
133
+ <form id="allDebridForm" class="space-y-4">
134
+ <div class="space-y-2">
135
+ <label for="adApiKey" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
136
+ AllDebrid API Key
137
+ </label>
138
+ <input
139
+ type="password"
140
+ id="adApiKey"
141
+ class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
142
+ required
143
+ >
144
+ <p class="text-sm text-gray-500 dark:text-gray-400">
145
+ You can find your API key in the AllDebrid dashboard
146
+ </p>
147
+ </div>
148
+ <div class="flex items-center space-x-2">
149
+ <input
150
+ type="checkbox"
151
+ id="rememberAdKey"
152
+ class="rounded border-gray-300 dark:border-gray-600"
153
+ >
154
+ <label for="rememberAdKey" class="text-sm text-gray-600 dark:text-gray-400">
155
+ Remember API key
156
+ </label>
157
+ </div>
158
+ <div class="flex space-x-3">
159
+ <button
160
+ type="button"
161
+ onclick="showView('selectionView')"
162
+ class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
163
+ >
164
+ Back
165
+ </button>
166
+ <button
167
+ type="submit"
168
+ class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
169
+ >
170
+ Start Test
171
+ </button>
172
+ </div>
173
+ </form>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- Testing View -->
178
+ <div id="testingView" class="max-w-4xl mx-auto space-y-6 hidden">
179
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
180
+ <!-- User Info Section -->
181
+ <div id="userInfo" class="mb-6 hidden">
182
+ <!-- User info will be populated dynamically -->
183
+ </div>
184
+
185
+ <!-- Progress Section -->
186
+ <div class="space-y-4">
187
+ <div class="text-center text-gray-600 dark:text-gray-300" id="currentLocation">
188
+ Initializing test...
189
+ </div>
190
+ <div class="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
191
+ <div class="h-full bg-blue-500 animate-progress" id="progressBar"></div>
192
+ </div>
193
+ </div>
194
+
195
+ <!-- Results Container -->
196
+ <div id="resultsContainer" class="mt-8">
197
+ <!-- Results will be populated dynamically -->
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <!-- Results View -->
203
+ <div id="resultsView" class="max-w-4xl mx-auto space-y-6 hidden">
204
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
205
+ <div class="space-y-6">
206
+ <!-- Summary Section -->
207
+ <div class="border-b border-gray-200 dark:border-gray-700 pb-4">
208
+ <h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Test Summary</h3>
209
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
210
+ <div class="space-y-1">
211
+ <div class="text-sm text-gray-500 dark:text-gray-400">Fastest Server</div>
212
+ <div id="fastestServer" class="font-medium text-gray-900 dark:text-white"></div>
213
+ </div>
214
+ <div class="space-y-1">
215
+ <div class="text-sm text-gray-500 dark:text-gray-400">Top Speed</div>
216
+ <div id="topSpeed" class="font-medium text-green-500"></div>
217
+ </div>
218
+ <div class="space-y-1">
219
+ <div class="text-sm text-gray-500 dark:text-gray-400">Average Speed</div>
220
+ <div id="avgSpeed" class="font-medium text-blue-500"></div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Detailed Results -->
226
+ <div id="finalResults" class="space-y-4">
227
+ <!-- Results will be populated here -->
228
+ </div>
229
+ </div>
230
+ </div>
231
+
232
+ <div class="text-center mt-6">
233
+ <button onclick="resetTest()" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
234
+ Test Another Provider
235
+ </button>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- Error View -->
240
+ <div id="errorView" class="max-w-4xl mx-auto space-y-6 hidden">
241
+ <div class="bg-red-50 dark:bg-red-900/50 border-l-4 border-red-500 p-4 rounded">
242
+ <div class="flex">
243
+ <div class="flex-shrink-0">
244
+ <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
245
+ <path fill-rule="evenodd"
246
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
247
+ clip-rule="evenodd"/>
248
+ </svg>
249
+ </div>
250
+ <div class="ml-3">
251
+ <p class="text-sm text-red-700 dark:text-red-200" id="errorMessage"></p>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ <div class="text-center">
257
+ <button onclick="resetTest()"
258
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:hover:bg-blue-500 transition-colors duration-200">
259
+ Try Again
260
+ </button>
261
+ </div>
262
+ </div>
263
+
264
+ </div>
265
+ </main>
266
+
267
+ <script>
268
+ // Config and State
269
+ const STATE = {
270
+ apiPassword: localStorage.getItem('speedtest_api_password'),
271
+ adApiKey: localStorage.getItem('ad_api_key'),
272
+ currentTaskId: null,
273
+ resultsCount: 0,
274
+ };
275
+
276
+ // Theme handling
277
+ function setTheme(theme) {
278
+ document.documentElement.classList.toggle('dark', theme === 'dark');
279
+ localStorage.theme = theme;
280
+ }
281
+
282
+ function initTheme() {
283
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
284
+ setTheme(localStorage.theme || (prefersDark ? 'dark' : 'light'));
285
+ }
286
+
287
+ // View management
288
+ function showView(viewId) {
289
+ document.querySelectorAll('#views-container > div').forEach(view => {
290
+ view.classList.toggle('hidden', view.id !== viewId);
291
+ });
292
+ }
293
+
294
+
295
+ function createErrorResult(location, data) {
296
+ return `
297
+ <div class="py-4">
298
+ <div class="flex justify-between items-center">
299
+ <div>
300
+ <span class="font-medium text-gray-800 dark:text-white">${location}</span>
301
+ <span class="ml-2 text-sm text-gray-500 dark:text-gray-400">${data.server_name || ''}</span>
302
+ </div>
303
+ <span class="text-sm text-red-500 dark:text-red-400">
304
+ Failed
305
+ </span>
306
+ </div>
307
+ <div class="mt-1 text-sm text-red-400 dark:text-red-300">
308
+ ${data.error || 'Test failed'}
309
+ </div>
310
+ <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
311
+ Server: ${data.server_url}
312
+ </div>
313
+ </div>
314
+ `;
315
+ }
316
+
317
+
318
+ function formatBytes(bytes) {
319
+ const units = ['B', 'KB', 'MB', 'GB'];
320
+ let value = bytes;
321
+ let unitIndex = 0;
322
+
323
+ while (value >= 1024 && unitIndex < units.length - 1) {
324
+ value /= 1024;
325
+ unitIndex++;
326
+ }
327
+
328
+ return `${value.toFixed(2)} ${units[unitIndex]}`;
329
+ }
330
+
331
+ function handleAuthError() {
332
+ localStorage.removeItem('speedtest_api_password');
333
+ STATE.apiPassword = null;
334
+ showError('Authentication failed. Please check your API password.');
335
+ }
336
+
337
+ function showError(message) {
338
+ document.getElementById('errorMessage').textContent = message;
339
+ showView('errorView');
340
+ }
341
+
342
+ function resetTest() {
343
+ window.location.reload();
344
+ }
345
+
346
+ function showAllDebridSetup() {
347
+ showView('allDebridSetupView');
348
+ }
349
+
350
+ async function startTest(provider) {
351
+ if (provider === 'all_debrid' && !STATE.adApiKey) {
352
+ showAllDebridSetup();
353
+ return;
354
+ }
355
+
356
+ showView('testingView');
357
+ initializeResultsContainer();
358
+
359
+ try {
360
+ const params = new URLSearchParams({provider});
361
+ const headers = {'api_password': STATE.apiPassword};
362
+
363
+ if (provider === 'all_debrid' && STATE.adApiKey) {
364
+ headers['api_key'] = STATE.adApiKey;
365
+ }
366
+
367
+ const response = await fetch(`/speedtest/start?${params}`, {
368
+ method: 'POST',
369
+ headers
370
+ });
371
+
372
+ if (!response.ok) {
373
+ if (response.status === 403) {
374
+ handleAuthError();
375
+ return;
376
+ }
377
+ throw new Error('Failed to start speed test');
378
+ }
379
+
380
+ const {task_id} = await response.json();
381
+ STATE.currentTaskId = task_id;
382
+ await pollResults(task_id);
383
+ } catch (error) {
384
+ showError(error.message);
385
+ }
386
+ }
387
+
388
+ function initializeResultsContainer() {
389
+ const container = document.getElementById('resultsContainer');
390
+ container.innerHTML = `
391
+ <div class="space-y-4">
392
+ <div id="locationResults" class="divide-y divide-gray-200 dark:divide-gray-700">
393
+ <!-- Results will be populated here -->
394
+ </div>
395
+ <div id="summaryStats" class="hidden pt-4">
396
+ <!-- Summary stats will be populated here -->
397
+ </div>
398
+ </div>
399
+ `;
400
+ }
401
+
402
+ async function pollResults(taskId) {
403
+ try {
404
+ while (true) {
405
+ const response = await fetch(`/speedtest/results/${taskId}`, {
406
+ headers: {'api_password': STATE.apiPassword}
407
+ });
408
+
409
+ if (!response.ok) {
410
+ if (response.status === 403) {
411
+ handleAuthError();
412
+ return;
413
+ }
414
+ throw new Error('Failed to fetch results');
415
+ }
416
+
417
+ const data = await response.json();
418
+
419
+ if (data.status === 'failed') {
420
+ throw new Error('Speed test failed');
421
+ }
422
+
423
+ updateUI(data);
424
+
425
+ if (data.status === 'completed') {
426
+ showFinalResults(data);
427
+ break;
428
+ }
429
+
430
+ await new Promise(resolve => setTimeout(resolve, 2000));
431
+ }
432
+ } catch (error) {
433
+ showError(error.message);
434
+ }
435
+ }
436
+
437
+ function updateUI(data) {
438
+ if (data.user_info) {
439
+ updateUserInfo(data.user_info);
440
+ }
441
+
442
+ if (data.current_location) {
443
+ document.getElementById('currentLocation').textContent =
444
+ `Testing server ${data.current_location}...`;
445
+ }
446
+
447
+ updateResults(data.results);
448
+ }
449
+
450
+ function updateUserInfo(userInfo) {
451
+ const userInfoDiv = document.getElementById('userInfo');
452
+ userInfoDiv.innerHTML = `
453
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
454
+ <div class="space-y-1">
455
+ <div class="text-sm text-gray-500 dark:text-gray-400">IP Address</div>
456
+ <div class="font-medium text-gray-900 dark:text-white">${userInfo.ip}</div>
457
+ </div>
458
+ <div class="space-y-1">
459
+ <div class="text-sm text-gray-500 dark:text-gray-400">ISP</div>
460
+ <div class="font-medium text-gray-900 dark:text-white">${userInfo.isp}</div>
461
+ </div>
462
+ <div class="space-y-1">
463
+ <div class="text-sm text-gray-500 dark:text-gray-400">Country</div>
464
+ <div class="font-medium text-gray-900 dark:text-white">${userInfo.country?.toUpperCase()}</div>
465
+ </div>
466
+ </div>
467
+ `;
468
+ userInfoDiv.classList.remove('hidden');
469
+ }
470
+
471
+ function updateResults(results) {
472
+ const container = document.getElementById('resultsContainer');
473
+ const validResults = Object.entries(results)
474
+ .filter(([, data]) => data.result !== null && !data.error)
475
+ .sort(([, a], [, b]) => (b.result.speed_mbps) - (a.result.speed_mbps));
476
+
477
+ const failedResults = Object.entries(results)
478
+ .filter(([, data]) => data.error || data.result === null);
479
+
480
+ // Generate HTML for results
481
+ const resultsHTML = [
482
+ // Successful results
483
+ ...validResults.map(([location, data]) => createSuccessResult(location, data)),
484
+ // Failed results
485
+ ...failedResults.map(([location, data]) => createErrorResult(location, data))
486
+ ].join('');
487
+
488
+ container.innerHTML = `
489
+ <div class="space-y-4">
490
+ <!-- Summary Stats -->
491
+ ${createSummaryStats(validResults)}
492
+ <!-- Individual Results -->
493
+ <div class="mt-6 divide-y divide-gray-200 dark:divide-gray-700">
494
+ ${resultsHTML}
495
+ </div>
496
+ </div>
497
+ `;
498
+ }
499
+
500
+
501
+ function createSummaryStats(validResults) {
502
+ if (validResults.length === 0) return '';
503
+
504
+ const speeds = validResults.map(([, data]) => data.result.speed_mbps);
505
+ const maxSpeed = Math.max(...speeds);
506
+ const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
507
+ const fastestServer = validResults[0][0]; // First server after sorting
508
+
509
+ return `
510
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
511
+ <div class="text-center">
512
+ <div class="text-sm text-gray-500 dark:text-gray-400">Fastest Server</div>
513
+ <div class="font-medium text-gray-900 dark:text-white">${fastestServer}</div>
514
+ </div>
515
+ <div class="text-center">
516
+ <div class="text-sm text-gray-500 dark:text-gray-400">Top Speed</div>
517
+ <div class="font-medium text-green-500">${maxSpeed.toFixed(2)} Mbps</div>
518
+ </div>
519
+ <div class="text-center">
520
+ <div class="text-sm text-gray-500 dark:text-gray-400">Average Speed</div>
521
+ <div class="font-medium text-blue-500">${avgSpeed.toFixed(2)} Mbps</div>
522
+ </div>
523
+ </div>
524
+ `;
525
+ }
526
+
527
+ function createSuccessResult(location, data) {
528
+ const speedClass = getSpeedClass(data.result.speed_mbps);
529
+ return `
530
+ <div class="py-4">
531
+ <div class="flex justify-between items-center">
532
+ <div>
533
+ <span class="font-medium text-gray-800 dark:text-white">${location}</span>
534
+ <span class="ml-2 text-sm text-gray-500 dark:text-gray-400">${data.server_name || ''}</span>
535
+ </div>
536
+ <span class="text-lg font-semibold ${speedClass}">${data.result.speed_mbps.toFixed(2)} Mbps</span>
537
+ </div>
538
+ <div class="mt-1 text-sm text-gray-500 dark:text-gray-400">
539
+ Duration: ${data.result.duration.toFixed(2)}s •
540
+ Data: ${formatBytes(data.result.data_transferred)}
541
+ </div>
542
+ <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
543
+ Server: ${data.server_url}
544
+ </div>
545
+ </div>
546
+ `;
547
+ }
548
+
549
+ function getSpeedClass(speed) {
550
+ if (speed >= 10) return 'text-green-500 dark:text-green-400';
551
+ if (speed >= 5) return 'text-blue-500 dark:text-blue-400';
552
+ if (speed >= 2) return 'text-yellow-500 dark:text-yellow-400';
553
+ return 'text-red-500 dark:text-red-400';
554
+ }
555
+
556
+ function showFinalResults(data) {
557
+ // Stop the progress animation
558
+ document.querySelector('#progressBar').style.animation = 'none';
559
+
560
+ // Update the final results view
561
+ const validResults = Object.entries(data.results)
562
+ .filter(([, data]) => data.result !== null && !data.error)
563
+ .sort(([, a], [, b]) => (b.result.speed_mbps) - (a.result.speed_mbps));
564
+
565
+ const failedResults = Object.entries(data.results)
566
+ .filter(([, data]) => data.error || data.result === null);
567
+
568
+ // Update summary stats
569
+ if (validResults.length > 0) {
570
+ const speeds = validResults.map(([, data]) => data.result.speed_mbps);
571
+ const maxSpeed = Math.max(...speeds);
572
+ const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
573
+ const fastestServer = validResults[0][0];
574
+
575
+ document.getElementById('fastestServer').textContent = fastestServer;
576
+ document.getElementById('topSpeed').textContent = `${maxSpeed.toFixed(2)} Mbps`;
577
+ document.getElementById('avgSpeed').textContent = `${avgSpeed.toFixed(2)} Mbps`;
578
+ }
579
+
580
+ // Generate detailed results HTML
581
+ const finalResultsHTML = `
582
+ ${validResults.map(([location, data]) => `
583
+ <div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
584
+ <div class="flex justify-between items-center">
585
+ <div>
586
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white">${location}</h3>
587
+ <p class="text-sm text-gray-500 dark:text-gray-400">${data.server_name || ''}</p>
588
+ </div>
589
+ <div class="text-right">
590
+ <p class="text-2xl font-bold ${getSpeedClass(data.result.speed_mbps)}">
591
+ ${data.result.speed_mbps.toFixed(2)} Mbps
592
+ </p>
593
+ <p class="text-sm text-gray-500 dark:text-gray-400">
594
+ ${data.result.duration.toFixed(2)}s • ${formatBytes(data.result.data_transferred)}
595
+ </p>
596
+ </div>
597
+ </div>
598
+ <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
599
+ ${data.server_url}
600
+ </div>
601
+ </div>
602
+ `).join('')}
603
+
604
+ ${failedResults.length > 0 ? `
605
+ <div class="mt-6">
606
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Failed Tests</h3>
607
+ ${failedResults.map(([location, data]) => `
608
+ <div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 mb-4">
609
+ <div class="flex justify-between items-center">
610
+ <div>
611
+ <h4 class="font-medium text-red-800 dark:text-red-200">
612
+ ${location} ${data.server_name ? `(${data.server_name})` : ''}
613
+ </h4>
614
+ <p class="text-sm text-red-700 dark:text-red-300">
615
+ ${data.error || 'Test failed'}
616
+ </p>
617
+ <p class="text-xs text-red-600 dark:text-red-400 mt-1">
618
+ ${data.server_url}
619
+ </p>
620
+ </div>
621
+ </div>
622
+ </div>
623
+ `).join('')}
624
+ </div>
625
+ ` : ''}
626
+ `;
627
+
628
+ document.getElementById('finalResults').innerHTML = finalResultsHTML;
629
+
630
+ // If we have user info from AllDebrid, copy it to the final view
631
+ const userInfoDiv = document.getElementById('userInfo');
632
+ if (!userInfoDiv.classList.contains('hidden') && data.user_info) {
633
+ const userInfoContent = userInfoDiv.innerHTML;
634
+ document.getElementById('finalResults').insertAdjacentHTML('afterbegin', `
635
+ <div class="mb-6">
636
+ ${userInfoContent}
637
+ </div>
638
+ `);
639
+ }
640
+
641
+ // Show the final results view
642
+ showView('resultsView');
643
+ }
644
+
645
+ function initializeView() {
646
+ initTheme();
647
+ showView(STATE.apiPassword ? 'selectionView' : 'passwordView');
648
+ }
649
+
650
+ function initializeFormHandlers() {
651
+ // Password form handler
652
+ document.getElementById('passwordForm').addEventListener('submit', (e) => {
653
+ e.preventDefault();
654
+ const password = document.getElementById('apiPassword').value;
655
+ const remember = document.getElementById('rememberPassword').checked;
656
+
657
+ if (remember) {
658
+ localStorage.setItem('speedtest_api_password', password);
659
+ }
660
+ STATE.apiPassword = password;
661
+ showView('selectionView');
662
+ });
663
+
664
+ // AllDebrid form handler
665
+ document.getElementById('allDebridForm').addEventListener('submit', async (e) => {
666
+ e.preventDefault();
667
+ const apiKey = document.getElementById('adApiKey').value;
668
+ const remember = document.getElementById('rememberAdKey').checked;
669
+
670
+ if (remember) {
671
+ localStorage.setItem('ad_api_key', apiKey);
672
+ }
673
+ STATE.adApiKey = apiKey;
674
+ await startTest('all_debrid');
675
+ });
676
+ }
677
+
678
+ document.addEventListener('DOMContentLoaded', () => {
679
+ initializeView();
680
+ initializeFormHandlers();
681
+ });
682
+
683
+ // Theme Toggle Event Listener
684
+ document.getElementById('themeToggle').addEventListener('click', () => {
685
+ setTheme(document.documentElement.classList.contains('dark') ? 'light' : 'dark');
686
+ });
687
+ </script>
688
+ </body>
689
+ </html>
mediaflow_proxy/static/speedtest_progress.html DELETED
@@ -1,140 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Speedtest</title>
7
- <style>
8
- body.light-mode {
9
- font-family: Arial, sans-serif;
10
- display: flex;
11
- justify-content: center;
12
- align-items: center;
13
- height: 100vh;
14
- margin: 0;
15
- background-color: #f4f4f9;
16
- color: #333;
17
- }
18
- body.dark-mode {
19
- font-family: Arial, sans-serif;
20
- display: flex;
21
- justify-content: center;
22
- align-items: center;
23
- height: 100vh;
24
- margin: 0;
25
- background-color: #121212;
26
- color: #ffffff;
27
- }
28
- .container {
29
- text-align: center;
30
- }
31
- h1 {
32
- font-size: 24px;
33
- }
34
- .progress-bar {
35
- width: 80%;
36
- margin: 20px auto;
37
- height: 20px;
38
- background-color: #e0e0e0;
39
- border-radius: 10px;
40
- overflow: hidden;
41
- position: relative;
42
- }
43
- .progress-bar::after {
44
- content: "";
45
- position: absolute;
46
- top: 0;
47
- left: 0;
48
- width: 0%;
49
- height: 100%;
50
- background-color: #3498db;
51
- animation: progress 180s linear forwards;
52
- }
53
- @keyframes progress {
54
- 0% { width: 0%; }
55
- 100% { width: 100%; }
56
- }
57
- .toggle-switch {
58
- position: absolute;
59
- top: 10px;
60
- right: 10px;
61
- }
62
- </style>
63
- <script>
64
- const urlParams = new URLSearchParams(window.location.search);
65
- const taskId = urlParams.get("task_id");
66
- if (!taskId || !/^[a-zA-Z0-9-_]+$/.test(taskId)) {
67
- window.location.href = "/speedtest";
68
- }
69
-
70
- let statusCheckTimeout;
71
- let retryCount = 0;
72
- const MAX_RETRIES = 5;
73
-
74
- async function checkStatus() {
75
- try {
76
- const controller = new AbortController();
77
- const timeoutId = setTimeout(() => controller.abort(), 5000);
78
- const response = await fetch(`/speedtest/results/${taskId}`, {
79
- signal: controller.signal
80
- });
81
- clearTimeout(timeoutId);
82
-
83
- if (!response.ok) {
84
- throw new Error('Network response was not ok');
85
- }
86
- const data = await response.json();
87
- console.log("Fetched data:", data);
88
- retryCount = 0;
89
-
90
- if (data && data.message && data.message.includes("still running")) {
91
- console.log("Test still running, polling again...");
92
- statusCheckTimeout = setTimeout(checkStatus, 5000);
93
- } else {
94
- console.log("Test complete, redirecting after a short delay...");
95
- setTimeout(() => {
96
- window.location.href = `/speedtest/results/${taskId}`;
97
- }, 2000);
98
- }
99
- } catch (error) {
100
- console.error("Error fetching status:", error);
101
- retryCount++;
102
- if (retryCount < MAX_RETRIES) {
103
- statusCheckTimeout = setTimeout(checkStatus, 5000);
104
- } else {
105
- alert("Failed to check status after multiple attempts. Please refresh the page.");
106
- }
107
- }
108
- }
109
-
110
- // Cleanup on page unload
111
- window.addEventListener('unload', () => {
112
- if (statusCheckTimeout) {
113
- clearTimeout(statusCheckTimeout);
114
- }
115
- });
116
-
117
- // Start the first status check after 120 seconds (120000 milliseconds)
118
- setTimeout(checkStatus, 120000);
119
-
120
- // Toggle dark mode
121
- function toggleDarkMode() {
122
- const body = document.body;
123
- body.classList.toggle('dark-mode');
124
- body.classList.toggle('light-mode');
125
- }
126
- </script>
127
- </head>
128
- <body class="light-mode">
129
- <div class="toggle-switch">
130
- <label for="darkModeToggle" class="switch">
131
- <input type="checkbox" id="darkModeToggle" onclick="toggleDarkMode()" aria-label="Toggle dark mode">
132
- <span class="slider">Dark Mode</span>
133
- </label>
134
- </div>
135
- <div class="container">
136
- <h1>Speedtest in progress... Please wait up to 3 minutes.</h1>
137
- <div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div>
138
- </div>
139
- </body>
140
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mediaflow_proxy/utils/http_utils.py CHANGED
@@ -245,7 +245,7 @@ async def download_file_with_retry(
245
 
246
  async def request_with_retry(
247
  method: str, url: str, headers: dict, timeout: float = 10.0, use_request_proxy: bool = True, **kwargs
248
- ):
249
  """
250
  Sends an HTTP request with retry logic.
251
 
 
245
 
246
  async def request_with_retry(
247
  method: str, url: str, headers: dict, timeout: float = 10.0, use_request_proxy: bool = True, **kwargs
248
+ ) -> httpx.Response:
249
  """
250
  Sends an HTTP request with retry logic.
251