Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions mopidy_tidal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ def setup(self, registry):
from .backend import TidalBackend

registry.add("backend", TidalBackend)

from .http import TidalRpcHandler

registry.add('http:app', {
'name': 'tidal',
'factory': lambda config, core: [
(r'/rpc', TidalRpcHandler, {'core': core}),
],
})
6 changes: 6 additions & 0 deletions mopidy_tidal/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ def on_start(self):
else:
logger.info("[TIDAL BACKEND] Not using OAuth/PKCE or not logged in - token refresh not applicable")

# Start playlist periodic refresh if configured
self.playlists.start_periodic_refresh()

logger.info("[TIDAL BACKEND] Backend initialization complete")
logger.info("=" * 80)

Expand Down Expand Up @@ -351,6 +354,9 @@ def on_stop(self):
# Stop token refresh thread
self._stop_token_refresh_thread()

# Stop playlist periodic refresh thread
self.playlists.stop_periodic_refresh()

logger.info("[TIDAL BACKEND] ✓ Tidal backend stopped")

def _start_token_refresh_thread(self):
Expand Down
125 changes: 125 additions & 0 deletions mopidy_tidal/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import json
import logging
import tornado.web

logger = logging.getLogger(__name__)

class TidalRpcHandler(tornado.web.RequestHandler):
"""JSON-RPC 2.0 handler for Tidal-specific methods"""

def initialize(self, core):
self.core = core

def set_default_headers(self):
self.set_header("Content-Type", "application/json")
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.set_header("Access-Control-Allow-Headers", "Content-Type")

def options(self):
self.set_status(204)
self.finish()

def _get_tidal_backend(self):
"""Helper to get Tidal backend"""
try:
# Get the actual backends list from the proxy
backends = self.core.backends.get(timeout=1)

for backend in backends:
uri_schemes = backend.uri_schemes.get(timeout=1)
if uri_schemes and 'tidal' in uri_schemes:
return backend
except Exception as e:
logger.warning(f"Error finding Tidal backend: {e}")

return None

def _jsonrpc_error(self, request_id, code, message):
"""Create JSON-RPC error response"""
return {
'jsonrpc': '2.0',
'id': request_id,
'error': {'code': code, 'message': message}
}

def _jsonrpc_success(self, request_id, result):
"""Create JSON-RPC success response"""
return {
'jsonrpc': '2.0',
'id': request_id,
'result': result
}

async def post(self):
try:
request = json.loads(self.request.body.decode('utf-8'))
request_id = request.get('id')

# Validate JSON-RPC
if request.get('jsonrpc') != '2.0':
self.write(self._jsonrpc_error(
request_id, -32600, 'Invalid Request'
))
return

method = request.get('method')
params = request.get('params', {})

logger.info(f"Tidal RPC call: {method}")

# Get backend
backend = self._get_tidal_backend()
if not backend:
self.write(self._jsonrpc_error(
request_id, -32603, 'Tidal backend not available'
))
return

# Route methods
if method == 'tidal.refresh_playlists':
playlist_uris = params.get('uris')
if not playlist_uris:
self.write(self._jsonrpc_error(
request_id,
-32602,
'Missing required parameter: uris. '
'Use the core playlists.refresh() method if you want to refresh all playlists.'
))
return

backend.playlists.refresh(*playlist_uris).get()
self.write(self._jsonrpc_success(request_id, {
'refreshed': playlist_uris
}))

elif method == 'tidal.describe':
# List available methods
result = {
'methods': [
'tidal.refresh_playlists',
'tidal.describe'
],
'tidal.refresh_playlists': {
'description': 'Refresh specific playlists by URI',
'params': {
'uris': 'List of playlist URIs to refresh (required)'
}
}
}
self.write(self._jsonrpc_success(request_id, result))

else:
self.write(self._jsonrpc_error(
request_id, -32601, f'Method not found: {method}'
))

except json.JSONDecodeError:
self.write(self._jsonrpc_error(
None, -32700, 'Parse error'
))
except Exception as e:
logger.exception("Tidal RPC error")
self.write(self._jsonrpc_error(
request.get('id'), -32603, str(e)
))
Loading