35 Commits

Author SHA1 Message Date
bad5dff2b2 ci(version): 0.4.2 2026-07-22 21:52:30 +03:00
e2af31b812 fix(gismeteo): fix empty wind direction value in parser 2026-07-22 21:52:15 +03:00
6c76dae760 ci(version): 0.4.1 2026-07-02 14:17:55 +03:00
28951df9b5 fix(openweather): optional visability field in forecast model 2026-07-02 14:17:48 +03:00
5aa290c772 ci(version): 0.4.0 2026-07-02 14:06:15 +03:00
13a70990e5 feat(weather): add hail precipitation variant 2026-07-02 02:59:49 +03:00
a3fab92e3d feat(view): fix index page 2026-07-02 02:43:43 +03:00
4fdb7e9717 fix(view): fix index pages api resolving 2026-07-02 01:30:38 +03:00
1d13f6e05d refactor(doc): update docs static 2026-07-01 22:48:24 +03:00
fcb000afb0 test: fix tests 2026-07-01 21:05:49 +03:00
0730b9f567 build(poetry): update python dependencies 2026-07-01 20:53:01 +03:00
081aa8d17f feat(openweather): add location name to forecast response 2026-07-01 16:40:16 +03:00
29fe06462e feat(schedule): implement multiple schedule providers 2026-06-27 16:03:28 +03:00
8b20eb1220 test(openweather): add openweather location seacrh test 2026-06-26 16:27:36 +03:00
5333690c47 feat(weather): implement provider in weather api endpoints 2026-06-25 19:19:21 +03:00
d82fc4ea46 feat(weather): improve weather locations list 2026-06-24 21:02:06 +03:00
df2f1d0d81 refactor(easel): add view root module 2026-06-24 16:00:39 +03:00
9192ca3e4a feat(easel): implement multiple weather providers 2026-06-24 16:00:39 +03:00
e37ca6eeac feat(easel): add api provider 2026-06-24 16:00:39 +03:00
657a62eab5 ci(version): 0.3.4 2026-06-24 15:54:43 +03:00
d8e2be013a fix(gismeteo): fix precipitation parser 2026-06-24 15:54:35 +03:00
a91ec12d22 ci(version): 0.3.3 2026-06-22 15:33:36 +03:00
9c862863e5 fix(gismeteo): fix precipitation parser 2026-06-22 15:33:19 +03:00
fc59e52337 ci(version): 0.3.2 2026-06-20 18:59:43 +03:00
3e93a41400 fix(gismeteo): fix date-time row parsing 2026-06-20 18:59:27 +03:00
c2cd18386b refactor(easel): update api router 2026-06-16 21:18:16 +03:00
2bca3dd75a ci(version): 0.3.1 2026-06-16 20:17:37 +03:00
469bd9bc1f feat(easel): add version to header 2026-06-16 20:17:12 +03:00
027d1e2d55 build(docker): add docker build caches mount 2026-06-16 20:01:29 +03:00
7cf0012229 feat(schedule): update navigate icons 2026-06-16 20:00:46 +03:00
edc014d98c docs: update screenshot 2026-06-15 23:37:31 +03:00
1813ec213b ci(version): 0.3.0 2026-06-15 23:19:57 +03:00
1b700086f2 feat(weather): update navigate icons 2026-06-15 23:18:48 +03:00
02a6ffc931 feat(weather): new weather icons 2026-06-15 11:22:41 +03:00
2dfedfea57 fix(easel): fix header icons 2026-06-15 11:22:28 +03:00
87 changed files with 9869 additions and 7063 deletions

View File

@@ -2,4 +2,7 @@ DOCKER_REPO=git.shmyga.ru
DOCKER_GROUP=infernalgames DOCKER_GROUP=infernalgames
DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP" DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP"
VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+') VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+')
PYTHON_VERSION=$(grep -m 1 'python' ./pyproject.toml | grep -oP 'python\s*=\s*"\^?\K[^"]+')
DOCKER_PROJECTS=("gallery") DOCKER_PROJECTS=("gallery")
OPENWEATHER_KEY="<EMPTY>"

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
#.vscode #.vscode
static/node_modules static/node_modules
static/dist static/dist
.env

View File

@@ -1,4 +1,4 @@
FROM python:3.12 AS builder FROM python:3.14 AS builder
ENV POETRY_HOME="/opt/poetry" ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH" ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt update && \ RUN apt update && \
@@ -7,7 +7,9 @@ WORKDIR /app
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -sSL https://install.python-poetry.org | python3 -
COPY pyproject.toml poetry.lock README.md ./ COPY pyproject.toml poetry.lock README.md ./
RUN poetry config virtualenvs.in-project true RUN poetry config virtualenvs.in-project true
RUN poetry install --with app --no-root RUN --mount=type=cache,target=/root/.cache/pypoetry/cache \
--mount=type=cache,target=/root/.cache/pypoetry/artifacts \
poetry install --with app --no-root
COPY locales ./locales COPY locales ./locales
RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po
@@ -15,11 +17,12 @@ FROM node:24 AS node-builder
ENV PATH=/app/node_modules/.bin:$PATH ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app WORKDIR /app
COPY static/package.json static/package-lock.json ./ COPY static/package.json static/package-lock.json ./
RUN npm ci RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY static ./ COPY static ./
RUN npm run build RUN npm run build
FROM python:3.12-slim FROM python:3.14-slim
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app WORKDIR /app
@@ -27,7 +30,6 @@ ENV TZ="Europe/Moscow"
COPY --from=builder /app ./ COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/ COPY gallery gallery/
#COPY --from=builder /app/gallery/easel/route/view/locales /app/gallery/easel/route/view/locales
COPY --from=builder --parents locales/**/*.mo ./ COPY --from=builder --parents locales/**/*.mo ./
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"] CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]

View File

@@ -12,6 +12,7 @@ services:
build: . build: .
environment: environment:
- REDIS_HOST=redis - REDIS_HOST=redis
- OPENWEATHER_KEY=$OPENWEATHER_KEY
- DEBUG=1 - DEBUG=1
ports: ports:
- 8000:80 - 8000:80

View File

@@ -12,6 +12,7 @@ services:
image: ${DOCKER_ROOT}/gallery image: ${DOCKER_ROOT}/gallery
environment: environment:
- REDIS_HOST=redis - REDIS_HOST=redis
- OPENWEATHER_KEY=$OPENWEATHER_KEY
depends_on: depends_on:
- redis - redis
ports: ports:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -8,6 +8,7 @@
"python.testing.pytestArgs": ["tests", "-s"], "python.testing.pytestArgs": ["tests", "-s"],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.terminal.useEnvFile": true,
"python-envs.pythonProjects": [ "python-envs.pythonProjects": [
{ {
"path": ".", "path": ".",

View File

@@ -4,8 +4,7 @@ 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 from .route import api, doc, view
from .route.view import router as view_router
def build_app(api_bundle: ApiBundle) -> FastAPI: def build_app(api_bundle: ApiBundle) -> FastAPI:
@@ -17,6 +16,6 @@ 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) doc.mount(app)
api.mount(app) app.include_router(api.router)
app.include_router(view_router) app.include_router(view.router)
return app return app

View File

View File

@@ -0,0 +1,17 @@
from fastapi import HTTPException, status
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) -> API:
providers = request.app.state.api.get_api_providers(api_type)
if provider not in providers:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"provider": f"'{provider}' not in {providers}"},
)
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

@@ -0,0 +1,9 @@
from typing import Annotated
from fastapi import Depends
from gallery.sketch.weather.api import WeatherApi
from .api import api_resolver
WeatherApiDepends = Annotated[WeatherApi, Depends(api_resolver(WeatherApi))]

View File

@@ -1,8 +1,7 @@
from fastapi import FastAPI from fastapi import APIRouter
from . import schedule, weather from . import schedule, weather
router = APIRouter(prefix="/api")
def mount(app: FastAPI): router.include_router(weather.router)
weather.mount(app) router.include_router(schedule.router)
schedule.mount(app)

View File

@@ -1,18 +1,27 @@
import datetime import datetime
from fastapi import FastAPI 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.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, Schedule
router = APIRouter(prefix="/schedule", tags=["Schedule"])
def mount(app: FastAPI): @router.get("/providers")
@app.get("/api/schedule/channels", tags=["API"]) async def get_api_weather_providers(request: AppRequest) -> list[str]:
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]: return request.app.state.api.get_api_providers(ScheduleApi)
schedule_api = request.app.state.api.schedule
return await schedule_api.get_channels()
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
async def get_api_schedule_channel_schedule(request: AppRequest, channel: str, date: datetime.date) -> Schedule: @router.get("/{provider}/channels")
schedule_api = request.app.state.api.schedule async def find_api_schedule_channels(schedule_api: ScheduleApiDepends, query: str) -> list[Channel]:
return await schedule_api.get_channel_schedule(ChannelId(channel), date) return await schedule_api.find_channels(query)
@router.get("/{provider}/{channel}/{date}")
async def get_api_schedule_channel_schedule(
schedule_api: ScheduleApiDepends, channel: str, date: datetime.date
) -> Schedule:
return await schedule_api.get_schedule(channel, date)

View File

@@ -1,23 +1,30 @@
import datetime import datetime
from fastapi import FastAPI from fastapi import APIRouter
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import Location, WeatherResponse from gallery.sketch.weather.model import Location, WeatherResponse
router = APIRouter(prefix="/weather", tags=["Weather"])
def mount(app: FastAPI):
@app.get("/api/weather/locations", tags=["API"]) @router.get("/providers")
async def get_api_weather_locations(request: AppRequest, query: str) -> list[Location]: async def get_api_weather_providers(request: AppRequest) -> list[str]:
weather_api = request.app.state.api.weather return request.app.state.api.get_api_providers(WeatherApi)
@router.get("/{provider}/locations")
async def find_api_weather_locations(weather_api: WeatherApiDepends, query: str) -> list[Location]:
return await weather_api.find_locations(query) return await weather_api.find_locations(query)
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
async def get_api_weather_day(request: AppRequest, location: str, date: datetime.date) -> WeatherResponse: @router.get("/{provider}/{location}/day/{date}")
weather_api = request.app.state.api.weather async def get_api_weather_day(weather_api: WeatherApiDepends, location: str, date: datetime.date) -> WeatherResponse:
return await weather_api.get_day(location, date) return await weather_api.get_day(location, date)
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
async def get_api_weather_days(request: AppRequest, location: str, days: int) -> WeatherResponse: @router.get("/{provider}/{location}/days/{days}")
weather_api = request.app.state.api.weather async def get_api_weather_days(weather_api: WeatherApiDepends, location: str, days: int) -> WeatherResponse:
return await weather_api.get_days(location, days) return await weather_api.get_days(location, days)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from .common import router as common_router from .root import router as root_router
from .schedule import router as schedule_router from .schedule import router as schedule_router
from .translation import set_language from .translation import set_language
from .weather import router as weather_router from .weather import router as weather_router
router = APIRouter(dependencies=[Depends(set_language)]) router = APIRouter(tags=["view"], dependencies=[Depends(set_language)], include_in_schema=False)
router.include_router(common_router) router.include_router(root_router)
router.include_router(weather_router) router.include_router(weather_router)
router.include_router(schedule_router) router.include_router(schedule_router)

View File

@@ -1,36 +0,0 @@
from pathlib import Path
from typing import NamedTuple
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from ..common.utils.template import build_templates
class Section(NamedTuple):
link: str
title: str
icon: str
SECTIONS = [
Section("weather", "Weather", "brightness-high"),
Section("schedule", "TV program", "tv"),
]
base_dir = Path(__file__).parent
router = APIRouter()
templates = build_templates()
@router.get("/", response_class=HTMLResponse)
async def get_section_list(request: Request):
return templates.TemplateResponse(
request=request,
name="root_index.html",
context={
"sections": SECTIONS,
},
)

View File

@@ -37,7 +37,7 @@
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="{{_('Select language')}} (default)"> aria-label="{{_('Select language')}} (default)">
<span class="fi fir fi-gb me-2 language-icon-active"></span> <span class="fi fir fi-gb me-2 language-icon-active icon-header"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-language-text">{{_("Select language")}}</span> id="bd-language-text">{{_("Select language")}}</span>
</button> </button>
@@ -70,7 +70,7 @@
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"> aria-label="Toggle theme (auto)">
<span class="bi bi-circle-half me-2 opacity-50 theme-icon-active"></span> <span class="bi bi-circle-half me-2 opacity-50 theme-icon-active icon-header"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-theme-text">{{_("Toggle theme")}}</span> id="bd-theme-text">{{_("Toggle theme")}}</span>
</button> </button>
@@ -112,8 +112,11 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
{% if not is_widget %} {% if not is_widget %}
<footer class="pt-5 my-5 text-muted border-top"> <footer class="app-footer pt-5 my-5 text-muted border-top">
Created by shmyga &middot; &copy; 2026 <div class="d-flex justify-content-between">
<span>Created by shmyga &middot; &copy; 2026</span>
<span>v{{ version }}</span>
</div>
</footer> </footer>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,26 +0,0 @@
{% extends "base.html" %}
{% block title %}{{_("Index")}}{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>{{_("View")}}</h1>
<div class="list-group mb-5">
{% for section in sections %}
<a href="{{section.link}}"
class="list-group-item list-group-item-action px-4">
<app-link href="{{section.link}}"
icon="{{section.icon}}">
{{_(section.title)}}
</app-link>
</a>
{% endfor %}
</div>
<hr class="col-3 col-md-2 mb-5">
<h1>{{_("Docs")}}</h1>
<a href="/docs"
target="_blank">
<h4>Swagger</h4>
</a>
{% endblock %}

View File

@@ -1,12 +1,18 @@
import datetime
import typing
from pathlib import Path from pathlib import Path
from babel.dates import format_date from babel.dates import format_date
from fastapi import Request from fastapi import Request
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from gallery.easel.core import AppRequest
from gallery.version import __version__ from gallery.version import __version__
from ...translation import _ from ...translation import _
from .tag import TagUtil
ContextProcessor = typing.Callable[[AppRequest], dict[str, typing.Any]]
def is_widget(request: Request) -> bool: def is_widget(request: Request) -> bool:
@@ -15,22 +21,31 @@ def is_widget(request: Request) -> bool:
) )
def context_processor(request: Request) -> dict: def base_context_processor(request: Request) -> dict:
return { return {
"is_widget": is_widget(request), "is_widget": is_widget(request),
"provider": request.query_params.get("provider"),
} }
def build_templates(templates_dir: Path | None = None, filters: dict | None = None) -> Jinja2Templates: def build_templates(
templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None
) -> Jinja2Templates:
directory = [Path(__file__).parent.parent / "templates"] directory = [Path(__file__).parent.parent / "templates"]
if templates_dir: if templates_dir:
directory.append(templates_dir) directory.append(templates_dir)
templates = Jinja2Templates(directory=directory, context_processors=[context_processor]) context_processors: list[ContextProcessor] = [base_context_processor]
if context_processor:
context_processors.append(context_processor)
templates = Jinja2Templates(directory=directory, context_processors=context_processors)
templates.env.globals.update( templates.env.globals.update(
{ {
"_": _, "_": _,
"version": __version__, "version": __version__,
"format_date": format_date, "format_date": format_date,
"datetime": datetime,
"tag_util": TagUtil,
"DATE_FORMAT": "E, d MMMM Y",
} }
) )
if filters: if filters:

View File

@@ -0,0 +1,36 @@
from pathlib import Path
from typing import NamedTuple
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from ..common.utils.template import build_templates
class Section(NamedTuple):
link: str
title: str
icon: str
SECTIONS = [
Section("weather", "Weather", "brightness-high"),
Section("schedule", "TV program", "tv"),
]
base_dir = Path(__file__).parent
router = APIRouter()
templates = build_templates(Path(__file__).parent / "templates")
@router.get("/", response_class=HTMLResponse)
async def get_section_list(request: Request):
return templates.TemplateResponse(
request=request,
name="root_index.html",
context={
"sections": SECTIONS,
},
)

View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{_("Index")}}{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<h1>{{_("View")}}</h1>
<div class="list-group mb-3">
{% for section in sections %}
<app-link href="{{section.link}}"
icon="{{section.icon}}"
class="list-group-item list-group-item-action">
{{_(section.title)}}
</app-link>
{% endfor %}
</div>
<h1>{{_("Docs")}}</h1>
<div class="list-group mb-3">
<app-link href="/docs"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
Swagger UI
</app-link>
<app-link href="/redoc"
target="_blank"
icon="file-earmark-text"
class="list-group-item list-group-item-action">
ReDoc
</app-link>
</div>
{% endblock %}

View File

@@ -5,65 +5,58 @@ 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.sketch.schedule.catalog import BUNDLE from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi
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
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(ScheduleApi),
}
templates = build_templates( templates = build_templates(
Path(__file__).parent / "templates", Path(__file__).parent / "templates",
{ {
"timedelta_format": timedelta_format, "timedelta_format": timedelta_format,
}, },
context_procesor,
) )
router = APIRouter() router = APIRouter(prefix="/schedule")
@router.get("/schedule", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
async def get_schedule_list(request: AppRequest): async def get_schedule_index(request: AppRequest, provider: str | None = None, query: str | None = None):
schedule_api = request.app.state.api.schedule if query and provider:
channels = await schedule_api.get_channels() schedule_api = api_resolver(ScheduleApi)(request, provider)
channels_data = BUNDLE.select_items(channels) channels = await schedule_api.find_channels(query)
else:
channels = []
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"channels": channels_data, "channels": channels,
}, },
) )
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse) @router.get("/{provider}/{channel}", response_class=RedirectResponse)
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
tag_value = TagUtil.parse_tag(tag)
schedule_api = request.app.state.api.schedule
results = await schedule_api.get_all_schedules(tag_value.date)
return templates.TemplateResponse(
request=request,
name="schedule.html",
context={
"tag_util": TagUtil,
"datetime": datetime,
"response": results[0],
"responses": results,
"live": live,
},
)
@router.get("/schedule/{channel}", response_class=RedirectResponse)
async def get_channel_default(channel: str): async def get_channel_default(channel: str):
return RedirectResponse(f"{channel}/tag/today") return RedirectResponse(f"{channel}/tag/today")
@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse) @router.get("/{provider}/{channel}/tag/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, 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)
schedule_api = request.app.state.api.schedule
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

@@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}} {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -10,27 +11,34 @@
{% block content %} {% block content %}
<h4> <h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="button" <i class="bi bi-arrow-left-square"></i>
href="../..">⬆️</a> </a>
<span>{{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <a class="icon-link"
<a class="button" href="../../..">
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> <i class="bi bi-arrow-up-square"></i>
</a>
<span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
</h4> </h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<td></td> <td></td>
<td></td> <td></td>
<td></td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for value in response.values %} {% for value in response.values %}
<tr class="{{'table-success' if value.live else ''}}"> <tr class="{{'table-success' if value.live else ''}}">
<td>{{value.start.strftime('%H:%M')}}</td> <td>
<td>{{(value.end - value.start) | timedelta_format}}</td> <span>{{value.start.strftime('%H:%M')}}</span>
<span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span>
</td>
<td>{{value.label}}</td> <td>{{value.label}}</td>
</tr> </tr>
{% endfor %} {% endfor %}

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="schedule/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="schedule/{{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 channel="{{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

@@ -1,51 +0,0 @@
{% extends "base.html" %}
{% block title %}
{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
{% endblock %}
{% block header %}
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}
<h4>
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button"
href="..">⬆️</a>
<span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4>
<div>
<table class="table">
<thead>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
{% for response in responses %}
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
{% if values|length > 0 %}
<tr class="table-primary fs-4">
<td colspan="3">
<div>{{response.channel.name}}</div>
</td>
</tr>
{% for value in values %}
<tr class="{{'table-success' if not live and value.live else ''}}">
<td>{{value.start.strftime('%H:%M')}}</td>
<td>{{(value.end - value.start) | timedelta_format}}</td>
<td>{{value.label}}</td>
</tr>
{% endfor %}
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -5,18 +5,29 @@ 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.api import api_resolver
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import WeatherResponse from gallery.sketch.weather.model import WeatherResponse
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 cloudness_icon, wind_direction_icon from .filters import cloudness_icon, wind_direction_icon
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(WeatherApi),
}
templates = build_templates( templates = build_templates(
Path(__file__).parent / "templates", Path(__file__).parent / "templates",
{ {
"wind_direction_icon": wind_direction_icon, "wind_direction_icon": wind_direction_icon,
"cloudness_icon": cloudness_icon, "cloudness_icon": cloudness_icon,
}, },
context_procesor,
) )
@@ -25,20 +36,21 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
request=request, request=request,
name="weather.html", name="weather.html",
context={ context={
"tag_util": TagUtil,
"datetime": datetime,
"response": response, "response": response,
}, },
) )
router = APIRouter() router = APIRouter(prefix="/weather")
@router.get("/weather", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, query: str | None = None): async def get_weather_index(request: AppRequest, provider: str | None = None, query: str | None = None):
weather_api = request.app.state.api.weather if query and provider:
locations = (await weather_api.find_locations(query)) if query else [] weather_api = api_resolver(WeatherApi)(request, provider)
locations = await weather_api.find_locations(query)
else:
locations = []
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", name="index.html",
@@ -48,29 +60,31 @@ async def get_weather_index(request: AppRequest, query: str | None = None):
) )
@router.get("/weather/{location}", response_class=RedirectResponse) @router.get("/{provider}/{location}", response_class=RedirectResponse)
async def get_weather_default(location: str): async def get_weather(location: str):
return RedirectResponse(f"{location}/tag/today") return RedirectResponse(f"{location}/tag/today")
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse) @router.get("/{provider}/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather_day(request: AppRequest, location: str, date: datetime.date): async def get_weather_day(
weather_api = request.app.state.api.weather request: AppRequest,
weather_api: WeatherApiDepends,
location: str,
date: datetime.date,
):
response = await weather_api.get_day(location, date) response = await weather_api.get_day(location, date)
return build_weather_response(request, response) return build_weather_response(request, response)
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse) @router.get("/{provider}/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, location: str, days: int): async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
weather_api = request.app.state.api.weather
response = await weather_api.get_days(location, days) response = await weather_api.get_days(location, days)
return build_weather_response(request, response) return build_weather_response(request, response)
@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse) @router.get("/{provider}/{location}/tag/{tag}", response_class=HTMLResponse)
async def get_weather_tag(request: AppRequest, location: str, tag: str): async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
tag_value = TagUtil.parse_tag(tag) tag_value = TagUtil.parse_tag(tag)
weather_api = request.app.state.api.weather
if tag_value.type == TagType.DAY: if tag_value.type == TagType.DAY:
response = await weather_api.get_day(location, tag_value.date) response = await weather_api.get_day(location, tag_value.date)
elif tag_value.type == TagType.DAYS: elif tag_value.type == TagType.DAYS:
@@ -78,3 +92,10 @@ async def get_weather_tag(request: AppRequest, location: str, tag: str):
else: else:
raise ValueError(tag) raise ValueError(tag)
return build_weather_response(request, response) return build_weather_response(request, response)
@router.get("/zmiyevka-184640/tag/{tag}", response_class=RedirectResponse)
async def get_zmiyevka_weather_compat(request: AppRequest):
path_parts = request.url.path.split("/")
path_parts.insert(2, "gismeteo")
return RedirectResponse("/".join(path_parts))

View File

@@ -1,6 +1,12 @@
import datetime import datetime
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection, WindDirectionDeg from gallery.sketch.weather.model import (
Cloudness,
Precipitation,
Sky,
WindDirection,
WindDirectionDeg,
)
def wind_direction_icon(wind_direction_deg: float) -> str: def wind_direction_icon(wind_direction_deg: float) -> str:
@@ -42,6 +48,7 @@ def cloudness_icon(sky: Sky, date: datetime.datetime, period: str) -> list[str]:
Precipitation.SHOWER: "rain", Precipitation.SHOWER: "rain",
Precipitation.SNOW: "snow", Precipitation.SNOW: "snow",
Precipitation.HEAVY_SNOW: "snow", Precipitation.HEAVY_SNOW: "snow",
Precipitation.HAIL: "hail",
}[sky.precipitation] }[sky.precipitation]
if sky.cloudness == Cloudness.PARTLY_CLOUDY: if sky.cloudness == Cloudness.PARTLY_CLOUDY:
main_icon = f"{day_prefix}-{main_icon}" main_icon = f"{day_prefix}-{main_icon}"

View File

@@ -12,63 +12,51 @@
id="query" id="query"
name="query" name="query"
placeholder="{{_('Enter the city name')}}"> placeholder="{{_('Enter the city name')}}">
<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 %}
</ul>
<button class="btn btn-primary" <button class="btn btn-primary"
type="submit">{{_("Search")}}</button> type="submit">{{_("Search")}}</button>
</div> </div>
</form> </form>
{% if locations %}
<ul id="locations" <ul id="locations"
class="list-group mb-5"> class="list-group mb-5">
{% for location in locations %} {% for location in locations %}
<a href="weather/{{location.id}}" <weather-location location="{{location.model_dump() | tojson | forceescape}}"></weather-location>
class="list-group-item list-group-item-action px-4"
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
<span class="fi fi-{{location.country_code}} me-1"></span>
<span class="text-primary">{{location.name}}</span>
<span class="small ms-1 text-secondary">
{{location.country}}, {{location.district}}, {{location.subdistrict}}
</span>
<span></span>
</a>
{% endfor %} {% endfor %}
</ul> </ul>
<hr>
{% endif %}
<ul id="storedLocations"
class="list-group mb-5">
</ul>
<script> <script>
(function () { (function () {
document.loadLocations = () => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
const container = document.querySelector('#locations');
container.innerHTML = '';
for (const [id, name] of Object.entries(locations)) {
const element = document.createElement('a');
element.href = `weather/${id}`;
element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
element.innerHTML = `
<span class="text-primary me-auto">${name}</span>
<span class="text-danger" onclick="removeLocation('${id}'); event.preventDefault();">&#x2715;</span>
`;
container.appendChild(element);
}
}
document.saveLocation = (location) => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
locations[location.id] = location.name;
window.localStorage.setItem('locations', JSON.stringify(locations));
}
document.removeLocation = (id) => {
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
delete locations[id];
window.localStorage.setItem('locations', JSON.stringify(locations));
document.loadLocations();
}
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const searchQuery = params.get('query'); const searchQuery = params.get('query');
if (searchQuery) { if (searchQuery) {
document.querySelector('#query').value = searchQuery; document.querySelector('#query').value = searchQuery;
} else {
document.loadLocations();
} }
document.addEventListener("DOMContentLoaded", (event) => {
weatherLocationManager.loadLocations('#storedLocations');
});
})(); })();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -9,16 +9,24 @@
{% block content %} {% block content %}
<h4> <h4>
{% if response.period == 'day' %} {% if response.period == 'day' %}
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="button" <i class="bi bi-arrow-left-square"></i>
href="../tag/days-10">⬆️</a> </a>
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <a class="icon-link"
<a class="button" href="../tag/days-10">
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> <i class="bi bi-arrow-up-square"></i>
</a>
<span>{{response.location}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
<i class="bi bi-arrow-right-square"></i>
</a>
{% endif %} {% endif %}
{% if response.period == 'days' %} {% if response.period == 'days' %}
<span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span> <span>{{response.location}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<span>- {{format_date(response.date + datetime.timedelta(days=(response.values | length - 1)), DATE_FORMAT,
locale=request.state.language)}}</span>
{% endif %} {% endif %}
</h4> </h4>
<div class="table-responsive"> <div class="table-responsive">
@@ -58,8 +66,7 @@
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="{{ value.sky }}"> data-bs-title="{{ value.sky }}">
{% for icon in value.sky | cloudness_icon(value.date, response.period) %} {% for icon in value.sky | cloudness_icon(value.date, response.period) %}
<div class="wi wi-{{icon}} text-primary" <div class="wi wi-{{icon}} wi-xl text-primary"></div>
style="font-size: 3.5rem;"></div>
{% endfor %} {% endfor %}
</td> </td>
{% endfor %} {% endfor %}
@@ -95,7 +102,7 @@
<td class="wind" <td class="wind"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-title="{{ value.wind_direction }}"> data-bs-title="{{ value.wind_direction }}">
<div class="wi wi-wind-deg wi-{{value.wind_direction | wind_direction_icon}} fs-1 text-primary"></div> <div class="wi wi-wind-deg wi-{{value.wind_direction | wind_direction_icon}} wi-l text-primary"></div>
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>
@@ -130,7 +137,7 @@
{% for value in response.values %} {% for value in response.values %}
<td class="precipitation" <td class="precipitation"
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});"> style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
<span class="value">{{value.precipitation or ''}}</span> <span class="value">{{(value.precipitation | round(2)) if value.precipitation else ''}}</span>
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>

View File

@@ -85,11 +85,14 @@ class GismeteoApi(WeatherApi):
Location( Location(
id=f"{item['slug']}-{item['id']}", id=f"{item['slug']}-{item['id']}",
name=item["translations"]["kk"]["city"]["name"], name=item["translations"]["kk"]["city"]["name"],
provider=self.provider,
lat=item["coordinates"]["latitude"], lat=item["coordinates"]["latitude"],
lon=item["coordinates"]["longitude"], lon=item["coordinates"]["longitude"],
country=item["translations"]["kk"]["country"]["name"], country=item["translations"]["kk"]["country"]["name"],
country_code=item["country"]["code"].lower(), country_code=item["country"]["code"].lower(),
district=item["translations"]["kk"]["district"]["name"], district=(
item["translations"]["kk"]["district"]["name"] if item["translations"]["kk"]["district"] else ""
),
subdistrict=( subdistrict=(
item["translations"]["kk"]["subdistrict"]["name"] item["translations"]["kk"]["subdistrict"]["name"]
if "subdistrict" in item["translations"]["kk"] if "subdistrict" in item["translations"]["kk"]

View File

@@ -39,14 +39,11 @@ class DateParser(RowParser[datetime.datetime]):
KEY = "date" KEY = "date"
def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]: def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]:
datetime_date_tag = tag.select_one(".widget-row.widget-row-datetime-date > .row-item") datetime_time_row = tag.select_one(".widget-row.widget-row-datetime-time")
if datetime_date_tag: if datetime_time_row:
date_str = datetime_date_tag.find(text=True, recursive=False).text for item in datetime_time_row.select(".row-item > time-value"):
date = dateparser.parse(date_str, languages=["ru"]) timestamp = int(item.attrs["timestamp"])
for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"): time = datetime.datetime.fromtimestamp(timestamp)
time_str = item.text
time = dateparser.parse(time_str, languages=["ru"])
time = time.replace(year=date.year, month=date.month, day=date.day)
yield time yield time
else: else:
for item in tag.select(".widget-row.widget-row-date > .row-item"): for item in tag.select(".widget-row.widget-row-date > .row-item"):
@@ -68,9 +65,7 @@ class SkyParser(RowParser[Sky]):
PRECIPITATION_MAP: dict[str, Precipitation] = { PRECIPITATION_MAP: dict[str, Precipitation] = {
"без осадков": Precipitation.NO, "без осадков": Precipitation.NO,
"небольшой дождь": Precipitation.SMALL_RAIN, # TODO: remove it?
"небольшой дождь": Precipitation.SMALL_RAIN, "небольшой дождь": Precipitation.SMALL_RAIN,
"сильный дождь": Precipitation.HEAVY_RAIN, # TODO: remove it?
"сильный дождь": Precipitation.HEAVY_RAIN, "сильный дождь": Precipitation.HEAVY_RAIN,
"ливневый дождь": Precipitation.SHOWER, "ливневый дождь": Precipitation.SHOWER,
"дождь": Precipitation.RAIN, "дождь": Precipitation.RAIN,
@@ -83,6 +78,7 @@ class SkyParser(RowParser[Sky]):
"сильный снег с дождём": Precipitation.HEAVY_SNOW, "сильный снег с дождём": Precipitation.HEAVY_SNOW,
"небольшой снег с дождём": Precipitation.SNOW, "небольшой снег с дождём": Precipitation.SNOW,
"небольшой мокрый снег": Precipitation.SNOW, "небольшой мокрый снег": Precipitation.SNOW,
"град": Precipitation.HAIL,
} }
THUNDER = "гроза" THUNDER = "гроза"
@@ -152,6 +148,7 @@ class WindDirectionParser(RowParser[WindDirection]):
WIND_DIRECTION_MAP: dict[str, WindDirection] = { WIND_DIRECTION_MAP: dict[str, WindDirection] = {
"": WindDirection.CALM, "": WindDirection.CALM,
"": WindDirection.CALM,
"штиль": WindDirection.CALM, "штиль": WindDirection.CALM,
"с": WindDirection.N, "с": WindDirection.N,
"св": WindDirection.NE, "св": WindDirection.NE,
@@ -173,8 +170,12 @@ class PrecipitationParser(RowParser[float]):
KEY = "precipitation" KEY = "precipitation"
def parse_row(self, tag: Tag) -> Iterable[float]: def parse_row(self, tag: Tag) -> Iterable[float]:
for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"): for item in tag.select(".widget-row[data-row=precipitation-bars] > .row-item"):
yield float(item.text.replace(",", ".")) value = item.select_one("precipitation-value")
if value:
yield float(value.attrs["value"])
else:
yield 0
class PressureParser(RowParser[list[int]]): class PressureParser(RowParser[list[int]]):

View File

@@ -1,10 +1,9 @@
import datetime import datetime
import json
import logging import logging
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,31 +13,30 @@ 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 [ endpoint = "api/v1/channels"
ChannelId.MATCH_TV, data = json.loads(await self.SOURCE.request(endpoint))
ChannelId.MATCH_IGRA, result = []
ChannelId.MATCH_ARENA, query = query.lower()
ChannelId.MATCH_FUTBOL_1, for item in data["result"]:
ChannelId.MATCH_FUTBOL_2, if query in item["name"].lower() or query in item["alias"]:
ChannelId.MATCH_FUTBOL_3, channel_id = item["alias"]
ChannelId.MATCH_STRANA, name = item["name"].split("|")[0].strip()
] 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_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}" endpoint = f"api/v1/channels/{channel_id}/tv-schedule?date={date:%Y%m%d}"
data = await self.SOURCE.request(endpoint) data = json.loads(await self.SOURCE.request(endpoint))
soup = BeautifulSoup(data, features="html.parser") channel_data = data["result"]["channels"][0]
channel = Channel(id=channel_data["alias"], name=channel_data["name"], provider=self.provider)
values = [] values = []
channel_name = soup.select_one(".p-tv-guide-header__title").text.replace("Телепрограмма ", "").strip()
current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time()) current_day = datetime.datetime.combine(date.today(), datetime.datetime.min.time())
end = current_day + datetime.timedelta(days=1, hours=6) end = current_day + datetime.timedelta(days=1, hours=6)
prev_value: ScheduleValue | None = None prev_value: ScheduleValue | None = None
for item in soup.select( for item in channel_data["schedule"]:
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission" title = item["title"]
): time_str = item["time"]
title = item.select_one(".p-tv-guide-schedule-channel-transmission__title").text.strip()
time_str = item.select_one(".p-tv-guide-schedule-channel-transmission__time-block").text.strip()
hours, minutes = map(int, time_str.split(":")) hours, minutes = map(int, time_str.split(":"))
item_date = current_day.replace(hour=hours, minute=minutes) item_date = current_day.replace(hour=hours, minute=minutes)
if prev_value is not None and item_date.hour < prev_value.start.hour: if prev_value is not None and item_date.hour < prev_value.start.hour:
@@ -50,4 +48,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,
date=date,
values=values,
)

View File

@@ -1,6 +1,7 @@
import datetime import datetime
import logging import logging
from collections import defaultdict from collections import defaultdict
from os import environ
from aiocache import cached from aiocache import cached
@@ -9,7 +10,9 @@ from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
from gallery.sketch.weather.util import merge_weather_values from gallery.sketch.weather.util import merge_weather_values
from gallery.util import TimeUnit from gallery.util import TimeUnit
from .openweather import Forecast, OpenWeather from .openweather import Forecast
from .openweather import Location as OpenWeatherLocation
from .openweather import OpenWeather
from .parser import FORECAST_ITEM_PARSER from .parser import FORECAST_ITEM_PARSER
logger = logging.getLogger("openweather") logger = logging.getLogger("openweather")
@@ -17,12 +20,20 @@ logger = logging.getLogger("openweather")
class OpenWeatherApi(WeatherApi): class OpenWeatherApi(WeatherApi):
PROVIDER = "openweather" PROVIDER = "openweather"
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf") SOURCE = OpenWeather(environ["OPENWEATHER_KEY"])
@classmethod @classmethod
def _parse_location(cls, location_id: str) -> tuple[float, float]: def _parse_location(cls, location_id: str) -> tuple[float, float]:
return tuple(map(float, location_id.split(":", maxsplit=2))) return tuple(map(float, location_id.split(":", maxsplit=2)))
@cached(
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.location",
alias="redis",
ttl=TimeUnit.DAY,
)
async def _get_location(self, location_id: str) -> OpenWeatherLocation:
return await self.SOURCE.get_location(*self._parse_location(location_id))
@cached( @cached(
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast", key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
alias="redis", alias="redis",
@@ -32,9 +43,24 @@ class OpenWeatherApi(WeatherApi):
return await self.SOURCE.get_forecast(*self._parse_location(location_id)) return await self.SOURCE.get_forecast(*self._parse_location(location_id))
async def find_locations(self, query: str) -> list[Location]: async def find_locations(self, query: str) -> list[Location]:
raise NotImplementedError result = await self.SOURCE.find_locations(query)
return [
Location(
id=f"{item.lat}:{item.lon}",
name=item.name,
provider=self.provider,
lat=item.lat,
lon=item.lon,
country=item.country,
country_code=item.country.lower(),
district=item.state or "",
subdistrict="",
)
for item in result
]
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse: async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id) data: Forecast = await self._get_location_forecast(location_id)
values = [] values = []
for item in data.list: for item in data.list:
@@ -42,13 +68,14 @@ class OpenWeatherApi(WeatherApi):
if value.date.date() == date: if value.date.date() == date:
values.append(value) values.append(value)
return WeatherResponse( return WeatherResponse(
location=location_id, location=location.name,
date=date, date=date,
period="day", period="day",
values=values, values=values,
) )
async def get_days(self, location_id: str, days: int) -> WeatherResponse: async def get_days(self, location_id: str, days: int) -> WeatherResponse:
location: OpenWeatherLocation = await self._get_location(location_id)
data: Forecast = await self._get_location_forecast(location_id) data: Forecast = await self._get_location_forecast(location_id)
values_by_date: dict[datetime.datetime, list[WeatherValue]] = defaultdict(list) values_by_date: dict[datetime.datetime, list[WeatherValue]] = defaultdict(list)
for item in data.list: for item in data.list:
@@ -57,7 +84,7 @@ class OpenWeatherApi(WeatherApi):
values_by_date[item_date].append(value) values_by_date[item_date].append(value)
values = [merge_weather_values(date, values) for date, values in values_by_date.items()] values = [merge_weather_values(date, values) for date, values in values_by_date.items()]
return WeatherResponse( return WeatherResponse(
location=location_id, location=location.name,
date=datetime.date.today(), date=datetime.date.today(),
period="days", period="days",
values=list(sorted(values, key=lambda item: item.date)), values=list(sorted(values, key=lambda item: item.date)),

View File

@@ -53,7 +53,7 @@ class ForecastItem(Model):
weather: list[Weather] weather: list[Weather]
clouds: Clouds clouds: Clouds
wind: Wind wind: Wind
visibility: int visibility: int | None = None
pop: float pop: float
rain: Rain | None = None rain: Rain | None = None
sys: Sys sys: Sys
@@ -67,6 +67,15 @@ class Forecast(Model):
list: list[ForecastItem] list: list[ForecastItem]
class Location(Model):
name: str
lat: float
lon: float
country: str
state: str | None = None
local_names: dict[str, str] | None = None
class OpenWeather: class OpenWeather:
BASE_URL = "https://api.openweathermap.org" BASE_URL = "https://api.openweathermap.org"
@@ -78,4 +87,17 @@ class OpenWeather:
endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric" endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
response = await self._source.request(endpoint) response = await self._source.request(endpoint)
response_data = json.loads(response) response_data = json.loads(response)
return Forecast(**response_data) return Forecast.model_validate(response_data)
async def find_locations(self, query: str, limit: int = 5) -> list[Location]:
endpoint = f"geo/1.0/direct?q={query}&limit={limit}&appid={self._api_key}"
response = await self._source.request(endpoint)
response_data = json.loads(response)
return [Location.model_validate(item) for item in response_data]
async def get_location(self, lat: float, lon: float) -> Location:
limit = 1
endpoint = f"geo/1.0/reverse?lat={lat}&lon={lon}&limit={limit}&appid={self._api_key}"
response = await self._source.request(endpoint)
response_data = json.loads(response)
return Location.model_validate(response_data[0])

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,21 @@ 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 = (
"https://suggest-multi.yandex.ru/suggest-tv2?"
f"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:
if content["label"] == "Каналы":
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_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}" endpoint = f"channels/{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 +83,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

@@ -2,8 +2,13 @@ from typing import TypeVar
class Api: class Api:
TYPE: str
PROVIDER: str PROVIDER: str
@property
def type(self) -> str:
return self.TYPE
@property @property
def provider(self) -> str: def provider(self) -> str:
return self.PROVIDER return self.PROVIDER

View File

@@ -1,28 +1,19 @@
from collections import defaultdict
from .api import API, Api from .api import API, Api
from .schedule.api import ScheduleApi
from .weather.api import WeatherApi
class ApiBundle(list[Api]): class ApiBundle:
def __init__(self, values: list[Api]) -> None: def __init__(self, values: list[Api]):
super().__init__(values) self._values = values
self._providers_by_api = defaultdict(list)
self._api_map = {}
for value in self._values:
self._providers_by_api[value.type].append(value.provider)
self._api_map[(value.type, value.provider)] = value
def get_api_by_provider(self, provider: str) -> Api: def get_api_providers(self, api_type: type[API]) -> list[str]:
for value in self: return self._providers_by_api[api_type.TYPE]
if value.PROVIDER == provider:
return value
raise ValueError(provider)
def get_api_by_type(self, api_type: type[API]) -> API: def get_api(self, api_type: type[API], provider: str) -> API:
for value in self: return self._api_map[(api_type.TYPE, provider)]
if isinstance(value, api_type):
return value
raise ValueError(api_type)
@property
def weather(self) -> WeatherApi:
return self.get_api_by_type(WeatherApi)
@property
def schedule(self) -> ScheduleApi:
return self.get_api_by_type(ScheduleApi)

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,24 +1,14 @@
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):
INTERVAL: float = 0.5 TYPE = "schedule"
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,20 +6,20 @@ 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)
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]): class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
CACHE_KEY = "schedule" 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_schedule(self, channel_id: str, date: datetime.date) -> Schedule:
return await self._api.get_channel_schedule(channel_id, date) return await self._api.get_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

@@ -5,6 +5,7 @@ from .model import Location, WeatherResponse
class WeatherApi(Api): class WeatherApi(Api):
TYPE = "weather"
async def find_locations(self, query: str) -> list[Location]: async def find_locations(self, query: str) -> list[Location]:
raise NotImplementedError raise NotImplementedError

View File

@@ -11,7 +11,7 @@ CACHE_PRESET = DEFAULT_CACHE_PRESET
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]): class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
CACHE_KEY = "weather" CACHE_KEY = WeatherApi.TYPE
@cached( @cached(
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}", key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",

View File

@@ -1,5 +1,6 @@
import datetime import datetime
from enum import Enum from enum import StrEnum, auto
from typing import Self
from pydantic import BaseModel from pydantic import BaseModel
@@ -12,6 +13,7 @@ class Model(BaseModel):
class Location(Model): class Location(Model):
id: str id: str
name: str name: str
provider: str
lat: float lat: float
lon: float lon: float
country: str country: str
@@ -20,21 +22,22 @@ class Location(Model):
subdistrict: str subdistrict: str
class Cloudness(str, Enum): class Cloudness(StrEnum):
CLEAR = "clear" CLEAR = auto()
PARTLY_CLOUDY = "party_cloudy" PARTLY_CLOUDY = auto()
CLOUDY = "cloudy" CLOUDY = auto()
MAINLY_CLOUDY = "mainly_cloudy" MAINLY_CLOUDY = auto()
class Precipitation(str, Enum): class Precipitation(StrEnum):
NO = "no" NO = auto()
SMALL_RAIN = "small_rain" SMALL_RAIN = auto()
RAIN = "rain" RAIN = auto()
HEAVY_RAIN = "heavy_rain" HEAVY_RAIN = auto()
SHOWER = "shower" SHOWER = auto()
SNOW = "snow" SNOW = auto()
HEAVY_SNOW = "heavy_snow" HEAVY_SNOW = auto()
HAIL = auto()
class Sky(Model): class Sky(Model):
@@ -44,16 +47,16 @@ class Sky(Model):
fog: bool fog: bool
class WindDirection(str, Enum): class WindDirection(StrEnum):
CALM = "calm" CALM = auto()
N = "N" N = auto()
NE = "NE" NE = auto()
E = "E" E = auto()
SE = "SE" SE = auto()
S = "S" S = auto()
SW = "SW" SW = auto()
W = "W" W = auto()
NW = "NW" NW = auto()
class WindDirectionDeg(float): class WindDirectionDeg(float):
@@ -89,7 +92,7 @@ class WindDirectionDeg(float):
raise ValueError(self) raise ValueError(self)
@classmethod @classmethod
def from_direction(cls, direction: WindDirection) -> "WindDirectionDeg": def from_direction(cls, direction: WindDirection) -> Self:
return cls( return cls(
{ {
WindDirection.CALM: -1, WindDirection.CALM: -1,

View File

@@ -1,5 +1,3 @@
__version__ = "0.2.2"
import tomllib import tomllib
from pathlib import Path from pathlib import Path

2837
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,32 @@
[tool.poetry] [tool.poetry]
name = "gallery" name = "gallery"
version = "0.2.3" version = "0.4.2"
description = "" description = ""
authors = ["shmyga <shmyga.z@gmail.com>"] authors = ["shmyga <shmyga.z@gmail.com>"]
readme = "README.md" readme = "README.md"
packages = [{ include = "gallery" }] packages = [{ include = "gallery" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.14"
aiohttp = "^3.9.5" aiohttp = "^3.14"
beautifulsoup4 = "^4.12.3" beautifulsoup4 = "^4.15"
dateparser = "^1.2.0" dateparser = "^1.4"
pydantic = "^2.8.2" pydantic = "^2.13"
aiocache = { extras = ["redis"], version = "^0.12.2" } aiocache = { extras = ["redis"], version = "^0.12" }
[tool.poetry.group.app.dependencies] [tool.poetry.group.app.dependencies]
fastapi = "^0.111.1" fastapi = {extras = ["standard"], version = "^0.139.0"}
jinja2 = "^3.1.4" jinja2 = "^3.1"
babel = "^2.18.0" babel = "^2.18"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
pytest = "^8.3.1" pytest = "^9.1"
pytest-asyncio = "^0.23.8" pytest-asyncio = "^1.4"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pylint = "^3.2.6" pylint = "^4.0"
black = "^24.4.2" black = "^26.5"
isort = "^5.13.2" isort = "^8.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@@ -4,6 +4,6 @@ cd "$(dirname $(dirname "$0"))" || exit
TARGET="gallery" TARGET="gallery"
poetry run pylint $TARGET poetry run pylint $TARGET -sn
poetry run isort $TARGET --check-only poetry run isort $TARGET --check-only
poetry run black $TARGET -q --check --diff poetry run black $TARGET -q --check --diff

View File

@@ -2,10 +2,17 @@
set -e set -e
cd "$(dirname $(dirname "$0"))" || exit cd "$(dirname $(dirname "$0"))" || exit
PYTHON_VERSION=3.12 # env
if [[ ! -f .env ]]; then
cp .env-base .env
fi
source .env
# python
poetry env use ${PYTHON_VERSION} poetry env use ${PYTHON_VERSION}
poetry install poetry install
# static
cd static || exit cd static || exit
if [[ -f $HOME/.nvm/nvm.sh ]]; then if [[ -f $HOME/.nvm/nvm.sh ]]; then

View File

@@ -2,4 +2,4 @@
set -e set -e
cd "$(dirname $(dirname "$0"))" || exit cd "$(dirname $(dirname "$0"))" || exit
poetry run pytest tests OPENWEATHER_KEY="" poetry run pytest tests

View File

@@ -1,12 +1,12 @@
{ {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.4.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.4.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",

View File

@@ -1,6 +1,6 @@
{ {
"name": "gallery", "name": "gallery",
"version": "0.2.3", "version": "0.4.2",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite build --watch" "dev": "vite build --watch"

View File

@@ -1,10 +1,10 @@
class AppLinkElement extends HTMLElement { class AppLinkElement extends HTMLElement {
static observedAttributes = ["icon", "href"]; static observedAttributes = ["icon", "href", "target"];
constructor() { constructor() {
super(); super();
this.innerHTML = ` this.innerHTML = `
<a href="${this.getAttribute("href")}" <a href="${this.getAttribute("href")}" target="${this.getAttribute("target") || ""}"
class="d-flex align-items-center text-body text-decoration-none"> class="d-flex align-items-center text-body text-decoration-none">
<span class="fs-4"> <span class="fs-4">
<span class="bi bi-${this.getAttribute("icon")} me-1"></span> <span class="bi bi-${this.getAttribute("icon")} me-1"></span>

View File

@@ -3,6 +3,8 @@ import "./components";
import "./language"; import "./language";
import "./main.scss"; import "./main.scss";
import "./theme"; import "./theme";
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

@@ -1,4 +1,6 @@
//$bootstrap-icons: "circle-half", "moon-stars-fill", "brightness-high", "gear", "sun-fill", "tv"; .icon-link {
vertical-align: -0.25rem;
}
.bi { .bi {
display: inline-block; display: inline-block;
@@ -13,12 +15,6 @@
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: currentColor; background-color: currentColor;
//@each $icon in $bootstrap-icons {
// &.bi-#{$icon} {
// mask-image: url("bootstrap-icons/icons/#{$icon}.svg");
// }
///
&.bi-circle-half { &.bi-circle-half {
mask-image: url(bootstrap-icons/icons/circle-half.svg); mask-image: url(bootstrap-icons/icons/circle-half.svg);
} }
@@ -37,4 +33,16 @@
&.bi-tv { &.bi-tv {
mask-image: url(bootstrap-icons/icons/tv.svg); mask-image: url(bootstrap-icons/icons/tv.svg);
} }
&.bi-arrow-left-square {
mask-image: url(bootstrap-icons/icons/arrow-left-square.svg);
}
&.bi-arrow-right-square {
mask-image: url(bootstrap-icons/icons/arrow-right-square.svg);
}
&.bi-arrow-up-square {
mask-image: url(bootstrap-icons/icons/arrow-up-square.svg);
}
&.bi-file-earmark-text {
mask-image: url(bootstrap-icons/icons/file-earmark-text.svg);
}
} }

View File

@@ -30,7 +30,7 @@
//@import "bootstrap/scss/accordion"; //@import "bootstrap/scss/accordion";
//@import "bootstrap/scss/breadcrumb"; //@import "bootstrap/scss/breadcrumb";
//@import "bootstrap/scss/pagination"; //@import "bootstrap/scss/pagination";
//@import "bootstrap/scss/badge"; @import "bootstrap/scss/badge";
//@import "bootstrap/scss/alert"; //@import "bootstrap/scss/alert";
//@import "bootstrap/scss/progress"; //@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group"; @import "bootstrap/scss/list-group";

View File

@@ -12,4 +12,6 @@
.fir { .fir {
@extend .fis; @extend .fis;
border-radius: 50%; border-radius: 50%;
height: 1em;
border: 1px solid gray;
} }

View File

@@ -7,12 +7,18 @@
.wi-thunderstorm { .wi-thunderstorm {
mask-image: url(./svg/wi-thunderstorm.svg); mask-image: url(./svg/wi-thunderstorm.svg);
} }
.wi-hail {
mask-image: url(./svg/wi-hail.svg);
}
.wi-day-rain { .wi-day-rain {
mask-image: url(./svg/wi-day-rain.svg); mask-image: url(./svg/wi-day-rain.svg);
} }
.wi-cloudy { .wi-cloudy {
mask-image: url(./svg/wi-cloudy.svg); mask-image: url(./svg/wi-cloudy.svg);
} }
.wi-day-hail {
mask-image: url(./svg/wi-day-hail.svg);
}
.wi-night-clear { .wi-night-clear {
mask-image: url(./svg/wi-night-clear.svg); mask-image: url(./svg/wi-night-clear.svg);
} }
@@ -49,6 +55,9 @@
.wi-day-sunny { .wi-day-sunny {
mask-image: url(./svg/wi-day-sunny.svg); mask-image: url(./svg/wi-day-sunny.svg);
} }
.wi-night-alt-hail {
mask-image: url(./svg/wi-night-alt-hail.svg);
}
.wi-night-alt-cloudy { .wi-night-alt-cloudy {
mask-image: url(./svg/wi-night-alt-cloudy.svg); mask-image: url(./svg/wi-night-alt-cloudy.svg);
} }

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M1.48,16.88c0,1.34,0.47,2.49,1.4,3.45s2.07,1.47,3.4,1.53c0.12,0,0.18-0.06,0.18-0.17v-1.34c0-0.12-0.06-0.18-0.18-0.18
c-0.86-0.04-1.59-0.39-2.19-1.03s-0.9-1.4-0.9-2.26c0-0.83,0.28-1.55,0.85-2.17s1.27-0.97,2.1-1.07l0.53-0.04
c0.13,0,0.2-0.06,0.2-0.18l0.07-0.55c0.11-1.08,0.56-1.99,1.37-2.71c0.81-0.73,1.76-1.09,2.86-1.09c1.09,0,2.04,0.36,2.86,1.09
c0.82,0.73,1.29,1.63,1.4,2.71l0.07,0.58c0,0.12,0.06,0.19,0.17,0.19h1.62c0.89,0,1.67,0.32,2.32,0.96c0.65,0.64,0.98,1.4,0.98,2.27
c0,0.87-0.3,1.62-0.91,2.26c-0.61,0.64-1.34,0.98-2.19,1.03c-0.13,0-0.2,0.06-0.2,0.18v1.34c0,0.11,0.07,0.17,0.2,0.17
c1.34-0.04,2.47-0.55,3.39-1.51c0.93-0.97,1.39-2.12,1.39-3.46c0-0.74-0.14-1.41-0.41-2.01c0.79-0.96,1.18-2.07,1.18-3.33
c0-0.94-0.24-1.82-0.71-2.63c-0.48-0.81-1.12-1.45-1.93-1.93c-0.81-0.47-1.69-0.71-2.63-0.71c-1.56,0-2.86,0.58-3.9,1.75
c-0.8-0.44-1.7-0.66-2.71-0.66c-1.41,0-2.67,0.44-3.76,1.31s-1.8,2-2.11,3.37c-1.11,0.26-2.02,0.84-2.74,1.74
C1.84,14.7,1.48,15.73,1.48,16.88z M6.82,23.94c0.1,0.22,0.25,0.37,0.46,0.45c0.2,0.1,0.41,0.11,0.63,0.02
c0.22-0.08,0.37-0.23,0.45-0.45c0.1-0.22,0.11-0.43,0.02-0.65c-0.08-0.21-0.23-0.36-0.45-0.44c-0.2-0.11-0.41-0.12-0.62-0.03
c-0.22,0.09-0.37,0.24-0.47,0.47C6.74,23.49,6.73,23.7,6.82,23.94z M7.46,21.1c0,0.14,0.03,0.27,0.09,0.38
c0.19,0.31,0.49,0.46,0.89,0.46c0.32,0,0.55-0.22,0.69-0.65l1.04-3.22c0.08-0.24,0.06-0.47-0.07-0.67s-0.31-0.33-0.55-0.37
C9.34,16.98,9.13,17,8.93,17.1c-0.2,0.11-0.34,0.28-0.41,0.5l-1.03,3.22C7.47,20.92,7.46,21.02,7.46,21.1z M9.33,26.72
c0,0.13,0.02,0.23,0.05,0.29c0.09,0.22,0.24,0.37,0.45,0.45c0.09,0.05,0.21,0.07,0.35,0.07c0.06,0,0.16-0.02,0.3-0.06
c0.22-0.08,0.38-0.23,0.47-0.45s0.1-0.44,0-0.66c-0.1-0.22-0.25-0.37-0.45-0.45s-0.41-0.08-0.62,0c-0.19,0.07-0.33,0.19-0.42,0.35
C9.37,26.42,9.33,26.58,9.33,26.72z M9.94,4.57c0,0.25,0.08,0.45,0.24,0.6l0.65,0.65c0.16,0.16,0.34,0.25,0.54,0.27
c0.21,0.03,0.41-0.05,0.61-0.23c0.2-0.18,0.3-0.4,0.3-0.64c0-0.24-0.08-0.44-0.24-0.6l-0.64-0.64c-0.19-0.17-0.39-0.25-0.62-0.25
c-0.24,0-0.45,0.08-0.61,0.24C10.02,4.14,9.94,4.34,9.94,4.57z M10.06,24.03c0,0.16,0.05,0.32,0.16,0.48s0.27,0.27,0.48,0.33
c0.11,0.02,0.19,0.04,0.24,0.04c0.15,0,0.28-0.03,0.38-0.08c0.2-0.08,0.34-0.27,0.43-0.57l1.8-6.14c0.07-0.24,0.05-0.45-0.06-0.65
c-0.11-0.2-0.27-0.33-0.5-0.39c-0.24-0.07-0.46-0.05-0.66,0.06c-0.2,0.11-0.34,0.27-0.41,0.51l-1.84,6.19
C10.07,23.92,10.06,24,10.06,24.03z M13.51,23.64c0,0.13,0.02,0.23,0.07,0.31c0.09,0.21,0.24,0.35,0.45,0.44
c0.11,0.05,0.22,0.08,0.35,0.08c0.06,0,0.16-0.02,0.3-0.06c0.23-0.09,0.38-0.23,0.46-0.44c0.08-0.22,0.08-0.43,0-0.63
c-0.08-0.2-0.22-0.35-0.42-0.45c-0.23-0.11-0.44-0.12-0.66-0.03c-0.21,0.09-0.37,0.24-0.48,0.47
C13.53,23.41,13.51,23.51,13.51,23.64z M14.23,21.08c0,0.16,0.05,0.31,0.15,0.45c0.1,0.15,0.26,0.25,0.46,0.31
c0.09,0.02,0.17,0.03,0.25,0.03c0.39,0,0.65-0.2,0.79-0.61l1.03-3.18c0.08-0.23,0.05-0.45-0.07-0.65s-0.29-0.33-0.52-0.39
c-0.24-0.07-0.45-0.05-0.64,0.06s-0.32,0.27-0.4,0.51l-1.02,3.2C14.25,20.94,14.23,21.03,14.23,21.08z M15.3,9
c0.67-0.64,1.5-0.97,2.48-0.97c0.98,0,1.81,0.34,2.5,1.03c0.69,0.68,1.04,1.51,1.04,2.49c0,0.62-0.17,1.24-0.52,1.85
c-0.96-0.96-2.12-1.44-3.51-1.44H17C16.7,10.8,16.14,9.81,15.3,9z M16.92,3.73c0,0.24,0.08,0.44,0.25,0.61
c0.17,0.17,0.37,0.25,0.61,0.25c0.23,0,0.43-0.08,0.59-0.25c0.16-0.17,0.24-0.37,0.24-0.61V1.67c0-0.24-0.08-0.44-0.24-0.61
c-0.16-0.17-0.35-0.25-0.59-0.25c-0.24,0-0.44,0.08-0.61,0.25c-0.17,0.17-0.25,0.37-0.25,0.61V3.73z M22.47,6.02
c0,0.24,0.08,0.44,0.25,0.6c0.15,0.17,0.34,0.26,0.58,0.26c0.23,0,0.44-0.09,0.6-0.26l1.44-1.44c0.18-0.15,0.27-0.35,0.27-0.6
c0-0.24-0.09-0.44-0.26-0.61c-0.17-0.17-0.38-0.25-0.61-0.25c-0.22,0-0.41,0.09-0.57,0.27l-1.45,1.43
C22.56,5.57,22.47,5.78,22.47,6.02z M23.28,17.92c0,0.23,0.08,0.43,0.24,0.6l0.66,0.63c0.14,0.18,0.34,0.27,0.6,0.27
c0.24,0,0.43-0.09,0.57-0.27c0.18-0.16,0.27-0.36,0.27-0.6c0-0.24-0.09-0.44-0.27-0.61l-0.65-0.62c-0.16-0.18-0.35-0.26-0.58-0.26
s-0.43,0.08-0.6,0.25C23.36,17.48,23.28,17.69,23.28,17.92z M24.74,11.55c0,0.24,0.09,0.44,0.26,0.6c0.18,0.18,0.38,0.26,0.62,0.26
h2.03c0.24,0,0.44-0.08,0.61-0.25c0.17-0.17,0.26-0.37,0.26-0.61c0-0.23-0.08-0.43-0.25-0.59c-0.17-0.16-0.38-0.24-0.62-0.24h-2.03
c-0.25,0-0.46,0.08-0.63,0.24C24.83,11.12,24.74,11.32,24.74,11.55z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.64,16.9c0,1.33,0.46,2.47,1.39,3.43c0.93,0.96,2.06,1.47,3.4,1.53c0.11,0,0.17-0.06,0.17-0.17v-1.34
c0-0.11-0.06-0.17-0.17-0.17c-0.86-0.04-1.58-0.38-2.18-1.02c-0.6-0.64-0.9-1.39-0.9-2.26c0-0.83,0.28-1.54,0.84-2.16
c0.56-0.61,1.26-0.97,2.09-1.07l0.53-0.03c0.13,0,0.2-0.06,0.2-0.19l0.06-0.53c0.11-1.08,0.56-1.99,1.37-2.71
c0.81-0.73,1.76-1.09,2.85-1.09c1.09,0,2.04,0.36,2.85,1.09c0.81,0.73,1.27,1.63,1.39,2.71l0.08,0.58c0,0.11,0.06,0.17,0.18,0.17
h1.61c0.89,0,1.66,0.32,2.31,0.96c0.65,0.64,0.98,1.39,0.98,2.27c0,0.87-0.3,1.62-0.9,2.26c-0.6,0.64-1.33,0.98-2.18,1.02
c-0.13,0-0.2,0.06-0.2,0.17v1.34c0,0.11,0.07,0.17,0.2,0.17c0.87-0.02,1.67-0.26,2.4-0.71c0.73-0.45,1.31-1.05,1.73-1.8
c0.42-0.75,0.63-1.57,0.63-2.44c0-0.89-0.22-1.72-0.67-2.47c-0.44-0.75-1.05-1.35-1.81-1.78S21.29,12,20.4,12h-0.32
c-0.32-1.34-1.03-2.43-2.1-3.28s-2.3-1.28-3.68-1.28c-1.41,0-2.66,0.44-3.75,1.31c-1.09,0.87-1.79,1.99-2.1,3.35
c-1.11,0.26-2.02,0.83-2.73,1.73S4.64,15.75,4.64,16.9z M10.09,24.1c0.09,0.21,0.25,0.37,0.46,0.46c0.2,0.1,0.41,0.11,0.62,0.02
c0.22-0.09,0.36-0.24,0.45-0.45c0.1-0.22,0.11-0.43,0.02-0.64c-0.08-0.21-0.24-0.35-0.45-0.44c-0.2-0.11-0.4-0.12-0.61-0.03
c-0.21,0.09-0.36,0.24-0.46,0.47C10.01,23.66,10.01,23.86,10.09,24.1z M10.72,21.28c0,0.16,0.05,0.31,0.15,0.45
c0.1,0.15,0.26,0.25,0.46,0.32c0.19,0.11,0.4,0.12,0.62,0.01c0.22-0.1,0.37-0.3,0.44-0.6l0.9-3.38c0.06-0.25,0.04-0.47-0.08-0.67
c-0.12-0.2-0.29-0.32-0.53-0.36c-0.08-0.02-0.16-0.03-0.24-0.03c-0.16,0-0.32,0.05-0.47,0.15c-0.15,0.1-0.26,0.25-0.32,0.44
l-0.88,3.39C10.73,21.16,10.72,21.25,10.72,21.28z M12.58,26.87c0,0.12,0.02,0.22,0.06,0.29c0.09,0.22,0.24,0.37,0.45,0.45
c0.09,0.05,0.2,0.08,0.33,0.08c0.06,0,0.16-0.02,0.3-0.06c0.22-0.08,0.38-0.23,0.47-0.45c0.1-0.22,0.1-0.44,0-0.66
c-0.1-0.22-0.25-0.37-0.45-0.46c-0.2-0.09-0.4-0.09-0.62,0c-0.19,0.08-0.32,0.2-0.41,0.36C12.62,26.58,12.58,26.73,12.58,26.87z
M13.31,24.26c0,0.37,0.21,0.61,0.63,0.73c0.11,0.03,0.19,0.04,0.24,0.04c0.15,0,0.28-0.03,0.38-0.08c0.21-0.08,0.35-0.27,0.42-0.57
l1.67-6.29c0.06-0.24,0.04-0.45-0.06-0.65c-0.1-0.19-0.27-0.32-0.49-0.38c-0.08-0.02-0.17-0.03-0.27-0.03
c-0.16,0-0.32,0.05-0.48,0.15c-0.16,0.1-0.26,0.25-0.3,0.44l-1.71,6.34C13.32,24.1,13.31,24.2,13.31,24.26z M16.74,23.8
c0,0.12,0.02,0.23,0.08,0.32c0.08,0.19,0.23,0.34,0.44,0.44c0.11,0.04,0.23,0.07,0.35,0.07c0.06,0,0.16-0.02,0.3-0.06
c0.21-0.08,0.37-0.23,0.46-0.44c0.07-0.22,0.07-0.43-0.01-0.63c-0.08-0.2-0.22-0.35-0.42-0.45c-0.23-0.11-0.44-0.12-0.65-0.03
c-0.21,0.09-0.36,0.24-0.46,0.47C16.77,23.59,16.74,23.69,16.74,23.8z M17.47,21.23c0,0.14,0.05,0.29,0.16,0.45
c0.11,0.16,0.26,0.27,0.45,0.33c0.16,0.03,0.25,0.05,0.27,0.05c0.09,0,0.22-0.03,0.37-0.1c0.2-0.09,0.33-0.27,0.4-0.52l0.9-3.34
c0.02-0.17,0.03-0.26,0.03-0.26c0-0.16-0.05-0.31-0.15-0.46c-0.1-0.15-0.25-0.25-0.45-0.31c-0.09-0.02-0.18-0.03-0.26-0.03
c-0.16,0-0.32,0.05-0.47,0.15s-0.25,0.25-0.31,0.45l-0.9,3.36L17.47,21.23z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<path d="M4.1,16.91c0,1.33,0.46,2.48,1.39,3.43s2.06,1.47,3.4,1.53c0.12,0,0.18-0.06,0.18-0.17v-1.34c0-0.11-0.06-0.17-0.18-0.17
c-0.86-0.04-1.58-0.38-2.18-1.02s-0.9-1.39-0.9-2.26c0-0.83,0.28-1.55,0.84-2.17c0.56-0.61,1.26-0.97,2.1-1.07l0.53-0.03
c0.13,0,0.2-0.06,0.2-0.18l0.07-0.54c0.11-1.08,0.56-1.99,1.37-2.72c0.81-0.73,1.76-1.1,2.85-1.1c1.08,0,2.03,0.37,2.85,1.1
c0.82,0.73,1.28,1.64,1.4,2.72l0.08,0.58c0,0.11,0.06,0.17,0.17,0.17h1.61c0.89,0,1.66,0.32,2.31,0.96c0.65,0.64,0.98,1.4,0.98,2.27
c0,0.87-0.3,1.62-0.9,2.26c-0.6,0.64-1.33,0.98-2.18,1.02c-0.13,0-0.2,0.06-0.2,0.17v1.34c0,0.11,0.07,0.17,0.2,0.17
c1.33-0.04,2.46-0.55,3.38-1.51c0.93-0.96,1.39-2.11,1.39-3.45c0-0.86-0.22-1.66-0.65-2.41c0.79-0.74,1.3-1.62,1.55-2.62l0.13-0.68
c0.02-0.01,0.03-0.03,0.03-0.07c0-0.07-0.05-0.13-0.16-0.16l-0.56-0.17c-0.57-0.17-1.05-0.45-1.46-0.85
c-0.4-0.4-0.69-0.81-0.86-1.25c-0.17-0.43-0.25-0.87-0.25-1.32c-0.01-0.24,0.02-0.51,0.08-0.79l0.14-0.58
c0.03-0.09-0.02-0.16-0.14-0.22l-0.8-0.25c-0.42-0.12-0.86-0.19-1.31-0.19c-0.35,0-0.71,0.04-1.08,0.13s-0.76,0.22-1.17,0.4
c-0.41,0.18-0.8,0.45-1.19,0.8c-0.38,0.35-0.72,0.75-1,1.22c-0.75-0.32-1.54-0.49-2.37-0.49c-1.41,0-2.67,0.44-3.76,1.31
s-1.79,1.99-2.1,3.36c-1.11,0.26-2.02,0.83-2.74,1.73S4.1,15.76,4.1,16.91z M9.58,23.94c0.09,0.21,0.24,0.36,0.46,0.45
c0.19,0.1,0.4,0.11,0.62,0.02c0.22-0.08,0.37-0.23,0.45-0.45c0.1-0.22,0.11-0.43,0.02-0.65c-0.08-0.21-0.23-0.36-0.45-0.44
c-0.2-0.1-0.41-0.11-0.62-0.02c-0.21,0.09-0.37,0.24-0.47,0.46C9.5,23.48,9.49,23.69,9.58,23.94z M10.2,21.11
c0,0.15,0.05,0.3,0.16,0.45s0.26,0.26,0.46,0.32c0.26,0.1,0.48,0.1,0.67,0c0.19-0.1,0.32-0.29,0.4-0.57l0.88-3.21
c0.07-0.25,0.04-0.47-0.08-0.67c-0.12-0.2-0.3-0.32-0.54-0.37c-0.22-0.07-0.43-0.05-0.63,0.07c-0.2,0.11-0.33,0.28-0.4,0.51
l-0.88,3.22c0,0.02-0.01,0.06-0.02,0.12C10.21,21.03,10.2,21.08,10.2,21.11z M12.07,26.71c0,0.12,0.02,0.22,0.06,0.29
c0.09,0.22,0.24,0.37,0.45,0.45c0.09,0.05,0.2,0.07,0.33,0.07c0.06,0,0.16-0.02,0.3-0.06c0.23-0.08,0.39-0.23,0.48-0.45
c0.1-0.22,0.1-0.44,0-0.66c-0.1-0.22-0.25-0.37-0.45-0.46c-0.2-0.09-0.4-0.09-0.61,0c-0.19,0.08-0.33,0.2-0.42,0.36
C12.11,26.42,12.07,26.57,12.07,26.71z M12.81,24.06c0,0.38,0.21,0.64,0.64,0.78c0.09,0.03,0.17,0.05,0.23,0.05
c0.11,0,0.23-0.03,0.35-0.08c0.23-0.08,0.39-0.27,0.47-0.57l1.65-6.12c0.06-0.24,0.04-0.45-0.07-0.65c-0.11-0.19-0.28-0.32-0.5-0.39
c-0.23-0.07-0.45-0.05-0.65,0.07c-0.2,0.11-0.34,0.28-0.4,0.51l-1.68,6.17C12.82,23.92,12.81,24,12.81,24.06z M16.25,23.64
c0,0.13,0.02,0.23,0.07,0.31c0.08,0.2,0.23,0.35,0.44,0.44c0.12,0.05,0.23,0.08,0.35,0.08c0.06,0,0.16-0.02,0.3-0.06
c0.22-0.09,0.37-0.23,0.45-0.44c0.08-0.22,0.08-0.43,0-0.63c-0.08-0.2-0.22-0.35-0.42-0.45c-0.22-0.1-0.44-0.11-0.65-0.02
c-0.22,0.08-0.37,0.24-0.47,0.46C16.27,23.41,16.25,23.51,16.25,23.64z M16.97,21.08c0,0.16,0.05,0.32,0.15,0.46
c0.1,0.14,0.25,0.25,0.45,0.31c0.17,0.02,0.26,0.03,0.27,0.03c0.41,0,0.66-0.2,0.77-0.61l0.87-3.17c0.06-0.24,0.04-0.45-0.07-0.65
c-0.11-0.19-0.28-0.32-0.5-0.39c-0.23-0.07-0.45-0.05-0.64,0.07c-0.2,0.11-0.33,0.28-0.4,0.51L17,20.81
C16.98,20.9,16.97,20.99,16.97,21.08z M17.62,8.83c0.31-0.57,0.75-1.01,1.3-1.31c0.55-0.3,1.14-0.45,1.76-0.44
c0.11,0,0.2,0.01,0.25,0.02v0.31c0,0.98,0.26,1.89,0.78,2.75c0.52,0.86,1.25,1.51,2.17,1.95c-0.19,0.44-0.44,0.79-0.75,1.07
C22.25,12.39,21.17,12,19.88,12h-0.32C19.3,10.75,18.66,9.69,17.62,8.83z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -13,4 +13,11 @@
mask-position: 50%; mask-position: 50%;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background-color: currentColor; background-color: currentColor;
&.wi-l {
font-size: 2.5rem;
}
&.wi-xl {
font-size: 3.5rem;
}
} }

View File

@@ -4,7 +4,7 @@
@import "./lib/weather-icons/weather-icons"; @import "./lib/weather-icons/weather-icons";
@import "./widget.scss"; @import "./widget.scss";
@import "./weather.scss"; @import "./weather/weather.scss";
.table.table-compact { .table.table-compact {
td { td {
@@ -12,11 +12,8 @@
} }
} }
.icon { .icon-header {
display: inline-block; @include font-size($h4-font-size);
width: 2rem;
height: 2rem;
background-size: contain;
} }
.app-header { .app-header {

View File

@@ -1,13 +1,17 @@
@import "./lib/bootstrap"; @import "./lib/bootstrap";
$default-space: 2;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
.app-header { .app-header {
flex-direction: column; flex-direction: column;
padding-bottom: map-get($spacers, $default-space) !important;
margin-bottom: map-get($spacers, $default-space) !important;
> .link-list { > .link-list {
border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
padding-bottom: map-get($spacers, 1); padding-bottom: map-get($spacers, $default-space);
margin-bottom: map-get($spacers, 1); margin-bottom: map-get($spacers, $default-space);
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
@@ -22,12 +26,17 @@
} }
} }
.app-footer {
padding-top: map-get($spacers, $default-space) !important;
margin-top: map-get($spacers, $default-space) !important;
}
.navbar-nav { .navbar-nav {
flex-direction: column !important; flex-direction: column !important;
> .nav-item { > .nav-item {
.btn { .btn {
padding: 0.125rem !important; padding: map-get($spacers, 1) !important;
} }
} }
} }

View File

@@ -0,0 +1,111 @@
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="d-flex justify-content-between align-items-start text-decoration-none">
<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.classList = "list-group-item list-group-item-action";
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();
console.log("ok");

View File

@@ -0,0 +1,118 @@
export interface Location {
id: string;
name: string;
provider: string;
country_code: string;
country?: string;
district?: string;
subdistrict?: string;
}
export class WeatherLocationStorage {
private storageKey: string = "weather:locations";
getAll(): Location[] {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
return Object.values(locations);
}
add(location: Location) {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
locations[location.id] = location;
window.localStorage.setItem(this.storageKey, JSON.stringify(locations));
}
remove(id: string) {
const locations = JSON.parse(window.localStorage.getItem(this.storageKey) || "{}");
delete locations[id];
window.localStorage.setItem(this.storageKey, JSON.stringify(locations));
}
}
export class WeatherLocationManager {
loadLocations(selector: string) {
const locations = window.weatherLocationStorage.getAll();
const container = document.querySelector(selector);
if (container) {
container.innerHTML = "";
for (const location of locations) {
const element = new WeatherLocationElement();
element.setAttribute("location", JSON.stringify(location));
element.setAttribute("removable", "");
container.appendChild(element);
}
}
}
}
class WeatherLocationElement extends HTMLElement {
static observedAttributes = ["location", "removable"];
location: Location | null = null;
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
}
connectedCallback() {
this.location = JSON.parse(this.getAttribute("location") || "{}");
this.innerHTML = `
<a href="${this.location?.provider}/${this.location?.id}"
class="d-flex justify-content-between align-items-start text-decoration-none">
<span>
<span class="fi fi-${this.location?.country_code} me-1"></span>
<span class="text-primary">${this.location?.name}</span>
<span class="small ms-1 text-secondary">
${this.location?.country}, ${this.location?.district}, ${this.location?.subdistrict}
</span>
</span>
<span>
<span class="badge text-bg-secondary">${this.location?.provider}</span>
<span class="text-danger ms-2" data-remove>
&#x2715;
</span>
</span>
</a>`;
this.classList = 'list-group-item list-group-item-action';
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.location) {
window.weatherLocationStorage.add(this.location);
}
}
handleRemoveClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (this.location) {
window.weatherLocationStorage.remove(this.location.id);
}
this.remove();
}
}
customElements.define("weather-location", WeatherLocationElement);
declare global {
interface Window {
weatherLocationStorage: WeatherLocationStorage;
weatherLocationManager: WeatherLocationManager;
}
}
window.weatherLocationStorage = new WeatherLocationStorage();
window.weatherLocationManager = new WeatherLocationManager();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ from tests.common.mock import MockSource
MATCHTV_MOCK_SOURCE = MockSource( MATCHTV_MOCK_SOURCE = MockSource(
Path(__file__).parent, Path(__file__).parent,
{ {
"test": "test.html", "test": "test.json",
"channels": "channels.json",
}, },
) )

View File

@@ -0,0 +1,588 @@
{
"result": [
{
"id": 10,
"weight": 1,
"alias": "matchtv",
"name": "\u041c\u0430\u0442\u0447 \u0422\u0412",
"description": "\u003Cp\u003E\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb\u003C\/span\u003E\u0026nbsp;\u2014 \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0439 \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b \u043e\u0026nbsp;\u0441\u043f\u043e\u0440\u0442\u0435 \u0438\u0026nbsp;\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u043e\u0431\u0440\u0430\u0437\u0435 \u0436\u0438\u0437\u043d\u0438.\u003C\/p\u003E\u003Cp\u003E\u041c\u0430\u0442\u0447 \u0422\u0412\u0026nbsp;\u2014 \u044d\u0442\u043e\u0026nbsp;\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430\u003C\/span\u003E\u0026nbsp;\u0432\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430!\u003Cbr\u003E\u003C\/p\u003E\u003Cp\u003E\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u0412\u0026nbsp;\u043d\u0430\u0448\u0435\u043c \u044d\u0444\u0438\u0440\u0435\u003C\/span\u003E\u0026nbsp;\u2014 \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430 \u0438\u0026nbsp;\u0440\u0430\u0437\u0432\u043b\u0435\u043a\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0446\u0438\u043a\u043b\u044b \u0438\u0026nbsp;\u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438, \u0440\u0435\u0430\u043b\u0438\u0442\u0438- \u0438\u0026nbsp;\u0442\u043e\u043a-\u0448\u043e\u0443, \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b \u0438\u0026nbsp;\u0441\u0435\u0440\u0438\u0430\u043b\u044b:\u0026nbsp;\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u0432\u0441\u0451 \u043e\u0026nbsp;\u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u043c \u0438\u0026nbsp;\u043c\u0438\u0440\u043e\u0432\u043e\u043c \u0441\u043f\u043e\u0440\u0442\u0435!\u003C\/span\u003E\u003C\/p\u003E\u003Cp\u003E\u041a\u0430\u043d\u0430\u043b \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0435\u0442 \u043b\u0443\u0447\u0448\u0438\u0445 \u0441\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u044b\u0445\u0026nbsp;\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u0438\u0026nbsp;\u0432\u0435\u0434\u0443\u0449\u0438\u0445\u003C\/span\u003E. \u042d\u0442\u043e \u0441\u0430\u043c\u0430\u044f \u043e\u043f\u044b\u0442\u043d\u0430\u044f \u0438\u0026nbsp;\u043f\u0440\u043e\u0444\u0435\u0441\u0441\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u043d\u0430\u0026nbsp;\u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u043c \u0422\u0412\u0026nbsp;\u2014\u0026nbsp;\u003Cspan style=\u0022font-weight: 700;\u0022\u003E\u0432\u043c\u0435\u0441\u0442\u0435 \u0441\u0026nbsp;\u043d\u0430\u0448\u0438\u043c\u0438 \u0437\u0440\u0438\u0442\u0435\u043b\u044f\u043c\u0438 \u0438\u0026nbsp;\u0431\u043e\u043b\u0435\u043b\u044c\u0449\u0438\u043a\u0430\u043c\u0438\u003C\/span\u003E\u0026nbsp;\u043c\u044b\u0026nbsp;\u043f\u0440\u043e\u0448\u043b\u0438 \u0432\u0441\u0435 \u0432\u0430\u0436\u043d\u0435\u0439\u0448\u0438\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b \u0438\u0026nbsp;\u043f\u0435\u0440\u0432\u0435\u043d\u0441\u0442\u0432\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 \u043b\u0435\u0442.\u003C\/p\u003E\u003Cp\u003E\u041c\u044b\u0026nbsp;\u0437\u043e\u0432\u0435\u043c \u0432\u0441\u0435\u0445 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u0442\u044c\u0441\u044f \u043a\u0026nbsp;\u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u0443 \u043b\u044e\u0434\u0435\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e-\u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u043c\u0443 \u043b\u044e\u0431\u044f\u0442 \u0441\u043f\u043e\u0440\u0442 \u0438\u0026nbsp;\u0436\u0438\u0432\u0443\u0442\u0026nbsp;\u0438\u043c: \u0443\u0026nbsp;\u044d\u043a\u0440\u0430\u043d\u043e\u0432\u0026nbsp;\u0422\u0412, \u043d\u0430\u0026nbsp;\u0441\u0442\u0430\u0434\u0438\u043e\u043d\u0430\u0445 \u0438\u043b\u0438 \u0432\u0026nbsp;\u0441\u043f\u043e\u0440\u0442\u0437\u0430\u043b\u0430\u0445.\u003C\/p\u003E\u003Cp\u003E#\u0432\u0441\u0435\u043d\u0430\u043c\u0430\u0442\u0447!\u003C\/p\u003E",
"shortDescription": "\u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb \u2014 \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0439 \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b \u043e \u0441\u043f\u043e\u0440\u0442\u0435 \u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u043e\u0431\u0440\u0430\u0437\u0435 \u0436\u0438\u0437\u043d\u0438. \u041c\u0430\u0442\u0447 \u0422\u0412 \u2014 \u044d\u0442\u043e \u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430 \u0432\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430!\r\n\u0412 \u043d\u0430\u0448\u0435\u043c \u044d\u0444\u0438\u0440\u0435 \u2014 \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430 \u0438 \u0440\u0430\u0437\u0432\u043b\u0435\u043a\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0446\u0438\u043a\u043b\u044b \u0438 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438, \u0440\u0435\u0430\u043b\u0438\u0442\u0438- \u0438 \u0442\u043e\u043a-\u0448\u043e\u0443, \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b \u0438 \u0441\u0435\u0440\u0438\u0430\u043b\u044b: \u0432\u0441\u0451 \u043e \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u043c \u0438 \u043c\u0438\u0440\u043e\u0432\u043e\u043c \u0441\u043f\u043e\u0440\u0442\u0435!",
"videoPlayerCode": "https:\/\/video.matchtv.ru\/iframe\/channel\/106",
"platformVideoId": null,
"package": null,
"isNeedAuthorization": false,
"webcasterVideoId": 606457,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447 \u0422\u0412",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447 \u0422\u0412. \u0410\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043e\u0431\u0437\u043e\u0440\u044b, \u0438\u043d\u0442\u0435\u0440\u0432\u044c\u044e \u0441\u043f\u043e\u0440\u0442\u0441\u043c\u0435\u043d\u043e\u0432",
"seoDescription": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447 \u0422\u0412. \u0420\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0439 \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b \u043e \u0441\u043f\u043e\u0440\u0442\u0435 \u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u043e\u0431\u0440\u0430\u0437\u0435 \u0436\u0438\u0437\u043d\u0438.",
"seoKeywords": "\u041c\u0430\u0442\u0447 \u0422\u0412",
"seoMeta": "\u041c\u0430\u0442\u0447 \u0422\u0412 \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/70c6\/1bb4\/70c61bb4b7a0ccb739834fa99a5dd9a8.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/0de1\/49ba\/0de149babcacd733028970463f32a00d.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/4fbd\/42cc\/4fbd42ccdde063ff630a7970dbce1034.png",
"tvChannel": { "id": 130, "name": "\u041c\u0430\u0442\u0447 \u0422\u0412", "serviceTvId": 2883, "priority": 20 },
"terms": [
{ "id": 167824, "title": "\u041c\u0430\u0442\u0447 \u0422\u0412" },
{ "id": 174766, "title": "\u0412\u0442\u043e\u0440\u043e\u0435 \u0434\u044b\u0445\u0430\u043d\u0438\u0435" },
{
"id": 174768,
"title": "\u041a\u043e\u043d\u0442\u0438\u043d\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0432\u0435\u0447\u0435\u0440"
},
{
"id": 174769,
"title": "\u041b\u0443\u0447\u0448\u0430\u044f \u0438\u0433\u0440\u0430 \u0441 \u043c\u044f\u0447\u043e\u043c"
},
{
"id": 174770,
"title": "\u0411\u0438\u0430\u0442\u043b\u043e\u043d \u0441 \u0414\u043c\u0438\u0442\u0440\u0438\u0435\u043c \u0413\u0443\u0431\u0435\u0440\u043d\u0438\u0435\u0432\u044b\u043c"
},
{ "id": 174771, "title": "\u041e\u0441\u043e\u0431\u044b\u0439 \u0434\u0435\u043d\u044c" },
{ "id": 174772, "title": "\u041c\u0430\u043c\u0430 \u0432 \u0438\u0433\u0440\u0435" },
{
"id": 174773,
"title": "\u0410\u043d\u0430\u0442\u043e\u043c\u0438\u044f \u0441\u043f\u043e\u0440\u0442\u0430"
},
{ "id": 174774, "title": "\u0414\u0443\u0431\u043b\u0435\u0440" },
{
"id": 174775,
"title": "\u0421\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0435\u0441"
},
{ "id": 174776, "title": "1+1" },
{ "id": 174778, "title": "\u0411\u0435\u0437\u0443\u043c\u043d\u044b\u0439 \u0441\u043f\u043e\u0440\u0442" },
{ "id": 176117, "title": "\u0414\u0435\u0442\u0441\u043a\u0438\u0439 \u0432\u043e\u043f\u0440\u043e\u0441" },
{ "id": 176120, "title": "\u0422\u043e\u0447\u043a\u0430" },
{ "id": 176121, "title": "\u0421\u043f\u043e\u0440\u0442 \u0437\u0430 \u0433\u0440\u0430\u043d\u044c\u044e" },
{
"id": 183813,
"title": "\u0412\u0441\u0435 \u043d\u0430 \u0444\u0443\u0442\u0431\u043e\u043b: \u0410\u0444\u0438\u0448\u0430"
},
{ "id": 183814, "title": "\u0417\u0430\u043a\u0443\u043b\u0438\u0441\u044c\u0435 \u041a\u0425\u041b" },
{
"id": 183815,
"title": "\u0417\u0430\u043a\u0443\u043b\u0438\u0441\u044c\u0435 \u0427\u041c \u0441 \u0410\u043b\u0438\u0441\u043e\u0439 \u0417\u043d\u0430\u0440\u043e\u043a"
},
{
"id": 183816,
"title": "\u0411\u043e\u0439 \u0432 \u0431\u043e\u043b\u044c\u0448\u043e\u043c \u0433\u043e\u0440\u043e\u0434\u0435"
},
{ "id": 183818, "title": "\u0418\u043d\u0441\u043f\u0435\u043a\u0442\u043e\u0440 \u0417\u041e\u0416" },
{
"id": 183819,
"title": "\u0414\u0435\u043d\u044c\u0433\u0438 \u0431\u043e\u043b\u044c\u0448\u043e\u0433\u043e \u0441\u043f\u043e\u0440\u0442\u0430"
},
{ "id": 183820, "title": "\u041e\u043b\u0438\u043c\u043f\u0438\u0439\u0446\u044b.Live" },
{
"id": 183821,
"title": "\u041f\u043e\u0441\u043b\u0435 \u0444\u0443\u0442\u0431\u043e\u043b\u0430 \u0441 \u0413\u0435\u043e\u0440\u0433\u0438\u0435\u043c \u0427\u0435\u0440\u0434\u0430\u043d\u0446\u0435\u0432\u044b\u043c"
},
{ "id": 183822, "title": "\u0414\u0435\u0441\u044f\u0442\u043a\u0430" },
{ "id": 183824, "title": "\u0412\u0441\u0435 \u043d\u0430 \u0444\u0443\u0442\u0431\u043e\u043b" },
{ "id": 183825, "title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447!" },
{
"id": 183826,
"title": "\u041a\u0443\u0431\u043e\u043a \u0432\u043e\u0439\u043d\u044b \u0438 \u043c\u0438\u0440\u0430"
},
{
"id": 185340,
"title": "\u0421\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u044b\u0439 \u0437\u0430\u0433\u043e\u0432\u043e\u0440"
},
{
"id": 185341,
"title": "\u0421\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u043f\u043e\u0440\u0442\u0435\u0440"
},
{
"id": 187565,
"title": "\u0422\u043e\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0444\u0443\u0442\u0431\u043e\u043b"
},
{ "id": 188522, "title": "\u0410\u0432\u0442\u043e\u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f" },
{
"id": 195075,
"title": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043d\u0430 \u043f\u0440\u043e\u043a\u0430\u0447\u043a\u0443 "
},
{ "id": 195076, "title": "\u0411\u0435\u0448\u0435\u043d\u0430\u044f \u0441\u0443\u0448\u043a\u0430" },
{ "id": 195079, "title": "\u0411\u043b\u0438\u0446" },
{ "id": 195510, "title": "\u0421\u0438\u043b\u044c\u043d\u043e\u0435 \u0448\u043e\u0443" },
{
"id": 195771,
"title": "\u0423\u0442\u043e\u043c\u043b\u0435\u043d\u043d\u044b\u0435 \u0441\u043b\u0430\u0432\u043e\u0439"
},
{
"id": 198090,
"title": "\u0420\u043e\u0441\u0441\u0438\u044f \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u0430\u044f"
},
{ "id": 198535, "title": "\u0414\u0435\u043d\u044c \u0418\u043a\u0441" },
{ "id": 199051, "title": "\u0412\u044d\u043b\u043a\u0430\u043c \u0442\u0443 \u0420\u0430\u0448\u0430" },
{ "id": 199111, "title": "\u041d\u0430\u0448\u0438 \u043d\u0430 \u0427\u041c" },
{ "id": 199112, "title": "\u0420\u043e\u0441\u0441\u0438\u044f \u0436\u0434\u0435\u0442! " },
{
"id": 199117,
"title": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u044f \u0441\u0431\u043e\u0440\u043d\u043e\u0439"
},
{ "id": 199447, "title": "\u041d\u0430\u0448\u0438 \u043f\u043e\u0431\u0435\u0434\u044b" },
{
"id": 199675,
"title": "\u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430. Live"
},
{ "id": 201543, "title": "\u0428\u0435\u043b\u043a\u043e\u0432\u044b\u0439 \u043f\u0443\u0442\u044c" },
{
"id": 203403,
"title": "\u0422\u0430\u0435\u0442 \u043b\u0435\u0434 \u0441 \u0410\u043b\u0435\u043a\u0441\u0435\u0435\u043c \u042f\u0433\u0443\u0434\u0438\u043d\u044b\u043c"
},
{
"id": 204089,
"title": "\u0421 \u0447\u0435\u0433\u043e \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0444\u0443\u0442\u0431\u043e\u043b"
},
{ "id": 204096, "title": "\u0412\u0441\u0435 \u043d\u0430 \u0445\u043e\u043a\u043a\u0435\u0439" },
{ "id": 204138, "title": "\u041d\u043e\u0432\u043e\u0441\u0442\u0438" },
{ "id": 204186, "title": "\u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u043e\u0440\u0442" },
{ "id": 204582, "title": "\u0413\u0435\u043d \u043f\u043e\u0431\u0435\u0434\u044b" },
{ "id": 204593, "title": "\u0424\u0443\u0442\u0411\u043e\u043b\u044c\u043d\u043e" },
{ "id": 204848, "title": "\u041a\u0438\u0431\u0435\u0440\u0430\u0442\u043b\u0435\u0442\u0438\u043a\u0430" },
{ "id": 205028, "title": "\u041a\u0443\u0440\u0441 \u0415\u0432\u0440\u043e" },
{ "id": 205174, "title": "\u0421\u0430\u043c\u044b\u0435 \u0441\u0438\u043b\u044c\u043d\u044b\u0435" },
{ "id": 206192, "title": "\u0412\u0430\u043d\u043a\u0443\u0432\u0435\u0440. Live" },
{ "id": 206286, "title": "\u0414\u0430\u043a\u0430\u0440-2019" },
{ "id": 206303, "title": "\u041a\u0430\u0436\u0434\u043e\u043c\u0443 - \u0441\u043f\u043e\u0440\u0442" },
{ "id": 206587, "title": "\u041a\u0430\u0442\u0430\u0440 Live" },
{ "id": 206631, "title": "\u041a\u0430\u0442\u0430\u0440\u0441\u043a\u0438\u0435 \u0438\u0433\u0440\u044b" },
{ "id": 207868, "title": "\u0412\u0441\u0435 \u043d\u0430 \u043b\u044b\u0436\u0438!" },
{
"id": 208192,
"title": "\u0414\u043d\u0435\u0432\u043d\u0438\u043a \u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0430\u0434\u044b "
},
{
"id": 208377,
"title": "\u0422\u0440\u0435\u043d\u0435\u0440\u0441\u043a\u0438\u0439 \u0448\u0442\u0430\u0431"
},
{ "id": 208446, "title": "\u041a\u0430\u043f\u0438\u0442\u0430\u043d\u044b" },
{
"id": 208506,
"title": "\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436"
},
{
"id": 208625,
"title": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b"
},
{ "id": 208750, "title": "\u0418\u0433\u0440\u0430\u0435\u043c \u0437\u0430 \u0432\u0430\u0441" },
{ "id": 209068, "title": "\u041c\u0430\u0441\u0442\u0435\u0440 \u0441\u043f\u043e\u0440\u0442\u0430" },
{
"id": 209349,
"title": "\u041d\u0435\u0438\u0437\u0432\u0435\u0434\u0430\u043d\u043d\u0430\u044f \u0445\u043e\u043a\u043a\u0435\u0439\u043d\u0430\u044f \u0420\u043e\u0441\u0441\u0438\u044f"
},
{
"id": 213332,
"title": "\u0413\u0440\u0430\u043d-\u043f\u0440\u0438 \u0441 \u0410\u043b\u0435\u043a\u0441\u0435\u0435\u043c \u041f\u043e\u043f\u043e\u0432\u044b\u043c"
}
],
"currentTvTransmissions": [
{
"id": 17829286,
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. \u0421\u0435\u043d\u0435\u0433\u0430\u043b - \u0418\u0440\u0430\u043a. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041a\u0430\u043d\u0430\u0434\u044b [6+]",
"description": "",
"start": 1782542400,
"finish": 1782550500,
"startTime": "2026-06-27T09:40:00+03:00",
"finishTime": "2026-06-27T11:55:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17829286-1782495234.jpg"
}
]
},
{
"id": 5,
"weight": 2,
"alias": "premier",
"name": "\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440",
"description": "\u003Cp\u003E\u041f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b \u043e\u0026nbsp;\u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u0435:\u003C\/p\u003E\u003Cul\u003E\u003Cli\u003E\u0432\u0441\u0435 \u043c\u0430\u0442\u0447\u0438 \u041c\u0418\u0420 \u0420\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u0439 \u041f\u0440\u0435\u043c\u044c\u0435\u0440-\u041b\u0438\u0433\u0438;\u003C\/li\u003E\u003Cli\u003E\u043a\u0443\u0431\u043a\u043e\u0432\u044b\u0435 \u043c\u0430\u0442\u0447\u0438 \u0438\u0026nbsp;\u0442\u043e\u0432\u0430\u0440\u0438\u0449\u0435\u0441\u043a\u0438\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0438 \u043a\u043b\u0443\u0431\u043e\u0432 \u0420\u041f\u041b;\u003Cbr\u003E\u003C\/li\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u041b\u0438\u0433\u0438 PARI;\u003C\/li\u003E\u003Cli\u003E\u0438\u0433\u0440\u044b \u0441\u0431\u043e\u0440\u043d\u043e\u0439 \u0420\u043e\u0441\u0441\u0438\u0438 \u0438\u0026nbsp;\u043c\u043e\u043b\u043e\u0434\u0435\u0436\u043d\u043e\u0439 \u0441\u0431\u043e\u0440\u043d\u043e\u0439.\u003Cbr\u003E\u003C\/li\u003E\u003C\/ul\u003E\u003Cp\u003E\u0422\u0430\u043a\u0436\u0435 \u0432\u0026nbsp;\u044d\u0444\u0438\u0440\u0435 \u041c\u0410\u0422\u0427 \u041f\u0420\u0415\u041c\u042c\u0415\u0420: \u044d\u043a\u0441\u043a\u043b\u044e\u0437\u0438\u0432\u043d\u044b\u0435 \u0432\u044b\u043f\u0443\u0441\u043a\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c \u00ab8-16\u00bb, \u00ab\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b\u00bb, \u00ab\u0420\u0435\u0446\u0435\u043f\u0422\u0443\u0440\u0430\u00bb, \u0441\u0442\u0443\u0434\u0438\u0439\u043d\u0430\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430, \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438. \u0412\u0435\u0441\u044c \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0439 \u0444\u0443\u0442\u0431\u043e\u043b \u0432\u0026nbsp;\u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435! \u042d\u0442\u043e \u0438\u0441\u0442\u043e\u0440\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u044b\u0026nbsp;\u0434\u0435\u043b\u0430\u0435\u043c \u0432\u043c\u0435\u0441\u0442\u0435.\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u041f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b \u043e \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u0435, \u0433\u0434\u0435 \u0435\u0441\u0442\u044c \u0432\u0441\u0435 \u043c\u0430\u0442\u0447\u0438 \u041c\u0418\u0420 \u0420\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u0439 \u041f\u0440\u0435\u043c\u044c\u0435\u0440-\u041b\u0438\u0433\u0438, \u043a\u0443\u0431\u043a\u043e\u0432\u044b\u0435 \u043c\u0430\u0442\u0447\u0438 \u0438 \u0442\u043e\u0432\u0430\u0440\u0438\u0449\u0435\u0441\u043a\u0438\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0438 \u043a\u043b\u0443\u0431\u043e\u0432 \u041c\u0438\u0440 \u0420\u041f\u041b, \u0438\u0433\u0440\u044b \u0441\u0431\u043e\u0440\u043d\u043e\u0439 \u0420\u043e\u0441\u0441\u0438\u0438 \u0438 \u043c\u043e\u043b\u043e\u0434\u0435\u0436\u043d\u043e\u0439 \u0441\u0431\u043e\u0440\u043d\u043e\u0439 \u0438 \u00ab\u043f\u0435\u0440\u0435\u043a\u043b\u0438\u0447\u043a\u0438\u00bb \u043c\u0435\u0436\u0434\u0443 \u043c\u0430\u0442\u0447\u0430\u043c\u0438, \u0438\u0434\u0443\u0449\u0438\u043c\u0438 \u0432 \u043e\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u044f.\r\n\u0422\u0430\u043a\u0436\u0435 \u0432 \u044d\u0444\u0438\u0440\u0435 \u041c\u0410\u0422\u0427 \u041f\u0420\u0415\u041c\u042c\u0415\u0420: \u044d\u043a\u0441\u043a\u043b\u044e\u0437\u0438\u0432\u043d\u044b\u0435 \u0432\u044b\u043f\u0443\u0441\u043a\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c \u00ab8-16\u00bb, \u00ab\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438\u0433\u0440\u044b\u00bb, \u00ab\u0420\u0435\u0446\u0435\u043f\u0422\u0443\u0440\u0430\u00bb, \u0441\u0442\u0443\u0434\u0438\u0439\u043d\u0430\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430, \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438. \u0412\u0435\u0441\u044c \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u0438\u0439 \u0444\u0443\u0442\u0431\u043e\u043b \u0432 \u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435! \u042d\u0442\u043e \u0438\u0441\u0442\u043e\u0440\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u044b \u0434\u0435\u043b\u0430\u0435\u043c \u0432\u043c\u0435\u0441\u0442\u0435.",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_04153373fcaa00fc4e37db89e6d7f82a\/17_91502360\/8be99f14cce8abdaa9e2dd4bc8f12c8c\/4934710878?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "41f83e48ab4a92e5afd57c6ab57b2584",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 1142310,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043c\u0430\u0442\u0447\u0435\u0439 \u0420\u041f\u041b \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0432\u0441\u0435\u0445 \u043c\u0430\u0442\u0447\u0435\u0439 \u041c\u0438\u0440 \u0420\u041f\u041b, \u0438\u0433\u0440 \u041a\u0443\u0431\u043a\u0430 \u0420\u043e\u0441\u0441\u0438\u0438, \u0442\u043e\u0432\u0430\u0440\u0438\u0449\u0435\u0441\u043a\u0438\u0445 \u0432\u0441\u0442\u0440\u0435\u0447 \u043a\u043b\u0443\u0431\u043e\u0432 \u041c\u0438\u0440 \u0420\u041f\u041b \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440\u00bb!",
"seoKeywords": "\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/1969\/778c\/1969778c1a0e470077b19bd32648f18f.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/dd5c\/7cb6\/dd5c7cb6cf41d45010a6485dd66415d9.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/7168\/f84a\/7168f84a807771d47d5af0d7a0749ca1.svg",
"tvChannel": {
"id": 7,
"name": "\u041c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440",
"serviceTvId": 3459,
"priority": 25
},
"terms": [
{ "id": 20317, "title": "\u041c\u0438\u0440 \u0420\u041f\u041b" },
{
"id": 205664,
"title": "\u0424\u041e\u041d\u0411\u0415\u0422 \u041a\u0423\u0411\u041e\u041a \u041c\u0410\u0422\u0427 \u041f\u0420\u0415\u041c\u042c\u0415\u0420"
},
{
"id": 211845,
"title": "\u041a\u0443\u0431\u043e\u043a \u041f\u0430\u0440\u0438\u043c\u0430\u0442\u0447 \u041f\u0440\u0435\u043c\u044c\u0435\u0440"
},
{ "id": 204095, "title": "8-16" },
{ "id": 205212, "title": "\u0418\u043d\u0441\u0430\u0439\u0434\u0435\u0440\u044b" },
{
"id": 208377,
"title": "\u0422\u0440\u0435\u043d\u0435\u0440\u0441\u043a\u0438\u0439 \u0448\u0442\u0430\u0431"
},
{ "id": 208446, "title": "\u041a\u0430\u043f\u0438\u0442\u0430\u043d\u044b" }
],
"currentTvTransmissions": [
{
"id": 17828869,
"title": "\u0022\u0421\u043e\u0432\u0435\u0442\u0441\u043a\u0438\u0439 \u0444\u0443\u0442\u0431\u043e\u043b\u0022. \u0426\u0421\u041a\u0410 (\u041c\u043e\u0441\u043a\u0432\u0430). \u0427\u0430\u0441\u0442\u044c 2 [12+]",
"description": "\u041a\u0430\u043a \u043d\u0430\u0447\u0438\u043d\u0430\u043b\u0441\u044f \u0441\u043e\u0432\u0435\u0442\u0441\u043a\u0438\u0439 \u0444\u0443\u0442\u0431\u043e\u043b? \u041a\u0430\u043a\u0438\u043c\u0438 \u0431\u044b\u043b\u0438 \u0435\u0433\u043e \u043f\u0435\u0440\u0432\u044b\u0435 \u0443\u0441\u043f\u0435\u0445\u0438? \u041a\u0430\u043a\u0438\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0441\u0442\u0430\u043b\u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u043c\u0438 \u044d\u043f\u043e\u0445\u0438? \u041a\u0430\u043a\u043e\u0432\u044b \u0431\u044b\u043b\u0438 \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u0432\u0435\u0442\u0441\u043a\u043e\u0439 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0439 \u0448\u043a\u043e\u043b\u044b? \u041e\u0442\u0432\u0435\u0442\u044b \u043d\u0430 \u044d\u0442\u0438 \u0432\u043e\u043f\u0440\u043e\u0441\u044b \u2014 \u0432 \u043d\u0430\u0448\u0435\u0439 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0435!",
"start": 1782547500,
"finish": 1782549000,
"startTime": "2026-06-27T11:05:00+03:00",
"finishTime": "2026-06-27T11:30:00+03:00",
"ageRating": "12+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17828869-1781880696.jpg"
}
]
},
{
"id": 6,
"weight": 3,
"alias": "strana",
"name": "\u041c\u0430\u0442\u0447! \u0421\u0442\u0440\u0430\u043d\u0430",
"description": "\u003Cp\u003E\u042d\u0442\u043e \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b, \u0446\u0435\u043b\u0438\u043a\u043e\u043c \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u043e\u0439 \u0436\u0438\u0437\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0026nbsp;\u0440\u0430\u0437\u0432\u0438\u0442\u0438\u044e \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u043e\u0440\u0442\u0430. \u0411\u043e\u043b\u044c\u0448\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0432\u0435\u0449\u0430\u043d\u0438\u044f\u0026nbsp;\u2014 \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043a\u043b\u0443\u0431\u043d\u044b\u0445 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439. \u0410\u0026nbsp;\u0442\u0430\u043a\u0436\u0435 \u0432\u0435\u0441\u044c \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442: \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0441\u043f\u0435\u0446\u043f\u0440\u043e\u0435\u043a\u0442\u044b, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b \u0438\u0026nbsp;\u043f\u0440\u0435\u043c\u044c\u0435\u0440\u044b \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c. \u0421\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u0448 \u0441\u043f\u043e\u0440\u0442 \u0432\u0441\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u043e\u0439 \u0438\u0026nbsp;\u0431\u043e\u043b\u0435\u0435\u043c \u0437\u0430 \u043d\u0430\u0448\u0438\u0445\u003C\/p\u003E",
"shortDescription": "\u042d\u0442\u043e \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0430\u043d\u0430\u043b, \u0446\u0435\u043b\u0438\u043a\u043e\u043c \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u043e\u0439 \u0436\u0438\u0437\u043d\u0438 \u0441\u0442\u0440\u0430\u043d\u044b \u0438 \u0440\u0430\u0437\u0432\u0438\u0442\u0438\u044e \u0440\u043e\u0441\u0441\u0438\u0439\u0441\u043a\u043e\u0433\u043e \u0441\u043f\u043e\u0440\u0442\u0430. \u0411\u043e\u043b\u044c\u0448\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0432\u0435\u0449\u0430\u043d\u0438\u044f \u2014 \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043a\u043b\u0443\u0431\u043d\u044b\u0445 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439. \u0410 \u0442\u0430\u043a\u0436\u0435 \u0432\u0435\u0441\u044c \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442: \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0441\u043f\u0435\u0446\u043f\u0440\u043e\u0435\u043a\u0442\u044b, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b \u0438 \u043f\u0440\u0435\u043c\u044c\u0435\u0440\u044b \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c. \u0421\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430\u0448 \u0441\u043f\u043e\u0440\u0442 \u0432\u0441\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u043e\u0439 \u0438 \u0431\u043e\u043b\u0435\u0435\u043c \u0437\u0430 \u043d\u0430\u0448\u0438\u0445!",
"videoPlayerCode": "https:\/\/video.matchtv.ru\/iframe\/channel\/96",
"platformVideoId": null,
"package": null,
"isNeedAuthorization": false,
"webcasterVideoId": 606417,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447! C\u0442\u0440\u0430\u043d\u0430",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0421\u0442\u0440\u0430\u043d\u0430 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0421\u0442\u0440\u0430\u043d\u0430\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439, \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0421\u0442\u0440\u0430\u043d\u0430\u00bb!",
"seoKeywords": "\u041c\u0430\u0442\u0447! C\u0442\u0440\u0430\u043d\u0430",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/492b\/2610\/492b26105962fd92ea1eb704a40d009b.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/2fd5\/d4e4\/2fd5d4e435c7c25bc29b752383b2e02e.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/a86c\/df54\/a86cdf54fdb7ab9e893986bcbbf8e017.svg",
"tvChannel": {
"id": 143,
"name": "\u041c\u0430\u0442\u0447 \u0421\u0442\u0440\u0430\u043d\u0430",
"serviceTvId": 3568,
"priority": 28
},
"terms": [
{ "id": 207869, "title": "\u041c\u0430\u0442\u0447 \u0421\u0442\u0440\u0430\u043d\u0430" },
{ "id": 183823, "title": "\u0412\u0438\u0434 \u0441\u0432\u0435\u0440\u0445\u0443" },
{
"id": 208192,
"title": "\u0414\u043d\u0435\u0432\u043d\u0438\u043a \u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0430\u0434\u044b "
},
{
"id": 208212,
"title": "\u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0438\u0430\u0434\u0430-2019. \u0421\u0442\u0443\u0434\u0438\u044f"
},
{ "id": 208507, "title": "\u0421\u0442\u0440\u0430\u043d\u0430.Live" },
{
"id": 208508,
"title": "\u0421\u0442\u0440\u0430\u043d\u0430 \u0441\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u0430\u044f"
},
{
"id": 208958,
"title": "\u0421\u0442\u0440\u0430\u043d\u0430 \u0441\u043c\u043e\u0442\u0440\u0438\u0442 \u0441\u043f\u043e\u0440\u0442"
},
{ "id": 211925, "title": "\u0418\u0433\u0440\u044b \u043a\u043e\u0440\u043e\u043b\u0435\u0439" },
{ "id": 211926, "title": "\u041f\u043e\u0440\u0430 \u043d\u0430 \u0442\u0435\u043d\u043d\u0438\u0441" },
{ "id": 252187, "title": "\u041b\u0438\u0446\u0430 \u0441\u0442\u0440\u0430\u043d\u044b" }
],
"currentTvTransmissions": [
{
"id": 17830233,
"title": "\u0421\u0430\u043c\u0431\u043e. \u041a\u043e\u043c\u0430\u043d\u0434\u043d\u044b\u0439 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u0420\u043e\u0441\u0441\u0438\u0438. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041c\u043e\u0441\u043a\u0432\u044b [12+]",
"description": "",
"start": 1782547500,
"finish": 1782551700,
"startTime": "2026-06-27T11:05:00+03:00",
"finishTime": "2026-06-27T12:15:00+03:00",
"ageRating": "12+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17830233-1782222276.jpg"
}
]
},
{
"id": 2,
"weight": 4,
"alias": "futbol-1",
"name": "\u0424\u0443\u0442\u0431\u043e\u043b 1 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u003Cp\u003E\u0424\u0443\u0442\u0431\u043e\u043b\u0026nbsp;1,2,3\u0026nbsp;\u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432\u0026nbsp;\u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432\u0026nbsp;\u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435:\u003Cbr\u003E\u003C\/p\u003E\u003Cul\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438\u0026nbsp;\u043b\u0438\u0433;\u003C\/li\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0437\u0430\u0026nbsp;\u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430;\u003C\/li\u003E\u003Cli\u003E\u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b;\u003C\/li\u003E\u003Cli\u003E\u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438\u0026nbsp;\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430.\u003C\/li\u003E\u003C\/ul\u003E\u003Cp\u003E\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435\u0441\u044c \u0438\u0026nbsp;\u043d\u0430\u0441\u043b\u0430\u0436\u0434\u0430\u0439\u0442\u0435\u0441\u044c \u0442\u043e\u043f\u043e\u0432\u044b\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u043e\u043c \u0432\u0026nbsp;\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u043b\u0443\u0447\u0448\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u0438\u0026nbsp;\u044d\u043a\u0441\u043f\u0435\u0440\u0442\u043e\u0432!\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u0424\u0443\u0442\u0431\u043e\u043b 1,2,3 \u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043b\u0438\u0433, \u043c\u0430\u0442\u0447\u0438 \u0437\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430, \u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0443.",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_8546b1b795cf43e91e6cbb319db40fea\/17_97595835\/f119d3c8f364d6a7f3747fecf47c70ea\/4934710873?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "df213c9c6dc3669522dc77447bf69bdf",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 1044754,
"chatRoomId": null,
"seoH1": "\u0424\u0443\u0442\u0431\u043e\u043b 1 | \u041f\u0430\u043a\u0435\u0442 \u0421\u043f\u043e\u0440\u0442",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 1 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043c\u0430\u0442\u0447\u0435\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 1\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043c\u0430\u0442\u0447\u0435\u0439 \u0435\u0432\u0440\u043e\u043f\u0435\u0439\u0441\u043a\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043a\u0443\u0431\u043a\u043e\u0432, \u043e\u0431\u0437\u043e\u0440\u044b \u0438\u0433\u0440 \u0438 \u0442\u0443\u0440\u043e\u0432, \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0441\u0442\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 1\u00bb!",
"seoKeywords": "\u0424\u0443\u0442\u0431\u043e\u043b 1",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/042c\/80f5\/042c80f5a8074b9d3458af1561ba5ec0.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/b407\/d1aa\/b407d1aa02288bdfcc7f9736645693d5.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/964b\/5631\/964b5631bc31a3141083cbd8520993f1.svg",
"tvChannel": { "id": 8, "name": "\u0424\u0443\u0442\u0431\u043e\u043b 1", "serviceTvId": 205, "priority": 50 },
"terms": [
{ "id": 118, "title": "\u0424\u0443\u0442\u0431\u043e\u043b" },
{ "id": 213528, "title": "\u0424\u0443\u0442\u0431\u043e\u043b 1" }
],
"currentTvTransmissions": [
{
"id": 17828977,
"title": "\u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. \u0423\u0440\u0443\u0433\u0432\u0430\u0439 - \u0418\u0441\u043f\u0430\u043d\u0438\u044f. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041c\u0435\u043a\u0441\u0438\u043a\u0438 [6+]",
"description": "",
"start": 1782547800,
"finish": 1782555300,
"startTime": "2026-06-27T11:10:00+03:00",
"finishTime": "2026-06-27T13:15:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17828977-1782307268.jpg"
}
]
},
{
"id": 3,
"weight": 5,
"alias": "futbol-2",
"name": "\u0424\u0443\u0442\u0431\u043e\u043b 2 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u003Cp\u003E\u0424\u0443\u0442\u0431\u043e\u043b\u0026nbsp;1,2,3\u0026nbsp;\u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432\u0026nbsp;\u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432\u0026nbsp;\u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435:\u003Cbr\u003E\u003C\/p\u003E\u003Cul\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438\u0026nbsp;\u043b\u0438\u0433;\u003C\/li\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0437\u0430\u0026nbsp;\u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430;\u003C\/li\u003E\u003Cli\u003E\u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b;\u003C\/li\u003E\u003Cli\u003E\u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438\u0026nbsp;\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430.\u003C\/li\u003E\u003C\/ul\u003E\u003Cp\u003E\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435\u0441\u044c \u0438\u0026nbsp;\u043d\u0430\u0441\u043b\u0430\u0436\u0434\u0430\u0439\u0442\u0435\u0441\u044c \u0442\u043e\u043f\u043e\u0432\u044b\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u043e\u043c \u0432\u0026nbsp;\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u043b\u0443\u0447\u0448\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u0438\u0026nbsp;\u044d\u043a\u0441\u043f\u0435\u0440\u0442\u043e\u0432!\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u0424\u0443\u0442\u0431\u043e\u043b 1,2,3 \u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043b\u0438\u0433, \u043c\u0430\u0442\u0447\u0438 \u0437\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430, \u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0443. \r\n\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435\u0441\u044c \u0438 \u043d\u0430\u0441\u043b\u0430\u0436\u0434\u0430\u0439\u0442\u0435\u0441\u044c \u0442\u043e\u043f\u043e\u0432\u044b\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u043e\u043c \u0432 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u043b\u0443\u0447\u0448\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0442\u043e\u0432!",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_79df03f3381d2bac53c7dbce81bf1003\/17_900a338a44154bfccc74e8b251cb4850\/aa7c274e64856c35114ee0ddfc50dfb4\/4934710899?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "900a338a44154bfccc74e8b251cb4850",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 606437,
"chatRoomId": null,
"seoH1": "\u0424\u0443\u0442\u0431\u043e\u043b 2 | \u041f\u0430\u043a\u0435\u0442 \u0421\u043f\u043e\u0440\u0442",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 2 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043c\u0430\u0442\u0447\u0435\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 2\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043c\u0430\u0442\u0447\u0435\u0439 \u0435\u0432\u0440\u043e\u043f\u0435\u0439\u0441\u043a\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043a\u0443\u0431\u043a\u043e\u0432, \u043e\u0431\u0437\u043e\u0440\u044b \u0438\u0433\u0440 \u0438 \u0442\u0443\u0440\u043e\u0432, \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0441\u0442\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 2\u00bb!",
"seoKeywords": "\u0424\u0443\u0442\u0431\u043e\u043b 2",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/ab60\/61f4\/ab6061f4cecf5112c21bd1e549f467dc.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/7013\/6755\/701367555ed778b6fac513ec9060b43e.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/1c9d\/f020\/1c9df020717d419561cac6c7b439dc00.svg",
"tvChannel": { "id": 14, "name": "\u0424\u0443\u0442\u0431\u043e\u043b 2", "serviceTvId": 1261, "priority": 60 },
"terms": [
{ "id": 118, "title": "\u0424\u0443\u0442\u0431\u043e\u043b" },
{ "id": 213529, "title": "\u0424\u0443\u0442\u0431\u043e\u043b 2" }
],
"currentTvTransmissions": [
{
"id": 17829068,
"title": "\u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u0418\u0441\u043f\u0430\u043d\u0438\u0438. \u0411\u0430\u0440\u0441\u0435\u043b\u043e\u043d\u0430 - \u0410\u043b\u0430\u0432\u0435\u0441 [6+]",
"description": "",
"start": 1782543600,
"finish": 1782550800,
"startTime": "2026-06-27T10:00:00+03:00",
"finishTime": "2026-06-27T12:00:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17829068-1782282983.jpg"
}
]
},
{
"id": 4,
"weight": 6,
"alias": "futbol-3",
"name": "\u0424\u0443\u0442\u0431\u043e\u043b 3 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u003Cp\u003E\u0424\u0443\u0442\u0431\u043e\u043b\u0026nbsp;1,2,3\u0026nbsp;\u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432\u0026nbsp;\u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432\u0026nbsp;\u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435:\u003Cbr\u003E\u003C\/p\u003E\u003Cul\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438\u0026nbsp;\u043b\u0438\u0433;\u003C\/li\u003E\u003Cli\u003E\u043c\u0430\u0442\u0447\u0438 \u0437\u0430\u0026nbsp;\u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430;\u003C\/li\u003E\u003Cli\u003E\u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b;\u003C\/li\u003E\u003Cli\u003E\u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438\u0026nbsp;\u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0430.\u003C\/li\u003E\u003C\/ul\u003E\u003Cp\u003E\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435\u0441\u044c \u0438\u0026nbsp;\u043d\u0430\u0441\u043b\u0430\u0436\u0434\u0430\u0439\u0442\u0435\u0441\u044c \u0442\u043e\u043f\u043e\u0432\u044b\u043c \u0444\u0443\u0442\u0431\u043e\u043b\u043e\u043c \u0432\u0026nbsp;\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u043b\u0443\u0447\u0448\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u0438\u0026nbsp;\u044d\u043a\u0441\u043f\u0435\u0440\u0442\u043e\u0432!\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u0424\u0443\u0442\u0431\u043e\u043b 1,2,3 \u2014 \u044d\u0442\u043e \u0441\u0440\u0430\u0437\u0443 \u0442\u0440\u0438 \u043a\u0430\u043d\u0430\u043b\u0430 \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u0440\u0435\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0430\u043a\u0435\u0442\u0435. \u0417\u0434\u0435\u0441\u044c \u043c\u043e\u0436\u043d\u043e \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u0432\u044b\u0441\u043e\u043a\u043e\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0430\u0442\u0447\u0438 \u0432\u0435\u0434\u0443\u0449\u0438\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043b\u0438\u0433, \u043c\u0430\u0442\u0447\u0438 \u0437\u0430 \u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043a\u0443\u0431\u043a\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0438\u0440\u0430, \u043e\u0442\u0431\u043e\u0440\u043e\u0447\u043d\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0438 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a\u0443.",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_1f8dc610f3a99c722390b4da99c3eb97\/17_c21e41488d60529b326f403d57bfc904\/629c2077843e3af30d42fedd5c694a43\/4934710893?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "c21e41488d60529b326f403d57bfc904",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 606433,
"chatRoomId": null,
"seoH1": "\u0424\u0443\u0442\u0431\u043e\u043b 3",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 3 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043c\u0430\u0442\u0447\u0435\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 3\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u043c\u0430\u0442\u0447\u0435\u0439 \u0435\u0432\u0440\u043e\u043f\u0435\u0439\u0441\u043a\u0438\u0445 \u0444\u0443\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u0447\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442\u043e\u0432 \u0438 \u043a\u0443\u0431\u043a\u043e\u0432, \u043e\u0431\u0437\u043e\u0440\u044b \u0438\u0433\u0440 \u0438 \u0442\u0443\u0440\u043e\u0432, \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0441\u0442\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0424\u0443\u0442\u0431\u043e\u043b 3\u00bb!",
"seoKeywords": "\u0424\u0443\u0442\u0431\u043e\u043b 3",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/3d63\/1cbe\/3d631cbef4cc0b09408fc9ac1ada1a99.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/e434\/26c6\/e43426c648c7fb88f7e4dd0bd0dbf503.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/9baa\/c2fe\/9baac2fecd8a8a19f8e7072eb6c87cc7.svg",
"tvChannel": { "id": 140, "name": "\u0424\u0443\u0442\u0431\u043e\u043b 3", "serviceTvId": 2305, "priority": 70 },
"terms": [
{ "id": 118, "title": "\u0424\u0443\u0442\u0431\u043e\u043b" },
{ "id": 213530, "title": "\u0424\u0443\u0442\u0431\u043e\u043b 3" }
],
"currentTvTransmissions": [
{
"id": 17829808,
"title": "\u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u0411\u0440\u0430\u0437\u0438\u043b\u0438\u0438. \u0022\u041a\u043e\u0440\u0438\u0442\u0438\u0431\u0430\u0022 - \u0022\u0411\u0430\u0438\u044f\u0022 [6+]",
"description": "",
"start": 1782543600,
"finish": 1782550800,
"startTime": "2026-06-27T10:00:00+03:00",
"finishTime": "2026-06-27T12:00:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17829808-1781881048.jpg"
}
]
},
{
"id": 1,
"weight": 7,
"alias": "boec",
"name": "\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u003Cp\u003E\u003C\/p\u003E\u003Cp\u003E\u0422\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447 \u0411\u043e\u0435\u0446\u0026nbsp;\u2014 \u044d\u0442\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c \u043f\u0440\u044f\u043c\u044b\u0445 \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0439 \u0438\u0437\u0026nbsp;\u0440\u0438\u043d\u0433\u0430 \u0438\u0026nbsp;\u043e\u043a\u0442\u0430\u0433\u043e\u043d\u0430. \u0411\u043e\u043a\u0441, \u041c\u041c\u0410, \u043a\u0438\u043a\u0431\u043e\u043a\u0441\u0438\u043d\u0433, \u0441\u0430\u043c\u0431\u043e \u0438\u0026nbsp;\u0431\u043e\u0440\u044c\u0431\u0430, \u0432\u043e\u0441\u0442\u043e\u0447\u043d\u044b\u0435 \u0435\u0434\u0438\u043d\u043e\u0431\u043e\u0440\u0441\u0442\u0432\u0430 \u0438\u0026nbsp;\u043d\u0435\u0026nbsp;\u0442\u043e\u043b\u044c\u043a\u043e. \u041b\u0443\u0447\u0448\u0438\u0435 \u043f\u043e\u0435\u0434\u0438\u043d\u043a\u0438 \u043e\u0442\u0026nbsp;\u043f\u0440\u043e\u043c\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u0432\u0441\u0435\u0433\u043e \u043c\u0438\u0440\u0430: \u0431\u043e\u0438 \u043b\u0435\u0433\u0435\u043d\u0434 \u0438\u0026nbsp;\u0432\u044b\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0437\u0432\u0435\u0437\u0434. \u041c\u0430\u0442\u0447 \u0411\u043e\u0435\u0446\u0026nbsp;\u2014 \u044d\u0442\u043e \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0431\u043e\u0435\u0432\u043e\u0435 \u0438\u0441\u043a\u0443\u0441\u0441\u0442\u0432\u043e!\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u0422\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447 \u0411\u043e\u0435\u0446 \u2014 \u044d\u0442\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c \u043f\u0440\u044f\u043c\u044b\u0445 \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0439 \u0438\u0437 \u0440\u0438\u043d\u0433\u0430 \u0438 \u043e\u043a\u0442\u0430\u0433\u043e\u043d\u0430. \u0411\u043e\u043a\u0441, \u041c\u041c\u0410, \u043a\u0438\u043a\u0431\u043e\u043a\u0441\u0438\u043d\u0433, \u0441\u0430\u043c\u0431\u043e \u0438 \u0431\u043e\u0440\u044c\u0431\u0430, \u0432\u043e\u0441\u0442\u043e\u0447\u043d\u044b\u0435 \u0435\u0434\u0438\u043d\u043e\u0431\u043e\u0440\u0441\u0442\u0432\u0430 \u0438 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e. \u041b\u0443\u0447\u0448\u0438\u0435 \u043f\u043e\u0435\u0434\u0438\u043d\u043a\u0438 \u043e\u0442 \u043f\u0440\u043e\u043c\u043e\u0443\u0442\u0435\u0440\u043e\u0432 \u0432\u0441\u0435\u0433\u043e \u043c\u0438\u0440\u0430: \u0431\u043e\u0438 \u043b\u0435\u0433\u0435\u043d\u0434 \u0438 \u0432\u044b\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0437\u0432\u0435\u0437\u0434. \u041c\u0430\u0442\u0447 \u0411\u043e\u0435\u0446 \u2014 \u044d\u0442\u043e \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0431\u043e\u0435\u0432\u043e\u0435 \u0438\u0441\u043a\u0443\u0441\u0441\u0442\u0432\u043e!",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_8e011e545b9aa539a9e585eb0015d0d4\/17_6a6ffd269d21331c36923698e5058dff\/27a704d2e26671812d744beb5f2174a9\/4934710901?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "6a6ffd269d21331c36923698e5058dff",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 606425,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446 | \u041f\u0430\u043a\u0435\u0442 \u0421\u043f\u043e\u0440\u0442",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u041c\u041c\u0410 \u0438 \u0431\u043e\u043a\u0441\u0430 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0442\u0443\u0440\u043d\u0438\u0440\u043e\u0432 \u043f\u043e \u041c\u041c\u0410, \u0431\u043e\u0435\u0432 \u043f\u043e \u0431\u043e\u043a\u0441\u0443, \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043f\u043e \u0434\u0440\u0443\u0433\u0438\u043c \u0435\u0434\u0438\u043d\u043e\u0431\u043e\u0440\u0441\u0442\u0432\u0430\u043c, \u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446\u00bb!",
"seoKeywords": "\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446",
"seoMeta": "\u003Clink rel=\u0022canonical\u0022 href=\u0022https:\/\/matchtv.ru\/channel\/boec\u0022\u003E",
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/a5ee\/374a\/a5ee374ac3cfa8d2ad130bf37a29a53b.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/5b37\/7bf3\/5b377bf386985eb0bc00c7f727c045c6.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/dfd7\/8054\/dfd78054f3b2b5a3ee14c527632ce48f.svg",
"tvChannel": {
"id": 3,
"name": "\u041c\u0430\u0442\u0447 \u0411\u043e\u0435\u0446",
"serviceTvId": 554,
"priority": 100
},
"terms": [
{ "id": 175983, "title": "\u0411\u043e\u043a\u0441\/MMA" },
{ "id": 213534, "title": "\u041c\u0430\u0442\u0447! \u0411\u043e\u0435\u0446" }
],
"currentTvTransmissions": [
{
"id": 17828753,
"title": "\u0411\u043e\u043a\u0441. Bare Knuckle FC. \u041a\u044d\u043c\u0435\u0440\u043e\u043d \u0412\u0430\u043d\u043a\u0430\u043c\u043f \u043f\u0440\u043e\u0442\u0438\u0432 \u0413\u0440\u0435\u0433\u043e\u0440\u0438\u0441\u0430 \u0421\u0438\u0441\u043d\u0435\u0440\u043e\u0441\u0430. \u0410\u0440\u043d\u043e\u043b\u044c\u0434 \u0410\u0434\u0430\u043c\u0441 \u043f\u0440\u043e\u0442\u0438\u0432 \u0421\u0442\u0438\u0432\u0430 \u0411\u044d\u043d\u043a\u0441. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410 [16+]",
"description": "",
"start": 1782541800,
"finish": 1782550500,
"startTime": "2026-06-27T09:30:00+03:00",
"finishTime": "2026-06-27T11:55:00+03:00",
"ageRating": "16+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17828753-1781880649.jpg"
}
]
},
{
"id": 8,
"weight": 9,
"alias": "arena",
"name": "\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u0421\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u0026nbsp;\u0431\u0438\u0430\u0442\u043b\u043e\u043d\u0443 \u0438\u0026nbsp;\u043b\u044b\u0436\u043d\u044b\u043c \u0433\u043e\u043d\u043a\u0430\u043c, \u0444\u0438\u0433\u0443\u0440\u043d\u043e\u043c\u0443 \u043a\u0430\u0442\u0430\u043d\u0438\u044e \u0438\u0026nbsp;\u043f\u043b\u0430\u0432\u0430\u043d\u0438\u044e, \u043b\u0435\u0433\u043a\u043e\u0439 \u0430\u0442\u043b\u0435\u0442\u0438\u043a\u0435 \u0438\u0026nbsp;\u0430\u0432\u0442\u043e\u0441\u043f\u043e\u0440\u0442\u0443\u0026nbsp;\u2014 \u043d\u0430\u0026nbsp;\u041c\u0430\u0442\u0447 \u0410\u0440\u0435\u043d\u0430 \u0441\u043e\u0431\u0440\u0430\u043d\u044b \u0432\u0438\u0434\u044b, \u0433\u0434\u0435 \u0443\u0441\u043f\u0435\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442\u0026nbsp;\u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0430\u0441\u0442\u0435\u0440\u0441\u0442\u0432\u0430!\u003Cp\u003E\u003C\/p\u003E",
"shortDescription": "\u0421\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e \u0431\u0438\u0430\u0442\u043b\u043e\u043d\u0443 \u0438 \u043b\u044b\u0436\u043d\u044b\u043c \u0433\u043e\u043d\u043a\u0430\u043c, \u0444\u0438\u0433\u0443\u0440\u043d\u043e\u043c\u0443 \u043a\u0430\u0442\u0430\u043d\u0438\u044e \u0438 \u043f\u043b\u0430\u0432\u0430\u043d\u0438\u044e, \u043b\u0435\u0433\u043a\u043e\u0439 \u0430\u0442\u043b\u0435\u0442\u0438\u043a\u0435 \u0438 \u0430\u0432\u0442\u043e\u0441\u043f\u043e\u0440\u0442\u0443 \u2014 \u043d\u0430 \u041c\u0430\u0442\u0447 \u0410\u0440\u0435\u043d\u0430 \u0441\u043e\u0431\u0440\u0430\u043d\u044b \u0432\u0438\u0434\u044b, \u0433\u0434\u0435 \u0443\u0441\u043f\u0435\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0430\u0441\u0442\u0435\u0440\u0441\u0442\u0432\u0430!",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_a26972ccc9ac0100160dc6dbddf8e692\/17_9a32d7c6e26c38cbf12eac3bdb76849c\/764c339814404f3aec301f92f46f3249\/4934710917?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "9a32d7c6e26c38cbf12eac3bdb76849c",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 606429,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043f\u043e\u0440\u0442\u0438\u0432\u043d\u044b\u0445 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043f\u043e \u0431\u0438\u0430\u0442\u043b\u043e\u043d\u0443, \u043b\u044b\u0436\u043d\u044b\u043c \u0433\u043e\u043d\u043a\u0430\u043c, \u043b\u0435\u0433\u043a\u043e\u0439 \u0430\u0442\u043b\u0435\u0442\u0438\u043a\u0435 \u0438 \u0434\u0440\u0443\u0433\u0438\u043c \u0432\u0438\u0434\u0430\u043c \u0441\u043f\u043e\u0440\u0442\u0430 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430\u00bb!",
"seoKeywords": "\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/f768\/e9e1\/f768e9e1e6f4580f69c916d5f5a16132.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/44a3\/6689\/44a366893d98e8cfc1c1491ea5bce6de.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/f2f6\/45b7\/f2f645b7cf0962908fdb5bb485bd0cad.svg",
"tvChannel": {
"id": 137,
"name": "\u041c\u0430\u0442\u0447 \u0410\u0440\u0435\u043d\u0430",
"serviceTvId": 2944,
"priority": 80
},
"terms": [
{ "id": 38, "title": "\u0424\u043e\u0440\u043c\u0443\u043b\u0430-1" },
{ "id": 12218, "title": "\u0410\u0432\u0442\u043e\u0441\u043f\u043e\u0440\u0442" },
{
"id": 160328,
"title": "\u0415\u0432\u0440\u043e\u043f\u0435\u0439\u0441\u043a\u0438\u0435 \u0438\u0433\u0440\u044b"
},
{ "id": 176009, "title": "\u041b\u0435\u0442\u043d\u0438\u0435 \u0432\u0438\u0434\u044b" },
{ "id": 213531, "title": "\u041c\u0430\u0442\u0447! \u0410\u0440\u0435\u043d\u0430" }
],
"currentTvTransmissions": [
{
"id": 17829580,
"title": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0435 \u043f\u043b\u0430\u0432\u0430\u043d\u0438\u0435. \u041a\u0443\u0431\u043e\u043a \u043c\u0438\u0440\u0430. \u0414\u0443\u044d\u0442\u044b. \u0422\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041a\u0438\u0442\u0430\u044f [6+]",
"description": "",
"start": 1782540900,
"finish": 1782549300,
"startTime": "2026-06-27T09:15:00+03:00",
"finishTime": "2026-06-27T11:35:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17829580-1781880962.jpg"
}
]
},
{
"id": 7,
"weight": 10,
"alias": "igra",
"name": "\u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"description": "\u003Cp\u003E\u042d\u0442\u043e\u0442 \u043a\u0430\u043d\u0430\u043b \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u044b\u043c \u0432\u0438\u0434\u0430\u043c \u0441\u043f\u043e\u0440\u0442\u0430. \u041d\u0430\u0026nbsp;\u00ab\u0418\u0433\u0440\u0435\u00bb\u0026nbsp;\u2014 \u0442\u043e\u043f\u043e\u0432\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b \u043f\u043e\u0026nbsp;\u0431\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b\u0443, \u0433\u0430\u043d\u0434\u0431\u043e\u043b\u0443, \u0432\u043e\u043b\u0435\u0439\u0431\u043e\u043b\u0443 \u0438\u0026nbsp;\u043d\u0435\u0026nbsp;\u0442\u043e\u043b\u044c\u043a\u043e. \u0410\u0026nbsp;\u043d\u0430\u0448\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432\u0026nbsp;\u2014 \u0432\u0441\u0435\u0433\u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441\u0026nbsp;\u043f\u043e\u043b\u043d\u043e\u0439 \u043e\u0442\u0434\u0430\u0447\u0435\u0439!\u003C\/p\u003E\u003Cp\u003E\u003Cbr\u003E\u003C\/p\u003E",
"shortDescription": "\u042d\u0442\u043e\u0442 \u043a\u0430\u043d\u0430\u043b \u043f\u043e\u0441\u0432\u044f\u0449\u0435\u043d \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u044b\u043c \u0432\u0438\u0434\u0430\u043c \u0441\u043f\u043e\u0440\u0442\u0430. \u041d\u0430 \u00ab\u0418\u0433\u0440\u0435\u00bb \u2014 \u0442\u043e\u043f\u043e\u0432\u044b\u0435 \u0442\u0443\u0440\u043d\u0438\u0440\u044b \u043f\u043e \u0431\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b\u0443, \u0433\u0430\u043d\u0434\u0431\u043e\u043b\u0443, \u0432\u043e\u043b\u0435\u0439\u0431\u043e\u043b\u0443 \u0438 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e. \u0410 \u043d\u0430\u0448\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0442\u043e\u0440\u043e\u0432 \u2014 \u0432\u0441\u0435\u0433\u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u043f\u043e\u043b\u043d\u043e\u0439 \u043e\u0442\u0434\u0430\u0447\u0435\u0439!",
"videoPlayerCode": "\/\/video.matchtv.ru\/iframe\/feed\/start\/na_5e5f7b66fb3f1deb56f706bef2366edf\/17_76666997491217f4514cc1c9316ab99d\/791e47a7b142fe09d5040cc98caf676e\/4934710916?sr=14\u0026type_id=\u0026width=100%25\u0026height=100%25\u0026iframe_width=100%25\u0026iframe_height=100%25\u0026lang=ru\u0026skin_name=matchtv",
"platformVideoId": "76666997491217f4514cc1c9316ab99d",
"package": "16",
"isNeedAuthorization": false,
"webcasterVideoId": 606421,
"chatRoomId": null,
"seoH1": "\u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430 | \u041c! \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c",
"seoTitle": "\u041a\u0430\u043d\u0430\u043b \u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430 - \u043f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u043e\u043d\u043b\u0430\u0439\u043d, \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435",
"seoDescription": "\u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440 \u0444\u0435\u0434\u0435\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u043a\u0430\u043d\u0430\u043b\u0430 \u00ab\u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430\u00bb - \u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e \u043e\u043d\u043b\u0430\u0439\u043d \u0432 \u0445\u043e\u0440\u043e\u0448\u0435\u043c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 \u00ab\u041c\u0430\u0442\u0447 \u0422\u0412\u00bb. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u0438 \u0441\u043e\u0440\u0435\u0432\u043d\u043e\u0432\u0430\u043d\u0438\u0439, \u043d\u043e\u0432\u043e\u0441\u0442\u0438, \u0440\u0435\u043f\u043e\u0440\u0442\u0430\u0436\u0438, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0438\u043b\u044c\u043c\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u0430\u043d\u0430\u043b\u0435 \u00ab\u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430\u00bb!",
"seoKeywords": "\u041c\u0430\u0442\u0447! \u0418\u0433\u0440\u0430",
"seoMeta": null,
"seoText": null,
"imageUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/e939\/2d16\/e9392d16bb9fbda00953e29d7a3af284.jpg",
"thumbnailUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/6b45\/0490\/6b4504901702d992df910d8072e67eee.png",
"iconUrl": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/195d\/f488\/195df488b284a339df19aaa6660a70cd.svg",
"tvChannel": {
"id": 138,
"name": "\u041c\u0430\u0442\u0447 \u0418\u0433\u0440\u0430",
"serviceTvId": 2945,
"priority": 90
},
"terms": [
{ "id": 45, "title": "\u0411\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b" },
{ "id": 169, "title": "\u0422\u0435\u043d\u043d\u0438\u0441" },
{ "id": 178, "title": "\u0412\u043e\u043b\u0435\u0439\u0431\u043e\u043b" }
],
"currentTvTransmissions": [
{
"id": 17829708,
"title": "\u0412\u043e\u043b\u0435\u0439\u0431\u043e\u043b. \u041a\u0443\u0431\u043e\u043a \u0420\u043e\u0441\u0441\u0438\u0438. \u0416\u0435\u043d\u0449\u0438\u043d\u044b. \u0424\u0438\u043d\u0430\u043b \u0448\u0435\u0441\u0442\u0438. 1\/2 \u0444\u0438\u043d\u0430\u043b\u0430. \u0022\u0414\u0438\u043d\u0430\u043c\u043e\u0022 (\u041c\u043e\u0441\u043a\u0432\u0430) - \u0022\u0423\u0440\u0430\u043b\u043e\u0447\u043a\u0430-\u041d\u0422\u041c\u041a\u0022 (\u0421\u0432\u0435\u0440\u0434\u043b\u043e\u0432\u0441\u043a\u0430\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u044c). \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041c\u043e\u0441\u043a\u0432\u044b [6+]",
"description": "",
"start": 1782544500,
"finish": 1782554100,
"startTime": "2026-06-27T10:15:00+03:00",
"finishTime": "2026-06-27T12:55:00+03:00",
"ageRating": "6+",
"sourceImagePath": "https:\/\/s-cdn.sportbox.ru\/images\/tv_transmission\/17829708-1782195506.jpg"
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,115 @@
{
"result": {
"info": {
"filters": [
"\u0444\u0443\u0442\u0431\u043e\u043b",
"\u0445\u043e\u043a\u043a\u0435\u0439",
"\u0435\u0434\u0438\u043d\u043e\u0431\u043e\u0440\u0441\u0442\u0432\u0430",
"\u0431\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b",
"\u0431\u0438\u0430\u0442\u043b\u043e\u043d",
"\u043b\u044b\u0436\u0438"
],
"availableDates": { "min": "2026-06-24T00:00:00+03:00", "max": "2026-07-05T00:00:00+03:00" }
},
"channels": [
{
"id": 10,
"tvChannelId": 130,
"alias": "matchtv",
"name": "\u041c\u0430\u0442\u0447 \u0422\u0412",
"url": "\/channel\/matchtv",
"icon": "https:\/\/fb-cdn.matchtv.ru\/files\/default\/upload\/4fbd\/42cc\/4fbd42ccdde063ff630a7970dbce1034.png",
"currentTransmissionProgress": 97,
"schedule": [
{
"time": "06:00",
"title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447! \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
},
{ "time": "09:00", "title": "\u041d\u043e\u0432\u043e\u0441\u0442\u0438", "genre": "", "current": false },
{
"time": "09:05",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u041a\u043e\u0442-\u0434\u0027\u0418\u0432\u0443\u0430\u0440 - \u041d\u043e\u0440\u0432\u0435\u0433\u0438\u044f. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410 [6+]",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": false
},
{
"time": "11:20",
"title": "\u0423\u043b\u0451\u0442\u043d\u044b\u0439 \u0444\u0443\u0442\u0431\u043e\u043b. \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
},
{
"time": "12:10",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. \u041e\u0431\u0437\u043e\u0440 [6+]",
"genre": "",
"current": false
},
{ "time": "12:40", "title": "\u041d\u043e\u0432\u043e\u0441\u0442\u0438", "genre": "", "current": false },
{
"time": "12:45",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u041c\u0435\u043a\u0441\u0438\u043a\u0430 - \u042d\u043a\u0432\u0430\u0434\u043e\u0440. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u041c\u0435\u043a\u0441\u0438\u043a\u0438 [6+]",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": false
},
{
"time": "15:00",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u0424\u0440\u0430\u043d\u0446\u0438\u044f - \u0428\u0432\u0435\u0446\u0438\u044f. \u0422\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410 [6+]",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": false
},
{
"time": "17:15",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. \u041e\u0431\u0437\u043e\u0440 [6+]",
"genre": "",
"current": false
},
{ "time": "17:45", "title": "\u041d\u043e\u0432\u043e\u0441\u0442\u0438", "genre": "", "current": false },
{
"time": "17:50",
"title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447! \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
},
{
"time": "18:30",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u0410\u043d\u0433\u043b\u0438\u044f - \u0414\u0420 \u041a\u043e\u043d\u0433\u043e. \u041f\u0440\u044f\u043c\u0430\u044f \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": true
},
{
"time": "21:05",
"title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447! \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
},
{
"time": "22:00",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u0411\u0435\u043b\u044c\u0433\u0438\u044f - \u0421\u0435\u043d\u0435\u0433\u0430\u043b. \u041f\u0440\u044f\u043c\u0430\u044f \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": false
},
{
"time": "01:05",
"title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447! \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
},
{
"time": "02:00",
"title": "\u0424\u0443\u0442\u0431\u043e\u043b. \u0427\u0435\u043c\u043f\u0438\u043e\u043d\u0430\u0442 \u043c\u0438\u0440\u0430-2026. 1\/16 \u0444\u0438\u043d\u0430\u043b\u0430. \u0421\u0428\u0410 - \u0411\u043e\u0441\u043d\u0438\u044f \u0438 \u0413\u0435\u0440\u0446\u0435\u0433\u043e\u0432\u0438\u043d\u0430. \u041f\u0440\u044f\u043c\u0430\u044f \u0442\u0440\u0430\u043d\u0441\u043b\u044f\u0446\u0438\u044f \u0438\u0437 \u0421\u0428\u0410",
"genre": "\u0444\u0443\u0442\u0431\u043e\u043b",
"current": false
},
{
"time": "05:05",
"title": "\u0412\u0441\u0435 \u043d\u0430 \u041c\u0430\u0442\u0447! \u041f\u0440\u044f\u043c\u043e\u0439 \u044d\u0444\u0438\u0440",
"genre": "",
"current": false
}
]
}
]
}
}

View File

@@ -6,5 +6,7 @@ OPENWEATHER_MOCK_SOURCE = MockSource(
Path(__file__).parent, Path(__file__).parent,
{ {
"forecast": "forecast.json", "forecast": "forecast.json",
"direct": "direct.json",
"reverse": "reverse.json",
}, },
) )

View File

@@ -0,0 +1,254 @@
[
{
"name": "London",
"local_names": {
"fo": "London",
"nn": "London",
"tg": "Лондон",
"uk": "Лондон",
"lt": "Londonas",
"zu": "ILondon",
"eo": "Londono",
"os": "Лондон",
"pa": "ਲੰਡਨ",
"jv": "London",
"hu": "London",
"gd": "Lunnainn",
"hy": "Լոնդոն",
"to": "Lonitoni",
"sr": "Лондон",
"sv": "London",
"ku": "London",
"te": "లండన్",
"tw": "London",
"co": "Londra",
"eu": "Londres",
"et": "London",
"ca": "Londres",
"nl": "Londen",
"kl": "London",
"fi": "Lontoo",
"az": "London",
"mr": "लंडन",
"km": "ឡុងដ៍",
"af": "Londen",
"hi": "लंदन",
"gl": "Londres",
"ka": "ლონდონი",
"ff": "London",
"de": "London",
"sl": "London",
"th": "ลอนดอน",
"bn": "লন্ডন",
"pl": "Londyn",
"an": "Londres",
"bg": "Лондон",
"ht": "Lonn",
"so": "London",
"mn": "Лондон",
"id": "London",
"ko": "런던",
"yo": "Lọndọnu",
"sa": "लन्डन्",
"nv": "Tooh Dineʼé Bikin Haalʼá",
"br": "Londrez",
"rm": "Londra",
"ascii": "London",
"en": "London",
"ba": "Лондон",
"ga": "Londain",
"tr": "Londra",
"om": "Landan",
"bh": "लंदन",
"gu": "લંડન",
"ee": "London",
"se": "London",
"vi": "Luân Đôn",
"sk": "Londýn",
"lb": "London",
"cs": "Londýn",
"io": "London",
"ab": "Лондон",
"ug": "لوندۇن",
"ha": "Landan",
"wo": "Londar",
"bi": "London",
"st": "London",
"fj": "Lodoni",
"tt": "Лондон",
"mt": "Londra",
"vo": "London",
"bs": "London",
"or": "ଲଣ୍ଡନ",
"is": "London",
"tk": "London",
"he": "לונדון",
"ia": "London",
"kn": "ಲಂಡನ್",
"ie": "London",
"es": "Londres",
"my": "လန်ဒန်မြို့",
"sn": "London",
"zh": "伦敦",
"wa": "Londe",
"fy": "Londen",
"sq": "Londra",
"be": "Лондан",
"oc": "Londres",
"hr": "London",
"da": "London",
"bm": "London",
"ru": "Лондон",
"ne": "लन्डन",
"pt": "Londres",
"lo": "ລອນດອນ",
"ro": "Londra",
"cu": "Лондонъ",
"ce": "Лондон",
"ny": "London",
"am": "ለንደን",
"gv": "Lunnin",
"no": "London",
"it": "Londra",
"cv": "Лондон",
"ta": "இலண்டன்",
"ar": "لندن",
"av": "Лондон",
"sm": "Lonetona",
"ja": "ロンドン",
"ky": "Лондон",
"si": "ලන්ඩන්",
"tl": "Londres",
"na": "London",
"sw": "London",
"mi": "Rānana",
"lv": "Londona",
"gn": "Lóndyre",
"su": "London",
"mg": "Lôndôna",
"ml": "ലണ്ടൻ",
"fr": "Londres",
"ur": "علاقہ لندن",
"kw": "Loundres",
"yi": "לאנדאן",
"ig": "London",
"sc": "Londra",
"ln": "Lóndɛlɛ",
"kk": "Лондон",
"el": "Λονδίνο",
"sd": "لنڊن",
"li": "Londe",
"cy": "Llundain",
"ms": "London",
"fa": "لندن",
"sh": "London",
"feature_name": "London",
"uz": "London",
"bo": "ལོན་ཊོན།",
"mk": "Лондон",
"qu": "London",
"ay": "London",
"kv": "Лондон",
"ps": "لندن"
},
"lat": 51.5073219,
"lon": -0.1276474,
"country": "GB",
"state": "England"
},
{
"name": "City of London",
"local_names": {
"lt": "Londono Sitis",
"zh": "倫敦市",
"hi": "सिटी ऑफ़ लंदन",
"ko": "시티 오브 런던",
"es": "City de Londres",
"ur": "لندن شہر",
"uk": "Лондонське Сіті",
"he": "הסיטי של לונדון",
"en": "City of London",
"pt": "Cidade de Londres",
"fr": "Cité de Londres",
"ru": "Сити"
},
"lat": 51.5156177,
"lon": -0.0919983,
"country": "GB",
"state": "England"
},
{
"name": "London",
"local_names": {
"ga": "Londain",
"ka": "ლონდონი",
"yi": "לאנדאן",
"cr": "ᓬᐊᐣᑕᐣ",
"th": "ลอนดอน",
"he": "לונדון",
"el": "Λόντον",
"lt": "Londonas",
"ja": "ロンドン",
"fa": "لندن",
"bn": "লন্ডন",
"iu": "ᓚᓐᑕᓐ",
"ru": "Лондон",
"oj": "Baketigweyaang",
"ug": "لوندۇن",
"en": "London",
"be": "Лондан",
"ar": "لندن",
"fr": "London",
"lv": "Landona",
"ko": "런던",
"hy": "Լոնտոն"
},
"lat": 42.9832406,
"lon": -81.243372,
"country": "CA",
"state": "Ontario"
},
{
"name": "Chelsea",
"local_names": {
"id": "Chelsea, London",
"ru": "Челси",
"ga": "Chelsea",
"ur": "چیلسی، لندن",
"et": "Chelsea",
"pl": "Chelsea",
"da": "Chelsea",
"ar": "تشيلسي",
"fa": "چلسی",
"ko": "첼시",
"sv": "Chelsea, London",
"es": "Chelsea",
"eu": "Chelsea",
"nl": "Chelsea",
"sk": "Chelsea",
"no": "Chelsea",
"af": "Chelsea, Londen",
"uk": "Челсі",
"he": "צ'לסי",
"it": "Chelsea",
"hu": "Chelsea",
"ja": "チェルシー",
"zh": "車路士",
"hi": "चेल्सी, लंदन",
"pt": "Chelsea",
"az": "Çelsi",
"el": "Τσέλσι",
"vi": "Chelsea, Luân Đôn",
"en": "Chelsea",
"de": "Chelsea",
"tr": "Chelsea, Londra",
"fr": "Chelsea",
"sh": "Chelsea, London"
},
"lat": 51.4875167,
"lon": -0.1687007,
"country": "GB",
"state": "England"
},
{ "name": "London", "lat": 37.1289771, "lon": -84.0832646, "country": "US", "state": "Kentucky" }
]

View File

@@ -0,0 +1,18 @@
[
{
"name": "Oryol",
"local_names": {
"be": "Арол",
"ca": "Oriol",
"cs": "Orjol",
"de": "Orjol",
"en": "Oryol",
"ru": "Орёл",
"sr": "Орел"
},
"lat": 52.968017149999994,
"lon": 36.09949941816104,
"country": "RU",
"state": "Oryol Oblast"
}
]

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

@@ -3,7 +3,6 @@ import datetime
import pytest import pytest
from gallery.painting.matchtv.api import MatchTvApi from gallery.painting.matchtv.api import MatchTvApi
from gallery.sketch.schedule.model import ChannelId
from tests.data.matchtv import MATCHTV_MOCK_SOURCE from tests.data.matchtv import MATCHTV_MOCK_SOURCE
@@ -14,9 +13,12 @@ def matchtv_api_fixture() -> MatchTvApi:
return api return api
async def test_search(matchtv_api: MatchTvApi):
result = await matchtv_api.find_channels("матч")
assert len(result) == 6
async def test_channel(matchtv_api: MatchTvApi): async def test_channel(matchtv_api: MatchTvApi):
result = await matchtv_api.get_channel_schedule( result = await matchtv_api.get_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

View File

@@ -19,6 +19,11 @@ def openweather_api_fixture() -> OpenWeatherApi:
return api return api
async def test_search(openweather_api: OpenWeatherApi):
result = await openweather_api.find_locations("test")
assert len(result) == 5
async def test_day(openweather_api: OpenWeatherApi): async def test_day(openweather_api: OpenWeatherApi):
result = await openweather_api.get_day("52.968498:36.0695", datetime.date(2024, 8, 23)) result = await openweather_api.get_day("52.968498:36.0695", datetime.date(2024, 8, 23))
assert len(result.values) == 8 assert len(result.values) == 8

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,15 @@ 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("матч")
assert len(result) == 4
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_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