580 lines
25 KiB
Python
580 lines
25 KiB
Python
from datetime import datetime, timedelta
|
||
from sanic import response
|
||
from sqlalchemy import select, and_, func
|
||
from aiogram import Bot, types
|
||
from app.core.logger import make_log
|
||
from app.core.models._config import ServiceConfig
|
||
from app.core.models.node_storage import StoredContent
|
||
from app.core.models.keys import KnownKey
|
||
from app.core.models import StarsInvoice
|
||
from app.core.models.content.user_content import UserContent
|
||
from app.core._config import CLIENT_TELEGRAM_API_KEY, PROJECT_HOST
|
||
from app.core.models.content_v3 import EncryptedContent as ECv3, ContentDerivative as CDv3, UploadSession
|
||
from app.core.content.content_id import ContentId
|
||
import json
|
||
import uuid
|
||
|
||
|
||
async def s_api_v1_content_list(request):
|
||
offset = int(request.args.get('offset', 0))
|
||
limit = int(request.args.get('limit', 100))
|
||
assert 0 <= offset, "Invalid offset"
|
||
assert 0 < limit <= 1000, "Invalid limit"
|
||
|
||
store = request.args.get('store', 'local')
|
||
assert store in ('local', 'onchain'), "Invalid store"
|
||
|
||
stmt = (
|
||
select(StoredContent)
|
||
.where(
|
||
StoredContent.type.like(store + '%'),
|
||
StoredContent.disabled.is_(None)
|
||
)
|
||
.order_by(StoredContent.created.desc())
|
||
.offset(offset)
|
||
.limit(limit)
|
||
)
|
||
rows = (await request.ctx.db_session.execute(stmt)).scalars().all()
|
||
make_log("Content", f"Listed {len(rows)} contents", level='info')
|
||
result = {}
|
||
for content in rows:
|
||
content_json = content.json_format()
|
||
result[content_json["cid"]] = content_json
|
||
|
||
return response.json(result)
|
||
|
||
|
||
async def s_api_v1_content_view(request, content_address: str):
|
||
# content_address can be CID or TON address
|
||
|
||
license_exist = (await request.ctx.db_session.execute(
|
||
select(UserContent).where(UserContent.onchain_address == content_address)
|
||
)).scalars().first()
|
||
license_address = None
|
||
if license_exist:
|
||
license_address = license_exist.onchain_address
|
||
if license_exist.content_id:
|
||
linked_content = (await request.ctx.db_session.execute(
|
||
select(StoredContent).where(StoredContent.id == license_exist.content_id)
|
||
)).scalars().first()
|
||
if linked_content:
|
||
content_address = linked_content.cid.serialize_v2()
|
||
|
||
from app.core.content.content_id import ContentId
|
||
cid = ContentId.deserialize(content_address)
|
||
r_content = (await request.ctx.db_session.execute(
|
||
select(StoredContent).where(StoredContent.hash == cid.content_hash_b58)
|
||
)).scalars().first()
|
||
async def open_content_async(session, sc: StoredContent):
|
||
encrypted = sc if sc.encrypted else None
|
||
decrypted = sc if not sc.encrypted else None
|
||
if not sc.encrypted:
|
||
encrypted = (await session.execute(select(StoredContent).where(StoredContent.decrypted_content_id == sc.id))).scalars().first()
|
||
else:
|
||
decrypted = (await session.execute(select(StoredContent).where(StoredContent.id == sc.decrypted_content_id))).scalars().first()
|
||
if not encrypted:
|
||
raise AssertionError("Can't open content")
|
||
content_mime = None
|
||
if decrypted:
|
||
try:
|
||
content_mime = decrypted.json_format().get('content_type')
|
||
except Exception:
|
||
content_mime = None
|
||
if not content_mime:
|
||
meta = encrypted.meta or {}
|
||
content_mime = meta.get('content_type') or 'application/octet-stream'
|
||
try:
|
||
content_type = content_mime.split('/')[0]
|
||
except Exception:
|
||
content_type = 'application'
|
||
return {
|
||
'encrypted_content': encrypted,
|
||
'decrypted_content': decrypted,
|
||
'content_type': content_type,
|
||
'content_mime': content_mime,
|
||
}
|
||
content = await open_content_async(request.ctx.db_session, r_content)
|
||
encrypted_content = content['encrypted_content']
|
||
decrypted_content = content.get('decrypted_content')
|
||
content_mime = content.get('content_mime') or 'application/octet-stream'
|
||
is_audio = content_mime.startswith('audio/')
|
||
is_video = content_mime.startswith('video/')
|
||
content_kind = 'audio' if is_audio else ('video' if is_video else 'other')
|
||
|
||
master_address = encrypted_content.meta.get('item_address', '')
|
||
opts = {
|
||
'content_type': content_kind,
|
||
'content_kind': content_kind,
|
||
'content_mime': content_mime,
|
||
'content_address': license_address or master_address,
|
||
'license_address': license_address,
|
||
'master_address': master_address,
|
||
}
|
||
if encrypted_content.key_id:
|
||
known_key = (await request.ctx.db_session.execute(
|
||
select(KnownKey).where(KnownKey.id == encrypted_content.key_id)
|
||
)).scalars().first()
|
||
if known_key:
|
||
opts['key_hash'] = known_key.seed_hash # нахер не нужно на данный момент
|
||
|
||
# чисто болванки, заполнение дальше
|
||
opts['have_licenses'] = []
|
||
opts['invoice'] = None
|
||
|
||
have_access = False
|
||
if request.ctx.user:
|
||
user_wallet_address = await request.ctx.user.wallet_address_async(request.ctx.db_session)
|
||
have_access = (
|
||
(encrypted_content.owner_address == user_wallet_address)
|
||
or bool((await request.ctx.db_session.execute(select(UserContent).where(
|
||
and_(UserContent.owner_address == user_wallet_address, UserContent.status == 'active', UserContent.content_id == encrypted_content.id)
|
||
))).scalars().first()) \
|
||
or bool((await request.ctx.db_session.execute(select(StarsInvoice).where(
|
||
and_(
|
||
StarsInvoice.user_id == request.ctx.user.id,
|
||
StarsInvoice.content_hash == encrypted_content.hash,
|
||
StarsInvoice.paid == True
|
||
)
|
||
))).scalars().first())
|
||
)
|
||
|
||
if not have_access:
|
||
current_star_rate = (await ServiceConfig(request.ctx.db_session).get('live_tonPerStar', [0, 0]))[0]
|
||
if current_star_rate < 0:
|
||
current_star_rate = 0.00000001
|
||
|
||
stars_cost = int(int(encrypted_content.meta['license']['resale']['price']) / 1e9 / current_star_rate * 1.2)
|
||
if request.ctx.user.telegram_id in [5587262915, 6861699286]:
|
||
stars_cost = 2
|
||
|
||
invoice_id = f"access_{uuid.uuid4().hex}"
|
||
exist_invoice = (await request.ctx.db_session.execute(select(StarsInvoice).where(
|
||
and_(
|
||
StarsInvoice.user_id == request.ctx.user.id,
|
||
StarsInvoice.created > datetime.now() - timedelta(minutes=25),
|
||
StarsInvoice.amount == stars_cost,
|
||
StarsInvoice.content_hash == encrypted_content.hash,
|
||
)
|
||
))).scalars().first()
|
||
if exist_invoice:
|
||
invoice_url = exist_invoice.invoice_url
|
||
else:
|
||
invoice_url = None
|
||
try:
|
||
invoice_url = await Bot(token=CLIENT_TELEGRAM_API_KEY).create_invoice_link(
|
||
'Неограниченный доступ к контенту',
|
||
'Неограниченный доступ к контенту',
|
||
invoice_id, "XTR",
|
||
[
|
||
types.LabeledPrice(label='Lifetime access', amount=stars_cost),
|
||
], provider_token = ''
|
||
)
|
||
request.ctx.db_session.add(
|
||
StarsInvoice(
|
||
external_id=invoice_id,
|
||
type='access',
|
||
amount=stars_cost,
|
||
user_id=request.ctx.user.id,
|
||
content_hash=encrypted_content.hash,
|
||
invoice_url=invoice_url
|
||
)
|
||
)
|
||
await request.ctx.db_session.commit()
|
||
except BaseException as e:
|
||
make_log("Content", f"Can't create invoice link: {e}", level='warning')
|
||
|
||
if invoice_url:
|
||
opts['invoice'] = {
|
||
'url': invoice_url,
|
||
'amount': stars_cost,
|
||
}
|
||
|
||
display_options = {
|
||
'content_url': None,
|
||
'download_url': None,
|
||
'content_kind': content_kind,
|
||
'content_mime': content_mime,
|
||
}
|
||
|
||
if have_access:
|
||
opts['have_licenses'].append('listen')
|
||
|
||
enc_cid = encrypted_content.meta.get('content_cid') or encrypted_content.meta.get('encrypted_cid')
|
||
ec_v3 = None
|
||
derivative_rows = []
|
||
if enc_cid:
|
||
ec_v3 = (await request.ctx.db_session.execute(select(ECv3).where(ECv3.encrypted_cid == enc_cid))).scalars().first()
|
||
if ec_v3:
|
||
derivative_rows = (await request.ctx.db_session.execute(select(CDv3).where(CDv3.content_id == ec_v3.id))).scalars().all()
|
||
|
||
upload_row = None
|
||
if enc_cid:
|
||
upload_row = (await request.ctx.db_session.execute(select(UploadSession).where(UploadSession.encrypted_cid == enc_cid))).scalars().first()
|
||
|
||
converted_meta_map = dict(encrypted_content.meta.get('converted_content') or {})
|
||
|
||
derivative_latest = {}
|
||
if derivative_rows:
|
||
derivative_sorted = sorted(derivative_rows, key=lambda row: row.created_at or datetime.min)
|
||
for row in derivative_sorted:
|
||
derivative_latest[row.kind] = row
|
||
|
||
def _row_to_hash_and_url(row):
|
||
if not row or not row.local_path:
|
||
return None, None
|
||
file_hash = row.local_path.split('/')[-1]
|
||
return file_hash, f"{PROJECT_HOST}/api/v1.5/storage/{file_hash}"
|
||
|
||
preview_row = None
|
||
download_row = None
|
||
if have_access:
|
||
preview_priority = ['decrypted_low', 'decrypted_high'] if (is_audio or is_video) else []
|
||
download_priority = ['decrypted_high', 'decrypted_low']
|
||
if content_kind == 'other':
|
||
download_priority = ['decrypted_original']
|
||
for key in preview_priority:
|
||
if key in derivative_latest:
|
||
preview_row = derivative_latest[key]
|
||
break
|
||
for key in download_priority:
|
||
if key in derivative_latest:
|
||
download_row = derivative_latest[key]
|
||
break
|
||
else:
|
||
preview_priority = ['decrypted_preview', 'decrypted_low'] if (is_audio or is_video) else []
|
||
for key in preview_priority:
|
||
if key in derivative_latest:
|
||
preview_row = derivative_latest[key]
|
||
break
|
||
|
||
if preview_row:
|
||
file_hash, url = _row_to_hash_and_url(preview_row)
|
||
if url:
|
||
display_options['content_url'] = url
|
||
if preview_row.content_type and not opts.get('content_ext'):
|
||
opts['content_ext'] = (preview_row.content_type or '').split('/')[-1]
|
||
preview_map = {
|
||
'decrypted_low': 'low',
|
||
'decrypted_high': 'high',
|
||
'decrypted_preview': 'low_preview',
|
||
}
|
||
cache_key = preview_map.get(preview_row.kind)
|
||
if cache_key:
|
||
converted_meta_map.setdefault(cache_key, file_hash)
|
||
|
||
if download_row and have_access:
|
||
download_hash, download_url = _row_to_hash_and_url(download_row)
|
||
if download_url:
|
||
display_options['download_url'] = download_url
|
||
if download_row.content_type and not opts.get('content_ext'):
|
||
opts['content_ext'] = download_row.content_type.split('/')[-1]
|
||
download_map = {
|
||
'decrypted_high': 'high',
|
||
'decrypted_low': 'low',
|
||
'decrypted_original': 'original',
|
||
}
|
||
d_cache_key = download_map.get(download_row.kind)
|
||
if d_cache_key:
|
||
converted_meta_map.setdefault(d_cache_key, download_hash)
|
||
|
||
if not display_options['content_url'] and converted_meta_map:
|
||
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:
|
||
continue
|
||
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
|
||
if not opts.get('content_ext'):
|
||
opts['content_ext'] = stored.filename.split('.')[-1]
|
||
break
|
||
|
||
if have_access and not display_options['download_url'] and converted_meta_map:
|
||
download_keys = ['original', 'high', 'low']
|
||
for key in download_keys:
|
||
hash_value = converted_meta_map.get(key)
|
||
if not hash_value:
|
||
continue
|
||
stored = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == hash_value))).scalars().first()
|
||
if stored:
|
||
display_options['download_url'] = stored.web_url
|
||
if not opts.get('content_ext'):
|
||
opts['content_ext'] = stored.filename.split('.')[-1]
|
||
break
|
||
|
||
if not opts.get('content_ext'):
|
||
opts['content_ext'] = content_mime.split('/')[-1] if '/' in content_mime else None
|
||
|
||
# Metadata fallback
|
||
content_meta = encrypted_content.json_format()
|
||
content_metadata_json = None
|
||
_mcid = content_meta.get('metadata_cid') or None
|
||
if _mcid:
|
||
_cid = ContentId.deserialize(_mcid)
|
||
content_metadata = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == _cid.content_hash_b58))).scalars().first()
|
||
if content_metadata:
|
||
try:
|
||
with open(content_metadata.filepath, 'r') as f:
|
||
content_metadata_json = json.loads(f.read())
|
||
except Exception as exc:
|
||
make_log("Content", f"Can't read metadata file: {exc}", level='warning')
|
||
|
||
if not content_metadata_json:
|
||
fallback_name = (ec_v3.title if ec_v3 else None) or content_meta.get('title') or content_meta.get('cid')
|
||
fallback_description = (ec_v3.description if ec_v3 else '') or ''
|
||
fallback_artist = content_meta.get('artist') or None
|
||
content_metadata_json = {
|
||
'name': fallback_name or 'Без названия',
|
||
'title': fallback_name or 'Без названия',
|
||
'artist': fallback_artist,
|
||
'description': fallback_description,
|
||
'downloadable': False,
|
||
}
|
||
cover_cid = content_meta.get('cover_cid')
|
||
if cover_cid:
|
||
content_metadata_json.setdefault('image', f"{PROJECT_HOST}/api/v1.5/storage/{cover_cid}")
|
||
else:
|
||
if 'title' not in content_metadata_json or not content_metadata_json.get('title'):
|
||
content_metadata_json['title'] = content_metadata_json.get('name')
|
||
if 'artist' not in content_metadata_json:
|
||
inferred_artist = None
|
||
authors_list = content_metadata_json.get('authors')
|
||
if isinstance(authors_list, list) and authors_list:
|
||
inferred_artist = authors_list[0]
|
||
content_metadata_json['artist'] = inferred_artist
|
||
if content_metadata_json.get('artist') in ('', None):
|
||
content_metadata_json['artist'] = None
|
||
if not content_metadata_json.get('name'):
|
||
content_metadata_json['name'] = content_metadata_json.get('title') or 'Без названия'
|
||
|
||
if ec_v3 and not content_metadata_json.get('artist') and getattr(ec_v3, 'artist', None):
|
||
content_metadata_json['artist'] = ec_v3.artist
|
||
if ec_v3 and not content_metadata_json.get('title') and getattr(ec_v3, 'title', None):
|
||
content_metadata_json['title'] = ec_v3.title
|
||
|
||
display_title = content_metadata_json.get('title') or content_metadata_json.get('name') or 'Без названия'
|
||
display_artist = content_metadata_json.get('artist')
|
||
if display_artist:
|
||
content_metadata_json['display_name'] = f"{display_artist} – {display_title}"
|
||
else:
|
||
content_metadata_json['display_name'] = display_title
|
||
content_metadata_json['mime_type'] = content_mime
|
||
if 'file_extension' not in content_metadata_json or not content_metadata_json.get('file_extension'):
|
||
try:
|
||
content_metadata_json['file_extension'] = content_mime.split('/')[1]
|
||
except Exception:
|
||
content_metadata_json['file_extension'] = None
|
||
content_metadata_json['content_kind'] = content_kind
|
||
|
||
display_options['metadata'] = content_metadata_json
|
||
display_options['is_preview_available'] = bool(display_options.get('content_url'))
|
||
display_options['is_download_available'] = bool(display_options.get('download_url'))
|
||
|
||
base_downloadable = content_metadata_json.get('downloadable', False)
|
||
if content_kind == 'other':
|
||
base_downloadable = True
|
||
opts['downloadable'] = bool(display_options.get('download_url')) and base_downloadable and have_access
|
||
|
||
# Conversion status summary
|
||
conversion_summary = {}
|
||
conversion_details = []
|
||
derivative_summary_map = {}
|
||
for row in derivative_latest.values():
|
||
conversion_summary[row.status] = conversion_summary.get(row.status, 0) + 1
|
||
derivative_summary_map[row.kind] = row
|
||
conversion_details.append({
|
||
'kind': row.kind,
|
||
'status': row.status,
|
||
'size_bytes': row.size_bytes,
|
||
'content_type': row.content_type,
|
||
'error': row.error,
|
||
'updated_at': (row.last_access_at or row.created_at).isoformat() + 'Z' if (row.last_access_at or row.created_at) else None,
|
||
})
|
||
|
||
effective_mime = (ec_v3.content_type if ec_v3 and ec_v3.content_type else content_mime) or ''
|
||
if effective_mime.startswith('audio/'):
|
||
required_kinds = {'decrypted_low', 'decrypted_high'}
|
||
elif effective_mime.startswith('video/'):
|
||
required_kinds = {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
|
||
else:
|
||
required_kinds = {'decrypted_original'}
|
||
|
||
statuses_by_kind = {kind: row.status for kind, row in derivative_summary_map.items() if kind in required_kinds}
|
||
conversion_state = 'pending'
|
||
if required_kinds and all(statuses_by_kind.get(kind) == 'ready' for kind in required_kinds):
|
||
conversion_state = 'ready'
|
||
elif any(statuses_by_kind.get(kind) == 'failed' for kind in required_kinds):
|
||
conversion_state = 'failed'
|
||
elif any(statuses_by_kind.get(kind) in ('processing', 'pending') for kind in required_kinds):
|
||
conversion_state = 'processing'
|
||
elif statuses_by_kind:
|
||
conversion_state = 'partial'
|
||
|
||
if display_options['content_url'] or (content_kind == 'other' and display_options.get('download_url')):
|
||
conversion_state = 'ready'
|
||
|
||
upload_info = None
|
||
if upload_row:
|
||
upload_info = {
|
||
'id': upload_row.id,
|
||
'state': upload_row.state,
|
||
'error': upload_row.error,
|
||
'created_at': upload_row.created_at.isoformat() + 'Z' if upload_row.created_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'] or (content_kind == 'other' and display_options.get('download_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'
|
||
|
||
conversion_info = {
|
||
'state': conversion_state,
|
||
'summary': conversion_summary,
|
||
'details': conversion_details,
|
||
'required_kinds': list(required_kinds),
|
||
}
|
||
|
||
opts['conversion'] = conversion_info
|
||
opts['upload'] = upload_info
|
||
opts['status'] = {
|
||
'state': final_state,
|
||
'conversion_state': conversion_state,
|
||
'upload_state': upload_info['state'] if upload_info else None,
|
||
}
|
||
|
||
encrypted_payload = encrypted_content.json_format()
|
||
if ec_v3:
|
||
encrypted_payload['artist'] = getattr(ec_v3, 'artist', None)
|
||
return response.json({
|
||
**opts,
|
||
'encrypted': encrypted_payload,
|
||
'display_options': display_options,
|
||
})
|
||
|
||
|
||
async def s_api_v1_content_friendly_list(request):
|
||
# return html table with content list. bootstrap is used
|
||
|
||
result = """
|
||
<html>
|
||
<head>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||
</head>
|
||
<body>
|
||
<table class="table table-striped">
|
||
<thead>
|
||
<tr>
|
||
<th>CID</th>
|
||
<th>Title</th>
|
||
<th>Onchain</th>
|
||
<th>Preview link</th>
|
||
</tr>
|
||
</thead>
|
||
"""
|
||
contents = (await request.ctx.db_session.execute(select(StoredContent).where(
|
||
StoredContent.type == 'onchain/content'
|
||
))).scalars().all()
|
||
for content in contents:
|
||
if not content.meta.get('metadata_cid'):
|
||
make_log("Content", f"Content {content.cid.serialize_v2()} has no metadata", level='warning')
|
||
continue
|
||
|
||
from app.core.content.content_id import ContentId
|
||
_cid = ContentId.deserialize(content.meta.get('metadata_cid'))
|
||
metadata_content = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == _cid.content_hash_b58))).scalars().first()
|
||
with open(metadata_content.filepath, 'r') as f:
|
||
metadata = json.loads(f.read())
|
||
|
||
preview_link = None
|
||
if content.meta.get('converted_content'):
|
||
preview_link = f"{PROJECT_HOST}/api/v1.5/storage/{content.meta['converted_content']['low_preview']}"
|
||
|
||
result += f"""
|
||
<tr>
|
||
<td>{content.cid.serialize_v2()}</td>
|
||
<td>{metadata.get('name', "")}</td>
|
||
<td>{content.meta.get('item_address')}</td>
|
||
<td>""" + (f'<a href="{preview_link}">Preview</a>' if preview_link else "not ready") + """</td>
|
||
</tr>
|
||
"""
|
||
result += """
|
||
</table>
|
||
</body>
|
||
</html>
|
||
"""
|
||
return response.html(result)
|
||
|
||
async def s_api_v1_5_content_list(request):
|
||
# Validate offset and limit parameters
|
||
offset = int(request.args.get('offset', 0))
|
||
limit = int(request.args.get('limit', 100))
|
||
if offset < 0:
|
||
return response.json({'error': 'Invalid offset'}, status=400)
|
||
|
||
if limit <= 0 or limit > 1000:
|
||
return response.json({'error': 'Invalid limit'}, status=400)
|
||
|
||
# Query onchain contents which are not disabled
|
||
contents = (await request.ctx.db_session.execute(
|
||
select(StoredContent)
|
||
.where(StoredContent.type == 'onchain/content', StoredContent.disabled == False)
|
||
.order_by(StoredContent.created.desc())
|
||
.offset(offset).limit(limit)
|
||
)).scalars().all()
|
||
|
||
result = []
|
||
for content in contents:
|
||
# Retrieve metadata content using metadata_cid from content.meta
|
||
metadata_cid = content.meta.get('metadata_cid')
|
||
if not metadata_cid:
|
||
continue # Skip if no metadata_cid is found
|
||
|
||
from app.core.content.content_id import ContentId
|
||
_cid = ContentId.deserialize(metadata_cid)
|
||
metadata_content = (await request.ctx.db_session.execute(select(StoredContent).where(StoredContent.hash == _cid.content_hash_b58))).scalars().first()
|
||
try:
|
||
with open(metadata_content.filepath, 'r') as f:
|
||
metadata = json.load(f)
|
||
except Exception as e:
|
||
metadata = {}
|
||
|
||
media_type = 'audio'
|
||
|
||
# Get title from metadata (key 'name')
|
||
title = metadata.get('name', '')
|
||
|
||
# Build preview link if converted_content exists and contains 'low_preview'
|
||
preview_link = None
|
||
converted_content = content.meta.get('converted_content')
|
||
if converted_content:
|
||
converted_content = (await request.ctx.db_session.execute(select(StoredContent).where(
|
||
StoredContent.hash == converted_content['low_preview']
|
||
))).scalars().first()
|
||
preview_link = converted_content.web_url
|
||
if converted_content.filename.split('.')[-1] in ('mp4', 'mov'):
|
||
media_type = 'video'
|
||
else:
|
||
preview_link = None
|
||
|
||
# Get onchain address from content.meta
|
||
onchain_address = content.meta.get('item_address', '')
|
||
|
||
result.append({
|
||
'cid': content.cid.serialize_v2(),
|
||
'onchain_address': onchain_address,
|
||
'type': media_type,
|
||
'title': title,
|
||
'preview_link': preview_link,
|
||
'created_at': content.created.isoformat() # ISO 8601 format for datetime
|
||
})
|
||
|
||
return response.json(result)
|