Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

Implement file downloading under g3pylib.recordings.recording #92

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
downloaded_recordings/

# Translations
*.mo
Expand Down
126 changes: 105 additions & 21 deletions src/g3pylib/recordings/recording.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import asyncio
import base64
import json
import logging
import os
from datetime import datetime, timedelta
from typing import List, Optional, cast

Expand Down Expand Up @@ -171,40 +174,121 @@ def uuid(self) -> str:
"""The uuid of the recording."""
return self._uuid

async def get_scenevideo_url(self) -> str:
"""Returns a URL to the recording's video file."""
async def get_json_data(self) -> str:
"""Returns a JSON string that holds metadata for all of the recording files."""
if self._http_url is None:
raise FeatureNotAvailableError(
"This Glasses3 object was initialized without a proper HTTP url."
)
data_url = f"{self._http_url}{await self.get_http_path()}"
async with aiohttp.ClientSession() as session:
async with session.get(data_url) as response:
data = json.loads(await response.text())
return await response.text()

def _get_file_name_from_json_data(
self, data_str: str, key: str, data_url: str
) -> str:
"""Extracts the desired file name from `data_str` collected from `data_url` represented by `key`. `data_url` is used exclusively for the error message."""
data = json.loads(data_str)
try:
scenevideo_file_name = data["scenecamera"]["file"]
except KeyError:
file_name = data[key]["file"]
except KeyError as exc:
self.logger.warning(
f"Could not retrieve file name for recording from recording data collected from {data_url}."
f"Could not retrieve file name for {key} data from recording data collected from {data_url}."
)
raise InvalidResponseError
raise InvalidResponseError from exc
return file_name

async def get_scenevideo_url(self) -> str:
"""Returns a URL to the recording's video file."""
data_url = f"{self._http_url}{await self.get_http_path()}"
scenevideo_file_name = self._get_file_name_from_json_data(
await self.get_json_data(), "scenecamera", data_url
)
return f"{data_url}/{scenevideo_file_name}"

async def get_gazedata_url(self) -> str:
"""Returns a URL to the recording's decompressed gaze data file."""
if self._http_url is None:
raise FeatureNotAvailableError(
"This Glasses3 object was initialized without a proper HTTP url."
)
data_url = f"{self._http_url}{await self.get_http_path()}"
async with aiohttp.ClientSession() as session:
async with session.get(data_url) as response:
data = json.loads(await response.text())
try:
gaze_file_name = data["gaze"]["file"]
except KeyError:
self.logger.warning(
f"Could not retrieve file name for gaze data from recording data collected from {data_url}."
)
raise InvalidResponseError
gaze_file_name = self._get_file_name_from_json_data(
await self.get_json_data(), "gaze", data_url
)
return f"{data_url}/{gaze_file_name}?use-content-encoding=true"

async def download_files(self, path: str = ".") -> str:
"""
Downloads all recording files into one folder under the specified `path`.
Returns name of the downloaded folder under `path`.
"""
data_url = f"{self._http_url}{await self.get_http_path()}"
data_str = await self.get_json_data()
data_json = json.loads(data_str)

# workaround for making subsequent aiohttp clientsessions
# TODO: find a more appropriate solution
await asyncio.sleep(0.5)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a workaround to #91.


# create download folder and the meta folder within it
folder_name = data_json["name"]
os.makedirs(os.path.join(path, folder_name), exist_ok=True)
meta_folder_name = data_json["meta-folder"]
os.makedirs(os.path.join(path, folder_name, meta_folder_name), exist_ok=True)

# write json data to recording.g3 file
with open(os.path.join(path, folder_name, "recording.g3"), "w") as f:
f.write(data_str)

# generate filenames and file urls
scenevideo_file_name = self._get_file_name_from_json_data(
data_str, "scenecamera", data_url
)
scenevideo_url = f"{data_url}/{scenevideo_file_name}"

gazedata_file_name = self._get_file_name_from_json_data(
data_str, "gaze", data_url
)
gazedata_url = f"{data_url}/{gazedata_file_name}"

eventdata_file_name = self._get_file_name_from_json_data(
data_str, "events", data_url
)
eventdata_url = f"{data_url}/{eventdata_file_name}"

imudata_file_name = self._get_file_name_from_json_data(
data_str, "imu", data_url
)
imudata_url = f"{data_url}/{imudata_file_name}"

async with aiohttp.ClientSession() as session:

async def download(url: str, file_name: str) -> None:
async with session.get(url) as response:
with open(os.path.join(path, folder_name, file_name), "wb") as f:
f.write(await response.read())

async def download_meta(key: str) -> None:
with open(
os.path.join(path, folder_name, meta_folder_name, key), "w"
) as f:
encoded_value = await self.meta_lookup(key)
# decode returned string if it is base64 encoded, otherwise write it raw
try:
value = base64.b64decode(encoded_value).decode()
except ValueError:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that the default metadata like HuSerial, RuSerial and RuVersion are base64-encoded as described in the developer guide, but custom ones, like the ones tests/api_components/test_recording.py::test_meta_data created, are plain text. Is this intended behavior?

value = encoded_value
f.write(value)

# get meta keys
meta_keys = await self.meta_keys()

# create tasks that download files and meta files
task_list = [
download(scenevideo_url, scenevideo_file_name),
download(gazedata_url, gazedata_file_name),
download(eventdata_url, eventdata_file_name),
download(imudata_url, imudata_file_name),
] + [download_meta(key) for key in meta_keys]

await asyncio.gather(*task_list)

return folder_name
28 changes: 28 additions & 0 deletions tests/api_components/test_recording.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
from datetime import datetime, timedelta
from os import path
from typing import cast

import aiohttp
Expand Down Expand Up @@ -111,3 +113,29 @@ async def test_get_gazedata_url(recording: Recording):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
assert response.status == 200


async def test_download_files(recording: Recording):
download_path = "./downloaded_recordings"
folder_name = await recording.download_files(download_path)

# check recording.g3 file and read its content
assert path.isfile(path.join(download_path, folder_name, "recording.g3"))
with open(path.join(download_path, folder_name, "recording.g3"), "r") as f:
data_json = json.loads(f.read())

# check data files exist
assert path.isfile(
path.join(download_path, folder_name, data_json["scenecamera"]["file"])
)
assert path.isfile(path.join(download_path, folder_name, data_json["gaze"]["file"]))
assert path.isfile(
path.join(download_path, folder_name, data_json["events"]["file"])
)
assert path.isfile(path.join(download_path, folder_name, data_json["imu"]["file"]))

# check meta files exist
meta_folder = data_json["meta-folder"]
meta_keys = await recording.meta_keys()
for key in meta_keys:
assert path.isfile(path.join(download_path, folder_name, meta_folder, key))