ampv 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +20 -0
- data/bin/ampv +6 -0
- data/input.conf +28 -0
- data/lib/ampv.rb +380 -0
- data/lib/ampv/mpvwidget.rb +125 -0
- data/lib/ampv/playlist.rb +207 -0
- data/lib/ampv/progressbarwidget.rb +36 -0
- data/lib/ampv/version.rb +6 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZTMwNzg3MDNhNjM2ZTk1Njk4MmYyNzllNTYzOTQ4MzQ1OTJiNmJkOQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MmFmYTY5YmQ4Zjc0OTQ4NzE3Y2IxNTBmNmZhNGQ5ZTc5M2E5NDBiMQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YzFkNDdmOTYyOTg1NDdkNjEyZTgwN2NlY2Y0NzU3MjkzODE2OTQ1YWFjZDNm
|
10
|
+
ZTJlMWQyNTUwNTc5ZTVjZTFlZjEzZGZmMjMzY2E1MTRmZDZmOWQ2YzE3MjUx
|
11
|
+
NmU0ZTQyNzA2YWMzMjU2MGI5YzRiMmJjMjk5YjJhYTJmNDcxMDM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MzE0ZjZhZGYyYzhkNWZmNjhiZDA1ZjAyNDBlMmVmMDQyYWVhNGZkOTk4YmQw
|
14
|
+
MWJiMDU0ZGNmYzBjMDIyNzg1OWI3NDE0MWM1Zjk3ZTgwMzRkY2VlOTE1ZWJj
|
15
|
+
OWNiMjZkOTYzY2YxOWEwMGZiMmZlN2RkYzdiNWQxODYxZTZkZjU=
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 ahoka
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bin/ampv
ADDED
data/input.conf
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
MOUSE_BTN0_DBL cycle fullscreen
|
2
|
+
MOUSE_BTN1 cycle pause
|
3
|
+
MOUSE_BTN3 add volume 1
|
4
|
+
MOUSE_BTN4 add volume -1
|
5
|
+
MOUSE_BTN7 playlist_prev
|
6
|
+
MOUSE_BTN8 playlist_next
|
7
|
+
MOUSE_BTN9 cycle pause
|
8
|
+
|
9
|
+
RIGHT seek 10
|
10
|
+
LEFT seek -10
|
11
|
+
UP add volume 1
|
12
|
+
DOWN add volume -1
|
13
|
+
|
14
|
+
PGUP seek -90 - exact
|
15
|
+
PGDWN seek 90 - exact
|
16
|
+
HOME add chapter -1
|
17
|
+
END add chapter 1
|
18
|
+
INS playlist_prev
|
19
|
+
DEL playlist_next
|
20
|
+
|
21
|
+
f cycle fullscreen
|
22
|
+
SPACE cycle pause
|
23
|
+
ESC quit_watch_later
|
24
|
+
|
25
|
+
# ampv commands
|
26
|
+
l cycle playlist
|
27
|
+
o open_file_chooser
|
28
|
+
p cycle progress_bar
|
data/lib/ampv.rb
ADDED
@@ -0,0 +1,380 @@
|
|
1
|
+
|
2
|
+
require "gtk2"
|
3
|
+
require "ampv/mpvwidget"
|
4
|
+
require "ampv/playlist"
|
5
|
+
require "ampv/progressbarwidget"
|
6
|
+
require "ampv/version"
|
7
|
+
require "uri"
|
8
|
+
|
9
|
+
module Ampv
|
10
|
+
class MainWindow < Gtk::Window
|
11
|
+
|
12
|
+
MAIN_CONF = "#{ENV["HOME"]}/.config/ampv.conf"
|
13
|
+
INPUT_CONF = "#{ENV["HOME"]}/.mpv/input.conf"
|
14
|
+
VIDEO_EXTS = [ ".avi", ".mkv", ".mp4", ".mpeg", ".mpg", ".ogm", ".ogv" ]
|
15
|
+
KEY_NAMES = {
|
16
|
+
"esc" => "Escape",
|
17
|
+
"space" => "space",
|
18
|
+
"right" => "Right",
|
19
|
+
"left" => "Left",
|
20
|
+
"up" => "Up",
|
21
|
+
"down" => "Down",
|
22
|
+
"pgup" => "Page_Up",
|
23
|
+
"pgdwn" => "Page_Down",
|
24
|
+
"home" => "Home",
|
25
|
+
"end" => "End",
|
26
|
+
"ins" => "Insert",
|
27
|
+
"del" => "Delete",
|
28
|
+
}
|
29
|
+
WHEEL_BUTTONS = {
|
30
|
+
Gdk::EventScroll::UP => 4,
|
31
|
+
Gdk::EventScroll::DOWN => 5,
|
32
|
+
Gdk::EventScroll::LEFT => 6,
|
33
|
+
Gdk::EventScroll::RIGHT => 7
|
34
|
+
}
|
35
|
+
|
36
|
+
LEFT_PTR = Gdk::Cursor.new(Gdk::Cursor::LEFT_PTR)
|
37
|
+
BLANK_CURSOR = Gdk::Cursor.new(Gdk::Cursor::BLANK_CURSOR)
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
load_config
|
41
|
+
super
|
42
|
+
set_title(PACKAGE)
|
43
|
+
set_default_size(@config["width"], @config["height"])
|
44
|
+
set_window_position(Gtk::Window::POS_CENTER)
|
45
|
+
move(@config["x"], @config["y"]) unless @config["x"] == -1 and @config["y"] == -1
|
46
|
+
|
47
|
+
Gtk::Drag.dest_set(self, Gtk::Drag::DEST_DEFAULT_ALL,
|
48
|
+
[ [ "text/uri-list", 0, 0 ] ],
|
49
|
+
Gdk::DragContext::ACTION_LINK)
|
50
|
+
|
51
|
+
signal_connect("delete_event") { quit }
|
52
|
+
signal_connect("scroll_event") { |w, e| handle_mouse_event(e) }
|
53
|
+
signal_connect("button_press_event") { |w, e| handle_mouse_event(e) }
|
54
|
+
signal_connect("key_press_event") { |w, e| handle_keyboard_event(e) }
|
55
|
+
signal_connect("drag_data_received") { |w, dc, x, y, sd, type, time|
|
56
|
+
handle_drop_event(sd.data, dc, time, false, true)
|
57
|
+
}
|
58
|
+
|
59
|
+
vbox = Gtk::VBox.new
|
60
|
+
add(vbox)
|
61
|
+
|
62
|
+
args = process_args
|
63
|
+
load_bindings
|
64
|
+
|
65
|
+
@mpv = MPvWidget.new(args, @config["scrobbler"])
|
66
|
+
@mpv.signal_connect("file_changed") { |w, file|
|
67
|
+
@playing = file
|
68
|
+
@mpv.send("show_text ${media-title} 1500") if window.state.fullscreen?
|
69
|
+
@playlist.set_selected(@playing)
|
70
|
+
set_title(File.basename(@playing))
|
71
|
+
}
|
72
|
+
@mpv.signal_connect("length_changed") { |w, len|
|
73
|
+
@length = len
|
74
|
+
@playlist.update_length(@length)
|
75
|
+
}
|
76
|
+
@mpv.signal_connect("time_pos_changed") { |w, pos| @progress_bar.value = pos / @length.to_f }
|
77
|
+
@mpv.signal_connect("stopped") {
|
78
|
+
@progress_bar.value = 0
|
79
|
+
next_file = @playlist.get_next
|
80
|
+
@really_stop = next_file.nil? or @really_stop
|
81
|
+
set_title(PACKAGE)
|
82
|
+
if @really_stop and window.state.fullscreen?
|
83
|
+
toggle_fullscreen
|
84
|
+
elsif not @really_stop
|
85
|
+
load_file(next_file, false)
|
86
|
+
end
|
87
|
+
@really_stop = false
|
88
|
+
}
|
89
|
+
|
90
|
+
vbox.pack_start(@mpv)
|
91
|
+
|
92
|
+
@playlist = Playlist.new(@config["playlist_x"],
|
93
|
+
@config["playlist_y"],
|
94
|
+
@config["playlist_width"],
|
95
|
+
@config["playlist_height"],
|
96
|
+
@config["playlist_visible"])
|
97
|
+
@playlist.signal_connect("open_file_chooser") { open_file_chooser }
|
98
|
+
@playlist.signal_connect("drag_data_received") { |w, dc, x, y, sd, type, time|
|
99
|
+
handle_drop_event(sd.data, dc, time, true, false)
|
100
|
+
}
|
101
|
+
@playlist.signal_connect("play_entry") { |w, file| load_file(file, false, false, false, true) }
|
102
|
+
@playlist.signal_connect("playing_removed") { @mpv.send("stop"); @really_stop = true }
|
103
|
+
|
104
|
+
@progress_bar = ProgressBarWidget.new(@config["bar_color"],
|
105
|
+
@config["head_color"],
|
106
|
+
@config["progress_bar_height"])
|
107
|
+
@progress_bar.add_events(Gdk::Event::BUTTON_PRESS_MASK)
|
108
|
+
@progress_bar.signal_connect("button_press_event") { |w, e| handle_seek_event(e) }
|
109
|
+
vbox.pack_start(@progress_bar, false, false)
|
110
|
+
|
111
|
+
show_all
|
112
|
+
@mpv.start
|
113
|
+
|
114
|
+
argv = ARGV.join(" ")
|
115
|
+
if not argv.empty?
|
116
|
+
load_file(argv)
|
117
|
+
elsif not @config["playlist"].empty?
|
118
|
+
@config["playlist"].each { |x| load_file(x, true, true, false) }
|
119
|
+
@playlist.set_selected(@config["playlist_selected"])
|
120
|
+
end
|
121
|
+
|
122
|
+
Gtk.main
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_config
|
126
|
+
@config = {
|
127
|
+
"width" => Gdk::Screen.default.width > 1280 ? 1280 : 853,
|
128
|
+
"height" => Gdk::Screen.default.width > 1280 ? 726 : 486,
|
129
|
+
"x" => -1,
|
130
|
+
"y" => -1,
|
131
|
+
"fullscreen_progressbar" => false,
|
132
|
+
"progress_bar_visible" => true,
|
133
|
+
"progress_bar_height" => 6,
|
134
|
+
"bar_color" => "#8f5b5b",
|
135
|
+
"head_color" => "#c48181",
|
136
|
+
"playlist_width" => 360,
|
137
|
+
"playlist_height" => 550,
|
138
|
+
"playlist_x" => 0,
|
139
|
+
"playlist_y" => 0,
|
140
|
+
"playlist_visible" => true,
|
141
|
+
"always_save_position" => false,
|
142
|
+
"scrobbler" => "",
|
143
|
+
"playlist_selected" => "",
|
144
|
+
"playlist" => [ ],
|
145
|
+
}
|
146
|
+
|
147
|
+
if File.exists?(MAIN_CONF)
|
148
|
+
File.readlines(MAIN_CONF).each { |line|
|
149
|
+
key, _, val = line.partition("=")
|
150
|
+
key = key.strip
|
151
|
+
val = val.strip
|
152
|
+
next unless @config.has_key?(key) and not key.start_with?("#")
|
153
|
+
|
154
|
+
if @config[key].is_a?(Integer)
|
155
|
+
val = val.to_i
|
156
|
+
elsif @config[key].is_a?(TrueClass) or @config[key].is_a?(FalseClass)
|
157
|
+
val = val == "true"
|
158
|
+
elsif @config[key].is_a?(Array)
|
159
|
+
val = val.split("|")
|
160
|
+
elsif val.start_with?("#")
|
161
|
+
begin
|
162
|
+
c = Gdk::Color.parse(val)
|
163
|
+
rescue
|
164
|
+
puts("Invalid hexidecimal color for setting `#{key}': `#{val}")
|
165
|
+
c = Gdk::Color.parse(@config[key])
|
166
|
+
end
|
167
|
+
val = c
|
168
|
+
end
|
169
|
+
|
170
|
+
@config[key] = val
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def process_args
|
176
|
+
args = [ ]
|
177
|
+
ARGV.dup.each { |arg|
|
178
|
+
if arg.start_with?("-")
|
179
|
+
args.push(arg)
|
180
|
+
ARGV.delete(arg)
|
181
|
+
end
|
182
|
+
}
|
183
|
+
return args
|
184
|
+
end
|
185
|
+
|
186
|
+
def load_bindings
|
187
|
+
@mouse_bindings = [ ]
|
188
|
+
@key_bindings = [ ]
|
189
|
+
if File.exists?(INPUT_CONF)
|
190
|
+
File.readlines(INPUT_CONF).each { |line|
|
191
|
+
line = line.strip
|
192
|
+
if line.start_with?("MOUSE_BTN")
|
193
|
+
# 4 = up, 5 = down, 6 = left, 7 = right
|
194
|
+
button, cmd = line.match(/MOUSE_BTN(\d+)(?:_DBL)?\s+(.+)$/).captures
|
195
|
+
button = button.to_i + 1
|
196
|
+
type = (4..7).include?(button) ? Gdk::Event::SCROLL :
|
197
|
+
line.include?("DBL") ? Gdk::Event::BUTTON2_PRESS : Gdk::Event::BUTTON_PRESS
|
198
|
+
@mouse_bindings[type] = [ ] if @mouse_bindings[type].nil?
|
199
|
+
@mouse_bindings[type][button] = cmd
|
200
|
+
elsif not line.empty?
|
201
|
+
key, cmd = line.match(/^([^\s]+)\s+(.+)$/).captures
|
202
|
+
if name = KEY_NAMES[key.downcase]
|
203
|
+
keyval = Gdk::Keyval.from_name(name)
|
204
|
+
else
|
205
|
+
keyval = Gdk::Keyval.from_name(key)
|
206
|
+
end
|
207
|
+
|
208
|
+
@key_bindings[keyval] = cmd if keyval > 0
|
209
|
+
end
|
210
|
+
}
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def load_file(file, add_to_playlist=true, do_not_play=false, auto_add=true, force_play=false)
|
215
|
+
return if file.nil? or file.empty?
|
216
|
+
file = File.expand_path(file) if file[0] == "~"
|
217
|
+
|
218
|
+
if add_to_playlist
|
219
|
+
if @playlist.count == 0 and auto_add and file !~ /^https?:\/\//
|
220
|
+
dir = File.directory?(file) ? file : File.dirname(file)
|
221
|
+
entries = Dir.entries(dir).sort
|
222
|
+
entries.delete_if { |x| x.start_with?(".") or not valid_video_file(x) }
|
223
|
+
entries.map { |x|
|
224
|
+
x = dir + "/" + x
|
225
|
+
@playlist.add_file(x)
|
226
|
+
file = x if file == dir
|
227
|
+
}
|
228
|
+
else
|
229
|
+
@playlist.add_file(file)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
@mpv.load_file(file, force_play) unless do_not_play
|
234
|
+
end
|
235
|
+
|
236
|
+
def handle_mouse_event(e)
|
237
|
+
button = e.event_type == Gdk::Event::SCROLL ? WHEEL_BUTTONS[e.direction] : e.button
|
238
|
+
return if @mouse_bindings[e.event_type].nil?
|
239
|
+
|
240
|
+
process_cmd(@mouse_bindings[e.event_type][button])
|
241
|
+
|
242
|
+
return true
|
243
|
+
end
|
244
|
+
|
245
|
+
def handle_keyboard_event(e)
|
246
|
+
process_cmd(@key_bindings[e.keyval])
|
247
|
+
|
248
|
+
return true
|
249
|
+
end
|
250
|
+
|
251
|
+
def handle_seek_event(e)
|
252
|
+
if e.event_type == Gdk::Event::BUTTON_PRESS and e.button == 1
|
253
|
+
pos = e.x / allocation.width * @length.to_f
|
254
|
+
seek("seek #{pos} absolute")
|
255
|
+
end
|
256
|
+
|
257
|
+
return true
|
258
|
+
end
|
259
|
+
|
260
|
+
def handle_drop_event(data, context, time, do_not_play, replace)
|
261
|
+
files = URI.decode(data).gsub("file://", "").split("\r\n")
|
262
|
+
@playlist.clear if replace
|
263
|
+
|
264
|
+
files.each { |x|
|
265
|
+
load_file(x, true, do_not_play) if valid_video_file(x)
|
266
|
+
}
|
267
|
+
|
268
|
+
Gtk::Drag.finish(context, true, true, time)
|
269
|
+
end
|
270
|
+
|
271
|
+
def process_cmd(cmd)
|
272
|
+
case cmd
|
273
|
+
when "cycle fullscreen"
|
274
|
+
toggle_fullscreen
|
275
|
+
when "cycle pause"
|
276
|
+
@mpv.play_pause
|
277
|
+
when "cycle playlist"
|
278
|
+
@playlist.visible? ? @playlist.hide : @playlist.show
|
279
|
+
when /seek /
|
280
|
+
seek(cmd)
|
281
|
+
when /add chapter/
|
282
|
+
seek(cmd)
|
283
|
+
when "playlist_next"
|
284
|
+
load_file(@playlist.get_next, false)
|
285
|
+
when "playlist_prev"
|
286
|
+
load_file(@playlist.get_prev, false)
|
287
|
+
when "open_file_chooser"
|
288
|
+
open_file_chooser
|
289
|
+
when "cycle progress_bar"
|
290
|
+
toggle_progress_bar
|
291
|
+
when "quit_watch_later"
|
292
|
+
quit(true)
|
293
|
+
when "quit"
|
294
|
+
quit
|
295
|
+
else
|
296
|
+
@mpv.send(cmd) if cmd
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def seek(cmd)
|
301
|
+
cmd = "no-osd " + cmd unless cmd.start_with?("no-osd") or
|
302
|
+
not @progress_bar.visible?
|
303
|
+
@mpv.send(cmd)
|
304
|
+
@mpv.send("get_property time-pos")
|
305
|
+
end
|
306
|
+
|
307
|
+
def toggle_fullscreen
|
308
|
+
if window.state.fullscreen?
|
309
|
+
@progress_bar.show unless @progress_bar_user_hidden
|
310
|
+
unfullscreen
|
311
|
+
window.set_cursor(LEFT_PTR)
|
312
|
+
else
|
313
|
+
@progress_bar.hide unless @config["fullscreen_progressbar"]
|
314
|
+
fullscreen
|
315
|
+
window.set_cursor(BLANK_CURSOR)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def toggle_progress_bar
|
320
|
+
if @progress_bar.visible?
|
321
|
+
@progress_bar.hide
|
322
|
+
@progress_bar_user_hidden = true
|
323
|
+
else
|
324
|
+
@progress_bar.show
|
325
|
+
@progress_bar_user_hidden = false
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def valid_video_file(x)
|
330
|
+
return (VIDEO_EXTS.include?(File.extname(x)) or
|
331
|
+
`file -b --mime-type "#{x}"`.start_with?("video"))
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
def open_file_chooser
|
336
|
+
dialog = Gtk::FileChooserDialog.new("Open File - #{PACKAGE}",
|
337
|
+
self, Gtk::FileChooser::ACTION_OPEN, nil,
|
338
|
+
[ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
|
339
|
+
[ Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT ])
|
340
|
+
dialog.select_multiple = true
|
341
|
+
do_not_play = @playlist.count > 0
|
342
|
+
|
343
|
+
filter = Gtk::FileFilter.new
|
344
|
+
filter.name = "Video Files"
|
345
|
+
VIDEO_EXTS.each { |x| filter.add_pattern("*#{x}") }
|
346
|
+
dialog.add_filter(filter)
|
347
|
+
|
348
|
+
filterAll = Gtk::FileFilter.new
|
349
|
+
filterAll.name = "All Files"
|
350
|
+
filterAll.add_pattern("*.*")
|
351
|
+
dialog.add_filter(filterAll)
|
352
|
+
|
353
|
+
dialog.filenames.each { |x|
|
354
|
+
load_file(x, true, do_not_play)
|
355
|
+
do_not_play = true
|
356
|
+
} if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT
|
357
|
+
dialog.destroy
|
358
|
+
end
|
359
|
+
|
360
|
+
def quit(watch_later=false)
|
361
|
+
@config["x"],
|
362
|
+
@config["y"],
|
363
|
+
@config["width"],
|
364
|
+
@config["height"] = window.geometry unless window.state.fullscreen?
|
365
|
+
@config["playlist_x"],
|
366
|
+
@config["playlist_y"],
|
367
|
+
@config["playlist_width"],
|
368
|
+
@config["playlist_height"] = @playlist.window.geometry
|
369
|
+
@config["playlist_visible"] = @playlist.visible?
|
370
|
+
@config["playlist_selected"] = @playing
|
371
|
+
@config["playlist"] = @playlist.get_entries.join("|")
|
372
|
+
@config["progress_bar_visible"] = @progress_bar.visible?
|
373
|
+
File.open(MAIN_CONF, "w") { |file| @config.each { |k, v| file.puts("#{k}=#{v}") } }
|
374
|
+
|
375
|
+
@mpv.quit(@config["always_save_position"] ? true : watch_later)
|
376
|
+
Gtk.main_quit
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require "fifo"
|
2
|
+
|
3
|
+
module Ampv
|
4
|
+
class MPvWidget < Gtk::EventBox
|
5
|
+
|
6
|
+
type_register
|
7
|
+
signal_new("file_changed", GLib::Signal::RUN_FIRST, nil, nil, String)
|
8
|
+
signal_new("length_changed", GLib::Signal::RUN_FIRST, nil, nil, Integer)
|
9
|
+
signal_new("time_pos_changed", GLib::Signal::RUN_FIRST, nil, nil, Float)
|
10
|
+
signal_new("stopped", GLib::Signal::RUN_FIRST, nil, nil)
|
11
|
+
|
12
|
+
def initialize(args, scrobbler)
|
13
|
+
if args.include?("--debug")
|
14
|
+
args.delete("--debug")
|
15
|
+
@debug = true
|
16
|
+
end
|
17
|
+
|
18
|
+
@scrobbler = scrobbler
|
19
|
+
@mpv_path = "/usr/bin/mpv"
|
20
|
+
@mpv_options = args.join(" ")
|
21
|
+
@mpv_fifo = "/tmp/mpv.fifo." + Process.pid.to_s
|
22
|
+
|
23
|
+
super()
|
24
|
+
|
25
|
+
@socket = Gtk::Socket.new
|
26
|
+
@socket.modify_bg(Gtk::STATE_NORMAL, Gdk::Color.parse("#000"))
|
27
|
+
|
28
|
+
@socket.signal_connect("plug_removed") { signal_emit("stopped"); true }
|
29
|
+
add(@socket)
|
30
|
+
end
|
31
|
+
|
32
|
+
def start
|
33
|
+
if @thread.nil?
|
34
|
+
@fifo = Fifo.new(@mpv_fifo, :w, :nowait)
|
35
|
+
|
36
|
+
cmd = "#{@mpv_path} \
|
37
|
+
--identify \
|
38
|
+
--idle \
|
39
|
+
--input-file=#{@mpv_fifo} \
|
40
|
+
--no-mouse-movements \
|
41
|
+
--cursor-autohide=no \
|
42
|
+
--msglevel=all=2 \
|
43
|
+
--msglevel=global=4 \
|
44
|
+
--wid=#{@socket.id} #{@mpv_options}"
|
45
|
+
@thread = Thread.new { slave_reader(cmd) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def send(cmd)
|
50
|
+
@fifo.puts(cmd) unless @fifo.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_file(file, force_play=false)
|
54
|
+
send("loadfile \"#{file}\"")
|
55
|
+
@force_play = force_play
|
56
|
+
end
|
57
|
+
|
58
|
+
def play_pause
|
59
|
+
send("cycle pause")
|
60
|
+
@is_paused = @is_paused ? false : true
|
61
|
+
end
|
62
|
+
|
63
|
+
def quit(watch_later)
|
64
|
+
send("quit" + (watch_later ? "_watch_later" : ""))
|
65
|
+
@thread.join unless @thread.nil? or not @thread.alive?
|
66
|
+
@fifo.close
|
67
|
+
File.delete(@mpv_fifo)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def slave_reader(cmd)
|
72
|
+
@pipe = IO.popen(cmd, "a+")
|
73
|
+
|
74
|
+
until @pipe.nil? or @pipe.closed? or @pipe.eof?
|
75
|
+
line = @pipe.readline.chomp
|
76
|
+
if line.include?("ID_FILENAME=")
|
77
|
+
signal_emit("file_changed", (@playing = line.partition("ID_FILENAME=").last))
|
78
|
+
send("get_property pause") # saved position also saves play state
|
79
|
+
|
80
|
+
@prog_thread.kill unless @prog_thread.nil? or not @prog_thread.alive?
|
81
|
+
@prog_thread = Thread.new { progress_update }
|
82
|
+
elsif line.start_with?("ID_LENGTH=")
|
83
|
+
signal_emit("length_changed", (@length = line.rpartition("=").last.to_i))
|
84
|
+
elsif line.start_with?("ANS_pause=")
|
85
|
+
@is_paused = line.rpartition("=").last == "yes"
|
86
|
+
play_pause if @force_play and @is_paused
|
87
|
+
elsif line.start_with?("ANS_time-pos=")
|
88
|
+
signal_emit("time_pos_changed", line.rpartition("=").last.to_f)
|
89
|
+
end
|
90
|
+
|
91
|
+
if @debug or line.start_with?("Error")
|
92
|
+
puts(line) unless line.start_with?("ANS_time-pos=") or
|
93
|
+
line.start_with?("ANS_ERROR") or
|
94
|
+
line.start_with?("Failed to get") or
|
95
|
+
line.start_with?("Command ")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def progress_update
|
101
|
+
scrobbled = false
|
102
|
+
watched = 0
|
103
|
+
|
104
|
+
while true
|
105
|
+
send("get_property time-pos") unless @is_paused
|
106
|
+
|
107
|
+
if not @scrobbler.nil? and not scrobbled and watched >= @length * 0.5
|
108
|
+
system("#{@scrobbler} \"#{@playing}\"")
|
109
|
+
scrobbled = true
|
110
|
+
end
|
111
|
+
|
112
|
+
sleep(1)
|
113
|
+
watched += 1 unless @is_paused
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def signal_do_file_changed(file) end
|
118
|
+
def signal_do_length_changed(len) end
|
119
|
+
def signal_do_time_pos_changed(pos) end
|
120
|
+
def signal_do_stopped()
|
121
|
+
@prog_thread.kill
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,207 @@
|
|
1
|
+
#require "gtk2"
|
2
|
+
|
3
|
+
module Ampv
|
4
|
+
class Playlist < Gtk::Window
|
5
|
+
|
6
|
+
type_register
|
7
|
+
signal_new("play_entry", GLib::Signal::RUN_FIRST, nil, nil, String)
|
8
|
+
signal_new("playing_removed", GLib::Signal::RUN_FIRST, nil, nil)
|
9
|
+
signal_new("open_file_chooser", GLib::Signal::RUN_FIRST, nil, nil)
|
10
|
+
|
11
|
+
def initialize(x, y, w, h, is_visible)
|
12
|
+
buttons = {
|
13
|
+
[ Gtk::Stock::OPEN, "Add to Playlist" ] => lambda { signal_emit("open_file_chooser") },
|
14
|
+
[ Gtk::Stock::GO_UP, "Move Up" ] => lambda { move_selected_up },
|
15
|
+
[ Gtk::Stock::GO_DOWN, "Move Down" ] => lambda { move_selected_down },
|
16
|
+
[ Gtk::Stock::REMOVE, "Remove Selected" ] => lambda { remove_selected },
|
17
|
+
[ Gtk::Stock::CLEAR, "Clear Playlist" ] => lambda { clear }
|
18
|
+
}
|
19
|
+
|
20
|
+
super()
|
21
|
+
set_title("Playlist - #{PACKAGE}")
|
22
|
+
set_default_size(w, h)
|
23
|
+
set_skip_taskbar_hint(true)
|
24
|
+
move(x, y)
|
25
|
+
|
26
|
+
Gtk::Drag.dest_set(self, Gtk::Drag::DEST_DEFAULT_ALL,
|
27
|
+
[ [ "text/uri-list", 0, 0 ] ],
|
28
|
+
Gdk::DragContext::ACTION_LINK)
|
29
|
+
|
30
|
+
signal_connect("show") { move(@pos[0], @pos[1]) unless @pos.nil? }
|
31
|
+
signal_connect("hide") { @pos = window.root_origin }
|
32
|
+
signal_connect("delete_event") { hide_on_delete }
|
33
|
+
|
34
|
+
signal_connect("key_press_event") { |_w, e|
|
35
|
+
hide_on_delete if e.keyval == Gdk::Keyval::GDK_Escape
|
36
|
+
}
|
37
|
+
|
38
|
+
vbox = Gtk::VBox.new(false, 10)
|
39
|
+
vbox.border_width = 10
|
40
|
+
add(vbox)
|
41
|
+
|
42
|
+
sw = Gtk::ScrolledWindow.new
|
43
|
+
sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
|
44
|
+
vbox.pack_start(sw)
|
45
|
+
|
46
|
+
@model = Gtk::ListStore.new(String, String)
|
47
|
+
@treeview = Gtk::TreeView.new(@model)
|
48
|
+
@treeview.enable_search = false
|
49
|
+
@treeview.rubber_banding = true
|
50
|
+
@treeview.reorderable = true
|
51
|
+
@treeview.selection.mode = Gtk::SELECTION_MULTIPLE
|
52
|
+
|
53
|
+
@treeview.signal_connect("row_activated") { |_w, p, c|
|
54
|
+
signal_emit("play_entry", @model.get_iter(p)[0])
|
55
|
+
}
|
56
|
+
@treeview.signal_connect("key_press_event") { |_w, e|
|
57
|
+
remove_selected if e.keyval == Gdk::Keyval::GDK_Delete
|
58
|
+
}
|
59
|
+
@treeview.signal_connect("button_press_event") { |_w, e|
|
60
|
+
@menu.popup(nil, nil, e.button, e.time) if
|
61
|
+
e.event_type == Gdk::Event::BUTTON_PRESS and e.button == 3
|
62
|
+
}
|
63
|
+
|
64
|
+
["Name", "Length"].each_with_index { |_x, i|
|
65
|
+
renderer = Gtk::CellRendererText.new
|
66
|
+
column = Gtk::TreeViewColumn.new(_x,
|
67
|
+
renderer,
|
68
|
+
:text => i)
|
69
|
+
if _x == "Name"
|
70
|
+
renderer.ellipsize = Pango::ELLIPSIZE_MIDDLE
|
71
|
+
column.expand = true
|
72
|
+
column.set_cell_data_func(renderer) { |t, c, m, j|
|
73
|
+
c.text = File.basename(m.get_value(j, 0)) unless m.get_value(j, 0).nil?
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
@treeview.append_column(column)
|
78
|
+
}
|
79
|
+
|
80
|
+
sw.add(@treeview)
|
81
|
+
|
82
|
+
hbox = Gtk::HBox.new(true, 5)
|
83
|
+
|
84
|
+
buttons.each { |k, v|
|
85
|
+
button = Gtk::Button.new
|
86
|
+
button.image = Gtk::Image.new(k[0], Gtk::IconSize::BUTTON)
|
87
|
+
button.height_request = 36
|
88
|
+
button.set_tooltip_text(k[1])
|
89
|
+
button.signal_connect("clicked") { v.call }
|
90
|
+
hbox.pack_start(button)
|
91
|
+
}
|
92
|
+
|
93
|
+
vbox.pack_start(hbox, false)
|
94
|
+
|
95
|
+
@menu = Gtk::Menu.new
|
96
|
+
buttons.each { |k, v|
|
97
|
+
item = Gtk::ImageMenuItem.new(k[1])
|
98
|
+
item.image = Gtk::Image.new(k[0], Gtk::IconSize::MENU)
|
99
|
+
item.signal_connect("activate") { v.call }
|
100
|
+
@menu.append(item)
|
101
|
+
}
|
102
|
+
@menu.show_all
|
103
|
+
|
104
|
+
show_all if is_visible
|
105
|
+
end
|
106
|
+
|
107
|
+
def count
|
108
|
+
i = 0
|
109
|
+
@model.each { i += 1 }
|
110
|
+
return i
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_file(file)
|
114
|
+
contains = false
|
115
|
+
@model.each { |m, p, iter|
|
116
|
+
if iter[0] == file
|
117
|
+
contains = true
|
118
|
+
break
|
119
|
+
end
|
120
|
+
}
|
121
|
+
unless contains
|
122
|
+
iter = @model.append
|
123
|
+
iter[0] = file
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def move_selected_up
|
128
|
+
@treeview.selection.selected_rows.each { |path|
|
129
|
+
tmp = path.dup
|
130
|
+
break if not tmp.prev! or @treeview.selection.selected_rows.include?(tmp)
|
131
|
+
@model.move_before(@model.get_iter(path), @model.get_iter(tmp))
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
def move_selected_down
|
136
|
+
@treeview.selection.selected_rows.reverse.each { |path|
|
137
|
+
break if @treeview.selection.selected_rows.include?((tmp = path.dup.next!))
|
138
|
+
tmp = @model.get_iter(tmp)
|
139
|
+
@model.move_after(@model.get_iter(path), tmp) unless tmp.nil?
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def remove_selected
|
144
|
+
to_remove = [ ]
|
145
|
+
@treeview.selection.selected_rows.each { |path|
|
146
|
+
to_remove.push(@model.get_iter(path))
|
147
|
+
}
|
148
|
+
to_remove.each { |iter|
|
149
|
+
signal_emit("playing_removed") if iter[0] == @playing
|
150
|
+
@model.remove(iter)
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_next
|
155
|
+
@model.each { |m, p, iter|
|
156
|
+
return iter.next! ? iter[0] : nil if iter[0] == @playing
|
157
|
+
}
|
158
|
+
return nil
|
159
|
+
end
|
160
|
+
|
161
|
+
def get_prev
|
162
|
+
prev = nil
|
163
|
+
@model.each { |m, p, iter|
|
164
|
+
return prev if iter[0] == @playing
|
165
|
+
prev = iter[0]
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def get_entries
|
170
|
+
entries = [ ]
|
171
|
+
@model.each { |m, p, iter| entries.push(iter[0]) }
|
172
|
+
return entries
|
173
|
+
end
|
174
|
+
|
175
|
+
def clear
|
176
|
+
@model.clear
|
177
|
+
signal_emit("playing_removed")
|
178
|
+
end
|
179
|
+
|
180
|
+
def set_selected(file)
|
181
|
+
@playing = file
|
182
|
+
i = 0
|
183
|
+
@model.each { |m, p, iter|
|
184
|
+
if iter[0] == @playing
|
185
|
+
@treeview.set_cursor(Gtk::TreePath.new(i), nil, false)
|
186
|
+
break
|
187
|
+
end
|
188
|
+
i += 1
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
def update_length(length)
|
193
|
+
@model.each { |m, p, iter|
|
194
|
+
if iter[0] == @playing
|
195
|
+
iter[1] = Time.at(length).utc.strftime("%H:%M:%S") unless length == 0
|
196
|
+
break
|
197
|
+
end
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def signal_do_play_entry(file) end
|
203
|
+
def signal_do_playing_removed() end
|
204
|
+
def signal_do_open_file_chooser() end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module Ampv
|
3
|
+
class ProgressBarWidget < Gtk::DrawingArea
|
4
|
+
def initialize(bar_color, head_color, height)
|
5
|
+
super()
|
6
|
+
modify_bg(Gtk::STATE_NORMAL, Gdk::Color.parse("#000"))
|
7
|
+
set_height_request(height)
|
8
|
+
|
9
|
+
signal_connect("expose_event") {
|
10
|
+
@cx = window.create_cairo_context
|
11
|
+
draw_widget
|
12
|
+
}
|
13
|
+
@value = 0
|
14
|
+
@bar_color = bar_color
|
15
|
+
@head_color = head_color
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw_widget
|
19
|
+
@cx.set_source_color(@bar_color)
|
20
|
+
@cx.rectangle(0, 0, allocation.width * @value.to_f, allocation.height)
|
21
|
+
@cx.fill
|
22
|
+
|
23
|
+
if @value > 0
|
24
|
+
@cx.set_source_color(@head_color)
|
25
|
+
@cx.rectangle(allocation.width * @value.to_f, 0, 2, allocation.height)
|
26
|
+
@cx.fill
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def value=(v)
|
31
|
+
@value = v
|
32
|
+
queue_draw
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/lib/ampv/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ampv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ahoka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gtk2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-fifo
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: A minimal GTK2 mpv frontend.
|
42
|
+
email:
|
43
|
+
executables:
|
44
|
+
- ampv
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- LICENSE
|
49
|
+
- input.conf
|
50
|
+
- lib/ampv/version.rb
|
51
|
+
- lib/ampv/playlist.rb
|
52
|
+
- lib/ampv/progressbarwidget.rb
|
53
|
+
- lib/ampv/mpvwidget.rb
|
54
|
+
- lib/ampv.rb
|
55
|
+
- bin/ampv
|
56
|
+
homepage: https://github.com/ahodesuka/ampv
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.9.3
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements:
|
75
|
+
- gtk2
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 2.0.3
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: ampv
|
81
|
+
test_files: []
|