Dec 19, 2016 Tags: muzak, programming, ruby
Preword: This is the third weekly post on muzak’s development. You can find the rest under the muzak tag.
Some quick statistics:
git diff --stat HEAD HEAD~21
)This week was a big refactoring week, which means that the user facing components haven’t changed much week-over-week. I haven’t even bothered to make a screenshot/recording of this week’s work, since it’s almost entirely internal.
Previously, a playlist could be loaded (and optionally created)
with playlist-load <name>
and modified
with the playlist-add-*
and playlist-del-*
commands. This was simple to
implement since only one playlist could be loaded at a time, but made it
unnecessarily difficult to modify and enqueue multiple playlists during a
single session.
To fix this, I removed playlist-load
entirely and made all playlists
load on startup by default. This requires that playlist-*
commands
now take a playlist name as a first argument.
Additionally, playlists are now automatically created once accessed for the first time:
1
2
3
# ~/.config/playlists/dad-rock.yml
muzak> playlist-add-album dad-rock Meet the Beatles
muzak> enqueue-playlist dad-rock
This should result in a much smoother playlist experience.
Muzak’s configuration is now loaded into a big static Muzak::Const
class
at load time, instead of being created with each instance. This has two major
benefits:
Any program that require
s muzak’s library can now access configuration,
without having to become a client to an active instance.
Components of muzak that aren’t directly tied to a Muzak::Instance
(plugins)
can change their behavior based on user configuration.
Just like instance commands, configuration keys are translated into (class)
methods. For example, deep-index: true
in muzak.yml
becomes
Muzak::Config.deep_index
. There’s also a Config#plugin?(pname)
convenience
method for testing whether a given plugin has been enabled.
Last week, I added ~/.config/muzak/plugins
as a loading directory for user
plugins. This week, I added ~/.config/muzak/commands
as a directory
for custom user commands.
Commands are different from plugins in a few different ways:
Direct access to the muzak instance. Commands are loaded directly into the instance, meaning that they can change the instance’s state as they please. This is very powerful, but it’s also very dangerous when compared to plugins (which only get access to the data supplied by the callback).
Call-based rather than event-based. User commands show up in
Muzak::Cmd.commands
, and are visible in muzak-dmenu
and every other client.
They’re invoked by the user, instead of being executed whenever a
corresponding event is sent.
Not isolated. Since all commands (user or core) are defined under
Muzak::Cmd
and user commands are loaded after core commands, they can
be used to overwrite the core behavior of muzak by redefining the method.
This is almost always a bad idea.
As an example of custom user commands, here are some that I’ve been using:
~/.config/muzak/commands/favorite.rb
:
1
2
3
4
5
6
7
8
9
10
11
module Muzak
module Cmd
def favorite
playlist_add_current "favorites"
end
def unfavorite
playlist_del_current "favorites"
end
end
end
~/.config/muzak/commands/morelike.rb
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module Muzak
module Cmd
def more_by_artist
np = player.now_playing
return unless np
# obvious problem: we're comparing ID3 artist to filesystem artist
songs = index.songs_by(np.artist)
clear_queue
songs.each do |song|
player.enqueue_song song
end
end
def more_from_album
np = player.now_playing
return unless np
# obvious problem: we're comparing ID3 album to filesystem album
album = index.albums[np.album]
return unless album
clear_queue
album.songs.each do |song|
player.enqueue_song song
end
end
end
end
Previously, Muzak::Instance
initialization was a pretty messy affair. There
were lots of calls into methods (doubling as commands) defined in Muzak::Cmd
,
plus lots of small details that should have been handled by the Muzak::Index
,
Muzak::Playlist
, etc. classes individually.
Instance initialization now looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def initialize(opts = {})
$debug = opts[:debug]
$verbose = opts[:verbose]
verbose "muzak is starting..."
@index = Index.new(Config.music, deep: Config.deep_index)
@player = Player::PLAYER_MAP[Config.player].new(self)
@plugins = Plugin.load_plugins!
@playlists = Playlist.load_playlists!
enqueue_playlist Config.autoplay if Config.autoplay
end
Those global variables and player initialization are still a bit of an eyesore,
but they’ll go away eventually (probably via Config
).
Muzak now uses Marshal instead of
YAML to serialize the Muzak::Index
hash,
improving both on-disk size and load speed significantly.
My (deep) index (>10,000 songs by >250 artists) went from nearly 7MB to 3.2MB, and now loads in around 150ms (compared to approximately 1 second with the YAML index). Depending on the performance hit, I may also enable a transparent compression option.
Lots of small bugs were fixed, including a failure to index music files
that ended with non-lowercase suffixes (e.g., song.MP3
) and an incorrectly
placed Thread#join
on event threading.
Thanks for reading!
- William