Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions fluxtuner/core/discovery_execution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: MIT
"""Execute desktop discovery with a worker-owned, read-only local connection."""

from __future__ import annotations

import sqlite3
from contextlib import closing
from pathlib import Path

from fluxtuner.core import db
from fluxtuner.core.search_service import SearchRequest, SearchResult, SearchService
from fluxtuner.players.capabilities import PlayerCapabilities


def search_desktop_stations(
request: SearchRequest,
*,
profile_name: str | None = None,
capabilities: PlayerCapabilities | None = None,
db_path: Path | None = None,
) -> SearchResult:
"""Search without creating profiles or sharing SQLite connections across threads."""
if request.source == "radio-browser":
return SearchService(capabilities=capabilities).search(request)

if request.source not in ("local", "all"):
raise ValueError("Unknown station discovery source.")

name = db.DEFAULT_PROFILE_NAME if profile_name is None else profile_name.strip()
if not name:
raise ValueError("An existing profile is required.")

path = db.DB_FILE if db_path is None else db_path
uri = path.resolve().as_uri() + "?mode=ro"

with closing(sqlite3.connect(uri, uri=True)) as conn:
conn.row_factory = sqlite3.Row
profile = conn.execute(
"""
SELECT p.id
FROM profiles AS p
JOIN users AS u ON u.id = p.user_id
WHERE lower(u.username) = lower(?)
AND lower(p.name) = lower(?)
""",
(db.DEFAULT_USER_NAME, name),
).fetchone()
if profile is None:
raise ValueError("The current profile is unavailable.")

service = SearchService(
capabilities=capabilities,
local_connection=conn,
profile_id=int(profile["id"]),
)
return service.search(request)
56 changes: 48 additions & 8 deletions fluxtuner/gui/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from contextlib import suppress
from pathlib import Path
from time import monotonic
from typing import Any
from typing import Any, Literal
from uuid import uuid4

import gi
Expand All @@ -30,6 +30,7 @@
)
from fluxtuner.core.custom_stations import add_custom_station # noqa: E402
from fluxtuner.core.data_usage import DataUsageTracker, format_usage_line # noqa: E402
from fluxtuner.core.discovery_execution import search_desktop_stations # noqa: E402
from fluxtuner.core.favorites import ( # noqa: E402
add_favorite,
all_favorite_tags,
Expand Down Expand Up @@ -358,6 +359,14 @@ def _build_search_bar(self, root: Gtk.Box) -> None:
self.min_bitrate_entry.connect("activate", self.on_search_clicked)
search_bar.append(self.min_bitrate_entry)

self.search_source_dropdown = Gtk.DropDown.new_from_strings(
["Radio Browser", "Local", "All"]
)
self.search_source_dropdown.set_selected(0)
self.search_source_dropdown.set_tooltip_text("Station discovery source")
search_bar.append(self.search_source_dropdown)
self.search_source_dropdown.connect("notify::selected", self._on_search_source_changed)

search_button = Gtk.Button(label="Search")
search_button.connect("clicked", self.on_search_clicked)
search_bar.append(search_button)
Expand Down Expand Up @@ -1354,25 +1363,56 @@ def _set_view_status(
station_word = "station" if count == 1 else "stations"
self.status_label.set_text(f"{view_name}{suffix} · {count} {station_word}")

def _selected_search_source(self) -> Literal["radio-browser", "local", "all"]:
sources: tuple[Literal["radio-browser", "local", "all"], ...] = (
"radio-browser",
"local",
"all",
)
selected = self.search_source_dropdown.get_selected()
if selected >= len(sources):
raise ValueError("Unknown station discovery source.")
return sources[selected]

def _on_search_source_changed(self, _dropdown: Gtk.DropDown, _param: object) -> None:
self._search_lifecycle.invalidate()
self._view_state.show_search()
self.last_search_results = []
self.stations = []
self.selected_station = None
self._render_results()
self._update_playlist_status()
self._set_view_status("Search results", 0)

def on_search_clicked(self, _widget: Gtk.Widget) -> None:
query = self.search_entry.get_text().strip()
country = self.country_entry.get_text().strip() or None
min_bitrate_text = self.min_bitrate_entry.get_text().strip()
min_bitrate = int(min_bitrate_text) if min_bitrate_text.isdigit() else None

source = self._selected_search_source()
profile_name = self.profile_name
capabilities = self.player_capabilities
generation = self._search_lifecycle.begin()
self.status_label.set_text("Searching…")

def worker() -> None:
try:
result = self.search_service.search(
SearchRequest(
query=query,
country=country,
min_bitrate=min_bitrate,
limit=50,
)
request = SearchRequest(
query=query,
country=country,
min_bitrate=min_bitrate,
limit=50,
source=source,
)
if source == "radio-browser":
result = self.search_service.search(request)
else:
result = search_desktop_stations(
request,
profile_name=profile_name,
capabilities=capabilities,
)
stations = result.stations
except Exception as exc: # noqa: BLE001 - user-facing status in GTK GUI.
GLib.idle_add(self._search_failed, generation, str(exc))
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/amber.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Footer .footer--highlight {
color: #f5e6c8;
}

#source-filter > SelectCurrent {
border: round #e3b241;
background: #111111;
color: #f5e6c8;
}

#source-filter:focus > SelectCurrent {
border: heavy #e3b241;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/default.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Footer .footer--highlight {
color: #e5e7eb;
}

#source-filter > SelectCurrent {
border: round #60a5fa;
background: #0f172a;
color: #e5e7eb;
}

#source-filter:focus > SelectCurrent {
border: heavy #60a5fa;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/dracula.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Footer .footer--highlight {
color: #f8f8f2;
}

#source-filter > SelectCurrent {
border: round #bd93f9;
background: #282a36;
color: #f8f8f2;
}

#source-filter:focus > SelectCurrent {
border: heavy #bd93f9;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/fluxtuner.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ Footer .footer--highlight {
color: #f4f7fb;
}

#source-filter > SelectCurrent {
border: round #6ee7d8;
background: #090d14;
color: #f4f7fb;
}

#source-filter:focus > SelectCurrent {
border: heavy #6ee7d8;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/nord.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Footer .footer--highlight {
color: #eceff4;
}

#source-filter > SelectCurrent {
border: round #88c0d0;
background: #2e3440;
color: #eceff4;
}

#source-filter:focus > SelectCurrent {
border: heavy #88c0d0;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
11 changes: 11 additions & 0 deletions fluxtuner/themes/ptmtrx.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Footer .footer--highlight {
color: #F8F8F2;
}

#source-filter > SelectCurrent {
border: round #58d68d;
background: #1B1D1E;
color: #F8F8F2;
}

#source-filter:focus > SelectCurrent {
border: heavy #58d68d;
}


.filter-label {
width: 10;
content-align: right middle;
Expand Down
Loading
Loading