kryten-webqueue 0.16.0__tar.gz → 0.17.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/CHANGELOG.md +9 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/catalog/db.py +42 -2
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/catalog.py +8 -4
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/pages.py +5 -4
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/catalog/browse.html +34 -6
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/pyproject.toml +1 -1
- kryten_webqueue-0.17.0/tests/test_search_facets.py +81 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/.gitignore +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/README.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/config.example.json +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/docs/UX_POLISH_PLAN.md +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/api_gate/client.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/logging_config.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/queue.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/routes/user.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/static/css/main.css +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/user/dashboard.html +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/tests/test_save_results_to_playlist.py +0 -0
|
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.17.0] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Search now combines with category/tag filters.** A free-text search ANDs with the selected category and/or tag instead of ignoring them. `db.search()` / `db.search_count()` accept `category` and `tag`; the `/catalog/search` page and JSON route pass them through and keep the dropdowns populated/selected; `applyFacets()` includes the active facets when a query is present. (Shared `_facet_filter()` SQL helper keeps browse and search behavior identical.)
|
|
14
|
+
- **Clear empty-results messaging.** When a search and/or facet filter returns nothing, the catalog page now explains *why* — naming the active query and filters — and offers one-click escapes ("Search without filters", "Clear filters", "Back to browse") instead of a bare "No items found."
|
|
15
|
+
|
|
16
|
+
[0.17.0]: https://github.com/grobertson/kryten-webqueue/releases/tag/v0.17.0
|
|
17
|
+
|
|
9
18
|
## [0.16.0] — 2026-06-17
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -59,6 +59,36 @@ def _hidden_exclusion(alias: str = "c") -> tuple[str, list]:
|
|
|
59
59
|
return sql, [*HIDDEN_CATEGORY_NAMES, *HIDDEN_TAG_NAMES]
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _facet_filter(alias: str, category: str | None, tag: str | None) -> tuple[str, list]:
|
|
63
|
+
"""SQL fragment (+ params) AND-filtering by a category slug and/or tag name.
|
|
64
|
+
|
|
65
|
+
Each filter is an ``AND friendly_token IN (...)`` subquery on the catalog row
|
|
66
|
+
under ``alias``; an absent filter contributes nothing. Shared by browse() and
|
|
67
|
+
search() so the two paths narrow results identically.
|
|
68
|
+
"""
|
|
69
|
+
sql = ""
|
|
70
|
+
params: list = []
|
|
71
|
+
if category:
|
|
72
|
+
sql += f"""
|
|
73
|
+
AND {alias}.friendly_token IN (
|
|
74
|
+
SELECT cc.friendly_token FROM catalog_categories cc
|
|
75
|
+
JOIN categories cat ON cc.category_id = cat.id
|
|
76
|
+
WHERE cat.slug = ?
|
|
77
|
+
)
|
|
78
|
+
"""
|
|
79
|
+
params.append(category)
|
|
80
|
+
if tag:
|
|
81
|
+
sql += f"""
|
|
82
|
+
AND {alias}.friendly_token IN (
|
|
83
|
+
SELECT ct.friendly_token FROM catalog_tags ct
|
|
84
|
+
JOIN tags t ON ct.tag_id = t.id
|
|
85
|
+
WHERE t.name = ?
|
|
86
|
+
)
|
|
87
|
+
"""
|
|
88
|
+
params.append(tag)
|
|
89
|
+
return sql, params
|
|
90
|
+
|
|
91
|
+
|
|
62
92
|
# Default quality-weighted ordering (see browse() for rationale).
|
|
63
93
|
_DEFAULT_ORDER = """
|
|
64
94
|
ORDER BY
|
|
@@ -446,7 +476,8 @@ class Database:
|
|
|
446
476
|
row = await self._fetch_one(query, params)
|
|
447
477
|
return row["cnt"] if row else 0
|
|
448
478
|
|
|
449
|
-
async def search(self, query_text: str, *,
|
|
479
|
+
async def search(self, query_text: str, *, category: str | None = None, tag: str | None = None,
|
|
480
|
+
page: int = 1, per_page: int = 24, show_hidden: bool = False, sort: str = "default") -> list[dict]:
|
|
450
481
|
sql = """
|
|
451
482
|
SELECT c.friendly_token, c.title, c.duration_sec, c.cover_art_path, c.cover_art_source, c.thumbnail_url, c.manifest_url,
|
|
452
483
|
rank AS relevance
|
|
@@ -464,6 +495,11 @@ class Database:
|
|
|
464
495
|
excl_sql, excl_params = _hidden_exclusion("c")
|
|
465
496
|
sql += excl_sql
|
|
466
497
|
params.extend(excl_params)
|
|
498
|
+
# Category/tag facets AND with the text match (same subqueries browse()
|
|
499
|
+
# uses), so a search can be narrowed by the selected facets.
|
|
500
|
+
facet_sql, facet_params = _facet_filter("c", category, tag)
|
|
501
|
+
sql += facet_sql
|
|
502
|
+
params.extend(facet_params)
|
|
467
503
|
# Relevance is the natural default for a text query; other sort keys let
|
|
468
504
|
# the user reorder the matched set explicitly.
|
|
469
505
|
sql += " ORDER BY rank " if (sort or "default") == "default" else _browse_order_clause(sort)
|
|
@@ -471,7 +507,8 @@ class Database:
|
|
|
471
507
|
params.extend([per_page, (page - 1) * per_page])
|
|
472
508
|
return await self._fetch_all(sql, params)
|
|
473
509
|
|
|
474
|
-
async def search_count(self, query_text: str, *,
|
|
510
|
+
async def search_count(self, query_text: str, *, category: str | None = None, tag: str | None = None,
|
|
511
|
+
show_hidden: bool = False) -> int:
|
|
475
512
|
sql = """
|
|
476
513
|
SELECT COUNT(*) as cnt
|
|
477
514
|
FROM catalog_fts fts
|
|
@@ -488,6 +525,9 @@ class Database:
|
|
|
488
525
|
excl_sql, excl_params = _hidden_exclusion("c")
|
|
489
526
|
sql += excl_sql
|
|
490
527
|
params.extend(excl_params)
|
|
528
|
+
facet_sql, facet_params = _facet_filter("c", category, tag)
|
|
529
|
+
sql += facet_sql
|
|
530
|
+
params.extend(facet_params)
|
|
491
531
|
row = await self._fetch_one(sql, params)
|
|
492
532
|
return row["cnt"] if row else 0
|
|
493
533
|
|
|
@@ -19,15 +19,19 @@ async def browse(request: Request, category: str | None = None, tag: str | None
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@router.get("/search")
|
|
22
|
-
async def search(request: Request, q: str = "",
|
|
22
|
+
async def search(request: Request, q: str = "", category: str | None = None, tag: str | None = None,
|
|
23
|
+
page: int = 1, show_hidden: int = 0,
|
|
23
24
|
sort: str = "default", user: dict = Depends(get_current_user)):
|
|
24
|
-
"""Full-text search of catalog."""
|
|
25
|
+
"""Full-text search of catalog, optionally narrowed by category/tag."""
|
|
25
26
|
if not q.strip():
|
|
26
27
|
raise HTTPException(400, "Query required")
|
|
27
28
|
db = request.app.state.db
|
|
28
29
|
show_hidden = bool(show_hidden) and (user.get("rank") or 0) >= 3
|
|
29
|
-
items = await db.search(q, page=page, show_hidden=show_hidden, sort=sort)
|
|
30
|
-
|
|
30
|
+
items = await db.search(q, category=category, tag=tag, page=page, show_hidden=show_hidden, sort=sort)
|
|
31
|
+
categories = await db.get_categories(show_hidden=show_hidden)
|
|
32
|
+
tags = await db.get_tags(show_hidden=show_hidden)
|
|
33
|
+
return {"items": items, "categories": categories, "tags": tags,
|
|
34
|
+
"query": q, "active_category": category, "active_tag": tag, "page": page, "sort": sort}
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
@router.get("/item/{friendly_token}")
|
|
@@ -107,6 +107,7 @@ async def catalog_browse_page(request: Request, category: str | None = None,
|
|
|
107
107
|
|
|
108
108
|
@router.get("/catalog/search", response_class=HTMLResponse)
|
|
109
109
|
async def catalog_search_page(request: Request, q: str = "", page: int = 1,
|
|
110
|
+
category: str | None = None, tag: str | None = None,
|
|
110
111
|
show_hidden: int = 0, sort: str = "default"):
|
|
111
112
|
user = _get_user_or_none(request)
|
|
112
113
|
if not user:
|
|
@@ -117,8 +118,8 @@ async def catalog_search_page(request: Request, q: str = "", page: int = 1,
|
|
|
117
118
|
is_admin = (user.get("rank") or 0) >= 3
|
|
118
119
|
show_hidden = bool(show_hidden) and is_admin
|
|
119
120
|
sort = sort if sort in _VALID_SORTS else "default"
|
|
120
|
-
items = await db.search(q, page=page, show_hidden=show_hidden, sort=sort)
|
|
121
|
-
total = await db.search_count(q, show_hidden=show_hidden)
|
|
121
|
+
items = await db.search(q, category=category, tag=tag, page=page, show_hidden=show_hidden, sort=sort)
|
|
122
|
+
total = await db.search_count(q, category=category, tag=tag, show_hidden=show_hidden)
|
|
122
123
|
total_pages = max(1, (total + 23) // 24)
|
|
123
124
|
categories = await db.get_categories(show_hidden=show_hidden)
|
|
124
125
|
tags = await db.get_tags(show_hidden=show_hidden)
|
|
@@ -130,8 +131,8 @@ async def catalog_search_page(request: Request, q: str = "", page: int = 1,
|
|
|
130
131
|
"tags": tags,
|
|
131
132
|
"page": page,
|
|
132
133
|
"total_pages": total_pages,
|
|
133
|
-
"active_category":
|
|
134
|
-
"active_tag":
|
|
134
|
+
"active_category": category,
|
|
135
|
+
"active_tag": tag,
|
|
135
136
|
"query": q,
|
|
136
137
|
"is_admin": is_admin,
|
|
137
138
|
"show_hidden": show_hidden,
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/catalog/browse.html
RENAMED
|
@@ -101,13 +101,41 @@
|
|
|
101
101
|
{% endfor %}
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
|
+
{% set sort_q = ('&sort=' ~ sort) if (sort and sort != 'default') else '' %}
|
|
104
105
|
{% if not items %}
|
|
105
106
|
<div class="empty-state">
|
|
106
|
-
|
|
107
|
+
{% if query and (active_category or active_tag) %}
|
|
108
|
+
<p>No results for "{{ query }}" within the selected filter{% if active_category and active_tag %}s{% endif %}.</p>
|
|
109
|
+
<p class="muted">
|
|
110
|
+
Active:
|
|
111
|
+
{% if active_category %}<strong>category</strong>{% endif %}
|
|
112
|
+
{% if active_category and active_tag %} and {% endif %}
|
|
113
|
+
{% if active_tag %}<strong>tag “{{ active_tag }}”</strong>{% endif %}.
|
|
114
|
+
Try widening your search.
|
|
115
|
+
</p>
|
|
116
|
+
<p>
|
|
117
|
+
<a class="btn btn-sm" href="/catalog/search?q={{ query | urlencode }}{{ sort_q }}">Search without filters</a>
|
|
118
|
+
<a class="btn btn-sm" href="/catalog/browse">Clear search</a>
|
|
119
|
+
</p>
|
|
120
|
+
{% elif query %}
|
|
121
|
+
<p>No results for "{{ query }}".</p>
|
|
122
|
+
<p class="muted">Check the spelling or try fewer / different words.</p>
|
|
123
|
+
<p><a class="btn btn-sm" href="/catalog/browse">Back to browse</a></p>
|
|
124
|
+
{% elif active_category or active_tag %}
|
|
125
|
+
<p>No items match the selected filter{% if active_category and active_tag %}s{% endif %}.</p>
|
|
126
|
+
<p class="muted">
|
|
127
|
+
Active:
|
|
128
|
+
{% if active_category %}<strong>category</strong>{% endif %}
|
|
129
|
+
{% if active_category and active_tag %} and {% endif %}
|
|
130
|
+
{% if active_tag %}<strong>tag “{{ active_tag }}”</strong>{% endif %}.
|
|
131
|
+
</p>
|
|
132
|
+
<p><a class="btn btn-sm" href="/catalog/browse">Clear filters</a></p>
|
|
133
|
+
{% else %}
|
|
134
|
+
<p>No items found.</p>
|
|
135
|
+
{% endif %}
|
|
107
136
|
</div>
|
|
108
137
|
{% endif %}
|
|
109
138
|
|
|
110
|
-
{% set sort_q = ('&sort=' ~ sort) if (sort and sort != 'default') else '' %}
|
|
111
139
|
<div class="pagination">
|
|
112
140
|
{% if page > 1 %}
|
|
113
141
|
<a href="?page={{ page - 1 }}{% if query %}&q={{ query }}{% endif %}{% if active_category %}&category={{ active_category }}{% endif %}{% if active_tag %}&tag={{ active_tag | urlencode }}{% endif %}{{ sort_q }}" class="btn btn-sm">← Prev</a>
|
|
@@ -141,13 +169,13 @@ function applyFacets() {
|
|
|
141
169
|
const sort = document.getElementById('sort-select').value;
|
|
142
170
|
try { localStorage.setItem('browse_sort', sort); } catch (e) { /* ignore */ }
|
|
143
171
|
const params = new URLSearchParams();
|
|
144
|
-
// On a search results page
|
|
172
|
+
// On a search results page keep the query AND apply the facets so search
|
|
173
|
+
// results are narrowed by category/tag (they AND together server-side).
|
|
145
174
|
if (CURRENT_QUERY) {
|
|
146
175
|
params.set('q', CURRENT_QUERY);
|
|
147
|
-
} else {
|
|
148
|
-
if (cat) params.set('category', cat);
|
|
149
|
-
if (tag) params.set('tag', tag);
|
|
150
176
|
}
|
|
177
|
+
if (cat) params.set('category', cat);
|
|
178
|
+
if (tag) params.set('tag', tag);
|
|
151
179
|
if (sort && sort !== 'default') params.set('sort', sort);
|
|
152
180
|
const base = CURRENT_QUERY ? '/catalog/search' : '/catalog/browse';
|
|
153
181
|
const qs = params.toString();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Search combines with category/tag facets (Item 4 of the UX polish plan).
|
|
2
|
+
|
|
3
|
+
A free-text search now ANDs with the selected category and/or tag, mirroring how
|
|
4
|
+
browse() already filters. These tests pin that behavior at the DB layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from kryten_webqueue.catalog.db import Database
|
|
10
|
+
|
|
11
|
+
MEDIACMS = "https://www.dropsugar.com"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
async def db(tmp_path):
|
|
16
|
+
database = Database(str(tmp_path / "search_facets.db"))
|
|
17
|
+
await database.connect()
|
|
18
|
+
await database.run_migrations()
|
|
19
|
+
yield database
|
|
20
|
+
await database.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _add_catalog(db, token, title):
|
|
24
|
+
await db.insert_catalog({
|
|
25
|
+
"friendly_token": token,
|
|
26
|
+
"title": title,
|
|
27
|
+
"description": "",
|
|
28
|
+
"duration_sec": 600,
|
|
29
|
+
"manifest_url": f"{MEDIACMS}/api/v1/media/cytube/{token}.json?format=json",
|
|
30
|
+
"thumbnail_url": "",
|
|
31
|
+
"synced_at": "2026-01-01T00:00:00+00:00",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def test_search_ands_with_category_and_tag(db):
|
|
36
|
+
# Three items all matching the query "Dragon", differentiated by facets.
|
|
37
|
+
await _add_catalog(db, "tok_action", "Dragon Action")
|
|
38
|
+
await _add_catalog(db, "tok_comedy", "Dragon Comedy")
|
|
39
|
+
await _add_catalog(db, "tok_plain", "Dragon Plain")
|
|
40
|
+
|
|
41
|
+
action_id = await db.upsert_category("Action")
|
|
42
|
+
comedy_id = await db.upsert_category("Comedy")
|
|
43
|
+
await db.set_catalog_categories("tok_action", [action_id])
|
|
44
|
+
await db.set_catalog_categories("tok_comedy", [comedy_id])
|
|
45
|
+
|
|
46
|
+
epic_tag = await db.upsert_tag("Epic")
|
|
47
|
+
await db.set_catalog_tags("tok_action", [epic_tag])
|
|
48
|
+
|
|
49
|
+
# Resolve the Action slug (upsert derives it).
|
|
50
|
+
action_slug = next(c["slug"] for c in await db.get_categories() if c["name"] == "Action")
|
|
51
|
+
|
|
52
|
+
# Plain query: all three match.
|
|
53
|
+
assert {r["friendly_token"] for r in await db.search("Dragon")} == {
|
|
54
|
+
"tok_action", "tok_comedy", "tok_plain",
|
|
55
|
+
}
|
|
56
|
+
assert await db.search_count("Dragon") == 3
|
|
57
|
+
|
|
58
|
+
# Query + category: only the Action item.
|
|
59
|
+
cat_results = {r["friendly_token"] for r in await db.search("Dragon", category=action_slug)}
|
|
60
|
+
assert cat_results == {"tok_action"}
|
|
61
|
+
assert await db.search_count("Dragon", category=action_slug) == 1
|
|
62
|
+
|
|
63
|
+
# Query + tag: only the Epic-tagged item.
|
|
64
|
+
tag_results = {r["friendly_token"] for r in await db.search("Dragon", tag="Epic")}
|
|
65
|
+
assert tag_results == {"tok_action"}
|
|
66
|
+
assert await db.search_count("Dragon", tag="Epic") == 1
|
|
67
|
+
|
|
68
|
+
# Query + category + tag that don't co-occur: empty (true intersection).
|
|
69
|
+
assert await db.search("Dragon", category=action_slug, tag="Nonexistent") == []
|
|
70
|
+
assert await db.search_count("Dragon", category=action_slug, tag="Nonexistent") == 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def test_search_facet_with_no_text_match_is_empty(db):
|
|
74
|
+
await _add_catalog(db, "tok1", "Comedy Night")
|
|
75
|
+
comedy_id = await db.upsert_category("Comedy")
|
|
76
|
+
await db.set_catalog_categories("tok1", [comedy_id])
|
|
77
|
+
comedy_slug = next(c["slug"] for c in await db.get_categories() if c["name"] == "Comedy")
|
|
78
|
+
|
|
79
|
+
# The category matches the item, but the query does not -> no results.
|
|
80
|
+
assert await db.search("Horror", category=comedy_slug) == []
|
|
81
|
+
assert await db.search_count("Horror", category=comedy_slug) == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/_common.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/ytpipe/__init__.py
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/integrations/ytpipe/downloader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/playlists.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/promos.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/queue_mgmt.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/admin/schedules.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/catalog/item_detail.html
RENAMED
|
File without changes
|
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/queue/index.html
RENAMED
|
File without changes
|
{kryten_webqueue-0.16.0 → kryten_webqueue-0.17.0}/kryten_webqueue/templates/user/dashboard.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|