smashed updated

This commit is contained in:
root 2025-10-16 16:23:36 +00:00
parent 77921ba6a8
commit 1da0b26320
14 changed files with 321 additions and 96 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ venv
logs
sqlStorage
playground
alembic.ini
.DS_Store
messages.pot
activeConfig

View File

@ -1,3 +1,4 @@
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
@ -7,6 +8,10 @@ from alembic import context
config = context.config
database_url = os.environ.get("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)

View File

@ -56,6 +56,15 @@ async def s_api_v1_blockchain_send_new_content_message(request):
assert field_key in request.json, f"No {field_key} provided"
assert field_value(request.json[field_key]), f"Invalid {field_key} provided"
artist = request.json.get('artist')
if artist is not None:
assert isinstance(artist, str), "Invalid artist provided"
artist = artist.strip()
if artist == "":
artist = None
else:
artist = None
# Support legacy: 'content' as decrypted ContentId; and new: 'content' as encrypted IPFS CID
source_content_cid, cid_err = resolve_content(request.json['content'])
assert not cid_err, f"Invalid content CID provided: {cid_err}"
@ -85,11 +94,16 @@ async def s_api_v1_blockchain_send_new_content_message(request):
image_content = None
content_title = f"{', '.join(request.json['authors'])} {request.json['title']}" if request.json['authors'] else request.json['title']
content_title = request.json['title']
if artist:
content_title = f"{artist} {content_title}"
elif request.json['authors']:
content_title = f"{', '.join(request.json['authors'])} {request.json['title']}"
metadata_content = await create_metadata_for_item(
request.ctx.db_session,
title=content_title,
title=request.json['title'],
artist=artist,
cover_url=f"{PROJECT_HOST}/api/v1.5/storage/{image_content_cid.serialize_v2()}" if image_content_cid else None,
authors=request.json['authors'],
hashtags=request.json['hashtags'],

View File

@ -181,11 +181,14 @@ def _storage_download_url(file_hash: Optional[str]) -> Optional[str]:
def _pick_primary_download(candidates: List[tuple[str, Optional[str], Optional[int]]]) -> Optional[str]:
priority = (
'decrypted_high',
'decrypted_original',
'decrypted_low',
'decrypted_preview',
'high',
'low',
'preview',
'original',
'stored',
)
for target in priority:
for kind, url, _ in candidates:
@ -656,6 +659,7 @@ async def s_api_v1_admin_uploads(request):
}
search_parts: List[Any] = [
content.artist,
content.title,
content.description,
content.encrypted_cid,
@ -697,6 +701,7 @@ async def s_api_v1_admin_uploads(request):
'metadata_cid': metadata_cid,
'content_hash': content_hash,
'title': content.title,
'artist': content.artist,
'description': content.description,
'content_type': content.content_type,
'size': {
@ -1342,10 +1347,21 @@ async def s_api_v1_admin_licenses(request):
metadata_candidate = stored_meta.get('metadata') if isinstance(stored_meta.get('metadata'), dict) else {}
title_candidates = [
stored_meta.get('title'),
metadata_candidate.get('title') if isinstance(metadata_candidate, dict) else None,
metadata_candidate.get('name') if isinstance(metadata_candidate, dict) else None,
stored_meta.get('license', {}).get('title') if isinstance(stored_meta.get('license'), dict) else None,
]
title_value = next((value for value in title_candidates if isinstance(value, str) and value.strip()), None)
artist_candidates = []
if isinstance(metadata_candidate, dict):
artist_candidates.extend([
metadata_candidate.get('artist'),
(metadata_candidate.get('authors') or [None])[0] if isinstance(metadata_candidate.get('authors'), list) else None,
])
artist_candidates.extend([
stored_meta.get('artist'),
])
artist_value = next((value for value in artist_candidates if isinstance(value, str) and value.strip()), None)
try:
cid_value = stored_content.cid.serialize_v2()
except Exception:
@ -1354,7 +1370,8 @@ async def s_api_v1_admin_licenses(request):
'id': stored_content.id,
'hash': stored_content.hash,
'cid': cid_value,
'title': title_value or stored_content.hash,
'title': (title_value or stored_content.hash),
'artist': artist_value,
'type': stored_content.type,
'owner_address': stored_content.owner_address,
'onchain_index': stored_content.onchain_index,
@ -1773,11 +1790,17 @@ async def s_api_v1_admin_status(request):
ec = (await session.execute(select(EncryptedContent))).scalars().all()
backlog = 0
for e in ec:
if not e.preview_enabled:
ctype = (e.content_type or '').lower()
if ctype.startswith('audio/'):
req = {'decrypted_low', 'decrypted_high'}
elif ctype.startswith('video/'):
req = {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
else:
req = {'decrypted_original'}
if not req:
continue
kinds = [d.kind for d in deriv if d.content_id == e.id and d.status == 'ready']
req = {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
if not req.issubset(set(kinds)):
kinds = {d.kind for d in deriv if d.content_id == e.id and d.status == 'ready'}
if not req.issubset(kinds):
backlog += 1
try:
bs = await bitswap_stat()

View File

@ -2,7 +2,6 @@ from datetime import datetime, timedelta
from sanic import response
from sqlalchemy import select, and_, func
from aiogram import Bot, types
from sqlalchemy import and_
from app.core.logger import make_log
from app.core.models._config import ServiceConfig
from app.core.models.node_storage import StoredContent
@ -67,31 +66,53 @@ async def s_api_v1_content_view(request, content_address: str):
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:
decrypted = sc
encrypted = (await session.execute(select(StoredContent).where(StoredContent.decrypted_content_id == sc.id))).scalars().first()
else:
encrypted = sc
decrypted = (await session.execute(select(StoredContent).where(StoredContent.id == sc.decrypted_content_id))).scalars().first()
assert decrypted and encrypted, "Can't open content"
ctype = decrypted.json_format().get('content_type', 'application/x-binary')
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 = ctype.split('/')[0]
content_type = content_mime.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': 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 = content['encrypted_content'].meta.get('item_address', '')
master_address = encrypted_content.meta.get('item_address', '')
opts = {
'content_type': content['content_type'], # возможно с ошибками, нужно переделать на ffprobe
'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 content['encrypted_content'].key_id:
if encrypted_content.key_id:
known_key = (await request.ctx.db_session.execute(
select(KnownKey).where(KnownKey.id == content['encrypted_content'].key_id)
select(KnownKey).where(KnownKey.id == encrypted_content.key_id)
)).scalars().first()
if known_key:
opts['key_hash'] = known_key.seed_hash # нахер не нужно на данный момент
@ -104,14 +125,14 @@ async def s_api_v1_content_view(request, content_address: str):
if request.ctx.user:
user_wallet_address = await request.ctx.user.wallet_address_async(request.ctx.db_session)
have_access = (
(content['encrypted_content'].owner_address == user_wallet_address)
(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 == content['encrypted_content'].id)
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 == content['encrypted_content'].hash,
StarsInvoice.content_hash == encrypted_content.hash,
StarsInvoice.paid == True
)
))).scalars().first())
@ -122,7 +143,7 @@ async def s_api_v1_content_view(request, content_address: str):
if current_star_rate < 0:
current_star_rate = 0.00000001
stars_cost = int(int(content['encrypted_content'].meta['license']['resale']['price']) / 1e9 / current_star_rate * 1.2)
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
@ -132,7 +153,7 @@ async def s_api_v1_content_view(request, content_address: str):
StarsInvoice.user_id == request.ctx.user.id,
StarsInvoice.created > datetime.now() - timedelta(minutes=25),
StarsInvoice.amount == stars_cost,
StarsInvoice.content_hash == content['encrypted_content'].hash,
StarsInvoice.content_hash == encrypted_content.hash,
)
))).scalars().first()
if exist_invoice:
@ -154,7 +175,7 @@ async def s_api_v1_content_view(request, content_address: str):
type='access',
amount=stars_cost,
user_id=request.ctx.user.id,
content_hash=content['encrypted_content'].hash,
content_hash=encrypted_content.hash,
invoice_url=invoice_url
)
)
@ -168,12 +189,17 @@ async def s_api_v1_content_view(request, content_address: str):
'amount': stars_cost,
}
display_options = {'content_url': None}
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 = content['encrypted_content'].meta.get('content_cid') or content['encrypted_content'].meta.get('encrypted_cid')
enc_cid = encrypted_content.meta.get('content_cid') or encrypted_content.meta.get('encrypted_cid')
ec_v3 = None
derivative_rows = []
if enc_cid:
@ -185,7 +211,7 @@ async def s_api_v1_content_view(request, content_address: str):
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(content['encrypted_content'].meta.get('converted_content') or {})
converted_meta_map = dict(encrypted_content.meta.get('converted_content') or {})
derivative_latest = {}
if derivative_rows:
@ -199,24 +225,57 @@ 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}"
chosen_row = None
preview_row = None
download_row = None
if have_access:
for key in ('decrypted_low', 'decrypted_high'):
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:
chosen_row = derivative_latest[key]
preview_row = derivative_latest[key]
break
for key in download_priority:
if key in derivative_latest:
download_row = derivative_latest[key]
break
else:
for key in ('decrypted_preview', 'decrypted_low'):
preview_priority = ['decrypted_preview', 'decrypted_low'] if (is_audio or is_video) else []
for key in preview_priority:
if key in derivative_latest:
chosen_row = derivative_latest[key]
preview_row = derivative_latest[key]
break
if chosen_row:
file_hash, url = _row_to_hash_and_url(chosen_row)
if preview_row:
file_hash, url = _row_to_hash_and_url(preview_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)
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']
@ -227,11 +286,28 @@ 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]
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 = content['encrypted_content'].json_format()
content_meta = encrypted_content.json_format()
content_metadata_json = None
_mcid = content_meta.get('metadata_cid') or None
if _mcid:
@ -247,20 +323,58 @@ async def s_api_v1_content_view(request, content_address: str):
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'))
opts['downloadable'] = content_metadata_json.get('downloadable', False)
if opts['downloadable'] and 'listen' not in opts['have_licenses']:
opts['downloadable'] = False
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 = {}
@ -278,9 +392,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,
})
required_kinds = {'decrypted_low', 'decrypted_high'}
if ec_v3 and ec_v3.content_type.startswith('video/'):
required_kinds.add('decrypted_preview')
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'
@ -293,7 +411,7 @@ async def s_api_v1_content_view(request, content_address: str):
elif statuses_by_kind:
conversion_state = 'partial'
if display_options['content_url']:
if display_options['content_url'] or (content_kind == 'other' and display_options.get('download_url')):
conversion_state = 'ready'
upload_info = None
@ -306,7 +424,7 @@ 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
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'):
@ -331,9 +449,12 @@ async def s_api_v1_content_view(request, content_address: str):
'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': content['encrypted_content'].json_format(),
'encrypted': encrypted_payload,
'display_options': display_options,
})

View File

@ -27,9 +27,12 @@ async def s_api_v1_upload_status(request, upload_id: str):
{"kind": kind, "status": status}
for kind, status in derivative_rows
]
required = {"decrypted_high", "decrypted_low"}
if ec.preview_enabled and ec.content_type.startswith("video/"):
required.add("decrypted_preview")
if ec.content_type and ec.content_type.startswith("audio/"):
required = {"decrypted_high", "decrypted_low"}
elif ec.content_type and ec.content_type.startswith("video/"):
required = {"decrypted_high", "decrypted_low", "decrypted_preview"}
else:
required = {"decrypted_original"}
statuses = {kind: status for kind, status in derivative_rows}
if required and all(statuses.get(k) == "ready" for k in required):
conv_state = "ready"

View File

@ -69,6 +69,7 @@ async def s_api_v1_upload_tus_hook(request):
meta = upload.get("MetaData") or {}
# Common metadata keys
title = meta.get("title") or meta.get("Title") or meta.get("name") or "Untitled"
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/")
@ -155,6 +156,7 @@ async def s_api_v1_upload_tus_hook(request):
ec = EncryptedContent(
encrypted_cid=encrypted_cid,
title=title,
artist=artist or None,
description=description,
content_type=content_type,
enc_size_bytes=enc_size,
@ -196,7 +198,9 @@ async def s_api_v1_upload_tus_hook(request):
'storage': 'ipfs',
'encrypted_cid': encrypted_cid,
'upload_id': upload_id,
'source': 'tusd'
'source': 'tusd',
'title': title,
'artist': artist or None,
}
encrypted_stored_content = StoredContent(
type="local/encrypted_ipfs",
@ -218,12 +222,14 @@ async def s_api_v1_upload_tus_hook(request):
"encrypted_cid": encrypted_cid,
"title": title,
"description": description,
"artist": artist,
"content_type": content_type,
"size_bytes": enc_size,
"preview_enabled": preview_enabled,
"preview_conf": ec.preview_conf,
"issuer_node_id": key_fpr,
"salt_b64": _b64(salt),
"artist": artist or None,
}
try:
from app.core._crypto.signer import Signer

View File

@ -58,9 +58,12 @@ async def _compute_content_status(db_session, encrypted_cid: Optional[str], fall
'updated_at': (row.last_access_at or row.created_at).isoformat() + 'Z' if (row.last_access_at or row.created_at) else None,
})
required = {'decrypted_low', 'decrypted_high'}
if content_type.startswith('video/'):
required.add('decrypted_preview')
if content_type.startswith('audio/'):
required = {'decrypted_low', 'decrypted_high'}
elif content_type.startswith('video/'):
required = {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
else:
required = {'decrypted_original'}
statuses_by_kind = {kind: derivative_latest[kind].status for kind in required if kind in derivative_latest}
conversion_state = 'pending'

View File

@ -8,7 +8,7 @@ from datetime import datetime
from pathlib import Path
from typing import List, Optional, Tuple
from sqlalchemy import select
from sqlalchemy import select, and_, or_
from app.core.logger import make_log
from app.core.storage import db_session
@ -196,17 +196,45 @@ async def _convert_content(ec: EncryptedContent, staging: PlainStaging):
plain_filename = f"{ec.encrypted_cid}.{input_ext}" if input_ext else ec.encrypted_cid
async with db_session() as session:
existing = (await session.execute(select(StoredContent).where(StoredContent.hash == file_hash))).scalars().first()
if not existing:
if existing:
sc = existing
sc.type = sc.type or "local/content_bin"
sc.filename = plain_filename
sc.meta = {
**(sc.meta or {}),
'encrypted_cid': ec.encrypted_cid,
'kind': 'original',
'content_type': ec.content_type,
}
sc.updated = datetime.utcnow()
else:
sc = StoredContent(
type="local/content_bin",
hash=file_hash,
user_id=None,
filename=plain_filename,
meta={'encrypted_cid': ec.encrypted_cid, 'kind': 'original'},
meta={
'encrypted_cid': ec.encrypted_cid,
'kind': 'original',
'content_type': ec.content_type,
},
created=datetime.utcnow(),
)
session.add(sc)
await session.flush()
encrypted_records = (await session.execute(select(StoredContent).where(StoredContent.hash == encrypted_hash_b58))).scalars().all()
for encrypted_sc in encrypted_records:
meta = dict(encrypted_sc.meta or {})
converted = dict(meta.get('converted_content') or {})
converted['original'] = file_hash
meta['converted_content'] = converted
if 'content_type' not in meta:
meta['content_type'] = ec.content_type
encrypted_sc.meta = meta
encrypted_sc.decrypted_content_id = sc.id
encrypted_sc.updated = datetime.utcnow()
derivative = ContentDerivative(
content_id=ec.id,
kind='decrypted_original',
@ -341,10 +369,17 @@ async def _convert_content(ec: EncryptedContent, staging: PlainStaging):
async def _pick_pending(limit: int) -> List[Tuple[EncryptedContent, PlainStaging]]:
async with db_session() as session:
# Find A/V contents with preview_enabled and no ready low/low_preview derivatives yet
ecs = (await session.execute(select(EncryptedContent).where(
EncryptedContent.preview_enabled == True
).order_by(EncryptedContent.created_at.desc()))).scalars().all()
# Include preview-enabled media and non-media content that need decrypted originals
non_media_filter = and_(
EncryptedContent.content_type.isnot(None),
~EncryptedContent.content_type.like('audio/%'),
~EncryptedContent.content_type.like('video/%'),
)
ecs = (await session.execute(
select(EncryptedContent)
.where(or_(EncryptedContent.preview_enabled == True, non_media_filter))
.order_by(EncryptedContent.created_at.desc())
)).scalars().all()
picked: List[Tuple[EncryptedContent, PlainStaging]] = []
for ec in ecs:
@ -365,7 +400,12 @@ async def _pick_pending(limit: int) -> List[Tuple[EncryptedContent, PlainStaging
# Check if derivatives already ready
rows = (await session.execute(select(ContentDerivative).where(ContentDerivative.content_id == ec.id))).scalars().all()
kinds_ready = {r.kind for r in rows if r.status == 'ready'}
required = {'decrypted_low', 'decrypted_high'} if ec.content_type.startswith('audio/') else {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
if ec.content_type.startswith('audio/'):
required = {'decrypted_low', 'decrypted_high'}
elif ec.content_type.startswith('video/'):
required = {'decrypted_low', 'decrypted_high', 'decrypted_preview'}
else:
required = {'decrypted_original'}
if required.issubset(kinds_ready):
continue
# Always decrypt from IPFS using local or remote key

View File

@ -86,11 +86,14 @@ async def indexer_loop(memory, platform_found: bool, seqno: int) -> [bool, int]:
wallet_owner_user = await session.get(User, wallet_owner_connection.user_id) if wallet_owner_connection else None
if wallet_owner_user.telegram_id:
wallet_owner_bot = Wrapped_CBotChat(memory._telegram_bot, chat_id=wallet_owner_user.telegram_id, user=wallet_owner_user, db_session=session)
meta_title = content_metadata.get('title') or content_metadata.get('name') or 'Unknown'
meta_artist = content_metadata.get('artist')
formatted_title = f"{meta_artist} {meta_title}" if meta_artist else meta_title
await wallet_owner_bot.send_message(
user.translated('p_licenseWasBought').format(
username=user.front_format(),
nft_address=f'"https://tonviewer.com/{new_license.onchain_address}"',
content_title=content_metadata.get('name', 'Unknown'),
content_title=formatted_title,
),
message_type='notification',
)

View File

@ -115,6 +115,7 @@ def _clean_text_content(text: str, is_hashtag: bool = False) -> str:
async def create_metadata_for_item(
db_session,
title: str = None,
artist: str = None,
cover_url: str = None,
authors: list = None,
hashtags: list = [],
@ -128,6 +129,15 @@ async def create_metadata_for_item(
cleaned_title = cleaned_title[:100].strip() # Truncate and strip after cleaning
assert len(cleaned_title) > 3, f"Cleaned title '{cleaned_title}' (from original '{title}') is too short or became empty after cleaning."
cleaned_artist = None
if artist:
cleaned_artist = _clean_text_content(artist, is_hashtag=False)
cleaned_artist = cleaned_artist[:100].strip()
if not cleaned_artist:
cleaned_artist = None
display_name = f"{cleaned_artist} {cleaned_title}" if cleaned_artist else cleaned_title
# Process and clean hashtags
processed_hashtags = []
if hashtags and isinstance(hashtags, list):
@ -142,17 +152,21 @@ async def create_metadata_for_item(
processed_hashtags = list(dict.fromkeys(processed_hashtags))[:10]
item_metadata = {
'name': cleaned_title,
'attributes': [
# {
# 'trait_type': 'Artist',
# 'value': 'Unknown'
# },
],
'name': display_name,
'title': cleaned_title,
'display_name': display_name,
'downloadable': downloadable,
'tags': processed_hashtags, # New field for storing the list of cleaned hashtags
'attributes': [],
}
if cleaned_artist:
item_metadata['artist'] = cleaned_artist
item_metadata['attributes'].append({
'trait_type': 'Artist',
'value': cleaned_artist,
})
# Generate description from the processed hashtags
item_metadata['description'] = ' '.join([f"#{h}" for h in processed_hashtags if h])

View File

@ -1,4 +1,3 @@
import html
from sqlalchemy import and_, select
from app.core.models.node_storage import StoredContent
from app.core.models.content.user_content import UserContent, UserAction
@ -26,7 +25,6 @@ class PlayerTemplates:
text = ""
content_metadata_json = {}
description_block = ""
status_hint = ""
if content:
assert content.type.startswith('onchain/content'), "Invalid nodeStorage content type"
cd_log = f"Content (SHA256: {content.hash}), Encrypted: {content.encrypted}, TelegramCID: {content.telegram_cid}. "
@ -114,9 +112,6 @@ class PlayerTemplates:
if encrypted_content_row:
break
if not local_content:
status_hint = self.user.translated('p_playerContext_contentNotReady')
description = (content_metadata_json.get('description') or '').strip()
encrypted_description = (encrypted_content_row.description or '').strip() if encrypted_content_row and encrypted_content_row.description else ''
if not description and encrypted_description:
@ -124,18 +119,24 @@ class PlayerTemplates:
if description:
description_block = f"{description}\n"
title = (
content_metadata_json.get('name')
or (encrypted_content_row.title if encrypted_content_row and encrypted_content_row.title else None)
or (local_content.filename if local_content else None)
or (content.filename if content else None)
or content.cid.serialize_v2()
)
status_block = f"{status_hint}\n" if status_hint else ""
metadata_title = content_metadata_json.get('title') or content_metadata_json.get('name')
if not metadata_title:
metadata_title = (
(encrypted_content_row.title if encrypted_content_row and encrypted_content_row.title else None)
or (local_content.filename if local_content else None)
or (content.filename if content else None)
or content.cid.serialize_v2()
)
metadata_artist = content_metadata_json.get('artist')
if metadata_artist in ('', None):
metadata_artist = None
if not metadata_artist:
encrypted_artist = getattr(encrypted_content_row, 'artist', None)
metadata_artist = encrypted_artist if encrypted_artist else metadata_artist
title = f"{metadata_artist} {metadata_title}" if metadata_artist else metadata_title
text = f"""<b>{title}</b>
{description_block}{status_block}Этот контент был загружен в MY
{description_block}Этот контент был загружен в MY
\t/ p2p content market /
<blockquote><a href="{content_share_link['url']}">🔴 «открыть в MY»</a></blockquote>"""
@ -152,16 +153,7 @@ class PlayerTemplates:
)
)).scalars().all()
if not local_content:
if not processing_messages:
notice = f"Контент «{html.escape(title)}» обрабатывается. Как только всё будет готово, отправим полную публикацию."
await self.send_message(
notice,
message_type='content/processing',
message_meta={'content_id': content.id},
content_id=content.id,
)
else:
if local_content and processing_messages:
for msg in processing_messages:
await self.delete_message(msg.message_id)

View File

@ -16,6 +16,7 @@ class EncryptedContent(AlchemyBase):
# Public metadata
title = Column(String(512), nullable=False)
artist = Column(String(512), nullable=True)
description = Column(String(4096), nullable=True)
content_type = Column(String(64), nullable=False) # e.g. audio/flac, video/mp4, application/octet-stream

View File

@ -18,3 +18,4 @@ pillow==10.2.0
ffmpeg-python==0.2.0
python-magic==0.4.27
cryptography==42.0.5
alembic==1.13.1