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()