diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py
index 7151960..d9f405b 100644
--- a/backend/app/routes/feed.py
+++ b/backend/app/routes/feed.py
@@ -124,7 +124,7 @@ def _filtered_query(
include_live: bool,
show: str,
scope: str = "my",
- exclude_search_discovered: bool = True,
+ library_source: str = "organic",
exclude_tag_category: str | None = None,
) -> tuple[Select, object]:
"""Build the feed query (joins + all WHERE filters), shared by /feed and /feed/count.
@@ -172,11 +172,16 @@ def _filtered_query(
# either way they shouldn't show in any feed view meanwhile.
query = query.where(Video.unavailable_since.is_(None))
- # Hide videos that only entered the catalog via a live YouTube search (search-discovered
- # "noise") from the Library by default. Only meaningful in "all" scope: "my" already
- # restricts to subscribed channels, where a once-searched video is legitimately the user's.
- if scope == "all" and exclude_search_discovered:
- query = query.where(Video.via_search.is_(False))
+ # Provenance filter for the Library: search-discovered videos are "noise" hidden by
+ # default ("organic"), can be mixed in ("all"), or shown exclusively ("search"). Only
+ # meaningful in "all" scope: "my" already restricts to subscribed channels, where a
+ # once-searched video is legitimately the user's.
+ if scope == "all":
+ if library_source == "organic":
+ query = query.where(Video.via_search.is_(False))
+ elif library_source == "search":
+ query = query.where(Video.via_search.is_(True))
+ # "all" → both organic and search-discovered videos
if channel_id:
query = query.where(Video.channel_id == channel_id)
@@ -278,7 +283,7 @@ def _feed_params(
include_live: bool = False,
show: str = "unwatched",
scope: str = "my",
- exclude_search_discovered: bool = True,
+ library_source: str = "organic",
) -> dict:
return {
"tags": tags,
@@ -295,7 +300,7 @@ def _feed_params(
"include_live": include_live,
"show": show,
"scope": scope,
- "exclude_search_discovered": exclude_search_discovered,
+ "library_source": library_source,
}
diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index 06e473c..ac289ea 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -417,20 +417,24 @@ export default function Feed({
{filters.scope === "all" && (
<>
-
+
>
)}
diff --git a/frontend/src/i18n/locales/de/feed.json b/frontend/src/i18n/locales/de/feed.json
index 9c26f56..c86f13c 100644
--- a/frontend/src/i18n/locales/de/feed.json
+++ b/frontend/src/i18n/locales/de/feed.json
@@ -30,8 +30,13 @@
"markedWatchedNamed": "Als angesehen markiert: „{{title}}”",
"markedWatched": "Als angesehen markiert",
"unwatch": "Nicht mehr als angesehen",
- "searchDiscovered": "Über Suche gefunden",
- "searchDiscoveredTip": "Auch Videos anzeigen, die nur über eine Live-YouTube-Suche in die Bibliothek kamen.",
+ "source": {
+ "label": "Quelle",
+ "tip": "Bibliothek danach filtern, wie Videos hierher kamen: nur organischer Katalog, mit Suchergebnissen, oder nur über Live-YouTube-Suche gefundene Videos.",
+ "organic": "Ohne Suchergebnisse",
+ "all": "Mit Suchergebnissen",
+ "only": "Nur Suchergebnisse"
+ },
"yt": {
"searchFor": "Auf YouTube suchen nach „{{query}}”",
"resultsFor": "YouTube-Ergebnisse für „{{query}}”",
diff --git a/frontend/src/i18n/locales/en/feed.json b/frontend/src/i18n/locales/en/feed.json
index f2c87f2..34f6a50 100644
--- a/frontend/src/i18n/locales/en/feed.json
+++ b/frontend/src/i18n/locales/en/feed.json
@@ -30,8 +30,13 @@
"markedWatchedNamed": "Marked watched “{{title}}”",
"markedWatched": "Marked watched",
"unwatch": "Unwatch",
- "searchDiscovered": "Search-discovered",
- "searchDiscoveredTip": "Also show videos that entered the library only through a live YouTube search.",
+ "source": {
+ "label": "Source",
+ "tip": "Filter the Library by how videos got here: organic catalog only, plus search results, or only videos found via live YouTube search.",
+ "organic": "Hide search results",
+ "all": "Include search results",
+ "only": "Search results only"
+ },
"yt": {
"searchFor": "Search YouTube for “{{query}}”",
"resultsFor": "YouTube results for “{{query}}”",
diff --git a/frontend/src/i18n/locales/hu/feed.json b/frontend/src/i18n/locales/hu/feed.json
index 5601183..0f5a805 100644
--- a/frontend/src/i18n/locales/hu/feed.json
+++ b/frontend/src/i18n/locales/hu/feed.json
@@ -30,8 +30,13 @@
"markedWatchedNamed": "Megnézettnek jelölve: „{{title}}”",
"markedWatched": "Megnézettnek jelölve",
"unwatch": "Megnézés visszavonása",
- "searchDiscovered": "Keresésből talált",
- "searchDiscoveredTip": "Mutasd azokat a videókat is, amelyek csak élő YouTube-keresésen át kerültek a könyvtárba.",
+ "source": {
+ "label": "Forrás",
+ "tip": "Szűrd a könyvtárat aszerint, hogyan kerültek be a videók: csak organikus katalógus, kereséssel együtt, vagy csak az élő YouTube-keresésből talált videók.",
+ "organic": "Keresés nélkül",
+ "all": "Kereséssel együtt",
+ "only": "Csak keresésből"
+ },
"yt": {
"searchFor": "Keresés a YouTube-on: „{{query}}”",
"resultsFor": "YouTube találatok erre: „{{query}}”",
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 516a102..098d47e 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -141,9 +141,9 @@ export interface FeedFilters {
includeShorts: boolean;
includeLive: boolean;
show: string;
- // Library (scope "all") only: when set, also show videos that entered the catalog via a
- // live YouTube search. Default (falsy) hides that search-discovered "noise".
- showSearchDiscovered?: boolean;
+ // Library (scope "all") only — provenance filter: "organic" (default) hides search-
+ // discovered videos, "all" mixes them in, "search" shows only them. Ignored for "my".
+ librarySource?: "organic" | "all" | "search";
channelId?: string;
channelName?: string;
maxAgeDays?: number;
@@ -309,8 +309,8 @@ function filterParams(f: FeedFilters): URLSearchParams {
if (f.dateTo) p.set("published_before", f.dateTo);
if (f.minDuration != null) p.set("min_duration", String(f.minDuration));
if (f.maxDuration != null) p.set("max_duration", String(f.maxDuration));
- // Only relevant in Library scope; the backend ignores it for "my". Default = hide.
- p.set("exclude_search_discovered", String(!f.showSearchDiscovered));
+ // Only relevant in Library scope; the backend ignores it for "my". Default = hide search.
+ p.set("library_source", f.librarySource ?? "organic");
return p;
}