149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""Search and paginate the public live TV catalog at the database boundary."""
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import Select, func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.time import to_iso8601_utc
|
|
from app.models.collected_data import CollectedData
|
|
from app.services.tv_streams import (
|
|
TV_LIVE_SOURCE_COLLECTOR,
|
|
TV_LIVE_SOURCE_DATA_TYPE,
|
|
_build_collected_tv_source,
|
|
build_public_tv_payload,
|
|
get_tv_settings_payload,
|
|
)
|
|
|
|
|
|
def _source_key():
|
|
return func.coalesce(
|
|
func.nullif(CollectedData.extra_data["id"].as_string(), ""),
|
|
func.nullif(CollectedData.source_id, ""),
|
|
CollectedData.entity_key,
|
|
)
|
|
|
|
|
|
def _collected_catalog_query(configured_ids: list[str]) -> Select[tuple[CollectedData]]:
|
|
metadata = CollectedData.extra_data
|
|
source_id = _source_key()
|
|
enabled = func.lower(func.trim(func.coalesce(metadata["is_enabled"].as_string(), "true")))
|
|
ranked = (
|
|
select(
|
|
CollectedData.id,
|
|
func.row_number()
|
|
.over(
|
|
partition_by=source_id,
|
|
order_by=CollectedData.id.desc(),
|
|
)
|
|
.label("source_rank"),
|
|
)
|
|
.where(
|
|
CollectedData.source == TV_LIVE_SOURCE_COLLECTOR,
|
|
CollectedData.data_type == TV_LIVE_SOURCE_DATA_TYPE,
|
|
CollectedData.is_current.is_(True),
|
|
CollectedData.is_valid == 1,
|
|
CollectedData.deleted_at.is_(None),
|
|
enabled.notin_(("false", "0", "no", "off")),
|
|
source_id.notin_(configured_ids),
|
|
)
|
|
.subquery()
|
|
)
|
|
return (
|
|
select(CollectedData)
|
|
.join(ranked, ranked.c.id == CollectedData.id)
|
|
.where(ranked.c.source_rank == 1)
|
|
)
|
|
|
|
|
|
def _filter_catalog_query(
|
|
query: Select[tuple[CollectedData]], terms: list[str]
|
|
) -> Select[tuple[CollectedData]]:
|
|
metadata = CollectedData.extra_data
|
|
searchable = func.lower(
|
|
func.concat_ws(
|
|
" ",
|
|
CollectedData.name,
|
|
CollectedData.title,
|
|
CollectedData.source_id,
|
|
metadata["name"].as_string(),
|
|
metadata["provider"].as_string(),
|
|
metadata["region"].as_string(),
|
|
metadata["country"].as_string(),
|
|
metadata["language"].as_string(),
|
|
)
|
|
)
|
|
for term in terms:
|
|
query = query.where(searchable.contains(term, autoescape=True))
|
|
return query
|
|
|
|
|
|
def _matches_source(source: dict[str, Any], terms: list[str]) -> bool:
|
|
searchable = " ".join(
|
|
str(source.get(key) or "") for key in ("id", "name", "provider", "region", "language")
|
|
).lower()
|
|
return all(term in searchable for term in terms)
|
|
|
|
|
|
async def get_tv_catalog_page(
|
|
db: AsyncSession,
|
|
*,
|
|
offset: int = 0,
|
|
limit: int = 50,
|
|
q: str = "",
|
|
selected_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
settings = await get_tv_settings_payload(db)
|
|
payload = build_public_tv_payload(settings, [])
|
|
configured = payload["sources"]
|
|
query = _collected_catalog_query([source["id"] for source in settings["sources"]])
|
|
if selected_id:
|
|
selected = next((source for source in configured if source["id"] == selected_id), None)
|
|
if selected is None:
|
|
record = await db.scalar(query.where(_source_key() == selected_id).limit(1))
|
|
selected = _build_collected_tv_source(record, 0) if record else None
|
|
if selected:
|
|
payload["selected_source"] = selected
|
|
summary = (
|
|
await db.execute(
|
|
select(func.count(), func.max(CollectedData.collected_at))
|
|
.select_from(CollectedData)
|
|
.where(CollectedData.id.in_(query.with_only_columns(CollectedData.id)))
|
|
)
|
|
).one()
|
|
total_collected, latest_update = summary
|
|
terms = q.lower().split()
|
|
matched_configured = [source for source in configured if _matches_source(source, terms)]
|
|
filtered_query = _filter_catalog_query(query, terms)
|
|
matched_collected = (
|
|
await db.scalar(select(func.count()).select_from(filtered_query.subquery()))
|
|
if terms
|
|
else total_collected
|
|
)
|
|
sources = matched_configured[offset : offset + limit]
|
|
remaining = limit - len(sources)
|
|
if remaining:
|
|
rows = await db.scalars(
|
|
filtered_query.order_by(func.lower(CollectedData.name), CollectedData.id)
|
|
.offset(max(0, offset - len(matched_configured)))
|
|
.limit(remaining)
|
|
)
|
|
sources.extend(
|
|
_build_collected_tv_source(record, index) for index, record in enumerate(rows)
|
|
)
|
|
total = len(matched_configured) + matched_collected
|
|
next_offset = offset + len(sources)
|
|
return {
|
|
**payload,
|
|
"sources": sources,
|
|
"source_count": len(configured) + total_collected,
|
|
"latest_updated_at": (
|
|
to_iso8601_utc(latest_update) if latest_update else payload["latest_updated_at"]
|
|
),
|
|
"total": total,
|
|
"offset": offset,
|
|
"limit": limit,
|
|
"has_more": next_offset < total,
|
|
"next_offset": next_offset if next_offset < total else None,
|
|
}
|