muzak 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,16 @@
1
+ module Muzak
2
+ module Plugin
3
+ class Notify < StubPlugin
4
+ def song_loaded(song)
5
+ notify song.full_title
6
+ end
7
+
8
+ private
9
+
10
+ def notify(msg)
11
+ pid = Process.spawn("notify-send", "muzak", msg)
12
+ Process.detach(pid)
13
+ end
14
+ end
15
+ end
16
+ 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
+
@@ -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
@@ -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: []