Skip to content

Commit

Permalink
Add zeroconf dependency and zeroconf based scanner
Browse files Browse the repository at this point in the history
Refs: #175
  • Loading branch information
orontee committed Jun 8, 2024
1 parent e3872bc commit f303019
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 13 deletions.
9 changes: 6 additions & 3 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ or from a shell run::

$ flatpak run io.github.orontee.Argos

Note that the Python interpreter of the Flatpak environment is CPython
3.10.

Debugging
---------

Expand Down Expand Up @@ -72,9 +75,9 @@ To update translation files::
Dependencies
============

Runtime dependencies are listed in the file
`generated-poetry-sources.json </generated-poetry-sources.json>`_. It
is generated from ``poetry``'s lock file using `flatpak-builder-tools
Runtime dependencies are listed in the file `argos-dependencies.json
</argos-dependencies.json>`_. It has been generated from ``poetry``'s
lock file using `flatpak-builder-tools
<https://github.com/flatpak/flatpak-builder-tools>`_.

Build dependencies are listed in the `Containerfile </Containerfile>`_.
Expand Down
13 changes: 12 additions & 1 deletion generated-poetry-sources.json → argos-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@
"name": "poetry-deps",
"buildsystem": "simple",
"build-commands": [
"pip3 install --no-build-isolation --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} aiohttp url_normalize aiohttp-client-cache aiosignal aiosqlite async-timeout attrs charset-normalizer frozenlist idna multidict pycairo pygobject pyxdg yarl"
"pip3 debug",
"pip3 install --no-build-isolation --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} aiohttp url_normalize aiohttp-client-cache aiosignal aiosqlite async-timeout attrs charset-normalizer frozenlist idna multidict pycairo pygobject pyxdg yarl zeroconf"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl",
"sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/6f/2b/508d2fd0fcafa33c512a327dd47aa223e3aec2951e37fcad5a9360b019e5/zeroconf-0.132.2-cp310-cp310-manylinux_2_31_x86_64.whl",
"sha256": "ddae9592604fe04ec065cc53a321844c3592c812988346136d8ee548127f3d12"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/07/2f/27c9ae85646de72784529a86d2a98c7cfae4ff9eab0004becf47da66c7ec/aiohttp-3.9.2.tar.gz",
Expand Down
10 changes: 9 additions & 1 deletion argos/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from argos.model import Model
from argos.notify import Notifier
from argos.placement import WindowPlacement
from argos.scanner import MopidyServiceScanner
from argos.session import HTTPSessionManager
from argos.time import TimePositionTracker
from argos.utils import configure_logger
Expand Down Expand Up @@ -55,7 +56,7 @@ class Application(Gtk.Application):
hide_close_button = GObject.Property(type=bool, default=False)
version = GObject.Property(type=str)

def __init__(self, *args: list[Any], **kwargs: dict[Any, Any]):
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
*args,
Expand Down Expand Up @@ -93,6 +94,7 @@ def _exception_handler(
self._download = ImageDownloader(self)
self._information = InformationService(self)
self._notifier = Notifier(self)
self._service_scanner = MopidyServiceScanner(self)

self._controllers = Gio.ListStore.new(ControllerBase)
self._controllers.append(PlaybackController(self))
Expand Down Expand Up @@ -511,11 +513,17 @@ def show_preferences_activate_cb(
if self.window is None:
return

scanner_handle = self._loop.call_soon_threadsafe(
partial(self._loop.create_task, self._service_scanner())
)

prefs_window = PreferencesWindow(self)

def on_prefs_window_delete_event(_1: Gtk.Widget, _2: Gdk.Event) -> bool:
LOGGER.debug("Hiding preferences window")

scanner_handle.cancel()

# Default handler will destroy window
return False

Expand Down
1 change: 1 addition & 0 deletions argos/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ argos_sources = [
'message.py',
'notify.py',
'placement.py',
'scanner.py',
'session.py',
'time.py',
'utils.py',
Expand Down
89 changes: 89 additions & 0 deletions argos/scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Zeroconf based scan.
Code derived from
https://github.com/python-zeroconf/python-zeroconf/blob/master/examples/async_apple_scanner.py
"""

import asyncio
import logging
from functools import partial
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, cast

from gi.repository import GLib, GObject
from zeroconf import DNSQuestionType, IPVersion, ServiceStateChange, Zeroconf
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf

if TYPE_CHECKING:
from argos.app import Application

MOPIDY_SERVICE: str = "_mopidy-http._tcp.local."

LOGGER = logging.getLogger(__name__)


class MopidyServiceScanner(GObject.Object):
"""Scan Mopidy HTTP services."""

__gsignals__: Dict[str, Tuple[int, Any, Sequence]] = {
"service-discovered": (GObject.SIGNAL_RUN_FIRST, None, (str, str)),
}

def __init__(self, application: "Application") -> None:
super().__init__()

self.aiobrowser: Optional[AsyncServiceBrowser] = None
self.aiozc: Optional[AsyncZeroconf] = None

async def __call__(self) -> None:
LOGGER.debug("Scanning for Mopidy HTTP services")

self.aiozc = AsyncZeroconf(ip_version=IPVersion.V4Only)
self.aiobrowser = AsyncServiceBrowser(
self.aiozc.zeroconf,
[MOPIDY_SERVICE],
handlers=[self.on_service_state_change],
)

try:
while True:
await asyncio.sleep(1)
except asyncio.exceptions.CancelledError:
LOGGER.debug("Won't scan for Mopidy HTTP services anymore")
assert self.aiozc is not None
assert self.aiobrowser is not None
await self.aiobrowser.async_cancel()
await self.aiozc.async_close()

def on_service_state_change(
self,
zeroconf: Zeroconf,
service_type: str,
name: str,
state_change: ServiceStateChange,
) -> None:
LOGGER.debug(f"New state {state_change} for service {name}")
if state_change is not ServiceStateChange.Added:
return
asyncio.ensure_future(
self.notify_service_discovered(zeroconf, service_type, name)
)

async def notify_service_discovered(
self, zeroconf: Zeroconf, service_type: str, name: str
) -> None:
info = AsyncServiceInfo(service_type, name)
await info.async_request(zeroconf, 5000)
if info:
addresses = [
"%s:%d" % (addr, cast(int, info.port))
for addr in info.parsed_scoped_addresses()
]
LOGGER.debug(f"Service {name} is listening at {addresses}")

if len(addresses) < 1:
return

GLib.idle_add(partial(self.emit, "service-discovered", name, addresses[0]))
else:
LOGGER.warn(f"No info on {name} service")
72 changes: 67 additions & 5 deletions argos/ui/preferences.ui
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,78 @@
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="service_discovery_info_bar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="message-type">question</property>
<property name="revealed">False</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="service_discovery_set_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="use-stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="service_discovery_question_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Do you want to use the Mopidy HTTP service advertising on local network?</property>
<property name="wrap">True</property>
<property name="max-width-chars">50</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="services_label">
<property name="visible">True</property>
Expand All @@ -194,7 +256,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
<child>
Expand Down Expand Up @@ -261,7 +323,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
</object>
Expand Down
36 changes: 35 additions & 1 deletion argos/widgets/preferences.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import gettext
import logging
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any, Optional

from gi.repository import Gdk, Gio, GLib, GObject, Gtk

Expand All @@ -20,6 +20,9 @@ class PreferencesWindow(Gtk.Window):

mopidy_base_url_info_bar: Gtk.InfoBar = Gtk.Template.Child()
mopidy_base_url_entry: Gtk.Entry = Gtk.Template.Child()
service_discovery_info_bar: Gtk.InfoBar = Gtk.Template.Child()
service_discovery_question_label: Gtk.Label = Gtk.Template.Child()
service_discovery_set_button: Gtk.Button = Gtk.Template.Child()
information_service_switch: Gtk.Switch = Gtk.Template.Child()
index_mopidy_local_albums_button: Gtk.CheckButton = Gtk.Template.Child()
history_playlist_check_button: Gtk.CheckButton = Gtk.Template.Child()
Expand Down Expand Up @@ -133,6 +136,10 @@ def __init__(
"notify::active", self.on_start_fullscreen_switch_activated
)

application._service_scanner.connect(
"service-discovered", self.on_service_discovered
)

title_bar = Gtk.HeaderBar(title=_("Preferences"), show_close_button=True)
self.set_titlebar(title_bar)

Expand Down Expand Up @@ -167,6 +174,33 @@ def on_mopidy_base_url_entry_changed(self, entry: Gtk.Entry) -> None:
base_url = entry.get_text()
self._settings.set_string("mopidy-base-url", base_url)

def on_service_discovered(
self, scanner: Any, service_name: str, service_address: str
) -> None:
if len(service_address) <= 0:
return

self.service_discovery_question_label.set_text(
(
_(
"Do you want to use the Mopidy HTTP service "
"discovered at {address}"
)
).format(address=service_address)
)

self.service_discovery_info_bar.set_revealed(True)

mopidy_base_url_entry: Gtk.Entry = self.mopidy_base_url_entry

def on_service_discovery_set_button_clicked(_1: Gtk.Button) -> None:
LOGGER.debug("Base URL set to address of discovered service")
mopidy_base_url_entry.set_text(f"http://{service_address}")

self.service_discovery_set_button.connect(
"clicked", on_service_discovery_set_button_clicked
)

def on_information_service_switch_activated(
self,
switch: Gtk.Switch,
Expand Down
2 changes: 1 addition & 1 deletion io.github.orontee.Argos.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"*.a"
],
"modules" : [
"generated-poetry-sources.json",
"argos-dependencies.json",
{
"name": "webp-pixbuf-loader",
"buildsystem": "meson",
Expand Down
Loading

0 comments on commit f303019

Please sign in to comment.