ektoplayer 0.1.0

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 (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