vlcraptor 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/Gemfile +4 -5
- data/Gemfile.lock +44 -5
- data/README.md +58 -13
- data/exe/vlcraptor +56 -0
- data/lib/vlcraptor/ffmpeg.rb +70 -0
- data/lib/vlcraptor/notifiers.rb +79 -0
- data/lib/vlcraptor/player.rb +95 -0
- data/lib/vlcraptor/player_controller.rb +161 -0
- data/lib/vlcraptor/preferences.rb +67 -0
- data/lib/vlcraptor/queue.rb +79 -0
- data/lib/vlcraptor/scrobbler.rb +129 -0
- data/lib/vlcraptor/settings.rb +29 -0
- data/lib/vlcraptor/version.rb +1 -1
- data/lib/vlcraptor.rb +136 -3
- data/vlcraptor.gemspec +3 -5
- metadata +58 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62d27be7a5541a19d3e2f69cf98ee1506b8b65560deffa547c9e621f860977b9
|
4
|
+
data.tar.gz: af56137e7d23b949a5ba94453f9d3bb4e0c45020722b3d0456393b7c7bfed4b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03d0920671d6dfa40dd866c70a915fe7ac6c13a5d84dd0507cdaa6104d0fc2352b2d4b4ab6af10957b7b3ad078f673a2ceda91552134c0b32172186fcafcb75d
|
7
|
+
data.tar.gz: 8519f300d25106c8138cf4921c03f2394191f28f853220203b09079d39d9f972cf40331f6e30a2a9f8549028014a9d1542f6c1b841fef9a1f772fffa1d86c910
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.1.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,19 +1,36 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
vlcraptor (0.
|
4
|
+
vlcraptor (0.4.0)
|
5
|
+
curses
|
6
|
+
rainbow
|
7
|
+
vlc-client
|
5
8
|
|
6
9
|
GEM
|
7
10
|
remote: https://rubygems.org/
|
8
11
|
specs:
|
9
12
|
ast (2.4.2)
|
13
|
+
backport (1.2.0)
|
14
|
+
benchmark (0.2.0)
|
15
|
+
curses (1.4.4)
|
10
16
|
diff-lcs (1.5.0)
|
17
|
+
e2mmap (0.1.0)
|
18
|
+
jaro_winkler (1.5.4)
|
19
|
+
kramdown (2.3.1)
|
20
|
+
rexml
|
21
|
+
kramdown-parser-gfm (1.1.0)
|
22
|
+
kramdown (~> 2.0)
|
23
|
+
nokogiri (1.13.1-x86_64-darwin)
|
24
|
+
racc (~> 1.4)
|
11
25
|
parallel (1.22.1)
|
12
26
|
parser (3.1.1.0)
|
13
27
|
ast (~> 2.4.1)
|
28
|
+
racc (1.6.0)
|
14
29
|
rainbow (3.1.1)
|
15
30
|
rake (13.0.6)
|
16
31
|
regexp_parser (2.2.1)
|
32
|
+
reverse_markdown (2.1.1)
|
33
|
+
nokogiri
|
17
34
|
rexml (3.2.5)
|
18
35
|
rspec (3.11.0)
|
19
36
|
rspec-core (~> 3.11.0)
|
@@ -40,16 +57,38 @@ GEM
|
|
40
57
|
rubocop-ast (1.16.0)
|
41
58
|
parser (>= 3.1.1.0)
|
42
59
|
ruby-progressbar (1.11.0)
|
60
|
+
solargraph (0.44.3)
|
61
|
+
backport (~> 1.2)
|
62
|
+
benchmark
|
63
|
+
bundler (>= 1.17.2)
|
64
|
+
diff-lcs (~> 1.4)
|
65
|
+
e2mmap
|
66
|
+
jaro_winkler (~> 1.5)
|
67
|
+
kramdown (~> 2.3)
|
68
|
+
kramdown-parser-gfm (~> 1.1)
|
69
|
+
parser (~> 3.0)
|
70
|
+
reverse_markdown (>= 1.0.5, < 3)
|
71
|
+
rubocop (>= 0.52)
|
72
|
+
thor (~> 1.0)
|
73
|
+
tilt (~> 2.0)
|
74
|
+
yard (~> 0.9, >= 0.9.24)
|
75
|
+
thor (1.2.1)
|
76
|
+
tilt (2.0.10)
|
43
77
|
unicode-display_width (2.1.0)
|
78
|
+
vlc-client (0.0.7)
|
79
|
+
webrick (1.7.0)
|
80
|
+
yard (0.9.27)
|
81
|
+
webrick (~> 1.7.0)
|
44
82
|
|
45
83
|
PLATFORMS
|
46
84
|
x86_64-darwin-21
|
47
85
|
|
48
86
|
DEPENDENCIES
|
49
|
-
rake
|
50
|
-
rspec
|
51
|
-
rubocop
|
87
|
+
rake
|
88
|
+
rspec
|
89
|
+
rubocop
|
90
|
+
solargraph
|
52
91
|
vlcraptor!
|
53
92
|
|
54
93
|
BUNDLED WITH
|
55
|
-
2.
|
94
|
+
2.3.3
|
data/README.md
CHANGED
@@ -1,28 +1,73 @@
|
|
1
1
|
# Vlcraptor
|
2
2
|
|
3
|
-
|
3
|
+
This is a queueing daemon for VLC - kind of like `mpd` but nowhere near as flexible or useful.
|
4
4
|
|
5
|
-
|
5
|
+
The player daemon starts two instances of VLC and then uses those to play any tracks placed in the queue.
|
6
6
|
|
7
|
-
|
7
|
+
Why two VLC instances? VLC doesn't support cross fading between tracks so this crossfades by starting the
|
8
|
+
other VLC instance playing the next track and adjusting volume between both.
|
8
9
|
|
9
|
-
|
10
|
+
At the moment, only mac os is supported - minor changes would be required to allow this to work on linux.
|
10
11
|
|
11
|
-
|
12
|
-
gem 'vlcraptor'
|
13
|
-
```
|
12
|
+
## Installation
|
14
13
|
|
15
|
-
|
14
|
+
You need to install VLC in the default location for mac os (`/Applications/VLC.app`).
|
16
15
|
|
17
|
-
|
16
|
+
`brew install ffmpeg` is required by the `queue` command for extracting tags to place in the queue.
|
18
17
|
|
19
|
-
|
18
|
+
`brew install terminal-notifier` is optional depending if you want terminal notifications when new tracks start.
|
20
19
|
|
21
|
-
|
20
|
+
This gem can be installed with `gem install vlcraptor`.
|
22
21
|
|
23
22
|
## Usage
|
24
23
|
|
25
|
-
|
24
|
+
Running `vlcraptor` without any parameters will list the available subcommands.
|
25
|
+
|
26
|
+
### Player
|
27
|
+
|
28
|
+
Run `vlcraptor player` in a shell session.
|
29
|
+
|
30
|
+
This is the player daemon that controls two instances
|
31
|
+
of VLC and is an ncurses application which will take over the display in that terminal.
|
32
|
+
|
33
|
+
You can quit with 'q', pause with ' ', stop with 's', play (resume) with 'p' and skip with 'n'.
|
34
|
+
|
35
|
+
### Adding tracks to the queue
|
36
|
+
|
37
|
+
`vlcraptor queue folder_containing_audio_files audio_file.mp3` will place any number of audio files in the
|
38
|
+
queue and the player should immediately start playing the first track.
|
39
|
+
|
40
|
+
### Viewing queue contents
|
41
|
+
|
42
|
+
`vlcraptor list` will list currently queued tracks with an estimated start time if the player is currently
|
43
|
+
running and playing a track.
|
44
|
+
|
45
|
+
### Managing queue
|
46
|
+
|
47
|
+
`vlcraptor clear` will clear all queued tracks.
|
48
|
+
|
49
|
+
`vlcraptor remove 2` will remove queued track at index position 2.
|
50
|
+
|
51
|
+
`vlcraptor swap 2 4` will swap the tracks at index positions 2 and 4.
|
52
|
+
|
53
|
+
### Media controls
|
54
|
+
|
55
|
+
`vlcraptor pause` will pause, `vlcraptor stop` will stop and `vlcraptor play` will resume.
|
56
|
+
|
57
|
+
`vlcraptor skip` will fade out the current track and start the next one (unless the queue is empty).
|
58
|
+
|
59
|
+
### Optional features
|
60
|
+
|
61
|
+
A number of features can be turned on/off while the player is running that will determine certain behaviour:
|
62
|
+
|
63
|
+
`vlcraptor autoplay off` will cause the player to stop and politely wait after the current track has finished.
|
64
|
+
`vlcraptor autoplay on` and tracks will start playing again.
|
65
|
+
|
66
|
+
`vlcraptor crossfade off` will turn off crossfading so new tracks will start once the previous one is
|
67
|
+
completely finished.
|
68
|
+
|
69
|
+
`vlcraptor scrobble on` will turn on last.fm scrobbling - you will require your own application api key
|
70
|
+
and secret to enable this.
|
26
71
|
|
27
72
|
## Development
|
28
73
|
|
@@ -32,4 +77,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
77
|
|
33
78
|
## Contributing
|
34
79
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
80
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/markryall/vlcraptor.
|
data/exe/vlcraptor
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), "..", "lib")
|
6
|
+
|
7
|
+
require "vlcraptor"
|
8
|
+
|
9
|
+
command = ARGV.shift
|
10
|
+
|
11
|
+
case command
|
12
|
+
when "autoplay"
|
13
|
+
Vlcraptor.autoplay(ARGV.shift)
|
14
|
+
when "clear"
|
15
|
+
Vlcraptor.clear
|
16
|
+
when "crossfade"
|
17
|
+
Vlcraptor.crossfade(ARGV.shift)
|
18
|
+
when "history"
|
19
|
+
Vlcraptor.history
|
20
|
+
when "list"
|
21
|
+
Vlcraptor.list
|
22
|
+
when "pause"
|
23
|
+
Vlcraptor.pause
|
24
|
+
when "play"
|
25
|
+
Vlcraptor.play
|
26
|
+
when "player"
|
27
|
+
Vlcraptor.player
|
28
|
+
when "remove"
|
29
|
+
Vlcraptor.remove(ARGV.shift)
|
30
|
+
when "queue"
|
31
|
+
Vlcraptor.queue(ARGV)
|
32
|
+
when "scrobble"
|
33
|
+
Vlcraptor.scrobble(ARGV.shift)
|
34
|
+
when "skip"
|
35
|
+
Vlcraptor.skip
|
36
|
+
when "stop"
|
37
|
+
Vlcraptor.stop
|
38
|
+
when "swap"
|
39
|
+
Vlcraptor.swap(ARGV)
|
40
|
+
else
|
41
|
+
puts "Unknown command \"#{command}\":"
|
42
|
+
puts " autoplay on/off: continue playing tracks or stop at the end of current track"
|
43
|
+
puts " clear: clear queue"
|
44
|
+
puts " crossfade on/off: 5 second crossfade when changing tracks"
|
45
|
+
puts " history: display play history"
|
46
|
+
puts " list: list current queue"
|
47
|
+
puts " pause: pause current track (resume with play)"
|
48
|
+
puts " play: resume after pause/stop"
|
49
|
+
puts " player: start the player"
|
50
|
+
puts " queue paths: queue folders or files containing music tracks"
|
51
|
+
puts " remove a: remove track from queue at index position a"
|
52
|
+
puts " scrobble on/off: send track information to last.fm (requires an api key)"
|
53
|
+
puts " skip: skip the current track"
|
54
|
+
puts " stop: stop the player (resume with play)"
|
55
|
+
puts " swap a b: swap tracks in queue at index positions a and b"
|
56
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vlcraptor
|
4
|
+
class Ffmpeg
|
5
|
+
attr_reader(
|
6
|
+
:title,
|
7
|
+
:album,
|
8
|
+
:artist,
|
9
|
+
:albumartist,
|
10
|
+
:time,
|
11
|
+
:date,
|
12
|
+
:track,
|
13
|
+
:puid,
|
14
|
+
:mbartistid,
|
15
|
+
:mbalbumid,
|
16
|
+
:mbalbumartistid,
|
17
|
+
:asin,
|
18
|
+
)
|
19
|
+
|
20
|
+
CHARS = " `';&!()$".scan(/./)
|
21
|
+
|
22
|
+
def initialize(path)
|
23
|
+
@path = CHARS.inject(path) { |s, char| s.gsub(char) { "\\#{char}" } }
|
24
|
+
`ffmpeg -i #{@path} 2>&1`.each_line do |line|
|
25
|
+
l = line.chomp
|
26
|
+
case l
|
27
|
+
when " Metadata:"
|
28
|
+
@meta = {}
|
29
|
+
else
|
30
|
+
if @meta
|
31
|
+
m = / *: */.match l
|
32
|
+
add_meta m.pre_match.strip.downcase.to_sym, m.post_match.strip if m
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@title = tag :title, :tit2
|
38
|
+
@album = tag :album, :talb
|
39
|
+
@artist = tag :artist, :tpe1, :tpe2
|
40
|
+
@albumartist = tag :album_artist, :tso2
|
41
|
+
@time = to_duration tag :duration
|
42
|
+
@date = tag :date, :tdrc, :tyer
|
43
|
+
@track = tag :track, :trck
|
44
|
+
@puid = tag :"musicip puid"
|
45
|
+
@mbartistid = tag :musicbrainz_artistid, :"musicbrainz artist id"
|
46
|
+
@mbalbumid = tag :musicbrainz_albumid, :"musicbrainz album id"
|
47
|
+
@mbalbumartistid = tag :musicbrainz_albumartistid, :"musicbrainz album artist id"
|
48
|
+
@asin = tag :asin
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_meta(key, value)
|
52
|
+
@meta[key] ||= value
|
53
|
+
end
|
54
|
+
|
55
|
+
def tag(*names)
|
56
|
+
names.each { |name| return @meta[name] if @meta[name] }
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def to_duration(s)
|
63
|
+
return nil unless s
|
64
|
+
|
65
|
+
first, = s.split ","
|
66
|
+
hours, minutes, seconds = first.split ":"
|
67
|
+
seconds.to_i + (minutes.to_i * 60) + (hours.to_i * 60 * 60)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rainbow"
|
4
|
+
require_relative "preferences"
|
5
|
+
require_relative "scrobbler"
|
6
|
+
|
7
|
+
module Vlcraptor
|
8
|
+
class Notifiers
|
9
|
+
def initialize
|
10
|
+
@preferences = Vlcraptor::Preferences.new
|
11
|
+
@history = "#{File.expand_path("~")}/.player_history"
|
12
|
+
end
|
13
|
+
|
14
|
+
def track_suspended
|
15
|
+
@preferences[:started] = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def track_resumed(track, elapsed)
|
19
|
+
return unless track
|
20
|
+
|
21
|
+
track[:start_time] = Time.now - elapsed
|
22
|
+
@preferences[:started] = track[:start_time].to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
def track_started(track)
|
26
|
+
return unless track
|
27
|
+
|
28
|
+
track[:start_time] = Time.now
|
29
|
+
@preferences[:started] = track[:start_time].to_i
|
30
|
+
scrobbler&.now_playing(track[:artist], track[:title])
|
31
|
+
terminal_notify(
|
32
|
+
message: "#{track[:title]} by #{track[:artist]}",
|
33
|
+
title: "Now Playing",
|
34
|
+
)
|
35
|
+
|
36
|
+
len = if track[:length] > 60
|
37
|
+
"(#{track[:length] / 60}m and #{track[:length] % 60}s)"
|
38
|
+
else
|
39
|
+
"(#{track[:length]}s)"
|
40
|
+
end
|
41
|
+
|
42
|
+
message = [
|
43
|
+
display_time(track[:start_time]),
|
44
|
+
Rainbow(track[:title]).green,
|
45
|
+
"by",
|
46
|
+
Rainbow(track[:artist]).yellow,
|
47
|
+
"from",
|
48
|
+
Rainbow(track[:album]).cyan,
|
49
|
+
len,
|
50
|
+
].join(" ")
|
51
|
+
|
52
|
+
File.open(@history, "a") { |file| file.puts message }
|
53
|
+
end
|
54
|
+
|
55
|
+
def track_finished(track)
|
56
|
+
@preferences[:started] = nil
|
57
|
+
|
58
|
+
return unless track
|
59
|
+
|
60
|
+
scrobbler&.scrobble(track[:artist], track[:title], timestamp: track[:start_time].to_i)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def display_time(time)
|
66
|
+
time.strftime("%I:%M:%S")
|
67
|
+
end
|
68
|
+
|
69
|
+
def terminal_notify(message:, title:)
|
70
|
+
return if `which terminal-notifier`.empty?
|
71
|
+
|
72
|
+
`terminal-notifier -group vlc -message "#{message}" -title "#{title}"`
|
73
|
+
end
|
74
|
+
|
75
|
+
def scrobbler
|
76
|
+
Vlcraptor::Scrobbler.load if @preferences.scrobble?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "vlc-client"
|
4
|
+
|
5
|
+
module Vlcraptor
|
6
|
+
class Player
|
7
|
+
def initialize
|
8
|
+
vlc_host = ENV.fetch("VLC_HOST", "localhost")
|
9
|
+
vlc_port_one = ENV.fetch("VLC_PORT", 4212)
|
10
|
+
vlc_port_two = ENV.fetch("VLC_PORT", 4213)
|
11
|
+
|
12
|
+
@pid_one = spawn(
|
13
|
+
"/Applications/VLC.app/Contents/MacOS/VLC --intf rc --rc-host localhost:#{vlc_port_one}",
|
14
|
+
%i[out err] => "/dev/null"
|
15
|
+
)
|
16
|
+
|
17
|
+
@pid_two = spawn(
|
18
|
+
"/Applications/VLC.app/Contents/MacOS/VLC --intf rc --rc-host localhost:#{vlc_port_two}",
|
19
|
+
%i[out err] => "/dev/null"
|
20
|
+
)
|
21
|
+
|
22
|
+
# wait a bit for the VLC processes to be started
|
23
|
+
sleep 0.5
|
24
|
+
|
25
|
+
@vlc = VLC::Client.new(vlc_host, vlc_port_one)
|
26
|
+
@vlc.connect
|
27
|
+
|
28
|
+
@vlc_other = VLC::Client.new(vlc_host, vlc_port_two)
|
29
|
+
@vlc_other.connect
|
30
|
+
end
|
31
|
+
|
32
|
+
def playing?
|
33
|
+
@vlc.playing?
|
34
|
+
end
|
35
|
+
|
36
|
+
def time
|
37
|
+
@vlc.time
|
38
|
+
end
|
39
|
+
|
40
|
+
def remaining
|
41
|
+
@vlc.length - @vlc.time
|
42
|
+
end
|
43
|
+
|
44
|
+
def fadeout
|
45
|
+
(0..10).each do |index|
|
46
|
+
diff = (256 * index) / 10
|
47
|
+
@vlc.volume = 256 - diff
|
48
|
+
sleep 0.5
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fadein
|
53
|
+
@vlc.volume = 0
|
54
|
+
@vlc.play
|
55
|
+
|
56
|
+
(0..10).each do |index|
|
57
|
+
diff = (256 * index) / 10
|
58
|
+
@vlc.volume = diff
|
59
|
+
sleep 0.5
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def crossfade(path)
|
64
|
+
@vlc_other.volume = 0
|
65
|
+
@vlc_other.play path
|
66
|
+
|
67
|
+
(0..10).each do |index|
|
68
|
+
diff = (256 * index) / 10
|
69
|
+
@vlc.volume = 256 - diff
|
70
|
+
@vlc_other.volume = diff
|
71
|
+
sleep 0.5
|
72
|
+
end
|
73
|
+
|
74
|
+
@vlc.stop
|
75
|
+
@vlc, @vlc_other = @vlc_other, @vlc
|
76
|
+
end
|
77
|
+
|
78
|
+
def play(path = nil)
|
79
|
+
@vlc.play(path)
|
80
|
+
end
|
81
|
+
|
82
|
+
def pause
|
83
|
+
@vlc.pause
|
84
|
+
end
|
85
|
+
|
86
|
+
def stop
|
87
|
+
@vlc.stop
|
88
|
+
end
|
89
|
+
|
90
|
+
def cleanup
|
91
|
+
`kill -9 #{@pid_one}` if @pid_one
|
92
|
+
`kill -9 #{@pid_two}` if @pid_two
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "player"
|
4
|
+
require_relative "preferences"
|
5
|
+
require_relative "queue"
|
6
|
+
require_relative "notifiers"
|
7
|
+
|
8
|
+
module Vlcraptor
|
9
|
+
class PlayerController
|
10
|
+
def initialize
|
11
|
+
@player = Vlcraptor::Player.new
|
12
|
+
@preferences = Vlcraptor::Preferences.new
|
13
|
+
@queue = Vlcraptor::Queue.new
|
14
|
+
@notifiers = Vlcraptor::Notifiers.new
|
15
|
+
@track = nil
|
16
|
+
@suspended = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def next
|
20
|
+
return on_pause if @preferences.pause?
|
21
|
+
return on_stop if @preferences.stop?
|
22
|
+
return on_play if @preferences.play?
|
23
|
+
return on_suspended if @suspended
|
24
|
+
return when_playing if @player.playing?
|
25
|
+
return when_auto if @preferences.continue?
|
26
|
+
|
27
|
+
when_manual
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup
|
31
|
+
@notifiers.track_suspended
|
32
|
+
@player.fadeout
|
33
|
+
@player.cleanup
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def on_pause
|
39
|
+
when_suspended("Now Paused", :pause)
|
40
|
+
end
|
41
|
+
|
42
|
+
def on_stop
|
43
|
+
when_suspended("Now Stopped", :stop)
|
44
|
+
end
|
45
|
+
|
46
|
+
def when_suspended(status, method)
|
47
|
+
if @player.playing?
|
48
|
+
@player.fadeout
|
49
|
+
@player.send(method)
|
50
|
+
end
|
51
|
+
@status = status
|
52
|
+
@suspended = true
|
53
|
+
@notifiers.track_suspended
|
54
|
+
on_suspended
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_suspended
|
58
|
+
build
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_play
|
62
|
+
unless @player.playing?
|
63
|
+
@player.fadein
|
64
|
+
end
|
65
|
+
@suspended = false
|
66
|
+
@notifiers.track_resumed(@track, @player.time)
|
67
|
+
when_playing_track(@player.remaining)
|
68
|
+
end
|
69
|
+
|
70
|
+
def when_playing
|
71
|
+
return on_skip if @preferences.skip?
|
72
|
+
return on_crossfade if @preferences.crossfade? && @player.remaining < 5
|
73
|
+
|
74
|
+
when_playing_track(@player.remaining)
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_skip
|
78
|
+
@track = @queue.next
|
79
|
+
@notifiers.track_suspended
|
80
|
+
if @track
|
81
|
+
@notifiers.track_started(@track)
|
82
|
+
@player.crossfade(@track[:path])
|
83
|
+
when_playing_track(@player.remaining)
|
84
|
+
else
|
85
|
+
@player.fadeout
|
86
|
+
when_empty
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def on_crossfade
|
91
|
+
@notifiers.track_finished(@track)
|
92
|
+
@track = @queue.next
|
93
|
+
if @track
|
94
|
+
@notifiers.track_started(@track)
|
95
|
+
@player.crossfade(@track[:path])
|
96
|
+
when_playing_track(@player.remaining)
|
97
|
+
else
|
98
|
+
@player.fadeout
|
99
|
+
when_empty
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def when_auto
|
104
|
+
@notifiers.track_finished(@track)
|
105
|
+
@track = @queue.next
|
106
|
+
return when_empty unless @track
|
107
|
+
|
108
|
+
@notifiers.track_started(@track)
|
109
|
+
@player.play(@track[:path])
|
110
|
+
when_playing_track(@track[:length])
|
111
|
+
end
|
112
|
+
|
113
|
+
def when_playing_track(remaining)
|
114
|
+
@status = "Now Playing"
|
115
|
+
remaining_color = remaining < 30 ? 9 : 5
|
116
|
+
build(
|
117
|
+
[2, @track[:title]],
|
118
|
+
[0, "by"],
|
119
|
+
[11, @track[:artist]],
|
120
|
+
[0, "from"],
|
121
|
+
[6, @track[:album]],
|
122
|
+
[0, "(#{duration(@track[:length])})"],
|
123
|
+
[remaining_color, "#{duration(remaining)} remaining"],
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def when_empty
|
128
|
+
@status = "Now Waiting"
|
129
|
+
build
|
130
|
+
end
|
131
|
+
|
132
|
+
def when_manual
|
133
|
+
@status = "Now Waiting"
|
134
|
+
build
|
135
|
+
end
|
136
|
+
|
137
|
+
def display_time(time)
|
138
|
+
time.strftime("%I:%M:%S")
|
139
|
+
end
|
140
|
+
|
141
|
+
def duration(seconds)
|
142
|
+
if seconds > 60
|
143
|
+
"#{seconds / 60}m and #{seconds % 60}s"
|
144
|
+
else
|
145
|
+
"#{seconds}s"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def build(*extra)
|
150
|
+
autoplay = @preferences[:autoplay] ? "+" : "-"
|
151
|
+
crossfade = @preferences[:crossfade] ? "+" : "-"
|
152
|
+
scrobble = @preferences[:scrobble] ? "+" : "-"
|
153
|
+
[
|
154
|
+
[0, display_time(Time.now)],
|
155
|
+
[0, @status],
|
156
|
+
[0, "#{Vlcraptor::Queue.length} items in queue"],
|
157
|
+
[8, "#{autoplay}autoplay #{crossfade}crossfade #{scrobble}scrobble"],
|
158
|
+
] + extra
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Vlcraptor
|
7
|
+
class Preferences
|
8
|
+
def initialize
|
9
|
+
@path = "#{File.expand_path("~")}/.player"
|
10
|
+
persist({ autoplay: true, crossfade: true }) unless File.exist?(@path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def continue?
|
14
|
+
self[:autoplay]
|
15
|
+
end
|
16
|
+
|
17
|
+
def crossfade?
|
18
|
+
self[:autoplay] && self[:crossfade]
|
19
|
+
end
|
20
|
+
|
21
|
+
def scrobble?
|
22
|
+
self[:scrobble]
|
23
|
+
end
|
24
|
+
|
25
|
+
def pause?
|
26
|
+
reset(:pause)
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop?
|
30
|
+
reset(:stop)
|
31
|
+
end
|
32
|
+
|
33
|
+
def play?
|
34
|
+
reset(:play)
|
35
|
+
end
|
36
|
+
|
37
|
+
def skip?
|
38
|
+
reset(:skip)
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
load_preferences[key]
|
43
|
+
end
|
44
|
+
|
45
|
+
def []=(key, value)
|
46
|
+
preferences = load_preferences
|
47
|
+
preferences[key] = value
|
48
|
+
persist(preferences)
|
49
|
+
end
|
50
|
+
|
51
|
+
def persist(preferences)
|
52
|
+
File.open(@path, "w") { |f| f.puts preferences.to_yaml }
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def load_preferences
|
58
|
+
YAML.load_file(@path)
|
59
|
+
end
|
60
|
+
|
61
|
+
def reset(key)
|
62
|
+
result = self[key]
|
63
|
+
self[key] = false if result
|
64
|
+
result
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require_relative "ffmpeg"
|
5
|
+
|
6
|
+
module Vlcraptor
|
7
|
+
class Queue
|
8
|
+
def initialize
|
9
|
+
@current_path = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def next
|
13
|
+
`rm -f #{@current_path}` if @current_path
|
14
|
+
@current_path = Dir["/tmp/queue/*.yml"].min
|
15
|
+
return unless @current_path
|
16
|
+
|
17
|
+
result = YAML.load_file(@current_path)
|
18
|
+
File.exist?(result[:path]) ? result : self.next
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.clear
|
22
|
+
`rm -rf /tmp/queue`
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.length
|
26
|
+
Dir["/tmp/queue/*.yml"].length
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.each
|
30
|
+
Dir["/tmp/queue/*.yml"].sort.each do |path|
|
31
|
+
yield YAML.load_file path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.remove(index)
|
36
|
+
path = Dir["/tmp/queue/*.yml"].sort[index.to_i]
|
37
|
+
if path
|
38
|
+
`rm #{path}`
|
39
|
+
yield
|
40
|
+
else
|
41
|
+
puts "Could not find track at position #{index}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.swap(a, b)
|
46
|
+
all = Dir["/tmp/queue/*.yml"].sort
|
47
|
+
path_a = all[a.to_i]
|
48
|
+
path_b = all[b.to_i]
|
49
|
+
|
50
|
+
if path_a && path_b
|
51
|
+
`mv #{path_a} #{path_a}.tmp`
|
52
|
+
`mv #{path_b} #{path_a}`
|
53
|
+
`mv #{path_a}.tmp #{path_b}`
|
54
|
+
yield
|
55
|
+
else
|
56
|
+
puts "Could not find tracks at positions #{a} and #{b}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.add(path)
|
61
|
+
unless %w[.mp3 .m4a].include?(File.extname(path))
|
62
|
+
puts "skipping #{path}"
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
puts "adding #{path}"
|
67
|
+
tags = Vlcraptor::Ffmpeg.new(path)
|
68
|
+
meta = {
|
69
|
+
title: tags.title,
|
70
|
+
artist: tags.artist,
|
71
|
+
album: tags.album,
|
72
|
+
length: tags.time,
|
73
|
+
path: File.expand_path(path),
|
74
|
+
}
|
75
|
+
`mkdir -p /tmp/queue`
|
76
|
+
File.open("/tmp/queue/#{(Time.now.to_f * 1000).to_i}.yml", "w") { |f| f.puts meta.to_yaml }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "digest/md5"
|
5
|
+
require "uri"
|
6
|
+
require "cgi"
|
7
|
+
require "rexml/document"
|
8
|
+
require_relative "settings"
|
9
|
+
|
10
|
+
module Vlcraptor
|
11
|
+
class Scrobbler
|
12
|
+
SCROBBLER_URL = "http://ws.audioscrobbler.com/2.0/"
|
13
|
+
|
14
|
+
SubmissionError = Class.new(RuntimeError)
|
15
|
+
SessionError = Class.new(RuntimeError)
|
16
|
+
|
17
|
+
def self.ask(prompt)
|
18
|
+
puts prompt
|
19
|
+
gets.chomp.strip
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.blank(attribute)
|
23
|
+
(attribute || "").length.zero?
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.load
|
27
|
+
conf = Vlcraptor::Settings.new("~/.lastfm.yml")
|
28
|
+
|
29
|
+
conf[:api_key] = ask("What is the api key? ") if blank(conf[:api_key])
|
30
|
+
conf[:secret] = ask("What is the secret? ") if blank(conf[:secret])
|
31
|
+
conf[:user] = ask("What is your lastfm username? ") if blank(conf[:user])
|
32
|
+
|
33
|
+
return nil if blank(conf[:api_key]) || blank(conf[:secret]) || blank(conf[:user])
|
34
|
+
|
35
|
+
scrobbler = Vlcraptor::Scrobbler.new(conf[:api_key], conf[:secret], conf[:user], conf[:session])
|
36
|
+
|
37
|
+
unless conf[:session]
|
38
|
+
conf[:session] = scrobbler.fetch_session_key do |url|
|
39
|
+
puts "A browser will now launch to allow to authorise this application to access your lastfm account"
|
40
|
+
`open '#{url}'`
|
41
|
+
puts "Press enter when you have authorised the application"
|
42
|
+
gets
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
return nil if blank(conf[:session])
|
47
|
+
|
48
|
+
scrobbler
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(api_key, secret, user, session_key = nil)
|
52
|
+
@api_key = api_key
|
53
|
+
@secret = secret
|
54
|
+
@user = user
|
55
|
+
@session_key = session_key
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :user, :api_key, :secret
|
59
|
+
|
60
|
+
def session_key
|
61
|
+
@session_key or raise SessionError, "The session key must be set or fetched"
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_session_key
|
65
|
+
doc = lfm :get, "auth.gettoken"
|
66
|
+
request_token = doc.root.elements["token"].text
|
67
|
+
yield "http://www.last.fm/api/auth/?api_key=#{api_key}&token=#{request_token}"
|
68
|
+
doc = lfm :get, "auth.getsession", token: request_token
|
69
|
+
status = doc.root.attributes["status"]
|
70
|
+
raise SubmissionError, status unless status == "ok"
|
71
|
+
|
72
|
+
@session_key = doc.root.elements["session"].elements["key"].text
|
73
|
+
end
|
74
|
+
|
75
|
+
def with_profile_url
|
76
|
+
yield "http://www.last.fm/user/#{user}" if user
|
77
|
+
end
|
78
|
+
|
79
|
+
# http://www.last.fm/api/show?service=443
|
80
|
+
def scrobble(artist, title, params = {})
|
81
|
+
lfm_track "track.scrobble", artist, title, params
|
82
|
+
end
|
83
|
+
|
84
|
+
# See http://www.last.fm/api/show?service=454 for more details
|
85
|
+
def now_playing(artist, title, params = {})
|
86
|
+
lfm_track "track.updateNowPlaying", artist, title, params
|
87
|
+
end
|
88
|
+
|
89
|
+
# http://www.last.fm/api/show?service=260
|
90
|
+
def love(artist, title, params = {})
|
91
|
+
lfm_track "track.love", artist, title, params
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def lfm_track(method, artist, title, params)
|
97
|
+
doc = lfm :post, method, params.merge(sk: session_key, artist: artist, track: title)
|
98
|
+
status = doc.root.attributes["status"]
|
99
|
+
raise SubmissionError, status unless status == "ok"
|
100
|
+
end
|
101
|
+
|
102
|
+
def lfm(get_or_post, method, parameters = {})
|
103
|
+
p = signed_parameters parameters.merge api_key: api_key, method: method
|
104
|
+
xml = send get_or_post, SCROBBLER_URL, p
|
105
|
+
REXML::Document.new xml
|
106
|
+
end
|
107
|
+
|
108
|
+
def get(url, parameters)
|
109
|
+
query_string = sort_parameters(parameters)
|
110
|
+
.map { |k, v| "#{k}=#{CGI.escape(v)}" }
|
111
|
+
.join("&")
|
112
|
+
Net::HTTP.get_response(URI.parse("#{url}?#{query_string}")).body
|
113
|
+
end
|
114
|
+
|
115
|
+
def post(url, parameters)
|
116
|
+
Net::HTTP.post_form(URI.parse(url), parameters).body
|
117
|
+
end
|
118
|
+
|
119
|
+
def signed_parameters(parameters)
|
120
|
+
sorted = sort_parameters parameters
|
121
|
+
signature = Digest::MD5.hexdigest(sorted.flatten.join + secret)
|
122
|
+
parameters.merge api_sig: signature
|
123
|
+
end
|
124
|
+
|
125
|
+
def sort_parameters(parameters)
|
126
|
+
parameters.map { |k, v| [k.to_s, v.to_s] }.sort
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Vlcraptor
|
7
|
+
class Settings
|
8
|
+
attr_reader :path, :preferences
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
@path = path.gsub("~", File.expand_path("~"))
|
12
|
+
FileUtils.mkdir_p File.dirname(@path)
|
13
|
+
@preferences = File.exist?(@path) ? YAML.load_file(@path) : {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
preferences[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(key, value)
|
21
|
+
preferences[key] = value
|
22
|
+
persist
|
23
|
+
end
|
24
|
+
|
25
|
+
def persist
|
26
|
+
File.open(path, "w") { |f| f.puts preferences.to_yaml }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/vlcraptor/version.rb
CHANGED
data/lib/vlcraptor.rb
CHANGED
@@ -1,8 +1,141 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "curses"
|
4
|
+
require "rainbow"
|
5
|
+
require_relative "vlcraptor/player"
|
6
|
+
require_relative "vlcraptor/player_controller"
|
7
|
+
require_relative "vlcraptor/preferences"
|
8
|
+
require_relative "vlcraptor/queue"
|
9
|
+
require_relative "vlcraptor/notifiers"
|
10
|
+
require_relative "vlcraptor/scrobbler"
|
4
11
|
|
5
12
|
module Vlcraptor
|
6
|
-
|
7
|
-
|
13
|
+
def self.autoplay(value)
|
14
|
+
Vlcraptor::Preferences.new[:autoplay] = value == "on"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.clear
|
18
|
+
Vlcraptor::Queue.clear
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.crossfade(value)
|
22
|
+
Vlcraptor::Preferences.new[:crossfade] = value == "on"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.history
|
26
|
+
system("cat #{File.expand_path("~")}/.player_history")
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.list
|
30
|
+
started = Vlcraptor::Preferences.new[:started]
|
31
|
+
offset = 0
|
32
|
+
index = 0
|
33
|
+
Vlcraptor::Queue.each do |track|
|
34
|
+
array = [Rainbow(index.to_s).magenta]
|
35
|
+
array << Time.at(started + offset).strftime("%I:%M:%S") if started
|
36
|
+
array += [Rainbow(track[:title]).green, "by", Rainbow(track[:artist]).yellow]
|
37
|
+
array += ["from", Rainbow(track[:album]).cyan] if (track[:album] || "").length.positive?
|
38
|
+
if track[:length]
|
39
|
+
mins = track[:length] / 60
|
40
|
+
secs = track[:length] % 60
|
41
|
+
array << "(#{mins} minutes and #{secs} seconds)"
|
42
|
+
end
|
43
|
+
puts array.join(" ")
|
44
|
+
offset += track[:length]
|
45
|
+
index += 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.pause
|
50
|
+
Vlcraptor::Preferences.new[:pause] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.play
|
54
|
+
Vlcraptor::Preferences.new[:play] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.player
|
58
|
+
Curses.init_screen
|
59
|
+
Curses.start_color
|
60
|
+
Curses.curs_set(0)
|
61
|
+
Curses.noecho
|
62
|
+
|
63
|
+
Curses.init_pair(0, 0, 0) # white
|
64
|
+
Curses.init_pair(2, 2, 0) # green
|
65
|
+
Curses.init_pair(5, 5, 0) # magenta
|
66
|
+
Curses.init_pair(6, 6, 0) # cyan
|
67
|
+
Curses.init_pair(8, 8, 0) # grey
|
68
|
+
Curses.init_pair(9, 9, 0) # orange
|
69
|
+
Curses.init_pair(11, 11, 0) # yellow
|
70
|
+
|
71
|
+
player_controller = Vlcraptor::PlayerController.new
|
72
|
+
window = Curses::Window.new(0, 0, 1, 2)
|
73
|
+
window.nodelay = true
|
74
|
+
|
75
|
+
loop do
|
76
|
+
window.setpos(0, 0)
|
77
|
+
|
78
|
+
player_controller.next.each do |pair|
|
79
|
+
window.attron(Curses.color_pair(pair.first)) { window << pair.last }
|
80
|
+
Curses.clrtoeol
|
81
|
+
window << "\n"
|
82
|
+
end
|
83
|
+
(window.maxy - window.cury).times { window.deleteln }
|
84
|
+
window.refresh
|
85
|
+
|
86
|
+
case window.getch.to_s
|
87
|
+
when " "
|
88
|
+
pause
|
89
|
+
when "n"
|
90
|
+
skip
|
91
|
+
when "p"
|
92
|
+
play
|
93
|
+
when "s"
|
94
|
+
stop
|
95
|
+
when "q"
|
96
|
+
break
|
97
|
+
end
|
98
|
+
|
99
|
+
sleep 0.1
|
100
|
+
end
|
101
|
+
ensure
|
102
|
+
player_controller.cleanup
|
103
|
+
Curses.close_screen
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.remove(index)
|
107
|
+
Vlcraptor::Queue.remove(index) { list }
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.queue(paths)
|
111
|
+
paths.each do |path|
|
112
|
+
if File.file?(path)
|
113
|
+
Vlcraptor::Queue.add(path)
|
114
|
+
else
|
115
|
+
Dir.glob("#{path}/**/*.*").each do |child_path|
|
116
|
+
Vlcraptor::Queue.add(child_path)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.scrobble(value)
|
123
|
+
preferences = Vlcraptor::Preferences.new
|
124
|
+
preferences[:scrobble] = value == "on"
|
125
|
+
scrobbler = Vlcraptor::Scrobbler.load if preferences.scrobble?
|
126
|
+
preferences[:scrobble] = false unless scrobbler
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.skip
|
130
|
+
Vlcraptor::Preferences.new[:skip] = true
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.stop
|
134
|
+
Vlcraptor::Preferences.new[:stop] = true
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.swap(args)
|
138
|
+
a, b = *args
|
139
|
+
Vlcraptor::Queue.swap(a, b) { list }
|
140
|
+
end
|
8
141
|
end
|
data/vlcraptor.gemspec
CHANGED
@@ -27,9 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# For more information and examples about making a new gem, checkout our
|
34
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
30
|
+
spec.add_dependency "curses"
|
31
|
+
spec.add_dependency "rainbow"
|
32
|
+
spec.add_dependency "vlc-client"
|
35
33
|
end
|
metadata
CHANGED
@@ -1,31 +1,84 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vlcraptor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Ryall
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
12
|
-
dependencies:
|
11
|
+
date: 2022-04-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
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: rainbow
|
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
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: vlc-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
13
55
|
description:
|
14
56
|
email:
|
15
57
|
- mark@ryall.name
|
16
|
-
executables:
|
58
|
+
executables:
|
59
|
+
- vlcraptor
|
17
60
|
extensions: []
|
18
61
|
extra_rdoc_files: []
|
19
62
|
files:
|
20
63
|
- ".rspec"
|
21
64
|
- ".rubocop.yml"
|
65
|
+
- ".tool-versions"
|
22
66
|
- Gemfile
|
23
67
|
- Gemfile.lock
|
24
68
|
- README.md
|
25
69
|
- Rakefile
|
26
70
|
- bin/console
|
27
71
|
- bin/setup
|
72
|
+
- exe/vlcraptor
|
28
73
|
- lib/vlcraptor.rb
|
74
|
+
- lib/vlcraptor/ffmpeg.rb
|
75
|
+
- lib/vlcraptor/notifiers.rb
|
76
|
+
- lib/vlcraptor/player.rb
|
77
|
+
- lib/vlcraptor/player_controller.rb
|
78
|
+
- lib/vlcraptor/preferences.rb
|
79
|
+
- lib/vlcraptor/queue.rb
|
80
|
+
- lib/vlcraptor/scrobbler.rb
|
81
|
+
- lib/vlcraptor/settings.rb
|
29
82
|
- lib/vlcraptor/version.rb
|
30
83
|
- vlcraptor.gemspec
|
31
84
|
homepage: https://github.com/markryall/vlcraptor
|
@@ -47,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
100
|
- !ruby/object:Gem::Version
|
48
101
|
version: '0'
|
49
102
|
requirements: []
|
50
|
-
rubygems_version: 3.
|
103
|
+
rubygems_version: 3.3.3
|
51
104
|
signing_key:
|
52
105
|
specification_version: 4
|
53
106
|
summary: Queueing daemon for VLC
|