linux observer
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3# ruff: noqa: F722, F821
4
5import logging
6import time
7from datetime import datetime
8
9from dbus_next import PropertyAccess, Variant
10from dbus_next.service import (
11 ServiceInterface,
12 dbus_property,
13 method,
14 signal as dbus_signal,
15)
16
17logger = logging.getLogger(__name__)
18
19BUS_NAME = "org.solpbc.solstone.Observer1"
20OBJECT_PATH = "/org/solpbc/solstone/Observer1"
21
22
23class ObserverService(ServiceInterface):
24 """D-Bus service interface for the observer."""
25
26 def __init__(self, observer):
27 super().__init__("org.solpbc.solstone.Observer1")
28 self._observer = observer
29
30 @dbus_property(access=PropertyAccess.READ)
31 def Status(self) -> "s":
32 if self._observer._paused:
33 return "paused"
34 if self._observer.current_mode == "screencast":
35 return "recording"
36 return "idle"
37
38 @dbus_property(access=PropertyAccess.READ)
39 def SyncStatus(self) -> "s":
40 if self._observer._sync:
41 return self._observer._sync.sync_status
42 return "synced"
43
44 @dbus_property(access=PropertyAccess.READ)
45 def SyncProgress(self) -> "s":
46 if self._observer._sync:
47 return self._observer._sync.sync_progress
48 return ""
49
50 @dbus_property(access=PropertyAccess.READ)
51 def CaptureDir(self) -> "s":
52 return str(self._observer.config.captures_dir)
53
54 @dbus_property(access=PropertyAccess.READ)
55 def SegmentTimer(self) -> "i":
56 if self._observer._paused or self._observer.segment_dir is None:
57 return 0
58 remaining = self._observer.interval - (
59 time.monotonic() - self._observer.start_at_mono
60 )
61 return max(0, int(remaining))
62
63 @dbus_property(access=PropertyAccess.READ)
64 def PauseRemaining(self) -> "i":
65 if not self._observer._paused or self._observer._pause_until <= 0:
66 return 0
67 return max(0, int(self._observer._pause_until - time.monotonic()))
68
69 @dbus_property(access=PropertyAccess.READ)
70 def Error(self) -> "s":
71 return ""
72
73 @dbus_property(access=PropertyAccess.READ)
74 def ServerUrl(self) -> "s":
75 return self._observer.config.server_url or ""
76
77 @dbus_property(access=PropertyAccess.READ)
78 def Stream(self) -> "s":
79 return self._observer.stream
80
81 @dbus_property(access=PropertyAccess.READ)
82 def SegmentInterval(self) -> "i":
83 return self._observer.interval
84
85 @method()
86 def Pause(self, duration_seconds: "i") -> "s":
87 self._observer.pause(duration_seconds)
88 return "ok"
89
90 @method()
91 def Resume(self) -> "s":
92 self._observer.resume()
93 return "ok"
94
95 @method()
96 def GetStats(self) -> "a{sv}":
97 captures_today = 0
98 total_size = 0
99 today = datetime.now().strftime("%Y%m%d")
100 captures_dir = self._observer.config.captures_dir
101
102 try:
103 if captures_dir.exists():
104 for day_dir in captures_dir.iterdir():
105 if not day_dir.is_dir():
106 continue
107 for stream_dir in day_dir.iterdir():
108 if not stream_dir.is_dir():
109 continue
110 for seg_dir in stream_dir.iterdir():
111 if not seg_dir.is_dir():
112 continue
113 if seg_dir.name.endswith(".incomplete"):
114 continue
115 if seg_dir.name.endswith(".failed"):
116 continue
117 if day_dir.name == today:
118 captures_today += 1
119 for file_path in seg_dir.iterdir():
120 if file_path.is_file():
121 total_size += file_path.stat().st_size
122 except OSError:
123 pass
124
125 synced_days = 0
126 if self._observer._sync:
127 synced_days = len(self._observer._sync._synced_days)
128
129 total_size_mb = int(total_size / (1024 * 1024))
130 uptime_seconds = int(time.monotonic() - self._observer._start_mono)
131
132 return {
133 "captures_today": Variant("i", captures_today),
134 "total_size_mb": Variant("i", total_size_mb),
135 "synced_days": Variant("i", synced_days),
136 "uptime_seconds": Variant("i", uptime_seconds),
137 }
138
139 @dbus_signal()
140 def StatusChanged(self, status) -> "s":
141 return status
142
143 @dbus_signal()
144 def SyncProgressChanged(self, progress) -> "s":
145 return progress
146
147 @dbus_signal()
148 def ErrorOccurred(self, message) -> "s":
149 return message