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]
except Exception:
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)
master_address = content['encrypted_content'].meta.get('item_address', '')
opts = {
'content_type': content['content_type'], # возможно с ошибками, нужно переделать на ffprobe
'content_mime': content.get('content_mime'),
'content_address': license_address or master_address,
'license_address': license_address,
'master_address': master_address,
@ -180,12 +186,21 @@ async def s_api_v1_content_view(request, content_address: str):
'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:
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
derivative_rows = []
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 {})
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 = {}
if derivative_rows:
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]
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
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'):
if key in derivative_latest:
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)
if url:
display_options['content_url'] = url
opts['content_ext'] = (chosen_row.content_type or '').split('/')[-1] if chosen_row.content_type else None
converted_meta_map.setdefault('low' if have_access else 'low_preview', file_hash)
ext_candidate = None
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:
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:
hash_value = converted_meta_map.get(key)
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()
if stored:
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
# Metadata fallback
content_meta = content['encrypted_content'].json_format()
content_meta = encrypted_json
content_metadata_json = None
_mcid = content_meta.get('metadata_cid') or None
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,
})
required_kinds = {'decrypted_low', 'decrypted_high'}
if ec_v3 and ec_v3.content_type.startswith('video/'):
required_kinds.add('decrypted_preview')
required_kinds = set()
if content_kind == 'binary':
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}
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,
}
final_state = 'ready' if display_options['content_url'] else None
if final_state != 'ready':
upload_state = upload_row.state if upload_row else None
if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'):
final_state = 'failed'
elif conversion_state in ('processing', 'partial') or upload_state in ('processing', 'pinned'):
final_state = 'processing'
else:
final_state = 'uploaded'
upload_state = upload_row.state if upload_row else None
if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'):
final_state = 'failed'
elif conversion_state == 'ready':
final_state = 'ready'
elif conversion_state in ('processing', 'partial') or upload_state in ('processing', 'pinned'):
final_state = 'processing'
else:
final_state = 'uploaded'
conversion_info = {
'state': conversion_state,
@ -341,7 +413,10 @@ async def s_api_v1_content_view(request, content_address: str):
'state': final_state,
'conversion_state': conversion_state,
'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({
**opts,

View File

@ -9,6 +9,7 @@ from typing import Dict, Any
import aiofiles
from base58 import b58encode
from sanic import response
import magic # type: ignore
from app.core._config import UPLOADS_DIR, PROJECT_HOST
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()
description = meta.get("description") or meta.get("Description") or ""
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
try:
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 datetime import datetime
@ -49,7 +49,7 @@ class StarsInvoice(AlchemyBase):
user_id = Column(Integer, ForeignKey('users.id'), 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)
paid = Column(Boolean, nullable=False, default=False)