fix: more thorough handling for internal image url fixes
This commit is contained in:
parent
569c827ab4
commit
7285c6f55a
1 changed files with 57 additions and 11 deletions
|
|
@ -18,7 +18,7 @@ import sqlite3
|
|||
import types
|
||||
import collections
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import quote, urljoin, urlparse
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from flask import Flask, render_template, request, jsonify, redirect, send_file, Response, session, g, abort
|
||||
|
|
@ -11127,7 +11127,7 @@ def maintain_search_history():
|
|||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
def fix_artist_image_url(thumb_url):
|
||||
"""Convert localhost URLs to proper server URLs using config"""
|
||||
"""Convert media-server image URLs into browser-safe URLs."""
|
||||
if not thumb_url:
|
||||
return None
|
||||
|
||||
|
|
@ -11136,6 +11136,11 @@ def fix_artist_image_url(thumb_url):
|
|||
needs_fixing = (
|
||||
thumb_url.startswith('http://localhost:') or
|
||||
thumb_url.startswith('https://localhost:') or
|
||||
thumb_url.startswith('http://127.0.0.1:') or
|
||||
thumb_url.startswith('https://127.0.0.1:') or
|
||||
thumb_url.startswith('http://host.docker.internal:') or
|
||||
thumb_url.startswith('https://host.docker.internal:') or
|
||||
(thumb_url.startswith('http://') and _is_internal_image_host(thumb_url)) or
|
||||
thumb_url.startswith('/library/') or # Plex relative paths
|
||||
thumb_url.startswith('/Items/') or # Jellyfin relative paths
|
||||
thumb_url.startswith('/api/') or # Old Navidrome API paths
|
||||
|
|
@ -11166,7 +11171,7 @@ def fix_artist_image_url(thumb_url):
|
|||
# Construct proper Plex URL with token
|
||||
fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}"
|
||||
logger.info(f"Fixed URL: {fixed_url}")
|
||||
return fixed_url
|
||||
return _browser_safe_image_url(fixed_url)
|
||||
|
||||
elif active_server == 'jellyfin':
|
||||
jellyfin_config = config_manager.get_jellyfin_config()
|
||||
|
|
@ -11192,7 +11197,7 @@ def fix_artist_image_url(thumb_url):
|
|||
else:
|
||||
fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}"
|
||||
logger.info(f"Fixed URL: {fixed_url}")
|
||||
return fixed_url
|
||||
return _browser_safe_image_url(fixed_url)
|
||||
|
||||
elif active_server == 'navidrome':
|
||||
navidrome_config = config_manager.get_navidrome_config()
|
||||
|
|
@ -11225,16 +11230,57 @@ def fix_artist_image_url(thumb_url):
|
|||
# Construct proper Navidrome Subsonic URL
|
||||
fixed_url = f"{navidrome_base_url.rstrip('/')}{path}{separator}{auth_params}"
|
||||
logger.info(f"Fixed URL: {fixed_url}")
|
||||
return fixed_url
|
||||
return _browser_safe_image_url(fixed_url)
|
||||
|
||||
logger.warning(f"No configuration found for {active_server} or unsupported server type")
|
||||
|
||||
# Return original URL if no fixing needed/possible
|
||||
return thumb_url
|
||||
# Return a browser-safe URL even if no server-specific rebuild was possible.
|
||||
return _browser_safe_image_url(thumb_url)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fixing image URL '{thumb_url}': {e}")
|
||||
return thumb_url
|
||||
return _browser_safe_image_url(thumb_url)
|
||||
|
||||
|
||||
def _is_internal_image_host(url: str) -> bool:
|
||||
"""Return True when an image URL points at a host the browser likely cannot reach directly."""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
host = (parsed.hostname or '').strip('[]').lower()
|
||||
if not host:
|
||||
return False
|
||||
|
||||
if host in {'localhost', '127.0.0.1', '::1', 'host.docker.internal'}:
|
||||
return True
|
||||
|
||||
# Single-label hosts are usually Docker service names or local LAN aliases.
|
||||
if '.' not in host:
|
||||
return True
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(host)
|
||||
return ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_reserved
|
||||
except ValueError:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _browser_safe_image_url(url: str) -> str:
|
||||
"""Return a browser-safe image URL, proxying internal hosts through SoulSync."""
|
||||
if not url:
|
||||
return url
|
||||
|
||||
if url.startswith('/api/image-proxy?url='):
|
||||
return url
|
||||
|
||||
if url.startswith('http://') or url.startswith('https://'):
|
||||
if _is_internal_image_host(url):
|
||||
return f"/api/image-proxy?url={quote(url, safe='')}"
|
||||
return url
|
||||
|
||||
# Relative media-server paths should already have been expanded before this point.
|
||||
return url
|
||||
|
||||
@app.route('/api/library/history')
|
||||
def get_library_history():
|
||||
|
|
@ -45220,8 +45266,7 @@ def image_proxy():
|
|||
url = request.args.get('url', '')
|
||||
if not url or not url.startswith('http'):
|
||||
return '', 400
|
||||
# Only allow known image CDNs
|
||||
from urllib.parse import urlparse
|
||||
|
||||
host = urlparse(url).hostname or ''
|
||||
allowed_hosts = [
|
||||
'i.scdn.co', 'mosaic.scdn.co', # Spotify
|
||||
|
|
@ -45230,8 +45275,9 @@ def image_proxy():
|
|||
'is1-ssl.mzstatic.com', 'is2-ssl.mzstatic.com', 'is3-ssl.mzstatic.com',
|
||||
'is4-ssl.mzstatic.com', 'is5-ssl.mzstatic.com', # iTunes/Apple
|
||||
'img.discogs.com', 'i.discogs.com', # Discogs
|
||||
'localhost', '127.0.0.1', 'host.docker.internal', # Local/Docker media servers
|
||||
]
|
||||
if not any(host == h or host.endswith('.' + h) for h in allowed_hosts):
|
||||
if not any(host == h or host.endswith('.' + h) for h in allowed_hosts) and not _is_internal_image_host(url):
|
||||
return '', 403
|
||||
try:
|
||||
resp = requests.get(url, timeout=10, stream=True, headers={
|
||||
|
|
|
|||
Loading…
Reference in a new issue