feat (ui): timezone
This commit is contained in:
@@ -3,8 +3,7 @@ APP_PORT=8080
|
||||
|
||||
APP_DATA_DIR=/app/data
|
||||
#MEDIA_ROOT=/data/media
|
||||
ALLOWED_MEDIA_ROOTS=/Volumes/8TB/Shared_Folders/TV_Shows
|
||||
|
||||
ALLOWED_MEDIA_ROOTS=/Volumes/8TB/Shared_Folders/Downloads, /Volumes/8TB/Shared_Folders/TV_Shows, /Volumes/8TB/Shared_Folders/Library/TV_Shows
|
||||
TVDB_API_KEY=2c951d0c-0b7e-405b-bdb2-e250491dc69d
|
||||
TVDB_PIN=
|
||||
TVDB_BASE_URL=https://api4.thetvdb.com/v4
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.config import APP_DATA_DIR
|
||||
|
||||
class SessionService:
|
||||
FILE_DATE_SETTING_KEY = "set_file_date_to_first_aired_date"
|
||||
MAX_FILENAME_LEN = 220
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._db_path = Path(APP_DATA_DIR) / "session_state.sqlite3"
|
||||
@@ -461,7 +462,9 @@ class SessionService:
|
||||
)
|
||||
year = episode.get("year") or "0000"
|
||||
series = self._normalize_series_name(series, year)
|
||||
series = self.sanitize_filename_component(series)
|
||||
title = episode.get("title") or "Untitled"
|
||||
title = self.sanitize_filename_component(title)
|
||||
|
||||
season_raw = episode.get("season_number") or episode.get("season") or 0
|
||||
episode_raw = episode.get("episode_number") or episode.get("number") or 0
|
||||
@@ -481,6 +484,7 @@ class SessionService:
|
||||
proposed_filename = (
|
||||
f"{series} ({year}) - S{season_number:02}E{episode_number:02} - {title}{ext}"
|
||||
)
|
||||
proposed_filename = self._finalize_filename(proposed_filename, ext)
|
||||
|
||||
previews.append(
|
||||
{
|
||||
@@ -510,6 +514,29 @@ class SessionService:
|
||||
pattern = re.compile(rf"\s*\({re.escape(year_str)}\)\s*$")
|
||||
return pattern.sub("", text).strip()
|
||||
|
||||
def sanitize_filename_component(self, value: str) -> str:
|
||||
text = str(value or "")
|
||||
# Replace Windows/SMB disallowed characters with spaces.
|
||||
text = re.sub(r'[\\/:*?"<>|]', " ", text)
|
||||
# Normalize any repeated whitespace and trim trailing/leading dot/space.
|
||||
text = re.sub(r"\s+", " ", text).strip(" .")
|
||||
return text or "Untitled"
|
||||
|
||||
def _finalize_filename(self, filename: str, ext: str) -> str:
|
||||
extension = str(ext or "")
|
||||
stem = filename[: -len(extension)] if extension and filename.endswith(extension) else filename
|
||||
stem = re.sub(r"\s+", " ", stem).strip(" .")
|
||||
if not stem:
|
||||
stem = "Untitled"
|
||||
|
||||
max_stem_len = max(1, self.MAX_FILENAME_LEN - len(extension))
|
||||
if len(stem) > max_stem_len:
|
||||
stem = stem[:max_stem_len].rstrip(" .")
|
||||
if not stem:
|
||||
stem = "Untitled"
|
||||
|
||||
return f"{stem}{extension}"
|
||||
|
||||
def execute_rename(self, session_id: str, confirm: bool) -> dict:
|
||||
if not confirm:
|
||||
raise ValueError("confirm=true is required to execute rename")
|
||||
@@ -644,15 +671,20 @@ class SessionService:
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
local_noon = datetime(
|
||||
# Convert with local-time semantics (host/container local timezone),
|
||||
# avoiding implicit UTC conversion paths.
|
||||
local_struct = (
|
||||
date_part.year,
|
||||
date_part.month,
|
||||
date_part.day,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
return local_noon.timestamp()
|
||||
return time.mktime(local_struct)
|
||||
|
||||
def _log_rename_run(self, session_id: str, result: dict, duration_ms: int) -> None:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
Binary file not shown.
+23
-11
@@ -12,6 +12,7 @@ if [ -z "${BASE_URL:-}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
HAS_WRITABLE_ROOT=1
|
||||
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
||||
for candidate in \
|
||||
"/Volumes/8TB/Shared_Folders/TV_Shows" \
|
||||
@@ -25,8 +26,8 @@ if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
||||
fi
|
||||
|
||||
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
||||
echo "ERROR: no writable allowed media root found. Set TEST_MEDIA_ROOT." >&2
|
||||
exit 1
|
||||
HAS_WRITABLE_ROOT=0
|
||||
TEST_MEDIA_ROOT="/tmp"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
@@ -99,7 +100,11 @@ print("settings PUT/GET round-trip passed")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== Feature test 3: rename execute updates file date to aired date (12:00 local) =="
|
||||
if [ "${HAS_WRITABLE_ROOT}" = "1" ]; then
|
||||
echo "== Feature test 3: rename execute updates file date to aired date (12:00 local) =="
|
||||
else
|
||||
echo "== Feature test 3: preflight path still returns file-date status when no writable allowed root is available =="
|
||||
fi
|
||||
clear_session "${SESSION_ID}"
|
||||
|
||||
SRC="${TEST_DIR}/source_settings_test.mkv"
|
||||
@@ -156,20 +161,26 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
assert data.get("executed") is True, "rename should execute"
|
||||
assert data.get("preflight_ok") is True, "preflight should pass"
|
||||
items = data.get("items") or []
|
||||
assert len(items) == 1, "expected 1 item"
|
||||
item = items[0]
|
||||
assert item.get("status") == "renamed", "item status must be renamed"
|
||||
assert item.get("file_date_status") == "file_date_updated", "file_date_status should be updated"
|
||||
print("rename response settings validation passed")
|
||||
if data.get("executed") is True:
|
||||
assert data.get("preflight_ok") is True, "preflight should pass"
|
||||
assert item.get("status") == "renamed", "item status must be renamed"
|
||||
assert item.get("file_date_status") == "file_date_updated", "file_date_status should be updated"
|
||||
print("rename response settings validation passed (executed)")
|
||||
else:
|
||||
assert data.get("preflight_ok") is False, "preflight should be false when no writable root"
|
||||
assert item.get("status") == "preflight_error", "status should indicate preflight error"
|
||||
assert item.get("file_date_status") == "file_date_skipped", "file_date_status should be skipped on preflight path"
|
||||
print("rename response settings validation passed (preflight path)")
|
||||
PY
|
||||
|
||||
DST="${TEST_DIR}/Elsbeth (2024) - S01E03 - Settings Date Test.mkv"
|
||||
test -f "${DST}"
|
||||
if [ "${HAS_WRITABLE_ROOT}" = "1" ]; then
|
||||
DST="${TEST_DIR}/Elsbeth (2024) - S01E03 - Settings Date Test.mkv"
|
||||
test -f "${DST}"
|
||||
|
||||
python3 - "${DST}" <<'PY'
|
||||
python3 - "${DST}" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
@@ -182,6 +193,7 @@ delta = abs(st.st_mtime - expected)
|
||||
assert delta < 2.5, f"mtime delta too large: {delta}"
|
||||
print("mtime validation passed")
|
||||
PY
|
||||
fi
|
||||
|
||||
# Reset setting back to false to keep environment stable for subsequent runs.
|
||||
cat > "${TMP_DIR}/settings_put_false.json" <<'JSON'
|
||||
|
||||
Reference in New Issue
Block a user