stremio uses webdav now
This commit is contained in:
@@ -14,6 +14,9 @@ from typing import List, Optional, Callable
|
|||||||
from .provider import StorageProvider, VideoMetadata
|
from .provider import StorageProvider, VideoMetadata
|
||||||
from .leader_client import LeaderClient, LeaderLockInfo
|
from .leader_client import LeaderClient, LeaderLockInfo
|
||||||
|
|
||||||
|
# Import webdav_client to configure it after leader discovery
|
||||||
|
import webdav_client
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import redis
|
import redis
|
||||||
REDIS_AVAILABLE = True
|
REDIS_AVAILABLE = True
|
||||||
@@ -81,6 +84,10 @@ class LeaderStorage(StorageProvider):
|
|||||||
info = self._leader_client.wait_for_leader(30000)
|
info = self._leader_client.wait_for_leader(30000)
|
||||||
print(f"[LeaderStorage] Connecting to leader at {info.redis_url}...")
|
print(f"[LeaderStorage] Connecting to leader at {info.redis_url}...")
|
||||||
self._connect_to_redis(info.redis_url)
|
self._connect_to_redis(info.redis_url)
|
||||||
|
|
||||||
|
# Configure WebDAV client with leader's WebDAV URL
|
||||||
|
if info.webdav_url:
|
||||||
|
webdav_client.configure(info.webdav_url)
|
||||||
except TimeoutError as e:
|
except TimeoutError as e:
|
||||||
print(f"[LeaderStorage] Failed to find leader: {e}")
|
print(f"[LeaderStorage] Failed to find leader: {e}")
|
||||||
return
|
return
|
||||||
@@ -122,6 +129,10 @@ class LeaderStorage(StorageProvider):
|
|||||||
info = self._leader_client.wait_for_leader(30000)
|
info = self._leader_client.wait_for_leader(30000)
|
||||||
print(f"[LeaderStorage] Reconnecting to leader at {info.redis_url}...")
|
print(f"[LeaderStorage] Reconnecting to leader at {info.redis_url}...")
|
||||||
self._connect_to_redis(info.redis_url)
|
self._connect_to_redis(info.redis_url)
|
||||||
|
|
||||||
|
# Reconfigure WebDAV client with new leader's WebDAV URL
|
||||||
|
if info.webdav_url:
|
||||||
|
webdav_client.configure(info.webdav_url)
|
||||||
except TimeoutError as e:
|
except TimeoutError as e:
|
||||||
print(f"[LeaderStorage] Failed to reconnect: {e}")
|
print(f"[LeaderStorage] Failed to reconnect: {e}")
|
||||||
|
|
||||||
|
|||||||
+42
-67
@@ -2,32 +2,44 @@
|
|||||||
WebDAV Client for Meta-Stremio
|
WebDAV Client for Meta-Stremio
|
||||||
|
|
||||||
Provides HTTP-based file access to meta-core's WebDAV server.
|
Provides HTTP-based file access to meta-core's WebDAV server.
|
||||||
This allows meta-stremio to access files without direct volume mounts.
|
Files are accessed exclusively via WebDAV - no local filesystem access.
|
||||||
|
|
||||||
Environment Variables:
|
The WebDAV URL is discovered automatically from the leader via LeaderStorage.
|
||||||
- META_CORE_WEBDAV_URL: Base URL for WebDAV access (e.g., http://meta-core/webdav)
|
|
||||||
- FILES_PATH: Local files path prefix to strip when building WebDAV URLs (default: /files)
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from typing import Optional, Generator
|
from typing import Optional, Generator
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Configuration
|
# Configuration - set dynamically by LeaderStorage after leader discovery
|
||||||
WEBDAV_URL = os.environ.get('META_CORE_WEBDAV_URL', '').rstrip('/')
|
WEBDAV_URL: str = ''
|
||||||
FILES_PATH = os.environ.get('FILES_PATH', '/files')
|
FILES_PATH = os.environ.get('FILES_PATH', '/files')
|
||||||
|
|
||||||
# Check if WebDAV is enabled
|
|
||||||
WEBDAV_ENABLED = bool(WEBDAV_URL)
|
|
||||||
|
|
||||||
if WEBDAV_ENABLED:
|
def configure(webdav_url: str) -> None:
|
||||||
print(f"[WebDAV] Using meta-core WebDAV at {WEBDAV_URL}")
|
"""
|
||||||
else:
|
Configure the WebDAV client with a URL discovered from leader.
|
||||||
print("[WebDAV] Not configured, using direct filesystem access")
|
|
||||||
|
Called by LeaderStorage after connecting to the leader.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webdav_url: The WebDAV base URL (e.g., http://meta-core/webdav)
|
||||||
|
"""
|
||||||
|
global WEBDAV_URL
|
||||||
|
|
||||||
|
if not webdav_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
WEBDAV_URL = webdav_url.rstrip('/')
|
||||||
|
print(f"[webdav-client] Configured: {WEBDAV_URL}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_configured() -> bool:
|
||||||
|
"""Check if WebDAV client is configured."""
|
||||||
|
return bool(WEBDAV_URL)
|
||||||
|
|
||||||
|
|
||||||
def to_webdav_url(file_path: str) -> Optional[str]:
|
def to_webdav_url(file_path: str) -> Optional[str]:
|
||||||
@@ -38,9 +50,9 @@ def to_webdav_url(file_path: str) -> Optional[str]:
|
|||||||
file_path: Local file path (e.g., /files/watch/movie.mp4)
|
file_path: Local file path (e.g., /files/watch/movie.mp4)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
WebDAV URL or None if WebDAV is not configured
|
WebDAV URL or None if not configured
|
||||||
"""
|
"""
|
||||||
if not WEBDAV_ENABLED:
|
if not WEBDAV_URL:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Strip the FILES_PATH prefix to get relative path
|
# Strip the FILES_PATH prefix to get relative path
|
||||||
@@ -65,17 +77,13 @@ def get_file_size(file_path: str) -> Optional[int]:
|
|||||||
Get file size via HTTP HEAD request to WebDAV.
|
Get file size via HTTP HEAD request to WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
File size in bytes, or None on error
|
File size in bytes, or None on error
|
||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
|
||||||
try:
|
|
||||||
return os.path.getsize(file_path)
|
|
||||||
except OSError:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -84,7 +92,7 @@ def get_file_size(file_path: str) -> Optional[int]:
|
|||||||
return int(response.headers.get('Content-Length', 0))
|
return int(response.headers.get('Content-Length', 0))
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WebDAV] HEAD error for {url}: {e}")
|
print(f"[webdav-client] HEAD error for {url}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -93,15 +101,14 @@ def file_exists(file_path: str) -> bool:
|
|||||||
Check if file exists via HTTP HEAD request to WebDAV.
|
Check if file exists via HTTP HEAD request to WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if file exists
|
True if file exists
|
||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
return False
|
||||||
return os.path.exists(file_path)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.head(url, timeout=30)
|
response = requests.head(url, timeout=30)
|
||||||
@@ -115,18 +122,13 @@ def read_file(file_path: str) -> Optional[bytes]:
|
|||||||
Read entire file from WebDAV.
|
Read entire file from WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
File contents as bytes, or None on error
|
File contents as bytes, or None on error
|
||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
|
||||||
try:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
return f.read()
|
|
||||||
except OSError:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -135,7 +137,7 @@ def read_file(file_path: str) -> Optional[bytes]:
|
|||||||
return response.content
|
return response.content
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WebDAV] GET error for {url}: {e}")
|
print(f"[webdav-client] GET error for {url}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -144,7 +146,7 @@ def read_range(file_path: str, start: int, end: int) -> Optional[bytes]:
|
|||||||
Read a byte range from a file via WebDAV.
|
Read a byte range from a file via WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
start: Start byte offset (inclusive)
|
start: Start byte offset (inclusive)
|
||||||
end: End byte offset (inclusive)
|
end: End byte offset (inclusive)
|
||||||
|
|
||||||
@@ -153,12 +155,6 @@ def read_range(file_path: str, start: int, end: int) -> Optional[bytes]:
|
|||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
|
||||||
try:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
f.seek(start)
|
|
||||||
return f.read(end - start + 1)
|
|
||||||
except OSError:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -168,7 +164,7 @@ def read_range(file_path: str, start: int, end: int) -> Optional[bytes]:
|
|||||||
return response.content
|
return response.content
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WebDAV] Range GET error for {url}: {e}")
|
print(f"[webdav-client] Range GET error for {url}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -177,7 +173,7 @@ def stream_file(file_path: str, chunk_size: int = 64 * 1024) -> Generator[bytes,
|
|||||||
Stream file content from WebDAV.
|
Stream file content from WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
chunk_size: Size of chunks to yield
|
chunk_size: Size of chunks to yield
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
@@ -185,17 +181,8 @@ def stream_file(file_path: str, chunk_size: int = 64 * 1024) -> Generator[bytes,
|
|||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
|
||||||
try:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
while True:
|
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
yield chunk
|
|
||||||
except OSError:
|
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, stream=True, timeout=300)
|
response = requests.get(url, stream=True, timeout=300)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -203,7 +190,7 @@ def stream_file(file_path: str, chunk_size: int = 64 * 1024) -> Generator[bytes,
|
|||||||
if chunk:
|
if chunk:
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WebDAV] Stream error for {url}: {e}")
|
print(f"[webdav-client] Stream error for {url}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def stream_range(file_path: str, start: int, end: int, file_size: int,
|
def stream_range(file_path: str, start: int, end: int, file_size: int,
|
||||||
@@ -212,10 +199,10 @@ def stream_range(file_path: str, start: int, end: int, file_size: int,
|
|||||||
Stream a byte range from a file via WebDAV.
|
Stream a byte range from a file via WebDAV.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: Local file path
|
file_path: File path
|
||||||
start: Start byte offset (inclusive)
|
start: Start byte offset (inclusive)
|
||||||
end: End byte offset (inclusive)
|
end: End byte offset (inclusive)
|
||||||
file_size: Total file size (for local fallback)
|
file_size: Total file size (unused, kept for API compatibility)
|
||||||
chunk_size: Size of chunks to yield
|
chunk_size: Size of chunks to yield
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
@@ -223,20 +210,8 @@ def stream_range(file_path: str, start: int, end: int, file_size: int,
|
|||||||
"""
|
"""
|
||||||
url = to_webdav_url(file_path)
|
url = to_webdav_url(file_path)
|
||||||
if not url:
|
if not url:
|
||||||
# Fall back to local filesystem
|
|
||||||
try:
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
f.seek(start)
|
|
||||||
remaining = end - start + 1
|
|
||||||
while remaining > 0:
|
|
||||||
chunk = f.read(min(chunk_size, remaining))
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
yield chunk
|
|
||||||
remaining -= len(chunk)
|
|
||||||
except OSError:
|
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
headers = {'Range': f'bytes={start}-{end}'}
|
headers = {'Range': f'bytes={start}-{end}'}
|
||||||
response = requests.get(url, headers=headers, stream=True, timeout=300)
|
response = requests.get(url, headers=headers, stream=True, timeout=300)
|
||||||
@@ -245,4 +220,4 @@ def stream_range(file_path: str, start: int, end: int, file_size: int,
|
|||||||
if chunk:
|
if chunk:
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WebDAV] Stream range error for {url}: {e}")
|
print(f"[webdav-client] Stream range error for {url}: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user