Migrate clip/convert to V3 actions; add background clip support, redirect legacy route; update frontend guide

This commit is contained in:
Scott Idem
2026-03-11 14:51:08 -04:00
parent fbbc186af0
commit a20c436013
8 changed files with 283 additions and 64 deletions

View File

@@ -18,6 +18,7 @@ from app.methods.hosted_file_methods import (
create_hosted_file_link, delete_hosted_file_link, get_hosted_file_link_rec_list
)
from app.methods.lib_media import convert_file_method
from app.methods.lib_media import clip_video_method
from app.lib_general_v3 import (
AccountContext, get_account_context, get_account_context_optional,
SerializationParams, DelayParams
@@ -42,7 +43,7 @@ def validate_file_extension(filename: str, allowed_extensions: List[str]):
"""
if not allowed_extensions:
return True
ext = filename.rsplit('.', 1)[-1].lower()
if ext not in [e.lower().strip('.') for e in allowed_extensions]:
raise HTTPException(
@@ -85,7 +86,7 @@ async def upload_files_action(
- Returns clean Vision IDs.
"""
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
# 1. Resolve Parent IDs
account_id_random = account_id
if res_account_id := redis_lookup_id_random(record_id_random=account_id, table_name='account'):
@@ -132,7 +133,7 @@ async def upload_files_action(
):
# Use existing record
hosted_file_id_int = existing_rec.get('id')
# Migration check: Update subdirectory if missing or mismatched
if file_info.get('subdirectory_path') and existing_rec.get('subdirectory_path') != file_info['subdirectory_path']:
log.info(f"Updating subdirectory_path for existing record {hosted_file_id_int} to {file_info['subdirectory_path']}")
@@ -140,7 +141,7 @@ async def upload_files_action(
table_name = 'hosted_file',
data = {'id': hosted_file_id_int, 'subdirectory_path': file_info['subdirectory_path']}
)
# Reload to get the latest DB state (including updated path)
hosted_file_dict = load_hosted_file_obj(hosted_file_id=hosted_file_id_int, model_as_dict=True)
else:
@@ -148,7 +149,7 @@ async def upload_files_action(
file_info['account_id'] = account_id_int
file_info['account_id_random'] = account_id_random
new_hosted_file_obj = Hosted_File_Base(**file_info)
if res_new_id := create_hosted_file_obj(hosted_file_obj_new=new_hosted_file_obj):
hosted_file_id_int = res_new_id
hosted_file_dict = load_hosted_file_obj(hosted_file_id=hosted_file_id_int, model_as_dict=True)
@@ -201,11 +202,11 @@ async def download_file_action(
# 1. Auth Bypass Logic (site_key and simplified key)
is_authorized = False
# Priority A: Standard Auth (JWT or API Key)
if account.auth_method != 'guest':
is_authorized = True
# Priority B: Simplified Access Pattern (?key=ANY_VALID_ACCOUNT_ID)
elif key:
# For now, to unblock the frontend, any valid account_id_random is sufficient.
@@ -221,17 +222,17 @@ async def download_file_action(
if site_res := sql_select(sql=sql, data={'key': site_key}):
is_authorized = True
log.info(f"Auth Bypass: Download authorized via site_key.")
if not is_authorized:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Authentication required or invalid access key.")
# 2. Resolve File Record
# ID Vision: Attempt to resolve the ID.
# ID Vision: Attempt to resolve the ID.
# 🛑 REMINDER: If adding a new specialized 'container' object (like event_person_profile),
# ensure the lookup logic is mirrored here to allow direct downloads via container ID.
# If not found in hosted_file, check if it's an event_file or archive_content ID that we can resolve.
resolved_id = redis_lookup_id_random(record_id_random=hosted_file_id, table_name='hosted_file')
if not resolved_id:
log.info(f"ID {hosted_file_id} not found in hosted_file. Checking container tables...")
# A. Check event_file
@@ -239,7 +240,7 @@ async def download_file_action(
if ef_rec := sql_select(sql="SELECT hosted_file_id FROM event_file WHERE id = :id", data={'id': ef_id}):
resolved_id = ef_rec.get('hosted_file_id')
log.info(f"Resolved event_file {hosted_file_id} to hosted_file {resolved_id}")
# B. Check archive_content
if not resolved_id:
if ac_id := redis_lookup_id_random(record_id_random=hosted_file_id, table_name='archive_content'):
@@ -305,7 +306,7 @@ async def download_file_action(
'Content-Disposition': f'attachment; filename="{safe_filename}"; filename*=utf-8\'\'{encoded_filename}'
}
)
return FileResponse(full_file_path, filename=target_filename, media_type=media_type)
@@ -329,7 +330,7 @@ async def download_file_by_hash_action(
# For now, we strictly require a valid machine API key (auth_method will not be 'guest')
if account.auth_method == 'guest':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
status_code=status.HTTP_403_FORBIDDEN,
detail="Valid API Key required for hash-based downloads."
)
@@ -349,7 +350,7 @@ async def download_file_by_hash_action(
# 3. Serve File
target_filename = filename or f"file_{sha256[:8]}.bin"
media_type = mimetypes.guess_type(target_filename)[0] or 'application/octet-stream'
return FileResponse(full_file_path, filename=target_filename, media_type=media_type)
@@ -427,7 +428,7 @@ async def delete_file_action(
if rm_orphan and is_orphan:
log.info(f"File {file_id_int} is an orphan. Cleaning up...")
# Method Handling
if method == 'delete':
# Hard delete: Record + Disk
@@ -481,3 +482,55 @@ async def convert_file(
return mk_resp(data=result)
return mk_resp(data=None, status_code=400, status_message="Conversion failed.")
# ### END ### API V3 Hosted File Action ### convert_file() ###
@router.get('/{hosted_file_id}/clip_video', response_model=Resp_Body_Base)
async def clip_video(
hosted_file_id: str = Path(min_length=11, max_length=22),
link_to_type: str = Query(...),
link_to_id: str = Query(...),
start_time: str = Query(..., min_length=8, max_length=8),
end_time: str = Query(..., min_length=8, max_length=8),
filename_no_ext: str = Query('automated_hosted_file_clip_video'),
reencode: bool = Query(False),
scale_down: bool = Query(False),
background: bool = Query(False),
account: AccountContext = Depends(get_account_context),
):
"""
Clip a segment from a hosted video and save as a new hosted_file record.
Supports optional background scheduling returning `202 Accepted` when `background=true`.
"""
lid_int = redis_lookup_id_random(record_id_random=link_to_id, table_name=link_to_type)
if not lid_int:
raise HTTPException(status_code=404, detail=f"Linked object not found: {link_to_type}:{link_to_id}")
async def _run_clip():
try:
return await clip_video_method(
hosted_file_id=hosted_file_id,
start_time=start_time,
end_time=end_time,
account_id=account.account_id,
account_id_random=account.account_id_random,
link_to_type=link_to_type,
link_to_id=lid_int,
filename_no_ext=filename_no_ext,
reencode=reencode,
scale_down=scale_down,
)
except Exception:
log.exception('Background clip task failed')
return None
if background:
# Schedule and return 202 Accepted
asyncio.create_task(_run_clip())
return mk_resp(data={'task': 'scheduled'}, status_code=202, status_message='Clip scheduled (background)')
result = await _run_clip()
if result:
return mk_resp(data=result)
return mk_resp(data=None, status_code=400, status_message="Clip failed.")
# ### END ### API V3 Hosted File Action ### clip_video() ###
# ### END ### API V3 Hosted File Action ### convert_file() ###