muzak 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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`?)
@@ -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
@@ -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
@@ -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
+
@@ -0,0 +1,11 @@
1
+ module Muzak
2
+ class Album
3
+ attr_reader :title, :songs, :cover_art
4
+
5
+ def initialize(title, album_hash)
6
+ @title = title
7
+ @songs = album_hash["songs"].map { |s| Song.new(s) }
8
+ @cover_art = album_hash["cover"]
9
+ end
10
+ end
11
+ end
@@ -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