feat (ui): keuze default roof folder
This commit is contained in:
+3
-2
@@ -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}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user