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

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

View File

@@ -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"))

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -0,0 +1,5 @@
import datetime
def timedelta_format(value: datetime.timedelta) -> str:
return ":".join(str(value).split(":")[:2])

View File

@@ -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;
} }

View File

@@ -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 %}

View File

@@ -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 %}

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

View File

@@ -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;

View File

@@ -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 %}

View File

@@ -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>

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
) )

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
gallery/version.py Normal file
View File

@@ -0,0 +1 @@
__version__ = "0.1.0"