diff --git a/jc/lib.py b/jc/lib.py index 4f57c5a0..3862a654 100644 --- a/jc/lib.py +++ b/jc/lib.py @@ -61,6 +61,7 @@ parsers = [ 'lsmod', 'lsof', 'lsusb', + 'm3u', 'mount', 'mpstat', 'mpstat-s', diff --git a/jc/parsers/m3u.py b/jc/parsers/m3u.py new file mode 100644 index 00000000..58910d0b --- /dev/null +++ b/jc/parsers/m3u.py @@ -0,0 +1,141 @@ +"""jc - JSON Convert `m3u` and `m3u8` file parser + +Only standard extended info fields are supported. + +Usage (cli): + + $ cat playlist.m3u | jc --m3u + +Usage (module): + + import jc + result = jc.parse('m3u', m3u_file_output) + +Schema: + + [ + { + "runtime": integer, + "display": string, + "path": string + } + ] + +Examples: + + $ cat playlist.m3u | jc --m3u -p + [ + { + "runtime": 105, + "display": "Example artist - Example title", + "path": "C:\\Files\\My Music\\Example.mp3" + }, + { + "runtime": 321, + "display": "Example Artist2 - Example title2", + "path": "C:\\Files\\My Music\\Favorites\\Example2.ogg" + } + ] + + $ cat playlist.m3u | jc --m3u -p -r + [ + { + "runtime": "105", + "display": "Example artist - Example title", + "path": "C:\\Files\\My Music\\Example.mp3" + }, + { + "runtime": "321", + "display": "Example Artist2 - Example title2", + "path": "C:\\Files\\My Music\\Favorites\\Example2.ogg" + } + ] +""" +from typing import List, Dict +import jc.utils + + +class info(): + """Provides parser metadata (version, author, etc.)""" + version = '1.0' + description = 'M3U and M3U8 file parser' + author = 'Kelly Brazil' + author_email = 'kellyjonbrazil@gmail.com' + compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] + + +__version__ = info.version + + +def _process(proc_data: List[Dict]) -> List[Dict]: + """ + Final processing to conform to the schema. + + Parameters: + + proc_data: (List of Dictionaries) raw structured data to process + + Returns: + + List of Dictionaries. Structured to conform to the schema. + """ + int_list = ['runtime'] + for entry in proc_data: + for key in entry: + if key in int_list: + entry[key] = jc.utils.convert_to_int(entry[key]) + + return proc_data + + +def parse( + data: str, + raw: bool = False, + quiet: bool = False +) -> List[Dict]: + """ + Main text parsing function + + 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. + """ + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: List = [] + output_line = {} + + if jc.utils.has_data(data): + + for line in filter(None, data.splitlines()): + # ignore any lines with only whitespace + if not jc.utils.has_data(line): + continue + + # standard extended info fields + if line.lstrip().startswith('#EXTINF:'): + output_line = { + 'runtime': line.split(':')[1].split(',')[0].strip(), + 'display': line.split(':')[1].split(',')[1].strip() + } + continue + + # ignore all other extension info (obsolete) + if line.lstrip().startswith('#'): + continue + + # any lines left over are paths + output_line.update( + {'path': line.strip()} + ) + raw_output.append(output_line) + output_line = {} + + return raw_output if raw else _process(raw_output) diff --git a/tests/fixtures/generic/m3u-example.json b/tests/fixtures/generic/m3u-example.json new file mode 100644 index 00000000..f22d7b5b --- /dev/null +++ b/tests/fixtures/generic/m3u-example.json @@ -0,0 +1 @@ +[{"runtime":111,"display":"Sample artist name - line between extinf and path","path":"C:\\Music\\SampleMusic - line between extinf and path.mp3"},{"runtime":222,"display":"Example Artist name - Example track title","path":"C:\\Music\\ExampleMusic.mp3"},{"path":"mp3/unsupported-extension-file-path.mp3"},{"path":"file with no extended info.mp3"},{"path":"another file with no extended info.mp3"},{"runtime":111,"display":"Sample artist name - Sample track title","path":"C:\\Music\\SampleMusic2.mp3"},{"path":"mp3/file_with_no_extended_info_right_after_one_that_has_it.mp3"},{"runtime":-1,"display":"Song after no extended","path":"mp3/song after no extended.mp3"}] diff --git a/tests/fixtures/generic/m3u-example.m3u b/tests/fixtures/generic/m3u-example.m3u new file mode 100644 index 00000000..497c2f66 --- /dev/null +++ b/tests/fixtures/generic/m3u-example.m3u @@ -0,0 +1,20 @@ +#EXTM3U + +#EXTINF:111, Sample artist name - line between extinf and path + +C:\Music\SampleMusic - line between extinf and path.mp3 + +#EXTINF:222,Example Artist name - Example track title +C:\Music\ExampleMusic.mp3 + +#UNSUPPORTED_EXTENSION:123,abc +mp3/unsupported-extension-file-path.mp3 + +file with no extended info.mp3 +another file with no extended info.mp3 + +#EXTINF:111, Sample artist name - Sample track title +C:\Music\SampleMusic2.mp3 +mp3/file_with_no_extended_info_right_after_one_that_has_it.mp3 +#EXTINF:-1, Song after no extended +mp3/song after no extended.mp3 diff --git a/tests/test_m3u.py b/tests/test_m3u.py new file mode 100644 index 00000000..73cd8459 --- /dev/null +++ b/tests/test_m3u.py @@ -0,0 +1,35 @@ +import os +import unittest +import json +import jc.parsers.m3u + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class MyTests(unittest.TestCase): + + def setUp(self): + # input + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/m3u-example.m3u'), 'r', encoding='utf-8') as f: + self.m3u_example = f.read() + + # output + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/m3u-example.json'), 'r', encoding='utf-8') as f: + self.m3u_example_json = json.loads(f.read()) + + + def test_m3u_nodata(self): + """ + Test 'm3u' parser with no data + """ + self.assertEqual(jc.parsers.m3u.parse('', quiet=True), []) + + def test_m3u_example(self): + """ + Test 'm3u' example file + """ + self.assertEqual(jc.parsers.m3u.parse(self.m3u_example, quiet=True), self.m3u_example_json) + + +if __name__ == '__main__': + unittest.main()