feat (ui): keuze default roof folder

This commit is contained in:
kodi
2026-03-09 13:41:04 +01:00
parent ab7a84ebe0
commit a22172f736
7 changed files with 146 additions and 12 deletions
+3 -2
View File
@@ -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}
+30 -7
View File
@@ -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()
+50 -1
View File
@@ -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();
}
+17
View File
@@ -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;
+9
View File
@@ -145,6 +145,15 @@
<h3>Settings</h3>
<button id="closeSettingsModalBtn" class="secondary">Close</button>
</div>
<section class="settings-section">
<h4>Generic Settings</h4>
<div class="settings-field">
<label for="defaultMediaRootSelect">Default Media Root</label>
<select id="defaultMediaRootSelect">
<option value="">Auto (first available)</option>
</select>
</div>
</section>
<section class="settings-section">
<h4>Renaming Rules</h4>
<label class="settings-check">
Binary file not shown.
+37 -2
View File
@@ -63,17 +63,49 @@ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
settings = data.get("settings")
assert isinstance(settings, dict), "settings must be object"
assert "set_file_date_to_first_aired_date" in settings, "missing setting key"
assert "default_media_root_path" in settings, "missing default root setting key"
assert isinstance(settings["set_file_date_to_first_aired_date"], bool), "setting must be boolean"
assert settings["default_media_root_path"] is None or isinstance(settings["default_media_root_path"], str), \
"default_media_root_path must be string or null"
print("settings GET validation passed")
PY
echo
echo "== Feature test 2: settings update persists round-trip =="
DEFAULT_ROOT_PATH=""
ROOTS_JSON="${TMP_DIR}/roots.json"
if curl --silent --show-error "${BASE_URL}/api/files/roots" -o "${ROOTS_JSON}"; then
DEFAULT_ROOT_PATH="$(python3 - "${ROOTS_JSON}" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
items = data.get("items") or []
if items and isinstance(items[0], dict):
print(items[0].get("path") or "")
else:
print("")
PY
)"
fi
cat > "${TMP_DIR}/settings_put_true.json" <<'JSON'
{
"set_file_date_to_first_aired_date": true
"set_file_date_to_first_aired_date": true,
"default_media_root_path": "__DEFAULT_ROOT_PATH__"
}
JSON
python3 - "${TMP_DIR}/settings_put_true.json" "${DEFAULT_ROOT_PATH}" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
default_path = sys.argv[2]
text = path.read_text(encoding="utf-8")
safe = default_path.replace("\\", "\\\\").replace('"', '\\"')
path.write_text(text.replace("__DEFAULT_ROOT_PATH__", safe), encoding="utf-8")
PY
curl --fail --silent --show-error \
-X PUT "${BASE_URL}/api/session/settings" \
@@ -96,6 +128,8 @@ put_data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
get_data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
assert put_data["settings"]["set_file_date_to_first_aired_date"] is True, "PUT should return true"
assert get_data["settings"]["set_file_date_to_first_aired_date"] is True, "GET after PUT should be true"
assert "default_media_root_path" in put_data["settings"], "PUT missing default root setting"
assert "default_media_root_path" in get_data["settings"], "GET missing default root setting"
print("settings PUT/GET round-trip passed")
PY
@@ -198,7 +232,8 @@ fi
# Reset setting back to false to keep environment stable for subsequent runs.
cat > "${TMP_DIR}/settings_put_false.json" <<'JSON'
{
"set_file_date_to_first_aired_date": false
"set_file_date_to_first_aired_date": false,
"default_media_root_path": null
}
JSON
curl --fail --silent --show-error \