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 import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from gallery.sketch.bundle import ApiBundle from gallery.sketch.bundle import ApiBundle
from gallery.util import root_path from gallery.util import root_path
from .route import api, doc, view from .route import api, view
def build_app(api_bundle: ApiBundle) -> FastAPI: def build_app(api_bundle: ApiBundle) -> FastAPI:
@@ -15,8 +16,12 @@ def build_app(api_bundle: ApiBundle) -> FastAPI:
) )
app.state.api = api_bundle app.state.api = api_bundle
app.mount("/static", StaticFiles(directory=root_path / "static/dist")) app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
doc.mount(app)
api.app.state.api = api_bundle api.app.state.api = api_bundle
app.mount("/api", api.app) app.mount("/api", api.app)
app.include_router(view.router) app.include_router(view.router)
@app.get("/docs")
def get_docs():
return RedirectResponse("/api/docs")
return app return app
+33 -1
View File
@@ -1,4 +1,4 @@
from typing import Annotated from typing import Annotated, NamedTuple
from fastapi import Depends from fastapi import Depends
@@ -7,3 +7,35 @@ from gallery.sketch.schedule.api import ScheduleApi
from .api import api_resolver from .api import api_resolver
ScheduleApiDepends = Annotated[ScheduleApi, Depends(api_resolver(ScheduleApi))] 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 import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from gallery.easel.route import doc
from . import schedule, weather from . import schedule, weather
app = FastAPI(openapi_prefix="/api") app = FastAPI(
docs_url=None,
redoc_url=None,
)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@@ -13,3 +18,4 @@ app.add_middleware(
) )
app.include_router(weather.router) app.include_router(weather.router)
app.include_router(schedule.router) app.include_router(schedule.router)
doc.mount(app, "/api")
+18 -1
View File
@@ -1,7 +1,11 @@
from fastapi import APIRouter from fastapi import APIRouter
from gallery.easel.core import AppRequest 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.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, Schedule from gallery.sketch.schedule.model import Channel, Schedule
@@ -10,6 +14,19 @@ from ...depends.date import DateDepends
router = APIRouter(prefix="/schedule", tags=["Schedule"]) 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") @router.get("/providers")
async def get_api_weather_providers(request: AppRequest) -> list[str]: async def get_api_weather_providers(request: AppRequest) -> list[str]:
return request.app.state.api.get_api_providers(ScheduleApi) 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 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( app.mount(
"/docs/static", "/docs/static",
StaticFiles(directory=Path(__file__).parent / "static"), StaticFiles(directory=Path(__file__).parent / "static"),
@@ -17,7 +18,7 @@ def mount(app: FastAPI):
@app.get("/docs", include_in_schema=False) @app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html(): async def custom_swagger_ui_html():
return get_swagger_ui_html( return get_swagger_ui_html(
openapi_url=app.openapi_url, openapi_url=openapi_url,
title=app.title + " - Swagger UI", title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="docs/static/swagger-ui-bundle.js", swagger_js_url="docs/static/swagger-ui-bundle.js",
@@ -31,7 +32,7 @@ def mount(app: FastAPI):
@app.get("/redoc", include_in_schema=False) @app.get("/redoc", include_in_schema=False)
async def redoc_html(): async def redoc_html():
return get_redoc_html( return get_redoc_html(
openapi_url=app.openapi_url, openapi_url=openapi_url,
title=app.title + " - ReDoc", title=app.title + " - ReDoc",
redoc_js_url="docs/static/redoc.standalone.js", redoc_js_url="docs/static/redoc.standalone.js",
) )
@@ -17,13 +17,13 @@
</div> </div>
<h1>{{_("Docs")}}</h1> <h1>{{_("Docs")}}</h1>
<div class="list-group mb-3"> <div class="list-group mb-3">
<app-link href="/docs" <app-link href="/api/docs"
target="_blank" target="_blank"
icon="file-earmark-text" icon="file-earmark-text"
class="list-group-item list-group-item-action"> class="list-group-item list-group-item-action">
Swagger UI Swagger UI
</app-link> </app-link>
<app-link href="/redoc" <app-link href="/api/redoc"
target="_blank" target="_blank"
icon="file-earmark-text" icon="file-earmark-text"
class="list-group-item list-group-item-action"> 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.core import AppRequest
from gallery.easel.depends.api import api_resolver from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.date import DateDepends 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.bundle import ApiBundle
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Schedule from gallery.sketch.schedule.model import Schedule
from ..common.utils.template import build_templates from ..common.utils.template import build_templates
from .bundle import GROUP_MAP
from .filters import timedelta_format from .filters import timedelta_format
@@ -45,7 +48,7 @@ async def get_schedule_index(request: AppRequest, provider: str | None = None, q
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", 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) @router.get("/group/{group_id}/{date}", response_class=HTMLResponse)
async def get_group(request: AppRequest, group_id: str, date: DateDepends): async def get_group(request: AppRequest, channels: ScheduleGroupChannelsDepends, date: DateDepends):
channels = GROUP_MAP.get(group_id, [tuple(item.split(":")) for item in group_id.split(",")]) responses = [
responses = await _get_group_schedule(request.app.state.api, channels, date) await request.app.state.api.get_api(ScheduleApi, channel.provider).get_schedule(channel.id, date)
for channel in channels
]
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="group.html", name="group.html",
+4 -2
View File
@@ -16,13 +16,15 @@ logger = logging.getLogger("matchtv")
class YandexTvApi(ScheduleApi): class YandexTvApi(ScheduleApi):
PROVIDER = "yandextv" PROVIDER = "yandextv"
lr = 213 # magic id
def _build_source(self) -> ApiSource: def _build_source(self) -> ApiSource:
return ApiSource("https://tv.yandex.ru", headers=build_headers("tv.yandex.ru")) return ApiSource("https://tv.yandex.ru", headers=build_headers("tv.yandex.ru"))
async def find_channels(self, query: str) -> list[Channel]: async def find_channels(self, query: str) -> list[Channel]:
url = ( url = (
"https://suggest-multi.yandex.ru/suggest-tv2?" "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)) _, values = json.loads(await self.source.request(url))
result = [] result = []
@@ -33,7 +35,7 @@ class YandexTvApi(ScheduleApi):
return result return result
async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule: 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) data = await self.source.request(endpoint)
soup = BeautifulSoup(data, features="html.parser") soup = BeautifulSoup(data, features="html.parser")
if soup.select_one(".CheckboxCaptcha") is not None: if soup.select_one(".CheckboxCaptcha") is not None: