amniotic 1.3.4__tar.gz

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.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: amniotic
3
+ Version: 1.3.4
4
+ Summary: A multi-output ambient sound mixer for Home Assistant
5
+ Home-page: https://github.com/None/amniotic
6
+ Author: Frontmatter
7
+ Author-email: innovative.fowler@mask.pro.fmtr.dev
8
+ License: Copyright © 2025 Frontmatter. All rights reserved.
9
+ Keywords: ambient sound audio white noise masking sleep
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: fmtr.tools[api,av,caching,debug,ha.api,http,logging,mqtt,path.app,sets,tabular,version.dev,yaml]
12
+ Requires-Dist: haco
13
+ Provides-Extra: all
14
+ Dynamic: author
15
+ Dynamic: author-email
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: keywords
20
+ Dynamic: license
21
+ Dynamic: provides-extra
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
24
+
25
+ # Amniotic
26
+
27
+ A multi-output ambient sound mixer for Home Assistant.
28
+
29
+ Amniotic lets you use a single device to create on-the-fly, custom ambient audio mixes - e.g. mixing Waterfall sounds with Birdsong on one media player entity, while playing Fireplace sounds from a second audio device - to suit your tastes and environment.
30
+
31
+ The library integrates with Home Assistant via MQTT as a new device, allowing you to create and control ambient mixes from the Home Assistant interface.
32
+
33
+ ### Why Would I Want Such a Thing?
34
+
35
+ I won't explain the general reasons for introducing non-musical sounds into one's environment, but if you find [sound masking](https://en.wikipedia.org/wiki/Sound_masking) helps you concentrate in noisy environments, if you're (as I am) slightly [misophonic](https://www.webmd.com/mental-health/what-is-misophonia), if you use [white noise machines](https://en.wikipedia.org/wiki/White_noise_machine) to induce sleep or relaxation, or if you just think sound is an important factor in setting a pleasant ambience, then you might find Amniotic useful.
36
+
37
+ ### Can't I do This with Spotify, Volumio, HifiBerry etc.?
38
+
39
+ Since those systems are intended for music, they aren't designed for playing or mixing multiple streams simultaneously with a single device, even if set up in multi-room configurations. Also, the streaming services often won't allow a single account to play multiple streams, even
40
+ _if_ multiple devices are used.
41
+
42
+ Anyway, those limitations motivated this library.
43
+
44
+ There are two ways to install and run Amniotic:
45
+
46
+ - On the Home Assistant machine itself, as an add-on.
47
+ - Install manually, on a separate machine.
48
+
49
+ ## Home Assistant Addon
50
+
51
+ To add as an Addon, click here:
52
+
53
+ [![Open your Home Assistant instance and show the add add-on repository dialog with the repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://fmtr.link/amniotic/addon-install)
54
+
55
+ ## Dashboard
56
+
57
+ [Lovelace Dashboard](https://fmtr.link/amniotic/doc/dashboard)
58
+
59
+ ![Dashboard Screenshot](https://fmtr.link/amniotic/doc/dashboard/screenshot)
60
+
61
+ ## Getting Started
62
+
63
+ Better documentation is coming soon. Currently, the easiest workflow is the following:
64
+
65
+ - Install as an Addon, using the button above.
66
+ - If you're using a non-default address for Home Assistant on your network (i.e. not
67
+ `homeassistant.local`), set that in the Addon configuration.
68
+ - Add some audio files to the Home Assistant `/media/Amniotic` directory.
69
+
70
+ #### Adding the Dashboard to Lovelace
71
+
72
+ - Find the [Lovelace Dashboard View here](https://fmtr.link/amniotic/doc/dashboard).
73
+ - Click on the Copy icon to "Copy raw file"
74
+ - In your Home Assistant UI, navigate to a dashboard you'd like to add the View to.
75
+ - Click on the Edit Dashboard icon.
76
+ - Click the `+` to add a new View.
77
+ - Click on the three dots in the top right corner of the new View editor, and select `Edit in YAML`.
78
+ - Overwrite the _entire_ edit box with the contents of your clipboard.
79
+ - Click `Save`.
80
+ - Click `Done` in the dashboard editor.
81
+
82
+ #### Using the Dashboard
83
+ - Select a Recording from the dropdown.
84
+ - Toggle to Enable the Recording.
85
+ - Select a Media Player to stream the Theme to. Note: Your media player needs to support streaming from a basic HTTP stream, which most should.
86
+ - Click Stream to Media Player.
87
+ - Your Theme should start playing on your Media Player.
88
+ - You can now Enable additional Recordings in the Theme, modify their volume, etc., to create a custom mix.
89
+ - Note: there's also a Current Theme URL, for if you want to manually paste stream to a non-HA player, like a phone or something.
90
+
91
+ ## Do I need a fancy Sonos-type Speaker? Can't I just use a Raspberry Pi, etc?
92
+
93
+ - You can use basically any device with audio hardware. You just need to allow Home Assistant to see it as a Media Player entity.
94
+ - In Home Assistant, install the [VLC Telnet integration](https://www.home-assistant.io/integrations/vlc_telnet).
95
+ - On your Device, install VLC, and start it in telnet mode, e.g.
96
+ `vlc -I telnet --telnet-password password --telnet-host 0.0.0.0:4212`
97
+ - Add your device to the VLC Telnet integration.
@@ -0,0 +1,73 @@
1
+ # Amniotic
2
+
3
+ A multi-output ambient sound mixer for Home Assistant.
4
+
5
+ Amniotic lets you use a single device to create on-the-fly, custom ambient audio mixes - e.g. mixing Waterfall sounds with Birdsong on one media player entity, while playing Fireplace sounds from a second audio device - to suit your tastes and environment.
6
+
7
+ The library integrates with Home Assistant via MQTT as a new device, allowing you to create and control ambient mixes from the Home Assistant interface.
8
+
9
+ ### Why Would I Want Such a Thing?
10
+
11
+ I won't explain the general reasons for introducing non-musical sounds into one's environment, but if you find [sound masking](https://en.wikipedia.org/wiki/Sound_masking) helps you concentrate in noisy environments, if you're (as I am) slightly [misophonic](https://www.webmd.com/mental-health/what-is-misophonia), if you use [white noise machines](https://en.wikipedia.org/wiki/White_noise_machine) to induce sleep or relaxation, or if you just think sound is an important factor in setting a pleasant ambience, then you might find Amniotic useful.
12
+
13
+ ### Can't I do This with Spotify, Volumio, HifiBerry etc.?
14
+
15
+ Since those systems are intended for music, they aren't designed for playing or mixing multiple streams simultaneously with a single device, even if set up in multi-room configurations. Also, the streaming services often won't allow a single account to play multiple streams, even
16
+ _if_ multiple devices are used.
17
+
18
+ Anyway, those limitations motivated this library.
19
+
20
+ There are two ways to install and run Amniotic:
21
+
22
+ - On the Home Assistant machine itself, as an add-on.
23
+ - Install manually, on a separate machine.
24
+
25
+ ## Home Assistant Addon
26
+
27
+ To add as an Addon, click here:
28
+
29
+ [![Open your Home Assistant instance and show the add add-on repository dialog with the repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://fmtr.link/amniotic/addon-install)
30
+
31
+ ## Dashboard
32
+
33
+ [Lovelace Dashboard](https://fmtr.link/amniotic/doc/dashboard)
34
+
35
+ ![Dashboard Screenshot](https://fmtr.link/amniotic/doc/dashboard/screenshot)
36
+
37
+ ## Getting Started
38
+
39
+ Better documentation is coming soon. Currently, the easiest workflow is the following:
40
+
41
+ - Install as an Addon, using the button above.
42
+ - If you're using a non-default address for Home Assistant on your network (i.e. not
43
+ `homeassistant.local`), set that in the Addon configuration.
44
+ - Add some audio files to the Home Assistant `/media/Amniotic` directory.
45
+
46
+ #### Adding the Dashboard to Lovelace
47
+
48
+ - Find the [Lovelace Dashboard View here](https://fmtr.link/amniotic/doc/dashboard).
49
+ - Click on the Copy icon to "Copy raw file"
50
+ - In your Home Assistant UI, navigate to a dashboard you'd like to add the View to.
51
+ - Click on the Edit Dashboard icon.
52
+ - Click the `+` to add a new View.
53
+ - Click on the three dots in the top right corner of the new View editor, and select `Edit in YAML`.
54
+ - Overwrite the _entire_ edit box with the contents of your clipboard.
55
+ - Click `Save`.
56
+ - Click `Done` in the dashboard editor.
57
+
58
+ #### Using the Dashboard
59
+ - Select a Recording from the dropdown.
60
+ - Toggle to Enable the Recording.
61
+ - Select a Media Player to stream the Theme to. Note: Your media player needs to support streaming from a basic HTTP stream, which most should.
62
+ - Click Stream to Media Player.
63
+ - Your Theme should start playing on your Media Player.
64
+ - You can now Enable additional Recordings in the Theme, modify their volume, etc., to create a custom mix.
65
+ - Note: there's also a Current Theme URL, for if you want to manually paste stream to a non-HA player, like a phone or something.
66
+
67
+ ## Do I need a fancy Sonos-type Speaker? Can't I just use a Raspberry Pi, etc?
68
+
69
+ - You can use basically any device with audio hardware. You just need to allow Home Assistant to see it as a Media Player entity.
70
+ - In Home Assistant, install the [VLC Telnet integration](https://www.home-assistant.io/integrations/vlc_telnet).
71
+ - On your Device, install VLC, and start it in telnet mode, e.g.
72
+ `vlc -I telnet --telnet-password password --telnet-host 0.0.0.0:4212`
73
+ - Add your device to the VLC Telnet integration.
File without changes
@@ -0,0 +1,48 @@
1
+ import logging
2
+ from fastapi.responses import StreamingResponse
3
+ from starlette.requests import Request
4
+
5
+ from amniotic.obs import logger
6
+ from amniotic.theme import ThemeDefinition
7
+ from amniotic.version import __version__
8
+ from fmtr.tools import api, mqtt
9
+
10
+ for name in ["uvicorn.access", "uvicorn.error", "uvicorn"]:
11
+ lgr = logging.getLogger(name)
12
+ lgr.handlers.clear()
13
+ lgr.propagate = False
14
+
15
+
16
+
17
+ class ApiAmniotic(api.Base):
18
+ TITLE = f'Amniotic {__version__} Streaming API'
19
+ URL_DOCS = '/'
20
+
21
+ def __init__(self, client: mqtt.Client):
22
+ super().__init__()
23
+
24
+ self.client = client
25
+
26
+ def get_endpoints(self):
27
+ endpoints = [
28
+ api.Endpoint(method_http=self.app.get, path='/stream/{id}', method=self.stream),
29
+
30
+ ]
31
+
32
+ return endpoints
33
+
34
+ async def stream(self, id: str, request: Request):
35
+ logger.info(f'Got streaming audio request {id=} {request.client=}')
36
+ theme_def: ThemeDefinition = self.client.device.themes.id[id]
37
+ stream = theme_def.get_stream()
38
+
39
+ if not stream.is_enabled:
40
+ logger.warning(f'Theme "{theme_def.name}" is streaming, but it has no recordings enabled. The stream will be silent. Enable some recordings to hear output.')
41
+
42
+ response = StreamingResponse(stream, media_type="audio/mpeg")
43
+ return response
44
+
45
+
46
+
47
+ if __name__ == '__main__':
48
+ ApiAmniotic.launch()
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+
3
+ from amniotic.api import ApiAmniotic
4
+ from amniotic.device import Amniotic
5
+ from amniotic.obs import logger
6
+ from fmtr.tools import http
7
+ from haco.client import ClientHaco
8
+
9
+
10
+ class ClientAmniotic(ClientHaco):
11
+ """
12
+ Take an extra API argument, and gather with super.start
13
+ """
14
+
15
+ API_CLASS = ApiAmniotic
16
+
17
+ def __init__(self, device: Amniotic, *args, **kwargs):
18
+ super().__init__(device=device, *args, **kwargs)
19
+
20
+ @logger.instrument('Connecting MQTT client to {self._client.username}@{self._hostname}:{self._port}...')
21
+ async def start(self):
22
+ await asyncio.gather(
23
+ super().start(),
24
+ self.API_CLASS.launch_async(self)
25
+ )
26
+
27
+ @classmethod
28
+ @logger.instrument('Instantiating MQTT client from Supervisor API...')
29
+ def from_supervisor(cls, device: Amniotic, **kwargs):
30
+ from amniotic.settings import settings
31
+
32
+ with http.Client() as client:
33
+ response = client.get(
34
+ f"{settings.ha_supervisor_api}/services/mqtt",
35
+ headers={
36
+ "Authorization": f"Bearer {settings.token}",
37
+ "Content-Type": "application/json",
38
+ },
39
+ )
40
+
41
+ data = response.json().get("data", {})
42
+
43
+ self = cls(device=device, hostname=data['host'], port=data['port'], username=data['username'], password=data['password'], **kwargs)
44
+ return self
@@ -0,0 +1,260 @@
1
+ from dataclasses import dataclass
2
+
3
+ from amniotic.obs import logger
4
+ from amniotic.recording import RecordingThemeInstance
5
+ from amniotic.theme import ThemeDefinition
6
+ from fmtr.tools import http
7
+ from haco.button import Button
8
+ from haco.control import Control
9
+ from haco.number import Number
10
+ from haco.select import Select
11
+ from haco.sensor import Sensor
12
+ from haco.switch import Switch
13
+ from haco.text import Text
14
+
15
+
16
+ @dataclass(kw_only=True)
17
+ class ThemeRelativeControl(Control):
18
+
19
+ @property
20
+ def themes(self):
21
+ return self.device.themes
22
+
23
+ @property
24
+ def theme(self):
25
+ return self.themes.current
26
+
27
+ @property
28
+ def instances(self):
29
+ return self.theme.instances
30
+
31
+ @property
32
+ def instance(self):
33
+ return self.instances.current
34
+
35
+
36
+ @dataclass(kw_only=True)
37
+ class SelectTheme(Select, ThemeRelativeControl):
38
+ icon: str = 'access-point'
39
+ name: str = 'Theme'
40
+
41
+ @logger.instrument('Setting Theme to "{value}"...')
42
+ async def command(self, value):
43
+ theme = self.themes.name[value]
44
+ self.themes.current = theme
45
+ return value
46
+
47
+
48
+ async def state(self, value=None):
49
+
50
+ if self.theme not in self.themes:
51
+ if self.themes:
52
+ self.themes.current = next(iter(self.themes))
53
+ else:
54
+ logger.warning('No themes exist. Creating default...')
55
+ theme = ThemeDefinition(amniotic=self.device, name="Default")
56
+ self.themes.append(theme)
57
+ self.themes.current = theme
58
+ self.themes.save()
59
+
60
+ options = sorted(self.themes.name.keys())
61
+ if self.options != options:
62
+ self.options = options
63
+ await self.announce()
64
+
65
+ name = self.theme.name
66
+
67
+ await self.device.select_recording.state()
68
+ await self.device.sns_url.state()
69
+ return name
70
+
71
+ @dataclass(kw_only=True)
72
+ class SelectRecording(Select, ThemeRelativeControl):
73
+ icon: str = 'waveform'
74
+ name: str = 'Recording'
75
+
76
+ @logger.instrument('Setting Theme "{self.theme.name}" current recording instance to "{value}"...')
77
+ async def command(self, value):
78
+
79
+ instance = self.instances.name.get(value)
80
+ if not instance:
81
+ logger.info(f'Creating new recording instance "{value}" for Theme "{self.theme.name}"...')
82
+ meta = self.device.metas.name[value]
83
+ instance = RecordingThemeInstance(path=meta.path_str, device=self.device)
84
+ self.instances.append(instance)
85
+
86
+ self.instances.current = instance
87
+ return value
88
+
89
+ async def state(self, value=None):
90
+
91
+ options = sorted(self.device.metas.name.keys())
92
+ if self.options != options:
93
+ self.options = options
94
+ await self.announce()
95
+
96
+ if not self.instance:
97
+ return None
98
+
99
+ await self.device.swt_play.state()
100
+ await self.device.nbr_volume.state()
101
+ return self.instance.name
102
+
103
+
104
+
105
+ @dataclass(kw_only=True)
106
+ class EnableRecording(Switch, ThemeRelativeControl):
107
+ icon: str = 'playlist-plus'
108
+ name: str = 'Enable Recording'
109
+
110
+ @logger.instrument('Toggling {value=} recording instance "{self.instances.current.name}" for Theme "{self.theme.name}"...')
111
+ async def command(self, value):
112
+
113
+ if not self.instance:
114
+ return None
115
+
116
+ self.instance.is_enabled = value
117
+ self.themes.save()
118
+
119
+
120
+ async def state(self, value=None):
121
+
122
+ if not self.instance:
123
+ return None
124
+
125
+ return self.instance.is_enabled
126
+
127
+
128
+
129
+ @dataclass(kw_only=True)
130
+ class NumberVolume(Number, ThemeRelativeControl):
131
+ icon: str = 'volume-medium'
132
+ name: str = 'Recording Volume'
133
+
134
+ @logger.instrument('Setting volume to {value} for recording instance "{self.instances.current.name}" for Theme "{self.theme.name}"...')
135
+ async def command(self, value):
136
+
137
+ if not self.instance:
138
+ return None
139
+
140
+ self.instance.volume = value / 100
141
+ self.themes.save()
142
+
143
+
144
+ async def state(self, value=None):
145
+
146
+ if not self.instance:
147
+ return None
148
+
149
+
150
+ return int(self.instance.volume * 100)
151
+
152
+
153
+
154
+ @dataclass(kw_only=True)
155
+ class SelectMediaPlayer(Select, ThemeRelativeControl):
156
+ icon: str = 'cast-audio'
157
+ name: str = 'Media Player'
158
+
159
+ @logger.instrument('Selecting Media Player "{value}" for Theme "{self.theme.name}"...')
160
+ async def command(self, value):
161
+ state = self.device.media_player_states.friendly_name[value]
162
+ self.device.media_player_states.current = state
163
+ return value
164
+
165
+ async def state(self, value):
166
+ player = self.device.media_player_states.current
167
+ if player:
168
+ return player.friendly_name
169
+ return None
170
+
171
+
172
+ @dataclass(kw_only=True)
173
+ class StreamURL(Sensor, ThemeRelativeControl):
174
+ icon: str = 'link-variant'
175
+ name: str = 'Stream URL'
176
+
177
+ async def state(self, value=None):
178
+ return self.theme.url
179
+
180
+
181
+ @dataclass(kw_only=True)
182
+ class PlayStreamButton(Button, ThemeRelativeControl):
183
+ icon: str = 'play-network'
184
+ name: str = 'Stream'
185
+
186
+ @property
187
+ def url_api(self):
188
+ from amniotic.settings import settings
189
+
190
+ return f"{settings.ha_core_api}/services/media_player/play_media"
191
+
192
+
193
+ async def command(self, value):
194
+
195
+ if not self.instance:
196
+ return None
197
+
198
+ state = self.device.media_player_states.current
199
+
200
+ if not state:
201
+ return
202
+
203
+ with logger.span(f'Posting request to HA API {self.url_api} {state.entity_id=} {self.theme.url=}') as span:
204
+ try:
205
+ await self.post(state)
206
+ except Exception as exception:
207
+ logger.error(f'Error posting to HA API: {repr(exception)}.')
208
+ span.record_exception(exception=exception)
209
+
210
+ async def post(self, state):
211
+ from amniotic.settings import settings
212
+
213
+ response = http.client.post(
214
+ self.url_api,
215
+ headers={
216
+ "Authorization": f"Bearer {settings.token}",
217
+ "Content-Type": "application/json",
218
+ },
219
+ json={
220
+ "entity_id": state.entity_id,
221
+ "media_content_id": self.theme.url,
222
+ "media_content_type": "music",
223
+ }
224
+ )
225
+
226
+ response.raise_for_status()
227
+
228
+
229
+ @dataclass(kw_only=True)
230
+ class NewTheme(Text, ThemeRelativeControl):
231
+ icon: str = 'access-point-plus'
232
+ name: str = 'New Theme'
233
+
234
+ @logger.instrument('Creating new Theme "{value}"...')
235
+ async def command(self, value):
236
+ if not value:
237
+ return
238
+
239
+ theme = ThemeDefinition(amniotic=self.device, name=value)
240
+ self.themes.append(theme)
241
+ self.themes.current = theme
242
+ self.themes.save()
243
+
244
+ await self.device.select_theme.state()
245
+
246
+ async def state(self, value=None):
247
+ return 'New'
248
+
249
+
250
+ @dataclass(kw_only=True)
251
+ class DeleteTheme(Button, ThemeRelativeControl):
252
+ icon: str = 'access-point-remove'
253
+ name: str = 'Delete Current Theme'
254
+
255
+ @logger.instrument('Deleting Theme "{self.theme.name}"...')
256
+ async def command(self, value):
257
+ self.themes.remove(self.theme)
258
+ self.themes.save()
259
+ await self.device.select_theme.state()
260
+ return value
@@ -0,0 +1,144 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+ from dataclasses import field, fields
4
+ from functools import cached_property
5
+ from typing import Self
6
+
7
+ import homeassistant_api
8
+
9
+ from amniotic.controls import SelectTheme, SelectRecording, EnableRecording, NumberVolume, SelectMediaPlayer, PlayStreamButton, StreamURL, NewTheme, DeleteTheme
10
+ from amniotic.obs import logger
11
+ from amniotic.recording import RecordingMetadata
12
+ from amniotic.theme import ThemeDefinition, IndexThemes
13
+ from fmtr.tools import Path
14
+ from fmtr.tools.iterator_tools import IndexList, IterDiffer
15
+ from haco.device import Device
16
+
17
+
18
+ @dataclass
19
+ class MediaState:
20
+ entity_id: str
21
+ state: str
22
+ friendly_name: str | None = None
23
+ supported_features: int | None = None
24
+
25
+ def __post_init__(self):
26
+ self.friendly_name = self.friendly_name or self.entity_id
27
+
28
+ @classmethod
29
+ def from_state(cls, state) -> Self:
30
+ data = state.model_dump()
31
+ data |= data.pop('attributes')
32
+ allowed = {f.name for f in fields(cls)}
33
+ filtered = {k: v for k, v in data.items() if k in allowed}
34
+ self = cls(**filtered)
35
+ return self
36
+
37
+
38
+ @dataclass(kw_only=True)
39
+ class Amniotic(Device):
40
+ themes: IndexList[ThemeDefinition] = field(default_factory=IndexList, metadata=dict(exclude=True))
41
+ metas: IndexList[RecordingMetadata] = field(default_factory=IndexList, metadata=dict(exclude=True))
42
+
43
+ client_ha: homeassistant_api.Client | None = field(default=None, metadata=dict(exclude=True))
44
+
45
+ path_audio: Path = field(metadata=dict(exclude=True))
46
+ path_audio_schedule_duration: int = field(default=10, metadata=dict(exclude=True))
47
+
48
+ path_audio_schedule_task: asyncio.Task | None = field(default=None, metadata=dict(exclude=True))
49
+
50
+ def __post_init__(self):
51
+ if not self.path_audio.exists():
52
+ logger.warning(f'Audio path "{self.path_audio}" does not exist. Will be created.')
53
+ self.path_audio.mkdir()
54
+ self.metas = IndexList()
55
+ self.refresh_metas()
56
+
57
+ if not self.metas:
58
+ logger.warning(f'No audio files found in "{self.path_audio}". You will need to add some before you can stream anything.')
59
+
60
+ self.themes = IndexThemes.load(self)
61
+
62
+
63
+ media_players_data = [state for state in self.client_ha.get_states() if state.entity_id.startswith("media_player.")]
64
+ self.media_player_states = IndexList(MediaState.from_state(data) for data in media_players_data)
65
+
66
+ self.controls = [
67
+ self.select_theme,
68
+ self.select_recording,
69
+ self.btn_delete_theme,
70
+ self.txt_new_theme,
71
+ self.swt_play,
72
+ self.nbr_volume,
73
+ self.select_media_player,
74
+ self.btn_play,
75
+ self.sns_url
76
+ ]
77
+
78
+
79
+ @cached_property
80
+ def select_theme(self):
81
+ return SelectTheme(options=[str(defin.name) for defin in self.themes])
82
+
83
+ @cached_property
84
+ def select_recording(self):
85
+ return SelectRecording(options=sorted(self.metas.name.keys()))
86
+
87
+ @cached_property
88
+ def select_media_player(self):
89
+ return SelectMediaPlayer(options=list(self.media_player_states.friendly_name.keys()))
90
+
91
+ @cached_property
92
+ def swt_play(self):
93
+ return EnableRecording()
94
+
95
+ @cached_property
96
+ def btn_play(self):
97
+ return PlayStreamButton()
98
+
99
+ @cached_property
100
+ def sns_url(self):
101
+ return StreamURL()
102
+
103
+ @cached_property
104
+ def nbr_volume(self):
105
+ return NumberVolume()
106
+
107
+ @cached_property
108
+ def txt_new_theme(self):
109
+ return NewTheme()
110
+
111
+ @cached_property
112
+ def btn_delete_theme(self):
113
+ return DeleteTheme()
114
+
115
+ def refresh_metas(self) -> bool:
116
+
117
+ paths_existing = self.metas.path.keys()
118
+ paths_disk = {path for path in self.path_audio.iterdir() if path.is_file()}
119
+
120
+ diff = IterDiffer(paths_existing, paths_disk)
121
+
122
+ for path in diff.added:
123
+ logger.info(f'Adding new recording: "{path}"...')
124
+ meta = RecordingMetadata(path)
125
+ self.metas.append(meta)
126
+ if not self.metas.current:
127
+ self.metas.current = meta
128
+
129
+ return diff.is_changed
130
+
131
+ @logger.instrument('Starting audio file monitoring task. Duration: {self.path_audio_schedule_duration}. Directory: "{self.path_audio}"...')
132
+ async def refresh_metas_task(self):
133
+ while True:
134
+ await asyncio.sleep(self.path_audio_schedule_duration)
135
+
136
+ logger.debug(f'Syncing recordings in "{self.path_audio}"...')
137
+ is_changed = self.refresh_metas()
138
+ if is_changed:
139
+ await self.select_recording.state()
140
+
141
+ async def initialise(self):
142
+ await super().initialise()
143
+ if not self.path_audio_schedule_task:
144
+ self.path_audio_schedule_task = asyncio.create_task(self.refresh_metas_task())
@@ -0,0 +1,7 @@
1
+ def main():
2
+ from amniotic.settings import settings
3
+ return settings.run()
4
+
5
+
6
+ if __name__ == '__main__':
7
+ main()
@@ -0,0 +1,15 @@
1
+ import logging as logging_native
2
+
3
+ from amniotic.paths import paths
4
+ from amniotic.version import __version__
5
+ from fmtr.tools import logging, debug, Constants
6
+ from fmtr.tools.environment_tools import get_bool
7
+
8
+ debug.trace()
9
+
10
+ logger = logging.get_logger(
11
+ name=paths.name_ns,
12
+ stream=Constants.DEVELOPMENT,
13
+ version=__version__,
14
+ level=logging_native.DEBUG if get_bool(Constants.FMTR_DEV_KEY, default=False) else logging_native.INFO # todo: fix runtime environment for addons
15
+ )