vlcraptor 0.1.0 → 0.2.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 +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
|