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
@@ -0,0 +1,45 @@
|
|
1
|
+
module Muzak
|
2
|
+
class Playlist
|
3
|
+
attr_accessor :name, :songs
|
4
|
+
|
5
|
+
def self.playlist_names
|
6
|
+
Dir.entries(PLAYLIST_DIR).reject do |ent|
|
7
|
+
ent.start_with?(".")
|
8
|
+
end.map do |ent|
|
9
|
+
File.basename(ent, File.extname(ent))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load_playlist(path)
|
14
|
+
instance = allocate
|
15
|
+
playlist_hash = YAML.load_file(path)
|
16
|
+
|
17
|
+
instance.name = File.basename(path, File.extname(path))
|
18
|
+
instance.songs = playlist_hash["songs"]
|
19
|
+
|
20
|
+
instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(name, songs)
|
24
|
+
@name = name
|
25
|
+
@songs = songs
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(song)
|
29
|
+
return if @songs.include?(song)
|
30
|
+
@songs << song
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(song)
|
34
|
+
@songs.delete(song)
|
35
|
+
end
|
36
|
+
|
37
|
+
def shuffle!
|
38
|
+
@songs.shuffle!
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_hash
|
42
|
+
{ "songs" => songs }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/muzak/plugin.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# we have to require StubPlugin first because ruby's module resolution is bad
|
2
|
+
require_relative "plugin/stub_plugin"
|
3
|
+
|
4
|
+
Dir.glob(File.join(__dir__, "plugin/*")) { |file| require_relative file }
|
5
|
+
|
6
|
+
module Muzak
|
7
|
+
module Plugin
|
8
|
+
def self.plugin_classes
|
9
|
+
constants.map(&Plugin.method(:const_get)).grep(Class)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.plugin_names
|
13
|
+
plugin_classes.map do |pk|
|
14
|
+
pk.plugin_name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
PLUGIN_MAP = plugin_names.zip(plugin_classes).to_h.freeze
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "shellwords"
|
2
|
+
|
3
|
+
module Muzak
|
4
|
+
module Plugin
|
5
|
+
class Cava < StubPlugin
|
6
|
+
include Utils
|
7
|
+
|
8
|
+
def initialize(instance)
|
9
|
+
super
|
10
|
+
@term_args = Shellwords.split instance.config["plugin-cava"]
|
11
|
+
@pid = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def player_activated
|
15
|
+
start_cava! unless cava_running?
|
16
|
+
end
|
17
|
+
|
18
|
+
def player_deactivated
|
19
|
+
stop_cava! if cava_running?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def cava_running?
|
25
|
+
begin
|
26
|
+
!!@pid && Process.waitpid(@pid, Process::WNOHANG).nil?
|
27
|
+
rescue Errno::ECHILD
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def start_cava!
|
33
|
+
args = [*@term_args, "-e", "cava"]
|
34
|
+
@pid = Process.spawn(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop_cava!
|
38
|
+
Process.kill :TERM, @pid
|
39
|
+
Process.wait @pid
|
40
|
+
@pid = nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "digest"
|
3
|
+
|
4
|
+
module Muzak
|
5
|
+
module Plugin
|
6
|
+
class Scrobble < StubPlugin
|
7
|
+
include Utils
|
8
|
+
|
9
|
+
def initialize(instance)
|
10
|
+
super
|
11
|
+
@username, @password_hash = instance.config["plugin-scrobble"].split(":")
|
12
|
+
end
|
13
|
+
|
14
|
+
def song_loaded(song)
|
15
|
+
if song.title.nil? || song.artist.nil?
|
16
|
+
debug "cowardly refusing to scrobble a song ('#{song.path}') with missing metadata"
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
scrobble song
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def scrobble(song)
|
26
|
+
if @username.nil? || @password_hash.nil?
|
27
|
+
error "missing username or password"
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
handshake_endpoint = "http://post.audioscrobbler.com/"
|
32
|
+
handshake_params = {
|
33
|
+
"hs" => true,
|
34
|
+
"p" => 1.1,
|
35
|
+
"c" => "lsd",
|
36
|
+
"v" => "1.0.4",
|
37
|
+
"u" => @username
|
38
|
+
}
|
39
|
+
|
40
|
+
uri = URI(handshake_endpoint)
|
41
|
+
uri.query = URI.encode_www_form(handshake_params)
|
42
|
+
|
43
|
+
resp = Net::HTTP.get_response(uri)
|
44
|
+
|
45
|
+
status, token, post_url, int = resp.body.split("\n")
|
46
|
+
|
47
|
+
unless status =~ /UP(TO)?DATE/
|
48
|
+
error "bad handshake, got '#{status}'"
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
session_token = Digest::MD5.hexdigest(@password_hash + token)
|
53
|
+
|
54
|
+
request_params = {
|
55
|
+
"u" => @username,
|
56
|
+
"s" => session_token,
|
57
|
+
"a[0]" => song.artist,
|
58
|
+
"t[0]" => song.title,
|
59
|
+
"b[0]" => song.album,
|
60
|
+
"m[0]" => "", # we don't know the MBID, so send an empty one
|
61
|
+
"l[0]" => song.length,
|
62
|
+
"i[0]" => Time.now.gmtime.strftime("%Y-%m-%d %H:%M:%S")
|
63
|
+
}
|
64
|
+
|
65
|
+
uri = URI(URI.encode(post_url))
|
66
|
+
# uri.query = URI.encode_www_form(request_params)
|
67
|
+
|
68
|
+
resp = Net::HTTP.post_form(uri, request_params)
|
69
|
+
|
70
|
+
status, int = resp.body.split("\n")
|
71
|
+
|
72
|
+
case status
|
73
|
+
when "OK"
|
74
|
+
debug "scrobble of '#{song.title}' successful"
|
75
|
+
else
|
76
|
+
debug "scrobble of '#{song.title}' failed, got '#{status}'"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Plugin
|
3
|
+
class StubPlugin
|
4
|
+
include Utils
|
5
|
+
|
6
|
+
def self.plugin_name
|
7
|
+
name.split("::").last.downcase
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :instance
|
11
|
+
|
12
|
+
def initialize(instance)
|
13
|
+
@instance = instance
|
14
|
+
debug "loading #{self.class}"
|
15
|
+
end
|
16
|
+
|
17
|
+
PLUGIN_EVENTS.each do |event|
|
18
|
+
define_method(event) do |*args|
|
19
|
+
nil # do nothing.
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
data/lib/muzak/song.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "taglib"
|
2
|
+
|
3
|
+
module Muzak
|
4
|
+
class Song
|
5
|
+
include Utils
|
6
|
+
|
7
|
+
attr_reader :path, :title, :artist, :album, :year, :track, :genre, :comment, :length
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
|
12
|
+
TagLib::FileRef.open(path) do |ref|
|
13
|
+
break if ref.null?
|
14
|
+
@title = ref.tag.title || File.basename(path, File.extname(path))
|
15
|
+
@artist = ref.tag.artist
|
16
|
+
@album = ref.tag.album
|
17
|
+
@year = ref.tag.year
|
18
|
+
@track = ref.tag.track
|
19
|
+
@genre = ref.tag.genre
|
20
|
+
@comment = ref.tag.comment
|
21
|
+
@length = ref.audio_properties.length
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def best_guess_album_art
|
26
|
+
album_dir = File.dirname(path)
|
27
|
+
|
28
|
+
art = Dir.entries(album_dir).find { |ent| album_art?(ent) }
|
29
|
+
File.join(album_dir, art) unless art.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def full_title
|
33
|
+
full = title.dup
|
34
|
+
full << " by #{artist}" if artist
|
35
|
+
full << " on #{album}" if album
|
36
|
+
|
37
|
+
full
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
path == other.path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/muzak/utils.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module Muzak
|
2
|
+
module Utils
|
3
|
+
def music?(filename)
|
4
|
+
[".mp3", ".flac", ".m4a", ".wav", ".ogg", ".oga", ".opus"].include?(File.extname(filename))
|
5
|
+
end
|
6
|
+
|
7
|
+
def album_art?(filename)
|
8
|
+
File.basename(filename) =~ /(cover)|(folder).(jpg)|(png)/i
|
9
|
+
end
|
10
|
+
|
11
|
+
def debug?
|
12
|
+
!!$debug
|
13
|
+
end
|
14
|
+
|
15
|
+
def verbose?
|
16
|
+
!!$verbose
|
17
|
+
end
|
18
|
+
|
19
|
+
def pretty(color = :none, str)
|
20
|
+
colors = {
|
21
|
+
none: 0,
|
22
|
+
red: 31,
|
23
|
+
green: 32,
|
24
|
+
yellow: 33,
|
25
|
+
blue: 34
|
26
|
+
}
|
27
|
+
|
28
|
+
"\e[#{colors[color]}m#{str}\e[0m"
|
29
|
+
end
|
30
|
+
|
31
|
+
def output(box, *args)
|
32
|
+
msg = args.join(" ")
|
33
|
+
puts "[#{box}] #{msg}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def info(*args)
|
37
|
+
output pretty(:green, "info"), args
|
38
|
+
end
|
39
|
+
|
40
|
+
def warn(*args)
|
41
|
+
output pretty(:yellow, "warn"), args
|
42
|
+
end
|
43
|
+
|
44
|
+
def error(*args)
|
45
|
+
output pretty(:red, "error"), "[#{self.class.name}]", args
|
46
|
+
end
|
47
|
+
|
48
|
+
def debug(*args)
|
49
|
+
return unless debug?
|
50
|
+
|
51
|
+
output pretty(:yellow, "debug"), "[#{self.class.name}]", args
|
52
|
+
end
|
53
|
+
|
54
|
+
def verbose(*args)
|
55
|
+
return unless verbose?
|
56
|
+
|
57
|
+
output pretty(:blue, "verbose"), args
|
58
|
+
end
|
59
|
+
|
60
|
+
def warn_arity(args, arity)
|
61
|
+
warn "expected #{arity} arguments, got #{args.length}" unless args.length == arity
|
62
|
+
end
|
63
|
+
|
64
|
+
def fail_arity(args, arity)
|
65
|
+
error "needed #{arity} arguments, got #{args.length}" unless args.length == arity
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: muzak
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- William Woodruff
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: taglib-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.7'
|
27
|
+
description: A library for controlling playlists and media players.
|
28
|
+
email: william@tuffbizz.com
|
29
|
+
executables:
|
30
|
+
- muzak
|
31
|
+
- muzakd
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- LICENSE
|
36
|
+
- README.md
|
37
|
+
- bin/muzak
|
38
|
+
- bin/muzakd
|
39
|
+
- lib/muzak.rb
|
40
|
+
- lib/muzak/album.rb
|
41
|
+
- lib/muzak/cmd.rb
|
42
|
+
- lib/muzak/cmd/config.rb
|
43
|
+
- lib/muzak/cmd/index.rb
|
44
|
+
- lib/muzak/cmd/meta.rb
|
45
|
+
- lib/muzak/cmd/player.rb
|
46
|
+
- lib/muzak/cmd/playlist.rb
|
47
|
+
- lib/muzak/const.rb
|
48
|
+
- lib/muzak/index.rb
|
49
|
+
- lib/muzak/instance.rb
|
50
|
+
- lib/muzak/player.rb
|
51
|
+
- lib/muzak/player/mpv.rb
|
52
|
+
- lib/muzak/player/stub_player.rb
|
53
|
+
- lib/muzak/playlist.rb
|
54
|
+
- lib/muzak/plugin.rb
|
55
|
+
- lib/muzak/plugin/cava.rb
|
56
|
+
- lib/muzak/plugin/notify.rb
|
57
|
+
- lib/muzak/plugin/scrobble.rb
|
58
|
+
- lib/muzak/plugin/stub_plugin.rb
|
59
|
+
- lib/muzak/song.rb
|
60
|
+
- lib/muzak/utils.rb
|
61
|
+
homepage: https://github.com/woodruffw/muzak
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 2.3.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.5.1
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: muzak - A metamusic player.
|
85
|
+
test_files: []
|