This commit is contained in:
root 2025-10-20 15:31:52 +00:00
parent 0405c340a3
commit 01bb82fa5a
3 changed files with 130 additions and 24 deletions

View File

@ -79,12 +79,18 @@ async def s_api_v1_content_view(request, content_address: str):
content_type = ctype.split('/')[0] content_type = ctype.split('/')[0]
except Exception: except Exception:
content_type = 'application' content_type = 'application'
return {'encrypted_content': encrypted, 'decrypted_content': decrypted, 'content_type': content_type} return {
'encrypted_content': encrypted,
'decrypted_content': decrypted,
'content_type': content_type,
'content_mime': ctype,
}
content = await open_content_async(request.ctx.db_session, r_content) content = await open_content_async(request.ctx.db_session, r_content)
master_address = content['encrypted_content'].meta.get('item_address', '') master_address = content['encrypted_content'].meta.get('item_address', '')
opts = { opts = {
'content_type': content['content_type'], # возможно с ошибками, нужно переделать на ffprobe 'content_type': content['content_type'], # возможно с ошибками, нужно переделать на ffprobe
'content_mime': content.get('content_mime'),
'content_address': license_address or master_address, 'content_address': license_address or master_address,
'license_address': license_address, 'license_address': license_address,
'master_address': master_address, 'master_address': master_address,
@ -180,12 +186,21 @@ async def s_api_v1_content_view(request, content_address: str):
'amount': stars_cost, 'amount': stars_cost,
} }
display_options = {'content_url': None} display_options = {
'content_url': None,
'content_kind': None,
'has_preview': False,
'original_available': False,
'requires_license': False,
}
if have_access: if have_access:
opts['have_licenses'].append('listen') opts['have_licenses'].append('listen')
enc_cid = content['encrypted_content'].meta.get('content_cid') or content['encrypted_content'].meta.get('encrypted_cid') encrypted_json = content['encrypted_content'].json_format()
decrypted_json = content['decrypted_content'].json_format()
enc_cid = encrypted_json.get('content_cid') or encrypted_json.get('encrypted_cid')
ec_v3 = None ec_v3 = None
derivative_rows = [] derivative_rows = []
if enc_cid: if enc_cid:
@ -199,6 +214,30 @@ async def s_api_v1_content_view(request, content_address: str):
converted_meta_map = dict(content['encrypted_content'].meta.get('converted_content') or {}) converted_meta_map = dict(content['encrypted_content'].meta.get('converted_content') or {})
content_mime = (
(ec_v3.content_type if ec_v3 and ec_v3.content_type else None)
or decrypted_json.get('content_type')
or encrypted_json.get('content_type')
or opts.get('content_mime')
or 'application/octet-stream'
)
opts['content_mime'] = content_mime
try:
opts['content_type'] = content_mime.split('/')[0]
except Exception:
opts['content_type'] = opts.get('content_type') or 'application'
content_kind = 'audio'
if content_mime.startswith('video/'):
content_kind = 'video'
elif content_mime.startswith('audio/'):
content_kind = 'audio'
else:
content_kind = 'binary'
display_options['content_kind'] = content_kind
display_options['requires_license'] = (not have_access) and content_kind == 'binary'
derivative_latest = {} derivative_latest = {}
if derivative_rows: if derivative_rows:
derivative_sorted = sorted(derivative_rows, key=lambda row: row.created_at or datetime.min) derivative_sorted = sorted(derivative_rows, key=lambda row: row.created_at or datetime.min)
@ -211,8 +250,15 @@ async def s_api_v1_content_view(request, content_address: str):
file_hash = row.local_path.split('/')[-1] file_hash = row.local_path.split('/')[-1]
return file_hash, f"{PROJECT_HOST}/api/v1.5/storage/{file_hash}" return file_hash, f"{PROJECT_HOST}/api/v1.5/storage/{file_hash}"
has_preview = bool(derivative_latest.get('decrypted_preview') or converted_meta_map.get('low_preview'))
display_options['has_preview'] = has_preview
display_options['original_available'] = bool(derivative_latest.get('decrypted_original') or converted_meta_map.get('original'))
chosen_row = None chosen_row = None
if have_access: if content_kind == 'binary':
if have_access and 'decrypted_original' in derivative_latest:
chosen_row = derivative_latest['decrypted_original']
elif have_access:
for key in ('decrypted_low', 'decrypted_high'): for key in ('decrypted_low', 'decrypted_high'):
if key in derivative_latest: if key in derivative_latest:
chosen_row = derivative_latest[key] chosen_row = derivative_latest[key]
@ -227,11 +273,26 @@ async def s_api_v1_content_view(request, content_address: str):
file_hash, url = _row_to_hash_and_url(chosen_row) file_hash, url = _row_to_hash_and_url(chosen_row)
if url: if url:
display_options['content_url'] = url display_options['content_url'] = url
opts['content_ext'] = (chosen_row.content_type or '').split('/')[-1] if chosen_row.content_type else None ext_candidate = None
converted_meta_map.setdefault('low' if have_access else 'low_preview', file_hash) if chosen_row.content_type:
ext_candidate = chosen_row.content_type.split('/')[-1]
elif '/' in content_mime:
ext_candidate = content_mime.split('/')[-1]
if ext_candidate:
opts['content_ext'] = ext_candidate
if content_kind == 'binary':
display_options['original_available'] = True
converted_meta_map.setdefault('original', file_hash)
elif have_access:
converted_meta_map.setdefault('low', file_hash)
else:
converted_meta_map.setdefault('low_preview', file_hash)
if not display_options['content_url'] and converted_meta_map: if not display_options['content_url'] and converted_meta_map:
preference = ['low', 'high', 'low_preview'] if have_access else ['low_preview', 'low', 'high'] if content_kind == 'binary':
preference = ['original'] if have_access else []
else:
preference = ['low', 'high', 'low_preview'] if have_access else ['low_preview', 'low', 'high']
for key in preference: for key in preference:
hash_value = converted_meta_map.get(key) hash_value = converted_meta_map.get(key)
if not hash_value: if not hash_value:
@ -239,11 +300,17 @@ async def s_api_v1_content_view(request, content_address: str):
stored = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == hash_value))).scalars().first() stored = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == hash_value))).scalars().first()
if stored: if stored:
display_options['content_url'] = stored.web_url display_options['content_url'] = stored.web_url
opts['content_ext'] = stored.filename.split('.')[-1] filename = stored.filename or ''
if '.' in filename:
opts['content_ext'] = filename.split('.')[-1]
elif '/' in content_mime:
opts['content_ext'] = content_mime.split('/')[-1]
if content_kind == 'binary':
display_options['original_available'] = True
break break
# Metadata fallback # Metadata fallback
content_meta = content['encrypted_content'].json_format() content_meta = encrypted_json
content_metadata_json = None content_metadata_json = None
_mcid = content_meta.get('metadata_cid') or None _mcid = content_meta.get('metadata_cid') or None
if _mcid: if _mcid:
@ -290,9 +357,14 @@ async def s_api_v1_content_view(request, content_address: str):
'updated_at': (row.last_access_at or row.created_at).isoformat() + 'Z' if (row.last_access_at or row.created_at) else None, 'updated_at': (row.last_access_at or row.created_at).isoformat() + 'Z' if (row.last_access_at or row.created_at) else None,
}) })
required_kinds = {'decrypted_low', 'decrypted_high'} required_kinds = set()
if ec_v3 and ec_v3.content_type.startswith('video/'): if content_kind == 'binary':
required_kinds.add('decrypted_preview') if derivative_latest.get('decrypted_original') or converted_meta_map.get('original'):
required_kinds.add('decrypted_original')
else:
required_kinds = {'decrypted_low', 'decrypted_high'}
if ec_v3 and ec_v3.content_type and ec_v3.content_type.startswith('video/'):
required_kinds.add('decrypted_preview')
statuses_by_kind = {kind: row.status for kind, row in derivative_summary_map.items() if kind in required_kinds} statuses_by_kind = {kind: row.status for kind, row in derivative_summary_map.items() if kind in required_kinds}
conversion_state = 'pending' conversion_state = 'pending'
@ -318,15 +390,15 @@ async def s_api_v1_content_view(request, content_address: str):
'updated_at': upload_row.updated_at.isoformat() + 'Z' if upload_row.updated_at else None, 'updated_at': upload_row.updated_at.isoformat() + 'Z' if upload_row.updated_at else None,
} }
final_state = 'ready' if display_options['content_url'] else None upload_state = upload_row.state if upload_row else None
if final_state != 'ready': if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'):
upload_state = upload_row.state if upload_row else None final_state = 'failed'
if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'): elif conversion_state == 'ready':
final_state = 'failed' final_state = 'ready'
elif conversion_state in ('processing', 'partial') or upload_state in ('processing', 'pinned'): elif conversion_state in ('processing', 'partial') or upload_state in ('processing', 'pinned'):
final_state = 'processing' final_state = 'processing'
else: else:
final_state = 'uploaded' final_state = 'uploaded'
conversion_info = { conversion_info = {
'state': conversion_state, 'state': conversion_state,
@ -341,7 +413,10 @@ async def s_api_v1_content_view(request, content_address: str):
'state': final_state, 'state': final_state,
'conversion_state': conversion_state, 'conversion_state': conversion_state,
'upload_state': upload_info['state'] if upload_info else None, 'upload_state': upload_info['state'] if upload_info else None,
'has_access': have_access,
} }
if not opts.get('content_ext') and '/' in content_mime:
opts['content_ext'] = content_mime.split('/')[-1]
return response.json({ return response.json({
**opts, **opts,

View File

@ -9,6 +9,7 @@ from typing import Dict, Any
import aiofiles import aiofiles
from base58 import b58encode from base58 import b58encode
from sanic import response from sanic import response
import magic # type: ignore
from app.core._config import UPLOADS_DIR, PROJECT_HOST from app.core._config import UPLOADS_DIR, PROJECT_HOST
from app.core._secrets import hot_pubkey from app.core._secrets import hot_pubkey
@ -73,7 +74,37 @@ async def s_api_v1_upload_tus_hook(request):
artist = (meta.get("artist") or meta.get("Artist") or "").strip() artist = (meta.get("artist") or meta.get("Artist") or "").strip()
description = meta.get("description") or meta.get("Description") or "" description = meta.get("description") or meta.get("Description") or ""
content_type = meta.get("content_type") or meta.get("Content-Type") or "application/octet-stream" content_type = meta.get("content_type") or meta.get("Content-Type") or "application/octet-stream"
preview_enabled = content_type.startswith("audio/") or content_type.startswith("video/") detected_content_type = None
try:
raw_detected = magic.from_file(file_path, mime=True)
if raw_detected:
detected_content_type = raw_detected.split(";")[0].strip()
except Exception as e:
make_log("tus-hook", f"magic MIME detection failed for {file_path}: {e}", level="warning")
def _is_av(mime: str | None) -> bool:
if not mime:
return False
return mime.startswith("audio/") or mime.startswith("video/")
if detected_content_type:
if not _is_av(detected_content_type):
if content_type != detected_content_type:
make_log(
"tus-hook",
f"Overriding declared content_type '{content_type}' with detected '{detected_content_type}' (binary upload)",
level="info",
)
content_type = detected_content_type
elif not _is_av(content_type):
make_log(
"tus-hook",
f"Detected audio/video MIME '{detected_content_type}' replacing non-AV declaration '{content_type}'",
level="info",
)
content_type = detected_content_type
preview_enabled = _is_av(content_type)
# Optional preview window overrides from tus metadata # Optional preview window overrides from tus metadata
try: try:
start_ms = int(meta.get("preview_start_ms") or 0) start_ms = int(meta.get("preview_start_ms") or 0)

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean, Float from sqlalchemy import Column, Integer, BigInteger, String, ForeignKey, DateTime, Boolean, Float
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
@ -49,7 +49,7 @@ class StarsInvoice(AlchemyBase):
user_id = Column(Integer, ForeignKey('users.id'), nullable=True) user_id = Column(Integer, ForeignKey('users.id'), nullable=True)
content_hash = Column(String(256), nullable=True) content_hash = Column(String(256), nullable=True)
telegram_id = Column(Integer, nullable=True) telegram_id = Column(BigInteger, nullable=True)
invoice_url = Column(String(256), nullable=True) invoice_url = Column(String(256), nullable=True)
paid = Column(Boolean, nullable=False, default=False) paid = Column(Boolean, nullable=False, default=False)