feat: add common view module

This commit is contained in:
2024-08-13 20:54:12 +03:00
parent bf51ddabc0
commit 05161749e8
19 changed files with 155 additions and 88 deletions
+2
View File
@@ -8,6 +8,7 @@ from gallery.sketch.weather.api import WeatherApi
from .route import doc from .route import doc
from .route.api import schedule as schedule_api_route from .route.api import schedule as schedule_api_route
from .route.api import weather as weather_api_route from .route.api import weather as weather_api_route
from .route.view import common as common_view_route
from .route.view import schedule as schedule_view_route from .route.view import schedule as schedule_view_route
from .route.view import weather as weather_view_route from .route.view import weather as weather_view_route
@@ -26,6 +27,7 @@ def build_app(
doc.mount(app) doc.mount(app)
weather_api_route.mount(app) weather_api_route.mount(app)
schedule_api_route.mount(app) schedule_api_route.mount(app)
common_view_route.mount(app)
weather_view_route.mount(app) weather_view_route.mount(app)
schedule_view_route.mount(app) schedule_view_route.mount(app)
return app return app
@@ -0,0 +1,9 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
def mount(app: FastAPI):
base_dir = Path(__file__).parent
app.mount("/static/common", StaticFiles(directory=base_dir / "static"))

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,76 @@
/*
base
*/
body {
font-size: 1.5rem;
}
h3 {
margin: 0.5rem 0;
}
/*
table
*/
table {
table-layout: fixed;
border-collapse: collapse;
}
table,
th,
td {
text-align: center;
}
td {
padding: 0.1rem 0.4rem;
}
/*
a.button
*/
a.button {
text-decoration: none;
color: inherit;
}
.button.disabled {
pointer-events: none;
cursor: default;
color: gray;
filter: grayscale(100%);
}
/*
app
*/
.app-container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
}
ul.app-list {
list-style: none;
}
ul.app-list > li {
border: 1px solid lightgrey;
}
ul.app-list > li > a {
display: block;
padding: 0.5rem 2rem;
text-decoration: none;
color: inherit;
}
ul.app-list > li:hover {
border-color: blue;
}
ul.app-list > li:hover > a {
color: blue;
}
@@ -6,16 +6,18 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from gallery.easel.route.view.weather.util import TagType, TagUtil
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
from gallery.version import __version__
from ..common.util import TagType, TagUtil
from .filters import timedelta_format
def mount(app: FastAPI): def mount(app: FastAPI):
base_dir = Path(__file__).parent base_dir = Path(__file__).parent
app.mount( app.mount("/static/schedule", StaticFiles(directory=base_dir / "static"))
"/schedule/static", StaticFiles(directory=base_dir / "static"), name="static"
)
templates = Jinja2Templates(directory=base_dir / "templates") templates = Jinja2Templates(directory=base_dir / "templates")
templates.env.filters["timedelta_format"] = timedelta_format
@app.get("/schedule", response_class=HTMLResponse) @app.get("/schedule", response_class=HTMLResponse)
async def get_schedule_list(request: Request): async def get_schedule_list(request: Request):
@@ -25,6 +27,7 @@ def mount(app: FastAPI):
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"version": __version__,
"channels": channels, "channels": channels,
}, },
) )
@@ -45,6 +48,7 @@ def mount(app: FastAPI):
request=request, request=request,
name="schedule.html", name="schedule.html",
context={ context={
"version": __version__,
"tag_util": TagUtil, "tag_util": TagUtil,
"datetime": datetime, "datetime": datetime,
"response": response, "response": response,
@@ -0,0 +1,5 @@
import datetime
def timedelta_format(value: datetime.timedelta) -> str:
return ":".join(str(value).split(":")[:2])
@@ -1,21 +1,7 @@
body {
font-size: 1.5rem;
}
.app-container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
}
table,
th,
td { td {
/* border: 1px solid rgba(0, 0, 0, 0.2); */
text-align: left; text-align: left;
} }
td { tr.live {
padding: 0.1rem 0.4rem; font-weight: bold;
} }
@@ -9,14 +9,16 @@
content="ie=edge"> content="ie=edge">
<title>ТВ</title> <title>ТВ</title>
<link rel="stylesheet" <link rel="stylesheet"
href="/schedule/static/style.css?v=1"> href="/static/common/style.css?v={{version}}">
<link rel="stylesheet"
href="/static/schedule/style.css?v={{version}}">
<link rel="icon" <link rel="icon"
href="/schedule/static/favicon.ico" href="/static/common/favicon.ico"
type="image/x-icon"> type="image/x-icon">
</head> </head>
<body class="app-container"> <body class="app-container">
<ul> <ul class="app-list">
{% for channel in channels %} {% for channel in channels %}
<li><a href="schedule/{{channel}}">{{channel}}</a></li> <li><a href="schedule/{{channel}}">{{channel}}</a></li>
{% endfor %} {% endfor %}
@@ -9,28 +9,38 @@
content="ie=edge"> content="ie=edge">
<title>Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</title> <title>Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
<link rel="stylesheet" <link rel="stylesheet"
href="/schedule/static/style.css?v=1"> href="/static/common/style.css?v={{version}}">
<link rel="stylesheet"
href="/static/schedule/style.css?v={{version}}">
<link rel="icon" <link rel="icon"
href="/schedule/static/favicon.ico" href="/static/common/favicon.ico"
type="image/x-icon"> type="image/x-icon">
</head> </head>
<body class="app-container"> <body class="app-container">
<h3> <h3>
{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}} <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>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
<a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h3> </h3>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>TIME</td> <td></td>
<td>NAME</td> <td></td>
<td></td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for value in response.values %} {% for value in response.values %}
<tr> <tr class="{{'live' if value.live else ''}}">
<td>{{value.start.strftime('%H:%M')}}</td> <td>{{value.start.strftime('%H:%M')}}</td>
<td>{{(value.end - value.start) | timedelta_format}}</td>
<td>{{value.label}}</td> <td>{{value.label}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
+5 -4
View File
@@ -9,16 +9,15 @@ from fastapi.templating import Jinja2Templates
from gallery.sketch.weather.api import WeatherApi from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.mock import WEATHER_MOCK_DATA from gallery.sketch.weather.mock import WEATHER_MOCK_DATA
from gallery.sketch.weather.model import WeatherResponse from gallery.sketch.weather.model import WeatherResponse
from gallery.version import __version__
from ..common.util import TagType, TagUtil
from .filters import cloudness_icon, wind_direction_icon from .filters import cloudness_icon, wind_direction_icon
from .util import TagType, TagUtil
def mount(app: FastAPI): def mount(app: FastAPI):
base_dir = Path(__file__).parent base_dir = Path(__file__).parent
app.mount( app.mount("/static/weather", StaticFiles(directory=base_dir / "static"))
"/weather/static", StaticFiles(directory=base_dir / "static"), name="static"
)
templates = Jinja2Templates(directory=base_dir / "templates") templates = Jinja2Templates(directory=base_dir / "templates")
templates.env.filters["wind_direction_icon"] = wind_direction_icon templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon templates.env.filters["cloudness_icon"] = cloudness_icon
@@ -28,6 +27,7 @@ def mount(app: FastAPI):
request=request, request=request,
name="weather.html", name="weather.html",
context={ context={
"version": __version__,
"tag_util": TagUtil, "tag_util": TagUtil,
"datetime": datetime, "datetime": datetime,
"response": response, "response": response,
@@ -42,6 +42,7 @@ def mount(app: FastAPI):
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"version": __version__,
"locations": locations, "locations": locations,
}, },
) )
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,47 +1,3 @@
body {
font-size: 1.5rem;
}
.app-container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
}
h3 {
margin: 0.5rem 0;
}
a.button {
text-decoration: none;
color: inherit;
}
.button.disabled {
pointer-events: none;
cursor: default;
color: gray;
filter: grayscale(100%);
}
table {
/* width: 100%; */
table-layout: fixed;
border-collapse: collapse;
}
table,
th,
td {
/* border: 1px solid rgba(0, 0, 0, 0.2); */
text-align: center;
}
td {
padding: 0.1rem 0.4rem;
}
.header { .header {
font-size: 1rem; font-size: 1rem;
text-align: left; text-align: left;
@@ -9,14 +9,16 @@
content="ie=edge"> content="ie=edge">
<title>Погода</title> <title>Погода</title>
<link rel="stylesheet" <link rel="stylesheet"
href="/weather/static/style.css?v=1"> href="/static/common/style.css?v={{version}}">
<link rel="stylesheet"
href="/static/weather/style.css?v={{version}}">
<link rel="icon" <link rel="icon"
href="/weather/static/favicon.ico" href="/static/common/favicon.ico"
type="image/x-icon"> type="image/x-icon">
</head> </head>
<body class="app-container"> <body class="app-container">
<ul> <ul class="app-list">
{% for location in locations %} {% for location in locations %}
<li><a href="weather/{{location}}">{{location}}</a></li> <li><a href="weather/{{location}}">{{location}}</a></li>
{% endfor %} {% endfor %}
@@ -9,9 +9,11 @@
content="ie=edge"> content="ie=edge">
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title> <title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
<link rel="stylesheet" <link rel="stylesheet"
href="/weather/static/style.css?v=1"> href="/static/common/style.css?v={{version}}">
<link rel="stylesheet"
href="/static/weather/style.css?v={{version}}">
<link rel="icon" <link rel="icon"
href="/weather/static/favicon.ico" href="/static/common/favicon.ico"
type="image/x-icon"> type="image/x-icon">
</head> </head>
+16 -6
View File
@@ -2,6 +2,7 @@ import datetime
import logging import logging
import aiohttp import aiohttp
from aiocache import cached
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from gallery.sketch.schedule.api import ScheduleApi from gallery.sketch.schedule.api import ScheduleApi
@@ -24,6 +25,7 @@ CHANNEL_LIST = [
class MatchTvApi(ScheduleApi): class MatchTvApi(ScheduleApi):
BASE_URL = "https://matchtv.ru" BASE_URL = "https://matchtv.ru"
CACHE_TTL = 30 * 60
USER_AGENT = ( USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) " "Mozilla/5.0 (X11; Linux x86_64) "
@@ -47,6 +49,7 @@ class MatchTvApi(ScheduleApi):
async def get_channels(self) -> list[str]: async def get_channels(self) -> list[str]:
return CHANNEL_LIST return CHANNEL_LIST
@cached(ttl=CACHE_TTL)
async def get_channel_schedule( async def get_channel_schedule(
self, channel_id: str, date: datetime.date self, channel_id: str, date: datetime.date
) -> Schedule: ) -> Schedule:
@@ -55,18 +58,25 @@ class MatchTvApi(ScheduleApi):
soup = BeautifulSoup(data, features="html.parser") soup = BeautifulSoup(data, features="html.parser")
values = [] values = []
channel_name = soup.select_one(".caption__heading").text.split("|")[0].strip() channel_name = soup.select_one(".caption__heading").text.split("|")[0].strip()
current_date = datetime.datetime.combine( current_day = datetime.datetime.combine(
date.today(), datetime.datetime.min.time() date.today(), datetime.datetime.min.time()
) )
end = current_day + datetime.timedelta(days=1, hours=6)
prev_value: ScheduleValue | None = None
for item in soup.select(".teleprogram-schedule .teleprogram-schedule__item"): for item in soup.select(".teleprogram-schedule .teleprogram-schedule__item"):
title = item.select_one(".teleprogram-item__title").text.strip() title = item.select_one(".teleprogram-item__title").text.strip()
time_str = item.select_one(".teleprogram-item__time").text.strip() time_str = item.select_one(".teleprogram-item__time").text.strip()
hours, minutes = map(int, time_str.split(":")) hours, minutes = map(int, time_str.split(":"))
item_date = datetime.datetime.combine( item_date = current_day.replace(hour=hours, minute=minutes)
date, datetime.time(hour=hours, minute=minutes) if prev_value is not None and item_date.hour < prev_value.start.hour:
) current_day += datetime.timedelta(days=1)
values.append(ScheduleValue(start=current_date, end=item_date, label=title)) item_date += datetime.timedelta(days=1)
current_date = item_date live = "Прямая трансляция" in title
value = ScheduleValue(start=item_date, end=end, label=title, live=live)
values.append(value)
if prev_value is not None:
prev_value.end = item_date
prev_value = value
return Schedule( return Schedule(
channel=Channel(id=channel_id, name=channel_name), date=date, values=values channel=Channel(id=channel_id, name=channel_name), date=date, values=values
) )
+1
View File
@@ -18,6 +18,7 @@ class ScheduleValue(Model):
end: datetime.datetime end: datetime.datetime
label: str label: str
category: str | None = None category: str | None = None
live: bool = False
class Schedule(Model): class Schedule(Model):
+1
View File
@@ -0,0 +1 @@
__version__ = "0.1.0"