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/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__ = 'Aliases', 'Cfg', 'InputDevice', 'InputDevices', 'Track'
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(PrefixDict[Track]):
13
+ class Aliases:
14
+ tracks: prefix_dict.PrefixDict[Track]
15
+
12
16
  def __init__(self, aliases: t.Sequence[str], devices: InputDevices) -> None:
13
- super().__init__()
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 ValueError(f'Duplicate aliases: {aliases}')
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 ValueError(f'Duplicate alias values: {duplicate_aliases}')
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
- bad_track_names = []
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 Exception:
49
- bad_track_names.append(name)
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
- if bad_track_names:
52
- s = 's' * (len(bad_track_names) != 1)
53
- raise ValueError(f'Bad device name{s}: {", ".join(bad_track_names)}')
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
- return result
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
- def display_name(self, x: InputDevice | Track) -> str:
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
- else:
61
- return self.inv.get(x, x.channels_name)
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, metadata, times
10
+ from recs.base import RecsError
10
11
  from recs.base.cfg_raw import CfgRaw
11
- from recs.base.prefix_dict import PrefixDict
12
- from recs.base.type_conversions import (
13
- FORMATS,
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
- SUBDIRECTORY = PrefixDict({s: s for s in Subdirectory})
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
- sdtype: SdType | None = None
30
- subtype: Subtype | None = None
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
- def get(d, key, flag):
37
- if not key:
38
- return None
39
- try:
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.format = get(FORMATS, cfg.format, 'format')
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
- if self.subtype:
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
- if not sf.check_format(self.format, self.subtype):
53
- raise RecsError(f'{self.format} and {self.subtype} are incompatible')
54
-
55
- elif self.sdtype:
56
- subtype = SDTYPE_TO_SUBTYPE.get(self.sdtype, None)
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
- msg = f'format={self.format:s}, sdtype={self.sdtype:s}'
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 _subdirectory(self) -> t.Sequence[Subdirectory]:
83
- subs = [(s, SUBDIRECTORY.get_value(s)) for s in self.cfg.subdirectory]
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)