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,47 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
require_relative 'controller'
|
4
|
+
|
5
|
+
module Ektoplayer
|
6
|
+
module Controllers
|
7
|
+
class Browser < Controller
|
8
|
+
def initialize(view, browser, view_operations, operations)
|
9
|
+
super(view)
|
10
|
+
view.attach(browser)
|
11
|
+
|
12
|
+
register = view_operations.with_register('browser.')
|
13
|
+
|
14
|
+
%w(up down page_up page_down top bottom
|
15
|
+
search_up search_down search_next search_prev).
|
16
|
+
each { |op| register.(op, &view.method(op)) }
|
17
|
+
|
18
|
+
register.(:enter) do
|
19
|
+
operations.send(:'browser.enter', view.selected)
|
20
|
+
end
|
21
|
+
|
22
|
+
register.(:add_to_playlist) do
|
23
|
+
#if tracks = browser.tracks(view.selected)
|
24
|
+
operations.send(:'browser.add_to_playlist', view.selected)
|
25
|
+
#end
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: mouse?
|
29
|
+
view.mouse.on(65536) do view.scroll_up(3) end
|
30
|
+
view.mouse.on(2097152) do view.scroll_down(3) end
|
31
|
+
|
32
|
+
view.mouse.on(Curses::BUTTON1_DOUBLE_CLICKED) do |mevent|
|
33
|
+
view.select_from_cursorpos(mevent.y)
|
34
|
+
view_operations.send(:'browser.enter')
|
35
|
+
end
|
36
|
+
|
37
|
+
[Curses::BUTTON1_CLICKED, Curses::BUTTON2_CLICKED, Curses::BUTTON3_CLICKED].
|
38
|
+
each do |button|
|
39
|
+
view.mouse.on(button) do |mevent|
|
40
|
+
view.select_from_cursorpos(mevent.y)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
require_relative 'controller'
|
4
|
+
|
5
|
+
module Ektoplayer
|
6
|
+
module Controllers
|
7
|
+
class Help < Controller
|
8
|
+
def initialize(view, view_operations)
|
9
|
+
super(view)
|
10
|
+
register = view_operations.with_register('help.')
|
11
|
+
%w(up down page_up page_down top bottom).
|
12
|
+
each { |op| register.(op, &view.method(op)) }
|
13
|
+
|
14
|
+
# TODO: mouse?
|
15
|
+
view.mouse.on(65536) { view.page_up }
|
16
|
+
view.mouse.on(2097152) { view.page_down }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
require_relative 'controller'
|
4
|
+
|
5
|
+
module Ektoplayer
|
6
|
+
module Controllers
|
7
|
+
class Info < Controller
|
8
|
+
def initialize(view, playlist, trackloader, database, view_operations)
|
9
|
+
super(view)
|
10
|
+
view.attach(playlist, trackloader, database)
|
11
|
+
register = view_operations.with_register('info.')
|
12
|
+
%w(up down page_up page_down top bottom).
|
13
|
+
each { |op| register.(op, &view.method(op)) }
|
14
|
+
|
15
|
+
# TODO: mouse?
|
16
|
+
view.mouse.on(65536) { view.page_up }
|
17
|
+
view.mouse.on(2097152) { view.page_down }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'controller'
|
2
|
+
|
3
|
+
module Ektoplayer
|
4
|
+
module Controllers
|
5
|
+
class MainWindow < Controller
|
6
|
+
def initialize(view, view_operations)
|
7
|
+
super(view)
|
8
|
+
ops = view_operations
|
9
|
+
ops.reg('splash.show') { view.tabs.selected=(view.splash) }
|
10
|
+
ops.reg('playlist.show') { view.tabs.selected=(view.playlist) }
|
11
|
+
ops.reg('browser.show') { view.tabs.selected=(view.browser) }
|
12
|
+
ops.reg('info.show') { view.tabs.selected=(view.info) }
|
13
|
+
ops.reg('help.show') { view.tabs.selected=(view.help) }
|
14
|
+
ops.reg('tabs.next') { view.tabs.next }
|
15
|
+
ops.reg('tabs.prev') { view.tabs.prev }
|
16
|
+
|
17
|
+
ops.reg('playinginfo.toggle') do
|
18
|
+
view.with_lock do
|
19
|
+
view.playinginfo.visible=(!view.playinginfo.visible?)
|
20
|
+
view.want_layout
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ops.reg('progressbar.toggle') do
|
25
|
+
view.with_lock do
|
26
|
+
view.progressbar.visible=(!view.progressbar.visible?)
|
27
|
+
view.want_layout
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
ops.reg('volumemeter.toggle') do
|
32
|
+
view.with_lock do
|
33
|
+
view.volumemeter.visible=(!view.volumemeter.visible?)
|
34
|
+
view.want_layout
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'controller'
|
2
|
+
require_relative '../bindings'
|
3
|
+
|
4
|
+
module Ektoplayer
|
5
|
+
module Controllers
|
6
|
+
class Playlist < Controller
|
7
|
+
def initialize(view, playlist, view_operations, operations)
|
8
|
+
super(view)
|
9
|
+
view.attach(playlist)
|
10
|
+
|
11
|
+
register = view_operations.with_register('playlist.')
|
12
|
+
%w(up down page_up page_down top bottom
|
13
|
+
search_up search_down search_next search_prev).
|
14
|
+
each { |op| register.(op, &@view.method(op)) }
|
15
|
+
|
16
|
+
register.(:play) do
|
17
|
+
operations.send(:'playlist.play', view.selected)
|
18
|
+
end
|
19
|
+
|
20
|
+
register.(:reload) do
|
21
|
+
operations.send(:'playlist.reload', view.selected)
|
22
|
+
end
|
23
|
+
|
24
|
+
register.(:download_album) do
|
25
|
+
operations.send(:'playlist.download_album', view.selected)
|
26
|
+
end
|
27
|
+
|
28
|
+
register.(:delete) do
|
29
|
+
old_cursor, old_selected = view.cursor, view.selected
|
30
|
+
operations.send(:'playlist.delete', old_selected)
|
31
|
+
view.selected=(old_selected)
|
32
|
+
view.force_cursorpos(old_cursor)
|
33
|
+
end
|
34
|
+
|
35
|
+
register.(:goto_current) do
|
36
|
+
if index = playlist.current_playing
|
37
|
+
view.selected=(index)
|
38
|
+
view.center()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: mouse?
|
43
|
+
view.mouse.on(65536) do view.scroll_up(3) end
|
44
|
+
view.mouse.on(2097152) do view.scroll_down(3) end
|
45
|
+
|
46
|
+
[Curses::BUTTON1_CLICKED, Curses::BUTTON2_CLICKED, Curses::BUTTON3_CLICKED].
|
47
|
+
each do |button|
|
48
|
+
view.mouse.on(button) do |mevent|
|
49
|
+
view.select_from_cursorpos(mevent.y)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
view.mouse.on(Curses::BUTTON1_DOUBLE_CLICKED) do |mevent|
|
54
|
+
view.select_from_cursorpos(mevent.y)
|
55
|
+
view_operations.send('playlist.play')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
require_relative 'events'
|
3
|
+
|
4
|
+
module Ektoplayer
|
5
|
+
class Database
|
6
|
+
SELECT_ARCHIVES = %q[
|
7
|
+
SELECT archive_url, archive_type
|
8
|
+
FROM archive_urls
|
9
|
+
JOIN tracks AS t ON t.album_url = archive_urls.album_url
|
10
|
+
WHERE t.url = ?
|
11
|
+
].squeeze(' ').freeze
|
12
|
+
|
13
|
+
SELECT = %q[
|
14
|
+
SELECT
|
15
|
+
%{SELECT_COLUMNS}
|
16
|
+
FROM (
|
17
|
+
SELECT DISTINCT
|
18
|
+
t.url AS url,
|
19
|
+
t.album_url AS album_url,
|
20
|
+
t.title AS title,
|
21
|
+
t.artist AS artist,
|
22
|
+
t.remix AS remix,
|
23
|
+
t.number AS number,
|
24
|
+
t.bpm AS bpm,
|
25
|
+
|
26
|
+
a.artist AS album_artist,
|
27
|
+
a.title AS album,
|
28
|
+
a.released_by AS released_by,
|
29
|
+
a.released_by_url AS released_by_url,
|
30
|
+
a.posted_by AS posted_by,
|
31
|
+
a.posted_by_url AS posted_by_url,
|
32
|
+
a.cover_url AS cover_url,
|
33
|
+
a.category AS category,
|
34
|
+
a.description AS description,
|
35
|
+
a.date AS date,
|
36
|
+
a.rating AS rating,
|
37
|
+
a.votes AS votes,
|
38
|
+
a.download_count AS download_count,
|
39
|
+
|
40
|
+
a_s.style AS style,
|
41
|
+
|
42
|
+
(
|
43
|
+
SELECT GROUP_CONCAT(style)
|
44
|
+
FROM albums_styles
|
45
|
+
WHERE albums_styles.album_url = t.album_url
|
46
|
+
) AS styles
|
47
|
+
FROM
|
48
|
+
tracks AS t
|
49
|
+
|
50
|
+
JOIN albums AS a ON a.url = t.album_url
|
51
|
+
JOIN albums_styles AS a_s ON a.url = a_s.album_url
|
52
|
+
)
|
53
|
+
|
54
|
+
WHERE 1 %{WHERE}
|
55
|
+
GROUP BY %{GROUP_BY}
|
56
|
+
ORDER BY %{ORDER_BY}
|
57
|
+
%{LIMIT} ].squeeze(' ').freeze
|
58
|
+
|
59
|
+
attr_reader :db, :events
|
60
|
+
|
61
|
+
def initialize(db_file)
|
62
|
+
@db = SQLite3::Database.new db_file
|
63
|
+
@db.results_as_hash = true
|
64
|
+
@events = Events.new(:changed)
|
65
|
+
create_tables
|
66
|
+
end
|
67
|
+
|
68
|
+
private def create_tables
|
69
|
+
@db.execute_batch %q{
|
70
|
+
CREATE TABLE IF NOT EXISTS albums (
|
71
|
+
url TEXT NOT NULL,
|
72
|
+
title TEXT NOT NULL,
|
73
|
+
artist TEXT,
|
74
|
+
released_by TEXT,
|
75
|
+
released_by_url TEXT,
|
76
|
+
posted_by TEXT,
|
77
|
+
posted_by_url TEXT,
|
78
|
+
cover_url TEXT,
|
79
|
+
category TEXT,
|
80
|
+
description TEXT,
|
81
|
+
date DATE,
|
82
|
+
rating FLOAT NOT NULL DEFAULT -1,
|
83
|
+
votes INT NOT NULL DEFAULT 0,
|
84
|
+
download_count INT NOT NULL DEFAULT 0,
|
85
|
+
PRIMARY KEY (url)
|
86
|
+
);
|
87
|
+
CREATE TABLE IF NOT EXISTS tracks (
|
88
|
+
url TEXT NOT NULL,
|
89
|
+
album_url TEXT NOT NULL REFERENCES albums(url),
|
90
|
+
title TEXT NOT NULL,
|
91
|
+
artist TEXT NOT NULL,
|
92
|
+
remix TEXT,
|
93
|
+
number INT NOT NULL,
|
94
|
+
bpm INT,
|
95
|
+
PRIMARY KEY (url)
|
96
|
+
);
|
97
|
+
CREATE TABLE IF NOT EXISTS styles (
|
98
|
+
style TEXT NOT NULL,
|
99
|
+
url TEXT NOT NULL,
|
100
|
+
PRIMARY KEY (style)
|
101
|
+
);
|
102
|
+
CREATE TABLE IF NOT EXISTS archive_urls (
|
103
|
+
album_url TEXT NOT NULL REFERENCES albums(url),
|
104
|
+
archive_url TEXT NOT NULL,
|
105
|
+
archive_type TEXT NOT NULL,
|
106
|
+
PRIMARY KEY (album_url, archive_url)
|
107
|
+
);
|
108
|
+
CREATE TABLE IF NOT EXISTS albums_styles (
|
109
|
+
album_url TEXT NOT NULL REFERENCES albums(url),
|
110
|
+
style TEXT NOT NULL REFERENCES styles(style),
|
111
|
+
PRIMARY KEY (album_url, style)
|
112
|
+
);}
|
113
|
+
end
|
114
|
+
|
115
|
+
private def drop_tables
|
116
|
+
%w(albums_styles archive_urls styles tracks albums).
|
117
|
+
each { |t| @db.execute("DROP TABLE IF EXISTS #{t}") }
|
118
|
+
end
|
119
|
+
|
120
|
+
def insert_into(table, hash, mode: :insert)
|
121
|
+
cols = ?( + (hash.keys * ?,) + ?)
|
122
|
+
values = ?( + (([??] * hash.size) * ?,) + ?)
|
123
|
+
q = @db.prepare "#{mode} INTO #{table} #{cols} VALUES #{values}"
|
124
|
+
q.bind_params(hash.values)
|
125
|
+
q.execute
|
126
|
+
@events.trigger(:changed)
|
127
|
+
rescue
|
128
|
+
Application.log(self, hash, $!)
|
129
|
+
end
|
130
|
+
|
131
|
+
def replace_into(table, hash)
|
132
|
+
insert_into(table, hash, mode: :replace)
|
133
|
+
end
|
134
|
+
|
135
|
+
def execute(query, params=nil)
|
136
|
+
stm = @db.prepare query
|
137
|
+
stm.bind_params(*params) if params
|
138
|
+
stm.execute.to_a
|
139
|
+
rescue
|
140
|
+
Application.log(self, $!)
|
141
|
+
end
|
142
|
+
|
143
|
+
def select(
|
144
|
+
columns: ?*,
|
145
|
+
filters: [],
|
146
|
+
group_by: 'url',
|
147
|
+
order_by: 'album, number',
|
148
|
+
limit: nil
|
149
|
+
)
|
150
|
+
where_clauses, where_params = [], []
|
151
|
+
|
152
|
+
filters.each do |filter|
|
153
|
+
where_clauses << "AND #{filter[:tag]} #{filter[:operator]} ?"
|
154
|
+
where_params << filter[:value]
|
155
|
+
end
|
156
|
+
|
157
|
+
if order_by.is_a?Array
|
158
|
+
fail ArgumentError, 'order_by is empty' if order_by.empty?
|
159
|
+
order_by = order_by.join(?,)
|
160
|
+
else
|
161
|
+
fail ArgumentError, 'order_by is empty' if order_by.empty?
|
162
|
+
end
|
163
|
+
|
164
|
+
if group_by.is_a?Array
|
165
|
+
fail ArgumentError, 'group_by is empty' if group_by.empty?
|
166
|
+
group_by = group_by.join(?,)
|
167
|
+
else
|
168
|
+
fail ArgumentError, 'group_by is empty' if group_by.empty?
|
169
|
+
end
|
170
|
+
|
171
|
+
limit = "LIMIT #{limit}" if limit
|
172
|
+
|
173
|
+
stm = @db.prepare SELECT % {
|
174
|
+
SELECT_COLUMNS: columns,
|
175
|
+
WHERE: where_clauses.join(' '),
|
176
|
+
GROUP_BY: group_by,
|
177
|
+
ORDER_BY: order_by,
|
178
|
+
LIMIT: limit
|
179
|
+
}
|
180
|
+
|
181
|
+
stm.bind_params(*where_params)
|
182
|
+
stm.execute.to_a
|
183
|
+
rescue
|
184
|
+
Application.log(self, $!)
|
185
|
+
end
|
186
|
+
|
187
|
+
def get_archives(url)
|
188
|
+
execute(SELECT_ARCHIVES, [url])
|
189
|
+
end
|
190
|
+
|
191
|
+
def track_count
|
192
|
+
@db.execute('SELECT COUNT(*) FROM tracks')[0][0]
|
193
|
+
end
|
194
|
+
|
195
|
+
def album_count
|
196
|
+
@db.execute('SELECT COUNT(*) FROM albums')[0][0]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Event class
|
2
|
+
class Events
|
3
|
+
# Create a new events object
|
4
|
+
def initialize(*known_events)
|
5
|
+
@on_all = []
|
6
|
+
|
7
|
+
if known_events.empty?
|
8
|
+
@map = {}
|
9
|
+
auto_create
|
10
|
+
else
|
11
|
+
@map = known_events.map { |e| [e, []] }.to_h
|
12
|
+
no_auto_create
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Disables auto creation of non existent events
|
17
|
+
def no_auto_create
|
18
|
+
@map.default_proc = proc { |h,k| fail KeyError, "Unknown event: #{k}" }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Enables auto creation of non existent events
|
22
|
+
def auto_create
|
23
|
+
@map.default_proc = proc { |h,k| h[k] = [] }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Registers a new event
|
27
|
+
def register(*events)
|
28
|
+
events.each { |event| @map[event] = [] }
|
29
|
+
end
|
30
|
+
alias :reg :register
|
31
|
+
|
32
|
+
# Register hook for event
|
33
|
+
def on(event, &block)
|
34
|
+
fail ArgumentError unless block
|
35
|
+
@map[event] << block
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_multi(*events, &block)
|
39
|
+
events.each { |event| on(event, &block) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Forward all events to +block(event, *args)+
|
43
|
+
def on_all(&block)
|
44
|
+
fail ArgumentError unless block
|
45
|
+
@on_all << block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Trigger event
|
49
|
+
def trigger(event, *args)
|
50
|
+
@on_all.each { |callback| callback.(event, *args) }
|
51
|
+
|
52
|
+
if @map.key?(event)
|
53
|
+
@map[event].each { |callback| callback.call(*args) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|