feat (ui): keuze default roof folder
This commit is contained in:
+3
-2
@@ -25,7 +25,8 @@ class SelectedFilesReorderRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SessionSettingsRequest(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:
|
def _normalize_session_id(session_id: str) -> str:
|
||||||
@@ -153,7 +154,7 @@ def get_session_settings():
|
|||||||
def put_session_settings(payload: SessionSettingsRequest):
|
def put_session_settings(payload: SessionSettingsRequest):
|
||||||
service = SessionService()
|
service = SessionService()
|
||||||
try:
|
try:
|
||||||
settings = service.update_settings(payload.model_dump())
|
settings = service.update_settings(payload.model_dump(exclude_unset=True))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
return {"settings": settings}
|
return {"settings": settings}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
DEFAULT_ROOT_SETTING_KEY = "default_media_root_path"
|
||||||
MAX_FILENAME_LEN = 220
|
MAX_FILENAME_LEN = 220
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -135,17 +136,29 @@ class SessionService:
|
|||||||
values = {str(row["key"]): str(row["value"]) for row in rows}
|
values = {str(row["key"]): str(row["value"]) for row in rows}
|
||||||
return {
|
return {
|
||||||
self.FILE_DATE_SETTING_KEY: values.get(self.FILE_DATE_SETTING_KEY, "0") == "1",
|
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:
|
def update_settings(self, settings: dict) -> dict:
|
||||||
if self.FILE_DATE_SETTING_KEY not in settings:
|
updated_at = datetime.now(timezone.utc).isoformat()
|
||||||
raise ValueError(f"missing required setting: {self.FILE_DATE_SETTING_KEY}")
|
current = self.get_settings()
|
||||||
setting_value = settings[self.FILE_DATE_SETTING_KEY]
|
|
||||||
if not isinstance(setting_value, bool):
|
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")
|
raise ValueError(f"{self.FILE_DATE_SETTING_KEY} must be boolean")
|
||||||
|
|
||||||
stored_value = "1" if setting_value else "0"
|
default_root_path = merged.get(self.DEFAULT_ROOT_SETTING_KEY)
|
||||||
updated_at = datetime.now(timezone.utc).isoformat()
|
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:
|
with self._connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -156,7 +169,17 @@ class SessionService:
|
|||||||
value = excluded.value,
|
value = excluded.value,
|
||||||
updated_at = excluded.updated_at
|
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()
|
return self.get_settings()
|
||||||
|
|||||||
+50
-1
@@ -16,6 +16,7 @@
|
|||||||
syncScrolling: false,
|
syncScrolling: false,
|
||||||
settings: {
|
settings: {
|
||||||
set_file_date_to_first_aired_date: false,
|
set_file_date_to_first_aired_date: false,
|
||||||
|
default_media_root_path: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
closeSettingsModalBtn: document.getElementById("closeSettingsModalBtn"),
|
closeSettingsModalBtn: document.getElementById("closeSettingsModalBtn"),
|
||||||
saveSettingsBtn: document.getElementById("saveSettingsBtn"),
|
saveSettingsBtn: document.getElementById("saveSettingsBtn"),
|
||||||
setFileDateToFirstAiredDateInput: document.getElementById("setFileDateToFirstAiredDateInput"),
|
setFileDateToFirstAiredDateInput: document.getElementById("setFileDateToFirstAiredDateInput"),
|
||||||
|
defaultMediaRootSelect: document.getElementById("defaultMediaRootSelect"),
|
||||||
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
||||||
episodesList: document.getElementById("episodesList"),
|
episodesList: document.getElementById("episodesList"),
|
||||||
episodeMeta: document.getElementById("episodeMeta"),
|
episodeMeta: document.getElementById("episodeMeta"),
|
||||||
@@ -209,6 +211,36 @@
|
|||||||
|
|
||||||
function applySettingsToForm() {
|
function applySettingsToForm() {
|
||||||
el.setFileDateToFirstAiredDateInput.checked = !!state.settings.set_file_date_to_first_aired_date;
|
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() {
|
function openSettingsModal() {
|
||||||
@@ -224,13 +256,17 @@
|
|||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const data = await api("/api/session/settings");
|
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();
|
applySettingsToForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
const payload = {
|
const payload = {
|
||||||
set_file_date_to_first_aired_date: !!el.setFileDateToFirstAiredDateInput.checked,
|
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", {
|
const data = await api("/api/session/settings", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -239,10 +275,21 @@
|
|||||||
});
|
});
|
||||||
state.settings = data.settings || payload;
|
state.settings = data.settings || payload;
|
||||||
applySettingsToForm();
|
applySettingsToForm();
|
||||||
|
await loadRoots();
|
||||||
closeSettingsModal();
|
closeSettingsModal();
|
||||||
out("Settings saved", data);
|
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) {
|
function selectPair(index) {
|
||||||
state.selectedPairIndex = index;
|
state.selectedPairIndex = index;
|
||||||
renderSelectedEpisodes();
|
renderSelectedEpisodes();
|
||||||
@@ -311,7 +358,9 @@
|
|||||||
opt.textContent = `${root.id}: ${root.path}`;
|
opt.textContent = `${root.id}: ${root.path}`;
|
||||||
el.modalRootSelect.appendChild(opt);
|
el.modalRootSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
populateDefaultRootOptions();
|
||||||
if (state.roots.length) {
|
if (state.roots.length) {
|
||||||
|
el.modalRootSelect.value = preferredRootId();
|
||||||
el.modalSubpathInput.value = "";
|
el.modalSubpathInput.value = "";
|
||||||
await loadModalFolders();
|
await loadModalFolders();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,23 @@ button.secondary {
|
|||||||
color: #1e293b;
|
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 {
|
.settings-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -145,6 +145,15 @@
|
|||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
<button id="closeSettingsModalBtn" class="secondary">Close</button>
|
<button id="closeSettingsModalBtn" class="secondary">Close</button>
|
||||||
</div>
|
</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">
|
<section class="settings-section">
|
||||||
<h4>Renaming Rules</h4>
|
<h4>Renaming Rules</h4>
|
||||||
<label class="settings-check">
|
<label class="settings-check">
|
||||||
|
|||||||
Binary file not shown.
@@ -63,17 +63,49 @@ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|||||||
settings = data.get("settings")
|
settings = data.get("settings")
|
||||||
assert isinstance(settings, dict), "settings must be object"
|
assert isinstance(settings, dict), "settings must be object"
|
||||||
assert "set_file_date_to_first_aired_date" in settings, "missing setting key"
|
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 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")
|
print("settings GET validation passed")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== Feature test 2: settings update persists round-trip =="
|
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'
|
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
|
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 \
|
curl --fail --silent --show-error \
|
||||||
-X PUT "${BASE_URL}/api/session/settings" \
|
-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"))
|
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 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 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")
|
print("settings PUT/GET round-trip passed")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
@@ -198,7 +232,8 @@ 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'
|
||||||
{
|
{
|
||||||
"set_file_date_to_first_aired_date": false
|
"set_file_date_to_first_aired_date": false,
|
||||||
|
"default_media_root_path": null
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
curl --fail --silent --show-error \
|
curl --fail --silent --show-error \
|
||||||
|
|||||||
Reference in New Issue
Block a user