mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2025-12-31 08:50:41 +00:00
Merge pull request #37 from iluvcapra/feature-interactive
Feature: interactive shell
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import json
|
||||
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):
|
||||
@@ -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"
|
||||
@@ -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__}")
|
||||
@@ -98,7 +187,12 @@ 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)
|
||||
@@ -106,6 +200,11 @@ def main():
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
if len(interactive_dict) > 0:
|
||||
cli = MetaBrowser()
|
||||
cli.metadata = interactive_dict
|
||||
cli.cmdloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
wavinfo \- probe wave files for metadata
|
||||
.SH SYNOPSIS
|
||||
.SY wavinfo
|
||||
.I "[\-i]"
|
||||
.I "[\-\-adm]"
|
||||
.I "[\-\-ixml]"
|
||||
.I FILE ...
|
||||
@@ -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
|
||||
|
||||
@@ -89,7 +89,7 @@ class WavBextReader:
|
||||
# umid_str = umid_parsed.basic_umid_to_str()
|
||||
# else:
|
||||
|
||||
umid_str = None
|
||||
# umid_str = None
|
||||
|
||||
return {'description': self.description,
|
||||
'originator': self.originator,
|
||||
@@ -98,7 +98,7 @@ class WavBextReader:
|
||||
'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,
|
||||
|
||||
Reference in New Issue
Block a user