ektoplayer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +49 -0
  3. data/bin/ektoplayer +7 -0
  4. data/lib/ektoplayer.rb +10 -0
  5. data/lib/ektoplayer/application.rb +148 -0
  6. data/lib/ektoplayer/bindings.rb +230 -0
  7. data/lib/ektoplayer/browsepage.rb +138 -0
  8. data/lib/ektoplayer/client.rb +18 -0
  9. data/lib/ektoplayer/common.rb +91 -0
  10. data/lib/ektoplayer/config.rb +247 -0
  11. data/lib/ektoplayer/controllers/browser.rb +47 -0
  12. data/lib/ektoplayer/controllers/controller.rb +9 -0
  13. data/lib/ektoplayer/controllers/help.rb +21 -0
  14. data/lib/ektoplayer/controllers/info.rb +22 -0
  15. data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
  16. data/lib/ektoplayer/controllers/playlist.rb +60 -0
  17. data/lib/ektoplayer/database.rb +199 -0
  18. data/lib/ektoplayer/events.rb +56 -0
  19. data/lib/ektoplayer/models/browser.rb +127 -0
  20. data/lib/ektoplayer/models/database.rb +49 -0
  21. data/lib/ektoplayer/models/model.rb +15 -0
  22. data/lib/ektoplayer/models/player.rb +28 -0
  23. data/lib/ektoplayer/models/playlist.rb +72 -0
  24. data/lib/ektoplayer/models/search.rb +42 -0
  25. data/lib/ektoplayer/models/trackloader.rb +17 -0
  26. data/lib/ektoplayer/mp3player.rb +151 -0
  27. data/lib/ektoplayer/operations/browser.rb +19 -0
  28. data/lib/ektoplayer/operations/operations.rb +26 -0
  29. data/lib/ektoplayer/operations/player.rb +11 -0
  30. data/lib/ektoplayer/operations/playlist.rb +67 -0
  31. data/lib/ektoplayer/theme.rb +102 -0
  32. data/lib/ektoplayer/trackloader.rb +146 -0
  33. data/lib/ektoplayer/ui.rb +404 -0
  34. data/lib/ektoplayer/ui/colors.rb +105 -0
  35. data/lib/ektoplayer/ui/widgets.rb +195 -0
  36. data/lib/ektoplayer/ui/widgets/container.rb +125 -0
  37. data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
  38. data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
  39. data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
  40. data/lib/ektoplayer/updater.rb +77 -0
  41. data/lib/ektoplayer/views/browser.rb +25 -0
  42. data/lib/ektoplayer/views/help.rb +46 -0
  43. data/lib/ektoplayer/views/info.rb +208 -0
  44. data/lib/ektoplayer/views/mainwindow.rb +64 -0
  45. data/lib/ektoplayer/views/playinginfo.rb +135 -0
  46. data/lib/ektoplayer/views/playlist.rb +39 -0
  47. data/lib/ektoplayer/views/progressbar.rb +51 -0
  48. data/lib/ektoplayer/views/splash.rb +99 -0
  49. data/lib/ektoplayer/views/trackrenderer.rb +137 -0
  50. data/lib/ektoplayer/views/volumemeter.rb +74 -0
  51. metadata +164 -0
@@ -0,0 +1,127 @@
1
+ require_relative 'model'
2
+
3
+ module Ektoplayer
4
+ module Models
5
+ class Browser < Model
6
+ PARENT_DIRECTORY = '..'.freeze
7
+
8
+ PATHS = {
9
+ style: [:style ].freeze,
10
+ artist: [:artist ].freeze,
11
+ album: [:album ].freeze,
12
+ released_by: [:released_by].freeze,
13
+ posted_by: [:posted_by ].freeze,
14
+ tracks: [].freeze
15
+ }.freeze
16
+
17
+ def initialize(client)
18
+ super()
19
+ @events.register(:changed)
20
+ @stack = []
21
+ @stack << BrowserRoot.new(client.database)
22
+ end
23
+
24
+ def current
25
+ @stack[-1]
26
+ end
27
+
28
+ def tracks(index)
29
+ current.tracks(index)
30
+ end
31
+
32
+ def reload
33
+ current.reload
34
+ @events.trigger(:changed)
35
+ end
36
+
37
+ def enter(index)
38
+ return unless (sub = current.enter(index))
39
+ return back() if sub == :parent
40
+ @stack.push(sub)
41
+ @events.trigger(:changed)
42
+ end
43
+
44
+ def back
45
+ return unless @stack.size > 1
46
+ @stack.pop
47
+ @events.trigger(:changed)
48
+ end
49
+
50
+ class BrowsableCollection
51
+ include Enumerable
52
+ def each(&block) @contents.each(&block) end
53
+ def [](*args) @contents[*args] end
54
+ def empty?; @contents.empty? end
55
+
56
+ def initialize(database, filters, tag_hierarchy)
57
+ @database, @tag_hierarchy = database, tag_hierarchy
58
+ @filters = filters
59
+ @tag = @tag_hierarchy.shift(1)[0]
60
+ reload
61
+ end
62
+
63
+ def reload
64
+ @contents = [PARENT_DIRECTORY]
65
+
66
+ if @tag
67
+ @contents.concat(
68
+ @database.select(
69
+ columns: @tag,
70
+ filters: @filters,
71
+ group_by: @tag,
72
+ order_by: [@tag] + @filters.map { |f| f[:tag] }
73
+ ).map { |r| r[0] }
74
+ )
75
+ else
76
+ @contents.concat(
77
+ @database.select(filters: @filters)
78
+ )
79
+ end
80
+ end
81
+
82
+ def enter(index)
83
+ return :parent if index == 0
84
+ return unless @tag
85
+ BrowsableCollection.new(@database, new_filters(index), @tag_hierarchy)
86
+ end
87
+
88
+ def tracks(index)
89
+ if not @tag
90
+ return [] if index == 0
91
+ return [ @contents[index] ]
92
+ else
93
+ @database.select(filters: new_filters(index)).map.to_a
94
+ end
95
+ end
96
+
97
+ private def new_filters(index)
98
+ @filters.dup << { tag: @tag, operator: :==, value: @contents[index] }
99
+ end
100
+ end
101
+
102
+ class BrowserRoot
103
+ CONTENTS = PATHS.keys.freeze
104
+
105
+ include Enumerable
106
+ def each(&block) CONTENTS.each(&block) end
107
+ def [](*args) CONTENTS[*args] end
108
+ def empty?; CONTENTS.empty? end
109
+
110
+ def initialize(database)
111
+ @database = database
112
+ end
113
+
114
+ def reload; end
115
+
116
+ def enter(index)
117
+ fail unless path = PATHS[CONTENTS[index]]
118
+ BrowsableCollection.new(@database, [], path.dup)
119
+ end
120
+
121
+ def tracks(index)
122
+ @database.select()
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'model'
2
+ require_relative '../updater'
3
+
4
+ module Ektoplayer
5
+ module Models
6
+ # This model represents the state of the database.
7
+ # Events:
8
+ # _changed_:: the database has been modified
9
+ # _update_started_:: database update has been started
10
+ # _update_finished_:: database update has been finished
11
+
12
+ class Database < Model
13
+ attr_reader :events
14
+
15
+ def initialize(client)
16
+ super()
17
+ @client = client
18
+ @client.database.events.on(:changed) { @events.trigger(:changed) }
19
+ @events.register(:update_started, :changed, :update_finished)
20
+ end
21
+
22
+ def track_count; @client.database.track_count end
23
+ def album_count; @client.database.album_count end
24
+
25
+ def updating?
26
+ @update_thread and @update_thread.alive?
27
+ end
28
+
29
+ def stop!
30
+ @update_thread and @update_thread.kill
31
+ @update_thread = nil
32
+ end
33
+
34
+ def update(**opts)
35
+ unless updating?
36
+ db_updater = DatabaseUpdater.new(@client.database)
37
+ @update_thread = Thread.new do
38
+ @events.trigger(:update_started)
39
+ db_updater.update(**opts)
40
+ @events.trigger(:update_finished)
41
+ end
42
+ else
43
+ Application.log(self, 'already updating')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,15 @@
1
+ require_relative '../events'
2
+
3
+ module Ektoplayer
4
+ module Models
5
+ # Base class for all other models.
6
+ class Model
7
+ attr_reader :events
8
+
9
+ def initialize
10
+ @events = Events.new
11
+ @events.no_auto_create
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'model'
2
+ require_relative '../mp3player'
3
+
4
+ module Ektoplayer
5
+ module Models
6
+ class Player < Model
7
+ def initialize(client)
8
+ super()
9
+ @client = client
10
+ @player = Mp3Player.new
11
+ @events.register(:position_change, :track_completed, :pause, :stop, :play)
12
+ @player.events.on_all(&@events.method(:trigger))
13
+
14
+ %w(pause toggle stop forward backward seek
15
+ length position position_percent level).each do |m|
16
+ self.define_singleton_method(m, &@player.method(m))
17
+ end
18
+ end
19
+
20
+ def play(file)
21
+ Application.log(self, 'playing', file)
22
+ @player.play(file) rescue Application.log(self, $!)
23
+ end
24
+
25
+ def close; @player.close end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,72 @@
1
+ require 'set'
2
+
3
+ require_relative 'model'
4
+
5
+ module Ektoplayer
6
+ module Models
7
+ class Playlist < Model
8
+ REPEAT_MODES = Set.new([:none, :playlist, :track]).freeze
9
+
10
+ attr_reader :current_playing, :repeat
11
+
12
+ def initialize(list: [])
13
+ super()
14
+ @events.register(:current_changed, :changed)
15
+ @playlist = list
16
+ @current_playing = nil
17
+ @repeat = :none
18
+ end
19
+
20
+ include Enumerable
21
+ def each(&block) @playlist.each(&block) end
22
+ def [](*args) @playlist[*args] end
23
+ def empty?; @playlist.empty? end
24
+ def size; @playlist.size end
25
+
26
+ def current_playing=(new)
27
+ new = new.clamp(0, @playlist.size - 1) if new
28
+
29
+ if @current_playing != new
30
+ @current_playing = new
31
+ events.trigger(:current_changed)
32
+ end
33
+ end
34
+
35
+ def get_next_pos
36
+ if @current_playing
37
+ if @repeat == :track
38
+ return @current_playing
39
+ elsif @current_playing + 1 >= @playlist.size
40
+ return 0 if @repeat == :playlist
41
+ else
42
+ return @current_playing + 1
43
+ end
44
+ end
45
+ end
46
+
47
+ def repeat=(new)
48
+ raise ArgumentError unless REPEAT_MODES.include? new
49
+ if @repeat != new
50
+ @repeat = new
51
+ events.trigger(:repeat_changed)
52
+ end
53
+ end
54
+
55
+ def add(*tracks)
56
+ @playlist.concat(tracks)
57
+ events.trigger(:changed, added: tracks)
58
+ Application.log(self, "added #{tracks.count} tracks to playlist")
59
+ end
60
+
61
+ def clear
62
+ @playlist.clear
63
+ events.trigger(:changed)
64
+ end
65
+
66
+ def delete(index)
67
+ @playlist.delete_at(index) rescue return
68
+ events.trigger(:changed, deleted: index)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'model'
2
+
3
+ module Ektoplayer
4
+ module Models
5
+ class Search < Model
6
+ include Enumerable
7
+ def each(&block) @contents.each(&block) end
8
+ def [](*args) @contents[*args] end
9
+
10
+ def initialize(client, sort: 'date')
11
+ super()
12
+ @params = { sort: 'date' }
13
+ @db = client.database
14
+ @events.register(:changed)
15
+ #reload { sort: 'date' }.update(params)
16
+ end
17
+
18
+ def reload(new_params)
19
+ return if new_params == @params
20
+ @params = new_params
21
+
22
+ @contents = @db.select(
23
+ order_by: "#{@params[:sort]}, album, number",
24
+ filters: todo #TODO
25
+ )
26
+ @events.trigger(:changed)
27
+ end
28
+
29
+ def completion_for(tag)
30
+ @db.select(columns: tag, group_by: tag, order_by: tag)
31
+ end
32
+
33
+ def sort(new)
34
+ reload @param.dup.update(sort: new)
35
+ end
36
+
37
+ def by_tag(tag, operator, value)
38
+ reload @param.dup.update()
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'model'
2
+
3
+ module Ektoplayer
4
+ module Models
5
+ class Trackloader < Model
6
+ def initialize(client)
7
+ super()
8
+ @trackloader = client.trackloader
9
+
10
+ %w(get_track_file download_album downloads
11
+ ).each do |m|
12
+ self.define_singleton_method(m, &@trackloader.method(m))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,151 @@
1
+ require 'thread'
2
+ require 'mpg123'
3
+ require 'portaudio'
4
+
5
+ require_relative 'events'
6
+
7
+ class Mpg123
8
+ alias :samples_per_frame :spf
9
+ alias :time_per_frame :tpf
10
+
11
+ def samples_to_frames(samples)
12
+ samples / samples_per_frame
13
+ end
14
+
15
+ def seconds_to_frames(seconds)
16
+ seconds / time_per_frame
17
+ end
18
+
19
+ def seconds_to_samples(seconds)
20
+ seconds_to_frames(seconds) * samples_per_frame
21
+ end
22
+
23
+ def samples_to_seconds(samples)
24
+ samples_to_frames(samples) * time_per_frame
25
+ end
26
+
27
+ def length_in_seconds
28
+ samples_to_frames(length) * time_per_frame
29
+ end
30
+
31
+ def tell_in_seconds
32
+ samples_to_frames(tell) * time_per_frame
33
+ end
34
+
35
+ def seek_in_seconds(seconds)
36
+ samples = seconds_to_samples(seconds)
37
+ seek(samples) if (0..length).include?(samples)
38
+ end
39
+ end
40
+
41
+ class Mp3Player
42
+ attr_reader :events
43
+
44
+ def initialize(buffer_size = 2**12)
45
+ @events = Events.new(:play, :pause, :stop, :position_change)
46
+ @portaudio = Portaudio.new(buffer_size)
47
+ @lock = Mutex.new
48
+ end
49
+
50
+ def play(song=nil)
51
+ @lock.synchronize do
52
+ @mp3 = Mpg123.new(song) if song
53
+ return unless @mp3
54
+
55
+ @portaudio.start if @portaudio.stopped?
56
+ @portaudio_thr ||= Thread.new do
57
+ begin
58
+ while @mp3
59
+ status = @portaudio.write_from_mpg(@mp3)
60
+ @events.trigger(:position_change)
61
+ break if status == :done or status == :need_more
62
+ @portaudio.wait
63
+ end
64
+ rescue
65
+ Application.log(self, $!)
66
+ ensure
67
+ @portaudio_thr = nil
68
+ if status == :done or status == :need_more
69
+ @events.trigger(:stop, :track_completed)
70
+ elsif stopped?
71
+ @events.trigger(:stop)
72
+ elsif paused?
73
+ @events.trigger(:pause)
74
+ end
75
+ end
76
+ end
77
+
78
+ @events.trigger(:position_change)
79
+ @events.trigger(:play)
80
+ end
81
+ end
82
+
83
+ def pause
84
+ @lock.synchronize do
85
+ @portaudio_thr.kill rescue nil
86
+ @events.trigger(:position_change)
87
+ @events.trigger(:pause)
88
+ end
89
+ end
90
+
91
+ def stop
92
+ @lock.synchronize do
93
+ @portaudio_thr.kill rescue nil
94
+ (@mp3.seek(0) rescue nil) if @mp3
95
+ @events.trigger(:position_change)
96
+ @events.trigger(:stop)
97
+ end
98
+ end
99
+
100
+ def file; @mp3.file if @mp3 end
101
+ def level; @portaudio.rms end
102
+ def toggle; playing? ? pause : play end
103
+ def playing?; @portaudio_thr end
104
+
105
+ def paused?
106
+ not @portaudio_thr and (@mp3 and @mp3.tell.to_i != 0)
107
+ end
108
+
109
+ def stopped?
110
+ not @portaudio_thr and (!@mp3 or @mp3.tell_in_seconds.to_i == 0)
111
+ end
112
+
113
+ def status
114
+ return :playing if playing?
115
+ return :paused if paused?
116
+ return :stopped if stopped?
117
+ end
118
+
119
+ # Returns current song position in seconds
120
+ def position
121
+ @mp3 ? @mp3.tell_in_seconds : 0
122
+ end
123
+
124
+ # Returns current song position as percentual value to song length
125
+ def position_percent
126
+ @mp3 ? (Float(@mp3.tell) / @mp3.length) : 0
127
+ end
128
+
129
+ # Returns current song length in seconds
130
+ def length
131
+ @mp3 ? @mp3.length_in_seconds : 0
132
+ end
133
+
134
+ # Seeks the current song to position in seconds
135
+ def seek(seconds)
136
+ return unless @mp3
137
+ @lock.synchronize do
138
+ @mp3.seek_in_seconds(seconds) rescue nil
139
+ events.trigger(:position_change, position)
140
+ end
141
+ end
142
+
143
+ def rewind(seconds = 2)
144
+ seek(position - seconds)
145
+ end
146
+ alias :backward :rewind
147
+
148
+ def forward(seconds = 2)
149
+ seek(position + seconds)
150
+ end
151
+ end