18 Commits

Author SHA1 Message Date
3036cdff2f upd banana badge 2023-07-01 19:26:56 +01:00
b02f3af665 add recorder, recorder.mode tests 2023-07-01 19:26:32 +01:00
145f85b7cd rename ARMSTRIPMIXIN_cls to ARMCHANNELMIXIN_cls 2023-07-01 18:09:31 +01:00
71f77b7830 md fix 2023-07-01 00:05:01 +01:00
4415851816 Recorder.Mode section added
new recorder properties and methods added
2023-06-30 23:53:08 +01:00
8b63cbfe8d add 2.1.0 section to readme 2023-06-30 23:51:43 +01:00
de4ce850eb add recorder.loop forwarder methods
add RecorderArmChannel class.

add logger warning if channel value not from 1 to 8
2023-06-30 23:51:20 +01:00
ee3fa0a372 adds more properties and methods to Recorder class
rename _make_armstrip_mixin to _make_armchannel_mixin
2023-06-30 19:00:27 +01:00
f92bb1e457 adds RecorderMode
RecorderArmStrip and RecorderArmBus
to Recorder class.

also adds a few properties, gain, channel, bitresolution.
2023-06-30 01:22:30 +01:00
5b99f8aae3 patch bump 2023-06-29 18:05:20 +01:00
59624ccb3e add VmGUI class to misc.
lets you check if gui was launched by the api
2023-06-29 18:05:07 +01:00
b2005030f2 bind double click event to slider 2023-06-28 13:57:45 +01:00
88a5686f27 upd strip comp, gate sections in readme 2023-06-25 13:47:21 +01:00
d0877dbdfd bump tested against versions 2023-06-25 11:14:22 +01:00
ce9a86de79 patch bump 2023-06-25 11:00:56 +01:00
58dba331a7 fix polling parameters in readme. 2023-06-25 11:00:32 +01:00
77003940f2 fix bus number in levels example 2023-06-25 10:59:35 +01:00
d794bd4b78 clears deprecation warning 2023-06-25 10:58:45 +01:00
12 changed files with 402 additions and 58 deletions

View File

@@ -11,6 +11,19 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x]
## [2.1.0] - 2023-07-01
### Added
- RecorderMode added to Recorder class. See Recorder section in README for new properties and methods.
- recorder.loop is now a forwarder method for recorder.mode.loop for backwards compatibility
- RecorderArmStrip, RecorderArmBus mixed into Recorder class.
### Removed
- Recorder.loop removed from documentation
## [2.0.0] - 2023-06-25
Where possible I've attempted to make the changes backwards compatible. The breaking changes affect two higher classes, Strip and Bus, as well as the behaviour of events. All other changes are additive or QOL aimed at giving more options to the developer. For example, every low-level CAPI call is now logged and error raised on Exception, you can now register callback functions as well as observer classes, extra examples to demonstrate different use cases etc.

View File

@@ -14,9 +14,9 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Tested against
- Basic 1.0.8.4
- Banana 2.0.6.4
- Potato 3.0.2.4
- Basic 1.0.8.8
- Banana 2.0.6.8
- Potato 3.0.2.8
## Requirements
@@ -155,6 +155,8 @@ vm.strip[5].appgain("Spotify", 0.5)
#### Strip.Comp
The following properties are available.
- `knob`: float, from 0.0 to 10.0
- `gainin`: float, from -24.0 to 24.0
- `ratio`: float, from 1.0 to 8.0
@@ -171,10 +173,14 @@ example:
print(vm.strip[4].comp.knob)
```
Strip Comp parameters are defined for PhysicalStrips, potato version only.
Strip Comp parameters are defined for PhysicalStrips.
`knob` defined for all versions, all other parameters potato only.
#### Strip.Gate
The following properties are available.
- `knob`: float, from 0.0 to 10.0
- `threshold`: float, from -60.0 to -10.0
- `damping`: float, from -60.0 to -10.0
@@ -189,10 +195,14 @@ example:
vm.strip[2].gate.attack = 300.8
```
Strip Gate parameters are defined for PhysicalStrips, potato version only.
Strip Gate parameters are defined for PhysicalStrips.
`knob` defined for all versions, all other parameters potato only.
#### Strip.Denoiser
The following properties are available.
- `knob`: float, from 0.0 to 10.0
example:
@@ -388,13 +398,19 @@ The following methods are available
- `record()`
- `ff()`
- `rew()`
- `load(<filepath>)`: string
- `load(filepath)`: raw string
- `goto(time_string)`: time string in format `hh:mm:ss`
- `filetype(filetype)`: string, ("wav", "aiff", "bwf", "mp3")
The following properties are available
- `loop`: boolean
- `A1 - A5`: boolean
- `B1 - A3`: boolean
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
- `bitresolution`: int, (8, 16, 24, 32)
- `channel`: int, from 1 to 8
- `kbps`: int, (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
- `gain`: float, from -60.0 to 12.0
example:
@@ -402,17 +418,46 @@ example:
vm.recorder.play()
vm.recorder.stop()
# Enable loop play
vm.recorder.loop = True
# Disable recorder out channel B2
vm.recorder.B2 = False
# filepath as raw string
vm.recorder.load(r'C:\music\mytune.mp3')
# set the goto time to 1m 30s
vm.recorder.goto("00:01:30")
```
Recorder properties are defined as write only.
#### Recorder.Mode
The following properties are available
- `recbus`: boolean
- `playonload`: boolean
- `loop`: boolean
- `multitrack`: boolean
example:
```python
# Enable loop play
vm.recorder.mode.loop = True
```
#### Recorder.ArmStrip[i]|ArmBus[i]
The following method is available
- `set(val)`: boolean
example:
```python
# Arm strip 3
vm.recorder.armstrip[3].set(True)
# Arm bus 0
vm.recorder.armbus[0].set(True)
```
### VBAN
@@ -654,7 +699,7 @@ vm.vban.outstream[0].apply(on: True, name: 'streamname', bit: 24)
## Config Files
`vm.apply_config(<configname>)`
`vm.apply_config(configname)`
You may load config files in TOML format.
Three example configs have been included with the package. Remember to save
@@ -755,12 +800,6 @@ Access to lower level Getters and Setters are provided with these functions:
- `vm.get(param, is_string=False)`: For getting the value of any parameter. Set string to True if getting a property value expected to return a string.
- `vm.set(param, value)`: For setting the value of any parameter.
Access to lower level polling functions are provided with these functions:
- `vm.pdirty()`: Returns True if a parameter has been updated.
- `vm.mdirty()`: Returns True if a macrobutton has been updated.
- `vm.ldirty()`: Returns True if a level has been updated.
example:
```python
@@ -769,6 +808,21 @@ vm.set('Strip[4].Label', 'stripname')
vm.set('Strip[0].Gain', -3.6)
```
Access to lower level polling functions are provided with the following property objects:
#### `vm.pdirty`
True iff a parameter has been updated.
#### `vm.mdirty`
True iff a macrobutton has been updated.
#### `vm.ldirty`
True iff a level has been updated.
### Run tests
To run all tests:

View File

@@ -8,6 +8,8 @@ from tkinter import ttk
class App(tk.Tk):
INDEX = 3
def __init__(self, vm):
super().__init__()
self.vm = vm
@@ -15,8 +17,8 @@ class App(tk.Tk):
self.vm.observer.add(self.on_ldirty)
# create widget variables
self.button_var = tk.BooleanVar(value=vm.strip[3].mute)
self.slider_var = tk.DoubleVar(value=vm.strip[3].gain)
self.button_var = tk.BooleanVar(value=vm.strip[self.INDEX].mute)
self.slider_var = tk.DoubleVar(value=vm.strip[self.INDEX].gain)
self.meter_var = tk.DoubleVar(value=self._get_level())
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
@@ -24,14 +26,15 @@ class App(tk.Tk):
self.style = ttk.Style()
self.style.theme_use("clam")
self.style.configure(
"Mute.TButton", foreground="#cd5c5c" if vm.strip[3].mute else "#5a5a5a"
"Mute.TButton",
foreground="#cd5c5c" if vm.strip[self.INDEX].mute else "#5a5a5a",
)
# create labelframe and grid it onto the mainframe
self.labelframe = tk.LabelFrame(text=self.vm.strip[3].label)
self.labelframe = tk.LabelFrame(self, text=self.vm.strip[self.INDEX].label)
self.labelframe.grid(padx=1)
# create slider and grid it
# create slider and grid it onto the labelframe
slider = ttk.Scale(
self.labelframe,
from_=12,
@@ -44,6 +47,7 @@ class App(tk.Tk):
column=0,
row=0,
)
slider.bind("<Double-Button-1>", self.on_button_double_click)
# create level meter and grid it onto the labelframe
level_meter = ttk.Progressbar(
@@ -72,18 +76,23 @@ class App(tk.Tk):
def on_slider_move(self, *args):
val = round(self.slider_var.get(), 1)
self.vm.strip[3].gain = val
self.vm.strip[self.INDEX].gain = val
self.gainlabel_var.set(val)
def on_button_press(self):
self.button_var.set(not self.button_var.get())
self.vm.strip[3].mute = self.button_var.get()
self.vm.strip[self.INDEX].mute = self.button_var.get()
self.style.configure(
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
)
def on_button_double_click(self, e):
self.slider_var.set(0)
self.gainlabel_var.set(0)
self.vm.strip[self.INDEX].gain = 0
def _get_level(self):
val = max(self.vm.strip[3].levels.postfader)
val = max(self.vm.strip[self.INDEX].levels.postfader)
return 0 if self.button_var.get() else 72 + val - 12
def on_ldirty(self):

View File

@@ -16,7 +16,7 @@ def main():
"\n".join(
[
f"{vm.strip[5]}: {vm.strip[5].levels.postmute}",
f"{vm.bus[1]}: {vm.bus[0].levels.all}",
f"{vm.bus[0]}: {vm.bus[0].levels.all}",
]
)
)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "voicemeeter-api"
version = "2.0.0"
version = "2.0.2"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"

View File

@@ -6,36 +6,50 @@ import voicemeeterlib
from voicemeeterlib.kinds import KindId
from voicemeeterlib.kinds import request_kind_map as kindmap
# let's keep things random
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
vm = voicemeeterlib.api(KIND_ID)
kind = kindmap(KIND_ID)
@dataclass
class Data:
"""bounds data to map tests to a kind"""
name: str = kind.name
phys_in: int = kind.ins[0] - 1
virt_in: int = kind.ins[0] + kind.ins[-1] - 1
phys_out: int = kind.outs[0] - 1
virt_out: int = kind.outs[0] + kind.outs[-1] - 1
vban_in: int = kind.vban[0] - 1
vban_out: int = kind.vban[-1] - 1
button_lower: int = 0
button_upper: int = 79
asio_in: int = kind.asio[0] - 1
asio_out: int = kind.asio[-1] - 1
insert_lower: int = 0
insert_higher: int = kind.insert - 1
name: str
phys_in: int
virt_in: int
phys_out: int
virt_out: int
vban_in: int
vban_out: int
button_lower: int
button_upper: int
asio_in: int
asio_out: int
insert_lower: int
insert_higher: int
@property
def channels(self):
return (2 * self.phys_in) + (8 * self.virt_in)
data = Data()
# let's keep things random
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
vm = voicemeeterlib.api(KIND_ID)
kind = kindmap(KIND_ID)
data = Data(
name=kind.name,
phys_in=kind.ins[0] - 1,
virt_in=kind.ins[0] + kind.ins[-1] - 1,
phys_out=kind.outs[0] - 1,
virt_out=kind.outs[0] + kind.outs[-1] - 1,
vban_in=kind.vban[0] - 1,
vban_out=kind.vban[-1] - 1,
button_lower=0,
button_upper=79,
asio_in=kind.asio[0] - 1,
asio_out=kind.asio[-1] - 1,
insert_lower=0,
insert_higher=kind.insert - 1,
)
def setup_module():

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 139"><title>tests: 139</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">139</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">139</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 155"><title>tests: 155</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">155</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">155</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -156,6 +156,7 @@ class TestSetAndGetBoolHigher:
[("A1"), ("B2")],
)
def test_it_sets_and_gets_recorder_bool_params(self, param, value):
assert hasattr(vm.recorder, param)
setattr(vm.recorder, param, value)
assert getattr(vm.recorder, param) == value
@@ -168,7 +169,56 @@ class TestSetAndGetBoolHigher:
[("loop")],
)
def test_it_sets_recorder_bool_params(self, param, value):
assert hasattr(vm.recorder, param)
setattr(vm.recorder, param, value)
assert getattr(vm.recorder, param) == value
""" recoder.mode tests """
@pytest.mark.skipif(
data.name == "basic",
reason="Skip test if kind is basic",
)
@pytest.mark.parametrize(
"param",
[("loop"), ("recbus")],
)
def test_it_sets_recorder_mode_bool_params(self, param, value):
assert hasattr(vm.recorder.mode, param)
setattr(vm.recorder.mode, param, value)
assert getattr(vm.recorder.mode, param) == value
""" recorder.armstrip """
@pytest.mark.skipif(
data.name == "basic",
reason="Skip test if kind is basic",
)
@pytest.mark.parametrize(
"index",
[
(data.phys_out),
(data.virt_out),
],
)
def test_it_sets_recorder_armstrip_bool_params(self, index, value):
vm.recorder.armstrip[index].set(value)
""" recorder.armbus """
@pytest.mark.skipif(
data.name == "basic",
reason="Skip test if kind is basic",
)
@pytest.mark.parametrize(
"index",
[
(data.phys_out),
(data.virt_out),
],
)
def test_it_sets_recorder_armbus_bool_params(self, index, value):
vm.recorder.armbus[index].set(True)
""" fx tests """
@@ -323,6 +373,26 @@ class TestSetAndGetIntHigher:
vm.option.delay[index].set(value)
assert vm.option.delay[index].get() == value
""" recorder tests """
@pytest.mark.skipif(
data.name == "basic",
reason="Skip test if kind is basic",
)
@pytest.mark.parametrize(
"param,value",
[
("samplerate", 32000),
("samplerate", 96000),
("bitresolution", 16),
("bitresolution", 32),
],
)
def test_it_sets_and_gets_recorder_int_params(self, param, value):
assert hasattr(vm.recorder, param)
setattr(vm.recorder, param, value)
assert getattr(vm.recorder, param) == value
class TestSetAndGetFloatHigher:
__test__ = True

View File

@@ -250,3 +250,19 @@ class Midi:
def _set(self, key: int, velocity: int):
self.cache[key] = velocity
class VmGui:
_launched = None
@property
def launched(self) -> bool:
return self._launched
@launched.setter
def launched(self, val: bool):
self._launched = val
@property
def launched_by_api(self):
return not self.launched

View File

@@ -1,3 +1,5 @@
import re
from .error import VMError
from .iremote import IRemote
from .kinds import kinds_all
@@ -19,9 +21,10 @@ class Recorder(IRemote):
Returns a Recorder class of a kind.
"""
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
ARMCHANNELMIXIN_cls = _make_armchannel_mixins(remote)[remote.kind.name]
REC_cls = type(
f"Recorder{remote.kind}",
(cls, CHANNELOUTMIXIN_cls),
(cls, CHANNELOUTMIXIN_cls, ARMCHANNELMIXIN_cls),
{
**{
param: action_fn(param)
@@ -35,6 +38,7 @@ class Recorder(IRemote):
"rew",
]
},
"mode": RecorderMode(remote),
},
)
return REC_cls(remote)
@@ -46,20 +50,183 @@ class Recorder(IRemote):
def identifier(self) -> str:
return "recorder"
@property
def samplerate(self) -> int:
return int(self.getter("samplerate"))
@samplerate.setter
def samplerate(self, val: int):
opts = (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
if val not in opts:
self.logger.warning(f"samplerate got: {val} but expected a value in {opts}")
self.setter("samplerate", val)
@property
def bitresolution(self) -> int:
return int(self.getter("bitresolution"))
@bitresolution.setter
def bitresolution(self, val: int):
opts = (8, 16, 24, 32)
if val not in opts:
self.logger.warning(
f"bitresolution got: {val} but expected a value in {opts}"
)
self.setter("bitresolution", val)
@property
def channel(self) -> int:
return int(self.getter("channel"))
@channel.setter
def channel(self, val: int):
if not 1 <= val <= 8:
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
self.setter("channel", val)
@property
def kbps(self):
return int(self.getter("kbps"))
@kbps.setter
def kbps(self, val: int):
opts = (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
if val not in opts:
self.logger.warning(f"kbps got: {val} but expected a value in {opts}")
self.setter("kbps", val)
@property
def gain(self) -> float:
return round(self.getter("gain"), 1)
@gain.setter
def gain(self, val: float):
self.setter("gain", val)
def load(self, file: str):
try:
self.setter("load", file)
except UnicodeError:
raise VMError("File full directory must be a raw string")
def set_loop(self, val: bool):
self.setter("mode.loop", 1 if val else 0)
# loop forwarder methods, for backwards compatibility
@property
def loop(self):
return self.mode.loop
loop = property(fset=set_loop)
@loop.setter
def loop(self, val: bool):
self.mode.loop = val
def goto(self, time_str):
def get_sec():
"""Get seconds from time string"""
h, m, s = time_str.split(":")
return int(h) * 3600 + int(m) * 60 + int(s)
time_str = str(time_str) # coerce the type
if (
match := re.match(
r"^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$",
time_str,
)
is not None
):
self.setter("goto", get_sec())
else:
self.logger.warning(
f"goto expects a string that matches the format 'hh:mm:ss'"
)
def filetype(self, val: str):
opts = {"wav": 1, "aiff": 2, "bwf": 3, "mp3": 100}
try:
self.setter("filetype", opts[val.lower()])
except KeyError:
self.logger.warning(
f"filetype got: {val} but expected a value in {list(opts.keys())}"
)
class RecorderMode(IRemote):
@property
def identifier(self):
return "recorder.mode"
@property
def recbus(self) -> bool:
return self.getter("recbus") == 1
@recbus.setter
def recbus(self, val: bool):
self.setter("recbus", 1 if val else 0)
@property
def playonload(self) -> bool:
return self.getter("playonload") == 1
@playonload.setter
def playonload(self, val: bool):
self.setter("playonload", 1 if val else 0)
@property
def loop(self) -> bool:
return self.getter("loop") == 1
@loop.setter
def loop(self, val: bool):
self.setter("loop", 1 if val else 0)
@property
def multitrack(self) -> bool:
return self.getter("multitrack") == 1
@multitrack.setter
def multitrack(self, val: bool):
self.setter("multitrack", 1 if val else 0)
class RecorderArmChannel(IRemote):
def __init__(self, remote, i):
super().__init__(remote)
self._i = i
def set(self, val: bool):
self.setter("", 1 if val else 0)
class RecorderArmStrip(RecorderArmChannel):
@property
def identifier(self):
return f"recorder.armstrip[{self._i}]"
class RecorderArmBus(RecorderArmChannel):
@property
def identifier(self):
return f"recorder.armbus[{self._i}]"
def _make_armchannel_mixin(remote, kind):
"""Creates an armchannel out mixin"""
return type(
f"ArmChannelMixin{kind}",
(),
{
"armstrip": tuple(
RecorderArmStrip(remote, i) for i in range(kind.num_strip)
),
"armbus": tuple(RecorderArmBus(remote, i) for i in range(kind.num_bus)),
},
)
def _make_armchannel_mixins(remote):
return {kind.name: _make_armchannel_mixin(remote, kind) for kind in kinds_all}
def _make_channelout_mixin(kind):
"""Creates a channel out property mixin"""
"""Creates a channel out mixin"""
return type(
f"ChannelOutMixin{kind}",
(),

View File

@@ -10,7 +10,7 @@ from .error import CAPIError, VMError
from .event import Event
from .inst import bits
from .kinds import KindId
from .misc import Midi
from .misc import Midi, VmGui
from .subject import Subject
from .updater import Producer, Updater
from .util import grouper, polling, script
@@ -32,6 +32,7 @@ class Remote(CBindings):
self.event = Event(
{k: kwargs.pop(k) for k in ("pdirty", "mdirty", "midi", "ldirty")}
)
self.gui = VmGui()
self.logger = logger.getChild(self.__class__.__name__)
for attr, val in kwargs.items():
@@ -63,8 +64,8 @@ class Remote(CBindings):
def login(self) -> NoReturn:
"""Login to the API, initialize dirty parameters"""
res = self.call(self.vm_login, ok=(0, 1))
if res == 1:
self.gui.launched = self.call(self.vm_login, ok=(0, 1)) == 0
if not self.gui.launched:
self.logger.info(
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
)

View File

@@ -27,7 +27,7 @@ class Producer(threading.Thread):
if self._remote.event.ldirty:
self.queue.put("ldirty")
time.sleep(self._remote.ratelimit)
self.logger.debug(f"terminating {self.getName()} thread")
self.logger.debug(f"terminating {self.name} thread")
self.queue.put(None)
@@ -61,7 +61,7 @@ class Updater(threading.Thread):
while True:
event = self.queue.get()
if event is None:
self.logger.debug(f"terminating {self.getName()} thread")
self.logger.debug(f"terminating {self.name} thread")
break
if event == "pdirty" and self._remote.pdirty: