muzak 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![screenshot](https://sr.ht/A-oS.png)
|
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
|