Skip to content

Commit

Permalink
Merge pull request #37 from iluvcapra/feature-interactive
Browse files Browse the repository at this point in the history
Feature: interactive shell
  • Loading branch information
iluvcapra authored Nov 25, 2024
2 parents 1d499d9 + 2830cb8 commit 94563f6
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 63 deletions.
166 changes: 106 additions & 60 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,135 @@ from the command line and output metadata to stdout.

.. code-block:: shell
$ wavinfo [--ixml | --adm] INFILE +
By default, `wavinfo` will output a JSON dictionary for each file argument.
$ wavinfo [[-i] | [--ixml | --adm]] INFILE +
Options
-------

Two option flags will change the behavior of the command:
By default, `wavinfo` will output a JSON dictionary for each file argument.

``-i``
`wavinfo` will run in `interactive mode`_.

Two option flags will change the behavior of the command in non-interactive
mode:

``--ixml``
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
of each input wave file, or will emit an error message to stderr if iXML
metadata is not present.
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata
payload of each input wave file, or will emit an error message to stderr if
iXML metadata is not present.

``--adm``
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
payload of each input wave file, or will emit an error message to stderr if
ADM XML metadata is not present.

These options are mutually-exclusive, with `\-\-adm` taking precedence.
These options are mutually-exclusive, with `\-\-adm` taking precedence. The
``--ixml`` and ``--adm`` flags futher take precedence over ``-i``.


Interactive Mode
-----------------

In interactive mode, `wavinfo` will present a command prompt which allows you
to query the files provided on the command line and explore the metadata tree
interactively. Each file on the command line is scanned and presented as a
tree of metadata records.

Commands include:

``ls``
List the available metadata keys at the current level.

``cd``
Traverse to a metadata key in the current level (or enter `..` to go up
to the prevvious level).

``bye``
Exit to the shell.

Type `help` or `?` at the prompt to get a full list of commands.


Example Output
--------------

.. attention::

Metadata fields containing binary data, such as the Broadcast-WAV UMID, will
be included in the JSON output as a base-64 encoded string, preceded by the
marker "base64:".

.. code-block:: javascript
{
"filename": "tests/test_files/sounddevices/A101_1.WAV",
"run_date": "2022-11-26T17:56:38.342935",
"application": "wavinfo 2.1.0",
"scopes": {
"fmt": {
"audio_format": 1,
"channel_count": 2,
"sample_rate": 48000,
"byte_rate": 288000,
"block_align": 6,
"bits_per_sample": 24
{
"filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav",
"run_date": "2024-11-25T10:26:11.280053",
"application": "wavinfo 3.0.0",
"scopes": {
"fmt": {
"audio_format": 65534,
"channel_count": 4,
"sample_rate": 48000,
"byte_rate": 576000,
"block_align": 12,
"bits_per_sample": 24
},
"data": {
"byte_count": 576000,
"frame_count": 48000
},
"ixml": {
"track_list": [
{
"channel_index": "1",
"interleave_index": "1",
"name": "",
"function": "ACN0-FOA"
},
"data": {
"byte_count": 1441434,
"frame_count": 240239
{
"channel_index": "2",
"interleave_index": "2",
"name": "",
"function": "ACN1-FOA"
},
"ixml": {
"track_list": [
{
"channel_index": "1",
"interleave_index": "1",
"name": "MKH516 A",
"function": ""
},
{
"channel_index": "2",
"interleave_index": "2",
"name": "Boom",
"function": ""
}
],
"project": "BMH",
"scene": "A101",
"take": "1",
"tape": "18Y12M31",
"family_uid": "USSDVGR1112089007124001008206300",
"family_name": null
{
"channel_index": "3",
"interleave_index": "3",
"name": "",
"function": "ACN2-FOA"
},
"bext": {
"description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n",
"originator": "Sound Dev: 702T S#GR1112089007",
"originator_ref": "USSDVGR1112089007124001008206301",
"originator_date": "2018-12-31",
"originator_time": "12:40:00",
"time_reference": 2190940753,
"version": 1,
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
"loudness_value": null,
"loudness_range": null,
"max_true_peak": null,
"max_momentary_loudness": null,
"max_shortterm_loudness": null
{
"channel_index": "4",
"interleave_index": "4",
"name": "",
"function": "ACN3-FOA"
}
],
"project": "wavinfo Test Project",
"scene": null,
"take": null,
"tape": null,
"family_uid": "E5DDE719B9484A758162FF7B652383A3",
"family_name": null
},
"bext": {
"description": "wavinfo Test Project Nuendo output",
"originator": "Nuendo",
"originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
"originator_date": "2022-12-02",
"originator_time": "10:21:06",
"time_reference": 172800000,
"version": 2,
"umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
"loudness_value": 327.67,
"loudness_range": 327.67,
"max_true_peak": 327.67,
"max_momentary_loudness": 327.67,
"max_shortterm_loudness": 327.67
}
}
}
}
101 changes: 100 additions & 1 deletion wavinfo/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from enum import Enum
import importlib.metadata
from base64 import b64encode
from cmd import Cmd
from shlex import split
from typing import List, Dict, Union


class MyJSONEncoder(json.JSONEncoder):
Expand All @@ -24,6 +27,85 @@ class MissingDataError(RuntimeError):
pass


class MetaBrowser(Cmd):
prompt = "(wavinfo) "

metadata: Union[List, Dict]
path: List[str] = []

@property
def cwd(self):
root: List | Dict = self.metadata
for key in self.path:
if isinstance(root, list):
root = root[int(key)]
else:
root = root[key]

return root

@staticmethod
def print_value(collection, key):
val = collection[key]
if isinstance(val, int):
print(f" - {key}: {val}")
elif isinstance(val, str):
print(f" - {key}: \"{val}\"")
elif isinstance(val, dict):
print(f" - {key}: Dict ({len(val)} keys)")
elif isinstance(val, list):
print(f" - {key}: List ({len(val)} keys)")
elif isinstance(val, bytes):
print(f" - {key}: ({len(val)} bytes)")
elif val is None:
print(f" - {key}: (NO VALUE)")
else:
print(f" - {key}: Unknown")

def do_ls(self, _):
'List items at the current node: LS'
root = self.cwd

if isinstance(root, list):
print("List:")
for i in range(len(root)):
self.print_value(root, i)

elif isinstance(root, dict):
print("Dictionary:")
for key in root:
self.print_value(root, key)

else:
print("Cannot print node, is not a list or dictionary.")

def do_cd(self, args):
'Switch to a different node: CD node-name | ".."'
argv = split(args)
if argv[0] == "..":
self.path = self.path[0:-1]
else:
if isinstance(self.cwd, list):
if int(argv[0]) < len(self.cwd):
self.path = self.path + [argv[0]]
else:
print(f"Index {argv[0]} does not exist")
elif isinstance(self.cwd, dict):
if argv[0] in self.cwd.keys():
self.path = self.path + [argv[0]]
else:
print(f"Key \"{argv[0]}\" does not exist")

if len(self.path) > 0:
self.prompt = "(" + "/".join(self.path) + ") "
else:
self.prompt = "(wavinfo) "

def do_bye(self, _):
'Exit the interactive browser: BYE'
return True


def main():
version = importlib.metadata.version('wavinfo')
manpath = os.path.dirname(__file__) + "/man"
Expand Down Expand Up @@ -51,8 +133,15 @@ def main():
default=False,
action='store_true')

parser.add_option('-i',
help='Read metadata with an interactive prompt',
default=False,
action='store_true')

(options, args) = parser.parse_args(sys.argv)

interactive_dict = []

# if options.install_manpages:
# print("Installing manpages...")
# print(f"Docfiles at {__file__}")
Expand Down Expand Up @@ -98,14 +187,24 @@ def main():

ret_dict['scopes'][scope][name] = value

json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
if options.i:
interactive_dict.append(ret_dict)
else:
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
indent=2)

except MissingDataError as e:
print("MissingDataError: Missing metadata (%s) in file %s" %
(e, arg), file=sys.stderr)
continue
except Exception as e:
raise e

if len(interactive_dict) > 0:
cli = MetaBrowser()
cli.metadata = interactive_dict
cli.cmdloop()


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions wavinfo/man/man1/wavinfo.1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
wavinfo \- probe wave files for metadata
.SH SYNOPSIS
.SY wavinfo
.I "[\-i]"
.I "[\-\-adm]"
.I "[\-\-ixml]"
.I FILE ...
Expand All @@ -24,6 +25,10 @@ Output any iXML metdata in
.BR FILE .
.IP "\-h, \-\-help"
Print brief help.
.IP "\-i"
Enter
.I "interactive mode"
and browse metadata in FILE with an interactive command prompt.
.SH DETAILED DESCRIPTION
.B wavinfo
collects metadata according to different
Expand Down
4 changes: 2 additions & 2 deletions wavinfo/wave_bext_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def to_dict(self):
# umid_str = umid_parsed.basic_umid_to_str()
# else:

umid_str = None
# umid_str = None

return {'description': self.description,
'originator': self.originator,
Expand All @@ -98,7 +98,7 @@ def to_dict(self):
'originator_time': self.originator_time,
'time_reference': self.time_reference,
'version': self.version,
'umid': umid_str,
'umid': self.umid,
'coding_history': self.coding_history,
'loudness_value': self.loudness_value,
'loudness_range': self.loudness_range,
Expand Down

0 comments on commit 94563f6

Please sign in to comment.