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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6f5abe6fe55f27ec9e8d8188af3617d7ee10134190c283d39d48bec9add7390
4
- data.tar.gz: 5a479cec89a39aaaf4347c38affdbe685a4dc0c55b181b353a66b6fe7b515e0c
3
+ metadata.gz: 62d27be7a5541a19d3e2f69cf98ee1506b8b65560deffa547c9e621f860977b9
4
+ data.tar.gz: af56137e7d23b949a5ba94453f9d3bb4e0c45020722b3d0456393b7c7bfed4b5
5
5
  SHA512:
6
- metadata.gz: 76a02e31336c49dd3743e2bc1464f85e11b93eeb32516887fe2998220af99887258a1e1b4ca647a965f6fe13b9399c21cc7113cc8512e6aa7d2f494f45bb2962
7
- data.tar.gz: 177485c96740373d117633de088f609bdf9c87ce43dbe4d9a2fd38ab43bdb19d49d7241002d0f1009b82dc4c96f82a4422046f26434fac0b9c9e60854c5901d9
6
+ metadata.gz: 03d0920671d6dfa40dd866c70a915fe7ac6c13a5d84dd0507cdaa6104d0fc2352b2d4b4ab6af10957b7b3ad078f673a2ceda91552134c0b32172186fcafcb75d
7
+ data.tar.gz: 8519f300d25106c8138cf4921c03f2394191f28f853220203b09079d39d9f972cf40331f6e30a2a9f8549028014a9d1542f6c1b841fef9a1f772fffa1d86c910
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.1.0
data/Gemfile CHANGED
@@ -5,8 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in vlcraptor.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
9
-
10
- gem "rspec", "~> 3.0"
11
-
12
- gem "rubocop", "~> 1.21"
8
+ gem "rake"
9
+ gem "rspec"
10
+ gem "rubocop"
11
+ gem "solargraph"
data/Gemfile.lock CHANGED
@@ -1,19 +1,36 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- vlcraptor (0.1.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 (~> 13.0)
50
- rspec (~> 3.0)
51
- rubocop (~> 1.21)
87
+ rake
88
+ rspec
89
+ rubocop
90
+ solargraph
52
91
  vlcraptor!
53
92
 
54
93
  BUNDLED WITH
55
- 2.2.32
94
+ 2.3.3
data/README.md CHANGED
@@ -1,28 +1,73 @@
1
1
  # Vlcraptor
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/vlcraptor`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ This is a queueing daemon for VLC - kind of like `mpd` but nowhere near as flexible or useful.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The player daemon starts two instances of VLC and then uses those to play any tracks placed in the queue.
6
6
 
7
- ## Installation
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
- Add this line to your application's Gemfile:
10
+ At the moment, only mac os is supported - minor changes would be required to allow this to work on linux.
10
11
 
11
- ```ruby
12
- gem 'vlcraptor'
13
- ```
12
+ ## Installation
14
13
 
15
- And then execute:
14
+ You need to install VLC in the default location for mac os (`/Applications/VLC.app`).
16
15
 
17
- $ bundle install
16
+ `brew install ffmpeg` is required by the `queue` command for extracting tags to place in the queue.
18
17
 
19
- Or install it yourself as:
18
+ `brew install terminal-notifier` is optional depending if you want terminal notifications when new tracks start.
20
19
 
21
- $ gem install vlcraptor
20
+ This gem can be installed with `gem install vlcraptor`.
22
21
 
23
22
  ## Usage
24
23
 
25
- TODO: Write usage instructions here
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/[USERNAME]/vlcraptor.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vlcraptor
4
- VERSION = "0.1.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/vlcraptor.rb CHANGED
@@ -1,8 +1,141 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "vlcraptor/version"
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
- class Error < StandardError; end
7
- # Your code goes here...
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
- # Uncomment to register a new dependency of your gem
31
- # spec.add_dependency "example-gem", "~> 1.0"
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.1.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-03-26 00:00:00.000000000 Z
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.2.32
103
+ rubygems_version: 3.3.3
51
104
  signing_key:
52
105
  specification_version: 4
53
106
  summary: Queueing daemon for VLC