recs 0.2.0__py3-none-any.whl → 0.3.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.
- recs/__main__.py +27 -4
- recs/audio/block.py +2 -5
- recs/audio/channel_writer.py +94 -78
- recs/audio/file_opener.py +14 -15
- recs/base/_query_device.py +21 -0
- recs/base/cfg_raw.py +16 -14
- recs/base/prefix_dict.py +10 -11
- recs/base/state.py +73 -0
- recs/base/times.py +3 -62
- recs/base/type_conversions.py +5 -5
- recs/base/types.py +2 -7
- recs/cfg/__init__.py +11 -2
- recs/cfg/aliases.py +32 -21
- recs/cfg/app.py +87 -0
- recs/cfg/cfg.py +36 -60
- recs/cfg/cli.py +161 -100
- recs/cfg/device.py +37 -43
- recs/{base → cfg}/metadata.py +23 -2
- recs/cfg/path_pattern.py +156 -0
- recs/cfg/run_cli.py +43 -0
- recs/cfg/time_settings.py +61 -0
- recs/cfg/track.py +4 -4
- recs/misc/contexts.py +10 -0
- recs/misc/counter.py +9 -8
- recs/misc/legal_filename.py +1 -1
- recs/misc/log.py +43 -0
- recs/ui/__init__.py +0 -1
- recs/ui/device_process.py +37 -0
- recs/ui/device_recorder.py +66 -82
- recs/ui/device_tracks.py +9 -1
- recs/ui/full_state.py +50 -0
- recs/ui/live.py +28 -16
- recs/ui/recorder.py +52 -71
- recs/ui/table.py +1 -1
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/METADATA +59 -17
- recs-0.3.0.dist-info/RECORD +47 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/WHEEL +1 -1
- recs/cfg/run.py +0 -32
- recs/misc/recording_path.py +0 -47
- recs/ui/channel_recorder.py +0 -51
- recs-0.2.0.dist-info/RECORD +0 -40
- /recs/{misc → cfg}/hash_cmp.py +0 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/LICENSE +0 -0
- {recs-0.2.0.dist-info → recs-0.3.0.dist-info}/entry_points.txt +0 -0
recs/cfg/__init__.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
from .aliases import Aliases
|
|
2
2
|
from .cfg import Cfg
|
|
3
|
-
from .device import InputDevice, InputDevices
|
|
3
|
+
from .device import InputDevice, InputDevices, InputStream
|
|
4
|
+
from .path_pattern import PathPattern
|
|
4
5
|
from .track import Track
|
|
5
6
|
|
|
6
|
-
__all__ =
|
|
7
|
+
__all__ = (
|
|
8
|
+
'Aliases',
|
|
9
|
+
'Cfg',
|
|
10
|
+
'InputDevice',
|
|
11
|
+
'InputDevices',
|
|
12
|
+
'InputStream',
|
|
13
|
+
'PathPattern',
|
|
14
|
+
'Track',
|
|
15
|
+
)
|
recs/cfg/aliases.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
|
|
3
3
|
from recs.base.prefix_dict import PrefixDict
|
|
4
|
+
from recs.base import prefix_dict, RecsError
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
from .device import InputDevice, InputDevices
|
|
6
8
|
from .track import Track
|
|
@@ -8,9 +10,11 @@ from .track import Track
|
|
|
8
10
|
CHANNEL_SPLITTER = '+'
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class Aliases
|
|
13
|
+
class Aliases:
|
|
14
|
+
tracks: prefix_dict.PrefixDict[Track]
|
|
15
|
+
|
|
12
16
|
def __init__(self, aliases: t.Sequence[str], devices: InputDevices) -> None:
|
|
13
|
-
|
|
17
|
+
self.tracks = PrefixDict()
|
|
14
18
|
|
|
15
19
|
assert devices
|
|
16
20
|
self.devices = devices
|
|
@@ -25,57 +29,64 @@ class Aliases(PrefixDict[Track]):
|
|
|
25
29
|
|
|
26
30
|
names, values = zip(*(split(n) for n in aliases))
|
|
27
31
|
if len(set(names)) < len(names):
|
|
28
|
-
raise
|
|
32
|
+
raise RecsError(f'Duplicate aliases: {aliases}')
|
|
29
33
|
|
|
30
|
-
self.update(sorted(zip(names, self.to_tracks(values))))
|
|
34
|
+
self.tracks.update(sorted(zip(names, self.to_tracks(values))))
|
|
31
35
|
|
|
32
36
|
inv: dict[Track, list[str]] = {}
|
|
33
|
-
for k, v in self.items():
|
|
37
|
+
for k, v in self.tracks.items():
|
|
34
38
|
inv.setdefault(v, []).append(k)
|
|
35
39
|
|
|
36
40
|
if duplicate_aliases := [(k, v) for k, v in inv.items() if len(v) > 1]:
|
|
37
|
-
raise
|
|
41
|
+
raise RecsError(f'Duplicate alias values: {duplicate_aliases}')
|
|
38
42
|
|
|
39
43
|
self.inv = {k: v[0] for k, v in sorted(inv.items())}
|
|
40
44
|
|
|
41
45
|
def to_tracks(self, names: t.Iterable[str]) -> t.Sequence[Track]:
|
|
42
|
-
|
|
46
|
+
errors: dict[str, list[str]] = {}
|
|
43
47
|
result: list[Track] = []
|
|
44
48
|
|
|
45
49
|
for name in names:
|
|
46
50
|
try:
|
|
47
51
|
result.append(self.to_track(name))
|
|
48
|
-
except
|
|
49
|
-
|
|
52
|
+
except KeyError as e:
|
|
53
|
+
key, error = e.args
|
|
54
|
+
errors.setdefault(error, []).append(key)
|
|
55
|
+
|
|
56
|
+
if not errors:
|
|
57
|
+
return result
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
s = 's' * (len(
|
|
53
|
-
|
|
59
|
+
def err(k, v) -> str:
|
|
60
|
+
s = 's' * (len(v) != 1)
|
|
61
|
+
return f'{k.capitalize()} device name{s}: {", ".join(v)}'
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
devs = '", "'.join(sorted(self.devices))
|
|
64
|
+
devices = f'Devices: "{devs}"'
|
|
65
|
+
errs = (err(k, v) for k, v in errors.items())
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
raise RecsError('\n'.join([*errs, devices]))
|
|
68
|
+
|
|
69
|
+
def display_name(self, x: InputDevice | Track, short: bool = True) -> str:
|
|
58
70
|
if isinstance(x, InputDevice):
|
|
59
71
|
return self.inv.get(Track(x), x.name)
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
|
|
73
|
+
default = x.name if short else str(x)
|
|
74
|
+
return self.inv.get(x, default)
|
|
62
75
|
|
|
63
76
|
def to_track(self, track_name: str) -> Track:
|
|
64
77
|
try:
|
|
65
|
-
return self[track_name]
|
|
78
|
+
return self.tracks[track_name]
|
|
66
79
|
except KeyError:
|
|
67
80
|
pass
|
|
68
81
|
|
|
69
82
|
name, _, channels = (i.strip() for i in track_name.partition(CHANNEL_SPLITTER))
|
|
70
83
|
try:
|
|
71
|
-
track = self[name]
|
|
84
|
+
track = self.tracks[name]
|
|
72
85
|
except KeyError:
|
|
73
86
|
device = self.devices[name]
|
|
74
87
|
else:
|
|
75
88
|
if track.channels:
|
|
76
|
-
raise KeyError(
|
|
77
|
-
f'Alias {name} is a device alias: "{track_name}" is not legal'
|
|
78
|
-
)
|
|
89
|
+
raise KeyError(track_name, 'impossible')
|
|
79
90
|
device = track.device
|
|
80
91
|
|
|
81
92
|
return Track(device, channels)
|
recs/cfg/app.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import dtyper
|
|
3
|
+
|
|
4
|
+
from recs.base import RecsError, pyproject, times
|
|
5
|
+
from recs.base.type_conversions import FORMATS, SDTYPES, SUBTYPES
|
|
6
|
+
|
|
7
|
+
from . import metadata
|
|
8
|
+
|
|
9
|
+
INTRO = f"""
|
|
10
|
+
{pyproject.message()}
|
|
11
|
+
|
|
12
|
+
============================================="""
|
|
13
|
+
LINES = (
|
|
14
|
+
INTRO,
|
|
15
|
+
'Why should there be a record button at all?',
|
|
16
|
+
'I wanted to digitize a huge number of cassettes and LPs, so I wanted a '
|
|
17
|
+
+ 'program that ran in the background and recorded everything except quiet.',
|
|
18
|
+
'Nothing like that existed so I wrote it. Free, open-source, configurable.',
|
|
19
|
+
'Full documentation here: https://github.com/rec/recs',
|
|
20
|
+
'',
|
|
21
|
+
)
|
|
22
|
+
HELP = '\n\n\n\n'.join(LINES)
|
|
23
|
+
|
|
24
|
+
app = dtyper.Typer(
|
|
25
|
+
add_completion=False,
|
|
26
|
+
context_settings={'help_option_names': ['--help', '-h']},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimeParam(click.ParamType):
|
|
31
|
+
name = 'TIME'
|
|
32
|
+
|
|
33
|
+
def convert(self, value, p, ctx) -> float:
|
|
34
|
+
if isinstance(value, (int, float)):
|
|
35
|
+
return value
|
|
36
|
+
try:
|
|
37
|
+
return times.to_time(value)
|
|
38
|
+
except ValueError as e:
|
|
39
|
+
self.fail(f'{p.opts[0]}: {e.args[0]}', p, ctx)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DictParam(click.ParamType):
|
|
43
|
+
name = 'NONE'
|
|
44
|
+
prefix_dict: dict
|
|
45
|
+
|
|
46
|
+
def convert(self, value, p, ctx):
|
|
47
|
+
if not value:
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
return value
|
|
51
|
+
try:
|
|
52
|
+
return self.prefix_dict[value]
|
|
53
|
+
except KeyError:
|
|
54
|
+
self.fail(f'Cannot understand {p.opts[0]}="{value}"')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FormatParam(DictParam):
|
|
58
|
+
name = 'AUDIO FORMAT'
|
|
59
|
+
prefix_dict = FORMATS
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SdTypeParam(DictParam):
|
|
63
|
+
name = 'NUMERIC TYPE'
|
|
64
|
+
prefix_dict = SDTYPES
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SubtypeParam(DictParam):
|
|
68
|
+
name = 'AUDIO SUBTYPE'
|
|
69
|
+
prefix_dict = SUBTYPES
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MetadataParam(click.ParamType):
|
|
73
|
+
name = 'METADATA'
|
|
74
|
+
|
|
75
|
+
def convert(self, value, p, ctx):
|
|
76
|
+
try:
|
|
77
|
+
metadata.to_dict([value])
|
|
78
|
+
return value
|
|
79
|
+
except RecsError as e:
|
|
80
|
+
self.fail(f'In {p.opts[0]}: "{e.args[0]}"')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AliasParam(click.ParamType):
|
|
84
|
+
name = 'ALIAS'
|
|
85
|
+
|
|
86
|
+
def convert(self, value, p, ctx):
|
|
87
|
+
return value
|
recs/cfg/cfg.py
CHANGED
|
@@ -1,66 +1,65 @@
|
|
|
1
1
|
import dataclasses as dc
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import typing as t
|
|
4
5
|
import warnings
|
|
5
6
|
from functools import wraps
|
|
6
7
|
|
|
7
8
|
import soundfile as sf
|
|
8
9
|
|
|
9
|
-
from recs.base import RecsError
|
|
10
|
+
from recs.base import RecsError
|
|
10
11
|
from recs.base.cfg_raw import CfgRaw
|
|
11
|
-
from recs.base.
|
|
12
|
-
from recs.base.
|
|
13
|
-
|
|
14
|
-
SDTYPE_TO_SUBTYPE,
|
|
15
|
-
SUBTYPE_TO_SDTYPE,
|
|
16
|
-
SUBTYPES,
|
|
17
|
-
)
|
|
18
|
-
from recs.base.types import SDTYPE, Format, SdType, Subdirectory, Subtype
|
|
19
|
-
|
|
20
|
-
from . import device
|
|
21
|
-
from .aliases import Aliases
|
|
12
|
+
from recs.base.type_conversions import SDTYPE_TO_SUBTYPE, SUBTYPE_TO_SDTYPE
|
|
13
|
+
from recs.base.types import SDTYPE, Format, SdType, Subtype
|
|
14
|
+
from recs.misc import log
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
from . import device, metadata, path_pattern, time_settings
|
|
17
|
+
from .aliases import Aliases
|
|
24
18
|
|
|
25
19
|
|
|
26
20
|
class Cfg:
|
|
27
21
|
devices: device.InputDevices
|
|
28
22
|
format: Format
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
subtype: Subtype | None
|
|
24
|
+
sdtype: SdType
|
|
31
25
|
|
|
32
26
|
@wraps(CfgRaw.__init__)
|
|
33
27
|
def __init__(self, *a, **ka) -> None:
|
|
34
28
|
self.cfg = cfg = CfgRaw(*a, **ka)
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return d[key]
|
|
41
|
-
except KeyError:
|
|
42
|
-
raise RecsError(f'Cannot understand --{flag}="{key}"') from None
|
|
30
|
+
# This constructor has this *global side-effect*, see log.py
|
|
31
|
+
log.VERBOSE = cfg.verbose
|
|
32
|
+
if cfg.verbose:
|
|
33
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
43
34
|
|
|
44
|
-
self.
|
|
45
|
-
self.sdtype = get(SdType, cfg.sdtype, 'sdtype')
|
|
46
|
-
self.subtype = get(SUBTYPES, cfg.subtype, 'subtype')
|
|
35
|
+
self.path = path_pattern.PathPattern(cfg.path)
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
if not self.sdtype:
|
|
50
|
-
self.sdtype = SUBTYPE_TO_SDTYPE.get(self.subtype, SDTYPE)
|
|
37
|
+
self.format = t.cast(Format, cfg.format)
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
if cfg.subtype:
|
|
40
|
+
self.subtype = t.cast(Subtype, cfg.subtype)
|
|
41
|
+
elif not cfg.sdtype:
|
|
42
|
+
self.subtype = None
|
|
43
|
+
else:
|
|
44
|
+
subtype = SDTYPE_TO_SUBTYPE[t.cast(SdType, cfg.sdtype)]
|
|
57
45
|
|
|
58
46
|
if sf.check_format(self.format, subtype):
|
|
59
47
|
self.subtype = subtype
|
|
60
48
|
else:
|
|
61
|
-
|
|
49
|
+
self.subtype = None
|
|
50
|
+
msg = f'format={self.format:s}, sdtype={cfg.sdtype:s}'
|
|
62
51
|
warnings.warn(f"Can't get subtype for {msg}")
|
|
63
52
|
|
|
53
|
+
if self.subtype and not sf.check_format(self.format, self.subtype):
|
|
54
|
+
raise RecsError(f'{self.format} and {self.subtype} are incompatible')
|
|
55
|
+
|
|
56
|
+
if cfg.sdtype:
|
|
57
|
+
self.sdtype = t.cast(SdType, cfg.sdtype)
|
|
58
|
+
elif self.subtype:
|
|
59
|
+
self.sdtype = SUBTYPE_TO_SDTYPE.get(self.subtype, SDTYPE)
|
|
60
|
+
else:
|
|
61
|
+
self.sdtype = SDTYPE
|
|
62
|
+
|
|
64
63
|
if cfg.devices.name:
|
|
65
64
|
if not cfg.devices.exists():
|
|
66
65
|
raise RecsError(f'{cfg.devices} does not exist')
|
|
@@ -73,35 +72,12 @@ class Cfg:
|
|
|
73
72
|
|
|
74
73
|
self.aliases = Aliases(cfg.alias, self.devices)
|
|
75
74
|
self.metadata = metadata.to_dict(cfg.metadata)
|
|
76
|
-
self.subdirectory = self._subdirectory()
|
|
77
75
|
self.times = self._times()
|
|
78
76
|
|
|
79
77
|
def __getattr__(self, k: str) -> t.Any:
|
|
80
78
|
return getattr(self.cfg, k)
|
|
81
79
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
res = tuple(t for s, t in subs if t is not None)
|
|
85
|
-
|
|
86
|
-
if bad_subdirectories := [s for s, t in subs if t is None]:
|
|
87
|
-
raise RecsError(f'Bad arguments to --subdirectory: {bad_subdirectories}')
|
|
88
|
-
|
|
89
|
-
if len(set(res)) < len(res):
|
|
90
|
-
raise RecsError('Duplicates in --subdirectory')
|
|
91
|
-
|
|
92
|
-
return res
|
|
93
|
-
|
|
94
|
-
def _times(self) -> times.TimeSettings:
|
|
95
|
-
fields = (f.name for f in dc.fields(times.TimeSettings))
|
|
80
|
+
def _times(self) -> time_settings.TimeSettings:
|
|
81
|
+
fields = (f.name for f in dc.fields(time_settings.TimeSettings))
|
|
96
82
|
d = {k: getattr(self, k) for k in fields}
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
d['longest_file_time'] = times.to_time(t := d['longest_file_time'])
|
|
100
|
-
except (ValueError, TypeError):
|
|
101
|
-
raise RecsError(f'Do not understand --longest-file-time={t}')
|
|
102
|
-
try:
|
|
103
|
-
d['shortest_file_time'] = times.to_time(t := d['shortest_file_time'])
|
|
104
|
-
except (ValueError, TypeError):
|
|
105
|
-
raise RecsError(f'Do not understand --shortest-file-time={t}')
|
|
106
|
-
|
|
107
|
-
return times.TimeSettings(**d)
|
|
83
|
+
return time_settings.TimeSettings(**d)
|