mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-22 09:23:03 +08:00
fix: 修复 Git 文件名大小写与文件系统不一致的问题
Windows 下 git core.ignorecase=true 导致文件重命名时 Git 无法 检测到大小写变化,推送后服务器上仍为旧命名。通过两步 git mv 强制更新索引,统一所有文件名为规范大小写。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# -*- 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())
|
||||
Reference in New Issue
Block a user