282 lines
11 KiB
Python
282 lines
11 KiB
Python
"""
|
|
MeTube client for REST API and WebSocket communication
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Dict, Any, Optional, Callable
|
|
import aiohttp
|
|
import socketio
|
|
from datetime import datetime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MeTubeClient:
|
|
"""Client for communicating with MeTube service"""
|
|
|
|
def __init__(self, base_url: str, timeout: int = 30):
|
|
self.base_url = base_url.rstrip("/")
|
|
self.timeout = timeout
|
|
self.session: aiohttp.ClientSession | None = None
|
|
self.socket_client: socketio.AsyncClient | None = None
|
|
self._event_callbacks: Dict[str, Callable] = {}
|
|
self._connected = False
|
|
self._websocket_connected = False
|
|
|
|
async def connect(self) -> None:
|
|
"""Connect to MeTube service"""
|
|
try:
|
|
# Create HTTP session
|
|
self.session = aiohttp.ClientSession(
|
|
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
headers={"User-Agent": "PlaylistMonitor/0.1.0"}
|
|
)
|
|
|
|
# Test HTTP connection
|
|
await self.health_check()
|
|
|
|
# Connect to WebSocket
|
|
await self._connect_websocket()
|
|
|
|
self._connected = True
|
|
logger.info(f"Successfully connected to MeTube at {self.base_url}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to MeTube: {e}")
|
|
raise
|
|
|
|
async def disconnect(self) -> None:
|
|
"""Disconnect from MeTube service"""
|
|
try:
|
|
# Disconnect WebSocket
|
|
if self.socket_client and self._websocket_connected:
|
|
await self.socket_client.disconnect()
|
|
self._websocket_connected = False
|
|
|
|
# Close HTTP session
|
|
if self.session:
|
|
await self.session.close()
|
|
self.session = None
|
|
|
|
self._connected = False
|
|
logger.info("Disconnected from MeTube service")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error disconnecting from MeTube: {e}")
|
|
|
|
async def health_check(self) -> bool:
|
|
"""Check if MeTube service is healthy"""
|
|
try:
|
|
if not self.session:
|
|
return False
|
|
|
|
async with self.session.get(f"{self.base_url}/info") as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
logger.debug(f"MeTube health check: {data}")
|
|
return True
|
|
else:
|
|
logger.warning(f"MeTube health check failed with status {response.status}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"MeTube health check failed: {e}")
|
|
return False
|
|
|
|
async def add_download(
|
|
self,
|
|
url: str,
|
|
quality: str = "best",
|
|
format: str = "mp4",
|
|
folder: Optional[str] = None,
|
|
custom_name_prefix: Optional[str] = None,
|
|
auto_start: bool = True,
|
|
playlist_item_limit: Optional[int] = None
|
|
) -> Dict[str, Any]:
|
|
"""Add a download to MeTube"""
|
|
try:
|
|
if not self.session:
|
|
raise RuntimeError("Not connected to MeTube")
|
|
|
|
payload = {
|
|
"url": url,
|
|
"quality": quality,
|
|
"format": format,
|
|
"auto_start": auto_start,
|
|
}
|
|
|
|
if folder:
|
|
payload["folder"] = folder
|
|
if custom_name_prefix:
|
|
payload["custom_name_prefix"] = custom_name_prefix
|
|
if playlist_item_limit:
|
|
payload["playlist_item_limit"] = playlist_item_limit
|
|
|
|
logger.debug(f"Adding download to MeTube: {payload}")
|
|
|
|
async with self.session.post(
|
|
f"{self.base_url}/add",
|
|
json=payload
|
|
) as response:
|
|
if response.status == 200:
|
|
result = await response.json()
|
|
logger.info(f"Successfully added download: {result}")
|
|
return result
|
|
else:
|
|
error_text = await response.text()
|
|
raise RuntimeError(f"Failed to add download: {response.status} - {error_text}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding download to MeTube: {e}")
|
|
raise
|
|
|
|
async def delete_download(self, download_id: str) -> Dict[str, Any]:
|
|
"""Delete/cancel a download"""
|
|
try:
|
|
if not self.session:
|
|
raise RuntimeError("Not connected to MeTube")
|
|
|
|
payload = {"ids": [download_id]}
|
|
|
|
async with self.session.post(
|
|
f"{self.base_url}/delete",
|
|
json=payload
|
|
) as response:
|
|
if response.status == 200:
|
|
result = await response.json()
|
|
logger.info(f"Successfully deleted download: {download_id}")
|
|
return result
|
|
else:
|
|
error_text = await response.text()
|
|
raise RuntimeError(f"Failed to delete download: {response.status} - {error_text}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error deleting download from MeTube: {e}")
|
|
raise
|
|
|
|
async def get_history(self) -> Dict[str, Any]:
|
|
"""Get download history from MeTube"""
|
|
try:
|
|
if not self.session:
|
|
raise RuntimeError("Not connected to MeTube")
|
|
|
|
async with self.session.get(f"{self.base_url}/history") as response:
|
|
if response.status == 200:
|
|
result = await response.json()
|
|
return result
|
|
else:
|
|
error_text = await response.text()
|
|
raise RuntimeError(f"Failed to get history: {response.status} - {error_text}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting history from MeTube: {e}")
|
|
raise
|
|
|
|
async def get_download_info(self, download_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Get information about a specific download"""
|
|
try:
|
|
history = await self.get_history()
|
|
|
|
# Search in completed downloads
|
|
for download in history.get("completed", []):
|
|
if download.get("id") == download_id:
|
|
return download
|
|
|
|
# Search in pending downloads
|
|
for download in history.get("pending", []):
|
|
if download.get("id") == download_id:
|
|
return download
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting download info: {e}")
|
|
return None
|
|
|
|
def register_event_callback(self, event: str, callback: Callable) -> None:
|
|
"""Register a callback for WebSocket events"""
|
|
self._event_callbacks[event] = callback
|
|
logger.debug(f"Registered callback for event: {event}")
|
|
|
|
async def _connect_websocket(self) -> None:
|
|
"""Connect to MeTube WebSocket"""
|
|
try:
|
|
self.socket_client = socketio.AsyncClient()
|
|
|
|
# Register event handlers
|
|
@self.socket_client.on("connect")
|
|
async def on_connect():
|
|
logger.info("Connected to MeTube WebSocket")
|
|
self._websocket_connected = True
|
|
|
|
@self.socket_client.on("disconnect")
|
|
async def on_disconnect():
|
|
logger.info("Disconnected from MeTube WebSocket")
|
|
self._websocket_connected = False
|
|
|
|
@self.socket_client.on("added")
|
|
async def on_added(data):
|
|
logger.debug(f"WebSocket event - added: {data}")
|
|
await self._handle_event("added", data)
|
|
|
|
@self.socket_client.on("updated")
|
|
async def on_updated(data):
|
|
logger.debug(f"WebSocket event - updated: {data}")
|
|
await self._handle_event("updated", data)
|
|
|
|
@self.socket_client.on("completed")
|
|
async def on_completed(data):
|
|
logger.debug(f"WebSocket event - completed: {data}")
|
|
await self._handle_event("completed", data)
|
|
|
|
@self.socket_client.on("canceled")
|
|
async def on_canceled(data):
|
|
logger.debug(f"WebSocket event - canceled: {data}")
|
|
await self._handle_event("canceled", data)
|
|
|
|
@self.socket_client.on("cleared")
|
|
async def on_cleared(data):
|
|
logger.debug(f"WebSocket event - cleared: {data}")
|
|
await self._handle_event("cleared", data)
|
|
|
|
# Connect to WebSocket
|
|
ws_url = self.base_url.replace("http://", "ws://").replace("https://", "wss://")
|
|
await self.socket_client.connect(ws_url)
|
|
|
|
# Start background task to keep connection alive
|
|
asyncio.create_task(self._keep_websocket_alive())
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to MeTube WebSocket: {e}")
|
|
# Don't fail the entire connection if WebSocket fails
|
|
self._websocket_connected = False
|
|
|
|
async def _handle_event(self, event: str, data: Dict[str, Any]) -> None:
|
|
"""Handle WebSocket events"""
|
|
if event in self._event_callbacks:
|
|
try:
|
|
await self._event_callbacks[event](data)
|
|
except Exception as e:
|
|
logger.error(f"Error handling WebSocket event {event}: {e}")
|
|
|
|
async def _keep_websocket_alive(self) -> None:
|
|
"""Keep WebSocket connection alive"""
|
|
while self._websocket_connected and self.socket_client:
|
|
try:
|
|
await asyncio.sleep(30) # Ping every 30 seconds
|
|
if self.socket_client:
|
|
await self.socket_client.emit("ping")
|
|
except Exception as e:
|
|
logger.error(f"Error keeping WebSocket alive: {e}")
|
|
break
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
"""Check if client is connected"""
|
|
return self._connected
|
|
|
|
@property
|
|
def is_websocket_connected(self) -> bool:
|
|
"""Check if WebSocket is connected"""
|
|
return self._websocket_connected |