hearken 0.0.1 → 0.0.2
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.
- data/.gitignore +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +2 -4
- data/README.rdoc +48 -0
- data/Rakefile +5 -0
- data/bin/hearken +7 -0
- data/hearken.gemspec +9 -0
- data/lib/hearken.rb +1 -5
- data/lib/hearken/cli.rb +24 -0
- data/lib/hearken/command.rb +35 -0
- data/lib/hearken/command/enqueue.rb +12 -0
- data/lib/hearken/command/flush.rb +7 -0
- data/lib/hearken/command/list.rb +29 -0
- data/lib/hearken/command/recent.rb +31 -0
- data/lib/hearken/command/reload.rb +7 -0
- data/lib/hearken/command/restart.rb +7 -0
- data/lib/hearken/command/scrobbling.rb +14 -0
- data/lib/hearken/command/search.rb +25 -0
- data/lib/hearken/command/setup_scrobbling.rb +7 -0
- data/lib/hearken/command/show_properties.rb +9 -0
- data/lib/hearken/command/shuffle.rb +13 -0
- data/lib/hearken/command/start.rb +7 -0
- data/lib/hearken/command/status.rb +7 -0
- data/lib/hearken/command/stop.rb +7 -0
- data/lib/hearken/console.rb +34 -0
- data/lib/hearken/debug.rb +16 -0
- data/lib/hearken/indexing.rb +7 -0
- data/lib/hearken/indexing/audio_traverser.rb +19 -0
- data/lib/hearken/indexing/executor.rb +33 -0
- data/lib/hearken/indexing/ffmpeg_file.rb +51 -0
- data/lib/hearken/indexing/file.rb +13 -0
- data/lib/hearken/indexing/indexer.rb +28 -0
- data/lib/hearken/indexing/parser.rb +7 -0
- data/lib/hearken/indexing/persistant_traverser.rb +32 -0
- data/lib/hearken/indexing/persisted_traverser.rb +30 -0
- data/lib/hearken/library.rb +46 -0
- data/lib/hearken/player.rb +91 -0
- data/lib/hearken/preferences.rb +29 -0
- data/lib/hearken/queue.rb +26 -0
- data/lib/hearken/range_expander.rb +30 -0
- data/lib/hearken/scrobbler.rb +76 -0
- data/lib/hearken/tagged.rb +15 -0
- data/lib/hearken/track.rb +38 -0
- data/lib/hearken/version.rb +2 -2
- data/media/applause.mp3 +0 -0
- data/spec/hearken/command/enqueue_spec.rb +24 -0
- data/spec/hearken/command/list_spec.rb +31 -0
- data/spec/hearken/command/reload_spec.rb +20 -0
- data/spec/hearken/command/shuffle_spec.rb +27 -0
- data/spec/hearken/player_spec.rb +37 -0
- data/spec/hearken/range_expander_spec.rb +28 -0
- data/spec/spec_helper.rb +5 -0
- metadata +136 -8
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'hearken/indexing/file'
|
3
|
+
|
4
|
+
class Hearken::Indexing::AudioTraverser
|
5
|
+
attr_reader :current
|
6
|
+
EXTS = %w{m4a mp3 ogg wma}.map {|e| '.'+e }
|
7
|
+
|
8
|
+
def initialize path
|
9
|
+
@path = Pathname.new path
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
@path.find { |child| yield Hearken::Indexing::File.new child, @path if is_audio? child }
|
14
|
+
end
|
15
|
+
private
|
16
|
+
def is_audio? current
|
17
|
+
current.file? and EXTS.include? current.extname.downcase
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'hearken/tagged'
|
2
|
+
|
3
|
+
class String
|
4
|
+
def escape char
|
5
|
+
gsub(char, "\\#{char}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def escape2 char
|
9
|
+
split(char).join("\\#{char}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Hearken::Indexing::Executor
|
14
|
+
include Hearken::Tagged
|
15
|
+
|
16
|
+
def extract_file_attributes path
|
17
|
+
@path = path.to_s
|
18
|
+
@timestamp = path.timestamp
|
19
|
+
end
|
20
|
+
|
21
|
+
def clean_path path
|
22
|
+
path.escape(" ").escape2("'").escape("!").escape2("`").escape("(").escape(")").escape2("&").escape2(";")
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute command
|
26
|
+
debug command
|
27
|
+
`#{command} 2>&1`
|
28
|
+
end
|
29
|
+
|
30
|
+
def debug message
|
31
|
+
puts message if ENV['DEBUG']
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'hearken/indexing/executor'
|
2
|
+
|
3
|
+
class Hearken::Indexing::FfmpegFile
|
4
|
+
include Hearken::Indexing::Executor
|
5
|
+
|
6
|
+
def initialize path
|
7
|
+
extract_file_attributes path
|
8
|
+
content = execute "ffmpeg -i #{clean_path @path}"
|
9
|
+
state = :draining
|
10
|
+
@meta = {}
|
11
|
+
content.each_line do |line|
|
12
|
+
l = line.chomp
|
13
|
+
case l
|
14
|
+
when " Metadata:"
|
15
|
+
state = :metadata
|
16
|
+
else
|
17
|
+
if state == :metadata
|
18
|
+
begin
|
19
|
+
m = / *: */.match l
|
20
|
+
@meta[m.pre_match.strip] = m.post_match.strip if m
|
21
|
+
rescue ArgumentError => e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@title = @meta['TIT2'] || @meta['title']
|
28
|
+
@album = @meta['TALB'] || @meta ['album']
|
29
|
+
@artist = @meta['TPE1'] || @meta['TPE2'] || @meta['artist']
|
30
|
+
@albumartist = @meta['TSO2'] || @meta['album_artist']
|
31
|
+
@time = to_duration @meta['Duration']
|
32
|
+
@date = @meta['TDRC'] || @meta['TYER'] || @meta['date']
|
33
|
+
@track = @meta['TRCK'] || @meta['track']
|
34
|
+
@puid = @meta['MusicIP PUID']
|
35
|
+
@mbartistid = @meta['MusicBrainz Artist Id']
|
36
|
+
@mbalbumid = @meta['MusicBrainz Album Id']
|
37
|
+
@mbalbumartistid = @meta['MusicBrainz Album Artist Id']
|
38
|
+
@asin = @meta['ASIN']
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing method
|
42
|
+
@meta[method.to_s]
|
43
|
+
end
|
44
|
+
private
|
45
|
+
def to_duration s
|
46
|
+
return nil unless s
|
47
|
+
first, *rest = s.split ','
|
48
|
+
hours, minutes, seconds = first.split ':'
|
49
|
+
seconds.to_i + (minutes.to_i * 60) + (hours.to_i * 60 * 60)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'splat'
|
2
|
+
require 'hearken/indexing/persistant_traverser'
|
3
|
+
|
4
|
+
class Hearken::Indexing::Indexer
|
5
|
+
def initialize path
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute
|
10
|
+
start = Time.now
|
11
|
+
count = 0
|
12
|
+
traverser = Hearken::Indexing::PersistantTraverser.new @path, Hearken::Indexing::PATH
|
13
|
+
|
14
|
+
traverser.each do |audio_file|
|
15
|
+
count += 1
|
16
|
+
show_progress start, count if count % 1000 == 0
|
17
|
+
end
|
18
|
+
|
19
|
+
show_progress start, count
|
20
|
+
|
21
|
+
(File.dirname(__FILE__)+'/../../../media/applause.mp3').to_player
|
22
|
+
end
|
23
|
+
private
|
24
|
+
def show_progress start, count
|
25
|
+
elapsed = Time.now-start
|
26
|
+
$stderr.puts "Processed #{count} audio files in #{elapsed} seconds (#{elapsed/count} per file)"
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'hearken/indexing/persisted_traverser'
|
2
|
+
require 'hearken/indexing/audio_traverser'
|
3
|
+
require 'hearken/indexing/parser'
|
4
|
+
|
5
|
+
class Hearken::Indexing::PersistantTraverser
|
6
|
+
def initialize audio_path, store_path
|
7
|
+
@audio_path, @store_path = audio_path, store_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def each
|
11
|
+
with_existing_entries do |existing_entries, persisted|
|
12
|
+
Hearken::Indexing::AudioTraverser.new(@audio_path).each do |path|
|
13
|
+
existing = existing_entries[path.to_s]
|
14
|
+
existing = nil if existing and existing.timestamp != path.timestamp
|
15
|
+
existing = nil if existing and existing.no_tag_fields?
|
16
|
+
track = existing || Hearken::Indexing::Parser.parse(path)
|
17
|
+
persisted.append track
|
18
|
+
yield track
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
private
|
23
|
+
def with_existing_entries
|
24
|
+
persisted = Hearken::Indexing::PersistedTraverser.new @store_path
|
25
|
+
entries = {}
|
26
|
+
persisted.each do |entry|
|
27
|
+
entries[entry.path] = entry
|
28
|
+
end
|
29
|
+
persisted.clear
|
30
|
+
yield entries, persisted
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'hearken/track'
|
2
|
+
|
3
|
+
class Hearken::Indexing::PersistedTraverser
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize path
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def each
|
11
|
+
File.open @path do |file|
|
12
|
+
while line = file.gets
|
13
|
+
row = line.chomp.split '<->'
|
14
|
+
track = Hearken::Track.new
|
15
|
+
Hearken::Tagged::FIELDS.each {|field| track.send "#{field}=", row.shift }
|
16
|
+
yield track
|
17
|
+
end
|
18
|
+
end if File.exist? @path
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
File.open @path, 'w'
|
23
|
+
end
|
24
|
+
|
25
|
+
def append track
|
26
|
+
File.open(@path, 'a') do |file|
|
27
|
+
file.puts track.to_a.join('<->')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'hearken/track'
|
2
|
+
|
3
|
+
class Hearken::Library
|
4
|
+
FILE_FIELDS = %w{path timestamp}
|
5
|
+
TAG_FIELDS = %w{album track title artist time date albumartist puid mbartistid mbalbumid mbalbumartistid asin}
|
6
|
+
FIELDS = FILE_FIELDS + TAG_FIELDS
|
7
|
+
|
8
|
+
include Hearken::Debug
|
9
|
+
attr_reader :tracks
|
10
|
+
|
11
|
+
def initialize preferences
|
12
|
+
end
|
13
|
+
|
14
|
+
def count
|
15
|
+
@tracks.count
|
16
|
+
end
|
17
|
+
|
18
|
+
def row id
|
19
|
+
@tracks[id]
|
20
|
+
end
|
21
|
+
|
22
|
+
def path row
|
23
|
+
row.path
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_track id
|
27
|
+
yield @tracks[id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def reload
|
31
|
+
s = Time.now.to_i
|
32
|
+
path = File.expand_path('~')+'/.music'
|
33
|
+
@tracks = []
|
34
|
+
File.open path do |file|
|
35
|
+
id = 0
|
36
|
+
while line = file.gets
|
37
|
+
row = line.chomp.split '<->'
|
38
|
+
track = Hearken::Track.new id
|
39
|
+
FIELDS.each {|field| track.send "#{field}=", row.shift }
|
40
|
+
@tracks << track
|
41
|
+
id += 1
|
42
|
+
end
|
43
|
+
end if File.exist? path
|
44
|
+
puts "Reloaded db with #{@tracks.size} tracks in #{Time.now.to_i-s} seconds"
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'splat'
|
2
|
+
|
3
|
+
require 'hearken/queue'
|
4
|
+
require 'hearken/scrobbler'
|
5
|
+
require 'hearken/library'
|
6
|
+
|
7
|
+
module Hearken
|
8
|
+
class Player
|
9
|
+
include Queue
|
10
|
+
attr_reader :library, :scrobbler
|
11
|
+
attr_accessor :scrobbling, :matches
|
12
|
+
|
13
|
+
def initialize preferences
|
14
|
+
@scrobbler = Scrobbler.new preferences
|
15
|
+
@scrobbling = true
|
16
|
+
@library = Library.new preferences
|
17
|
+
@library.reload
|
18
|
+
end
|
19
|
+
|
20
|
+
def c text,colour
|
21
|
+
text.to_s.foreground colour
|
22
|
+
end
|
23
|
+
|
24
|
+
def status
|
25
|
+
if @pid
|
26
|
+
track = self.current
|
27
|
+
puts "Since #{c Time.at(track.started), :cyan}\n\t#{track}"
|
28
|
+
played = Time.now.to_i-track.started
|
29
|
+
puts "#{c played, :yellow} seconds (#{c track.time.to_i-played, :yellow} remaining)" if track.time
|
30
|
+
else
|
31
|
+
puts 'not playing'.foreground(:yellow)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def current
|
36
|
+
(@pid and File.exist?('current_song')) ? YAML.load_file('current_song') : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def register track
|
40
|
+
track.started = Time.now.to_i
|
41
|
+
File.open('current_song', 'w') {|f| f.print track.to_yaml }
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
if @pid
|
46
|
+
puts "Already started (pid #{@pid})"
|
47
|
+
return
|
48
|
+
end
|
49
|
+
@pid = fork do
|
50
|
+
player_pid = nil
|
51
|
+
Signal.trap('TERM') do
|
52
|
+
Process.kill 'TERM', player_pid if player_pid
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
total_tracks = @library.count
|
56
|
+
loop do
|
57
|
+
id = dequeue || (rand * total_tracks).to_i
|
58
|
+
row = @library.row id
|
59
|
+
unless row
|
60
|
+
puts "track with id #{id} did not exist"
|
61
|
+
next
|
62
|
+
end
|
63
|
+
path = @library.path row
|
64
|
+
unless path and File.exist? path
|
65
|
+
puts "track with id #{id} did not refer to a file"
|
66
|
+
next
|
67
|
+
end
|
68
|
+
@library.with_track(id) do |track|
|
69
|
+
@scrobbler.update track if @scrobbling
|
70
|
+
register track
|
71
|
+
player_pid = path.to_player
|
72
|
+
Process.wait player_pid
|
73
|
+
@scrobbler.scrobble track if @scrobbling
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
puts "Started (pid #{@pid})"
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop
|
81
|
+
return unless @pid
|
82
|
+
Process.kill 'TERM', @pid
|
83
|
+
@pid = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def restart
|
87
|
+
stop
|
88
|
+
start
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hearken
|
2
|
+
class Preferences
|
3
|
+
def initialize
|
4
|
+
@preference_path = home_path '.hearken'
|
5
|
+
if File.exists? @preference_path
|
6
|
+
@preferences = YAML.load File.read(@preference_path)
|
7
|
+
else
|
8
|
+
@preferences = {}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def home_path *paths
|
13
|
+
File.join File.expand_path('~'), *paths
|
14
|
+
end
|
15
|
+
|
16
|
+
def [] key
|
17
|
+
@preferences[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def []= key, value
|
21
|
+
@preferences[key] = value
|
22
|
+
persist
|
23
|
+
end
|
24
|
+
|
25
|
+
def persist
|
26
|
+
File.open(@preference_path, 'w') {|f| f.puts @preferences.to_yaml}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hearken
|
2
|
+
module Queue
|
3
|
+
def enqueue id
|
4
|
+
@sequence ||= 0
|
5
|
+
@library.with_track id do |track|
|
6
|
+
File.open("#{Time.now.to_i}-#{@sequence.to_s.rjust(8,'0')}.song", 'w') {|f| f.print track.to_yaml }
|
7
|
+
@sequence += 1
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def each
|
12
|
+
Dir.glob('*.song').sort.each do |file|
|
13
|
+
yield YAML.load File.read(file)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def dequeue
|
18
|
+
file = Dir.glob('*.song').sort.first
|
19
|
+
return nil unless file
|
20
|
+
hash = YAML.load(File.read(file))
|
21
|
+
id = hash[:id] if hash
|
22
|
+
FileUtils.rm file
|
23
|
+
id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hearken
|
2
|
+
class RangeExpander
|
3
|
+
def expand text
|
4
|
+
text.split(/[^0-9a-z-]/).inject([]) {|acc, term| acc + expand_term(term) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def expand_to_ids text
|
8
|
+
expand(text).map {|number| from_number number }
|
9
|
+
end
|
10
|
+
private
|
11
|
+
def expand_term term
|
12
|
+
words = term.split '-'
|
13
|
+
words.empty? ? [] : range(words.first, words.last)
|
14
|
+
end
|
15
|
+
|
16
|
+
def range from, to
|
17
|
+
f, t = to_number(from), to_number(to)
|
18
|
+
t = to_number(from.slice(0...from.size-to.size)+to) if t < f
|
19
|
+
(f..t).to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_number term
|
23
|
+
term.to_s 36
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_number term
|
27
|
+
term.to_i 36
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|