Make OAuth callback ports configurable via environment variables

Hardcoded ports 8888/8889 conflict when SoulSync runs behind Gluetun or
other containers that claim those ports. Introduce SOULSYNC_SPOTIFY_CALLBACK_PORT
and SOULSYNC_TIDAL_CALLBACK_PORT env vars (defaulting to 8888/8889) so
users can remap without rebuilding the image.

docker-compose.yml exposes the vars with comments explaining how to keep
the port mappings in sync with the redirect URI in Settings → Connections.
This commit is contained in:
Broque Thomas 2026-04-14 14:08:31 -07:00
parent 6e405143a7
commit 1071b2ebe5
3 changed files with 39 additions and 30 deletions

View file

@ -1,3 +1,4 @@
import os
import requests import requests
import time import time
import re import re
@ -119,7 +120,8 @@ class TidalClient:
self.alt_base_url = "https://api.tidal.com/v1" # Alternative API base self.alt_base_url = "https://api.tidal.com/v1" # Alternative API base
self.auth_url = "https://login.tidal.com/authorize" self.auth_url = "https://login.tidal.com/authorize"
self.token_url = "https://auth.tidal.com/v1/oauth2/token" self.token_url = "https://auth.tidal.com/v1/oauth2/token"
self.redirect_uri = "http://127.0.0.1:8889/tidal/callback" # Default, will be updated from config _tidal_port = int(os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT', 8889))
self.redirect_uri = f"http://127.0.0.1:{_tidal_port}/tidal/callback" # Default, will be updated from config
self.session = requests.Session() self.session = requests.Session()
self.auth_server = None self.auth_server = None
self.auth_code = None self.auth_code = None
@ -347,7 +349,7 @@ class TidalClient:
pass # Suppress server logs pass # Suppress server logs
try: try:
port = 8889 port = int(os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT', 8889))
self.auth_server = HTTPServer(('localhost', port), CallbackHandler) self.auth_server = HTTPServer(('localhost', port), CallbackHandler)
server_thread = threading.Thread(target=self.auth_server.serve_forever) server_thread = threading.Thread(target=self.auth_server.serve_forever)
server_thread.daemon = True server_thread.daemon = True

View file

@ -20,10 +20,15 @@ services:
- SOULSYNC_CONFIG_PATH=/app/config/config.json - SOULSYNC_CONFIG_PATH=/app/config/config.json
# Set timezone (change to your timezone) # Set timezone (change to your timezone)
- TZ=America/New_York - TZ=America/New_York
# OAuth callback ports. If 8888/8889 conflict with another container (e.g. Gluetun),
# change the port numbers below AND update the matching port mappings in the ports section.
# Then update the redirect URI in SoulSync → Settings → Connections to match.
- SOULSYNC_SPOTIFY_CALLBACK_PORT=8888
- SOULSYNC_TIDAL_CALLBACK_PORT=8889
ports: ports:
- "8008:8008" # Main web app - "8008:8008" # Main web app
- "8888:8888" # Spotify OAuth callback - "8888:8888" # Spotify OAuth callback — keep in sync with SOULSYNC_SPOTIFY_CALLBACK_PORT above
- "8889:8889" # Tidal OAuth callback - "8889:8889" # Tidal OAuth callback — keep in sync with SOULSYNC_TIDAL_CALLBACK_PORT above
volumes: volumes:
# Persistent data volumes # Persistent data volumes
- ./config:/app/config - ./config:/app/config

View file

@ -2887,25 +2887,25 @@ class WebUIDownloadMonitor:
sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else '' sources_str = f' (tried {len(tried_sources)} source{"s" if len(tried_sources) != 1 else ""})' if tried_sources else ''
print(f"Task failed after 3 error retry attempts") print(f"Task failed after 3 error retry attempts")
task['status'] = 'failed' task['status'] = 'failed'
# Tidal-specific error: check if this was a quality issue. # Tidal-specific error: check if this was a quality issue.
# task['username'] is popped on error-retry (line ~2866) so we can't rely on it; # task['username'] is popped on error-retry (line ~2866) so we can't rely on it;
# used_sources keys are formatted as "{username}_{filename}", so startswith is exact. # used_sources keys are formatted as "{username}_{filename}", so startswith is exact.
is_tidal = any(s.startswith('tidal_') for s in tried_sources) is_tidal = any(s.startswith('tidal_') for s in tried_sources)
if is_tidal: if is_tidal:
tidal_quality = config_manager.get('tidal_download.quality', 'lossless') tidal_quality = config_manager.get('tidal_download.quality', 'lossless')
allow_fb = config_manager.get('tidal_download.allow_fallback', True) allow_fb = config_manager.get('tidal_download.allow_fallback', True)
if tidal_quality == 'hires' and not allow_fb: if tidal_quality == 'hires' and not allow_fb:
task['error_message'] = ( task['error_message'] = (
f'Tidal download failed for "{track_label}" — HiRes quality is unavailable for this track ' f'Tidal download failed for "{track_label}" — HiRes quality is unavailable for this track '
f'on your account or in your region. Enable "Quality Fallback" in Tidal settings to fall back to Lossless.' f'on your account or in your region. Enable "Quality Fallback" in Tidal settings to fall back to Lossless.'
) )
else: else:
task['error_message'] = ( task['error_message'] = (
f'Tidal download failed for "{track_label}"{sources_str}' f'Tidal download failed for "{track_label}"{sources_str}'
f'check Tidal authentication and quality settings.' f'check Tidal authentication and quality settings.'
) )
else: else:
task['error_message'] = f'Soulseek transfer errored 3 times for "{track_label}"{sources_str} — all sources failed or became unavailable' task['error_message'] = f'Soulseek transfer errored 3 times for "{track_label}"{sources_str} — all sources failed or became unavailable'
# CRITICAL: Notify batch manager so track is added to permanently_failed_tracks # CRITICAL: Notify batch manager so track is added to permanently_failed_tracks
batch_id = task.get('batch_id') batch_id = task.get('batch_id')
@ -49711,15 +49711,16 @@ def start_oauth_callback_servers():
# Start Spotify callback server # Start Spotify callback server
def run_spotify_server(): def run_spotify_server():
spotify_port = int(os.environ.get('SOULSYNC_SPOTIFY_CALLBACK_PORT', 8888))
try: try:
bind_addr = ('0.0.0.0', 8888) bind_addr = ('0.0.0.0', spotify_port)
spotify_server = HTTPServer(bind_addr, SpotifyCallbackHandler) spotify_server = HTTPServer(bind_addr, SpotifyCallbackHandler)
_oauth_logger.info(f"Spotify OAuth callback server listening on {bind_addr[0]}:{bind_addr[1]}") _oauth_logger.info(f"Spotify OAuth callback server listening on {bind_addr[0]}:{bind_addr[1]}")
print(f"Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}") print(f"Started Spotify OAuth callback server on {bind_addr[0]}:{bind_addr[1]}")
spotify_server.serve_forever() spotify_server.serve_forever()
except OSError as e: except OSError as e:
_oauth_logger.error(f"Failed to start Spotify callback server on port 8888: {e} — port may already be in use") _oauth_logger.error(f"Failed to start Spotify callback server on port {spotify_port}: {e} — port may already be in use")
print(f"Failed to start Spotify callback server on port 8888: {e}") print(f"Failed to start Spotify callback server on port {spotify_port}: {e}")
except Exception as e: except Exception as e:
_oauth_logger.error(f"Failed to start Spotify callback server: {e}") _oauth_logger.error(f"Failed to start Spotify callback server: {e}")
print(f"Failed to start Spotify callback server: {e}") print(f"Failed to start Spotify callback server: {e}")
@ -49789,9 +49790,10 @@ def start_oauth_callback_servers():
def run_tidal_server(): def run_tidal_server():
try: try:
tidal_server = HTTPServer(('0.0.0.0', 8889), TidalCallbackHandler) tidal_port = int(os.environ.get('SOULSYNC_TIDAL_CALLBACK_PORT', 8889))
print("Started Tidal OAuth callback server on port 8889") tidal_server = HTTPServer(('0.0.0.0', tidal_port), TidalCallbackHandler)
print(f"Tidal server listening on all interfaces, port 8889") print(f"Started Tidal OAuth callback server on port {tidal_port}")
print(f"Tidal server listening on all interfaces, port {tidal_port}")
tidal_server.serve_forever() tidal_server.serve_forever()
except Exception as e: except Exception as e:
print(f"Failed to start Tidal callback server: {e}") print(f"Failed to start Tidal callback server: {e}")