vlcraptor 0.1.0 → 0.2.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 +41 -4
- data/exe/vlcraptor +44 -0
- data/lib/vlcraptor/console.rb +23 -0
- data/lib/vlcraptor/ffmpeg.rb +70 -0
- data/lib/vlcraptor/notifiers.rb +88 -0
- data/lib/vlcraptor/player.rb +95 -0
- data/lib/vlcraptor/preferences.rb +67 -0
- data/lib/vlcraptor/queue.rb +43 -0
- data/lib/vlcraptor/scrobbler.rb +125 -0
- data/lib/vlcraptor/settings.rb +29 -0
- data/lib/vlcraptor/version.rb +1 -1
- data/lib/vlcraptor.rb +142 -3
- data/vlcraptor.gemspec +2 -5
- metadata +44 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6303432e0bfc5db3263ba6121fff94d9652dc0df2b00d427f06febeef2f8f27c
|
4
|
+
data.tar.gz: 2f6af90fb8a4a589f9e58d5bd7dc0bdbdb6fc7520051c4682ea9caa5183c71b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88c1e136354b579b937860753c4874f1af62bdf0bd0ebcf7f49e6cea0be96969c16c17423ddd228e745ac72a78936c08a4bc16cc724a973353d90edbab20cb98
|
7
|
+
data.tar.gz: 0e229b3e1db2d596661593d9a85866e5d234fc705b0d019ec468a8a174bb0ae3b1e6d47a19b46390f688e4583d0aa87cfb8fb4f5177ce0528b3c830ed279512c
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.1.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -2,18 +2,33 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
vlcraptor (0.1.0)
|
5
|
+
rainbow
|
6
|
+
vlc-client
|
5
7
|
|
6
8
|
GEM
|
7
9
|
remote: https://rubygems.org/
|
8
10
|
specs:
|
9
11
|
ast (2.4.2)
|
12
|
+
backport (1.2.0)
|
13
|
+
benchmark (0.2.0)
|
10
14
|
diff-lcs (1.5.0)
|
15
|
+
e2mmap (0.1.0)
|
16
|
+
jaro_winkler (1.5.4)
|
17
|
+
kramdown (2.3.1)
|
18
|
+
rexml
|
19
|
+
kramdown-parser-gfm (1.1.0)
|
20
|
+
kramdown (~> 2.0)
|
21
|
+
nokogiri (1.13.1-x86_64-darwin)
|
22
|
+
racc (~> 1.4)
|
11
23
|
parallel (1.22.1)
|
12
24
|
parser (3.1.1.0)
|
13
25
|
ast (~> 2.4.1)
|
26
|
+
racc (1.6.0)
|
14
27
|
rainbow (3.1.1)
|
15
28
|
rake (13.0.6)
|
16
29
|
regexp_parser (2.2.1)
|
30
|
+
reverse_markdown (2.1.1)
|
31
|
+
nokogiri
|
17
32
|
rexml (3.2.5)
|
18
33
|
rspec (3.11.0)
|
19
34
|
rspec-core (~> 3.11.0)
|
@@ -40,16 +55,38 @@ GEM
|
|
40
55
|
rubocop-ast (1.16.0)
|
41
56
|
parser (>= 3.1.1.0)
|
42
57
|
ruby-progressbar (1.11.0)
|
58
|
+
solargraph (0.44.3)
|
59
|
+
backport (~> 1.2)
|
60
|
+
benchmark
|
61
|
+
bundler (>= 1.17.2)
|
62
|
+
diff-lcs (~> 1.4)
|
63
|
+
e2mmap
|
64
|
+
jaro_winkler (~> 1.5)
|
65
|
+
kramdown (~> 2.3)
|
66
|
+
kramdown-parser-gfm (~> 1.1)
|
67
|
+
parser (~> 3.0)
|
68
|
+
reverse_markdown (>= 1.0.5, < 3)
|
69
|
+
rubocop (>= 0.52)
|
70
|
+
thor (~> 1.0)
|
71
|
+
tilt (~> 2.0)
|
72
|
+
yard (~> 0.9, >= 0.9.24)
|
73
|
+
thor (1.2.1)
|
74
|
+
tilt (2.0.10)
|
43
75
|
unicode-display_width (2.1.0)
|
76
|
+
vlc-client (0.0.7)
|
77
|
+
webrick (1.7.0)
|
78
|
+
yard (0.9.27)
|
79
|
+
webrick (~> 1.7.0)
|
44
80
|
|
45
81
|
PLATFORMS
|
46
82
|
x86_64-darwin-21
|
47
83
|
|
48
84
|
DEPENDENCIES
|
49
|
-
rake
|
50
|
-
rspec
|
51
|
-
rubocop
|
85
|
+
rake
|
86
|
+
rspec
|
87
|
+
rubocop
|
88
|
+
solargraph
|
52
89
|
vlcraptor!
|
53
90
|
|
54
91
|
BUNDLED WITH
|
55
|
-
2.
|
92
|
+
2.3.3
|
data/exe/vlcraptor
ADDED
@@ -0,0 +1,44 @@
|
|
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 "crossfade"
|
15
|
+
Vlcraptor.crossfade(ARGV.shift)
|
16
|
+
when "list"
|
17
|
+
Vlcraptor.list
|
18
|
+
when "pause"
|
19
|
+
Vlcraptor.pause
|
20
|
+
when "play"
|
21
|
+
Vlcraptor.play
|
22
|
+
when "player"
|
23
|
+
Vlcraptor.player
|
24
|
+
when "queue"
|
25
|
+
Vlcraptor.queue(ARGV)
|
26
|
+
when "scrobble"
|
27
|
+
Vlcraptor.scrobble(ARGV.shift)
|
28
|
+
when "skip"
|
29
|
+
Vlcraptor.skip
|
30
|
+
when "stop"
|
31
|
+
Vlcraptor.stop
|
32
|
+
else
|
33
|
+
puts "Unknown command \"#{command}\":"
|
34
|
+
puts " autoplay on/off: continue playing tracks or stop at the end of current track"
|
35
|
+
puts " crossfade on/off: 5 second crossfade when changing tracks"
|
36
|
+
puts " list: list current queue"
|
37
|
+
puts " pause: pause current track (resume with play)"
|
38
|
+
puts " play: resume after pause/stop"
|
39
|
+
puts " player: start the player"
|
40
|
+
puts " queue paths: queue folders or files containing music tracks"
|
41
|
+
puts " scrobble on/off: send track information to last.fm (requires an api key)"
|
42
|
+
puts " skip: skip the current track"
|
43
|
+
puts " stop: stop the player (resume with play)"
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vlcraptor
|
4
|
+
class Console
|
5
|
+
LENGTH = 120
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@first = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def change(line)
|
12
|
+
print "\b" * LENGTH unless @first
|
13
|
+
@first = false
|
14
|
+
print line[0...LENGTH].ljust LENGTH
|
15
|
+
end
|
16
|
+
|
17
|
+
def replace(line)
|
18
|
+
print "\b" * LENGTH unless @first
|
19
|
+
@first = true
|
20
|
+
puts line
|
21
|
+
end
|
22
|
+
end
|
23
|
+
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,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rainbow"
|
4
|
+
require_relative "console"
|
5
|
+
require_relative "scrobbler"
|
6
|
+
|
7
|
+
module Vlcraptor
|
8
|
+
class Notifiers
|
9
|
+
def initialize(preferences)
|
10
|
+
@preferences = preferences
|
11
|
+
@console = Vlcraptor::Console.new
|
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_progress(track, remaining)
|
26
|
+
return unless track
|
27
|
+
|
28
|
+
rem = if remaining > 60
|
29
|
+
"#{remaining / 60} minutes and #{remaining % 60} seconds remaining"
|
30
|
+
else
|
31
|
+
"#{remaining} seconds remaining"
|
32
|
+
end
|
33
|
+
@console.change(
|
34
|
+
[
|
35
|
+
" ",
|
36
|
+
display_time(Time.now + remaining),
|
37
|
+
remaining < 20 ? Rainbow(rem).tomato : rem,
|
38
|
+
].join(" ")
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def track_started(track)
|
43
|
+
return unless track
|
44
|
+
|
45
|
+
@preferences[:started] = Time.now.to_i
|
46
|
+
track[:start_time] = Time.now
|
47
|
+
scrobbler&.now_playing(track[:artist], track[:title])
|
48
|
+
terminal_notify(
|
49
|
+
message: "#{track[:title]} by #{track[:artist]}",
|
50
|
+
title: "Now Playing",
|
51
|
+
)
|
52
|
+
@console.replace(
|
53
|
+
[
|
54
|
+
display_time(Time.now),
|
55
|
+
display_time(Time.now + track[:length]),
|
56
|
+
Rainbow(track[:title]).green,
|
57
|
+
"by",
|
58
|
+
Rainbow(track[:artist]).yellow,
|
59
|
+
"from",
|
60
|
+
Rainbow(track[:album]).cyan,
|
61
|
+
"(#{track[:length] / 60}:#{track[:length] % 60})",
|
62
|
+
].join(" ")
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def track_finished(track)
|
67
|
+
return unless track
|
68
|
+
|
69
|
+
scrobbler&.scrobble(track[:artist], track[:title], timestamp: track[:start_time].to_i)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def display_time(time)
|
75
|
+
time.strftime("%I:%M:%S")
|
76
|
+
end
|
77
|
+
|
78
|
+
def terminal_notify(message:, title:)
|
79
|
+
return if `which terminal-notifier`.empty?
|
80
|
+
|
81
|
+
`terminal-notifier -group vlc -message "#{message}" -title "#{title}"`
|
82
|
+
end
|
83
|
+
|
84
|
+
def scrobbler
|
85
|
+
Vlcraptor::Scrobbler.load if @preferences.scrobble?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
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,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,43 @@
|
|
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
|
+
YAML.load_file(@current_path) if @current_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.each
|
19
|
+
Dir["/tmp/queue/*.yml"].sort.each do |path|
|
20
|
+
yield YAML.load_file path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.add(path)
|
25
|
+
unless %w[.mp3 .m4a].include?(File.extname(path))
|
26
|
+
puts "skipping #{path}"
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
puts "adding #{path}"
|
31
|
+
tags = Vlcraptor::Ffmpeg.new(path)
|
32
|
+
meta = {
|
33
|
+
title: tags.title,
|
34
|
+
artist: tags.artist,
|
35
|
+
album: tags.album,
|
36
|
+
length: tags.time,
|
37
|
+
path: File.expand_path(path),
|
38
|
+
}
|
39
|
+
`mkdir -p /tmp/queue`
|
40
|
+
File.open("/tmp/queue/#{(Time.now.to_f * 1000).to_i}.yml", "w") { |f| f.puts meta.to_yaml }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,125 @@
|
|
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
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.load
|
23
|
+
conf = Vlcraptor::Settings.new("~/lastfm.yaml")
|
24
|
+
|
25
|
+
unless conf["api_key"] && conf["secret"]
|
26
|
+
puts "You will need an api key and secret for last fm integration"
|
27
|
+
conf["api_key"] = ask("What is the api key? ")
|
28
|
+
conf["secret"] = ask("What is the secret ")
|
29
|
+
end
|
30
|
+
|
31
|
+
conf["user"] = ask("What is your lastfm username? ") unless conf["user"]
|
32
|
+
|
33
|
+
scrobbler = Vlcraptor::Scrobbler.new(conf["api_key"], conf["secret"], conf["user"], conf["session"])
|
34
|
+
|
35
|
+
unless conf["session"]
|
36
|
+
conf["session"] = scrobbler.fetch_session_key do |url|
|
37
|
+
puts "A browser will now launch to allow to authorise this application to access your lastfm account"
|
38
|
+
`open '#{url}'`
|
39
|
+
puts "Press enter when you have authorised the application"
|
40
|
+
gets
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
scrobbler
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(api_key, secret, user, session_key = nil)
|
48
|
+
@api_key = api_key
|
49
|
+
@secret = secret
|
50
|
+
@user = user
|
51
|
+
@session_key = session_key
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :user, :api_key, :secret
|
55
|
+
|
56
|
+
def session_key
|
57
|
+
@session_key or raise SessionError, "The session key must be set or fetched"
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_session_key
|
61
|
+
doc = lfm :get, "auth.gettoken"
|
62
|
+
request_token = doc.root.elements["token"].text
|
63
|
+
yield "http://www.last.fm/api/auth/?api_key=#{api_key}&token=#{request_token}"
|
64
|
+
doc = lfm :get, "auth.getsession", token: request_token
|
65
|
+
status = doc.root.attributes["status"]
|
66
|
+
raise SubmissionError, status unless status == "ok"
|
67
|
+
|
68
|
+
@session_key = doc.root.elements["session"].elements["key"].text
|
69
|
+
end
|
70
|
+
|
71
|
+
def with_profile_url
|
72
|
+
yield "http://www.last.fm/user/#{user}" if user
|
73
|
+
end
|
74
|
+
|
75
|
+
# http://www.last.fm/api/show?service=443
|
76
|
+
def scrobble(artist, title, params = {})
|
77
|
+
lfm_track "track.scrobble", artist, title, params
|
78
|
+
end
|
79
|
+
|
80
|
+
# See http://www.last.fm/api/show?service=454 for more details
|
81
|
+
def now_playing(artist, title, params = {})
|
82
|
+
lfm_track "track.updateNowPlaying", artist, title, params
|
83
|
+
end
|
84
|
+
|
85
|
+
# http://www.last.fm/api/show?service=260
|
86
|
+
def love(artist, title, params = {})
|
87
|
+
lfm_track "track.love", artist, title, params
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def lfm_track(method, artist, title, params)
|
93
|
+
doc = lfm :post, method, params.merge(sk: session_key, artist: artist, track: title)
|
94
|
+
status = doc.root.attributes["status"]
|
95
|
+
raise SubmissionError, status unless status == "ok"
|
96
|
+
end
|
97
|
+
|
98
|
+
def lfm(get_or_post, method, parameters = {})
|
99
|
+
p = signed_parameters parameters.merge api_key: api_key, method: method
|
100
|
+
xml = send get_or_post, SCROBBLER_URL, p
|
101
|
+
REXML::Document.new xml
|
102
|
+
end
|
103
|
+
|
104
|
+
def get(url, parameters)
|
105
|
+
query_string = sort_parameters(parameters)
|
106
|
+
.map { |k, v| "#{k}=#{CGI.escape(v)}" }
|
107
|
+
.join("&")
|
108
|
+
Net::HTTP.get_response(URI.parse("#{url}?#{query_string}")).body
|
109
|
+
end
|
110
|
+
|
111
|
+
def post(url, parameters)
|
112
|
+
Net::HTTP.post_form(URI.parse(url), parameters).body
|
113
|
+
end
|
114
|
+
|
115
|
+
def signed_parameters(parameters)
|
116
|
+
sorted = sort_parameters parameters
|
117
|
+
signature = Digest::MD5.hexdigest(sorted.flatten.join + secret)
|
118
|
+
parameters.merge api_sig: signature
|
119
|
+
end
|
120
|
+
|
121
|
+
def sort_parameters(parameters)
|
122
|
+
parameters.map { |k, v| [k.to_s, v.to_s] }.sort
|
123
|
+
end
|
124
|
+
end
|
125
|
+
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,147 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "rainbow"
|
4
|
+
require_relative "vlcraptor/player"
|
5
|
+
require_relative "vlcraptor/preferences"
|
6
|
+
require_relative "vlcraptor/queue"
|
7
|
+
require_relative "vlcraptor/notifiers"
|
4
8
|
|
5
9
|
module Vlcraptor
|
6
|
-
|
7
|
-
|
10
|
+
def self.autoplay(value)
|
11
|
+
Vlcraptor::Preferences.new[:autoplay] = value == "on"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.crossfade(value)
|
15
|
+
Vlcraptor::Preferences.new[:crossfade] = value == "on"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.list
|
19
|
+
started = Vlcraptor::Preferences.new[:started]
|
20
|
+
offset = 0
|
21
|
+
Vlcraptor::Queue.each do |track|
|
22
|
+
array = []
|
23
|
+
array << Time.at(started + offset).strftime("%I:%M:%S") if started
|
24
|
+
array += [Rainbow(track[:title]).green, "by", Rainbow(track[:artist]).yellow]
|
25
|
+
array += ["from", Rainbow(track[:album]).cyan] if (track[:album] || "").length.positive?
|
26
|
+
if track[:length]
|
27
|
+
mins = track[:length] / 60
|
28
|
+
secs = track[:length] % 60
|
29
|
+
array << "(#{mins} minutes and #{secs} seconds)"
|
30
|
+
end
|
31
|
+
puts array.join(" ")
|
32
|
+
offset += track[:length]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.pause
|
37
|
+
Vlcraptor::Preferences.new[:pause] = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.play
|
41
|
+
Vlcraptor::Preferences.new[:play] = true
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.player
|
45
|
+
player = Vlcraptor::Player.new
|
46
|
+
queue = Vlcraptor::Queue.new
|
47
|
+
preferences = Vlcraptor::Preferences.new
|
48
|
+
notifiers = Vlcraptor::Notifiers.new(preferences)
|
49
|
+
track = nil
|
50
|
+
suspended = false
|
51
|
+
|
52
|
+
loop do
|
53
|
+
sleep 0.2
|
54
|
+
|
55
|
+
if preferences.pause?
|
56
|
+
player.fadeout
|
57
|
+
player.pause
|
58
|
+
suspended = true
|
59
|
+
notifiers.track_suspended
|
60
|
+
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
if preferences.stop?
|
65
|
+
player.fadeout
|
66
|
+
player.stop
|
67
|
+
suspended = true
|
68
|
+
notifiers.track_suspended
|
69
|
+
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
if preferences.play?
|
74
|
+
player.fadein
|
75
|
+
suspended = false
|
76
|
+
notifiers.track_resumed(track, player.time)
|
77
|
+
|
78
|
+
next
|
79
|
+
end
|
80
|
+
|
81
|
+
next if suspended
|
82
|
+
|
83
|
+
if player.playing?
|
84
|
+
if preferences.skip?
|
85
|
+
track = queue.next
|
86
|
+
if track
|
87
|
+
notifiers.track_started(track)
|
88
|
+
player.crossfade(track[:path])
|
89
|
+
else
|
90
|
+
player.fadeout
|
91
|
+
end
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
if preferences.crossfade? && player.remaining < 5
|
96
|
+
notifiers.track_finished(track)
|
97
|
+
track = queue.next
|
98
|
+
if track
|
99
|
+
notifiers.track_started(track)
|
100
|
+
player.crossfade(track[:path])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
notifiers.track_progress(track, player.remaining)
|
105
|
+
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
next unless preferences.continue?
|
110
|
+
|
111
|
+
notifiers.track_finished(track)
|
112
|
+
track = queue.next
|
113
|
+
next unless track
|
114
|
+
|
115
|
+
notifiers.track_started(track)
|
116
|
+
player.play(track[:path])
|
117
|
+
end
|
118
|
+
rescue Interrupt
|
119
|
+
notifiers.track_suspended
|
120
|
+
player.cleanup
|
121
|
+
puts "Exiting"
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.queue(paths)
|
125
|
+
paths.each do |path|
|
126
|
+
if File.file?(path)
|
127
|
+
Vlcraptor::Queue.add(path)
|
128
|
+
else
|
129
|
+
Dir.glob("#{path}/**/*.*").each do |child_path|
|
130
|
+
Vlcraptor::Queue.add(child_path)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.scrobble(value)
|
137
|
+
Vlcraptor::Preferences.new[:scrobble] = value == "on"
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.skip
|
141
|
+
Vlcraptor::Preferences.new[:skip] = true
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.stop
|
145
|
+
Vlcraptor::Preferences.new[:stop] = true
|
146
|
+
end
|
8
147
|
end
|
data/vlcraptor.gemspec
CHANGED
@@ -27,9 +27,6 @@ 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 "rainbow"
|
31
|
+
spec.add_dependency "vlc-client"
|
35
32
|
end
|
metadata
CHANGED
@@ -1,31 +1,70 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vlcraptor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.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-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rainbow
|
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: vlc-client
|
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'
|
13
41
|
description:
|
14
42
|
email:
|
15
43
|
- mark@ryall.name
|
16
|
-
executables:
|
44
|
+
executables:
|
45
|
+
- vlcraptor
|
17
46
|
extensions: []
|
18
47
|
extra_rdoc_files: []
|
19
48
|
files:
|
20
49
|
- ".rspec"
|
21
50
|
- ".rubocop.yml"
|
51
|
+
- ".tool-versions"
|
22
52
|
- Gemfile
|
23
53
|
- Gemfile.lock
|
24
54
|
- README.md
|
25
55
|
- Rakefile
|
26
56
|
- bin/console
|
27
57
|
- bin/setup
|
58
|
+
- exe/vlcraptor
|
28
59
|
- lib/vlcraptor.rb
|
60
|
+
- lib/vlcraptor/console.rb
|
61
|
+
- lib/vlcraptor/ffmpeg.rb
|
62
|
+
- lib/vlcraptor/notifiers.rb
|
63
|
+
- lib/vlcraptor/player.rb
|
64
|
+
- lib/vlcraptor/preferences.rb
|
65
|
+
- lib/vlcraptor/queue.rb
|
66
|
+
- lib/vlcraptor/scrobbler.rb
|
67
|
+
- lib/vlcraptor/settings.rb
|
29
68
|
- lib/vlcraptor/version.rb
|
30
69
|
- vlcraptor.gemspec
|
31
70
|
homepage: https://github.com/markryall/vlcraptor
|
@@ -47,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
86
|
- !ruby/object:Gem::Version
|
48
87
|
version: '0'
|
49
88
|
requirements: []
|
50
|
-
rubygems_version: 3.
|
89
|
+
rubygems_version: 3.3.3
|
51
90
|
signing_key:
|
52
91
|
specification_version: 4
|
53
92
|
summary: Queueing daemon for VLC
|