diff --git a/README.md b/README.md index 0f8fe59..6b73744 100644 --- a/README.md +++ b/README.md @@ -54,18 +54,24 @@ plugins: - yapl yapl: - input_path: ~/Music/playlists/ - output_path: ~/Music/playlists/ + yapl_path: ~/Music/playlists/ + csv_path: ~/Music/playlists/ + m3u_path: ~/Music/playlists/ relative: true ``` -`input_path: path` decides what directory yapl will search for yapl files. -`output_path: path` decides where to output the compiled m3u files. Can be the same as input_path. -`relative: bool` controls whether to use absolute or relative filepaths in the outputted M3U files. +- `yapl_path: path`: Decides what directory yapl will search for yapl files and where created yapl files will be placed. Can be the same as m3u_path and csv_path. +- `csv_path: path`: Decides what directory yapl will search for csv files. Can be the same as m3u_path and csv_path. +- `m3u_path: path`: Decides where to output the compiled m3u files or grab input m3u files. Can be the same as yapl_path and csv_path. +- `relative: bool`: Controls whether to use absolute or relative filepaths in the outputted M3U files. #### Run -Once configured, run `beet yapl` to compile all the playlists in your `input_path` directory. Warnings will be issued for any ambiguous or resultless queries and these tracks will be left out of the output. +Once configured, run `beet yapl` to compile all the playlists in your `yapl_path` directory. Warnings will be issued for any ambiguous or resultless queries and these tracks will be left out of the output. + +You can also run `beet m3ub` or `beet m3ut` to compile yapl files from the m3u8 playlist files within your `m3u_path` directory. `beet m3ub` will use the beets database to grab metadata about your songs, and `beet m3ut` will use the file metadata on the songs. Warnings will be issued for any paths within the m3u8 file that aren't reachable, and after each file is processed a failure count will be outputted. + +To take data from csv files and turn it into corresponding yapl files, run `beet csv`. The input csv files will be grabbed from your `csv_path` directory and the output yapl files will be placed in your `yapl_path` directory ``` $ beet yapl diff --git a/beetsplug/__pycache__/__init__.cpython-39.pyc b/beetsplug/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..3c54856 Binary files /dev/null and b/beetsplug/__pycache__/__init__.cpython-39.pyc differ diff --git a/beetsplug/__pycache__/yapl.cpython-39.pyc b/beetsplug/__pycache__/yapl.cpython-39.pyc new file mode 100644 index 0000000..84f706d Binary files /dev/null and b/beetsplug/__pycache__/yapl.cpython-39.pyc differ diff --git a/beetsplug/yapl.py b/beetsplug/yapl.py index a16d2ac..6e81e48 100644 --- a/beetsplug/yapl.py +++ b/beetsplug/yapl.py @@ -4,17 +4,32 @@ import yaml import os from pathlib import Path +# csv imports +import io +import csv +import py_m3u +from tinytag import TinyTag + +fieldstograb = ["album", "artist", "genre", "length", "filesize", "title", "track", "year"] class Yapl(BeetsPlugin): + def commands(self): compile_command = Subcommand('yapl', help='compile yapl playlists') compile_command.func = self.compile - return [compile_command] + m3utoyapl_beet_command = Subcommand('m3ub', help='convert m3u playlists to yapl using metadata from the beets library') + m3utoyapl_beet_command.func = self.m3u_to_yapl_beets + m3utoyapl_mp3tag_command = Subcommand('m3ut', help='convert m3u playlists to yapl using metadata from the actual music files') + m3utoyapl_mp3tag_command.func = self.m3u_to_yapl_mp3tag + csvtoyapl_command = Subcommand('csv', help='convert csv to yapl') + csvtoyapl_command.func = self.csv_to_yapl + #return [csvtoyapl_command, compile_command] + return [csvtoyapl_command, compile_command, m3utoyapl_beet_command, m3utoyapl_mp3tag_command] def write_m3u(self, filename, playlist, items): print(f"Writing {filename}") relative = self.config['relative'].get(bool) - output_path = Path(self.config['output_path'].as_filename()) + output_path = Path(self.config['m3u_path'].as_filename()) output_file = output_path / filename with open(output_file, 'w') as f: f.write("#EXTM3U\n") @@ -29,28 +44,178 @@ def write_m3u(self, filename, playlist, items): f.write("\n") def compile(self, lib, opts, args): - input_path = Path(self.config['input_path'].as_filename()) + input_path = Path(self.config['yapl_path'].as_filename()) yaml_files = [f for f in os.listdir(input_path) if f.endswith('.yaml') or f.endswith('.yapl')] for yaml_file in yaml_files: print(f"Parsing {yaml_file}") - with open(input_path / yaml_file, 'r') as file: - playlist = yaml.safe_load(file) - items = [] - # Deprecated 'playlist' field - if 'playlist' in playlist and not 'tracks' in playlist: - print("Deprecation warning: 'playlist' field in yapl file renamed to 'tracks'") - tracks = playlist['playlist'] + with open(input_path / yaml_file, 'r', encoding='utf-8') as file: + try: + playlist = yaml.safe_load(file) + except: + print("Unable to open file.") + else: + items = [] + # Deprecated 'playlist' field + if 'playlist' in playlist and not 'tracks' in playlist: + print("Deprecation warning: 'playlist' field in yapl file renamed to 'tracks'") + tracks = playlist['playlist'] + else: + tracks = playlist['tracks'] + for track in tracks: + query = [f"{k}:{str(v)}" for k, v in track.items()] + results = lib.items(query) + # Replaced match with if, for python <3.10 + l = len(results) + if l == 1: items.append(results[0]) + elif l == 0: print(f"No results for query: {query}") + else : print(f"Multiple results for query: {query}") + output_file = Path(yaml_file).stem + ".m3u" + self.write_m3u(output_file, playlist, items) + + ## Write out the data from csv_to_yaml out to .yaml files + def write_yapl(self, filename, data): + output_path = Path(self.config['yapl_path'].as_filename()) + output_file = output_path / filename + print("Creating file: " + str(output_file)) + with io.open(output_file, 'w', encoding='utf8') as outfile: + yaml.dump(data, outfile, default_flow_style=False, allow_unicode=True) + + ## Take all csv files located at the input path and create yaml representations for them + def csv_to_yapl(self, lib, opts, args): + input_path = Path(self.config['csv_path'].as_filename()) + + csv_files = [f for f in os.listdir(input_path) if f.endswith('.csv')] + for csv_file in csv_files: + + print(f"Parsing {csv_file}") + with io.open(input_path / csv_file, 'r', encoding='utf8') as file: + playlist = csv.DictReader(file) + playlist_fields = playlist.fieldnames + output_name = Path(csv_file).stem + output_file = output_name + ".yaml" + # Defining the dictionary and list that will go inside the dictionary + data = dict() + datalist = list() + # Adding the high level parts of the dict thing + data["name"] = output_name + #print(playlist_fields) + for row in playlist: + tempdict = dict() + for field in playlist_fields: + #print(str(type(row[field]))) + if str(type(row[field])) == "": + if not len(row[field]) == 0: + lowerfield = field.lower() + if lowerfield in fieldstograb: + tempdict[lowerfield] = row[field] + + datalist.append(tempdict) + + print("Export path: " + str(output_file)) + data["tracks"] = datalist + self.write_yapl(output_file, data) + + ## Take all m3u files located at the input path and create yaml representations for them + def get_m3u_paths (self, input_path): + m3u_files = [f for f in os.listdir(input_path) if f.endswith('.m3u8')] + files_list = list() + for m3u_file in m3u_files: + fileinfo = dict() + fileinfo["filename"] = Path(m3u_file).stem + print(f"Parsing {m3u_file}") + paths = list() + parser = py_m3u.M3UParser() + with io.open (input_path / m3u_file, 'r', encoding='utf-8') as file: + try: + audiofiles = parser.load(file) + except: + print("Unable to read file.") else: - tracks = playlist['tracks'] - for track in tracks: - query = [f"{k}:{str(v)}" for k, v in track.items()] - results = lib.items(query) - # Replaced match with if, for python <3.10 - l = len(results) - if l == 1: items.append(results[0]) - elif l == 0: print(f"No results for query: {query}") - else : print(f"Multiple results for query: {query}") - output_file = Path(yaml_file).stem + ".m3u" - self.write_m3u(output_file, playlist, items) - + for audiofile in audiofiles: + #print("Path = " + audiofile.source) + if not str(audiofile.source).endswith("#"): + paths.append(audiofile.source) + fileinfo["paths"] = paths + files_list.append(fileinfo) + return files_list + + + + def m3u_to_yapl_beets(self, lib, opts, args): + input_path = Path(self.config['m3u_path'].as_filename()) + dataforyapl = dict() + file_list = self.get_m3u_paths(input_path) + for file in file_list: + filename = file['filename'] + output_file = filename + ".yaml" + paths = file['paths'] + songlist = list() + foundsongs = [] + dataforyapl["name"] = filename + # For each path in paths, see if it is present in the beets Library + for path in paths: + querystr = f'"path:{path}"' + results = lib.items(querystr) + l = len(results) + # If the path is present, add the song to the foundsongs list + if l == 1: + foundsongs.append(results[0]) + print(f"Results: {results}") + elif l == 0: print(f"No results for query: {querystr}") + else : print(f"Multiple results for query: {querystr}") + # For each song in foundsongs, create a dict to store the metadata, grab the metadata in each field listed in fieldstograb, and add the dict with the metadata to the list of songdata dicts + for song in foundsongs: + songdata = dict() + #fieldstograb = ["album", "artist", "genre", "length", "filesize", "title", "track", "year"] + for grabfield in fieldstograb: + if not len(str(song.get(grabfield))) == 0: + songdata[grabfield] = song.get(grabfield) + songlist.append(songdata) + # Add songlist, which contains loads of songdata dicts, to dataforyapl, and then call the write_yapl function to create a yapl file + dataforyapl["tracks"] = songlist + self.write_yapl(output_file, dataforyapl) + + + def m3u_to_yapl_mp3tag(self, lib, opts, args): + input_path = Path(self.config['m3u_path'].as_filename()) + dataforyapl = dict() + file_list = self.get_m3u_paths(input_path) + # For each file in paths_list, the filename and paths are grabbed + for file in file_list: + failcount = 0 + filename = file['filename'] + output_file = filename + ".yaml" + paths = file['paths'] + songlist = list() + foundsongs = [] + dataforyapl["name"] = filename + # For each path in paths, we are going to grab the metadata + for path in paths: + songdata = dict() + try: + metadata = TinyTag.get(path) + except: + print(f"No file found at the given path: {path}") + failcount = failcount + 1 + else: + mp3fields = fieldstograb.copy() + mp3fields.remove("length") + mp3fields.append("duration") + for grabfield in mp3fields: + if not str(type(getattr(metadata, grabfield))) == "" and not str(getattr(metadata, grabfield)) == '': + if grabfield == "track": + if "/" in metadata.track: + trackval = str(metadata.track).split("/") + songdata["track"] = trackval[0] + else: + songdata["track"] = metadata.track + else: + songdata[grabfield] = getattr(metadata, grabfield) + songlist.append(songdata) + dataforyapl["tracks"] = songlist + print(f"Failcount for {filename}: {failcount}") + self.write_yapl(output_file, dataforyapl) + + + \ No newline at end of file diff --git a/setup.py b/setup.py index 6a6f162..45240cd 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,8 @@ install_requires=[ 'beets>=1.4.7', + 'py-m3u>=0.0.1', + 'tinytag>=1.8.1' ], classifiers=[ diff --git a/test.py b/test.py new file mode 100644 index 0000000..21711cf --- /dev/null +++ b/test.py @@ -0,0 +1,104 @@ +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand +from os.path import relpath +import yaml +import os +from pathlib import Path +# csv imports +import io +import csv +import py_m3u + + +class Yapl: + + ## Write out the data from csv_to_yaml out to .yaml files + #def write_yapl(self, filename, data): + #output_path = Path(self.config['yaml_output_path'].as_filename()) + #output_path = Path("./test/csv_output/" + filename) + + #with io.open(output_path, 'w', encoding='utf8') as outfile: + #yaml.dump(data, outfile, default_flow_style=False, allow_unicode=True) + + ## Take all csv files located at the input path and create yaml representations for them + def csv_to_yapl(self): + #input_path = Path(self.config['csv_input_path'].as_filename()) + input_path = Path('./test/csv_input') + + csv_files = [f for f in os.listdir(input_path) if f.endswith('.csv')] + for csv_file in csv_files: + + print(f"Parsing {csv_file}") + with io.open(input_path / csv_file, 'r', encoding='utf8') as file: + #print("This thing is awesome sauce: " + str(input_path / csv_file)) + playlist = csv.DictReader(file) + playlist_fields = playlist.fieldnames + output_name = Path(csv_file).stem + output_file = output_name + ".yaml" + # Defining the dictionary and list that will go inside the dictionary + data = dict() + datalist = list() + # Adding the high level parts of the dict thing + data["name"] = output_name + + print(playlist_fields) + + for row in playlist: + #pprint.pprint(row) + tempdict = dict() + for field in playlist_fields: + lowerfield = field.lower() + if "path" not in lowerfield: + tempdict[lowerfield] = row[field] + + # Putting values into the temporary dictionary + #tempdict["filename"] = row["Filename"] + #tempdict["title"] = row["Title"] + #tempdict["artist"] = row["Artist"] + #tempdict["album"] = row["Album"] + + datalist.append(tempdict) + + print("Export path: " + str(output_file)) + data["tracks"] = datalist + #self.write_yapl(self, output_file, data) + output_path = Path("./test/csv_output/" + output_file) + + with io.open(output_path, 'w', encoding='utf8') as outfile: + yaml.dump(data, outfile, default_flow_style=False, allow_unicode=True) + + + + def m3u_to_yapl (lib, opts, args): + input_path = Path('./test/m3u_input') + + m3u_files = [f for f in os.listdir(input_path) if f.endswith('.m3u8')] + for m3u_file in m3u_files: + print(f"Parsing {m3u_file}") + #m3upath = str(Path(input_path) / Path(m3u_file)) + paths = list() + parser = py_m3u.M3UParser() + querybase = "path:" + with io.open (input_path / m3u_file, 'r') as file: + audiofiles = parser.load(file) + for audiofile in audiofiles: + #print(audiofile.source) + paths.append(audiofile.source) + #item = library.Item.read() + for path in paths: + querystr = querybase + path + pathquery = queryparse.query_from_strings(queries.AndQuery, library.Item, {}, querystr) + print("Pathquery type: " + str(type(pathquery))) + results = library.Library.items(querystr) + l = len(results) + if l == 1: items.append(results[0]) + elif l == 0: print(f"No results for query: {query}") + else : print(f"Multiple results for query: {query}") + + + + + +beans = Yapl +beans.csv_to_yapl(beans) +#beans.m3u_to_yapl(beans) \ No newline at end of file