From 7b90c05a291faa90f4b8b3804df0be98fbbfa5ad Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 9 Feb 2026 16:28:01 +0000 Subject: [PATCH] load image fallback --- backend/main.py | 45 ++++++++++++++++++++++++++++++++++++++++ frontend/js/favorites.js | 4 ++++ frontend/js/videos.js | 32 ++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/backend/main.py b/backend/main.py index f67da50..c65f9f3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -103,6 +103,51 @@ def videos_proxy(): except Exception as e: return jsonify({"error": str(e)}), 500 +@app.route('/api/image', methods=['GET', 'HEAD']) +def image_proxy(): + image_url = request.args.get('url') + if not image_url: + return jsonify({"error": "No URL provided"}), 400 + + parsed = urllib.parse.urlparse(image_url) + if parsed.scheme not in ('http', 'https') or not parsed.netloc: + return jsonify({"error": "Invalid target URL"}), 400 + + try: + safe_request_headers = {} + for k in ('User-Agent', 'Accept', 'Accept-Encoding', 'Accept-Language'): + if k in request.headers: + safe_request_headers[k] = request.headers[k] + + resp = session.get(image_url, headers=safe_request_headers, stream=True, timeout=15, allow_redirects=True) + + hop_by_hop = { + 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', + 'te', 'trailers', 'transfer-encoding', 'upgrade' + } + + forwarded_headers = [] + for name, value in resp.headers.items(): + if name.lower() in hop_by_hop: + continue + forwarded_headers.append((name, value)) + + if request.method == 'HEAD': + resp.close() + return Response("", status=resp.status_code, headers=forwarded_headers) + + def generate(): + try: + for chunk in resp.iter_content(1024 * 16): + if chunk: + yield chunk + finally: + resp.close() + + return Response(generate(), status=resp.status_code, headers=forwarded_headers) + except Exception as e: + return jsonify({"error": str(e)}), 500 + @app.route('/') def index(): return send_from_directory(app.static_folder, 'index.html') diff --git a/frontend/js/favorites.js b/frontend/js/favorites.js index b09d3be..9ee11b5 100644 --- a/frontend/js/favorites.js +++ b/frontend/js/favorites.js @@ -116,6 +116,10 @@ App.favorites = App.favorites || {}; ${uploaderText ? `

` : ''} `; + const thumb = card.querySelector('img'); + if (App.videos && typeof App.videos.attachNoReferrerRetry === 'function') { + App.videos.attachNoReferrerRetry(thumb); + } card.onclick = () => App.player.open(item.url); const favoriteBtn = card.querySelector('.favorite-btn'); if (favoriteBtn) { diff --git a/frontend/js/videos.js b/frontend/js/videos.js index f833020..69b48f9 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -28,6 +28,36 @@ App.videos = App.videos || {}; return `${minutes}m`; }; + App.videos.buildImageProxyUrl = function(imageUrl) { + if (!imageUrl) return ''; + try { + return `/api/image?url=${encodeURIComponent(imageUrl)}&ts=${Date.now()}`; + } catch (err) { + return ''; + } + }; + + App.videos.attachNoReferrerRetry = function(img) { + if (!img) return; + if (!img.dataset.originalSrc) { + img.dataset.originalSrc = img.currentSrc || img.src || ''; + } + img.dataset.noReferrerRetry = '0'; + img.addEventListener('error', () => { + if (img.dataset.noReferrerRetry === '1') return; + img.dataset.noReferrerRetry = '1'; + img.referrerPolicy = 'no-referrer'; + img.removeAttribute('crossorigin'); + const original = img.dataset.originalSrc || img.currentSrc || img.src || ''; + const proxyUrl = App.videos.buildImageProxyUrl(original); + if (proxyUrl) { + img.src = proxyUrl; + } else if (original) { + img.src = original; + } + }); + }; + // Fetches the next page of videos and renders them into the grid. App.videos.loadVideos = async function() { const session = App.storage.getSession(); @@ -108,6 +138,8 @@ App.videos = App.videos || {}; ${uploaderText ? `

` : ''} ${durationText ? `

${durationText}

` : ''} `; + const thumb = card.querySelector('img'); + App.videos.attachNoReferrerRetry(thumb); const favoriteBtn = card.querySelector('.favorite-btn'); if (favoriteBtn && favoriteKey) { App.favorites.setButtonState(favoriteBtn, favoritesSet.has(favoriteKey));