feat (ui): series onthouden
This commit is contained in:
@@ -27,6 +27,11 @@ class SelectedFilesReorderRequest(BaseModel):
|
||||
class SessionSettingsRequest(BaseModel):
|
||||
set_file_date_to_first_aired_date: bool | None = None
|
||||
default_media_root_path: str | None = None
|
||||
remember_max_series: int | None = Field(default=None, ge=1, le=100)
|
||||
|
||||
|
||||
class RememberedSeriesUpsertRequest(BaseModel):
|
||||
item: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
def _normalize_session_id(session_id: str) -> str:
|
||||
@@ -160,6 +165,30 @@ def put_session_settings(payload: SessionSettingsRequest):
|
||||
return {"settings": settings}
|
||||
|
||||
|
||||
@router.get("/remembered-series")
|
||||
def get_remembered_series():
|
||||
service = SessionService()
|
||||
items = service.list_remembered_series()
|
||||
return {"items": items}
|
||||
|
||||
|
||||
@router.post("/remembered-series")
|
||||
def post_remembered_series(payload: RememberedSeriesUpsertRequest):
|
||||
service = SessionService()
|
||||
try:
|
||||
items = service.remember_series(payload.item)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return {"items": items}
|
||||
|
||||
|
||||
@router.delete("/remembered-series")
|
||||
def delete_remembered_series():
|
||||
service = SessionService()
|
||||
service.clear_remembered_series()
|
||||
return {"items": []}
|
||||
|
||||
|
||||
@router.get("/mapping-preview")
|
||||
def get_mapping_preview(session_id: str = Query("default", min_length=1)):
|
||||
service = SessionService()
|
||||
|
||||
@@ -12,7 +12,9 @@ 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"
|
||||
REMEMBER_MAX_SERIES_KEY = "remember_max_series"
|
||||
MAX_FILENAME_LEN = 220
|
||||
DEFAULT_REMEMBER_MAX_SERIES = 10
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._db_path = Path(APP_DATA_DIR) / "session_state.sqlite3"
|
||||
@@ -123,6 +125,21 @@ class SessionService:
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS remembered_series (
|
||||
series_id TEXT PRIMARY KEY,
|
||||
payload_json TEXT NOT NULL,
|
||||
last_selected_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_remembered_series_last_selected
|
||||
ON remembered_series(last_selected_at DESC)
|
||||
"""
|
||||
)
|
||||
|
||||
def get_settings(self) -> dict:
|
||||
with self._connect() as conn:
|
||||
@@ -134,16 +151,27 @@ class SessionService:
|
||||
).fetchall()
|
||||
|
||||
values = {str(row["key"]): str(row["value"]) for row in rows}
|
||||
remember_max_raw = values.get(self.REMEMBER_MAX_SERIES_KEY, str(self.DEFAULT_REMEMBER_MAX_SERIES))
|
||||
try:
|
||||
remember_max = int(remember_max_raw)
|
||||
except ValueError:
|
||||
remember_max = self.DEFAULT_REMEMBER_MAX_SERIES
|
||||
remember_max = max(1, min(100, remember_max))
|
||||
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,
|
||||
self.REMEMBER_MAX_SERIES_KEY: remember_max,
|
||||
}
|
||||
|
||||
def update_settings(self, settings: dict) -> dict:
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
current = self.get_settings()
|
||||
|
||||
allowed_keys = {self.FILE_DATE_SETTING_KEY, self.DEFAULT_ROOT_SETTING_KEY}
|
||||
allowed_keys = {
|
||||
self.FILE_DATE_SETTING_KEY,
|
||||
self.DEFAULT_ROOT_SETTING_KEY,
|
||||
self.REMEMBER_MAX_SERIES_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]}")
|
||||
@@ -160,6 +188,12 @@ class SessionService:
|
||||
raise ValueError(f"{self.DEFAULT_ROOT_SETTING_KEY} must be string or null")
|
||||
default_root_path = (default_root_path or "").strip() or None
|
||||
|
||||
remember_max_value = merged.get(self.REMEMBER_MAX_SERIES_KEY, self.DEFAULT_REMEMBER_MAX_SERIES)
|
||||
if not isinstance(remember_max_value, int):
|
||||
raise ValueError(f"{self.REMEMBER_MAX_SERIES_KEY} must be integer")
|
||||
if remember_max_value < 1 or remember_max_value > 100:
|
||||
raise ValueError(f"{self.REMEMBER_MAX_SERIES_KEY} must be between 1 and 100")
|
||||
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -181,9 +215,92 @@ class SessionService:
|
||||
""",
|
||||
(self.DEFAULT_ROOT_SETTING_KEY, default_root_path or "", 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.REMEMBER_MAX_SERIES_KEY, str(remember_max_value), updated_at),
|
||||
)
|
||||
|
||||
self._enforce_remembered_series_limit(remember_max_value)
|
||||
|
||||
return self.get_settings()
|
||||
|
||||
def list_remembered_series(self) -> list[dict]:
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT series_id, payload_json, last_selected_at
|
||||
FROM remembered_series
|
||||
ORDER BY last_selected_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
payload = json.loads(row["payload_json"])
|
||||
items.append(
|
||||
{
|
||||
"series_id": row["series_id"],
|
||||
"last_selected_at": row["last_selected_at"],
|
||||
"series": payload,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
def remember_series(self, item: dict) -> list[dict]:
|
||||
series_id = str(item.get("id") or "").strip()
|
||||
if not series_id:
|
||||
raise ValueError("series id is required")
|
||||
|
||||
# Preserve only expected display fields plus raw payload.
|
||||
payload = {
|
||||
"id": item.get("id"),
|
||||
"name": item.get("name"),
|
||||
"year": item.get("year"),
|
||||
"display_name": item.get("display_name"),
|
||||
"raw": item.get("raw", {}),
|
||||
}
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO remembered_series (series_id, payload_json, last_selected_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(series_id) DO UPDATE SET
|
||||
payload_json = excluded.payload_json,
|
||||
last_selected_at = excluded.last_selected_at
|
||||
""",
|
||||
(series_id, json.dumps(payload, ensure_ascii=True), now),
|
||||
)
|
||||
|
||||
remember_max = int(self.get_settings().get(self.REMEMBER_MAX_SERIES_KEY, self.DEFAULT_REMEMBER_MAX_SERIES))
|
||||
self._enforce_remembered_series_limit(remember_max)
|
||||
return self.list_remembered_series()
|
||||
|
||||
def clear_remembered_series(self) -> None:
|
||||
with self._connect() as conn:
|
||||
conn.execute("DELETE FROM remembered_series")
|
||||
|
||||
def _enforce_remembered_series_limit(self, limit: int) -> None:
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM remembered_series
|
||||
WHERE series_id NOT IN (
|
||||
SELECT series_id
|
||||
FROM remembered_series
|
||||
ORDER BY last_selected_at DESC
|
||||
LIMIT ?
|
||||
)
|
||||
""",
|
||||
(int(limit),),
|
||||
)
|
||||
|
||||
def list_selected_episodes(self, session_id: str) -> list[dict]:
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
|
||||
+112
-24
@@ -20,13 +20,17 @@
|
||||
settings: {
|
||||
set_file_date_to_first_aired_date: false,
|
||||
default_media_root_path: null,
|
||||
remember_max_series: 10,
|
||||
},
|
||||
rememberedSeries: [],
|
||||
liveSearchItems: [],
|
||||
};
|
||||
|
||||
const el = {
|
||||
sessionMeta: document.getElementById("sessionMeta"),
|
||||
outputBox: document.getElementById("outputBox"),
|
||||
searchInput: document.getElementById("searchInput"),
|
||||
searchDropdownBtn: document.getElementById("searchDropdownBtn"),
|
||||
searchBtn: document.getElementById("searchBtn"),
|
||||
searchResults: document.getElementById("searchResults"),
|
||||
seriesInfo: document.getElementById("seriesInfo"),
|
||||
@@ -43,6 +47,8 @@
|
||||
saveSettingsBtn: document.getElementById("saveSettingsBtn"),
|
||||
setFileDateToFirstAiredDateInput: document.getElementById("setFileDateToFirstAiredDateInput"),
|
||||
defaultMediaRootSelect: document.getElementById("defaultMediaRootSelect"),
|
||||
rememberMaxSeriesInput: document.getElementById("rememberMaxSeriesInput"),
|
||||
purgeRememberedSeriesBtn: document.getElementById("purgeRememberedSeriesBtn"),
|
||||
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
||||
episodesList: document.getElementById("episodesList"),
|
||||
episodeMeta: document.getElementById("episodeMeta"),
|
||||
@@ -214,6 +220,9 @@
|
||||
|
||||
function applySettingsToForm() {
|
||||
el.setFileDateToFirstAiredDateInput.checked = !!state.settings.set_file_date_to_first_aired_date;
|
||||
if (el.rememberMaxSeriesInput) {
|
||||
el.rememberMaxSeriesInput.value = String(state.settings.remember_max_series || 10);
|
||||
}
|
||||
if (el.defaultMediaRootSelect) {
|
||||
const wanted = state.settings.default_media_root_path || "";
|
||||
el.defaultMediaRootSelect.value = wanted;
|
||||
@@ -262,14 +271,20 @@
|
||||
state.settings = data.settings || {
|
||||
set_file_date_to_first_aired_date: false,
|
||||
default_media_root_path: null,
|
||||
remember_max_series: 10,
|
||||
};
|
||||
applySettingsToForm();
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const rememberMax = Number(el.rememberMaxSeriesInput.value || "10");
|
||||
if (!Number.isInteger(rememberMax) || rememberMax < 1 || rememberMax > 100) {
|
||||
throw new Error("Remember max series must be an integer between 1 and 100");
|
||||
}
|
||||
const payload = {
|
||||
set_file_date_to_first_aired_date: !!el.setFileDateToFirstAiredDateInput.checked,
|
||||
default_media_root_path: (el.defaultMediaRootSelect.value || "").trim() || null,
|
||||
remember_max_series: rememberMax,
|
||||
};
|
||||
const data = await api("/api/session/settings", {
|
||||
method: "PUT",
|
||||
@@ -283,6 +298,89 @@
|
||||
out("Settings saved", data);
|
||||
}
|
||||
|
||||
async function loadRememberedSeries() {
|
||||
const data = await api("/api/session/remembered-series");
|
||||
state.rememberedSeries = data.items || [];
|
||||
}
|
||||
|
||||
async function rememberSeries(item) {
|
||||
await api("/api/session/remembered-series", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ item }),
|
||||
});
|
||||
await loadRememberedSeries();
|
||||
}
|
||||
|
||||
async function purgeRememberedSeries() {
|
||||
const ok = window.confirm("Are you sure?");
|
||||
if (!ok) return;
|
||||
await api("/api/session/remembered-series", { method: "DELETE" });
|
||||
await loadRememberedSeries();
|
||||
renderRememberedDropdown();
|
||||
}
|
||||
|
||||
function rememberedForQuery(query) {
|
||||
const normalized = (query || "").trim().toLowerCase();
|
||||
const rememberedItems = (state.rememberedSeries || []).map((entry) => entry.series || {});
|
||||
return rememberedItems.filter((item) => {
|
||||
if (!normalized) return true;
|
||||
const text = `${item.display_name || ""} ${item.name || ""}`.toLowerCase();
|
||||
return text.includes(normalized);
|
||||
});
|
||||
}
|
||||
|
||||
function renderRememberedDropdown() {
|
||||
const query = (el.searchInput.value || "").trim();
|
||||
const items = rememberedForQuery(query);
|
||||
el.searchResults.innerHTML = "";
|
||||
|
||||
items.forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
const left = document.createElement("span");
|
||||
const label = item.display_name || item.name || "(series)";
|
||||
left.textContent = label;
|
||||
|
||||
const right = document.createElement("div");
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "badge";
|
||||
tag.textContent = "Remembered";
|
||||
right.appendChild(tag);
|
||||
li.appendChild(left);
|
||||
li.appendChild(right);
|
||||
li.addEventListener("click", () => withHandler(() => selectSeries(item), li));
|
||||
el.searchResults.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchResults(items) {
|
||||
el.searchResults.innerHTML = "";
|
||||
(items || []).forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
const left = document.createElement("span");
|
||||
left.textContent = item.display_name || item.name || "(series)";
|
||||
li.appendChild(left);
|
||||
li.addEventListener("click", () => withHandler(() => selectSeries(item), li));
|
||||
el.searchResults.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function selectSeries(item) {
|
||||
state.selectedSeries = item;
|
||||
state.selectedSeriesSummary = null;
|
||||
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
||||
renderSelectedSeriesDetails();
|
||||
await rememberSeries(item);
|
||||
renderRememberedDropdown();
|
||||
try {
|
||||
await loadSeriesSummary(item.id);
|
||||
renderSelectedSeriesDetails();
|
||||
} catch (_err) {
|
||||
// Keep UI responsive with fallback data.
|
||||
}
|
||||
await loadEpisodes();
|
||||
}
|
||||
|
||||
function preferredRootId() {
|
||||
if (!state.roots.length) return "";
|
||||
const wantedPath = (state.settings.default_media_root_path || "").trim();
|
||||
@@ -370,31 +468,14 @@
|
||||
|
||||
async function doSearch() {
|
||||
const query = (el.searchInput.value || "").trim();
|
||||
if (!query) return;
|
||||
const data = await api(`/api/tvdb/search?q=${encodeURIComponent(query)}`);
|
||||
el.searchResults.innerHTML = "";
|
||||
(data.items || []).forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
const left = document.createElement("span");
|
||||
left.textContent = item.display_name || item.name || "(series)";
|
||||
const right = document.createElement("div");
|
||||
right.appendChild(makeBtn("Select", async () => {
|
||||
state.selectedSeries = item;
|
||||
state.selectedSeriesSummary = null;
|
||||
el.seriesInfo.textContent = `Selected: ${item.display_name || item.name}`;
|
||||
renderSelectedSeriesDetails();
|
||||
try {
|
||||
await loadSeriesSummary(item.id);
|
||||
renderSelectedSeriesDetails();
|
||||
} catch (_err) {
|
||||
// Keep UI responsive with search payload fallback if summary lookup fails.
|
||||
if (!query || query.length < 2) {
|
||||
state.liveSearchItems = [];
|
||||
renderRememberedDropdown();
|
||||
return;
|
||||
}
|
||||
await loadEpisodes();
|
||||
}));
|
||||
li.appendChild(left);
|
||||
li.appendChild(right);
|
||||
el.searchResults.appendChild(li);
|
||||
});
|
||||
const data = await api(`/api/tvdb/search?q=${encodeURIComponent(query)}`);
|
||||
state.liveSearchItems = data.items || [];
|
||||
renderSearchResults(state.liveSearchItems);
|
||||
out("Search result", data);
|
||||
}
|
||||
|
||||
@@ -704,9 +785,14 @@
|
||||
|
||||
function bindEvents() {
|
||||
el.searchBtn.addEventListener("click", () => withHandler(doSearch, el.searchBtn));
|
||||
el.searchInput.addEventListener("focus", renderRememberedDropdown);
|
||||
el.searchInput.addEventListener("click", renderRememberedDropdown);
|
||||
el.searchInput.addEventListener("input", renderRememberedDropdown);
|
||||
el.searchDropdownBtn.addEventListener("click", renderRememberedDropdown);
|
||||
el.settingsBtn.addEventListener("click", openSettingsModal);
|
||||
el.closeSettingsModalBtn.addEventListener("click", closeSettingsModal);
|
||||
el.saveSettingsBtn.addEventListener("click", () => withHandler(saveSettings, el.saveSettingsBtn));
|
||||
el.purgeRememberedSeriesBtn.addEventListener("click", () => withHandler(purgeRememberedSeries, el.purgeRememberedSeriesBtn));
|
||||
el.settingsModal.addEventListener("click", (e) => {
|
||||
if (e.target === el.settingsModal) closeSettingsModal();
|
||||
});
|
||||
@@ -765,6 +851,8 @@
|
||||
renderSelectedSeriesDetails();
|
||||
bindEvents();
|
||||
await loadSettings();
|
||||
await loadRememberedSeries();
|
||||
renderRememberedDropdown();
|
||||
await loadSelectedEpisodes();
|
||||
await loadSelectedFiles();
|
||||
await loadRoots();
|
||||
|
||||
@@ -109,6 +109,17 @@ body {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-combobox-row input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#searchDropdownBtn {
|
||||
min-width: 34px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -345,6 +356,15 @@ button.secondary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-field input[type="number"] {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-danger-row {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.settings-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
<div class="panel-head-actions panel-head-actions-empty" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="row search-combobox-row">
|
||||
<input id="searchInput" type="text" placeholder="Search series..." />
|
||||
<button id="searchDropdownBtn" class="secondary" aria-label="Show remembered series">▼</button>
|
||||
<button id="searchBtn">Search</button>
|
||||
</div>
|
||||
<div id="seriesInfo" class="muted"></div>
|
||||
@@ -152,6 +153,13 @@
|
||||
<option value="">Auto (first available)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="rememberMaxSeriesInput">Remember max series</label>
|
||||
<input id="rememberMaxSeriesInput" type="number" min="1" max="100" step="1" />
|
||||
</div>
|
||||
<div class="settings-field settings-danger-row">
|
||||
<button id="purgeRememberedSeriesBtn" class="secondary">Purge remembered series list</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h4>Renaming Rules</h4>
|
||||
|
||||
Binary file not shown.
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${BASE_URL:-}" ]; then
|
||||
if curl --silent --fail http://127.0.0.1:8085/api/health >/dev/null 2>&1; then
|
||||
BASE_URL="http://127.0.0.1:8085"
|
||||
elif curl --silent --fail http://host.containers.internal:8085/api/health >/dev/null 2>&1; then
|
||||
BASE_URL="http://host.containers.internal:8085"
|
||||
else
|
||||
echo "ERROR: could not determine BASE_URL. Tried 127.0.0.1 and host.containers.internal." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
echo "== Feature test 1: remembered series list can be purged and starts empty =="
|
||||
curl --fail --silent --show-error \
|
||||
-X DELETE "${BASE_URL}/api/session/remembered-series" \
|
||||
-o "${TMP_DIR}/purge.json"
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
"${BASE_URL}/api/session/remembered-series" \
|
||||
-o "${TMP_DIR}/remembered_empty.json"
|
||||
|
||||
cat "${TMP_DIR}/remembered_empty.json"
|
||||
|
||||
python3 - "${TMP_DIR}/remembered_empty.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")
|
||||
assert isinstance(items, list), "items must be list"
|
||||
assert len(items) == 0, "items should be empty after purge"
|
||||
print("purge baseline validation passed")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== Feature test 2: MRU and dedupe by series id =="
|
||||
cat > "${TMP_DIR}/s1.json" <<'JSON'
|
||||
{"item":{"id":"100","name":"Series A","year":"2020","display_name":"Series A (2020)","raw":{"slug":"series-a"}}}
|
||||
JSON
|
||||
cat > "${TMP_DIR}/s2.json" <<'JSON'
|
||||
{"item":{"id":"200","name":"Series B","year":"2021","display_name":"Series B (2021)","raw":{"slug":"series-b"}}}
|
||||
JSON
|
||||
cat > "${TMP_DIR}/s1b.json" <<'JSON'
|
||||
{"item":{"id":"100","name":"Series A","year":"2020","display_name":"Series A (2020)","raw":{"slug":"series-a"}}}
|
||||
JSON
|
||||
|
||||
curl --fail --silent --show-error -X POST "${BASE_URL}/api/session/remembered-series" -H "Content-Type: application/json" --data @"${TMP_DIR}/s1.json" >/dev/null
|
||||
curl --fail --silent --show-error -X POST "${BASE_URL}/api/session/remembered-series" -H "Content-Type: application/json" --data @"${TMP_DIR}/s2.json" >/dev/null
|
||||
curl --fail --silent --show-error -X POST "${BASE_URL}/api/session/remembered-series" -H "Content-Type: application/json" --data @"${TMP_DIR}/s1b.json" >/dev/null
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
"${BASE_URL}/api/session/remembered-series" \
|
||||
-o "${TMP_DIR}/remembered_mru.json"
|
||||
|
||||
cat "${TMP_DIR}/remembered_mru.json"
|
||||
|
||||
python3 - "${TMP_DIR}/remembered_mru.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 []
|
||||
assert len(items) == 2, "expected deduped list of 2"
|
||||
assert items[0]["series"]["id"] == "100", "series 100 should be most-recent after reselection"
|
||||
assert items[1]["series"]["id"] == "200", "series 200 should be second"
|
||||
print("MRU and dedupe validation passed")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "== Feature test 3: remember_max_series limit evicts oldest =="
|
||||
cat > "${TMP_DIR}/settings_limit_2.json" <<'JSON'
|
||||
{"remember_max_series":2}
|
||||
JSON
|
||||
curl --fail --silent --show-error \
|
||||
-X PUT "${BASE_URL}/api/session/settings" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data @"${TMP_DIR}/settings_limit_2.json" \
|
||||
-o "${TMP_DIR}/settings_limit_resp.json"
|
||||
|
||||
cat > "${TMP_DIR}/s3.json" <<'JSON'
|
||||
{"item":{"id":"300","name":"Series C","year":"2022","display_name":"Series C (2022)","raw":{"slug":"series-c"}}}
|
||||
JSON
|
||||
curl --fail --silent --show-error -X POST "${BASE_URL}/api/session/remembered-series" -H "Content-Type: application/json" --data @"${TMP_DIR}/s3.json" >/dev/null
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
"${BASE_URL}/api/session/remembered-series" \
|
||||
-o "${TMP_DIR}/remembered_limited.json"
|
||||
|
||||
cat "${TMP_DIR}/remembered_limited.json"
|
||||
|
||||
python3 - "${TMP_DIR}/remembered_limited.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 []
|
||||
ids = [str(i["series"]["id"]) for i in items]
|
||||
assert len(items) == 2, f"limit should keep 2, got {len(items)}"
|
||||
assert ids[0] == "300", "latest should be first"
|
||||
assert "200" not in ids, "oldest series should be evicted"
|
||||
print("limit enforcement validation passed")
|
||||
PY
|
||||
|
||||
cat > "${TMP_DIR}/settings_reset_10.json" <<'JSON'
|
||||
{"remember_max_series":10}
|
||||
JSON
|
||||
curl --fail --silent --show-error \
|
||||
-X PUT "${BASE_URL}/api/session/settings" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data @"${TMP_DIR}/settings_reset_10.json" \
|
||||
>/dev/null
|
||||
|
||||
echo
|
||||
echo "All remembered series feature tests passed."
|
||||
Reference in New Issue
Block a user