feat(schedule): add tv schedule group page

This commit is contained in:
2026-09-05 16:30:37 +03:00
parent 24214ebc3a
commit d44c8076b4
6 changed files with 134 additions and 7 deletions
@@ -1,6 +1,7 @@
import datetime import datetime
import typing import typing
from pathlib import Path from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlsplit
from babel.dates import format_date from babel.dates import format_date
from fastapi import Request from fastapi import Request
@@ -28,6 +29,14 @@ def base_context_processor(request: Request) -> dict:
} }
def build_url(request: Request, query: dict | None = None) -> str:
parts = urlsplit(str(request.url))
if query:
query_dict = {**dict(parse_qsl(parts.query)), **query}
parts = parts._replace(query=urlencode(query_dict))
return parts.geturl()
def build_templates( def build_templates(
templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None templates_dir: Path | None = None, filters: dict | None = None, context_processor: ContextProcessor | None = None
) -> Jinja2Templates: ) -> Jinja2Templates:
@@ -46,6 +55,7 @@ def build_templates(
"datetime": datetime, "datetime": datetime,
"tag_util": TagUtil, "tag_util": TagUtil,
"DATE_FORMAT": "E, d MMMM Y", "DATE_FORMAT": "E, d MMMM Y",
"build_url": build_url,
} }
) )
if filters: if filters:
@@ -68,3 +68,24 @@ async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends,
"response": response, "response": response,
}, },
) )
@router.get("/group", response_class=HTMLResponse)
async def get_group(request: AppRequest, ids: str, date: datetime.date | None = None, tag: str | None = None):
date = date or datetime.date.today()
if tag:
date = TagUtil.parse_tag(tag).date
channels = [item.split(":") for item in ids.split(" ")]
responses = [
await request.app.state.api.get_api(ScheduleApi, provider).get_schedule(channel_id, date)
for provider, channel_id in channels
]
return templates.TemplateResponse(
request=request,
name="group.html",
context={
"tag_util": TagUtil,
"datetime": datetime,
"responses": responses,
},
)
@@ -1,4 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% set live = request.query_params.live == '1' %}
{% set date = response.date %}
{% block title %} {% block title %}
{{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}} locale=request.state.language)}}
@@ -10,19 +13,23 @@ locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h4> <h4 class="mb-4">
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}" <a class="icon-link {{'disabled' if date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}"> href="{{build_url(request, query={'date': date - datetime.timedelta(days=1)})}}">
<i class="bi bi-arrow-left-square"></i> <i class="bi bi-arrow-left-square"></i>
</a> </a>
<a class="icon-link" <a class="icon-link"
href="../../.."> href=".">
<i class="bi bi-arrow-up-square"></i> <i class="bi bi-arrow-up-square"></i>
</a> </a>
<a class="icon-link {{'text-secondary' if not live else ''}}"
href="{{build_url(request, query={'live': 0 if live else 1})}}">
<i class="bi bi-cast"></i>
</a>
<span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span> <span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span>
<span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span> <span>{{response.channel.name}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link" <a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}"> href="{{build_url(request, query={'date': date + datetime.timedelta(days=1)})}}">
<i class="bi bi-arrow-right-square"></i> <i class="bi bi-arrow-right-square"></i>
</a> </a>
</h4> </h4>
@@ -35,7 +42,7 @@ locale=request.state.language)}}
</thead> </thead>
<tbody> <tbody>
{% for value in response.values %} {% for value in response.values %}
<tr class="{{'table-success' if value.live else ''}}"> <tr class="{{'d-none' if (live and not value.live) else 'table-success' if (not live and value.live) else ''}}">
<td> <td>
<span>{{value.start.strftime('%H:%M')}}</span> <span>{{value.start.strftime('%H:%M')}}</span>
<span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span> <span class="small ms-1">({{(value.end - value.start) | timedelta_format}})</span>
@@ -0,0 +1,59 @@
{% extends "base.html" %}
{% set live = request.query_params.live == '1' %}
{% set date = responses[0].date %}
{% block title %}
{{_("TV program")}}
{% endblock %}
{% block header %}
<app-link href="/schedule"
icon="tv">{{_("TV program")}}</app-link>
{% endblock %}
{% block content %}
<h4 class="mb-4">
<a class="icon-link {{'disabled' if date == datetime.date.today() else ''}}"
href="{{build_url(request, query={'date': date - datetime.timedelta(days=1)})}}">
<i class="bi bi-arrow-left-square"></i>
</a>
<a class="icon-link"
href=".">
<i class="bi bi-arrow-up-square"></i>
</a>
<a class="icon-link {{'text-secondary' if not live else ''}}"
href="{{build_url(request, query={'live': 0 if live else 1})}}">
<i class="bi bi-cast"></i>
</a>
<span>{{format_date(date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="{{build_url(request, query={'date': date + datetime.timedelta(days=1)})}}">
<i class="bi bi-arrow-right-square"></i>
</a>
</h4>
{% for response in responses %}
<h5>
<span class="badge rounded-pill text-bg-secondary fs-6">{{response.channel.provider}}</span>
<span>{{response.channel.name}}</span>
</h5>
<table class="table">
<thead>
<tr>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
{% for value in response.values %}
<tr class="{{'d-none' if (live and not value.live) else 'table-success' if (not live and value.live) else ''}}">
<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>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% endblock %}
+28 -1
View File
@@ -37,6 +37,9 @@ export class ScheduleChannelManager {
element.setAttribute("removable", ""); element.setAttribute("removable", "");
container.appendChild(element); container.appendChild(element);
} }
const groupElement = new ScheduleChannelGroupElement();
groupElement.setAttribute("channels", JSON.stringify(channels));
container.append(groupElement);
} }
} }
} }
@@ -58,10 +61,10 @@ class ScheduleChannelElement extends HTMLElement {
<a href="${this.channel?.provider}/${this.channel?.id}" <a href="${this.channel?.provider}/${this.channel?.id}"
class="d-flex justify-content-between align-items-start text-decoration-none"> class="d-flex justify-content-between align-items-start text-decoration-none">
<span> <span>
<span class="badge me-1 text-bg-secondary">${this.channel?.provider}</span>
<span class="text-primary">${this.channel?.name}</span> <span class="text-primary">${this.channel?.name}</span>
</span> </span>
<span> <span>
<span class="badge text-bg-secondary">${this.channel?.provider}</span>
<span class="text-danger ms-2" data-remove> <span class="text-danger ms-2" data-remove>
&#x2715; &#x2715;
</span> </span>
@@ -99,6 +102,30 @@ class ScheduleChannelElement extends HTMLElement {
customElements.define("schedule-channel", ScheduleChannelElement); customElements.define("schedule-channel", ScheduleChannelElement);
class ScheduleChannelGroupElement extends HTMLElement {
static observedAttributes = ["channels"];
channels: Channel[] | null = null;
constructor() {
super();
}
connectedCallback() {
this.channels = JSON.parse(this.getAttribute("channels") || "[]");
const ids = this.channels?.map((item) => `${item.provider}:${item.id}`).join("+");
this.innerHTML = `
<a href="group?ids=${ids}"
class="d-flex justify-content-between align-items-start text-decoration-none">
<span>
<span class="text-primary">Open All</span>
</span>
</a>`;
this.classList = "list-group-item list-group-item-action";
}
}
customElements.define("schedule-channel-group", ScheduleChannelGroupElement);
declare global { declare global {
interface Window { interface Window {
scheduleChannelStorage: ScheduleChannelStorage; scheduleChannelStorage: ScheduleChannelStorage;
+3
View File
@@ -45,4 +45,7 @@
&.bi-file-earmark-text { &.bi-file-earmark-text {
mask-image: url(bootstrap-icons/icons/file-earmark-text.svg); mask-image: url(bootstrap-icons/icons/file-earmark-text.svg);
} }
&.bi-cast {
mask-image: url(bootstrap-icons/icons/cast.svg);
}
} }