ampv 1.0.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 +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: []
|