feat(schedule): implement multiple schedule providers

This commit is contained in:
2026-06-26 16:36:40 +03:00
parent 8b20eb1220
commit a41b4e5308
20 changed files with 669 additions and 164 deletions

View File

@@ -0,0 +1,9 @@
from gallery.easel.core import AppRequest
from gallery.sketch.api import API
def api_resolver(api_type: type[API]):
def get_api(request: AppRequest, provider: str | None = None) -> API:
return request.app.state.api.get_api(api_type, provider)
return get_api

View File

@@ -0,0 +1,9 @@
from typing import Annotated
from fastapi import Depends
from gallery.sketch.schedule.api import ScheduleApi
from .api import api_resolver
ScheduleApiDepends = Annotated[ScheduleApi, Depends(api_resolver(ScheduleApi))]

View File

@@ -2,12 +2,8 @@ from typing import Annotated
from fastapi import Depends from fastapi import Depends
from gallery.easel.core import AppRequest
from gallery.sketch.weather.api import WeatherApi from gallery.sketch.weather.api import WeatherApi
from .api import api_resolver
async def get_weather_api(request: AppRequest, provider: str | None = None) -> WeatherApi: WeatherApiDepends = Annotated[WeatherApi, Depends(api_resolver(WeatherApi))]
return request.app.state.api.get_weather(provider)
WeatherApiDepends = Annotated[WeatherApi, Depends(get_weather_api)]

View File

@@ -3,13 +3,13 @@ import datetime
from fastapi import APIRouter from fastapi import APIRouter
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.sketch.schedule.model import ChannelId, Schedule from gallery.sketch.schedule.model import Channel, Schedule
router = APIRouter(prefix="/schedule") router = APIRouter(prefix="/schedule")
@router.get("/channels") @router.get("/channels")
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]: async def get_api_schedule_channels(request: AppRequest) -> list[Channel]:
schedule_api = request.app.state.api.schedule schedule_api = request.app.state.api.schedule
return await schedule_api.get_channels() return await schedule_api.get_channels()

View File

@@ -1,26 +1,18 @@
import datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.catalog import BUNDLE
from ..common.utils.tag import TagType, TagUtil from ..common.utils.tag import TagType, TagUtil
from ..common.utils.template import build_templates from ..common.utils.template import build_templates
from .filters import timedelta_format from .filters import timedelta_format
async def get_schedule_api(request: AppRequest, provider: str | None = None) -> ScheduleApi:
return request.app.state.api.get_schedule(provider)
ScheduleApiDepends = Annotated[ScheduleApi, Depends(get_schedule_api)]
def context_procesor(request: AppRequest) -> dict: def context_procesor(request: AppRequest) -> dict:
return { return {
"providers": request.app.state.api.get_api_providers(ScheduleApi.TYPE), "providers": request.app.state.api.get_api_providers(ScheduleApi.TYPE),
@@ -32,20 +24,20 @@ templates = build_templates(
{ {
"timedelta_format": timedelta_format, "timedelta_format": timedelta_format,
}, },
context_procesor,
) )
router = APIRouter(prefix="/schedule") router = APIRouter(prefix="/schedule")
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
async def get_schedule_list(request: AppRequest, schedule_api: ScheduleApiDepends): async def get_schedule_index(request: AppRequest, schedule_api: ScheduleApiDepends, query: str | None = None):
channels = await schedule_api.get_channels() channels = (await schedule_api.find_channels(query)) if query else []
channels_data = BUNDLE.select_items(channels)
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"channels": channels_data, "channels": channels,
}, },
) )
@@ -53,15 +45,15 @@ async def get_schedule_list(request: AppRequest, schedule_api: ScheduleApiDepend
@router.get("/tag/{tag}", response_class=HTMLResponse) @router.get("/tag/{tag}", response_class=HTMLResponse)
async def get_schedule_tag(request: AppRequest, schedule_api: ScheduleApiDepends, tag: str, live: bool = False): async def get_schedule_tag(request: AppRequest, schedule_api: ScheduleApiDepends, tag: str, live: bool = False):
tag_value = TagUtil.parse_tag(tag) tag_value = TagUtil.parse_tag(tag)
results = await schedule_api.get_all_schedules(tag_value.date) # results = await schedule_api.get_all_schedules(tag_value.date)
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="schedule.html", name="schedule.html",
context={ context={
"tag_util": TagUtil, "tag_util": TagUtil,
"datetime": datetime, "datetime": datetime,
"response": results[0], # "response": results[0],
"responses": results, # "responses": results,
"live": live, "live": live,
}, },
) )
@@ -72,11 +64,11 @@ async def get_channel_default(channel: str):
return RedirectResponse(f"{channel}/tag/today") return RedirectResponse(f"{channel}/tag/today")
@router.get("/{channel}/tag/{tag}", response_class=HTMLResponse) @router.get("/{provider}/{channel}/tag/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: str): async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: str):
tag_value = TagUtil.parse_tag(tag) tag_value = TagUtil.parse_tag(tag)
if tag_value.type == TagType.DAY: if tag_value.type == TagType.DAY:
response = await schedule_api.get_channel_schedule(channel, tag_value.date) response = await schedule_api.get_schedule(channel, tag_value.date)
else: else:
raise ValueError(tag) raise ValueError(tag)
return templates.TemplateResponse( return templates.TemplateResponse(

View File

@@ -3,16 +3,60 @@
{% block content %} {% block content %}
<h1>{{_("TV program")}}</h1> <h1>{{_("TV program")}}</h1>
<div class="list-group mb-5"> <form action=""
<a href="tag/today" method="get"
class="list-group-item list-group-item-action px-4"> class="mb-4">
<span class="fw-bold">Все</span> <div class="input-group mb-3">
</a> <input type="text"
{% for channel in channels %} class="form-control"
<a href="{{channel.id}}" id="query"
class="list-group-item list-group-item-action px-4"> name="query"
<span class="text-primary">{{channel.name}}</span> placeholder="{{_('Enter the channel name')}}">
</a> <input type="hidden"
class="form-control"
id="provider"
name="provider"
value="{{provider or providers[0]}}">
<button id="providerBtn"
class="btn btn-secondary dropdown-toggle"
type="text"
data-bs-toggle="dropdown"
aria-expanded="false">{{provider or providers[0]}}</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for provider in providers %}
<li>
<a class="dropdown-item"
onclick="provider.value='{{provider}}'; providerBtn.innerText='{{provider}}'">{{provider}}</a>
</li>
{% endfor %} {% endfor %}
</ul>
<button class="btn btn-primary"
type="submit">{{_("Search")}}</button>
</div> </div>
</form>
{% if channels %}
<ul id="channels"
class="list-group mb-5">
{% for channel in channels %}
<schedule-channel location="{{channel.model_dump() | tojson | forceescape}}"></schedule-channel>
{% endfor %}
</ul>
<hr>
{% endif %}
<ul id="storedChannels"
class="list-group mb-5">
</ul>
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const searchQuery = params.get('query');
if (searchQuery) {
document.querySelector('#query').value = searchQuery;
}
document.addEventListener("DOMContentLoaded", (event) => {
scheduleChannelManager.loadChannels('#storedChannels');
});
})();
</script>
{% endblock %} {% endblock %}

View File

@@ -4,7 +4,7 @@ import logging
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue from gallery.sketch.schedule.model import Channel, Schedule, ScheduleValue
from gallery.sketch.source import ApiSource from gallery.sketch.source import ApiSource
logger = logging.getLogger("matchtv") logger = logging.getLogger("matchtv")
@@ -14,18 +14,10 @@ class MatchTvApi(ScheduleApi):
PROVIDER = "matchtv" PROVIDER = "matchtv"
SOURCE = ApiSource("https://matchtv.ru") SOURCE = ApiSource("https://matchtv.ru")
async def get_channels(self) -> list[ChannelId]: async def find_channels(self, query: str) -> list[Channel]:
return [ raise NotImplementedError
ChannelId.MATCH_TV,
ChannelId.MATCH_IGRA,
ChannelId.MATCH_ARENA,
ChannelId.MATCH_FUTBOL_1,
ChannelId.MATCH_FUTBOL_2,
ChannelId.MATCH_FUTBOL_3,
ChannelId.MATCH_STRANA,
]
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule: async def get_channel_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}" endpoint = f"tvguide/{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")
@@ -50,4 +42,8 @@ class MatchTvApi(ScheduleApi):
if prev_value is not None: if prev_value is not None:
prev_value.end = item_date prev_value.end = item_date
prev_value = value prev_value = value
return Schedule(channel=Channel(id=channel_id, name=channel_name), date=date, values=values) return Schedule(
channel=Channel(id=channel_id, name=channel_name, provider=self.provider),
date=date,
values=values,
)

View File

@@ -1,27 +1,15 @@
import datetime import datetime
import json
import logging import logging
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue from gallery.sketch.schedule.model import Channel, Schedule, ScheduleValue
from gallery.sketch.source import ApiSource from gallery.sketch.source import ApiSource
logger = logging.getLogger("matchtv") logger = logging.getLogger("matchtv")
CHANNELS_MAP: dict[ChannelId, str] = {
ChannelId.MATCH_TV: "match-tv-49",
ChannelId.MATCH_IGRA: "match-igra-1174",
ChannelId.MATCH_ARENA: "match-arena-1173",
ChannelId.MATCH_FUTBOL_1: "match-futbol-1-646",
ChannelId.MATCH_FUTBOL_2: "match-futbol-2-593",
ChannelId.MATCH_FUTBOL_3: "match-futbol-3-797",
ChannelId.MATCH_STRANA: "match-strana-1356",
ChannelId.MATCH_PLANETA: "match-planeta-1177",
# ChannelId.EUROSPORT: "eurosport-677",
# ChannelId.EUROSPORT_2: "eurosport-2-720",
ChannelId.START: "start-103",
}
HEADERS: dict[str, str] = { HEADERS: dict[str, str] = {
"Accept": ( "Accept": (
@@ -57,11 +45,17 @@ class YandexTvApi(ScheduleApi):
PROVIDER = "yandextv" PROVIDER = "yandextv"
SOURCE = ApiSource("https://tv.yandex.ru", headers=HEADERS) SOURCE = ApiSource("https://tv.yandex.ru", headers=HEADERS)
async def get_channels(self) -> list[ChannelId]: async def find_channels(self, query: str) -> list[Channel]:
return list(CHANNELS_MAP.keys()) url = f"https://suggest-multi.yandex.ru/suggest-tv2?v=4&uil=ru&lr=10&count_channels=4&count_programs=4&sn=50&part={query}"
_, values = json.loads(await self.SOURCE.request(url))
result = []
for _, name, content in values[:-1]:
channel_id = content["url"].split("/")[-1]
result.append(Channel(id=channel_id, name=name, provider=self.provider))
return result
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule: async def get_channel_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}" endpoint = f"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:
@@ -85,4 +79,8 @@ class YandexTvApi(ScheduleApi):
if prev_value is not None: if prev_value is not None:
prev_value.end = item_date prev_value.end = item_date
prev_value = value prev_value = value
return Schedule(channel=Channel(id=channel_id, name=channel_name), date=date, values=values) return Schedule(
channel=Channel(id=channel_id, name=channel_name, provider=self.provider),
date=date,
values=values,
)

View File

@@ -1,6 +1,4 @@
from .api import API, Api from .api import API, Api
from .schedule.api import ScheduleApi
from .weather.api import WeatherApi
class ApiBundle: class ApiBundle:
@@ -21,9 +19,3 @@ class ApiBundle:
if provider is None or provider == value.provider: if provider is None or provider == value.provider:
return value return value
raise ValueError(api_type, provider) raise ValueError(api_type, provider)
def get_weather(self, provider: str | None = None) -> WeatherApi:
return self.get_api(WeatherApi, provider)
def get_schedule(self, provider: str | None = None) -> ScheduleApi:
return self.get_api(ScheduleApi, provider)

View File

@@ -1,14 +0,0 @@
from typing import Generic, TypeVar
T = TypeVar("T")
class CatalogBundle(Generic[T]):
def __init__(self, items: list[T]) -> None:
self._items_by_id = {item.id: item for item in items}
def get_item(self, item_id: str) -> T:
return self._items_by_id[item_id]
def select_items(self, ids: list[str]) -> list[T]:
return [self._items_by_id[id_] for id_ in ids]

View File

@@ -1,25 +1,15 @@
import asyncio
import datetime import datetime
from ..api import Api from ..api import Api
from .model import ChannelId, Schedule from .model import Channel, Schedule
class ScheduleApi(Api): class ScheduleApi(Api):
TYPE = "schedule" TYPE = "schedule"
INTERVAL: float = 0.5 INTERVAL: float = 0.5
async def get_channels(self) -> list[ChannelId]: async def find_channels(self, query: str) -> list[Channel]:
raise NotImplementedError raise NotImplementedError
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule: async def get_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
raise NotImplementedError raise NotImplementedError
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
channels = await self.get_channels()
results = []
for channel in channels:
results.append(await self.get_channel_schedule(channel_id=channel, date=date))
if self.INTERVAL > 0:
await asyncio.sleep(self.INTERVAL)
return results

View File

@@ -6,7 +6,7 @@ from gallery.sketch.cached import CachedApi, CachePreset
from gallery.util import TimeUnit from gallery.util import TimeUnit
from .api import ScheduleApi from .api import ScheduleApi
from .model import ChannelId, Schedule from .model import Channel, Schedule
CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6) CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6)
@@ -15,11 +15,11 @@ class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
CACHE_KEY = ScheduleApi.TYPE CACHE_KEY = ScheduleApi.TYPE
@cached( @cached(
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels", key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.find.{query}",
**CACHE_PRESET._asdict(), **CACHE_PRESET._asdict(),
) )
async def get_channels(self) -> list[ChannelId]: async def find_channels(self, query: str) -> list[Channel]:
return await self._api.get_channels() return await self._api.find_channels(query)
@cached( @cached(
key_builder=lambda fun, self, channel_id, date: ( key_builder=lambda fun, self, channel_id, date: (
@@ -27,12 +27,5 @@ class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
), ),
**CACHE_PRESET._asdict(), **CACHE_PRESET._asdict(),
) )
async def get_channel_schedule(self, channel_id: ChannelId, date: datetime.date) -> Schedule: async def get_channel_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
return await self._api.get_channel_schedule(channel_id, date) return await self._api.get_channel_schedule(channel_id, date)
@cached(
key_builder=lambda fun, self, date: (f"api.{self.CACHE_KEY}.{self.provider}.all.{date}"),
**CACHE_PRESET._asdict(),
)
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
return await self._api.get_all_schedules(date)

View File

@@ -1,21 +0,0 @@
from gallery.sketch.catalog import CatalogBundle
from .model import Channel, ChannelId
BUNDLE = CatalogBundle(
[
Channel(id=ChannelId.MATCH_TV, name="Матч ТВ"),
Channel(id=ChannelId.MATCH_IGRA, name="Матч! Игра"),
Channel(id=ChannelId.MATCH_ARENA, name="Матч! Арена"),
Channel(id=ChannelId.MATCH_FUTBOL_1, name="Футбол 1"),
Channel(id=ChannelId.MATCH_FUTBOL_2, name="Футбол 2"),
Channel(id=ChannelId.MATCH_FUTBOL_3, name="Футбол 3"),
Channel(id=ChannelId.MATCH_STRANA, name="Матч! Страна"),
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
Channel(id=ChannelId.EUROSPORT, name="Europsort"),
Channel(id=ChannelId.EUROSPORT_2, name="Europsort 2"),
Channel(id=ChannelId.START, name="Старт!"),
Channel(id=ChannelId.TEST, name="Тест"),
]
)

View File

@@ -1,5 +1,4 @@
import datetime import datetime
from enum import StrEnum
from pydantic import BaseModel from pydantic import BaseModel
@@ -9,27 +8,10 @@ class Model(BaseModel):
use_enum_values = True use_enum_values = True
class ChannelId(StrEnum):
MATCH_TV = "matchtv"
MATCH_IGRA = "igra"
MATCH_ARENA = "arena"
MATCH_FUTBOL_1 = "futbol-1"
MATCH_FUTBOL_2 = "futbol-2"
MATCH_FUTBOL_3 = "futbol-3"
MATCH_STRANA = "strana"
MATCH_PLANETA = "planeta"
EUROSPORT = "eurosport"
EUROSPORT_2 = "eurosport-2"
START = "start"
TEST = "test"
def __str__(self) -> str:
return self.value
class Channel(Model): class Channel(Model):
id: ChannelId id: str
name: str name: str
provider: str
class ScheduleValue(Model): class ScheduleValue(Model):

View File

@@ -26,6 +26,9 @@ class ApiSource:
self._headers = headers self._headers = headers
async def request(self, endpoint: str) -> str: async def request(self, endpoint: str) -> str:
if endpoint.startswith("https:"):
url = endpoint
else:
url = f"{self._base_url}/{endpoint}" url = f"{self._base_url}/{endpoint}"
logger.info(url) logger.info(url)
headers = {"User-Agent": self._user_agent, **(self._headers or {})} headers = {"User-Agent": self._user_agent, **(self._headers or {})}

View File

@@ -4,6 +4,7 @@ import "./language";
import "./main.scss"; import "./main.scss";
import "./theme"; import "./theme";
import "./weather/weather"; import "./weather/weather";
import "./schedule/schedule";
document.addEventListener("DOMContentLoaded", (event) => { document.addEventListener("DOMContentLoaded", (event) => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');

View File

@@ -0,0 +1,109 @@
export interface Channel {
id: string;
name: string;
provider: string;
}
export class ScheduleChannelStorage {
private storageKey: string = "schedule:channels";
getAll(): Channel[] {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
return Object.values(objects);
}
add(object: Channel) {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
objects[object.id] = object;
window.localStorage.setItem(this.storageKey, JSON.stringify(objects));
}
remove(id: string) {
const objects = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
delete objects[id];
window.localStorage.setItem(this.storageKey, JSON.stringify(objects));
}
}
export class ScheduleChannelManager {
loadChannels(selector: string) {
const channels = window.scheduleChannelStorage.getAll();
const container = document.querySelector(selector);
if (container) {
container.innerHTML = "";
for (const channel of channels) {
const element = new ScheduleChannelElement();
element.setAttribute("channel", JSON.stringify(channel));
element.setAttribute("removable", "");
container.appendChild(element);
}
}
}
}
class ScheduleChannelElement extends HTMLElement {
static observedAttributes = ["channel", "removable"];
channel: Channel | null = null;
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
}
connectedCallback() {
this.channel = JSON.parse(this.getAttribute("channel") || "{}");
this.innerHTML = `
<a href="${this.channel?.provider}/${this.channel?.id}"
class="list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start">
<span>
<span class="text-primary">${this.channel?.name}</span>
</span>
<span>
<span class="badge text-bg-secondary">${this.channel?.provider}</span>
<span class="text-danger ms-2" data-remove>
&#x2715;
</span>
</span>
</a>`;
this.addEventListener("click", this.handleClick);
if (this.hasAttribute("removable")) {
this.querySelector<HTMLElement>("[data-remove]")?.addEventListener("click", this.handleRemoveClick);
} else {
this.querySelector("[data-remove]")?.remove();
}
}
disconnectedCallback() {
this.removeEventListener("click", this.handleClick);
this.querySelector<HTMLElement>("[data-remove]")?.removeEventListener("click", this.handleRemoveClick);
}
handleClick(event: MouseEvent) {
if (this.channel) {
window.scheduleChannelStorage.add(this.channel);
}
}
handleRemoveClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.channel) {
window.scheduleChannelStorage.remove(this.channel.id);
}
this.remove();
}
}
customElements.define("schedule-channel", ScheduleChannelElement);
declare global {
interface Window {
scheduleChannelStorage: ScheduleChannelStorage;
scheduleChannelManager: ScheduleChannelManager;
}
}
window.scheduleChannelStorage = new ScheduleChannelStorage();
window.scheduleChannelManager = new ScheduleChannelManager();

View File

@@ -6,5 +6,6 @@ YANDEXTV_MOCK_SOURCE = MockSource(
Path(__file__).parent, Path(__file__).parent,
{ {
"test": "test.html", "test": "test.html",
"suggest-tv2": "suggest-tv2.json",
}, },
) )

View File

@@ -0,0 +1,423 @@
[
"матч",
[
[
"bemjson",
"Матч!",
{
"url": "https:\/\/tv.yandex.ru\/channels\/1593",
"target": "_self",
"bemjson": [
{
"elem": "icon",
"elemMods": {
"size": "l"
},
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/48x72",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/48x68",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/48x72",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/48x68",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/64x36",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/64x36",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/80x60",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/80x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/80x60",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/80x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/120x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/120x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/130x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/130x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/214x121",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/368x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/184x140",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/428x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/z",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/170x100",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/340x200",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/260x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x120",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/214x121",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/368x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/184x140",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/428x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/z",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/170x100",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/340x200",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/260x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x120",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/160x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016e2603ad95657bcd08dc16bad081\/orig",
"attrs": {
"style": "margin-right: 5px;"
}
},
{
"elem": "text",
"content": "Матч!"
}
],
"label": "Каналы"
}
],
[
"bemjson",
"Матч Премьер",
{
"url": "https:\/\/tv.yandex.ru\/channels\/1861",
"target": "_self",
"bemjson": [
{
"elem": "icon",
"elemMods": {
"size": "l"
},
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/80x60",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/80x60",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/120x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/120x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/130x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/130x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/184x140",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x120",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/260x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/428x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/340x200",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/z",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/214x121",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/368x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/170x100",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/184x140",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x120",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x90",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/260x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/428x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/340x200",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/z",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/214x121",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/160x160",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/368x280",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/170x100",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/28884\/2a00000164ea55a8e4e47294c57caf3f249c\/orig",
"attrs": {
"style": "margin-right: 5px;"
}
},
{
"elem": "text",
"content": "Матч Премьер"
}
],
"label": "Каналы"
}
],
[
"bemjson",
"Матч! Арена",
{
"url": "https:\/\/tv.yandex.ru\/channels\/1667",
"target": "_self",
"bemjson": [
{
"elem": "icon",
"elemMods": {
"size": "l"
},
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a00000160080262b3c5f1b231e9c1a50c16\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a00000160080262b3c5f1b231e9c1a50c16\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/30303\/2a0000016008026461f08cb3fdd86e3a2457\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/30303\/2a0000016008026461f08cb3fdd86e3a2457\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/51763\/2a0000016008026771d2f3c7dac0e8d437f9\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802686dbbbed380ec99afc655\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/51763\/2a000001600802653c391d2e32075819e8f6\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016008026696804b8c4e42f226e439\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/51763\/2a0000016008026771d2f3c7dac0e8d437f9\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802686dbbbed380ec99afc655\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/51763\/2a000001600802653c391d2e32075819e8f6\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a0000016008026696804b8c4e42f226e439\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802507cc14a8dfebd96835e47\/orig",
"attrs": {
"style": "margin-right: 5px;"
}
},
{
"elem": "text",
"content": "Матч! Арена"
}
],
"label": "Каналы"
}
],
[
"bemjson",
"Матч! Боец",
{
"url": "https:\/\/tv.yandex.ru\/channels\/454",
"target": "_self",
"bemjson": [
{
"elem": "icon",
"elemMods": {
"size": "l"
},
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/square",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/small",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/40x30",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/60x45",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/alice_64_48",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/x",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802e5ce16abfff4ef6582314b\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802e5ce16abfff4ef6582314b\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/114x80",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802ed550b3e50934bd9590a8d\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/27485\/2a000001600802ed550b3e50934bd9590a8d\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/middle",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/y",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802fb1f6c51e988763d812c9a\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a000001600803041768d758a55eb7e2badb\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/30303\/2a0000016008030a3439b27b50315e0282d5\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802f430b56de4f7f74fa9d062\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802fb1f6c51e988763d812c9a\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/70787\/2a000001600803041768d758a55eb7e2badb\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/30303\/2a0000016008030a3439b27b50315e0282d5\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802f430b56de4f7f74fa9d062\/orig",
"png": "\/\/avatars.mds.yandex.net\/get-tv-channel-logos\/69315\/2a000001600802bf4bc9e92d4f31f4a9878b\/orig",
"attrs": {
"style": "margin-right: 5px;"
}
},
{
"elem": "text",
"content": "Матч! Боец"
}
],
"label": "Каналы"
}
],
[
"bemjson",
"Матч Поинт",
{
"url": "https:\/\/tv.yandex.ru\/program\/8975707?eventId=258920426",
"target": "_self",
"label": "Передачи",
"bemjson": [
{
"elem": "info",
"content": [
{
"elem": "img",
"elemMods": {
"size": "m"
},
"src": "\/\/avatars.mds.yandex.net\/get-tv-shows\/28313\/2a0000019e1b8bb6911cbc36699f5e382156\/120x90"
},
{
"elem": "text",
"elemMods": {
"type": "title-url"
},
"content": "Матч Поинт"
},
{
"elem": "desc",
"content": "КИНОМАН, сегодня, 18:00"
}
]
}
]
}
],
[
"bemjson",
"Матч",
{
"url": "https:\/\/tv.yandex.ru\/program\/9094736?eventId=258988710",
"target": "_self",
"label": "Передачи",
"bemjson": [
{
"elem": "info",
"content": [
{
"elem": "img",
"elemMods": {
"size": "m"
},
"src": "\/\/avatars.mds.yandex.net\/get-tv-shows\/27487\/2a0000019ee0cfaf7272900dc66e5da38072\/120x90"
},
{
"elem": "text",
"elemMods": {
"type": "title-url"
},
"content": "Матч"
},
{
"elem": "desc",
"content": "Союзный, завтра, 19:00"
}
]
}
]
}
],
[
"bemjson",
"Матч-реванш",
{
"url": "https:\/\/tv.yandex.ru\/program\/8045748?eventId=258901794",
"target": "_self",
"label": "Передачи",
"bemjson": [
{
"elem": "info",
"content": [
{
"elem": "img",
"elemMods": {
"size": "m"
},
"src": "\/\/avatars.mds.yandex.net\/get-tv-shows\/28886\/2a0000019590a3a76b641bcec6df9bb179cc\/120x90"
},
{
"elem": "text",
"elemMods": {
"type": "title-url"
},
"content": "Матч-реванш"
},
{
"elem": "desc",
"content": "Советские мультфильмы, сегодня, 20:04"
}
]
}
]
}
],
[
"bemjson",
"Матч-реванш",
{
"url": "https:\/\/tv.yandex.ru\/program\/7398398?eventId=258967472",
"target": "_self",
"label": "Передачи",
"bemjson": [
{
"elem": "info",
"content": [
{
"elem": "img",
"elemMods": {
"size": "m"
},
"src": "\/\/avatars.mds.yandex.net\/get-tv-shows\/26422\/2a0000019409646c1723839178f3274f0a03\/120x90"
},
{
"elem": "text",
"elemMods": {
"type": "title-url"
},
"content": "Матч-реванш"
},
{
"elem": "desc",
"content": "Чижик, завтра, 07:55"
}
]
}
]
}
],
[
"bemjson",
"Все результаты",
{
"url": "https:\/\/tv.yandex.ru\/search?text=%D0%BC%D0%B0%D1%82%D1%87",
"target": "_self",
"label": "Другое",
"bemjson": [
{
"elem": "info",
"elemMods": {
"type": "title_url"
},
"content": "Все результаты"
}
]
}
]
]
]

View File

@@ -2,8 +2,7 @@ import datetime
import pytest import pytest
from gallery.painting.yandextv.api import CHANNELS_MAP, YandexTvApi from gallery.painting.yandextv.api import YandexTvApi
from gallery.sketch.schedule.model import ChannelId
from tests.data.yandextv import YANDEXTV_MOCK_SOURCE from tests.data.yandextv import YANDEXTV_MOCK_SOURCE
@@ -11,13 +10,16 @@ from tests.data.yandextv import YANDEXTV_MOCK_SOURCE
def yandextv_api_fixture() -> YandexTvApi: def yandextv_api_fixture() -> YandexTvApi:
api = YandexTvApi() api = YandexTvApi()
api.SOURCE = YANDEXTV_MOCK_SOURCE api.SOURCE = YANDEXTV_MOCK_SOURCE
CHANNELS_MAP[ChannelId("test")] = "test"
return api return api
async def test_search(yandextv_api: YandexTvApi):
result = await yandextv_api.find_channels("матч")
print(result)
assert len(result) == 8
async def test_channel(yandextv_api: YandexTvApi): async def test_channel(yandextv_api: YandexTvApi):
result = await yandextv_api.get_channel_schedule( result = await yandextv_api.get_channel_schedule("test", datetime.date.today())
ChannelId.TEST, datetime.date.today()
)
assert result is not None assert result is not None
assert len(result.values) > 0 assert len(result.values) > 0