Skip to content

Commit

Permalink
Merge branch 'dev' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
kellyjonbrazil authored Dec 20, 2024
2 parents 0c40e3a + a39cb05 commit ee3b873
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 1 deletion.
1 change: 1 addition & 0 deletions jc/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'acpi',
'airport',
'airport-s',
'amixer',
'apt-cache-show',
'apt-get-sqq',
'arp',
Expand Down
277 changes: 277 additions & 0 deletions jc/parsers/amixer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
r"""jc - JSON Convert `amixer sget` command output parser
Usage (cli):
$ amixer sget <control_name> | jc --amixer
$ amixer sget Master | jc --amixer
$ amixer sget Capture | jc --amixer
$ amixer sget Speakers | jc --amixer
Usage (module):
import jc
result = jc.parse('amixer', <amixer sget command output>)
Schema:
{
"control_name": string,
"capabilities": [
string
],
"playback_channels": [
string
],
"limits": {
"playback_min": integer,
"playback_max": integer
},
"mono": {
"playback_value": integer,
"percentage": integer,
"db": float,
"status": boolean
}
}
Examples:
$ amixer sget Master | jc --amixer -p
{
"control_name": "Capture",
"capabilities": [
"cvolume",
"cswitch"
],
"playback_channels": [],
"limits": {
"playback_min": 0,
"playback_max": 63
},
"front_left": {
"playback_value": 63,
"percentage": 100,
"db": 30.0,
"status": true
},
"front_right": {
"playback_value": 63,
"percentage": 100,
"db": 30.0,
"status": true
}
}
$ amixer sget Master | jc --amixer -p -r
{
"control_name": "Master",
"capabilities": [
"pvolume",
"pvolume-joined",
"pswitch",
"pswitch-joined"
],
"playback_channels": [
"Mono"
],
"limits": {
"playback_min": "0",
"playback_max": "87"
},
"mono": {
"playback_value": "87",
"percentage": "100%",
"db": "0.00db",
"status": "on"
}
}
"""
from typing import List, Dict

import jc.utils
from jc.utils import convert_to_int

class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = '`amixer` command parser'
author = 'Eden Refael'
author_email = 'edenraf@hotmail.com'
compatible = ['linux']
magic_commands = ['amixer']
tags = ['command']


__version__ = info.version


def _process(proc_data: dict) -> dict:
"""
Processes raw structured data to match the schema requirements.
Parameters:
proc_data: (dict) raw structured data from the parser
Returns:
(dict) processed structured data adhering to the schema
"""
# Initialize the processed dictionary
processed = {
"control_name": proc_data.get("control_name", ""),
"capabilities": proc_data.get("capabilities", []),
"playback_channels": proc_data.get("playback_channels", []),
"limits": {
"playback_min": convert_to_int(proc_data.get("limits", {}).get("playback_min", 0)),
"playback_max": convert_to_int(proc_data.get("limits", {}).get("playback_max", 0)),
},
}

# Process Mono or channel-specific data
channels = ["mono", "front_left", "front_right"]
for channel in channels:
if channel in proc_data:
channel_data = proc_data[channel]
processed[channel] = {
"playback_value": convert_to_int(channel_data.get("playback_value", 0)),
"percentage": convert_to_int(channel_data.get("percentage", "0%").strip("%")),
"db": float(channel_data.get("db", "0.0db").strip("db")),
"status": channel_data.get("status", "off") == "on",
}

return processed


def parse(
data: str,
raw: bool = False,
quiet: bool = False
) -> List[Dict]:
"""
Main text parsing function, The amixer is alsa mixer tool and output, Will work with Linux OS only.
Parameters:
data: (string) text data to parse
raw: (boolean) unprocessed output if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
push test
"""
"""
The Algorithm for parsing the `amixer sget` command, Input Explained/Rules/Pseudo Algorithm:
1. There will always be the first line which tells the user about the control name.
2. There will always be the Capabilities which include many of capabilities - It will be listed and separated by `" "`.
3. After that we'll need to distinct between the Channel - Could be many of channels - It will be listed and separated
by `" "`.
3a. Capture channels - List of channels
3b. Playback channels - List of channels
4. Limits - We'll always have the minimum limit and the maximum limit.
Input Example:
1."":~$ amixer sget Capture
Simple mixer control 'Capture',0
Capabilities: cvolume cswitch
Capture channels: Front Left - Front Right
Limits: Capture 0 - 63
Front Left: Capture 63 [100%] [30.00db] [on]
Front Right: Capture 63 [100%] [30.00db] [on]
2."":~$ amixer sget Master
Simple mixer control 'Master',0
Capabilities: pvolume pvolume-joined pswitch pswitch-joined
Playback channels: Mono
Limits: Playback 0 - 87
Mono: Playback 87 [100%] [0.00db] [on]
3."":~$ amixer sget Speaker
Simple mixer control 'Speaker',0
Capabilities: pvolume pswitch
Playback channels: Front Left - Front Right
Limits: Playback 0 - 87
Mono:
Front Left: Playback 87 [100%] [0.00db] [on]
Front Right: Playback 87 [100%] [0.00db] [on]
4."":~$ amixer sget Headphone
Simple mixer control 'Headphone',0
Capabilities: pvolume pswitch
Playback channels: Front Left - Front Right
Limits: Playback 0 - 87
Mono:
Front Left: Playback 0 [0%] [-65.25db] [off]
Front Right: Playback 0 [0%] [-65.25db] [off]
"""
# checks os compatibility and print a stderr massage if not compatible. quiet True could remove this check.
jc.utils.compatibility(__name__, info.compatible, quiet)

# check if string
jc.utils.input_type_check(data)

# starts the parsing from here
mapping = {}
# split lines and than work on each line
lines = data.splitlines()
first_line = lines[0].strip()

# Extract the control name from the first line
if first_line.startswith("Simple mixer control"):
control_name = first_line.split("'")[1]
else:
raise ValueError("Invalid amixer output format: missing control name.")
# map the control name
mapping["control_name"] = control_name

# Process subsequent lines for capabilities, channels, limits, and channel-specific mapping.
# gets the lines from the next line - because we already took care the first line.
for line in lines[1:]:
# strip the line (maybe there are white spaces in the begin&end)
line = line.strip()

if line.startswith("Capabilities:"):
mapping["capabilities"] = line.split(":")[1].strip().split()
elif line.startswith("Playback channels:"):
mapping["playback_channels"] = line.split(":")[1].strip().split(" - ")
elif line.startswith("Limits:"):
limits = line.split(":")[1].strip().split(" - ")
mapping["limits"] = {
"playback_min": limits[0].split()[1],
"playback_max": limits[1]
}
elif line.startswith("Mono:") or line.startswith("Front Left:") or line.startswith("Front Right:"):
# Identify the channel name and parse its information
channel_name = line.split(":")[0].strip().lower().replace(" ", "_")
channel_info = line.split(":")[1].strip()
# Example: "Playback 255 [100%] [0.00db] [on]"
channel_data = channel_info.split(" ")
if channel_data[0] == "":
continue
playback_value = channel_data[1]
percentage = channel_data[2].strip("[]") # Extract percentage e.g., "100%"
db_value = channel_data[3].strip("[]") # Extract db value e.g., "0.00db"
status = channel_data[4].strip("[]") # Extract status e.g., "on" or "off"

# Store channel mapping in the dictionary
mapping[channel_name] = {
"playback_value": playback_value,
"percentage": percentage,
"db": db_value.lower(),
"status": status
}

return mapping if raw else _process(mapping)
2 changes: 1 addition & 1 deletion runtests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# system should be in "America/Los_Angeles" timezone for all tests to pass
# ensure no local plugin parsers are installed for all tests to pass

python3 -m unittest -v
TZ=America/Los_Angeles python3 -m unittest -v
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name":"Capture","capabilities":["cvolume","cswitch"],"playback_channels":[],"limits":{"playback_min":0,"playback_max":63},"front_left":{"playback_value":63,"percentage":100,"db":30.0,"status":true},"front_right":{"playback_value":63,"percentage":100,"db":30.0,"status":true}}
1 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-capture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name": "Capture", "capabilities": ["cvolume", "cswitch"], "limits": {"playback_min": "0", "playback_max": "63"}, "front_left": {"playback_value": "63", "percentage": "100%", "db": "30.00db", "status": "on"}, "front_right": {"playback_value": "63", "percentage": "100%", "db": "30.00db", "status": "on"}}
6 changes: 6 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-capture.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Simple mixer control 'Capture',0
Capabilities: cvolume cswitch
Capture channels: Front Left - Front Right
Limits: Capture 0 - 63
Front Left: Capture 63 [100%] [30.00dB] [on]
Front Right: Capture 63 [100%] [30.00dB] [on]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name":"Headphone","capabilities":["pvolume","pswitch"],"playback_channels":["Front Left","Front Right"],"limits":{"playback_min":0,"playback_max":87},"front_left":{"playback_value":0,"percentage":0,"db":-65.25,"status":false},"front_right":{"playback_value":0,"percentage":0,"db":-65.25,"status":false}}
1 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-headphone.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name": "Headphone", "capabilities": ["pvolume", "pswitch"], "playback_channels": ["Front Left", "Front Right"], "limits": {"playback_min": "0", "playback_max": "87"}, "front_left": {"playback_value": "0", "percentage": "0%", "db": "-65.25db", "status": "off"}, "front_right": {"playback_value": "0", "percentage": "0%", "db": "-65.25db", "status": "off"}}
7 changes: 7 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-headphone.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Simple mixer control 'Headphone',0
Capabilities: pvolume pswitch
Playback channels: Front Left - Front Right
Limits: Playback 0 - 87
Mono:
Front Left: Playback 0 [0%] [-65.25dB] [off]
Front Right: Playback 0 [0%] [-65.25dB] [off]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name":"Master","capabilities":["pvolume","pvolume-joined","pswitch","pswitch-joined"],"playback_channels":["Mono"],"limits":{"playback_min":0,"playback_max":87},"mono":{"playback_value":87,"percentage":100,"db":0.0,"status":true}}
1 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-master.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name": "Master", "capabilities": ["pvolume", "pvolume-joined", "pswitch", "pswitch-joined"], "playback_channels": ["Mono"], "limits": {"playback_min": "0", "playback_max": "87"}, "mono": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}}
5 changes: 5 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-master.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Simple mixer control 'Master',0
Capabilities: pvolume pvolume-joined pswitch pswitch-joined
Playback channels: Mono
Limits: Playback 0 - 87
Mono: Playback 87 [100%] [0.00dB] [on]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name":"Speaker","capabilities":["pvolume","pswitch"],"playback_channels":["Front Left","Front Right"],"limits":{"playback_min":0,"playback_max":87},"front_left":{"playback_value":87,"percentage":100,"db":0.0,"status":true},"front_right":{"playback_value":87,"percentage":100,"db":0.0,"status":true}}
1 change: 1 addition & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-speakers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"control_name": "Speaker", "capabilities": ["pvolume", "pswitch"], "playback_channels": ["Front Left", "Front Right"], "limits": {"playback_min": "0", "playback_max": "87"}, "front_left": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}, "front_right": {"playback_value": "87", "percentage": "100%", "db": "0.00db", "status": "on"}}
7 changes: 7 additions & 0 deletions tests/fixtures/ubuntu-22.04/amixer-control-speakers.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Simple mixer control 'Speaker',0
Capabilities: pvolume pswitch
Playback channels: Front Left - Front Right
Limits: Playback 0 - 87
Mono:
Front Left: Playback 87 [100%] [0.00dB] [on]
Front Right: Playback 87 [100%] [0.00dB] [on]
48 changes: 48 additions & 0 deletions tests/test_amixer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest
import jc.parsers.amixer
import os
import json

THIS_DIR = os.path.dirname(os.path.abspath(__file__))


class AmixerTests(unittest.TestCase):
AMIXER_CMD = 'amixer'
UBUNTU_22_04_TEST_FIXTURES_PATH = f'{THIS_DIR}/fixtures/ubuntu-22.04/'
AMIXER_CONTROL_PATH = f'{UBUNTU_22_04_TEST_FIXTURES_PATH}amixer-control-'
TEST_FILES_NAME = [
f"{AMIXER_CONTROL_PATH}capture",
f'{AMIXER_CONTROL_PATH}headphone',
f'{AMIXER_CONTROL_PATH}master',
f'{AMIXER_CONTROL_PATH}speakers',
]

def setUp(self):
self.test_files_out = [f'{file}.out' for file in self.TEST_FILES_NAME]
self.test_files_json = [f'{file}.json' for file in self.TEST_FILES_NAME]
self.test_files_processed_json = [f'{file}-processed.json' for file in self.TEST_FILES_NAME]

def test_amixer_sget(self):
for file_out, file_json, file_processed_json in zip(self.test_files_out, self.test_files_json,
self.test_files_processed_json):
with open(file_out, 'r') as f:
amixer_sget_raw_output: str = f.read()
with open(file_json, 'r') as f:
expected_amixer_sget_json_output: str = f.read()
expected_amixer_sget_json_map: dict = json.loads(expected_amixer_sget_json_output)
with open(file_processed_json, 'r') as f:
expected_amixer_sget_processed_json_output: str = f.read()
expected_amixer_sget_processed_json_map: dict = json.loads(expected_amixer_sget_processed_json_output)

# Tests for raw=True
amixer_sget_json_map: dict = jc.parse(self.AMIXER_CMD, amixer_sget_raw_output, raw=True,
quiet=True)
self.assertEqual(amixer_sget_json_map, expected_amixer_sget_json_map)
# Tests for raw=False process
amixer_sget_json_processed_map: dict = jc.parse(self.AMIXER_CMD, amixer_sget_raw_output, raw=False,
quiet=True)
self.assertEqual(amixer_sget_json_processed_map, expected_amixer_sget_processed_json_map)


if __name__ == '__main__':
unittest.main()

0 comments on commit ee3b873

Please sign in to comment.