From 2500ba19bd188a0d6093a946529b949e2a119edf Mon Sep 17 00:00:00 2001 From: shmyga Date: Wed, 16 Sep 2026 15:52:38 +0300 Subject: [PATCH] feat(api): add schedule groups api --- gallery/easel/__init__.py | 9 +++-- gallery/easel/depends/schedule.py | 34 ++++++++++++++++++- gallery/easel/route/api/__init__.py | 8 ++++- gallery/easel/route/api/schedule.py | 19 ++++++++++- gallery/easel/route/doc/__init__.py | 7 ++-- .../route/view/root/templates/root_index.html | 4 +-- gallery/easel/route/view/schedule/bundle.py | 18 ---------- gallery/easel/route/view/schedule/route.py | 17 ++++++---- gallery/painting/yandextv/api.py | 6 ++-- 9 files changed, 86 insertions(+), 36 deletions(-) delete mode 100644 gallery/easel/route/view/schedule/bundle.py diff --git a/gallery/easel/__init__.py b/gallery/easel/__init__.py index 055baf2..901c16e 100644 --- a/gallery/easel/__init__.py +++ b/gallery/easel/__init__.py @@ -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 diff --git a/gallery/easel/depends/schedule.py b/gallery/easel/depends/schedule.py index 132cafe..baa7682 100644 --- a/gallery/easel/depends/schedule.py +++ b/gallery/easel/depends/schedule.py @@ -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)] diff --git a/gallery/easel/route/api/__init__.py b/gallery/easel/route/api/__init__.py index 33200b8..4dd5ceb 100644 --- a/gallery/easel/route/api/__init__.py +++ b/gallery/easel/route/api/__init__.py @@ -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") diff --git a/gallery/easel/route/api/schedule.py b/gallery/easel/route/api/schedule.py index 93f3af9..e55cc56 100644 --- a/gallery/easel/route/api/schedule.py +++ b/gallery/easel/route/api/schedule.py @@ -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) diff --git a/gallery/easel/route/doc/__init__.py b/gallery/easel/route/doc/__init__.py index fdba2b2..b4eda22 100644 --- a/gallery/easel/route/doc/__init__.py +++ b/gallery/easel/route/doc/__init__.py @@ -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", ) diff --git a/gallery/easel/route/view/root/templates/root_index.html b/gallery/easel/route/view/root/templates/root_index.html index 12f0fd9..396c88a 100644 --- a/gallery/easel/route/view/root/templates/root_index.html +++ b/gallery/easel/route/view/root/templates/root_index.html @@ -17,13 +17,13 @@

{{_("Docs")}}

- Swagger UI - diff --git a/gallery/easel/route/view/schedule/bundle.py b/gallery/easel/route/view/schedule/bundle.py deleted file mode 100644 index 8638a36..0000000 --- a/gallery/easel/route/view/schedule/bundle.py +++ /dev/null @@ -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 - ], -} diff --git a/gallery/easel/route/view/schedule/route.py b/gallery/easel/route/view/schedule/route.py index bdfee8b..88cd6d8 100644 --- a/gallery/easel/route/view/schedule/route.py +++ b/gallery/easel/route/view/schedule/route.py @@ -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", diff --git a/gallery/painting/yandextv/api.py b/gallery/painting/yandextv/api.py index 9e1416c..aa8a07d 100644 --- a/gallery/painting/yandextv/api.py +++ b/gallery/painting/yandextv/api.py @@ -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: