updates
This commit is contained in:
parent
0405c340a3
commit
01bb82fa5a
|
|
@ -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,10 +273,25 @@ 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:
|
||||||
|
if content_kind == 'binary':
|
||||||
|
preference = ['original'] if have_access else []
|
||||||
|
else:
|
||||||
preference = ['low', 'high', 'low_preview'] if have_access else ['low_preview', 'low', 'high']
|
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)
|
||||||
|
|
@ -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,8 +357,13 @@ 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 = 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'}
|
required_kinds = {'decrypted_low', 'decrypted_high'}
|
||||||
if ec_v3 and ec_v3.content_type.startswith('video/'):
|
if ec_v3 and ec_v3.content_type and ec_v3.content_type.startswith('video/'):
|
||||||
required_kinds.add('decrypted_preview')
|
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}
|
||||||
|
|
@ -318,11 +390,11 @@ 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
|
|
||||||
if final_state != 'ready':
|
|
||||||
upload_state = upload_row.state if upload_row else None
|
upload_state = upload_row.state if upload_row else None
|
||||||
if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'):
|
if conversion_state == 'failed' or upload_state in ('failed', 'conversion_failed'):
|
||||||
final_state = 'failed'
|
final_state = 'failed'
|
||||||
|
elif conversion_state == 'ready':
|
||||||
|
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:
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue