Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added features to create yapl files #4

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added beetsplug/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added beetsplug/__pycache__/yapl.cpython-39.pyc
Binary file not shown.
209 changes: 187 additions & 22 deletions beetsplug/yapl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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])) == "<class 'str'>":
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))) == "<class 'NoneType'>" 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)



2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

install_requires=[
'beets>=1.4.7',
'py-m3u>=0.0.1',
'tinytag>=1.8.1'
],

classifiers=[
Expand Down
104 changes: 104 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -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)