ncXBMC 0.3
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.
- data/README +14 -0
- data/bin/ncxbmc.rb +741 -0
- metadata +78 -0
data/README
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ncXBMC
|
|
2
|
+
==================
|
|
3
|
+
|
|
4
|
+
ncXBMC is a remote XBMC client, which aims to provide a full control of
|
|
5
|
+
the music player over a local network.
|
|
6
|
+
|
|
7
|
+
It can be used to browse library, manage playlist, and control playback.
|
|
8
|
+
|
|
9
|
+
The interface has been greatly inspired by ncmpc (http://hem.bredband.net/kaw/ncmpc/),
|
|
10
|
+
a curses client for the Music Player Daemon (MPD) (I'm a fan).
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Usage: ncxbmc.rb [options] hostname
|
|
14
|
+
|
data/bin/ncxbmc.rb
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
=begin
|
|
4
|
+
ncXBMC
|
|
5
|
+
|
|
6
|
+
ncXBMC is a remote XBMC client, which aims to provide a full control of
|
|
7
|
+
the music player over a local network.
|
|
8
|
+
|
|
9
|
+
It can be used to browse library, manage playlist, and control playback.
|
|
10
|
+
|
|
11
|
+
The interface has been greatly inspired by ncmpc (http://hem.bredband.net/kaw/ncmpc/),
|
|
12
|
+
a curses client for the Music Player Daemon (MPD) (I'm a fan).
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Copyright (C) 2009 Cedric TESSIER
|
|
16
|
+
|
|
17
|
+
Contact: nezetic.info
|
|
18
|
+
|
|
19
|
+
This program is free software: you can redistribute it and/or modify
|
|
20
|
+
it under the terms of the GNU General Public License as published by
|
|
21
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
22
|
+
(at your option) any later version.
|
|
23
|
+
|
|
24
|
+
This program is distributed in the hope that it will be useful,
|
|
25
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
26
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
27
|
+
GNU General Public License for more details.
|
|
28
|
+
|
|
29
|
+
You should have received a copy of the GNU General Public License
|
|
30
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
require 'optparse'
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
require 'rubygems'
|
|
38
|
+
rescue LoadError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
require 'ncurses'
|
|
42
|
+
require 'ruby-xbmc'
|
|
43
|
+
|
|
44
|
+
NCXMBC_DEFAULTPORT="8080"
|
|
45
|
+
NCXBMC_VERSION=0.3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
module Interface
|
|
49
|
+
KEY_TAB = 9
|
|
50
|
+
KEY_ENTER = 10
|
|
51
|
+
KEY_SPACE = 32
|
|
52
|
+
KEY_RIGHT = 261
|
|
53
|
+
KEY_LEFT = 260
|
|
54
|
+
KEY_UP = 259
|
|
55
|
+
KEY_DOWN = 258
|
|
56
|
+
KEY_b = 98
|
|
57
|
+
KEY_c = 99
|
|
58
|
+
KEY_d = 100
|
|
59
|
+
KEY_f = 102
|
|
60
|
+
KEY_h = 104
|
|
61
|
+
KEY_m = 109
|
|
62
|
+
KEY_p = 112
|
|
63
|
+
KEY_s = 115
|
|
64
|
+
KEY_RETURN = 127
|
|
65
|
+
|
|
66
|
+
class NcurseInterface
|
|
67
|
+
REFRESH_DELAY=1 # seconds
|
|
68
|
+
KEY_ESC=27
|
|
69
|
+
|
|
70
|
+
$interface_error = nil
|
|
71
|
+
|
|
72
|
+
def initialize(xbmc)
|
|
73
|
+
@stdscr = Ncurses.initscr
|
|
74
|
+
|
|
75
|
+
Ncurses.curs_set(0)
|
|
76
|
+
Ncurses.noecho
|
|
77
|
+
Ncurses.keypad(@stdscr,TRUE)
|
|
78
|
+
Ncurses.cbreak
|
|
79
|
+
@stdscr.nodelay(true)
|
|
80
|
+
|
|
81
|
+
ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)
|
|
82
|
+
|
|
83
|
+
if Ncurses.has_colors?
|
|
84
|
+
Ncurses.start_color
|
|
85
|
+
Ncurses.use_default_colors
|
|
86
|
+
init_bgcolor()
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@maxy=Ncurses.getmaxy(@stdscr)
|
|
90
|
+
@maxx=Ncurses.getmaxx(@stdscr)
|
|
91
|
+
|
|
92
|
+
@xbmc = xbmc
|
|
93
|
+
|
|
94
|
+
@wins = []
|
|
95
|
+
@wins << PlaylistWin.new(@stdscr, @xbmc)
|
|
96
|
+
@wins << LibraryWin.new(@stdscr, @xbmc)
|
|
97
|
+
|
|
98
|
+
@currentwin = 0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def refresh
|
|
102
|
+
@stdscr.refresh
|
|
103
|
+
@wins[@currentwin].refresh
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def drawCurrentWin
|
|
107
|
+
@wins[@currentwin].draw
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def UpdateCurrentWin
|
|
111
|
+
@wins[@currentwin].autorefresh
|
|
112
|
+
@wins[@currentwin].update
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def run
|
|
116
|
+
begin
|
|
117
|
+
self.drawCurrentWin
|
|
118
|
+
self.refresh
|
|
119
|
+
|
|
120
|
+
key=0
|
|
121
|
+
timespent=0
|
|
122
|
+
while key!=KEY_ESC
|
|
123
|
+
key=@stdscr.getch
|
|
124
|
+
|
|
125
|
+
if(key == KEY_TAB) # Tabs Cycling
|
|
126
|
+
@currentwin += 1
|
|
127
|
+
@currentwin = 0 if(@currentwin > (@wins.length - 1))
|
|
128
|
+
|
|
129
|
+
self.drawCurrentWin
|
|
130
|
+
self.refresh
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@wins[@currentwin].handleKey(key) if key != -1
|
|
135
|
+
|
|
136
|
+
sleep(0.01)
|
|
137
|
+
timespent += 0.01
|
|
138
|
+
|
|
139
|
+
if(timespent >= REFRESH_DELAY)
|
|
140
|
+
timespent = 0
|
|
141
|
+
self.UpdateCurrentWin
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
rescue Interrupt
|
|
145
|
+
rescue Exception => exc
|
|
146
|
+
$interface_error = exc
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
protected
|
|
151
|
+
|
|
152
|
+
def init_bgcolor(pair=1, bgcolor=Ncurses::COLOR_BLACK)
|
|
153
|
+
Ncurses.init_pair(1, Ncurses::COLOR_WHITE, bgcolor)
|
|
154
|
+
Ncurses.init_pair(2, Ncurses::COLOR_YELLOW, bgcolor)
|
|
155
|
+
Ncurses.init_pair(3, Ncurses::COLOR_RED, bgcolor)
|
|
156
|
+
Ncurses.init_pair(4, Ncurses::COLOR_GREEN, bgcolor)
|
|
157
|
+
Ncurses.init_pair(5, Ncurses::COLOR_BLUE, bgcolor)
|
|
158
|
+
Ncurses.init_pair(6, Ncurses::COLOR_CYAN, bgcolor)
|
|
159
|
+
Ncurses.init_pair(7, Ncurses::COLOR_MAGENTA, bgcolor)
|
|
160
|
+
Ncurses.init_pair(8, Ncurses::COLOR_BLACK, bgcolor)
|
|
161
|
+
Ncurses.bkgd(Ncurses.COLOR_PAIR(pair))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def self.finalize(id)
|
|
167
|
+
Ncurses.echo()
|
|
168
|
+
Ncurses.nocbreak()
|
|
169
|
+
Ncurses.nl()
|
|
170
|
+
Ncurses.curs_set(1)
|
|
171
|
+
|
|
172
|
+
Ncurses.endwin
|
|
173
|
+
|
|
174
|
+
if not $interface_error.nil?
|
|
175
|
+
case $interface_error
|
|
176
|
+
when SocketError, Errno::ETIMEDOUT then
|
|
177
|
+
puts "\nConnection lost !\n"
|
|
178
|
+
else
|
|
179
|
+
puts "\nFATAL ERROR in ncXBMC v%g !" % NCXBMC_VERSION
|
|
180
|
+
puts $interface_error.class.to_s + ": " + $interface_error.to_s
|
|
181
|
+
puts "Callstack:"
|
|
182
|
+
$interface_error.backtrace.each {|line| puts "\t#{line}"}
|
|
183
|
+
puts ""
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
class TabWins
|
|
190
|
+
|
|
191
|
+
def initialize(pscreen, xbmc)
|
|
192
|
+
@stdscr = pscreen
|
|
193
|
+
@xbmc = xbmc
|
|
194
|
+
|
|
195
|
+
@title = ""
|
|
196
|
+
|
|
197
|
+
@maxy=Ncurses.getmaxy(@stdscr)
|
|
198
|
+
@maxx=Ncurses.getmaxx(@stdscr)
|
|
199
|
+
|
|
200
|
+
@header = Ncurses::WINDOW.new(2,@maxx,0,0)
|
|
201
|
+
@main = Ncurses::WINDOW.new(@maxy-2-2,@maxx,2,0)
|
|
202
|
+
@footer = Ncurses::WINDOW.new(2,@maxx,@maxy-2,0)
|
|
203
|
+
|
|
204
|
+
@selected = 0
|
|
205
|
+
@scroll = 0
|
|
206
|
+
|
|
207
|
+
@rheader = false
|
|
208
|
+
@rmain = false
|
|
209
|
+
@rfooter = false
|
|
210
|
+
|
|
211
|
+
@autorefresh_header = false
|
|
212
|
+
@autorefresh_main = false
|
|
213
|
+
@autorefresh_footer = false
|
|
214
|
+
|
|
215
|
+
@defaultKeys = true
|
|
216
|
+
|
|
217
|
+
@helpMSGCommon = "TAB switch between windows\nESC quit ncXBMC\n\n"
|
|
218
|
+
@helpMSG = ""
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def draw
|
|
222
|
+
self.drawHeader
|
|
223
|
+
self.drawMain
|
|
224
|
+
self.drawFooter
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def autorefresh
|
|
228
|
+
self.updateHeader if @autorefresh_header
|
|
229
|
+
self.updateMain if @autorefresh_main
|
|
230
|
+
self.updateFooter if @autorefresh_footer
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def refresh
|
|
234
|
+
@header.refresh
|
|
235
|
+
@main.refresh
|
|
236
|
+
@footer.refresh
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def updateHeader
|
|
240
|
+
@rheader = true
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def updateMain
|
|
244
|
+
@rmain = true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def updateFooter
|
|
248
|
+
@rfooter = true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def update
|
|
252
|
+
if(@rheader)
|
|
253
|
+
self.drawHeader
|
|
254
|
+
@header.refresh
|
|
255
|
+
@rheader = false
|
|
256
|
+
end
|
|
257
|
+
if(@rmain)
|
|
258
|
+
self.drawMain
|
|
259
|
+
@main.refresh
|
|
260
|
+
@rmain = false
|
|
261
|
+
end
|
|
262
|
+
if(@rfooter)
|
|
263
|
+
self.drawFooter
|
|
264
|
+
@footer.refresh
|
|
265
|
+
@rfooter = false
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def handleKey(key)
|
|
270
|
+
#Ncurses.mvwaddstr(@main, 20, 1, "KeyCode: " + key.to_s)
|
|
271
|
+
#@main.refresh
|
|
272
|
+
|
|
273
|
+
if (@defaultKeys)
|
|
274
|
+
case key
|
|
275
|
+
when KEY_h then
|
|
276
|
+
self.showHelp
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
protected
|
|
282
|
+
|
|
283
|
+
def drawHeader
|
|
284
|
+
@header.erase
|
|
285
|
+
@header.attron(Ncurses::A_BOLD)
|
|
286
|
+
Ncurses.mvwaddstr(@header, 0, 1, "Ncurses XBMC Client v." + NCXBMC_VERSION.to_s + " -- " + @title)
|
|
287
|
+
@header.attroff(Ncurses::A_BOLD)
|
|
288
|
+
@header.mvhline(1,0,Ncurses::ACS_HLINE, @maxx)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def drawMain
|
|
292
|
+
@main.erase
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def drawFooter
|
|
296
|
+
@footer.erase
|
|
297
|
+
@footer.mvhline(0,0,Ncurses::ACS_HLINE, @maxx)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def handleScroll
|
|
301
|
+
maxwy = Ncurses.getmaxy(@main) - 1
|
|
302
|
+
if((@selected - @scroll) > maxwy)
|
|
303
|
+
@scroll += 1
|
|
304
|
+
elsif((@selected - @scroll) < 0)
|
|
305
|
+
@scroll -= 1
|
|
306
|
+
end
|
|
307
|
+
return maxwy
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def showHelp
|
|
311
|
+
border = 15
|
|
312
|
+
title = "Help"
|
|
313
|
+
|
|
314
|
+
helpmsg = @helpMSGCommon + @helpMSG
|
|
315
|
+
|
|
316
|
+
lines = helpmsg.count("\n") + 3
|
|
317
|
+
helpwin = Ncurses::WINDOW.new(lines, @maxx-border, 2, (@maxx/2 - (@maxx-border)/2))
|
|
318
|
+
Ncurses.box(helpwin, Ncurses::ACS_VLINE, Ncurses::ACS_HLINE)
|
|
319
|
+
helpwin.attron(Ncurses::A_REVERSE)
|
|
320
|
+
Ncurses.mvwaddstr(helpwin, 0, (@maxx-border)/2 - (title.length + 2)/2, " #{title} ")
|
|
321
|
+
helpwin.attroff(Ncurses::A_REVERSE)
|
|
322
|
+
|
|
323
|
+
linenbr = 1
|
|
324
|
+
helpmsg.each { |line|
|
|
325
|
+
Ncurses.mvwaddstr(helpwin, linenbr, 2, line.chomp)
|
|
326
|
+
linenbr += 1
|
|
327
|
+
}
|
|
328
|
+
helpwin.refresh
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class PlaylistWin < TabWins
|
|
333
|
+
def initialize(pscreen, xbmc)
|
|
334
|
+
super
|
|
335
|
+
@title = "Playlist"
|
|
336
|
+
@autorefresh_footer = true
|
|
337
|
+
|
|
338
|
+
@forcesel = 0
|
|
339
|
+
@fastRefresh = false
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def drawHeader
|
|
343
|
+
super
|
|
344
|
+
|
|
345
|
+
volume = @xbmc.GetVolume.to_i
|
|
346
|
+
if(volume > 0)
|
|
347
|
+
volume_label = "Volume %d%%" % volume
|
|
348
|
+
else
|
|
349
|
+
volume_label = "Volume Muted"
|
|
350
|
+
end
|
|
351
|
+
Ncurses.mvwaddstr(@header, 0, @maxx - volume_label.length - 1, volume_label)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def drawMain
|
|
355
|
+
super
|
|
356
|
+
|
|
357
|
+
if(not @fastRefresh)
|
|
358
|
+
getCurrentPlaylist
|
|
359
|
+
getCurrentSong
|
|
360
|
+
else
|
|
361
|
+
@fastRefresh = false
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
return if @playlist.length == 1 and @playlist[0]["artist"].nil?
|
|
365
|
+
|
|
366
|
+
maxwy = self.handleScroll
|
|
367
|
+
|
|
368
|
+
idx = 0
|
|
369
|
+
@playlist.each { |song|
|
|
370
|
+
line = idx - @scroll
|
|
371
|
+
if(line >= 0)
|
|
372
|
+
break if(line > maxwy)
|
|
373
|
+
|
|
374
|
+
current = (@playing and @current_song["URL"] == song["path"])
|
|
375
|
+
if(current)
|
|
376
|
+
@main.attron(Ncurses.COLOR_PAIR(2)) if Ncurses.has_colors?
|
|
377
|
+
@main.attron(Ncurses::A_BOLD)
|
|
378
|
+
end
|
|
379
|
+
@main.attron(Ncurses::A_REVERSE) if(idx == @selected)
|
|
380
|
+
Ncurses.mvwaddstr(@main, line, 1, "#{song["artist"]} - #{song["title"]}")
|
|
381
|
+
if(current)
|
|
382
|
+
@main.attron(Ncurses.COLOR_PAIR(1)) if Ncurses.has_colors?
|
|
383
|
+
@main.attroff(Ncurses::A_BOLD)
|
|
384
|
+
end
|
|
385
|
+
@main.attroff(Ncurses::A_REVERSE) if(idx == @selected)
|
|
386
|
+
end
|
|
387
|
+
idx += 1
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def drawFooter
|
|
393
|
+
super
|
|
394
|
+
|
|
395
|
+
getCurrentSong
|
|
396
|
+
|
|
397
|
+
if(@playing)
|
|
398
|
+
speed = @xbmc.GetPlaySpeed.to_i
|
|
399
|
+
@footer.mvwaddstr(0, 0, '=' * (@maxx * @current_song["Percentage"].to_i / 100) + (speed == 1 ? '0': (speed > 1 ? '>>':'<<')))
|
|
400
|
+
paused = (@current_song["PlayStatus"] == "Paused")
|
|
401
|
+
Ncurses.mvwaddstr(@footer, 1, 1, "Playing: #{@current_song["Artist"]} - #{@current_song["Title"]} #{(paused ? "(Pause)":"")}")
|
|
402
|
+
time_label = "["+@current_song["Time"]+"/"+@current_song["Duration"]+"]"
|
|
403
|
+
Ncurses.mvwaddstr(@footer, 1, @maxx - time_label.length - 1, time_label)
|
|
404
|
+
speed_label = "(%dX)" % speed
|
|
405
|
+
@footer.mvwaddstr(0, @maxx - speed_label.length - 1, speed_label) if(speed != 1)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def update
|
|
410
|
+
super
|
|
411
|
+
|
|
412
|
+
if(@playing) # new song playing, refresh playlist
|
|
413
|
+
if(not @rmain and (@current_song["Changed"] == "True"))
|
|
414
|
+
self.drawMain
|
|
415
|
+
@main.refresh
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def handleKey(key)
|
|
421
|
+
super
|
|
422
|
+
|
|
423
|
+
case key
|
|
424
|
+
when KEY_LEFT then
|
|
425
|
+
@xbmc.SetVolume(@xbmc.GetVolume.to_i - 1)
|
|
426
|
+
self.updateHeader
|
|
427
|
+
when KEY_RIGHT then
|
|
428
|
+
@xbmc.SetVolume(@xbmc.GetVolume.to_i + 1)
|
|
429
|
+
self.updateHeader
|
|
430
|
+
when KEY_DOWN then
|
|
431
|
+
@selected += 1 if @selected < (@playlist.length - 1)
|
|
432
|
+
@fastRefresh = true
|
|
433
|
+
self.updateMain
|
|
434
|
+
when KEY_UP then
|
|
435
|
+
@selected -= 1 if @selected > 0
|
|
436
|
+
@fastRefresh = true
|
|
437
|
+
self.updateMain
|
|
438
|
+
when KEY_ENTER then
|
|
439
|
+
if(@xbmc.GetPlaySpeed.to_i != 1)
|
|
440
|
+
@xbmc.SetPlaySpeed(1)
|
|
441
|
+
else
|
|
442
|
+
@xbmc.SetPlaylistSong(@selected)
|
|
443
|
+
end
|
|
444
|
+
self.updateMain
|
|
445
|
+
when KEY_d then
|
|
446
|
+
@xbmc.RemoveFromPlaylist(@selected)
|
|
447
|
+
@selected -= 1 if @selected >= (@playlist.length - 1)
|
|
448
|
+
@forcesel = @selected
|
|
449
|
+
self.updateMain
|
|
450
|
+
when KEY_c then
|
|
451
|
+
@xbmc.Stop
|
|
452
|
+
@xbmc.ClearPlayList(XBMC::MUSIC_PLAYLIST)
|
|
453
|
+
@selected = 0
|
|
454
|
+
self.updateMain
|
|
455
|
+
when KEY_f then
|
|
456
|
+
speed = @xbmc.GetPlaySpeed.to_i
|
|
457
|
+
newspeed = speed < 0 ? speed / 2 : speed * 2
|
|
458
|
+
newspeed = 1 if(newspeed == -1)
|
|
459
|
+
@xbmc.SetPlaySpeed(newspeed) if(speed < 32)
|
|
460
|
+
when KEY_b then
|
|
461
|
+
speed = @xbmc.GetPlaySpeed.to_i
|
|
462
|
+
newspeed = speed > 0 ? speed / 2 : speed * 2
|
|
463
|
+
newspeed = -2 if(newspeed == 0)
|
|
464
|
+
@xbmc.SetPlaySpeed(newspeed) if(speed > -32)
|
|
465
|
+
when KEY_m then
|
|
466
|
+
@xbmc.Mute
|
|
467
|
+
self.updateHeader
|
|
468
|
+
when KEY_p then
|
|
469
|
+
@xbmc.Pause
|
|
470
|
+
self.updateFooter
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
self.update
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def showHelp
|
|
477
|
+
@helpMSG = "UP select previous entry\n"
|
|
478
|
+
@helpMSG += "DOWN select next entry\n"
|
|
479
|
+
@helpMSG += "ENTER play selected entry\n"
|
|
480
|
+
@helpMSG += "d remove selected entry\n"
|
|
481
|
+
@helpMSG += "c clear current playlist\n\n"
|
|
482
|
+
@helpMSG += "RIGHT Volume +\n"
|
|
483
|
+
@helpMSG += "LEFT Volume -\n"
|
|
484
|
+
@helpMSG += "m mute volume\n\n"
|
|
485
|
+
@helpMSG += "f fast forward\n"
|
|
486
|
+
@helpMSG += "b fast rewind\n"
|
|
487
|
+
@helpMSG += "p pause playback"
|
|
488
|
+
super
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def getCurrentSong
|
|
492
|
+
@current_song = @xbmc.GetCurrentlyPlaying
|
|
493
|
+
@playing = (@current_song != nil)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def getCurrentPlaylist
|
|
497
|
+
playlist = @xbmc.GetPlaylistContents
|
|
498
|
+
return if(playlist == @last_playlist)
|
|
499
|
+
@last_playlist = playlist
|
|
500
|
+
|
|
501
|
+
@selected = @forcesel
|
|
502
|
+
@forcesel = 0
|
|
503
|
+
|
|
504
|
+
@playlist = [] # cache tags infos for playlist
|
|
505
|
+
playlist.each { |file|
|
|
506
|
+
song = @xbmc.GetTagFromFilename(file)
|
|
507
|
+
@playlist.push({"path"=>file, "artist"=>song["Artist"], "title"=>song["Title"]})
|
|
508
|
+
}
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
class LibraryWin < TabWins
|
|
514
|
+
|
|
515
|
+
def initialize(pscreen, xbmc)
|
|
516
|
+
super
|
|
517
|
+
@title = "Library"
|
|
518
|
+
@deepth = 0
|
|
519
|
+
@lastdeepth = -1
|
|
520
|
+
@currentdir = nil
|
|
521
|
+
@history = []
|
|
522
|
+
|
|
523
|
+
@search = false
|
|
524
|
+
@searched = ""
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def drawMain
|
|
528
|
+
super
|
|
529
|
+
|
|
530
|
+
if(@lastdeepth != @deepth) # refresh library entries list if needed
|
|
531
|
+
@list = @xbmc.GetMediaLocation("music", @currentdir)
|
|
532
|
+
@lastdeepth = @deepth
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
maxwy = self.handleScroll
|
|
536
|
+
|
|
537
|
+
idx = 0
|
|
538
|
+
@list.each { |entry|
|
|
539
|
+
line = idx - @scroll
|
|
540
|
+
if(line >= 0)
|
|
541
|
+
break if(line > maxwy)
|
|
542
|
+
|
|
543
|
+
@main.attron(Ncurses::A_REVERSE) if(idx == @selected)
|
|
544
|
+
Ncurses.mvwaddstr(@main, line, 1, "#{entry["name"]}")
|
|
545
|
+
@main.attroff(Ncurses::A_REVERSE) if(idx == @selected)
|
|
546
|
+
end
|
|
547
|
+
idx += 1
|
|
548
|
+
}
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def drawFooter
|
|
552
|
+
super
|
|
553
|
+
|
|
554
|
+
if (@search)
|
|
555
|
+
Ncurses.mvwaddstr(@footer, 1, 1, "/ " + @searched)
|
|
556
|
+
else
|
|
557
|
+
histline = ""
|
|
558
|
+
@history.reverse.each {|old|
|
|
559
|
+
histline += " > " + old[:currentname]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
Ncurses.mvwaddstr(@footer, 1, 1, histline)
|
|
563
|
+
|
|
564
|
+
nitems_label = @list.length.to_s + " items"
|
|
565
|
+
@footer.mvwaddstr(1, @maxx - nitems_label.length - 1, nitems_label)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def searchEntry
|
|
570
|
+
if @searched.size > 2
|
|
571
|
+
idx = 0
|
|
572
|
+
@list.each { |entry|
|
|
573
|
+
if entry["name"].downcase.include?(@searched.downcase)
|
|
574
|
+
@selected = idx
|
|
575
|
+
@scroll = idx
|
|
576
|
+
return true
|
|
577
|
+
end
|
|
578
|
+
idx += 1
|
|
579
|
+
}
|
|
580
|
+
end
|
|
581
|
+
return false
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def handleKeySearch(key)
|
|
585
|
+
|
|
586
|
+
case key
|
|
587
|
+
when KEY_ENTER then
|
|
588
|
+
@search = false
|
|
589
|
+
@searched = ""
|
|
590
|
+
@defaultKeys = true
|
|
591
|
+
self.updateFooter
|
|
592
|
+
when KEY_RETURN then
|
|
593
|
+
@searched.chop!()
|
|
594
|
+
self.updateFooter
|
|
595
|
+
else
|
|
596
|
+
begin
|
|
597
|
+
@searched += key.chr
|
|
598
|
+
self.updateFooter
|
|
599
|
+
rescue
|
|
600
|
+
return
|
|
601
|
+
end
|
|
602
|
+
self.updateMain if self.searchEntry
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
self.update
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def handleKeyMain(key)
|
|
609
|
+
|
|
610
|
+
case key
|
|
611
|
+
when KEY_DOWN then
|
|
612
|
+
@selected += 1 if @selected < (@list.length - 1)
|
|
613
|
+
self.updateMain
|
|
614
|
+
when KEY_UP then
|
|
615
|
+
@selected -= 1 if @selected > 0
|
|
616
|
+
self.updateMain
|
|
617
|
+
when KEY_ENTER then
|
|
618
|
+
if @list[@selected]["type"].to_i == XBMC::TYPE_DIRECTORY
|
|
619
|
+
@history.insert(0, {:currentdir=>@currentdir, :selected=>@selected, :scroll=>@scroll, :currentname=>@list[@selected]["name"]})
|
|
620
|
+
@currentdir = @list[@selected]["path"]
|
|
621
|
+
@selected = 0
|
|
622
|
+
@scroll = 0
|
|
623
|
+
@deepth += 1
|
|
624
|
+
end
|
|
625
|
+
self.updateMain
|
|
626
|
+
self.updateFooter
|
|
627
|
+
when KEY_RETURN then
|
|
628
|
+
return if(@history.length < 1)
|
|
629
|
+
oldentry = @history.shift
|
|
630
|
+
@currentdir = oldentry[:currentdir]
|
|
631
|
+
@selected = oldentry[:selected]
|
|
632
|
+
@scroll = oldentry[:scroll]
|
|
633
|
+
@deepth -= 1
|
|
634
|
+
self.updateMain
|
|
635
|
+
self.updateFooter
|
|
636
|
+
when KEY_SPACE then
|
|
637
|
+
@xbmc.AddToPlayList(@list[@selected]["path"], XBMC::MUSIC_PLAYLIST, "[music]")
|
|
638
|
+
@xbmc.SetCurrentPlaylist(XBMC::MUSIC_PLAYLIST)
|
|
639
|
+
#@xbmc.SetPlaylistSong(0)
|
|
640
|
+
when KEY_c then
|
|
641
|
+
@xbmc.Stop
|
|
642
|
+
@xbmc.ClearPlayList(XBMC::MUSIC_PLAYLIST)
|
|
643
|
+
when KEY_s then
|
|
644
|
+
@search = true
|
|
645
|
+
@defaultKeys = false
|
|
646
|
+
self.updateFooter
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
self.update
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def handleKey(key)
|
|
653
|
+
super
|
|
654
|
+
|
|
655
|
+
if (@search)
|
|
656
|
+
self.handleKeySearch(key)
|
|
657
|
+
else
|
|
658
|
+
self.handleKeyMain(key)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def showHelp
|
|
663
|
+
@helpMSG = "UP select previous entry\n"
|
|
664
|
+
@helpMSG += "DOWN select next entry\n"
|
|
665
|
+
@helpMSG += "ENTER browse selected directory\n"
|
|
666
|
+
@helpMSG += "RETURN return into previous directory\n"
|
|
667
|
+
@helpMSG += "SPACE add selected directory/file to playlist\n\n"
|
|
668
|
+
@helpMSG += "c clear current playlist\n"
|
|
669
|
+
@helpMSG += "s search an entry in playlist"
|
|
670
|
+
super
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
##### MAIN ######
|
|
676
|
+
|
|
677
|
+
options = {}
|
|
678
|
+
inputopts = OptionParser.new do |opts|
|
|
679
|
+
options[:port] = NCXMBC_DEFAULTPORT
|
|
680
|
+
|
|
681
|
+
opts.banner = "Usage: %s [options] hostname" % File.basename($0)
|
|
682
|
+
opts.separator " "
|
|
683
|
+
|
|
684
|
+
opts.on("-v", "--version", "Print version") do |v|
|
|
685
|
+
options[:version] = v
|
|
686
|
+
end
|
|
687
|
+
opts.on("-p", "--port [NUMBER]", Integer,"Port used (default %d)" % NCXMBC_DEFAULTPORT) do |port|
|
|
688
|
+
options[:port] = port
|
|
689
|
+
end
|
|
690
|
+
opts.on("-U", "--user [NAME]", String, "Username used for authentication") do |name|
|
|
691
|
+
options[:user] = name
|
|
692
|
+
end
|
|
693
|
+
opts.on("-P", "--password [PASS]", String, "Password used for authentication") do |pass|
|
|
694
|
+
options[:pass] = pass
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
begin
|
|
699
|
+
inputopts.parse!
|
|
700
|
+
rescue OptionParser::InvalidOption => e
|
|
701
|
+
puts "Error: " + e
|
|
702
|
+
puts "\n" + inputopts.banner
|
|
703
|
+
exit 1
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
if(options[:version])
|
|
707
|
+
puts "ncXBMC version " + NCXBMC_VERSION.to_s
|
|
708
|
+
puts <<EOF
|
|
709
|
+
|
|
710
|
+
Copyright (C) 2009-2010 Cedric TESSIER
|
|
711
|
+
|
|
712
|
+
This program may be redistributed under
|
|
713
|
+
the terms of the GPL v2 License
|
|
714
|
+
EOF
|
|
715
|
+
exit 0
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
if(ARGV.length != 1)
|
|
719
|
+
puts inputopts
|
|
720
|
+
exit 1
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
hostname = ARGV.first
|
|
724
|
+
|
|
725
|
+
xbmc = XBMC::XBMC.new(hostname, options[:port], options[:user], options[:pass])
|
|
726
|
+
|
|
727
|
+
begin
|
|
728
|
+
puts "Connected to " + xbmc.host + " (xbmc " + xbmc.GetSystemInfo(120).first + ")"
|
|
729
|
+
rescue XBMC::UnauthenticatedError => e
|
|
730
|
+
puts "ERROR: " + e
|
|
731
|
+
exit 1
|
|
732
|
+
rescue SocketError => e
|
|
733
|
+
puts "ERROR: Connection error"
|
|
734
|
+
puts "Please check given hostname (and/or port)"
|
|
735
|
+
exit 1
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
interface = Interface::NcurseInterface.new(xbmc)
|
|
739
|
+
|
|
740
|
+
interface.run
|
|
741
|
+
|
metadata
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ncXBMC
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: "0.3"
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cedric TESSIER
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2010-02-15 00:00:00 +01:00
|
|
13
|
+
default_executable: ncxbmc.rb
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: ncurses
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0.9"
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: nezetic-ruby-xbmc
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.1.2
|
|
34
|
+
version:
|
|
35
|
+
description: |-
|
|
36
|
+
ncXBMC is a remote XBMC client, with an ncurses interface, which aims to provide a full control of the music player over a local network.
|
|
37
|
+
|
|
38
|
+
It can be used to browse library, manage playlist, and control playback.
|
|
39
|
+
email: nezetic at gmail d o t com
|
|
40
|
+
executables:
|
|
41
|
+
- ncxbmc.rb
|
|
42
|
+
extensions: []
|
|
43
|
+
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
|
|
46
|
+
files:
|
|
47
|
+
- README
|
|
48
|
+
- bin/ncxbmc.rb
|
|
49
|
+
has_rdoc: true
|
|
50
|
+
homepage: http://github.com/nezetic/ncXBMC
|
|
51
|
+
licenses: []
|
|
52
|
+
|
|
53
|
+
post_install_message:
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: "0"
|
|
63
|
+
version:
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: "0"
|
|
69
|
+
version:
|
|
70
|
+
requirements: []
|
|
71
|
+
|
|
72
|
+
rubyforge_project:
|
|
73
|
+
rubygems_version: 1.3.5
|
|
74
|
+
signing_key:
|
|
75
|
+
specification_version: 3
|
|
76
|
+
summary: ncXBMC is a remote XBMC client, with an ncurses interface
|
|
77
|
+
test_files: []
|
|
78
|
+
|