mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-22 01:13:02 +08:00
b24f39456e
Windows 下 git core.ignorecase=true 导致文件重命名时 Git 无法 检测到大小写变化,推送后服务器上仍为旧命名。通过两步 git mv 强制更新索引,统一所有文件名为规范大小写。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
208 lines
5.0 KiB
Python
208 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright (c) 2026 KenanZhu.
|
|
All rights reserved.
|
|
|
|
This software is provided "as is", without any warranty of any kind.
|
|
You may use, modify, and distribute this file under the terms of the MIT License.
|
|
See the LICENSE file for details.
|
|
"""
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
|
|
|
|
def timeStrToMins(
|
|
time_str: str,
|
|
) -> int:
|
|
|
|
hour, minute = map(int, time_str.split(":"))
|
|
return hour*60 + minute
|
|
|
|
def minsToTimeStr(
|
|
mins: int,
|
|
) -> str:
|
|
|
|
hour, minute = divmod(int(mins), 60)
|
|
return f"{hour:02d}:{minute:02d}"
|
|
|
|
@dataclass
|
|
class TimeOption:
|
|
|
|
value: int
|
|
element_text: str
|
|
|
|
|
|
@dataclass
|
|
class TimeSelectionResult:
|
|
|
|
selected_index: int = -1
|
|
selected_value: int = 0
|
|
display_text: str = ""
|
|
actual_diff: int = 0
|
|
free_times: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class TimeRangeResult:
|
|
|
|
begin_result: TimeSelectionResult = field(default_factory=TimeSelectionResult)
|
|
end_result: TimeSelectionResult = field(default_factory=TimeSelectionResult)
|
|
actual_begin_mins: int = -1
|
|
actual_end_mins: int = -1
|
|
expect_end_mins: int = 0
|
|
|
|
|
|
class TimeOptionReader(ABC):
|
|
|
|
@abstractmethod
|
|
def readOptions(
|
|
self,
|
|
elements: list
|
|
) -> list[TimeOption]:
|
|
...
|
|
|
|
def formatFreeTime(
|
|
self,
|
|
opt: TimeOption
|
|
) -> str:
|
|
|
|
return opt.element_text
|
|
|
|
|
|
class ReserveTimeReader(TimeOptionReader):
|
|
"""
|
|
Reads the ``time`` HTML attribute for the reserve flow.
|
|
Special value ``"now"`` is resolved to the current wall-clock minute.
|
|
"""
|
|
|
|
def readOptions(
|
|
self,
|
|
elements: list
|
|
) -> list[TimeOption]:
|
|
|
|
options: list[TimeOption] = []
|
|
for el in elements:
|
|
time_attr = el.get_attribute("time")
|
|
if time_attr == "now":
|
|
now = datetime.now()
|
|
value = now.hour * 60 + now.minute
|
|
elif time_attr and time_attr.isdigit():
|
|
value = int(time_attr)
|
|
else:
|
|
continue
|
|
options.append(TimeOption(value=value, element_text=el.text.strip()))
|
|
return options
|
|
|
|
def formatFreeTime(
|
|
self,
|
|
opt: TimeOption
|
|
) -> str:
|
|
|
|
return minsToTimeStr(opt.value)
|
|
|
|
|
|
class RenewTimeReader(TimeOptionReader):
|
|
"""
|
|
Reads the ``id`` HTML attribute for the renewal flow.
|
|
"""
|
|
|
|
def readOptions(
|
|
self,
|
|
elements: list
|
|
) -> list[TimeOption]:
|
|
|
|
options: list[TimeOption] = []
|
|
for el in elements:
|
|
time_attr = el.get_attribute("id")
|
|
if not (time_attr and time_attr.isdigit()):
|
|
continue
|
|
options.append(TimeOption(value=int(time_attr), element_text=el.text.strip()))
|
|
return options
|
|
|
|
|
|
class TimeDecisionMaker:
|
|
|
|
def __init__(
|
|
self,
|
|
reader: TimeOptionReader
|
|
) -> None:
|
|
|
|
self._reader = reader
|
|
|
|
def decide(
|
|
self,
|
|
elements: list,
|
|
target_time: int,
|
|
max_time_diff: int,
|
|
prefer_earlier: bool
|
|
) -> TimeSelectionResult:
|
|
|
|
options = self._reader.readOptions(elements)
|
|
free_times = [self._reader.formatFreeTime(o) for o in options]
|
|
best_diff = max_time_diff
|
|
best_actual_diff = None
|
|
best_index = -1
|
|
for i, opt in enumerate(options):
|
|
actual_diff = opt.value - target_time
|
|
abs_diff = abs(actual_diff)
|
|
if abs_diff < best_diff or (
|
|
abs_diff == best_diff
|
|
and (
|
|
(prefer_earlier and actual_diff <= 0)
|
|
or (not prefer_earlier and actual_diff >= 0)
|
|
)
|
|
):
|
|
best_diff = abs_diff
|
|
best_actual_diff = actual_diff
|
|
best_index = i
|
|
if best_index == -1:
|
|
return TimeSelectionResult(free_times=free_times)
|
|
chosen = options[best_index]
|
|
return TimeSelectionResult(
|
|
selected_index=best_index,
|
|
selected_value=chosen.value,
|
|
display_text=chosen.element_text,
|
|
actual_diff=best_actual_diff or 0,
|
|
free_times=free_times,
|
|
)
|
|
|
|
|
|
class TimeSelectMaker:
|
|
|
|
LIBRARY_CLOSE_MINS = 1350 # 22:30
|
|
MAX_DURATION_HOURS = 8
|
|
|
|
@staticmethod
|
|
def calcEndTime(
|
|
begin_mins: int,
|
|
duration: int,
|
|
library_close_mins: int = LIBRARY_CLOSE_MINS
|
|
) -> int:
|
|
|
|
expect_end_mins = int(begin_mins + duration*60)
|
|
if expect_end_mins > library_close_mins:
|
|
return library_close_mins
|
|
return expect_end_mins
|
|
|
|
@staticmethod
|
|
def calcRemainingDuration(
|
|
end_time_str: str,
|
|
target_mins: int,
|
|
library_close_mins: int = LIBRARY_CLOSE_MINS
|
|
) -> int:
|
|
|
|
return library_close_mins - timeStrToMins(end_time_str)
|
|
|
|
@staticmethod
|
|
def forReserve(
|
|
) -> TimeDecisionMaker:
|
|
|
|
return TimeDecisionMaker(ReserveTimeReader())
|
|
|
|
@staticmethod
|
|
def forRenew(
|
|
) -> TimeDecisionMaker:
|
|
|
|
return TimeDecisionMaker(RenewTimeReader())
|