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.
@@ -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: []