feat(api): add schedule groups api

This commit is contained in:
2026-09-16 15:52:38 +03:00
parent f257caf42c
commit 2500ba19bd
9 changed files with 86 additions and 36 deletions
+7 -2
View File
@@ -1,10 +1,11 @@
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from gallery.sketch.bundle import ApiBundle
from gallery.util import root_path
from .route import api, doc, view
from .route import api, view
def build_app(api_bundle: ApiBundle) -> FastAPI:
@@ -15,8 +16,12 @@ def build_app(api_bundle: ApiBundle) -> FastAPI:
)
app.state.api = api_bundle
app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
doc.mount(app)
api.app.state.api = api_bundle
app.mount("/api", api.app)
app.include_router(view.router)
@app.get("/docs")
def get_docs():
return RedirectResponse("/api/docs")
return app
+33 -1
View File
@@ -1,4 +1,4 @@
from typing import Annotated
from typing import Annotated, NamedTuple
from fastapi import Depends
@@ -7,3 +7,35 @@ from gallery.sketch.schedule.api import ScheduleApi
from .api import api_resolver
ScheduleApiDepends = Annotated[ScheduleApi, Depends(api_resolver(ScheduleApi))]
class ChannelDef(NamedTuple):
provider: str
id: str
GROUP_CANNELS_MAP: dict[str, list[ChannelDef]] = {
"central": [
ChannelDef("yandextv", "pervyy-16"),
ChannelDef("yandextv", "rossiya-1-31"),
ChannelDef("yandextv", "ntv-11"),
ChannelDef("yandextv", "sts-8"),
],
"sport": [
ChannelDef("matchtv", "matchtv"),
ChannelDef("matchtv", "igra"),
ChannelDef("matchtv", "arena"),
ChannelDef("matchtv", "futbol-1"),
ChannelDef("matchtv", "futbol-2"),
ChannelDef("matchtv", "futbol-3"),
ChannelDef("matchtv", "strana"),
ChannelDef("yandextv", "start-103"),
],
}
def resolve_group_channels(group_id: str) -> list[ChannelDef]:
return GROUP_CANNELS_MAP.get(group_id) or [ChannelDef(*item.split(":")) for item in group_id.split(",")]
ScheduleGroupChannelsDepends = Annotated[list[ChannelDef], Depends(resolve_group_channels)]
+7 -1
View File
@@ -1,9 +1,14 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from gallery.easel.route import doc
from . import schedule, weather
app = FastAPI(openapi_prefix="/api")
app = FastAPI(
docs_url=None,
redoc_url=None,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -13,3 +18,4 @@ app.add_middleware(
)
app.include_router(weather.router)
app.include_router(schedule.router)
doc.mount(app, "/api")
+18 -1
View File
@@ -1,7 +1,11 @@
from fastapi import APIRouter
from gallery.easel.core import AppRequest
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.easel.depends.schedule import (
GROUP_CANNELS_MAP,
ScheduleApiDepends,
ScheduleGroupChannelsDepends,
)
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, Schedule
@@ -10,6 +14,19 @@ from ...depends.date import DateDepends
router = APIRouter(prefix="/schedule", tags=["Schedule"])
@router.get("/groups")
async def get_api_schedule_groups():
return list(GROUP_CANNELS_MAP.keys())
@router.get("/group/{group_id}/{date}")
async def get_api_schedule_group_schedule(
request: AppRequest, channels: ScheduleGroupChannelsDepends, date: DateDepends
) -> list[Schedule]:
api = request.app.state.api
return [await api.get_api(ScheduleApi, channel.provider).get_schedule(channel.id, date) for channel in channels]
@router.get("/providers")
async def get_api_weather_providers(request: AppRequest) -> list[str]:
return request.app.state.api.get_api_providers(ScheduleApi)
+4 -3
View File
@@ -8,7 +8,8 @@ from fastapi.openapi.docs import (
from fastapi.staticfiles import StaticFiles
def mount(app: FastAPI):
def mount(app: FastAPI, prefix: str | None = None):
openapi_url = prefix + app.openapi_url if prefix else app.openapi_url
app.mount(
"/docs/static",
StaticFiles(directory=Path(__file__).parent / "static"),
@@ -17,7 +18,7 @@ def mount(app: FastAPI):
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
openapi_url=openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="docs/static/swagger-ui-bundle.js",
@@ -31,7 +32,7 @@ def mount(app: FastAPI):
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
openapi_url=openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="docs/static/redoc.standalone.js",
)
@@ -17,13 +17,13 @@
</div>
<h1>{{_("Docs")}}</h1>
<div class="list-group mb-3">
<app-link href="/docs"
<app-link href="/api/docs"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
Swagger UI
</app-link>
<app-link href="/redoc"
<app-link href="/api/redoc"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
@@ -1,18 +0,0 @@
GROUP_MAP = {
"central": [
("yandextv", "146"), # pervyy
("yandextv", "587"), # rossya-1
("yandextv", "162"), # ntv
("yandextv", "79"), # sts
],
"sport": [
("matchtv", "matchtv"),
("matchtv", "igra"),
("matchtv", "arena"),
("matchtv", "futbol-1"),
("matchtv", "futbol-2"),
("matchtv", "futbol-3"),
("matchtv", "strana"),
("yandextv", "1799"), # start
],
}
+11 -6
View File
@@ -8,13 +8,16 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from gallery.easel.core import AppRequest
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.date import DateDepends
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.easel.depends.schedule import (
GROUP_CANNELS_MAP,
ScheduleApiDepends,
ScheduleGroupChannelsDepends,
)
from gallery.sketch.bundle import ApiBundle
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Schedule
from ..common.utils.template import build_templates
from .bundle import GROUP_MAP
from .filters import timedelta_format
@@ -45,7 +48,7 @@ async def get_schedule_index(request: AppRequest, provider: str | None = None, q
return templates.TemplateResponse(
request=request,
name="index.html",
context={"channels": [item.model_dump() for item in channels], "groups": GROUP_MAP.keys()},
context={"channels": [item.model_dump() for item in channels], "groups": GROUP_CANNELS_MAP.keys()},
)
@@ -61,9 +64,11 @@ async def get_group_default(group_id: str):
@router.get("/group/{group_id}/{date}", response_class=HTMLResponse)
async def get_group(request: AppRequest, group_id: str, date: DateDepends):
channels = GROUP_MAP.get(group_id, [tuple(item.split(":")) for item in group_id.split(",")])
responses = await _get_group_schedule(request.app.state.api, channels, date)
async def get_group(request: AppRequest, channels: ScheduleGroupChannelsDepends, date: DateDepends):
responses = [
await request.app.state.api.get_api(ScheduleApi, channel.provider).get_schedule(channel.id, date)
for channel in channels
]
return templates.TemplateResponse(
request=request,
name="group.html",
+4 -2
View File
@@ -16,13 +16,15 @@ logger = logging.getLogger("matchtv")
class YandexTvApi(ScheduleApi):
PROVIDER = "yandextv"
lr = 213 # magic id
def _build_source(self) -> ApiSource:
return ApiSource("https://tv.yandex.ru", headers=build_headers("tv.yandex.ru"))
async def find_channels(self, query: str) -> list[Channel]:
url = (
"https://suggest-multi.yandex.ru/suggest-tv2?"
f"v=4&uil=ru&lr=10&count_channels=4&count_programs=4&sn=50&part={query}"
f"v=4&uil=ru&lr={self.lr}&count_channels=4&count_programs=4&sn=50&part={query}"
)
_, values = json.loads(await self.source.request(url))
result = []
@@ -33,7 +35,7 @@ class YandexTvApi(ScheduleApi):
return result
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"channels/{channel_id}?date={date:%Y-%m-%d}"
endpoint = f"{self.lr}/channel/{channel_id}?date={date:%Y-%m-%d}"
data = await self.source.request(endpoint)
soup = BeautifulSoup(data, features="html.parser")
if soup.select_one(".CheckboxCaptcha") is not None: