Migrate clip/convert to V3 actions; add background clip support, redirect legacy route; update frontend guide
This commit is contained in:
@@ -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() ###
|
||||
|
||||
Reference in New Issue
Block a user