Options for scripting or command-line control
-
As long as the smart playlists don't offer all the options I'd like, I'm willing to write an external script to select songs from the database and add them.
What is the best option to...
- Identify songs in the database (mostly interested in songs from local directories). There's no unique song id, as far as I can see. Should I use the whole fingerprint? The url?
- Identify albums in the database. There's an album_id, but that's empty in my case. Should I use album + effictive_albumartist?
- Add songs to the playlist. Should I just use the command-line flag -a or -l, with a list of filenames? Or is there some other way to add songs from the database?
-
For it's worth, this is what I came up with. Note that it's customized for my own use and library, you may need to change some things if you want to use it.
Some notes:
- This is designed to be run when Strawberry is stopped, with an empty (or finished, or not yet started) playlist.
exclude
means the matching directories will be ignored. In this case everything with/Demos/
or/Classical/
.- Since this is for full album random play, it will stop if there is some half-played album (any album with songs with different playcounts).
- I have the
disc
tag only for series like "Complete Works", where I typically don't want to play all discs sequentially. For the more common double disc albums, I just tag all tracks sequentially. If you have disc tags, you may want to tweak the album selection scheme (album_is
).
What I'd appreciate from the Strawberry side:
- A way to add tracks directly from the database, without having to use the URL (e.g. some kind of track ids).
- Some notification when a playlist ends, or a way to run a script when a playlist ends (and possibly some events).
- Support for MPRIS TrackList interface.
- And of course the ability of doing all of this from within the player, without using an external script.
#!/usr/bin/env python3 import sqlite3 import dbus import urllib.parse import re import sys import os.path import subprocess import numpy as np from datetime import datetime, timedelta # Set up database location and options dbfile = '/home/ignacio/.local/share/strawberry/strawberry/strawberry.db' album_is = ['album', 'effective_albumartist', 'disc'] # These identify an album exclude_dirs = ['Demos', 'Classical'] recent = timedelta(days=120) # Format for album in notifications def album_print(row): disc = f' disc {row["disc"]}' if row['disc'] > 0 else '' return f'<i>{row["album"]}</i> ({row["effective_albumartist"]}){disc}' # Set up notification system item = 'org.freedesktop.Notifications' ntf = dbus.Interface(dbus.SessionBus().get_object(item, '/'+item.replace('.', '/')), item) def notify(title='Title', body='Body.'): ntf.Notify('randomalbum', 0, '', title, body, [], {'urgency': 1}, 5000) # Check Strawberry status try: plyr = dbus.Interface(dbus.SessionBus().get_object('org.mpris.MediaPlayer2.strawberry', '/org/mpris/MediaPlayer2'), 'org.freedesktop.DBus.Properties') status = plyr.Get('org.mpris.MediaPlayer2.Player', 'PlaybackStatus') except dbus.exceptions.DBusException: notify('Not running!', 'Strawberry is not running. Exit.') sys.exit(1) if status != 'Stopped': notify('Not stopped!', 'Strawberry is not stopped. Exit.') sys.exit(1) # Open database con = sqlite3.connect(f'file:{dbfile}?mode=ro', uri=True) con.row_factory = sqlite3.Row cur = con.cursor() # Check partially played albums (with multiple playcounts) album_is_list = ', '.join(album_is) res = cur.execute(f'SELECT COUNT ( DISTINCT playcount ) AS "nr_of_playcounts", {album_is_list} FROM songs GROUP BY {album_is_list}') first_songs = res.fetchall() msg = '' for i in first_songs: msg = [] if i['nr_of_playcounts'] > 1: msg += '* '+album_print(i) if msg: notify('Multiple playcounts', '\n'.join(msg)) sys.exit(1) # Get data for the first song of each album res = cur.execute(f'SELECT MIN(track), url, playcount, lastplayed, {album_is_list} FROM songs GROUP BY {album_is_list}') first_songs = res.fetchall() now = datetime.now() exclude_re = '/(' + '|'.join([urllib.parse.quote(i, safe="/(),'!&;+=") for i in exclude_dirs]) + ')/' mask = [True for i in first_songs] # Manually exclude directories for i,j in enumerate(first_songs): if re.search(exclude_re,j['url']): mask[i] = False max_playcount = max([j['playcount'] for i,j in enumerate(first_songs) if mask[i]]) # Exclude recently-played songs for i,j in enumerate(first_songs): dt = datetime.fromtimestamp(j['lastplayed']) if now-dt < recent: mask[i] = False # Tentatively exclude max-played songs, # unless the result is empty mp_mask = [True for i in first_songs] for i,j in enumerate(first_songs): if j['playcount'] >= max_playcount: mp_mask[i] = False new_mask = [i and j for i,j in zip(mask,mp_mask)] if any(new_mask): mask = new_mask else: notify('Library exhausted', f'Increasing max. playcount to {max_playcount+1}.') # Pick a random song out of the remaining ones weights = np.array([1 if i else 0 for i in mask]) left = np.count_nonzero(weights) pick = np.random.choice(len(mask), p=weights/np.sum(weights)) pick = first_songs[pick] filename = urllib.parse.unquote(urllib.parse.urlsplit(pick['url']).path) if (os.path.isfile(filename)): # Select the full album (disc) from the picked song notify('Next album', album_print(pick) + f'\n[out of {left}]') album = tuple([pick[i] for i in album_is]) album_exp = ' AND '.join([f'{i} IS ?' for i in album_is]) res = cur.execute(f'SELECT url FROM songs WHERE {album_exp} ORDER BY track', album) # Add the album, replacing the current playlist subprocess.call(['strawberry', '-l'] + [i['url'] for i in res.fetchall()]) else: notify('Missing file', f'File not found. Is the library mounted?\n{filename}') sys.exit(1) con.close()