From 7dc66abc2152005b8411a77bb101144e5f64866e Mon Sep 17 00:00:00 2001 From: yukkop Date: Sat, 23 May 2026 11:56:04 +0000 Subject: [PATCH] feat: `media-browser`: preview for local files --- nixos/module/hectic/service/matrix.nix | 2 +- package/media-browser/app.py | 102 +++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/nixos/module/hectic/service/matrix.nix b/nixos/module/hectic/service/matrix.nix index 3f16e6c..44ac32b 100644 --- a/nixos/module/hectic/service/matrix.nix +++ b/nixos/module/hectic/service/matrix.nix @@ -143,7 +143,7 @@ in { maxUploadSize = lib.mkOption { type = lib.types.str; - default = "100M"; + default = "2G"; description = '' Maximum file upload size accepted by Synapse and nginx. ''; diff --git a/package/media-browser/app.py b/package/media-browser/app.py index 9081875..952b7aa 100644 --- a/package/media-browser/app.py +++ b/package/media-browser/app.py @@ -207,11 +207,30 @@ def api_sync_status(): except Exception as e: return jsonify({'error': str(e)}), 500 +def get_media_type_from_path(filepath): + try: + parts = filepath.split('/') + if len(parts) >= 4 and parts[0] == 'local_content': + media_id = parts[1] + parts[2] + parts[3] + conn = get_db_conn() + cur = conn.cursor() + cur.execute( + "SELECT media_type FROM local_media_repository WHERE media_id = %s", + (media_id,) + ) + row = cur.fetchone() + cur.close() + conn.close() + if row: + return row[0] + except Exception: + pass + return None + @app.route('/view/local/') def view_local(filepath): try: safe_path = os.path.join(MEDIA_STORE_PATH, filepath) - # Security: ensure path is within MEDIA_STORE_PATH real_path = os.path.realpath(safe_path) real_base = os.path.realpath(MEDIA_STORE_PATH) if not real_path.startswith(real_base): @@ -219,8 +238,9 @@ def view_local(filepath): if not os.path.exists(real_path): return 'Not found', 404 - - return send_file(real_path) + + mimetype = get_media_type_from_path(filepath) + return send_file(real_path, mimetype=mimetype) except Exception as e: return str(e), 500 @@ -230,8 +250,16 @@ def view_s3(key): s3 = get_s3_client() full_key = f"{S3_PREFIX}/{key}" if S3_PREFIX else key + mimetype = None + if key.startswith('local_content/'): + mimetype = get_media_type_from_path(key) + + params = {'Bucket': S3_BUCKET, 'Key': full_key} + if mimetype: + params['ResponseContentType'] = mimetype + url = s3.generate_presigned_url('get_object', - Params={'Bucket': S3_BUCKET, 'Key': full_key}, + Params=params, ExpiresIn=3600) return jsonify({'url': url}) @@ -338,6 +366,34 @@ HTML_TEMPLATE = ''' .size { font-family: monospace; color: #8b949e; } .path { font-family: monospace; font-size: 0.85em; } #content { max-height: 70vh; overflow-y: auto; } + .preview-modal { + display: none; + position: fixed; + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.9); + z-index: 1000; + justify-content: center; + align-items: center; + flex-direction: column; + } + .preview-modal.active { display: flex; } + .preview-modal img, .preview-modal video { + max-width: 90vw; + max-height: 80vh; + border-radius: 8px; + } + .preview-modal audio { width: 500px; } + .preview-close { + position: absolute; + top: 20px; right: 30px; + font-size: 2em; color: white; + cursor: pointer; background: none; border: none; + } + .preview-info { + color: #8b949e; + margin-top: 15px; + font-size: 0.9em; + } @@ -375,6 +431,12 @@ HTML_TEMPLATE = '''
Loading...
+ +
+ +
+
+