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