Skip to content

Commit a6fa9c5

Browse files
committed
feat(filebrowser): add recording file metadata display and snapshot handling
- Introduced `RecordingDialog` tests to validate file metadata, snapshot display, and thumbnail rendering. - Implemented dynamic file rendering in the frontend with enhanced layout and preview logic. - Refactored backend to centralize snapshot metadata generation using `build_recording_snapshot_info`. - Updated metadata to include file sizes for both data and meta files in API responses. - Improved frontend to handle file types dynamically with icons and thumbnails for better user experience.
1 parent fbabb4a commit a6fa9c5

4 files changed

Lines changed: 265 additions & 43 deletions

File tree

backend/handlers/entities/filebrowser.py

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
from PIL import Image
3131

3232
from common.decoded_thumbnails import get_decoded_thumbnail_url
33-
from common.thumbnails import delete_image_thumbnail, get_image_thumbnail_url
33+
from common.thumbnails import (
34+
delete_image_thumbnail,
35+
get_image_thumbnail_path,
36+
get_image_thumbnail_url,
37+
)
3438

3539

3640
def get_disk_usage(path: Path) -> Dict[str, Union[int, str]]:
@@ -119,6 +123,35 @@ def get_image_dimensions(image_path: str) -> Tuple[Any, ...]:
119123
return (None, None)
120124

121125

126+
def build_recording_snapshot_info(snapshot_file: Path) -> Optional[Dict[str, Any]]:
127+
"""Build snapshot and generated-thumbnail metadata for a recording."""
128+
if not snapshot_file.exists() or not snapshot_file.is_file():
129+
return None
130+
131+
width, height = get_image_dimensions(str(snapshot_file))
132+
thumbnail_url = get_image_thumbnail_url(snapshot_file, "/recordings")
133+
thumbnail_info = None
134+
135+
if thumbnail_url:
136+
thumbnail_path = get_image_thumbnail_path(snapshot_file)
137+
if thumbnail_path.exists() and thumbnail_path.is_file():
138+
thumbnail_info = {
139+
"filename": thumbnail_path.name,
140+
"url": thumbnail_url,
141+
"size": thumbnail_path.stat().st_size,
142+
}
143+
144+
return {
145+
"filename": snapshot_file.name,
146+
"url": f"/recordings/{snapshot_file.name}",
147+
"thumbnail_url": thumbnail_url,
148+
"thumbnail": thumbnail_info,
149+
"size": snapshot_file.stat().st_size,
150+
"width": width,
151+
"height": height,
152+
}
153+
154+
122155
def parse_transcription_metadata(transcription_file_path: str) -> Dict[str, Any]:
123156
"""
124157
Parse metadata from a transcription file header.
@@ -469,23 +502,15 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
469502
continue
470503

471504
data_stat = data_file.stat()
505+
meta_stat = meta_file.stat()
472506
metadata = parse_sigmf_metadata(str(meta_file))
473507

474508
# Check if recording is in progress
475509
is_recording_in_progress = metadata.get("recording_in_progress", False)
476510

477511
# Check for waterfall snapshot
478512
snapshot_file = recordings_dir / f"{base_name}.png"
479-
snapshot_info = None
480-
if snapshot_file.exists():
481-
width, height = get_image_dimensions(str(snapshot_file))
482-
snapshot_info = {
483-
"filename": snapshot_file.name,
484-
"url": f"/recordings/{snapshot_file.name}",
485-
"thumbnail_url": get_image_thumbnail_url(snapshot_file, "/recordings"),
486-
"width": width,
487-
"height": height,
488-
}
513+
snapshot_info = build_recording_snapshot_info(snapshot_file)
489514

490515
processed_items.append(
491516
{
@@ -494,6 +519,7 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
494519
"data_file": data_file.name,
495520
"meta_file": meta_file.name,
496521
"data_size": data_stat.st_size,
522+
"meta_size": meta_stat.st_size,
497523
"created": datetime.fromtimestamp(
498524
data_stat.st_ctime, timezone.utc
499525
).isoformat(),
@@ -935,6 +961,7 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
935961

936962
# Get file stats
937963
data_stat = data_file.stat()
964+
meta_stat = meta_file.stat()
938965

939966
# Parse metadata
940967
metadata = parse_sigmf_metadata(str(meta_file))
@@ -944,22 +971,14 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
944971

945972
# Check for waterfall snapshot
946973
snapshot_file = recordings_dir / f"{base_name}.png"
947-
snapshot_info = None
948-
if snapshot_file.exists():
949-
width, height = get_image_dimensions(str(snapshot_file))
950-
snapshot_info = {
951-
"filename": snapshot_file.name,
952-
"url": f"/recordings/{snapshot_file.name}",
953-
"thumbnail_url": get_image_thumbnail_url(snapshot_file, "/recordings"),
954-
"width": width,
955-
"height": height,
956-
}
974+
snapshot_info = build_recording_snapshot_info(snapshot_file)
957975

958976
recording = {
959977
"name": base_name,
960978
"data_file": data_file.name,
961979
"meta_file": meta_file.name,
962980
"data_size": data_stat.st_size,
981+
"meta_size": meta_stat.st_size,
963982
"created": datetime.fromtimestamp(data_stat.st_ctime, timezone.utc).isoformat(),
964983
"modified": datetime.fromtimestamp(
965984
data_stat.st_mtime, timezone.utc
@@ -993,6 +1012,7 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
9931012

9941013
# Get file stats
9951014
data_stat = data_file.stat()
1015+
meta_stat = meta_file.stat()
9961016

9971017
# Parse metadata
9981018
metadata = parse_sigmf_metadata(str(meta_file))
@@ -1002,22 +1022,14 @@ async def filebrowser_request_routing(sio, cmd, data, logger, sid):
10021022

10031023
# Check for waterfall snapshot
10041024
snapshot_file = recordings_dir / f"{recording_name}.png"
1005-
snapshot_info = None
1006-
if snapshot_file.exists():
1007-
width, height = get_image_dimensions(str(snapshot_file))
1008-
snapshot_info = {
1009-
"filename": snapshot_file.name,
1010-
"url": f"/recordings/{snapshot_file.name}",
1011-
"thumbnail_url": get_image_thumbnail_url(snapshot_file, "/recordings"),
1012-
"width": width,
1013-
"height": height,
1014-
}
1025+
snapshot_info = build_recording_snapshot_info(snapshot_file)
10151026

10161027
recording = {
10171028
"name": recording_name,
10181029
"data_file": data_file.name,
10191030
"meta_file": meta_file.name,
10201031
"data_size": data_stat.st_size,
1032+
"meta_size": meta_stat.st_size,
10211033
"created": datetime.fromtimestamp(data_stat.st_ctime, timezone.utc).isoformat(),
10221034
"modified": datetime.fromtimestamp(data_stat.st_mtime, timezone.utc).isoformat(),
10231035
"metadata": metadata,

backend/tests/test_decoded_thumbnails.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pathlib import Path
22

3+
import pytest
34
from PIL import Image
45

56
from common.decoded_thumbnails import (
@@ -8,6 +9,8 @@
89
get_decoded_thumbnail_url,
910
select_decoded_thumbnail_source,
1011
)
12+
from common.thumbnails import get_image_thumbnail_path
13+
from handlers.entities.filebrowser import build_recording_snapshot_info
1114

1215

1316
def _write_png(path: Path, size=(1200, 700), color=(12, 34, 56)):
@@ -62,3 +65,22 @@ def test_get_decoded_thumbnail_url_lazily_generates_thumbnail(tmp_path):
6265
assert thumbnail_url is not None
6366
assert thumbnail_url.startswith(f"/decoded/{folder.name}/{THUMBNAIL_FILENAME}?v=")
6467
assert (folder / THUMBNAIL_FILENAME).exists()
68+
69+
70+
@pytest.mark.unit
71+
def test_build_recording_snapshot_info_includes_thumbnail_file_metadata(tmp_path):
72+
source = tmp_path / "recording.png"
73+
_write_png(source, size=(800, 400))
74+
75+
snapshot_info = build_recording_snapshot_info(source)
76+
77+
assert snapshot_info is not None
78+
assert snapshot_info["filename"] == "recording.png"
79+
assert snapshot_info["url"] == "/recordings/recording.png"
80+
assert snapshot_info["size"] == source.stat().st_size
81+
assert snapshot_info["width"] == 800
82+
assert snapshot_info["height"] == 400
83+
assert snapshot_info["thumbnail_url"].startswith("/recordings/thumbnails/recording.jpg?v=")
84+
assert snapshot_info["thumbnail"]["filename"] == "recording.jpg"
85+
assert snapshot_info["thumbnail"]["url"] == snapshot_info["thumbnail_url"]
86+
assert snapshot_info["thumbnail"]["size"] == get_image_thumbnail_path(source).stat().st_size
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { screen } from '@testing-library/react';
3+
import { renderWithProviders } from '../../../test/test-utils.jsx';
4+
import RecordingDialog from '../recording-dialog.jsx';
5+
6+
const recording = {
7+
name: 'NOAA_apt_20260101_120000',
8+
data_file: 'NOAA_apt_20260101_120000.sigmf-data',
9+
meta_file: 'NOAA_apt_20260101_120000.sigmf-meta',
10+
data_size: 1536,
11+
meta_size: 64,
12+
created: '2026-01-01T12:00:00Z',
13+
modified: '2026-01-01T12:01:00Z',
14+
metadata: {
15+
center_frequency: 137_100_000,
16+
sample_rate: 2_400_000,
17+
start_time: '2026-01-01T12:00:00Z',
18+
finalized_time: '2026-01-01T12:01:00Z',
19+
},
20+
snapshot: {
21+
filename: 'NOAA_apt_20260101_120000.png',
22+
url: '/recordings/NOAA_apt_20260101_120000.png',
23+
thumbnail_url: '/recordings/thumbnails/NOAA_apt_20260101_120000.jpg?v=1',
24+
width: 640,
25+
height: 360,
26+
size: 2048,
27+
thumbnail: {
28+
filename: 'NOAA_apt_20260101_120000.jpg',
29+
url: '/recordings/thumbnails/NOAA_apt_20260101_120000.jpg?v=1',
30+
size: 512,
31+
},
32+
},
33+
download_urls: {
34+
data: '/recordings/NOAA_apt_20260101_120000.sigmf-data',
35+
meta: '/recordings/NOAA_apt_20260101_120000.sigmf-meta',
36+
},
37+
};
38+
39+
describe('RecordingDialog', () => {
40+
it('shows associated recording files with thumbnails and sizes', () => {
41+
renderWithProviders(
42+
<RecordingDialog
43+
open
44+
onClose={vi.fn()}
45+
recording={recording}
46+
/>
47+
);
48+
49+
expect(screen.getByText('IQ Data')).toBeInTheDocument();
50+
expect(screen.getByText('NOAA_apt_20260101_120000.sigmf-data')).toBeInTheDocument();
51+
expect(screen.getAllByText('1.5 KB').length).toBeGreaterThan(0);
52+
53+
expect(screen.getAllByText('Metadata').length).toBeGreaterThan(0);
54+
expect(screen.getByText('NOAA_apt_20260101_120000.sigmf-meta')).toBeInTheDocument();
55+
expect(screen.getByText('64 Bytes')).toBeInTheDocument();
56+
57+
expect(screen.getByText('Waterfall Snapshot')).toBeInTheDocument();
58+
expect(screen.getByText('NOAA_apt_20260101_120000.png')).toBeInTheDocument();
59+
expect(screen.getAllByText('640×360').length).toBeGreaterThan(0);
60+
expect(screen.getByText('2 KB')).toBeInTheDocument();
61+
62+
expect(screen.getByText('Thumbnail')).toBeInTheDocument();
63+
expect(screen.getByText('NOAA_apt_20260101_120000.jpg')).toBeInTheDocument();
64+
expect(screen.getByText('512 Bytes')).toBeInTheDocument();
65+
expect(screen.getAllByAltText('Thumbnail preview')).toHaveLength(1);
66+
});
67+
});

0 commit comments

Comments
 (0)