feat: add common view module
This commit is contained in:
@@ -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
|
||||||
|
|||||||
9
gallery/easel/route/view/common/__init__.py
Normal file
9
gallery/easel/route/view/common/__init__.py
Normal 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"))
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
76
gallery/easel/route/view/common/static/style.css
Normal file
76
gallery/easel/route/view/common/static/style.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
5
gallery/easel/route/view/schedule/filters.py
Normal file
5
gallery/easel/route/view/schedule/filters.py
Normal file
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
1
gallery/version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.1.0"
|
||||||
Reference in New Issue
Block a user