hearken 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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