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.
Files changed (53) hide show
  1. data/.gitignore +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +2 -4
  4. data/README.rdoc +48 -0
  5. data/Rakefile +5 -0
  6. data/bin/hearken +7 -0
  7. data/hearken.gemspec +9 -0
  8. data/lib/hearken.rb +1 -5
  9. data/lib/hearken/cli.rb +24 -0
  10. data/lib/hearken/command.rb +35 -0
  11. data/lib/hearken/command/enqueue.rb +12 -0
  12. data/lib/hearken/command/flush.rb +7 -0
  13. data/lib/hearken/command/list.rb +29 -0
  14. data/lib/hearken/command/recent.rb +31 -0
  15. data/lib/hearken/command/reload.rb +7 -0
  16. data/lib/hearken/command/restart.rb +7 -0
  17. data/lib/hearken/command/scrobbling.rb +14 -0
  18. data/lib/hearken/command/search.rb +25 -0
  19. data/lib/hearken/command/setup_scrobbling.rb +7 -0
  20. data/lib/hearken/command/show_properties.rb +9 -0
  21. data/lib/hearken/command/shuffle.rb +13 -0
  22. data/lib/hearken/command/start.rb +7 -0
  23. data/lib/hearken/command/status.rb +7 -0
  24. data/lib/hearken/command/stop.rb +7 -0
  25. data/lib/hearken/console.rb +34 -0
  26. data/lib/hearken/debug.rb +16 -0
  27. data/lib/hearken/indexing.rb +7 -0
  28. data/lib/hearken/indexing/audio_traverser.rb +19 -0
  29. data/lib/hearken/indexing/executor.rb +33 -0
  30. data/lib/hearken/indexing/ffmpeg_file.rb +51 -0
  31. data/lib/hearken/indexing/file.rb +13 -0
  32. data/lib/hearken/indexing/indexer.rb +28 -0
  33. data/lib/hearken/indexing/parser.rb +7 -0
  34. data/lib/hearken/indexing/persistant_traverser.rb +32 -0
  35. data/lib/hearken/indexing/persisted_traverser.rb +30 -0
  36. data/lib/hearken/library.rb +46 -0
  37. data/lib/hearken/player.rb +91 -0
  38. data/lib/hearken/preferences.rb +29 -0
  39. data/lib/hearken/queue.rb +26 -0
  40. data/lib/hearken/range_expander.rb +30 -0
  41. data/lib/hearken/scrobbler.rb +76 -0
  42. data/lib/hearken/tagged.rb +15 -0
  43. data/lib/hearken/track.rb +38 -0
  44. data/lib/hearken/version.rb +2 -2
  45. data/media/applause.mp3 +0 -0
  46. data/spec/hearken/command/enqueue_spec.rb +24 -0
  47. data/spec/hearken/command/list_spec.rb +31 -0
  48. data/spec/hearken/command/reload_spec.rb +20 -0
  49. data/spec/hearken/command/shuffle_spec.rb +27 -0
  50. data/spec/hearken/player_spec.rb +37 -0
  51. data/spec/hearken/range_expander_spec.rb +28 -0
  52. data/spec/spec_helper.rb +5 -0
  53. 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,13 @@
1
+ class Hearken::Indexing::File
2
+ def initialize path, root
3
+ @path, @root = path, root
4
+ end
5
+
6
+ def timestamp
7
+ @path.mtime.to_i.to_s
8
+ end
9
+
10
+ def to_s
11
+ @path.to_s
12
+ end
13
+ 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,7 @@
1
+ require 'hearken/indexing/ffmpeg_file'
2
+
3
+ module Hearken::Indexing::Parser
4
+ def self.parse path
5
+ Hearken::Indexing::FfmpegFile.new path
6
+ end
7
+ 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