muzak 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +38 -0
- data/bin/muzak +50 -0
- data/bin/muzakd +32 -0
- data/lib/muzak.rb +11 -0
- data/lib/muzak/album.rb +11 -0
- data/lib/muzak/cmd.rb +18 -0
- data/lib/muzak/cmd/config.rb +76 -0
- data/lib/muzak/cmd/index.rb +63 -0
- data/lib/muzak/cmd/meta.rb +19 -0
- data/lib/muzak/cmd/player.rb +79 -0
- data/lib/muzak/cmd/playlist.rb +101 -0
- data/lib/muzak/const.rb +14 -0
- data/lib/muzak/index.rb +84 -0
- data/lib/muzak/instance.rb +47 -0
- data/lib/muzak/player.rb +13 -0
- data/lib/muzak/player/mpv.rb +222 -0
- data/lib/muzak/player/stub_player.rb +69 -0
- data/lib/muzak/playlist.rb +45 -0
- data/lib/muzak/plugin.rb +20 -0
- data/lib/muzak/plugin/cava.rb +44 -0
- data/lib/muzak/plugin/notify.rb +16 -0
- data/lib/muzak/plugin/scrobble.rb +81 -0
- data/lib/muzak/plugin/stub_plugin.rb +25 -0
- data/lib/muzak/song.rb +44 -0
- data/lib/muzak/utils.rb +68 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1a171d18242a3650ec6a70d1cedb0bb9e156229c
|
4
|
+
data.tar.gz: 5e5552a3b45cd2ced07e1ff2a79508bd8820dcd6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b02efb2b8ef6f3d2811cf469267c4e4f183459267c8027794a76ce04a2a08738b588ecfbeef8b091aa737b66b6dcea7a7fceadf348a2ae35747c13bb7b0cab50
|
7
|
+
data.tar.gz: b49dea00fefd160228bedc48e5b133b46ce781e5630052068f4f2d632db6c73f35201075173bdf873027821b2dd4e107e74df219834b01ae7027b056909b053f
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 William Woodruff <william @ tuffbizz.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
muzak
|
2
|
+
=====
|
3
|
+
|
4
|
+
muzak is an attempt at a metamusic player.
|
5
|
+
|
6
|
+
It indexes a filesystem tree, providing metadata to a user's preferred media
|
7
|
+
player. It also provides a pseudo-shell for controlling the indexed files
|
8
|
+
and the state of the player.
|
9
|
+
|
10
|
+
### Screenshot
|
11
|
+
|
12
|
+

|
13
|
+
|
14
|
+
### Usage
|
15
|
+
|
16
|
+
Muzak is still a work in process. Don't expect stability or pleasant output.
|
17
|
+
|
18
|
+
On the command-line:
|
19
|
+
|
20
|
+
```shell
|
21
|
+
$ ruby -Ilib muzak.rb # flags: --verbose, --debug
|
22
|
+
muzak> help
|
23
|
+
```
|
24
|
+
|
25
|
+
Daemonized:
|
26
|
+
|
27
|
+
```shell
|
28
|
+
$ ruby -Ilib muzakd.rb
|
29
|
+
echo "command" > ~/.config/muzak/muzak.fifo
|
30
|
+
```
|
31
|
+
|
32
|
+
### TODO
|
33
|
+
|
34
|
+
* current indexing/sorting logic is terrible
|
35
|
+
* index's timestamp should be used for automatic reindexing
|
36
|
+
* album art logic for `mpv` is funky
|
37
|
+
* all the documentation
|
38
|
+
* readline's tab complete is terrible with spaces (maybe use `bond`?)
|
data/bin/muzak
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "muzak"
|
4
|
+
require "readline"
|
5
|
+
require "shellwords"
|
6
|
+
|
7
|
+
opts = {
|
8
|
+
debug: ARGV.include?("--debug") || ARGV.include?("-d"),
|
9
|
+
verbose: ARGV.include?("--verbose") || ARGV.include?("-v"),
|
10
|
+
batch: ARGV.include?("--batch") || ARGV.include?("-b")
|
11
|
+
}
|
12
|
+
|
13
|
+
Thread.abort_on_exception = opts[:debug]
|
14
|
+
|
15
|
+
muzak = Muzak::Instance.new(opts)
|
16
|
+
|
17
|
+
COMMANDS = Muzak::Cmd.commands
|
18
|
+
|
19
|
+
CONFIG_REGEX = /^config-(get)|(set)|(del)/
|
20
|
+
ARTIST_REGEX = Regexp.union COMMANDS.select{ |c| c =~ /artist/ }.map { |c| /^#{c}/ }
|
21
|
+
ALBUM_REGEX = Regexp.union COMMANDS.select{ |c| c =~ /album/ }.map { |c| /^#{c}/ }
|
22
|
+
PLAYLIST_REGEX = /^playlist-(load)|(delete)/
|
23
|
+
|
24
|
+
comp = proc do |s|
|
25
|
+
case Readline.line_buffer
|
26
|
+
when CONFIG_REGEX
|
27
|
+
muzak.config.keys.grep(Regexp.new(Regexp.escape(s)))
|
28
|
+
when ARTIST_REGEX
|
29
|
+
ss = Readline.line_buffer.split(" ")
|
30
|
+
muzak.index.artists.grep(Regexp.new(Regexp.escape(ss[1..-1].join(" "))))
|
31
|
+
when ALBUM_REGEX
|
32
|
+
muzak.index.album_names.grep(Regexp.new(Regexp.escape(s)))
|
33
|
+
when PLAYLIST_REGEX
|
34
|
+
Muzak::Playlist.playlist_names.grep(Regexp.new(Regexp.escape(s)))
|
35
|
+
else
|
36
|
+
COMMANDS.grep(Regexp.new(Regexp.escape(s)))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Readline.completion_append_character = " "
|
41
|
+
Readline.completion_proc = comp
|
42
|
+
|
43
|
+
# ignore interrupts
|
44
|
+
trap("INT", "SIG_IGN")
|
45
|
+
|
46
|
+
while line = Readline.readline("muzak> ", true)
|
47
|
+
cmd_argv = Shellwords.split(line) rescue next
|
48
|
+
next if cmd_argv.empty? || cmd_argv.any?(&:empty?)
|
49
|
+
muzak.send Muzak::Cmd.resolve_command(cmd_argv.shift), *cmd_argv
|
50
|
+
end
|
data/bin/muzakd
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "muzak"
|
4
|
+
require "shellwords"
|
5
|
+
|
6
|
+
opts = {
|
7
|
+
debug: ARGV.include?("--debug") || ARGV.include?("-d"),
|
8
|
+
verbose: ARGV.include?("--verbose") || ARGV.include?("-v"),
|
9
|
+
batch: ARGV.include?("--batch") || ARGV.include?("-b")
|
10
|
+
}
|
11
|
+
|
12
|
+
Process.daemon unless opts[:debug] || opts[:verbose]
|
13
|
+
Thread.abort_on_exception = opts[:debug]
|
14
|
+
|
15
|
+
muzak = Muzak::Instance.new(opts)
|
16
|
+
|
17
|
+
fifo_path = File.join(Muzak::CONFIG_DIR, "muzak.fifo")
|
18
|
+
File.delete(fifo_path) if File.exist?(fifo_path) # just in case a previous session died
|
19
|
+
File.mkfifo(fifo_path)
|
20
|
+
|
21
|
+
File.open(fifo_path, "r") do |fifo|
|
22
|
+
loop do
|
23
|
+
cmd_argv = Shellwords.split(fifo.readline) rescue next
|
24
|
+
next if cmd_argv.empty? || cmd_argv.any?(&:empty?)
|
25
|
+
muzak.send Muzak::Cmd.resolve_command(cmd_argv.shift), *cmd_argv
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# there is definitely a cleaner way to do this.
|
30
|
+
at_exit do
|
31
|
+
File.delete(fifo_path) if File.exist?(fifo_path)
|
32
|
+
end
|
data/lib/muzak.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative "muzak/const"
|
2
|
+
require_relative "muzak/utils"
|
3
|
+
require_relative "muzak/plugin"
|
4
|
+
require_relative "muzak/song"
|
5
|
+
require_relative "muzak/album"
|
6
|
+
require_relative "muzak/playlist"
|
7
|
+
require_relative "muzak/index"
|
8
|
+
require_relative "muzak/cmd"
|
9
|
+
require_relative "muzak/player"
|
10
|
+
require_relative "muzak/instance"
|
11
|
+
|
data/lib/muzak/album.rb
ADDED
data/lib/muzak/cmd.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Dir.glob(File.join(__dir__, "cmd/*")) { |f| require_relative f }
|
2
|
+
|
3
|
+
module Muzak
|
4
|
+
module Cmd
|
5
|
+
def self.resolve_command(cmd)
|
6
|
+
cmd.tr "-", "_"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.resolve_method(meth)
|
10
|
+
meth.to_s.tr "_", "-"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.commands
|
14
|
+
commands = instance_methods.map(&:to_s).reject { |m| m.start_with?("_") }
|
15
|
+
commands.map { |c| c.tr "_", "-" }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Muzak
|
4
|
+
module Cmd
|
5
|
+
def _config_available?
|
6
|
+
File.file?(CONFIG_FILE)
|
7
|
+
end
|
8
|
+
|
9
|
+
def _config_loaded?
|
10
|
+
!!@config
|
11
|
+
end
|
12
|
+
|
13
|
+
def _config_sync
|
14
|
+
debug "syncing config hash with #{CONFIG_FILE}"
|
15
|
+
File.open(CONFIG_FILE, "w") { |io| io.write @config.to_yaml }
|
16
|
+
end
|
17
|
+
|
18
|
+
def _config_init
|
19
|
+
debug "creating a config file in #{CONFIG_FILE}"
|
20
|
+
|
21
|
+
@config = {
|
22
|
+
"music" => File.expand_path("~/music"),
|
23
|
+
"player" => "mpv",
|
24
|
+
}
|
25
|
+
|
26
|
+
Dir.mkdir(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
27
|
+
_config_sync
|
28
|
+
end
|
29
|
+
|
30
|
+
def _config_plugin?(name)
|
31
|
+
@config.key?("plugin-#{name}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def config_load
|
35
|
+
verbose "loading config from #{CONFIG_FILE}"
|
36
|
+
|
37
|
+
@config = YAML::load_file(CONFIG_FILE)
|
38
|
+
end
|
39
|
+
|
40
|
+
def config_set(*args)
|
41
|
+
return unless _config_loaded?
|
42
|
+
|
43
|
+
fail_arity(args, 2)
|
44
|
+
key, value = args
|
45
|
+
return if key.nil? || value.nil?
|
46
|
+
|
47
|
+
debug "setting '#{key}' to '#{value}' in config"
|
48
|
+
|
49
|
+
@config[key] = value
|
50
|
+
_config_sync
|
51
|
+
end
|
52
|
+
|
53
|
+
def config_del(*args)
|
54
|
+
return unless _config_loaded?
|
55
|
+
|
56
|
+
fail_arity(args, 1)
|
57
|
+
key = args.shift
|
58
|
+
return if key.nil?
|
59
|
+
|
60
|
+
debug "removing '#{key}' from config"
|
61
|
+
|
62
|
+
@config.delete(key)
|
63
|
+
_config_sync
|
64
|
+
end
|
65
|
+
|
66
|
+
def config_get(*args)
|
67
|
+
return unless _config_loaded?
|
68
|
+
|
69
|
+
fail_arity(args, 1)
|
70
|
+
key = args.shift
|
71
|
+
return if key.nil?
|
72
|
+
|
73
|
+
info "#{key}: #{@config[key]}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Cmd
|
3
|
+
def _index_available?
|
4
|
+
File.file?(INDEX_FILE)
|
5
|
+
end
|
6
|
+
|
7
|
+
def _index_loaded?
|
8
|
+
!!@index
|
9
|
+
end
|
10
|
+
|
11
|
+
def _index_sync
|
12
|
+
debug "syncing index hash with #{INDEX_FILE}"
|
13
|
+
File.open(INDEX_FILE, "w") { |io| io.write @index.hash.to_yaml }
|
14
|
+
end
|
15
|
+
|
16
|
+
def index_load
|
17
|
+
debug "loading index from #{INDEX_FILE}"
|
18
|
+
|
19
|
+
@index = Index.load_index(INDEX_FILE)
|
20
|
+
end
|
21
|
+
|
22
|
+
def index_build(*args)
|
23
|
+
warn_arity(args, 0)
|
24
|
+
|
25
|
+
@index = Index.new(@config["music"])
|
26
|
+
_index_sync
|
27
|
+
end
|
28
|
+
|
29
|
+
def list_artists(*args)
|
30
|
+
return unless _index_loaded?
|
31
|
+
|
32
|
+
warn_arity(args, 0)
|
33
|
+
|
34
|
+
puts @index.artists.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def list_albums(*args)
|
38
|
+
return unless _index_loaded?
|
39
|
+
|
40
|
+
warn_arity(args, 0)
|
41
|
+
|
42
|
+
puts @index.album_names.join("\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
def albums_by_artist(*args)
|
46
|
+
return unless _index_loaded?
|
47
|
+
|
48
|
+
artist = args.join(" ")
|
49
|
+
return if artist.nil?
|
50
|
+
|
51
|
+
puts @index.albums_by(artist).keys
|
52
|
+
end
|
53
|
+
|
54
|
+
def songs_by_artist(*args)
|
55
|
+
return unless _index_loaded?
|
56
|
+
|
57
|
+
artist = args.join(" ")
|
58
|
+
return if artist.nil?
|
59
|
+
|
60
|
+
puts @index.songs_by(artist)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Cmd
|
3
|
+
def help(*args)
|
4
|
+
commands = Muzak::Cmd.commands.join(", ")
|
5
|
+
info "available commands: #{commands}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def list_plugins
|
9
|
+
plugins = Plugin.plugin_names.join(", ")
|
10
|
+
puts "available plugins: #{plugins}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def quit
|
14
|
+
debug "muzak is quitting..."
|
15
|
+
@player.deactivate!
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Cmd
|
3
|
+
def player_activate
|
4
|
+
if @player.running?
|
5
|
+
warn "player is already running"
|
6
|
+
return
|
7
|
+
end
|
8
|
+
|
9
|
+
@player.activate!
|
10
|
+
end
|
11
|
+
|
12
|
+
def player_deactivate
|
13
|
+
warn "player is not running" unless @player.running?
|
14
|
+
|
15
|
+
# do cleanup even if the player isn't running, just in case
|
16
|
+
@player.deactivate!
|
17
|
+
end
|
18
|
+
|
19
|
+
def play
|
20
|
+
@player.play
|
21
|
+
end
|
22
|
+
|
23
|
+
def pause
|
24
|
+
@player.pause
|
25
|
+
end
|
26
|
+
|
27
|
+
def next
|
28
|
+
@player.next_song
|
29
|
+
end
|
30
|
+
|
31
|
+
def previous
|
32
|
+
@player.previous_song
|
33
|
+
end
|
34
|
+
|
35
|
+
def enqueue_artist(*args)
|
36
|
+
artist = args.join(" ")
|
37
|
+
return if artist.nil?
|
38
|
+
|
39
|
+
album_hashes = @index.albums_by(artist)
|
40
|
+
return if album_hashes.empty?
|
41
|
+
|
42
|
+
albums = album_hashes.map do |album_name, album_hash|
|
43
|
+
Album.new(album_name, album_hash)
|
44
|
+
end
|
45
|
+
|
46
|
+
albums.each do |album|
|
47
|
+
@player.enqueue_album album
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def enqueue_album(*args)
|
52
|
+
album_name = args.join(" ")
|
53
|
+
return if album_name.nil?
|
54
|
+
|
55
|
+
album_hash = @index.albums[album_name]
|
56
|
+
return if album_hash.nil?
|
57
|
+
|
58
|
+
album = Album.new(album_name, album_hash)
|
59
|
+
|
60
|
+
@player.enqueue_album album
|
61
|
+
end
|
62
|
+
|
63
|
+
def list_queue
|
64
|
+
puts @player.list_queue.map(&:title)
|
65
|
+
end
|
66
|
+
|
67
|
+
def shuffle_queue
|
68
|
+
@player.shuffle_queue
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear_queue
|
72
|
+
@player.clear_queue
|
73
|
+
end
|
74
|
+
|
75
|
+
def now_playing
|
76
|
+
info @player.now_playing.full_title
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Cmd
|
3
|
+
def _playlist_file(pname)
|
4
|
+
File.join(PLAYLIST_DIR, pname) + ".yml"
|
5
|
+
end
|
6
|
+
|
7
|
+
def _playlist_available?(pname)
|
8
|
+
File.exist?(_playlist_file(pname))
|
9
|
+
end
|
10
|
+
|
11
|
+
def _playlist_loaded?
|
12
|
+
!!@playlist
|
13
|
+
end
|
14
|
+
|
15
|
+
def list_playlists
|
16
|
+
Playlist.playlist_names.each do |playlist|
|
17
|
+
info playlist
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def playlist_load(*args)
|
22
|
+
fail_arity(args, 1)
|
23
|
+
pname = args.shift
|
24
|
+
|
25
|
+
if _playlist_available?(pname)
|
26
|
+
info "loading playlist '#{pname}'"
|
27
|
+
@playlist = Playlist.load_playlist(_playlist_file(pname))
|
28
|
+
else
|
29
|
+
info "creating playlist '#{pname}'"
|
30
|
+
@playlist = Playlist.new(pname, [])
|
31
|
+
playlist_sync
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def playlist_delete(*args)
|
36
|
+
fail_arity(args, 1)
|
37
|
+
pname = args.shift
|
38
|
+
|
39
|
+
debug "deleting playist '#{pname}'"
|
40
|
+
|
41
|
+
File.delete(_playlist_file(pname)) if _playlist_available?(pname)
|
42
|
+
@playlist = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def playlist_sync(*args)
|
46
|
+
return unless _playlist_loaded?
|
47
|
+
fail_arity(args, 0)
|
48
|
+
|
49
|
+
debug "syncing playlist '#{@playlist.name}'"
|
50
|
+
|
51
|
+
Dir.mkdir(PLAYLIST_DIR) unless Dir.exist?(PLAYLIST_DIR)
|
52
|
+
File.open(_playlist_file(@playlist.name), "w") { |io| io.write @playlist.to_hash.to_yaml }
|
53
|
+
end
|
54
|
+
|
55
|
+
def enqueue_playlist
|
56
|
+
return unless _playlist_loaded?
|
57
|
+
|
58
|
+
@player.enqueue_playlist(@playlist)
|
59
|
+
end
|
60
|
+
|
61
|
+
def playlist_add_album(*args)
|
62
|
+
return unless _playlist_loaded?
|
63
|
+
|
64
|
+
album_name = args.join(" ")
|
65
|
+
return if album_name.nil?
|
66
|
+
|
67
|
+
album_hash = @index.albums[album_name]
|
68
|
+
return if album_hash.nil?
|
69
|
+
|
70
|
+
album = Album.new(album_name, album_hash)
|
71
|
+
|
72
|
+
album.songs.each { |song| @playlist.add song }
|
73
|
+
|
74
|
+
playlist_sync
|
75
|
+
end
|
76
|
+
|
77
|
+
def playlist_add_current
|
78
|
+
return unless @player.running? && _playlist_loaded?
|
79
|
+
|
80
|
+
@playlist.add @player.now_playing
|
81
|
+
|
82
|
+
playlist_sync
|
83
|
+
end
|
84
|
+
|
85
|
+
def playlist_del_current
|
86
|
+
return unless @player.running? && _playlist_loaded?
|
87
|
+
|
88
|
+
@playlist.delete(@player.now_playing)
|
89
|
+
|
90
|
+
playlist_sync
|
91
|
+
end
|
92
|
+
|
93
|
+
def playlist_shuffle
|
94
|
+
return unless _playlist_loaded?
|
95
|
+
|
96
|
+
@playlist.shuffle!
|
97
|
+
|
98
|
+
playlist_sync
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|