From a22172f736bd38292b79ebdcbe5c238102e6e449 Mon Sep 17 00:00:00 2001 From: kodi Date: Mon, 9 Mar 2026 13:41:04 +0100 Subject: [PATCH] feat (ui): keuze default roof folder --- app/api/session.py | 5 ++-- app/services/session_service.py | 37 ++++++++++++++++++----- app/static/app.js | 51 +++++++++++++++++++++++++++++++- app/static/styles.css | 17 +++++++++++ app/templates/index.html | 9 ++++++ data/session_state.sqlite3 | Bin 102400 -> 102400 bytes feature_tests_settings.sh | 39 ++++++++++++++++++++++-- 7 files changed, 146 insertions(+), 12 deletions(-) diff --git a/app/api/session.py b/app/api/session.py index 1e6d796..c5031b1 100644 --- a/app/api/session.py +++ b/app/api/session.py @@ -25,7 +25,8 @@ class SelectedFilesReorderRequest(BaseModel): class SessionSettingsRequest(BaseModel): - set_file_date_to_first_aired_date: bool + set_file_date_to_first_aired_date: bool | None = None + default_media_root_path: str | None = None def _normalize_session_id(session_id: str) -> str: @@ -153,7 +154,7 @@ def get_session_settings(): def put_session_settings(payload: SessionSettingsRequest): service = SessionService() try: - settings = service.update_settings(payload.model_dump()) + settings = service.update_settings(payload.model_dump(exclude_unset=True)) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) return {"settings": settings} diff --git a/app/services/session_service.py b/app/services/session_service.py index c4f9b16..e2f46de 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -11,6 +11,7 @@ from app.config import APP_DATA_DIR class SessionService: FILE_DATE_SETTING_KEY = "set_file_date_to_first_aired_date" + DEFAULT_ROOT_SETTING_KEY = "default_media_root_path" MAX_FILENAME_LEN = 220 def __init__(self) -> None: @@ -135,17 +136,29 @@ class SessionService: values = {str(row["key"]): str(row["value"]) for row in rows} return { self.FILE_DATE_SETTING_KEY: values.get(self.FILE_DATE_SETTING_KEY, "0") == "1", + self.DEFAULT_ROOT_SETTING_KEY: values.get(self.DEFAULT_ROOT_SETTING_KEY) or None, } def update_settings(self, settings: dict) -> dict: - if self.FILE_DATE_SETTING_KEY not in settings: - raise ValueError(f"missing required setting: {self.FILE_DATE_SETTING_KEY}") - setting_value = settings[self.FILE_DATE_SETTING_KEY] - if not isinstance(setting_value, bool): + updated_at = datetime.now(timezone.utc).isoformat() + current = self.get_settings() + + allowed_keys = {self.FILE_DATE_SETTING_KEY, self.DEFAULT_ROOT_SETTING_KEY} + unknown_keys = [key for key in settings.keys() if key not in allowed_keys] + if unknown_keys: + raise ValueError(f"unknown setting key: {unknown_keys[0]}") + + merged = dict(current) + merged.update(settings) + + file_date_value = merged.get(self.FILE_DATE_SETTING_KEY) + if not isinstance(file_date_value, bool): raise ValueError(f"{self.FILE_DATE_SETTING_KEY} must be boolean") - stored_value = "1" if setting_value else "0" - updated_at = datetime.now(timezone.utc).isoformat() + default_root_path = merged.get(self.DEFAULT_ROOT_SETTING_KEY) + if default_root_path is not None and not isinstance(default_root_path, str): + raise ValueError(f"{self.DEFAULT_ROOT_SETTING_KEY} must be string or null") + default_root_path = (default_root_path or "").strip() or None with self._connect() as conn: conn.execute( @@ -156,7 +169,17 @@ class SessionService: value = excluded.value, updated_at = excluded.updated_at """, - (self.FILE_DATE_SETTING_KEY, stored_value, updated_at), + (self.FILE_DATE_SETTING_KEY, "1" if file_date_value else "0", updated_at), + ) + conn.execute( + """ + INSERT INTO app_settings (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + """, + (self.DEFAULT_ROOT_SETTING_KEY, default_root_path or "", updated_at), ) return self.get_settings() diff --git a/app/static/app.js b/app/static/app.js index 933eac1..e9fb639 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -16,6 +16,7 @@ syncScrolling: false, settings: { set_file_date_to_first_aired_date: false, + default_media_root_path: null, }, }; @@ -38,6 +39,7 @@ closeSettingsModalBtn: document.getElementById("closeSettingsModalBtn"), saveSettingsBtn: document.getElementById("saveSettingsBtn"), setFileDateToFirstAiredDateInput: document.getElementById("setFileDateToFirstAiredDateInput"), + defaultMediaRootSelect: document.getElementById("defaultMediaRootSelect"), refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"), episodesList: document.getElementById("episodesList"), episodeMeta: document.getElementById("episodeMeta"), @@ -209,6 +211,36 @@ function applySettingsToForm() { el.setFileDateToFirstAiredDateInput.checked = !!state.settings.set_file_date_to_first_aired_date; + if (el.defaultMediaRootSelect) { + const wanted = state.settings.default_media_root_path || ""; + el.defaultMediaRootSelect.value = wanted; + if (el.defaultMediaRootSelect.value !== wanted) { + el.defaultMediaRootSelect.value = ""; + } + } + } + + function populateDefaultRootOptions() { + if (!el.defaultMediaRootSelect) return; + const selectedValue = state.settings.default_media_root_path || ""; + + el.defaultMediaRootSelect.innerHTML = ""; + const auto = document.createElement("option"); + auto.value = ""; + auto.textContent = "Auto (first available)"; + el.defaultMediaRootSelect.appendChild(auto); + + state.roots.forEach((root) => { + const opt = document.createElement("option"); + opt.value = root.path || ""; + opt.textContent = `${root.id}: ${root.path}`; + el.defaultMediaRootSelect.appendChild(opt); + }); + + el.defaultMediaRootSelect.value = selectedValue; + if (el.defaultMediaRootSelect.value !== selectedValue) { + el.defaultMediaRootSelect.value = ""; + } } function openSettingsModal() { @@ -224,13 +256,17 @@ async function loadSettings() { const data = await api("/api/session/settings"); - state.settings = data.settings || { set_file_date_to_first_aired_date: false }; + state.settings = data.settings || { + set_file_date_to_first_aired_date: false, + default_media_root_path: null, + }; applySettingsToForm(); } async function saveSettings() { const payload = { set_file_date_to_first_aired_date: !!el.setFileDateToFirstAiredDateInput.checked, + default_media_root_path: (el.defaultMediaRootSelect.value || "").trim() || null, }; const data = await api("/api/session/settings", { method: "PUT", @@ -239,10 +275,21 @@ }); state.settings = data.settings || payload; applySettingsToForm(); + await loadRoots(); closeSettingsModal(); out("Settings saved", data); } + function preferredRootId() { + if (!state.roots.length) return ""; + const wantedPath = (state.settings.default_media_root_path || "").trim(); + if (wantedPath) { + const match = state.roots.find((root) => (root.path || "") === wantedPath); + if (match) return match.id; + } + return state.roots[0].id; + } + function selectPair(index) { state.selectedPairIndex = index; renderSelectedEpisodes(); @@ -311,7 +358,9 @@ opt.textContent = `${root.id}: ${root.path}`; el.modalRootSelect.appendChild(opt); }); + populateDefaultRootOptions(); if (state.roots.length) { + el.modalRootSelect.value = preferredRootId(); el.modalSubpathInput.value = ""; await loadModalFolders(); } diff --git a/app/static/styles.css b/app/static/styles.css index 2368c47..f9334c9 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -285,6 +285,23 @@ button.secondary { color: #1e293b; } +.settings-field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 4px; +} + +.settings-field label { + font-size: 12px; + color: #64748b; +} + +.settings-field select { + min-width: 0; + width: 100%; +} + .settings-check { display: flex; align-items: center; diff --git a/app/templates/index.html b/app/templates/index.html index a461a54..d97f783 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -145,6 +145,15 @@

Settings

+
+

Generic Settings

+
+ + +
+

Renaming Rules