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.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/bin/ektoplayer +7 -0
- data/lib/ektoplayer.rb +10 -0
- data/lib/ektoplayer/application.rb +148 -0
- data/lib/ektoplayer/bindings.rb +230 -0
- data/lib/ektoplayer/browsepage.rb +138 -0
- data/lib/ektoplayer/client.rb +18 -0
- data/lib/ektoplayer/common.rb +91 -0
- data/lib/ektoplayer/config.rb +247 -0
- data/lib/ektoplayer/controllers/browser.rb +47 -0
- data/lib/ektoplayer/controllers/controller.rb +9 -0
- data/lib/ektoplayer/controllers/help.rb +21 -0
- data/lib/ektoplayer/controllers/info.rb +22 -0
- data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
- data/lib/ektoplayer/controllers/playlist.rb +60 -0
- data/lib/ektoplayer/database.rb +199 -0
- data/lib/ektoplayer/events.rb +56 -0
- data/lib/ektoplayer/models/browser.rb +127 -0
- data/lib/ektoplayer/models/database.rb +49 -0
- data/lib/ektoplayer/models/model.rb +15 -0
- data/lib/ektoplayer/models/player.rb +28 -0
- data/lib/ektoplayer/models/playlist.rb +72 -0
- data/lib/ektoplayer/models/search.rb +42 -0
- data/lib/ektoplayer/models/trackloader.rb +17 -0
- data/lib/ektoplayer/mp3player.rb +151 -0
- data/lib/ektoplayer/operations/browser.rb +19 -0
- data/lib/ektoplayer/operations/operations.rb +26 -0
- data/lib/ektoplayer/operations/player.rb +11 -0
- data/lib/ektoplayer/operations/playlist.rb +67 -0
- data/lib/ektoplayer/theme.rb +102 -0
- data/lib/ektoplayer/trackloader.rb +146 -0
- data/lib/ektoplayer/ui.rb +404 -0
- data/lib/ektoplayer/ui/colors.rb +105 -0
- data/lib/ektoplayer/ui/widgets.rb +195 -0
- data/lib/ektoplayer/ui/widgets/container.rb +125 -0
- data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
- data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
- data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
- data/lib/ektoplayer/updater.rb +77 -0
- data/lib/ektoplayer/views/browser.rb +25 -0
- data/lib/ektoplayer/views/help.rb +46 -0
- data/lib/ektoplayer/views/info.rb +208 -0
- data/lib/ektoplayer/views/mainwindow.rb +64 -0
- data/lib/ektoplayer/views/playinginfo.rb +135 -0
- data/lib/ektoplayer/views/playlist.rb +39 -0
- data/lib/ektoplayer/views/progressbar.rb +51 -0
- data/lib/ektoplayer/views/splash.rb +99 -0
- data/lib/ektoplayer/views/trackrenderer.rb +137 -0
- data/lib/ektoplayer/views/volumemeter.rb +74 -0
- 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,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
|