feat (ui): timezone
This commit is contained in:
@@ -3,8 +3,7 @@ APP_PORT=8080
|
|||||||
|
|
||||||
APP_DATA_DIR=/app/data
|
APP_DATA_DIR=/app/data
|
||||||
#MEDIA_ROOT=/data/media
|
#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_API_KEY=2c951d0c-0b7e-405b-bdb2-e250491dc69d
|
||||||
TVDB_PIN=
|
TVDB_PIN=
|
||||||
TVDB_BASE_URL=https://api4.thetvdb.com/v4
|
TVDB_BASE_URL=https://api4.thetvdb.com/v4
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from app.config import APP_DATA_DIR
|
|||||||
|
|
||||||
class SessionService:
|
class SessionService:
|
||||||
FILE_DATE_SETTING_KEY = "set_file_date_to_first_aired_date"
|
FILE_DATE_SETTING_KEY = "set_file_date_to_first_aired_date"
|
||||||
|
MAX_FILENAME_LEN = 220
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._db_path = Path(APP_DATA_DIR) / "session_state.sqlite3"
|
self._db_path = Path(APP_DATA_DIR) / "session_state.sqlite3"
|
||||||
@@ -461,7 +462,9 @@ class SessionService:
|
|||||||
)
|
)
|
||||||
year = episode.get("year") or "0000"
|
year = episode.get("year") or "0000"
|
||||||
series = self._normalize_series_name(series, year)
|
series = self._normalize_series_name(series, year)
|
||||||
|
series = self.sanitize_filename_component(series)
|
||||||
title = episode.get("title") or "Untitled"
|
title = episode.get("title") or "Untitled"
|
||||||
|
title = self.sanitize_filename_component(title)
|
||||||
|
|
||||||
season_raw = episode.get("season_number") or episode.get("season") or 0
|
season_raw = episode.get("season_number") or episode.get("season") or 0
|
||||||
episode_raw = episode.get("episode_number") or episode.get("number") or 0
|
episode_raw = episode.get("episode_number") or episode.get("number") or 0
|
||||||
@@ -481,6 +484,7 @@ class SessionService:
|
|||||||
proposed_filename = (
|
proposed_filename = (
|
||||||
f"{series} ({year}) - S{season_number:02}E{episode_number:02} - {title}{ext}"
|
f"{series} ({year}) - S{season_number:02}E{episode_number:02} - {title}{ext}"
|
||||||
)
|
)
|
||||||
|
proposed_filename = self._finalize_filename(proposed_filename, ext)
|
||||||
|
|
||||||
previews.append(
|
previews.append(
|
||||||
{
|
{
|
||||||
@@ -510,6 +514,29 @@ class SessionService:
|
|||||||
pattern = re.compile(rf"\s*\({re.escape(year_str)}\)\s*$")
|
pattern = re.compile(rf"\s*\({re.escape(year_str)}\)\s*$")
|
||||||
return pattern.sub("", text).strip()
|
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:
|
def execute_rename(self, session_id: str, confirm: bool) -> dict:
|
||||||
if not confirm:
|
if not confirm:
|
||||||
raise ValueError("confirm=true is required to execute rename")
|
raise ValueError("confirm=true is required to execute rename")
|
||||||
@@ -644,15 +671,20 @@ class SessionService:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
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.year,
|
||||||
date_part.month,
|
date_part.month,
|
||||||
date_part.day,
|
date_part.day,
|
||||||
12,
|
12,
|
||||||
0,
|
0,
|
||||||
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:
|
def _log_rename_run(self, session_id: str, result: dict, duration_ms: int) -> None:
|
||||||
created_at = datetime.now(timezone.utc).isoformat()
|
created_at = datetime.now(timezone.utc).isoformat()
|
||||||
|
|||||||
Binary file not shown.
+23
-11
@@ -12,6 +12,7 @@ if [ -z "${BASE_URL:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
HAS_WRITABLE_ROOT=1
|
||||||
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
||||||
for candidate in \
|
for candidate in \
|
||||||
"/Volumes/8TB/Shared_Folders/TV_Shows" \
|
"/Volumes/8TB/Shared_Folders/TV_Shows" \
|
||||||
@@ -25,8 +26,8 @@ if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
if [ -z "${TEST_MEDIA_ROOT:-}" ]; then
|
||||||
echo "ERROR: no writable allowed media root found. Set TEST_MEDIA_ROOT." >&2
|
HAS_WRITABLE_ROOT=0
|
||||||
exit 1
|
TEST_MEDIA_ROOT="/tmp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
@@ -99,7 +100,11 @@ print("settings PUT/GET round-trip passed")
|
|||||||
PY
|
PY
|
||||||
|
|
||||||
echo
|
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}"
|
clear_session "${SESSION_ID}"
|
||||||
|
|
||||||
SRC="${TEST_DIR}/source_settings_test.mkv"
|
SRC="${TEST_DIR}/source_settings_test.mkv"
|
||||||
@@ -156,20 +161,26 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
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 []
|
items = data.get("items") or []
|
||||||
assert len(items) == 1, "expected 1 item"
|
assert len(items) == 1, "expected 1 item"
|
||||||
item = items[0]
|
item = items[0]
|
||||||
assert item.get("status") == "renamed", "item status must be renamed"
|
if data.get("executed") is True:
|
||||||
assert item.get("file_date_status") == "file_date_updated", "file_date_status should be updated"
|
assert data.get("preflight_ok") is True, "preflight should pass"
|
||||||
print("rename response settings validation passed")
|
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
|
PY
|
||||||
|
|
||||||
DST="${TEST_DIR}/Elsbeth (2024) - S01E03 - Settings Date Test.mkv"
|
if [ "${HAS_WRITABLE_ROOT}" = "1" ]; then
|
||||||
test -f "${DST}"
|
DST="${TEST_DIR}/Elsbeth (2024) - S01E03 - Settings Date Test.mkv"
|
||||||
|
test -f "${DST}"
|
||||||
|
|
||||||
python3 - "${DST}" <<'PY'
|
python3 - "${DST}" <<'PY'
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -182,6 +193,7 @@ delta = abs(st.st_mtime - expected)
|
|||||||
assert delta < 2.5, f"mtime delta too large: {delta}"
|
assert delta < 2.5, f"mtime delta too large: {delta}"
|
||||||
print("mtime validation passed")
|
print("mtime validation passed")
|
||||||
PY
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
# Reset setting back to false to keep environment stable for subsequent runs.
|
# Reset setting back to false to keep environment stable for subsequent runs.
|
||||||
cat > "${TMP_DIR}/settings_put_false.json" <<'JSON'
|
cat > "${TMP_DIR}/settings_put_false.json" <<'JSON'
|
||||||
|
|||||||
Reference in New Issue
Block a user