tinyjukebox 0.47.0__py3-none-any.whl

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.
Files changed (154) hide show
  1. tinyjukebox/__init__.py +7 -0
  2. tinyjukebox/albumplayer/__init__.py +1 -0
  3. tinyjukebox/albumplayer/aalbumplayer.py +402 -0
  4. tinyjukebox/albumplayer/clock.py +39 -0
  5. tinyjukebox/albumplayer/dummyplayer.py +49 -0
  6. tinyjukebox/albumplayer/player_interface.py +97 -0
  7. tinyjukebox/albumplayer/radioplayer.py +391 -0
  8. tinyjukebox/albumplayer/simpleplayer.py +419 -0
  9. tinyjukebox/albumplayer/spotifyplayer.py +671 -0
  10. tinyjukebox/backup.py +176 -0
  11. tinyjukebox/backups.py +340 -0
  12. tinyjukebox/cli.py +1076 -0
  13. tinyjukebox/control.py +1386 -0
  14. tinyjukebox/core/__init__.py +1 -0
  15. tinyjukebox/core/config.py +100 -0
  16. tinyjukebox/core/logging/__init__.py +113 -0
  17. tinyjukebox/core/microservice.py +303 -0
  18. tinyjukebox/core/pubsub/__init__.py +15 -0
  19. tinyjukebox/core/pubsub/base.py +196 -0
  20. tinyjukebox/core/pubsub/localqueue.py +102 -0
  21. tinyjukebox/core/pubsub/statistics.py +57 -0
  22. tinyjukebox/core/py.typed +0 -0
  23. tinyjukebox/core/schema/__init__.py +5 -0
  24. tinyjukebox/core/schema/logger.py +40 -0
  25. tinyjukebox/core/schema/microservice.py +48 -0
  26. tinyjukebox/core/threads.py +39 -0
  27. tinyjukebox/core/ui/__init__.py +5 -0
  28. tinyjukebox/core/ui/cli.py +120 -0
  29. tinyjukebox/core/ui/tools.py +73 -0
  30. tinyjukebox/core/unexpected.py +46 -0
  31. tinyjukebox/data/__init__.py +0 -0
  32. tinyjukebox/data/activities.py +175 -0
  33. tinyjukebox/data/activity.py +138 -0
  34. tinyjukebox/data/common.py +91 -0
  35. tinyjukebox/data/degraded.py +39 -0
  36. tinyjukebox/data/jsondata.py +218 -0
  37. tinyjukebox/data/sanitycheck.py +71 -0
  38. tinyjukebox/data/tag.py +98 -0
  39. tinyjukebox/data/tags.py +160 -0
  40. tinyjukebox/data/target.py +568 -0
  41. tinyjukebox/data/targetalbumtools.py +88 -0
  42. tinyjukebox/data/targets.py +256 -0
  43. tinyjukebox/data/volumelimits.py +68 -0
  44. tinyjukebox/data_folder.py +180 -0
  45. tinyjukebox/devices/__init__.py +1 -0
  46. tinyjukebox/devices/baseclasses/__init__.py +1 -0
  47. tinyjukebox/devices/baseclasses/adriver.py +115 -0
  48. tinyjukebox/devices/baseclasses/aevents.py +47 -0
  49. tinyjukebox/devices/baseclasses/gpiolevel.py +16 -0
  50. tinyjukebox/devices/bluetooth/__init__.py +1 -0
  51. tinyjukebox/devices/bluetooth/avrcp_input.py +118 -0
  52. tinyjukebox/devices/bluetooth/bluez.py +446 -0
  53. tinyjukebox/devices/bluetooth/link.py +396 -0
  54. tinyjukebox/devices/bluetooth/virtual.py +155 -0
  55. tinyjukebox/devices/drivermanager/__init__.py +1 -0
  56. tinyjukebox/devices/drivermanager/driverfactory.py +75 -0
  57. tinyjukebox/devices/drivermanager/drivermanager.py +151 -0
  58. tinyjukebox/devices/drivers/__init__.py +31 -0
  59. tinyjukebox/devices/drivers/bluetooth.py +257 -0
  60. tinyjukebox/devices/drivers/input.py +165 -0
  61. tinyjukebox/devices/drivers/rfid.py +297 -0
  62. tinyjukebox/devices/drivers/rgbled.py +215 -0
  63. tinyjukebox/devices/py.typed +0 -0
  64. tinyjukebox/devices/schema/__init__.py +1 -0
  65. tinyjukebox/devices/schema/adriver.py +33 -0
  66. tinyjukebox/devices/schema/bluetooth.py +31 -0
  67. tinyjukebox/devices/schema/drivermanager.py +41 -0
  68. tinyjukebox/devices/schema/input.py +23 -0
  69. tinyjukebox/devices/schema/rfid.py +44 -0
  70. tinyjukebox/devices/schema/rgbled.py +26 -0
  71. tinyjukebox/diagnostics.py +179 -0
  72. tinyjukebox/errors.py +91 -0
  73. tinyjukebox/headset.py +296 -0
  74. tinyjukebox/images.py +133 -0
  75. tinyjukebox/latency.py +43 -0
  76. tinyjukebox/library/__init__.py +2 -0
  77. tinyjukebox/library/diagnostics.py +183 -0
  78. tinyjukebox/library/journal.py +94 -0
  79. tinyjukebox/library/releases.py +348 -0
  80. tinyjukebox/library/schema.py +98 -0
  81. tinyjukebox/library/service.py +1115 -0
  82. tinyjukebox/library/statistics.py +288 -0
  83. tinyjukebox/library/store.py +1386 -0
  84. tinyjukebox/messages.py +202 -0
  85. tinyjukebox/metadata.py +133 -0
  86. tinyjukebox/py.typed +0 -0
  87. tinyjukebox/restore.py +391 -0
  88. tinyjukebox/rgbled.py +158 -0
  89. tinyjukebox/schema/__init__.py +1 -0
  90. tinyjukebox/schema/activities.py +27 -0
  91. tinyjukebox/schema/tags.py +54 -0
  92. tinyjukebox/schema/targets.py +83 -0
  93. tinyjukebox/schema/tinyjukebox.py +217 -0
  94. tinyjukebox/schema/volume.py +28 -0
  95. tinyjukebox/slots.py +234 -0
  96. tinyjukebox/spotify/__init__.py +1 -0
  97. tinyjukebox/spotify/account.py +647 -0
  98. tinyjukebox/spotify/engine.py +568 -0
  99. tinyjukebox/spotify/oembed.py +65 -0
  100. tinyjukebox/spotify/soloist.py +198 -0
  101. tinyjukebox/spotify/sound.py +316 -0
  102. tinyjukebox/stm/__init__.py +0 -0
  103. tinyjukebox/stm/base.py +174 -0
  104. tinyjukebox/stm/playradio.py +237 -0
  105. tinyjukebox/stm/playspotify.py +418 -0
  106. tinyjukebox/stm/stmoperation.py +769 -0
  107. tinyjukebox/stm/stmplayalbum.py +655 -0
  108. tinyjukebox/stm/stmvolume.py +221 -0
  109. tinyjukebox/sync/__init__.py +42 -0
  110. tinyjukebox/sync/box.py +486 -0
  111. tinyjukebox/sync/client.py +189 -0
  112. tinyjukebox/sync/data.py +489 -0
  113. tinyjukebox/sync/files.py +447 -0
  114. tinyjukebox/sync/handshake.py +42 -0
  115. tinyjukebox/sync/link.py +379 -0
  116. tinyjukebox/sync/manifest.py +162 -0
  117. tinyjukebox/sync/util.py +137 -0
  118. tinyjukebox/tinyjukebox.py +680 -0
  119. tinyjukebox/tools/__init__.py +1 -0
  120. tinyjukebox/tools/backup.py +173 -0
  121. tinyjukebox/tools/fill_library.py +120 -0
  122. tinyjukebox/tools/import_cards.py +274 -0
  123. tinyjukebox/tools/import_covers.py +138 -0
  124. tinyjukebox/tools/migrate_box01.py +151 -0
  125. tinyjukebox/volumemanager.py +355 -0
  126. tinyjukebox/web/__init__.py +1 -0
  127. tinyjukebox/web/auth.py +373 -0
  128. tinyjukebox/web/certificates.py +111 -0
  129. tinyjukebox/web/logbuffer.py +62 -0
  130. tinyjukebox/web/server.py +1237 -0
  131. tinyjukebox/web/uploads.py +262 -0
  132. tinyjukebox/web/users.py +106 -0
  133. tinyjukebox/web/web/app.js +3050 -0
  134. tinyjukebox/web/web/favicon.svg +6 -0
  135. tinyjukebox/web/web/help/de/admin.html +714 -0
  136. tinyjukebox/web/web/help/de/manual.html +1147 -0
  137. tinyjukebox/web/web/help/de/quick-guide.html +79 -0
  138. tinyjukebox/web/web/help/en/admin.html +701 -0
  139. tinyjukebox/web/web/help/en/manual.html +1126 -0
  140. tinyjukebox/web/web/help/en/quick-guide.html +79 -0
  141. tinyjukebox/web/web/help/help.css +25 -0
  142. tinyjukebox/web/web/help/images/box-2026.jpg +0 -0
  143. tinyjukebox/web/web/help/images/box-back.jpg +0 -0
  144. tinyjukebox/web/web/help/images/lid-top.jpg +0 -0
  145. tinyjukebox/web/web/help/images/logo-mark.svg +38 -0
  146. tinyjukebox/web/web/index.html +867 -0
  147. tinyjukebox/web/web/style.css +412 -0
  148. tinyjukebox/web/web/texts/de.json +892 -0
  149. tinyjukebox/web/web/texts/en.json +892 -0
  150. tinyjukebox-0.47.0.dist-info/METADATA +236 -0
  151. tinyjukebox-0.47.0.dist-info/RECORD +154 -0
  152. tinyjukebox-0.47.0.dist-info/WHEEL +4 -0
  153. tinyjukebox-0.47.0.dist-info/entry_points.txt +9 -0
  154. tinyjukebox-0.47.0.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,221 @@
1
+ """STMVolume - the state machine of the volume buttons."""
2
+
3
+ import logging
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ from tinyjukebox.core import unexpected
9
+ from tinyjukebox.core.logging import get_child
10
+ from tinyjukebox.core.pubsub import PubSubClient
11
+ from tinyjukebox.latency import ReactionTimer
12
+ from tinyjukebox.messages import BUTTON_HELD, BUTTON_PRESSED, Button
13
+ from tinyjukebox.stm.base import State, create_machine, write_dot_file
14
+
15
+ __all__ = ["STMVolume", "States", "Transitions", "VolumeButtons"]
16
+
17
+
18
+ class VolumeButtons(Protocol):
19
+ """What the buttons change: a :class:`~tinyjukebox.volumemanager.VolumeManager` or - speaker and headset - the
20
+ :class:`~tinyjukebox.volumemanager.Volumes`."""
21
+
22
+ def update_volume(self) -> None: ...
23
+
24
+ def volume_up(self, held: bool = False) -> None: ...
25
+
26
+ def volume_down(self, held: bool = False) -> None: ...
27
+
28
+ def volume_off(self) -> None: ...
29
+
30
+
31
+ class States(Enum):
32
+ IDLE = "idle"
33
+ INIT = "init"
34
+ VOLUME_UP = "up"
35
+ VOLUME_DOWN = "down"
36
+ VOLUME_UP_HELD = "up_held" # the small step of a button that is kept pressed
37
+ VOLUME_DOWN_HELD = "down_held"
38
+ END = "end"
39
+
40
+
41
+ class Transitions(Enum):
42
+ START = "start"
43
+ TO_IDLE = "to_idle"
44
+ TO_VOLUME_UP = "to_volume_up"
45
+ TO_VOLUME_DOWN = "to_volume_down"
46
+ TO_VOLUME_UP_HELD = "to_volume_up_held"
47
+ TO_VOLUME_DOWN_HELD = "to_volume_down_held"
48
+ END = "to_end"
49
+
50
+
51
+ _TRANSITIONS: dict[str, dict[object, Transitions]] = {
52
+ BUTTON_PRESSED: {
53
+ Button.VOLUME_UP: Transitions.TO_VOLUME_UP,
54
+ Button.VOLUME_DOWN: Transitions.TO_VOLUME_DOWN,
55
+ },
56
+ BUTTON_HELD: {
57
+ Button.VOLUME_UP: Transitions.TO_VOLUME_UP_HELD,
58
+ Button.VOLUME_DOWN: Transitions.TO_VOLUME_DOWN_HELD,
59
+ },
60
+ }
61
+
62
+
63
+ class Idle(State):
64
+ def __init__(self, logger: logging.Logger) -> None:
65
+ super().__init__(States.IDLE, logger)
66
+
67
+
68
+ class End(State):
69
+ def __init__(self, logger: logging.Logger, volumemanager: VolumeButtons) -> None:
70
+ super().__init__(States.END, logger)
71
+ self._volumemanager = volumemanager
72
+
73
+ def _on_entry(self) -> Enum | None:
74
+ self._volumemanager.volume_off() # mute on shutdown
75
+ return None
76
+
77
+
78
+ class Init(State):
79
+ def __init__(self, logger: logging.Logger, volumemanager: VolumeButtons) -> None:
80
+ super().__init__(States.INIT, logger)
81
+ self._volumemanager = volumemanager
82
+
83
+ def _on_exit(self) -> None:
84
+ self._volumemanager.update_volume()
85
+
86
+
87
+ class _VolumeStep(State):
88
+ """One step of the volume - ``held``: the small step of a button that is kept pressed.
89
+
90
+ The machine goes back to ``IDLE`` in any case. Only there it takes the next press: a step that failed (an
91
+ exception while the mixer is set) would otherwise leave the volume buttons without effect until the next start.
92
+ The exception is logged and reported as any unexpected one (:mod:`tinyjukebox.core.unexpected`)."""
93
+
94
+ def __init__(
95
+ self,
96
+ state_id: States,
97
+ logger: logging.Logger,
98
+ volumemanager: VolumeButtons,
99
+ latency: ReactionTimer,
100
+ held: bool,
101
+ ) -> None:
102
+ super().__init__(state_id, logger)
103
+ self._volumemanager = volumemanager
104
+ self._latency = latency
105
+ self._held = held
106
+
107
+ def _step(self) -> None:
108
+ raise NotImplementedError
109
+
110
+ def _on_entry(self) -> Enum | None:
111
+ try:
112
+ self._step()
113
+ except Exception as error:
114
+ self._logger.exception("_on_entry - the volume could not be set")
115
+ unexpected.report(error, f"volume button ({self.id.value})")
116
+ else:
117
+ self._latency.end("volume set")
118
+ return Transitions.TO_IDLE
119
+
120
+
121
+ class VolumeUp(_VolumeStep):
122
+ def __init__(
123
+ self, logger: logging.Logger, volumemanager: VolumeButtons, latency: ReactionTimer, held: bool = False
124
+ ) -> None:
125
+ super().__init__(States.VOLUME_UP_HELD if held else States.VOLUME_UP, logger, volumemanager, latency, held)
126
+
127
+ def _step(self) -> None:
128
+ self._volumemanager.volume_up(self._held)
129
+
130
+
131
+ class VolumeDown(_VolumeStep):
132
+ def __init__(
133
+ self, logger: logging.Logger, volumemanager: VolumeButtons, latency: ReactionTimer, held: bool = False
134
+ ) -> None:
135
+ super().__init__(States.VOLUME_DOWN_HELD if held else States.VOLUME_DOWN, logger, volumemanager, latency, held)
136
+
137
+ def _step(self) -> None:
138
+ self._volumemanager.volume_down(self._held)
139
+
140
+
141
+ class STMVolume:
142
+ """Changes the volume on the volume buttons: one step per press (:data:`~tinyjukebox.messages.BUTTON_PRESSED`), a
143
+ small step per hold event (:data:`~tinyjukebox.messages.BUTTON_HELD` - the input drivers of the volume buttons
144
+ repeat it while the button is kept pressed). The handlers only queue the event - setting the mixer runs in the
145
+ machine's thread, not in the pubsub dispatch thread."""
146
+
147
+ def __init__(
148
+ self,
149
+ pubsub_client: PubSubClient,
150
+ volumemanager: VolumeButtons,
151
+ logger: logging.Logger,
152
+ latency: ReactionTimer | None = None,
153
+ ) -> None:
154
+ """:param latency: measures button pressed → volume set (default: a timer of its own)."""
155
+ self._logger = get_child(logger, type(self).__name__)
156
+ self._pubsub_client = pubsub_client
157
+ self._latency = latency or ReactionTimer("volume button", self._logger)
158
+ self._machine = create_machine(self._logger)
159
+ for state in [
160
+ Init(self._logger, volumemanager),
161
+ Idle(self._logger),
162
+ VolumeUp(self._logger, volumemanager, self._latency),
163
+ VolumeDown(self._logger, volumemanager, self._latency),
164
+ VolumeUp(self._logger, volumemanager, self._latency, held=True),
165
+ VolumeDown(self._logger, volumemanager, self._latency, held=True),
166
+ End(self._logger, volumemanager),
167
+ ]:
168
+ self._machine.addstate(state)
169
+ self._machine.setstartstate(States.INIT)
170
+ self._machine.addtransition(States.INIT, Transitions.START, States.IDLE)
171
+ self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_UP, States.VOLUME_UP)
172
+ self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_DOWN, States.VOLUME_DOWN)
173
+ self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_UP_HELD, States.VOLUME_UP_HELD)
174
+ self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_DOWN_HELD, States.VOLUME_DOWN_HELD)
175
+ changes = (States.VOLUME_UP, States.VOLUME_DOWN, States.VOLUME_UP_HELD, States.VOLUME_DOWN_HELD)
176
+ for state_id in changes:
177
+ self._machine.addtransition(state_id, Transitions.TO_IDLE, States.IDLE)
178
+ for state_id in (States.IDLE, *changes):
179
+ self._machine.addtransition(state_id, Transitions.END, States.END)
180
+
181
+ @property
182
+ def machine(self) -> object:
183
+ """The tantamount machine (diagram export, tests)."""
184
+ return self._machine
185
+
186
+ def dump_to_dot(self, filename: str | Path) -> None:
187
+ write_dot_file(self._machine, filename)
188
+
189
+ def wait_until_idle(self) -> None:
190
+ """Block until all queued button events have been processed (tests)."""
191
+ self._machine.wait_until_idle()
192
+
193
+ def _handle(self, topic: str, message: object) -> None:
194
+ transition = _TRANSITIONS[topic].get(message)
195
+ if transition is None:
196
+ return
197
+ self._logger.info("_handle - received: %s (%s)", message, topic)
198
+ self._latency.begin(self._pubsub_client.published_at())
199
+ self._latency.step("handler")
200
+ self._machine.asyncoperate(transition)
201
+
202
+ def _button_handler(self, message: object) -> None:
203
+ """Handler for :data:`~tinyjukebox.messages.BUTTON_PRESSED` - reacts on the volume buttons only."""
204
+ self._handle(BUTTON_PRESSED, message)
205
+
206
+ def _held_handler(self, message: object) -> None:
207
+ """Handler for :data:`~tinyjukebox.messages.BUTTON_HELD` - reacts on the volume buttons only."""
208
+ self._handle(BUTTON_HELD, message)
209
+
210
+ def start(self) -> None:
211
+ self._machine.start()
212
+ self._machine.operate(Transitions.START)
213
+ self._pubsub_client.subscribe(BUTTON_PRESSED, self._button_handler)
214
+ self._pubsub_client.subscribe(BUTTON_HELD, self._held_handler)
215
+
216
+ def stop(self) -> None:
217
+ self._pubsub_client.unsubscribe(BUTTON_PRESSED, self._button_handler, ignore_not_found=True)
218
+ self._pubsub_client.unsubscribe(BUTTON_HELD, self._held_handler, ignore_not_found=True)
219
+ if self._machine._startdone: # not started if the start of the box failed before
220
+ self._machine.operate(Transitions.END)
221
+ self._machine.stop()
@@ -0,0 +1,42 @@
1
+ """What a box and the library share: the interface between them, files with checksums, the handshake.
2
+
3
+ The design is in docs/developer/library.md. ``data``, ``link``, ``files`` and ``box`` are the side of a box,
4
+ ``tinyjukebox.library`` is the library; ``client``, ``handshake``, ``manifest`` and ``util`` are used by both.
5
+ """
6
+
7
+ import contextlib
8
+ import contextvars
9
+ from collections.abc import Iterator
10
+
11
+ __all__ = ["INTERFACE", "KINDS", "LIBRARY", "SOFTWARE", "acting_as", "actor"]
12
+
13
+ INTERFACE = 2
14
+ """Version of the interface between box and library that this code speaks (a box) or serves (the library). Raised
15
+ only for a change that an older counterpart cannot cope with."""
16
+
17
+ KINDS = ("tag", "target", "position", "setting")
18
+ """What is synchronized entry by entry. Tags and targets are the catalog, the same for all boxes; positions and
19
+ settings belong to one box."""
20
+
21
+ LIBRARY = "library"
22
+ """The side that changed an entry last, if it was not a box: something entered on the page of the library."""
23
+
24
+ SOFTWARE = "software"
25
+ """Who made a change that no person asked for: a position that moved on, a tag created for an unknown uid."""
26
+
27
+ _actor: contextvars.ContextVar[str] = contextvars.ContextVar("tinyjukebox_actor", default=SOFTWARE)
28
+
29
+
30
+ def actor() -> str:
31
+ """Who is making the changes of this thread right now: a user of the web page, the console, or the software."""
32
+ return _actor.get()
33
+
34
+
35
+ @contextlib.contextmanager
36
+ def acting_as(who: str) -> Iterator[None]:
37
+ """Changes made inside are noted as made by ``who``, e.g. ``web:anna`` or ``console``."""
38
+ token = _actor.set(who)
39
+ try:
40
+ yield
41
+ finally:
42
+ _actor.reset(token)