nonebot-plugin-cnrail 0.2.0__tar.gz → 0.2.2.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/PKG-INFO +10 -3
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/README.md +8 -1
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/nonebot_plugin_cnrail/__init__.py +1 -1
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/nonebot_plugin_cnrail/__main__.py +22 -5
- nonebot_plugin_cnrail-0.2.2.dev1/nonebot_plugin_cnrail/config.py +13 -0
- nonebot_plugin_cnrail-0.2.2.dev1/nonebot_plugin_cnrail/data_source.py +177 -0
- nonebot_plugin_cnrail-0.2.2.dev1/nonebot_plugin_cnrail/models.py +122 -0
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/nonebot_plugin_cnrail/templates/train_table.html.jinja +23 -33
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/pyproject.toml +2 -2
- nonebot_plugin_cnrail-0.2.0/nonebot_plugin_cnrail/config.py +0 -9
- nonebot_plugin_cnrail-0.2.0/nonebot_plugin_cnrail/data_source.py +0 -202
- nonebot_plugin_cnrail-0.2.0/nonebot_plugin_cnrail/models.py +0 -102
- {nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nonebot-plugin-cnrail
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2.dev1
|
|
4
4
|
Summary: NoneBot2 plugin for query the train time table
|
|
5
5
|
Home-page: https://github.com/lgc-NB2Dev/nonebot-plugin-cnrail
|
|
6
6
|
Author-Email: student_2333 <lgc2333@126.com>, XieXiLin <support@xiexilin.com>
|
|
@@ -9,7 +9,7 @@ Project-URL: Homepage, https://github.com/lgc-NB2Dev/nonebot-plugin-cnrail
|
|
|
9
9
|
Requires-Python: <4.0,>=3.9
|
|
10
10
|
Requires-Dist: nonebot2>=2.2.0
|
|
11
11
|
Requires-Dist: httpx>=0.24
|
|
12
|
-
Requires-Dist: nonebot-plugin-alconna>=0.40.
|
|
12
|
+
Requires-Dist: nonebot-plugin-alconna>=0.40.1
|
|
13
13
|
Requires-Dist: nonebot-plugin-htmlrender>=0.2.2
|
|
14
14
|
Requires-Dist: jinja2>=3.1.2
|
|
15
15
|
Requires-Dist: pytz>=2023.3.post1
|
|
@@ -121,7 +121,9 @@ plugins = [
|
|
|
121
121
|
|
|
122
122
|
## ⚙️ 配置
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
125
|
+
| :--------------------: | :--: | :-------------------------------: | :----------------------------: |
|
|
126
|
+
| `CNRAIL_ACG_IMAGE_URL` | 否 | `https://www.loliapi.com/acg/pe/` | 用于指令返回图片背景的图片 URL |
|
|
125
127
|
|
|
126
128
|
## 🎉 使用
|
|
127
129
|
|
|
@@ -162,6 +164,11 @@ Telegram:[@lgc2333](https://t.me/lgc2333)
|
|
|
162
164
|
|
|
163
165
|
## 📝 更新日志
|
|
164
166
|
|
|
167
|
+
### 0.2.1
|
|
168
|
+
|
|
169
|
+
- 适配 [MoeFactory API](https://train.moefactory.com)
|
|
170
|
+
- 新增配置项 `CNRAIL_ACG_IMAGE_URL`
|
|
171
|
+
|
|
165
172
|
### 0.2.0
|
|
166
173
|
|
|
167
174
|
- 适配 Pydantic V1 & V2
|
|
@@ -104,7 +104,9 @@ plugins = [
|
|
|
104
104
|
|
|
105
105
|
## ⚙️ 配置
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
| 配置项 | 必填 | 默认值 | 说明 |
|
|
108
|
+
| :--------------------: | :--: | :-------------------------------: | :----------------------------: |
|
|
109
|
+
| `CNRAIL_ACG_IMAGE_URL` | 否 | `https://www.loliapi.com/acg/pe/` | 用于指令返回图片背景的图片 URL |
|
|
108
110
|
|
|
109
111
|
## 🎉 使用
|
|
110
112
|
|
|
@@ -145,6 +147,11 @@ Telegram:[@lgc2333](https://t.me/lgc2333)
|
|
|
145
147
|
|
|
146
148
|
## 📝 更新日志
|
|
147
149
|
|
|
150
|
+
### 0.2.1
|
|
151
|
+
|
|
152
|
+
- 适配 [MoeFactory API](https://train.moefactory.com)
|
|
153
|
+
- 新增配置项 `CNRAIL_ACG_IMAGE_URL`
|
|
154
|
+
|
|
148
155
|
### 0.2.0
|
|
149
156
|
|
|
150
157
|
- 适配 Pydantic V1 & V2
|
{nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/nonebot_plugin_cnrail/__init__.py
RENAMED
|
@@ -6,7 +6,7 @@ require("nonebot_plugin_htmlrender")
|
|
|
6
6
|
from . import __main__ as __main__ # noqa: E402
|
|
7
7
|
from .config import ConfigModel # noqa: E402
|
|
8
8
|
|
|
9
|
-
__version__ = "0.2.
|
|
9
|
+
__version__ = "0.2.2.dev1"
|
|
10
10
|
__plugin_meta__ = PluginMetadata(
|
|
11
11
|
name="CNRail",
|
|
12
12
|
description="查询 12306 列车时刻表",
|
{nonebot_plugin_cnrail-0.2.0 → nonebot_plugin_cnrail-0.2.2.dev1}/nonebot_plugin_cnrail/__main__.py
RENAMED
|
@@ -10,7 +10,12 @@ from nonebot import logger
|
|
|
10
10
|
from nonebot_plugin_alconna import AlconnaMatcher, CommandResult, on_alconna
|
|
11
11
|
from nonebot_plugin_alconna.uniseg import UniMessage
|
|
12
12
|
|
|
13
|
-
from .data_source import
|
|
13
|
+
from .data_source import (
|
|
14
|
+
MultipleTrainFoundError,
|
|
15
|
+
generate_word,
|
|
16
|
+
query_train_info,
|
|
17
|
+
render_train_info,
|
|
18
|
+
)
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
def parse_date(date_str: str) -> date:
|
|
@@ -22,7 +27,9 @@ def parse_date(date_str: str) -> date:
|
|
|
22
27
|
def parse(df: str) -> Optional[date]:
|
|
23
28
|
with suppress(ValueError):
|
|
24
29
|
parsed = (
|
|
25
|
-
datetime.strptime(date_str, df)
|
|
30
|
+
datetime.strptime(date_str, df)
|
|
31
|
+
.replace(year=today_date.year)
|
|
32
|
+
.date() # noqa: DTZ007
|
|
26
33
|
)
|
|
27
34
|
if parsed < today_date:
|
|
28
35
|
parsed = parsed.replace(year=today_date.year + 1)
|
|
@@ -67,7 +74,9 @@ async def _(matcher: AlconnaMatcher, parma: Arparma):
|
|
|
67
74
|
|
|
68
75
|
try:
|
|
69
76
|
# use local timezone
|
|
70
|
-
date_obj =
|
|
77
|
+
date_obj = (
|
|
78
|
+
parse_date(train_date) if train_date else date.today()
|
|
79
|
+
) # noqa: DTZ011
|
|
71
80
|
except ValueError:
|
|
72
81
|
await matcher.finish("日期格式不正确")
|
|
73
82
|
|
|
@@ -75,7 +84,12 @@ async def _(matcher: AlconnaMatcher, parma: Arparma):
|
|
|
75
84
|
train_info = await query_train_info(train_no, date_obj.isoformat())
|
|
76
85
|
except MultipleTrainFoundError as e:
|
|
77
86
|
much_text = "\n结果过多,仅显示前五个" if len(e.trains) > 5 else ""
|
|
78
|
-
info_text = "\n".join(
|
|
87
|
+
info_text = "\n".join(
|
|
88
|
+
[
|
|
89
|
+
await generate_word(train_code=i, train_date=date_obj.isoformat())
|
|
90
|
+
for i in e.trains[:5]
|
|
91
|
+
],
|
|
92
|
+
)
|
|
79
93
|
await matcher.finish(
|
|
80
94
|
f"查询到多个车次,请检查您的车次是否正确\n{info_text}{much_text}",
|
|
81
95
|
)
|
|
@@ -94,7 +108,10 @@ async def _(matcher: AlconnaMatcher, parma: Arparma):
|
|
|
94
108
|
)
|
|
95
109
|
|
|
96
110
|
try:
|
|
97
|
-
img_bytes = await render_train_info(
|
|
111
|
+
img_bytes = await render_train_info(
|
|
112
|
+
return_data=train_info,
|
|
113
|
+
train_date=date_obj.isoformat(),
|
|
114
|
+
)
|
|
98
115
|
except Exception:
|
|
99
116
|
logger.exception("Failed to render train info")
|
|
100
117
|
await matcher.finish("渲染图片时出现错误,请检查后台输出")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing_extensions import Annotated
|
|
2
|
+
|
|
3
|
+
from nonebot import get_plugin_config
|
|
4
|
+
from pydantic import BaseModel, Field, HttpUrl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigModel(BaseModel):
|
|
8
|
+
CNRAIL_ACG_IMAGE_URL: Annotated[str, HttpUrl] = Field(
|
|
9
|
+
"https://www.loliapi.com/acg/pe/",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
config = get_plugin_config(ConfigModel)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import jinja2
|
|
6
|
+
import pytz
|
|
7
|
+
from nonebot import logger
|
|
8
|
+
from nonebot.compat import type_validate_python
|
|
9
|
+
from nonebot_plugin_htmlrender import get_new_page
|
|
10
|
+
from playwright.async_api import Request, Route
|
|
11
|
+
|
|
12
|
+
from .config import config
|
|
13
|
+
from .models import ReturnData, TrainDetailData, TrainSearchResult, TrainSNData
|
|
14
|
+
|
|
15
|
+
MOERAIL_API_BASE = "https://train.moefactory.com/api/"
|
|
16
|
+
|
|
17
|
+
TEMPLATE_PATH = Path(__file__).parent / "templates" / "train_table.html.jinja"
|
|
18
|
+
|
|
19
|
+
ROUTE_BASE_URL = "https://cnrail.nonebot/"
|
|
20
|
+
ROUTE_IMAGE_URL = f"{ROUTE_BASE_URL}image"
|
|
21
|
+
|
|
22
|
+
TZ_SHANGHAI = pytz.timezone("Asia/Shanghai")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MultipleTrainFoundError(Exception):
|
|
26
|
+
def __init__(self, trains: List[str]) -> None:
|
|
27
|
+
self.trains = trains
|
|
28
|
+
super().__init__(trains)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def generate_word(train_code: str, train_date: str) -> str:
|
|
32
|
+
async with httpx.AsyncClient(
|
|
33
|
+
base_url=MOERAIL_API_BASE,
|
|
34
|
+
follow_redirects=True,
|
|
35
|
+
) as client:
|
|
36
|
+
resp = await client.post(
|
|
37
|
+
url="/trainNumber/query",
|
|
38
|
+
data={
|
|
39
|
+
"date": train_date.replace("-", ""),
|
|
40
|
+
"trainNumber": train_code,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
resp.raise_for_status()
|
|
44
|
+
result = type_validate_python(TrainSearchResult, resp.json()["data"])
|
|
45
|
+
return f"{result.data[0].train_number} | {result.data[0].begin_station_name} - {result.data[0].end_station_name} | 耗时 {result.data[0].duration_minutes} 分钟"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def get_train_sn(train_code: str) -> Optional[List[TrainSNData]]:
|
|
49
|
+
async with httpx.AsyncClient(
|
|
50
|
+
base_url=MOERAIL_API_BASE,
|
|
51
|
+
follow_redirects=True,
|
|
52
|
+
) as client:
|
|
53
|
+
resp = await client.post(
|
|
54
|
+
url="/crTrainSN/query",
|
|
55
|
+
data={
|
|
56
|
+
"keyword": train_code,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
resp.raise_for_status()
|
|
60
|
+
return (
|
|
61
|
+
type_validate_python(List[TrainSNData], resp.json()["data"])
|
|
62
|
+
if resp.json()["data"]
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def query_train_info(
|
|
68
|
+
train_code: str,
|
|
69
|
+
train_date: str,
|
|
70
|
+
) -> Optional[ReturnData]:
|
|
71
|
+
async with httpx.AsyncClient(
|
|
72
|
+
base_url=MOERAIL_API_BASE,
|
|
73
|
+
follow_redirects=True,
|
|
74
|
+
) as client:
|
|
75
|
+
resp = await client.post(
|
|
76
|
+
url="/trainNumber/query",
|
|
77
|
+
data={
|
|
78
|
+
"date": train_date.replace("-", ""),
|
|
79
|
+
"trainNumber": train_code,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
resp.raise_for_status()
|
|
83
|
+
result = type_validate_python(TrainSearchResult, resp.json()["data"])
|
|
84
|
+
|
|
85
|
+
if result.total_count != 1 and result.data[0].train_number != train_code:
|
|
86
|
+
async with httpx.AsyncClient(
|
|
87
|
+
base_url=MOERAIL_API_BASE,
|
|
88
|
+
follow_redirects=True,
|
|
89
|
+
) as client:
|
|
90
|
+
resp = await client.post(
|
|
91
|
+
url="/search/getTrainCandidates",
|
|
92
|
+
data={
|
|
93
|
+
"keywords": train_code,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
resp.raise_for_status()
|
|
97
|
+
result = resp.json()["data"]
|
|
98
|
+
if not result:
|
|
99
|
+
return None
|
|
100
|
+
raise MultipleTrainFoundError(result)
|
|
101
|
+
|
|
102
|
+
search_data = result.data[0]
|
|
103
|
+
|
|
104
|
+
async with httpx.AsyncClient(
|
|
105
|
+
base_url=MOERAIL_API_BASE,
|
|
106
|
+
follow_redirects=True,
|
|
107
|
+
) as client:
|
|
108
|
+
resp = await client.post(
|
|
109
|
+
url="/trainDetails/query",
|
|
110
|
+
data={
|
|
111
|
+
"date": train_date.replace("-", ""),
|
|
112
|
+
"trainIndex": search_data.train_index,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
|
|
117
|
+
datail_data = type_validate_python(TrainDetailData, resp.json()["data"])
|
|
118
|
+
|
|
119
|
+
sn_data = await get_train_sn(train_code)
|
|
120
|
+
|
|
121
|
+
return ReturnData(
|
|
122
|
+
search=search_data,
|
|
123
|
+
datail=datail_data,
|
|
124
|
+
sn=sn_data,
|
|
125
|
+
train_date=train_date,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def render_train_info(return_data: ReturnData, train_date: str) -> bytes:
|
|
130
|
+
template = jinja2.Template(
|
|
131
|
+
TEMPLATE_PATH.read_text(encoding="u8"),
|
|
132
|
+
enable_async=True,
|
|
133
|
+
)
|
|
134
|
+
html = await template.render_async(
|
|
135
|
+
summary=return_data.search,
|
|
136
|
+
detail=return_data.datail,
|
|
137
|
+
sn=(
|
|
138
|
+
next((i.train_sn for i in return_data.sn if i.date == train_date), None)
|
|
139
|
+
if return_data.sn
|
|
140
|
+
else None
|
|
141
|
+
),
|
|
142
|
+
train_date=return_data.train_date,
|
|
143
|
+
)
|
|
144
|
+
if (dbg := Path.cwd() / "cnrail-debug.html").exists():
|
|
145
|
+
dbg.write_text(html, encoding="u8")
|
|
146
|
+
|
|
147
|
+
async def bg_router(route: Route, _: Request):
|
|
148
|
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
149
|
+
resp = await client.get(config.CNRAIL_ACG_IMAGE_URL, follow_redirects=True)
|
|
150
|
+
try:
|
|
151
|
+
resp.raise_for_status()
|
|
152
|
+
except Exception:
|
|
153
|
+
logger.exception("Failed to fetch image")
|
|
154
|
+
await route.abort()
|
|
155
|
+
else:
|
|
156
|
+
await route.fulfill(
|
|
157
|
+
status=resp.status_code,
|
|
158
|
+
headers=dict(resp.headers),
|
|
159
|
+
body=resp.content,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def root_router(route: Route, _: Request):
|
|
163
|
+
await route.fulfill(
|
|
164
|
+
status=200,
|
|
165
|
+
headers={"Content-Type": "text/html"},
|
|
166
|
+
body=html,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async with get_new_page() as page:
|
|
170
|
+
await page.route(ROUTE_BASE_URL, root_router)
|
|
171
|
+
await page.route(ROUTE_IMAGE_URL, bg_router)
|
|
172
|
+
await page.goto(ROUTE_BASE_URL)
|
|
173
|
+
await page.wait_for_selector("#done", state="attached")
|
|
174
|
+
|
|
175
|
+
elem = await page.query_selector(".bg-wrapper")
|
|
176
|
+
assert elem
|
|
177
|
+
return await elem.screenshot(type="jpeg")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
from nonebot.log import logger
|
|
4
|
+
import pytz
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
TZ_SHANGHAI = pytz.timezone("Asia/Shanghai")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TrainSearchData(BaseModel):
|
|
11
|
+
train_index: int = Field(alias="trainIndex")
|
|
12
|
+
train_number: str = Field(alias="trainNumber")
|
|
13
|
+
begin_station_name: str = Field(alias="beginStationName")
|
|
14
|
+
departure_time: str = Field(alias="departureTime")
|
|
15
|
+
end_station_name: str = Field(alias="endStationName")
|
|
16
|
+
arrival_time: str = Field(alias="arrivalTime")
|
|
17
|
+
day_count: int = Field(alias="dayCount")
|
|
18
|
+
duration_minutes: int = Field(alias="durationMinutes")
|
|
19
|
+
distance: int = Field(alias="distance")
|
|
20
|
+
train_type: str = Field(alias="trainType")
|
|
21
|
+
cr_type: int = Field(alias="crType")
|
|
22
|
+
out_of_date_flag: int = Field(alias="outOfDateFlag")
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def pass_time(self) -> str:
|
|
26
|
+
start_datetime = datetime.strptime(self.departure_time, "%H:%M")
|
|
27
|
+
end_datetime = datetime.strptime(self.arrival_time, "%H:%M")
|
|
28
|
+
if end_datetime < start_datetime:
|
|
29
|
+
end_datetime += timedelta(days=1)
|
|
30
|
+
time_difference = end_datetime - start_datetime
|
|
31
|
+
return f"{(str(self.day_count - 1 + time_difference.days) + ' 天') if (self.day_count - 1 + time_difference.days) > 0 else ''} {time_difference.seconds // 3600} 时 {time_difference.seconds % 3600 // 60} 分"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TrainSearchResult(BaseModel):
|
|
35
|
+
page_index: int = Field(alias="pageIndex")
|
|
36
|
+
page_size: int = Field(alias="pageSize")
|
|
37
|
+
total_pages: int = Field(alias="totalPages")
|
|
38
|
+
total_count: int = Field(alias="totalCount")
|
|
39
|
+
data: List[TrainSearchData]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TrainDetailviaSation(BaseModel):
|
|
43
|
+
station_name: str = Field(alias="stationName")
|
|
44
|
+
station_telegram_code: Optional[str] = Field(alias="stationTelegramCode")
|
|
45
|
+
train_number: str = Field(alias="trainNumber")
|
|
46
|
+
arrival_time: Optional[str] = Field(alias="arrivalTime")
|
|
47
|
+
departure_time: Optional[str] = Field(alias="departureTime")
|
|
48
|
+
stop_minutes: int = Field(alias="stopMinutes")
|
|
49
|
+
distance: int
|
|
50
|
+
checkout_name: Optional[str] = Field(alias="checkoutName")
|
|
51
|
+
speed: Optional[int]
|
|
52
|
+
day_index: int = Field(alias="dayIndex")
|
|
53
|
+
company_name: str = Field(alias="companyName")
|
|
54
|
+
province: str
|
|
55
|
+
district: str
|
|
56
|
+
out_of_date_flag: int = Field(alias="outOfDateFlag")
|
|
57
|
+
is_turn: bool = Field(alias="isTurn")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TrainDetailRoutingItem(BaseModel):
|
|
61
|
+
train_number: str = Field(alias="trainNumber")
|
|
62
|
+
begin_station_name: str = Field(alias="beginStationName")
|
|
63
|
+
departure_time: str = Field(alias="departureTime")
|
|
64
|
+
end_station_name: str = Field(alias="endStationName")
|
|
65
|
+
arrival_time: str = Field(alias="arrivalTime")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TrainDetailRoutingMissingItem(BaseModel):
|
|
69
|
+
train_number: str = Field(alias="trainNumber")
|
|
70
|
+
begin_station_name: Optional[str] = Field(alias="beginStationName")
|
|
71
|
+
departure_time: Optional[str] = Field(alias="departureTime")
|
|
72
|
+
end_station_name: Optional[str] = Field(alias="endStationName")
|
|
73
|
+
arrival_time: Optional[str] = Field(alias="arrivalTime")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TrainDetailRouing(BaseModel):
|
|
77
|
+
routing_items: List[
|
|
78
|
+
Union[TrainDetailRoutingItem, TrainDetailRoutingMissingItem]
|
|
79
|
+
] = Field(alias="routingItems")
|
|
80
|
+
train_model: str = Field(alias="trainModel")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TrainDetailData(BaseModel):
|
|
84
|
+
train_number: str = Field(alias="trainNumber")
|
|
85
|
+
train_type: str = Field(alias="trainType")
|
|
86
|
+
company_name: str = Field(alias="companyName")
|
|
87
|
+
food_coach_name: Optional[str] = Field(alias="foodCoachName")
|
|
88
|
+
via_stations: List[TrainDetailviaSation] = Field(alias="viaStations")
|
|
89
|
+
cr_type: int = Field(alias="crType")
|
|
90
|
+
routing: TrainDetailRouing
|
|
91
|
+
|
|
92
|
+
def arrived(self, station_index: int, train_date: str) -> bool: # 有待修改
|
|
93
|
+
logger.debug(f"index: {station_index}, date: {train_date}")
|
|
94
|
+
station = self.via_stations[station_index]
|
|
95
|
+
arrive_time_str = (
|
|
96
|
+
station.arrival_time
|
|
97
|
+
if station.arrival_time is not None
|
|
98
|
+
else station.departure_time
|
|
99
|
+
)
|
|
100
|
+
arrive_datetime = (
|
|
101
|
+
datetime.fromisoformat(
|
|
102
|
+
f"{train_date}T{arrive_time_str}",
|
|
103
|
+
)
|
|
104
|
+
+ timedelta(days=station.day_index)
|
|
105
|
+
).replace(tzinfo=TZ_SHANGHAI)
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"arrive: {arrive_time_str}, arrive_datetime: {arrive_datetime}, now: {datetime.now(TZ_SHANGHAI)}, bool: {datetime.now(TZ_SHANGHAI) >= arrive_datetime}",
|
|
108
|
+
)
|
|
109
|
+
return datetime.now(TZ_SHANGHAI) >= arrive_datetime
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TrainSNData(BaseModel):
|
|
113
|
+
train_sn: str = Field(alias="trainSN")
|
|
114
|
+
date: str
|
|
115
|
+
train_number: str = Field(alias="trainNumber")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ReturnData(BaseModel):
|
|
119
|
+
search: TrainSearchData
|
|
120
|
+
datail: TrainDetailData
|
|
121
|
+
sn: Optional[List[TrainSNData]]
|
|
122
|
+
train_date: str
|
|
@@ -180,15 +180,14 @@
|
|
|
180
180
|
border-top: 1px solid var(--split-line-color);
|
|
181
181
|
margin: 10px 20px;
|
|
182
182
|
padding-top: 10px;
|
|
183
|
-
|
|
184
|
-
justify-content: space-between;
|
|
183
|
+
line-height: 1.7;
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
.admin,
|
|
188
187
|
.emu_no {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
color: var(--top-sub-color);
|
|
190
|
+
text-align: left;
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
.card-bottom {
|
|
@@ -289,46 +288,38 @@
|
|
|
289
288
|
<div class="main">
|
|
290
289
|
<div class="card-top">
|
|
291
290
|
<div class="card-title">
|
|
292
|
-
{{
|
|
291
|
+
{{ summary.train_number }}
|
|
293
292
|
</div>
|
|
294
293
|
<div class="station">
|
|
295
294
|
<div class="station-top">
|
|
296
295
|
<div class="departure-station">
|
|
297
|
-
<div class="place">{{
|
|
296
|
+
<div class="place">{{ summary.begin_station_name }}</div>
|
|
298
297
|
<div class="time">
|
|
299
298
|
<div class="departure-mark">始</div>
|
|
300
|
-
{{
|
|
299
|
+
{{ summary.departure_time }}
|
|
301
300
|
</div>
|
|
302
301
|
</div>
|
|
303
302
|
<div class="arrow-box">
|
|
304
|
-
|
|
305
|
-
{%- if info.arrive_next_day -%}
|
|
306
|
-
<div class="duration">历时 {{ total_time[0] }} 时 {{ total_time[1] }} 分</div>
|
|
303
|
+
<div class="duration">历时 {{ summary.pass_time }}</div>
|
|
307
304
|
<div class="arrow-icon">
|
|
308
305
|
<div class="line"></div>
|
|
309
306
|
<div class="triangle"></div>
|
|
310
307
|
</div>
|
|
311
|
-
{%- else -%}
|
|
312
|
-
<div class="date">{{ info.date_summary }}</div>
|
|
313
|
-
<div class="arrow-icon">
|
|
314
|
-
<div class="line"></div>
|
|
315
|
-
<div class="triangle"></div>
|
|
316
|
-
</div>
|
|
317
|
-
<div class="duration">历时 {{ total_time[0] }} 时 {{ total_time[1] }} 分</div>
|
|
318
|
-
{%- endif %}
|
|
319
308
|
</div>
|
|
320
309
|
<div class="terminal-station">
|
|
321
|
-
<div class="place">{{
|
|
310
|
+
<div class="place">{{ summary.end_station_name }}</div>
|
|
322
311
|
<div class="time">
|
|
323
|
-
{{
|
|
312
|
+
{{ summary.arrival_time }}
|
|
324
313
|
<div class="terminal-mark">终</div>
|
|
325
314
|
</div>
|
|
326
315
|
</div>
|
|
327
316
|
</div>
|
|
328
317
|
<div class="station-bottom">
|
|
329
|
-
<div class="admin">担当路局:{{
|
|
330
|
-
{%- if
|
|
331
|
-
<div class="emu_no">车组号:{{
|
|
318
|
+
<div class="admin">担当路局:{{ detail.company_name }}</div>
|
|
319
|
+
{%- if sn -%}
|
|
320
|
+
<div class="emu_no">车组号:{{ sn }}</div>
|
|
321
|
+
{%- else -%}
|
|
322
|
+
<div class="emu_no">列车型号:{{ detail.routing.train_model }}</div>
|
|
332
323
|
{%- endif %}
|
|
333
324
|
</div>
|
|
334
325
|
</div>
|
|
@@ -349,22 +340,21 @@
|
|
|
349
340
|
</tr>
|
|
350
341
|
</thead>
|
|
351
342
|
<tbody>
|
|
352
|
-
{% for station in
|
|
343
|
+
{% for station in detail.via_stations -%}
|
|
353
344
|
<tr>
|
|
354
345
|
<td>
|
|
355
|
-
|
|
346
|
+
|
|
347
|
+
<div class="station-point{% if detail.arrived(loop.index0, train_date) %} arrived{% endif %}"></div>
|
|
356
348
|
</td>
|
|
357
349
|
<td>{{ station.station_name }}</td>
|
|
358
350
|
<td>
|
|
359
|
-
{
|
|
360
|
-
{%- else %}{{ station.start_time }}{% endif -%}
|
|
351
|
+
{% if loop.first %}-{% else %}{{ station.arrival_time}}{% endif %}
|
|
361
352
|
</td>
|
|
362
353
|
<td>
|
|
363
|
-
{
|
|
364
|
-
{%- else %}{{ station.arrive_time }}{% endif -%}
|
|
354
|
+
{% if loop.last %}-{% else %}{{ station.departure_time }}{% endif %}
|
|
365
355
|
</td>
|
|
366
|
-
<td>{% if loop.first %}始发站{% elif loop.last %}终点站{% else %}{{ station.
|
|
367
|
-
<td>{{ station.
|
|
356
|
+
<td>{% if loop.first %}始发站{% elif loop.last %}终点站{% else %}{{ station.stop_minutes }} 分{% endif %}</td>
|
|
357
|
+
<td>{{ station.train_number }}</td>
|
|
368
358
|
</tr>
|
|
369
359
|
{% endfor -%}
|
|
370
360
|
</tbody>
|
|
@@ -595,4 +585,4 @@
|
|
|
595
585
|
main();
|
|
596
586
|
</script>
|
|
597
587
|
|
|
598
|
-
</html>
|
|
588
|
+
</html>
|
|
@@ -9,14 +9,14 @@ authors = [
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"nonebot2>=2.2.0",
|
|
11
11
|
"httpx>=0.24",
|
|
12
|
-
"nonebot-plugin-alconna>=0.40.
|
|
12
|
+
"nonebot-plugin-alconna>=0.40.1",
|
|
13
13
|
"nonebot-plugin-htmlrender>=0.2.2",
|
|
14
14
|
"jinja2>=3.1.2",
|
|
15
15
|
"pytz>=2023.3.post1",
|
|
16
16
|
]
|
|
17
17
|
requires-python = ">=3.9,<4.0"
|
|
18
18
|
readme = "README.md"
|
|
19
|
-
version = "0.2.
|
|
19
|
+
version = "0.2.2.dev1"
|
|
20
20
|
|
|
21
21
|
[project.license]
|
|
22
22
|
text = "MIT"
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
|
-
import jinja2
|
|
7
|
-
import pytz
|
|
8
|
-
from nonebot import logger
|
|
9
|
-
from nonebot.compat import type_validate_python
|
|
10
|
-
from nonebot_plugin_htmlrender import get_new_page
|
|
11
|
-
from playwright.async_api import Request, Route
|
|
12
|
-
|
|
13
|
-
from .models import TrainInfo, TrainStation, TrainSummary
|
|
14
|
-
|
|
15
|
-
CHINA_RAIL_SEARCH_API = "https://search.12306.cn/search/v1/train/search"
|
|
16
|
-
CHINA_RAIL_DETAIL_API = "https://kyfw.12306.cn/otn/queryTrainInfo/query"
|
|
17
|
-
RAIL_RE_API = "https://api.rail.re/"
|
|
18
|
-
|
|
19
|
-
CNRAIL_DATA_BASE_URL = "https://cnrail-data.baka.pub/data/"
|
|
20
|
-
|
|
21
|
-
ACG_IMAGE_URL = "https://www.loliapi.com/acg/pe/"
|
|
22
|
-
|
|
23
|
-
TEMPLATE_PATH = Path(__file__).parent / "templates" / "train_table.html.jinja"
|
|
24
|
-
|
|
25
|
-
ROUTE_BASE_URL = "https://cnrail.nonebot/"
|
|
26
|
-
ROUTE_IMAGE_URL = f"{ROUTE_BASE_URL}image"
|
|
27
|
-
|
|
28
|
-
TZ_SHANGHAI = pytz.timezone("Asia/Shanghai")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class MultipleTrainFoundError(Exception):
|
|
32
|
-
def __init__(self, trains: List[TrainSummary]) -> None:
|
|
33
|
-
self.trains = trains
|
|
34
|
-
super().__init__(trains)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
async def query_emu_from_train_code(train_code: str) -> Optional[List]:
|
|
38
|
-
async with httpx.AsyncClient(base_url=RAIL_RE_API, follow_redirects=True) as client:
|
|
39
|
-
resp = await client.get(f"/train/{train_code}")
|
|
40
|
-
resp.raise_for_status()
|
|
41
|
-
|
|
42
|
-
data = resp.json()
|
|
43
|
-
if not data:
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
return data
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
async def query_emu_from_emu_no(emu_no: str) -> Optional[List]:
|
|
50
|
-
async with httpx.AsyncClient(base_url=RAIL_RE_API, follow_redirects=True) as client:
|
|
51
|
-
resp = await client.get(f"/emu/{emu_no}")
|
|
52
|
-
resp.raise_for_status()
|
|
53
|
-
|
|
54
|
-
data = resp.json()
|
|
55
|
-
if not data:
|
|
56
|
-
return None
|
|
57
|
-
|
|
58
|
-
return data
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def query_train_info(train_code: str, train_date: str) -> Optional[TrainInfo]:
|
|
62
|
-
train_code = train_code.upper()
|
|
63
|
-
|
|
64
|
-
async def get_search_data(train_code: str, train_date: str) -> Optional[List]:
|
|
65
|
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
66
|
-
resp = await client.get(
|
|
67
|
-
CHINA_RAIL_SEARCH_API,
|
|
68
|
-
params={
|
|
69
|
-
"keyword": train_code,
|
|
70
|
-
"date": train_date.replace("-", ""),
|
|
71
|
-
},
|
|
72
|
-
)
|
|
73
|
-
resp.raise_for_status()
|
|
74
|
-
|
|
75
|
-
return resp.json()["data"]
|
|
76
|
-
|
|
77
|
-
raw_data = await get_search_data(train_code=train_code, train_date=train_date)
|
|
78
|
-
if not raw_data:
|
|
79
|
-
async with httpx.AsyncClient(
|
|
80
|
-
base_url=CNRAIL_DATA_BASE_URL,
|
|
81
|
-
follow_redirects=True,
|
|
82
|
-
) as client:
|
|
83
|
-
resp = await client.get("/alias.json")
|
|
84
|
-
resp.raise_for_status()
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
train_code = resp.json()[train_code]
|
|
88
|
-
except KeyError:
|
|
89
|
-
return None
|
|
90
|
-
|
|
91
|
-
raw_data = await get_search_data(train_code=train_code, train_date=train_date)
|
|
92
|
-
if not raw_data:
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
data = type_validate_python(List[TrainSummary], raw_data)
|
|
96
|
-
if len(data) > 1:
|
|
97
|
-
summary = next(
|
|
98
|
-
(train for train in data if train.station_train_code == train_code),
|
|
99
|
-
None,
|
|
100
|
-
)
|
|
101
|
-
if not summary:
|
|
102
|
-
raise MultipleTrainFoundError(data)
|
|
103
|
-
else:
|
|
104
|
-
summary = data[0]
|
|
105
|
-
|
|
106
|
-
if summary.station_train_code != train_code:
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
110
|
-
resp = await client.get(
|
|
111
|
-
CHINA_RAIL_DETAIL_API,
|
|
112
|
-
params={
|
|
113
|
-
"leftTicketDTO.train_no": summary.train_no,
|
|
114
|
-
"leftTicketDTO.train_date": train_date,
|
|
115
|
-
"rand_code": "",
|
|
116
|
-
},
|
|
117
|
-
)
|
|
118
|
-
resp.raise_for_status()
|
|
119
|
-
|
|
120
|
-
raw_data = resp.json()["data"]["data"]
|
|
121
|
-
if not raw_data:
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
stations = type_validate_python(List[TrainStation], raw_data)
|
|
125
|
-
|
|
126
|
-
async with httpx.AsyncClient(
|
|
127
|
-
base_url=CNRAIL_DATA_BASE_URL,
|
|
128
|
-
follow_redirects=True,
|
|
129
|
-
) as client:
|
|
130
|
-
resp = await client.get("/maintance.json")
|
|
131
|
-
resp.raise_for_status()
|
|
132
|
-
|
|
133
|
-
maintancer = resp.json()[train_code]
|
|
134
|
-
|
|
135
|
-
today_date = datetime.now(TZ_SHANGHAI).date()
|
|
136
|
-
train_date_obj = (
|
|
137
|
-
datetime.strptime(train_date, "%Y-%m-%d").replace(tzinfo=TZ_SHANGHAI).date()
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
emu_no = None
|
|
141
|
-
|
|
142
|
-
if (train_date_obj <= today_date) and (
|
|
143
|
-
emu_data := await query_emu_from_train_code(train_code)
|
|
144
|
-
):
|
|
145
|
-
for i in emu_data:
|
|
146
|
-
date_data = (
|
|
147
|
-
datetime.strptime(i["date"], "%Y-%m-%d %H:%M")
|
|
148
|
-
.replace(tzinfo=TZ_SHANGHAI)
|
|
149
|
-
.date()
|
|
150
|
-
)
|
|
151
|
-
if date_data == train_date_obj:
|
|
152
|
-
emu_no = i["emu_no"]
|
|
153
|
-
break
|
|
154
|
-
|
|
155
|
-
return TrainInfo(
|
|
156
|
-
summary=summary,
|
|
157
|
-
stations=stations,
|
|
158
|
-
maintancer=maintancer,
|
|
159
|
-
emu_no=emu_no,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
async def render_train_info(info: TrainInfo) -> bytes:
|
|
164
|
-
template = jinja2.Template(
|
|
165
|
-
TEMPLATE_PATH.read_text(encoding="u8"),
|
|
166
|
-
enable_async=True,
|
|
167
|
-
)
|
|
168
|
-
html = await template.render_async(info=info)
|
|
169
|
-
if (dbg := Path.cwd() / "cnrail-debug.html").exists():
|
|
170
|
-
dbg.write_text(html, encoding="u8")
|
|
171
|
-
|
|
172
|
-
async def bg_router(route: Route, _: Request):
|
|
173
|
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
|
174
|
-
resp = await client.get(ACG_IMAGE_URL)
|
|
175
|
-
try:
|
|
176
|
-
resp.raise_for_status()
|
|
177
|
-
except Exception:
|
|
178
|
-
logger.exception("Failed to fetch image")
|
|
179
|
-
await route.abort()
|
|
180
|
-
else:
|
|
181
|
-
await route.fulfill(
|
|
182
|
-
status=resp.status_code,
|
|
183
|
-
headers=dict(resp.headers),
|
|
184
|
-
body=resp.content,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
async def root_router(route: Route, _: Request):
|
|
188
|
-
await route.fulfill(
|
|
189
|
-
status=200,
|
|
190
|
-
headers={"Content-Type": "text/html"},
|
|
191
|
-
body=html,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
async with get_new_page() as page:
|
|
195
|
-
await page.route(ROUTE_BASE_URL, root_router)
|
|
196
|
-
await page.route(ROUTE_IMAGE_URL, bg_router)
|
|
197
|
-
await page.goto(ROUTE_BASE_URL)
|
|
198
|
-
await page.wait_for_selector("#done", state="attached")
|
|
199
|
-
|
|
200
|
-
elem = await page.query_selector(".bg-wrapper")
|
|
201
|
-
assert elem
|
|
202
|
-
return await elem.screenshot(type="jpeg")
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
from typing import Any, List, Optional, Tuple, cast
|
|
3
|
-
|
|
4
|
-
import pytz
|
|
5
|
-
from pydantic import BaseModel, Field, PrivateAttr
|
|
6
|
-
|
|
7
|
-
TZ_SHANGHAI = pytz.timezone("Asia/Shanghai")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TrainSummary(BaseModel):
|
|
11
|
-
date: str
|
|
12
|
-
from_station: str
|
|
13
|
-
station_train_code: str
|
|
14
|
-
to_station: str
|
|
15
|
-
total_num: str
|
|
16
|
-
train_no: str
|
|
17
|
-
|
|
18
|
-
@property
|
|
19
|
-
def date_iso(self) -> str:
|
|
20
|
-
return f"{self.date[:4]}-{self.date[4:6]}-{self.date[6:]}"
|
|
21
|
-
|
|
22
|
-
@property
|
|
23
|
-
def word(self) -> str:
|
|
24
|
-
return f"{self.station_train_code} {self.from_station}-{self.to_station} | 途经 {self.total_num} 站"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class TrainStation(BaseModel):
|
|
28
|
-
arrive_day_str: str
|
|
29
|
-
arrive_time: str
|
|
30
|
-
station_train_code: str
|
|
31
|
-
station_name: str
|
|
32
|
-
arrive_day_diff: str
|
|
33
|
-
OT: List[Any] = Field(default_factory=list)
|
|
34
|
-
start_time: str
|
|
35
|
-
wz_num: str
|
|
36
|
-
station_no: str
|
|
37
|
-
running_time: str
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def stay_minutes(self) -> int:
|
|
41
|
-
arrive_t = datetime.strptime(self.arrive_time, "%H:%M").replace(
|
|
42
|
-
tzinfo=TZ_SHANGHAI,
|
|
43
|
-
)
|
|
44
|
-
start_t = datetime.strptime(self.start_time, "%H:%M").replace(
|
|
45
|
-
tzinfo=TZ_SHANGHAI,
|
|
46
|
-
)
|
|
47
|
-
if arrive_t > start_t:
|
|
48
|
-
start_t += timedelta(days=1)
|
|
49
|
-
return (start_t - arrive_t).seconds // 60
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class TrainInfo(BaseModel):
|
|
53
|
-
summary: TrainSummary
|
|
54
|
-
stations: List[TrainStation]
|
|
55
|
-
maintancer: str
|
|
56
|
-
emu_no: Optional[str]
|
|
57
|
-
|
|
58
|
-
_arrive_next_day: Optional[bool] = PrivateAttr(None)
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def station_train_codes(self) -> List[str]:
|
|
62
|
-
codes = []
|
|
63
|
-
for x in (x for x in self.stations if x.station_train_code not in codes):
|
|
64
|
-
codes.append(x.station_train_code) # noqa: PERF401
|
|
65
|
-
return codes
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def total_time(self) -> Tuple[int, int]:
|
|
69
|
-
return cast(
|
|
70
|
-
Any,
|
|
71
|
-
tuple(int(x) for x in self.stations[-1].running_time.split(":")),
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def date_summary(self) -> str:
|
|
76
|
-
week_names = ["一", "二", "三", "四", "五", "六", "日"]
|
|
77
|
-
date = datetime.fromisoformat(self.summary.date_iso)
|
|
78
|
-
return f"{date.month} 月 {date.day} 日 周{week_names[date.weekday()]}"
|
|
79
|
-
|
|
80
|
-
@property
|
|
81
|
-
def arrive_next_day(self) -> bool:
|
|
82
|
-
if self._arrive_next_day is None:
|
|
83
|
-
self._arrive_next_day = bool(
|
|
84
|
-
next((x for x in self.stations if int(x.arrive_day_diff) > 0), None),
|
|
85
|
-
)
|
|
86
|
-
return self._arrive_next_day
|
|
87
|
-
|
|
88
|
-
def arrived(self, station_index: int) -> bool:
|
|
89
|
-
# 不在今日到达的车次,拿不到始发日期,摆烂了
|
|
90
|
-
if self.arrive_next_day:
|
|
91
|
-
return True
|
|
92
|
-
|
|
93
|
-
station = self.stations[station_index]
|
|
94
|
-
arrive_time_str = (
|
|
95
|
-
station.arrive_time if ":" in station.arrive_time else station.start_time
|
|
96
|
-
)
|
|
97
|
-
arrive_datetime = datetime.fromisoformat(
|
|
98
|
-
f"{self.summary.date_iso}T{arrive_time_str}",
|
|
99
|
-
).replace(tzinfo=TZ_SHANGHAI)
|
|
100
|
-
# if (day_diff := int(station.arrive_day_diff)) > 0:
|
|
101
|
-
# arrive_time += timedelta(days=day_diff)
|
|
102
|
-
return datetime.now(TZ_SHANGHAI) >= arrive_datetime
|
|
File without changes
|