muzak 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Muzak
2
+ VERSION = "0.0.1".freeze
3
+
4
+ CONFIG_DIR = File.expand_path("~/.config/muzak").freeze
5
+ CONFIG_FILE = File.join(CONFIG_DIR, "muzak.yml").freeze
6
+ INDEX_FILE = File.join(CONFIG_DIR, "index.yml").freeze
7
+ PLAYLIST_DIR = File.join(CONFIG_DIR, "playlists").freeze
8
+
9
+ PLUGIN_EVENTS = [
10
+ :player_activated,
11
+ :player_deactivated,
12
+ :song_loaded
13
+ ]
14
+ end
@@ -0,0 +1,84 @@
1
+ module Muzak
2
+ class Index
3
+ include Utils
4
+ attr_accessor :hash
5
+
6
+ def self.load_index(file)
7
+ instance = allocate
8
+ instance.hash = YAML::load_file(file)
9
+
10
+ instance
11
+ end
12
+
13
+ def initialize(tree)
14
+ @hash = {
15
+ "timestamp" => Time.now.to_i,
16
+ "artists" => {}
17
+ }
18
+
19
+ Dir.entries(tree).each do |artist|
20
+ next unless File.directory?(File.join(tree, artist))
21
+ next if artist.start_with?(".")
22
+
23
+ @hash["artists"][artist] = {}
24
+ @hash["artists"][artist]["albums"] = {}
25
+
26
+ Dir.entries(File.join(tree, artist)).each do |album|
27
+ next if album.start_with?(".")
28
+
29
+ @hash["artists"][artist]["albums"][album] = {}
30
+ @hash["artists"][artist]["albums"][album]["songs"] = []
31
+
32
+ Dir.glob(File.join(tree, artist, album, "**")) do |file|
33
+ @hash["artists"][artist]["albums"][album]["cover"] = file if album_art?(file)
34
+ @hash["artists"][artist]["albums"][album]["songs"] << file if music?(file)
35
+ end
36
+
37
+ @hash["artists"][artist]["albums"][album]["songs"].sort!
38
+ end
39
+ end
40
+ end
41
+
42
+ def artists
43
+ @hash["artists"].keys
44
+ end
45
+
46
+ def albums
47
+ albums_hash = {}
48
+
49
+ artists.each do |a|
50
+ @hash["artists"][a]["albums"].keys.each do |ak|
51
+ albums_hash[ak] = @hash["artists"][a]["albums"][ak]
52
+ end
53
+ end
54
+
55
+ albums_hash
56
+ end
57
+
58
+ def album_names
59
+ artists.map { |a| @hash["artists"][a]["albums"].keys }.flatten
60
+ end
61
+
62
+ def albums_by(artist)
63
+ error "no such artist: '#{artist}'" unless @hash["artists"].key?(artist)
64
+
65
+ begin
66
+ @hash["artists"][artist]["albums"]
67
+ rescue Exception => e
68
+ {}
69
+ end
70
+ end
71
+
72
+ def songs_by(artist)
73
+ error "no such artist: '#{artist}'" unless @hash["artists"].key?(artist)
74
+
75
+ begin
76
+ albums_by(artist).map do |_, album|
77
+ album["songs"].map { |s| File.basename(s) }.sort
78
+ end.flatten
79
+ rescue Exception => e
80
+ []
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ module Muzak
2
+ class Instance
3
+ include Cmd
4
+ include Utils
5
+
6
+ def method_missing(meth, *args)
7
+ warn "unknown command: #{Cmd.resolve_method(meth)}"
8
+ help
9
+ end
10
+
11
+ attr_reader :config, :player, :index, :playlist
12
+
13
+ def initialize(opts = {})
14
+ $debug = opts[:debug]
15
+ $verbose = opts[:verbose]
16
+
17
+ debug "muzak is starting..."
18
+
19
+ _config_init unless _config_available?
20
+ config_load
21
+
22
+ index_build unless _index_available?
23
+ index_load
24
+
25
+ @player = Player::PLAYER_MAP[@config["player"]].new(self)
26
+
27
+ playlist_load @config["default-playlist"] if @config["default-playlist"]
28
+
29
+ @plugins = initialize_plugins!
30
+ end
31
+
32
+ def initialize_plugins!
33
+ pks = Plugin.plugin_classes.select { |pk| _config_plugin? pk.plugin_name }
34
+ pks.map { |pk| pk.new(self) }
35
+ end
36
+
37
+ def event(type, *args)
38
+ return unless PLUGIN_EVENTS.include?(type)
39
+
40
+ @plugins.each do |plugin|
41
+ Thread.new do
42
+ plugin.send(type, *args)
43
+ end.join
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # we have to require StubPlayer first because ruby's module resolution is bad
2
+ require_relative "player/stub_player"
3
+
4
+ Dir.glob(File.join(__dir__, "player/*")) { |file| require_relative file }
5
+
6
+ module Muzak
7
+ module Player
8
+ PLAYER_MAP = {
9
+ "stub" => Player::StubPlayer,
10
+ "mpv" => Player::MPV
11
+ }
12
+ end
13
+ end
@@ -0,0 +1,222 @@
1
+ require "tempfile"
2
+ require "socket"
3
+ require "json"
4
+ require "thread"
5
+
6
+ module Muzak
7
+ module Player
8
+ class MPV < StubPlayer
9
+ def running?
10
+ begin
11
+ !!@pid && Process.waitpid(@pid, Process::WNOHANG).nil?
12
+ rescue Errno::ECHILD
13
+ false
14
+ end
15
+ end
16
+
17
+ def activate!
18
+ return if running?
19
+
20
+ debug "activating #{self.class}"
21
+
22
+ @sock_path = Dir::Tmpname.make_tmpname("/tmp/mpv", ".sock")
23
+ mpv_args = [
24
+ "--idle",
25
+ "--no-osc",
26
+ "--no-osd-bar",
27
+ "--no-input-default-bindings",
28
+ "--no-input-cursor",
29
+ "--no-terminal",
30
+ "--load-scripts=no", # autoload and other scripts with clobber our mpv management
31
+ "--input-ipc-server=%{socket}" % { socket: @sock_path }
32
+ ]
33
+
34
+ mpv_args << "--geometry=#{instance.config["art-geometry"]}" if instance.config["art-geometry"]
35
+
36
+ @pid = Process.spawn("mpv", *mpv_args)
37
+
38
+ until File.exists?(@sock_path)
39
+ sleep 0.1
40
+ end
41
+
42
+ @socket = UNIXSocket.new(@sock_path)
43
+
44
+ @command_queue = Queue.new
45
+ @result_queue = Queue.new
46
+ @event_queue = Queue.new
47
+
48
+ @command_thread = Thread.new { pump_commands! }
49
+ @results_thread = Thread.new { pump_results! }
50
+ @events_thread = Thread.new { dispatch_events! }
51
+
52
+ instance.event :player_activated
53
+ end
54
+
55
+ def deactivate!
56
+ return unless running?
57
+
58
+ debug "deactivating #{self.class}"
59
+
60
+ command "quit"
61
+
62
+ Process.kill :TERM, @pid
63
+ Process.wait @pid
64
+ @pid = nil
65
+
66
+ @socket.close
67
+ ensure
68
+ instance.event :player_deactivated
69
+ File.delete(@sock_path) if @sock_path && File.exists?(@sock_path)
70
+ end
71
+
72
+ def play
73
+ return unless running?
74
+
75
+ set_property "pause", false
76
+ end
77
+
78
+ def pause
79
+ return unless running?
80
+
81
+ set_property "pause", true
82
+ end
83
+
84
+ def playing?
85
+ return false unless running?
86
+
87
+ !get_property "pause"
88
+ end
89
+
90
+ def next_song
91
+ command "playlist-next"
92
+ end
93
+
94
+ def previous_song
95
+ command "playlist-prev"
96
+ end
97
+
98
+ def enqueue_song(song)
99
+ activate! unless running?
100
+
101
+ load_song song, song.best_guess_album_art
102
+ end
103
+
104
+ def enqueue_album(album)
105
+ activate! unless running?
106
+
107
+ album.songs.each do |song|
108
+ load_song song, album.cover_art
109
+ end
110
+ end
111
+
112
+ def enqueue_playlist(playlist)
113
+ activate! unless running?
114
+
115
+ playlist.songs.each do |song|
116
+ load_song song, song.best_guess_album_art
117
+ end
118
+ end
119
+
120
+ def list_queue
121
+ entries = get_property "playlist/count"
122
+
123
+ playlist = []
124
+
125
+ entries.times do |i|
126
+ playlist << Song.new(get_property("playlist/#{i}/filename"))
127
+ end
128
+
129
+ playlist
130
+ end
131
+
132
+ def shuffle_queue
133
+ command "playlist-shuffle"
134
+ end
135
+
136
+ def clear_queue
137
+ command "playlist-clear"
138
+ end
139
+
140
+ def now_playing
141
+ Song.new(get_property "path")
142
+ end
143
+
144
+ private
145
+
146
+ def load_song(song, art)
147
+ cmds = ["loadfile", song.path, "append-play"]
148
+ cmds << "external-file=#{art}" if art
149
+ command *cmds
150
+ end
151
+
152
+ def pump_commands!
153
+ loop do
154
+ begin
155
+ @socket.puts(@command_queue.pop)
156
+ rescue EOFError # the player is deactivating
157
+ Thread.exit
158
+ end
159
+ end
160
+ end
161
+
162
+ def pump_results!
163
+ loop do
164
+ begin
165
+ response = JSON.parse(@socket.readline)
166
+
167
+ if response["event"]
168
+ @event_queue << response["event"]
169
+ else
170
+ @result_queue << response
171
+ end
172
+ rescue EOFError # the player is deactivating
173
+ Thread.exit
174
+ end
175
+ end
176
+ end
177
+
178
+ def dispatch_events!
179
+ loop do
180
+ event = @event_queue.pop
181
+
182
+ Thread.new do
183
+ case event
184
+ when "file-loaded"
185
+ # this really isn't ideal, since we already have access
186
+ # to Song objects earlier in the object's lifetime.
187
+ # the "correct" way to do this would be to sync an external
188
+ # playlist with mpv's internal one and access that instead
189
+ # of re-creating the Song from mpv properties.
190
+ # another idea: serialize Song objects into mpv's properties
191
+ # somehow.
192
+ song = Song.new(get_property "path")
193
+ instance.event :song_loaded, song
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def command(*args)
200
+ return unless running?
201
+
202
+ payload = {
203
+ "command" => args
204
+ }
205
+
206
+ debug "mpv payload: #{payload.to_s}"
207
+
208
+ @command_queue << JSON.generate(payload)
209
+
210
+ @result_queue.pop
211
+ end
212
+
213
+ def set_property(*args)
214
+ command "set_property", *args
215
+ end
216
+
217
+ def get_property(*args)
218
+ command("get_property", *args)["data"]
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,69 @@
1
+ module Muzak
2
+ module Player
3
+ class StubPlayer
4
+ include Utils
5
+
6
+ attr_reader :instance
7
+
8
+ def initialize(instance)
9
+ @instance = instance
10
+ end
11
+
12
+ def running?
13
+ debug "#running?"
14
+ end
15
+
16
+ def activate!
17
+ debug "#activate!"
18
+ end
19
+
20
+ def deactivate!
21
+ debug "#deactivate!"
22
+ end
23
+
24
+ def play
25
+ debug "#play"
26
+ end
27
+
28
+ def pause
29
+ debug "#pause"
30
+ end
31
+
32
+ def next_song
33
+ debug "#next_song"
34
+ end
35
+
36
+ def previous_song
37
+ debug "#previous_song"
38
+ end
39
+
40
+ def enqueue_song(song)
41
+ debug "#enqueue_song"
42
+ end
43
+
44
+ def enqueue_album(album)
45
+ debug "#enqueue_album"
46
+ end
47
+
48
+ def enqueue_playlist(playlist)
49
+ debug "#enqueue_playlist"
50
+ end
51
+
52
+ def list_queue
53
+ debug "#list_queue"
54
+ end
55
+
56
+ def shuffle_queue
57
+ debug "#shuffle_queue"
58
+ end
59
+
60
+ def clear_queue
61
+ debug "#clear_queue"
62
+ end
63
+
64
+ def now_playing
65
+ debug "#now_playing"
66
+ end
67
+ end
68
+ end
69
+ end