caldera 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.md +21 -0
- data/README.md +44 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/caldera.gemspec +40 -0
- data/lib/caldera.rb +4 -0
- data/lib/caldera/client.rb +97 -0
- data/lib/caldera/events.rb +100 -0
- data/lib/caldera/model.rb +5 -0
- data/lib/caldera/model/load_tracks.rb +33 -0
- data/lib/caldera/model/playlist_info.rb +19 -0
- data/lib/caldera/model/track.rb +96 -0
- data/lib/caldera/node.rb +121 -0
- data/lib/caldera/player.rb +171 -0
- data/lib/caldera/version.rb +5 -0
- data/lib/caldera/websocket.rb +134 -0
- metadata +179 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dddc27f9f168e2f9269f622c5e1e955f7f184847f4783b9f46b471893720d373
|
4
|
+
data.tar.gz: 187ec6f7659d4539b8fe54bfe2a53375f321152f6c85dccdc1a71209841508b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 756487015dd81817229f34e5b849d5e77e9d5ede6916f40f1ac56b560e945ce5b4d112f4873b025bdbd9168d6d235bf4c4ee436d71a43366c6903afb8ca822c9
|
7
|
+
data.tar.gz: 2b93ac69880a1f26c353fb18b34bd07ee7a91e1468752d1c49c76ad9026ba7782d99845d2fd5f7d5a28f152c0413fd1c2ff987d42c60c743ad8bb12d1d45bd4b
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at TODO: Write your email address. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Matthew Carey
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Caldera
|
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/caldera`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'caldera'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install caldera
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/swarley/caldera. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/swarley/caldera/blob/main/CODE_OF_CONDUCT.md).
|
36
|
+
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
41
|
+
|
42
|
+
## Code of Conduct
|
43
|
+
|
44
|
+
Everyone interacting in the Caldera project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/swarley/caldera/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "caldera"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/caldera.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/caldera/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'caldera'
|
7
|
+
spec.version = Caldera::VERSION
|
8
|
+
spec.authors = ['Matthew Carey']
|
9
|
+
spec.email = ['matthew.b.carey@gmailcom']
|
10
|
+
|
11
|
+
spec.summary = 'Lavalink client'
|
12
|
+
spec.description = 'Lavalink client for use with Discord libraries.'
|
13
|
+
spec.homepage = 'https://github.com/swarley/caldera'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/swarley/caldera/blob/main/CHANGELOG.md'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
|
27
|
+
spec.bindir = 'exe'
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
32
|
+
spec.add_development_dependency 'rubocop', '~> 0.93.1'
|
33
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.8'
|
34
|
+
spec.add_development_dependency 'yard', '~> 0.9.25'
|
35
|
+
|
36
|
+
spec.add_dependency 'event_emitter', '~> 0.2.6'
|
37
|
+
spec.add_dependency 'logging', '~> 2.3'
|
38
|
+
spec.add_dependency 'permessage_deflate', '~> 0.1.4'
|
39
|
+
spec.add_dependency 'websocket-driver', '~> 0.7.3'
|
40
|
+
end
|
data/lib/caldera.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'caldera/node'
|
4
|
+
require 'logging'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Caldera
|
8
|
+
class Client
|
9
|
+
LOGGER = Logging.logger[self]
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :num_shards
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :user_id
|
16
|
+
|
17
|
+
# @return [Array<Player>]
|
18
|
+
attr_reader :players
|
19
|
+
|
20
|
+
# @return [Array<Node>]
|
21
|
+
attr_reader :nodes
|
22
|
+
|
23
|
+
# @param [Integer, String] num_shards
|
24
|
+
# @param [Integer, String] user_id
|
25
|
+
# @param [Proc<String, String>] connect
|
26
|
+
def initialize(num_shards:, user_id:, connect: nil)
|
27
|
+
@num_shards = num_shards.to_s
|
28
|
+
@user_id = user_id.to_s
|
29
|
+
@players = {}
|
30
|
+
@connect_proc = connect
|
31
|
+
@nodes = []
|
32
|
+
@voice_state_mutex = Mutex.new
|
33
|
+
@voice_states = Hash.new { |h, guild_id| h[guild_id] = {} }
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [Integer, String] guild_id
|
37
|
+
# @param [Integer, String] channel_id
|
38
|
+
# @param [Number] timeout
|
39
|
+
# @return [Caldera::Player]
|
40
|
+
def connect(guild_id, channel_id, timeout: nil)
|
41
|
+
return Timeout.timeout(timeout) { connect(guild_id, channel_id) } if timeout
|
42
|
+
|
43
|
+
gid = guild_id.to_s
|
44
|
+
|
45
|
+
return @players[gid] if @players[gid]
|
46
|
+
|
47
|
+
@connect_proc.call(gid, channel_id.to_s)
|
48
|
+
sleep 0.05 until @players[gid]
|
49
|
+
|
50
|
+
@players[gid]
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_voice_state(guild_id, session_id: nil, event: nil)
|
54
|
+
guild_id = guild_id.to_s
|
55
|
+
@voice_state_mutex.synchronize do
|
56
|
+
@voice_states[guild_id][:session_id] = session_id if session_id
|
57
|
+
@voice_states[guild_id][:event] = event if event
|
58
|
+
end
|
59
|
+
|
60
|
+
state = @voice_states[guild_id]
|
61
|
+
|
62
|
+
if state[:session_id] && state[:event]
|
63
|
+
LOGGER.info { "Creating player for #{guild_id}" }
|
64
|
+
best_node.create_player(guild_id, state[:session_id], state[:event])
|
65
|
+
@voice_states.delete(guild_id)
|
66
|
+
else
|
67
|
+
LOGGER.debug { "Recieved partial info for creating player for #{guild_id}: #{state}" }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_node(authorization:, uri: nil, rest_uri: nil, ws_uri: nil)
|
72
|
+
uri = URI(uri) unless uri.is_a?(URI::Generic)
|
73
|
+
rest_uri ||= uri.clone.tap { |u| u.scheme = 'http' }
|
74
|
+
ws_uri ||= uri.clone.tap { |u| u.scheme = 'ws' }
|
75
|
+
|
76
|
+
new_node = Node.new(rest_uri, ws_uri, authorization, self)
|
77
|
+
new_node.start
|
78
|
+
|
79
|
+
@nodes << new_node
|
80
|
+
end
|
81
|
+
|
82
|
+
def remove_node(node)
|
83
|
+
@nodes.delete(node)
|
84
|
+
node.stop
|
85
|
+
end
|
86
|
+
|
87
|
+
def best_node
|
88
|
+
@nodes.min { |n| n.stats['cpu']['systemLoad'] }
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_player(guild_id)
|
92
|
+
@players[guild_id.to_s]
|
93
|
+
end
|
94
|
+
|
95
|
+
# TODO: load balacing stuff
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'caldera/model'
|
4
|
+
|
5
|
+
module Caldera
|
6
|
+
module Events
|
7
|
+
class TrackStart
|
8
|
+
attr_reader :track, :player
|
9
|
+
|
10
|
+
def initialize(data, player)
|
11
|
+
@player = player
|
12
|
+
@track = Caldera::Model::Track.from_b64(data['track'])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class TrackEnd
|
17
|
+
attr_reader :reason, :guild_id, :track, :player
|
18
|
+
|
19
|
+
def initialize(data, player)
|
20
|
+
@player = player
|
21
|
+
@reason = data['reason'].to_sym
|
22
|
+
@guild_id = data['guildId']
|
23
|
+
@track = Caldera::Model::Track.from_b64(data['track'])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class TrackException
|
28
|
+
attr_reader :player, :error, :track
|
29
|
+
|
30
|
+
def initialize(data, player)
|
31
|
+
@player = player
|
32
|
+
@error = data['error']
|
33
|
+
@track = Caldera::Model::Track.from_b64(data['track'])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class TrackStuck
|
38
|
+
attr_reader :player, :threshold, :track
|
39
|
+
|
40
|
+
def initialize(data, player)
|
41
|
+
@player = player
|
42
|
+
@threshold = data['thresholdMs']
|
43
|
+
@track = Caldera::Model::Track.from_b64(data['track'])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class WebSocketClosed
|
48
|
+
# @returns [String]
|
49
|
+
attr_reader :guild_id
|
50
|
+
|
51
|
+
# @returns [Integer]
|
52
|
+
attr_reader :code
|
53
|
+
|
54
|
+
# @returns [String]
|
55
|
+
attr_reader :reason
|
56
|
+
|
57
|
+
# @returns [true, false]
|
58
|
+
attr_reader :by_remote
|
59
|
+
alias by_remote? by_remote
|
60
|
+
|
61
|
+
def initialize(data, _player)
|
62
|
+
@guild_id = data['guildId']
|
63
|
+
@code = data['code']
|
64
|
+
@reason = data['reason']
|
65
|
+
@by_remote = data['byRemote']
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class StatsEvent
|
70
|
+
Memory = Struct.new('Memory', :reservable, :used, :free, :allocated, keyword_init: true)
|
71
|
+
Cpu = Struct.new('Cpu', :cores, :system_load, :lavalink_load, :uptime, keyword_init: true)
|
72
|
+
|
73
|
+
attr_reader :playing_players, :memory, :cpu, :uptime
|
74
|
+
|
75
|
+
def initialize(data, _node)
|
76
|
+
@playing_players = data['playingPlayers']
|
77
|
+
@memory = Memory.new(**data['memory'])
|
78
|
+
|
79
|
+
cpu_data = data['cpu']
|
80
|
+
snake_case_data = {
|
81
|
+
cores: cpu_data['cores'],
|
82
|
+
system_load: cpu_data['systemLoad'],
|
83
|
+
lavalink_load: cpu_data['lavalinkLoad']
|
84
|
+
}
|
85
|
+
@cpu = Cpu.new(**snake_case_data)
|
86
|
+
@uptime = data['uptime']
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class PlayerUpdateEvent
|
91
|
+
attr_reader :player, :time, :position
|
92
|
+
|
93
|
+
def initialize(data, player)
|
94
|
+
@player = player
|
95
|
+
@time = Time.at(data['time'])
|
96
|
+
@position = data['position']
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caldera
|
4
|
+
module Model
|
5
|
+
# Represents the struct returned from {Client#load_tracks}
|
6
|
+
class LoadTracks
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @return [PlaylistInfo]
|
10
|
+
attr_reader :playlist_info
|
11
|
+
|
12
|
+
# @return [Array<Track>]
|
13
|
+
attr_reader :tracks
|
14
|
+
|
15
|
+
# @return [:TRACK_LOADED, :PLAYLIST_LOADED, :SEARCH_RESULT, :NO_MATCHES, :LOAD_FAILED]
|
16
|
+
attr_reader :load_type
|
17
|
+
|
18
|
+
def initialize(data)
|
19
|
+
playlist_info = data['playlistInfo']
|
20
|
+
|
21
|
+
@playlist_info = PlaylistInfo.new(playlist_info) if playlist_info
|
22
|
+
@tracks = data['tracks'].collect { |track_data| Model::Track.new(track_data) }
|
23
|
+
@load_type = data['loadType'].to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
# Operate on each track.
|
27
|
+
# @yieldparam [Track]
|
28
|
+
def each(&block)
|
29
|
+
@tracks.each(&block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Caldera
|
4
|
+
module Model
|
5
|
+
# Information about a playlist loaded through {Client#load_tracks}
|
6
|
+
class PlaylistInfo
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# @return [Integer]
|
11
|
+
attr_reader :selected_track
|
12
|
+
|
13
|
+
def initialize(data)
|
14
|
+
@selected_track = data['selectedTrack']
|
15
|
+
@name = data['name']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Caldera
|
6
|
+
module Model
|
7
|
+
class Track
|
8
|
+
# @param [String] Base64 representation of the track
|
9
|
+
attr_reader :track_data
|
10
|
+
|
11
|
+
# @param [String] The track identifier
|
12
|
+
attr_reader :identifier
|
13
|
+
|
14
|
+
# @param [true, false]
|
15
|
+
attr_reader :seekable
|
16
|
+
|
17
|
+
# @param [String] The track author.
|
18
|
+
attr_reader :author
|
19
|
+
|
20
|
+
# @param [Integer] Length in milliseconds.
|
21
|
+
attr_reader :length
|
22
|
+
|
23
|
+
# @param [true, false]
|
24
|
+
attr_reader :stream
|
25
|
+
|
26
|
+
# @param [Integer] The current position in milliseconds.
|
27
|
+
attr_reader :position
|
28
|
+
|
29
|
+
# @param [String] The track title.
|
30
|
+
attr_reader :title
|
31
|
+
|
32
|
+
# @param [String] The URI to the track source.
|
33
|
+
attr_reader :uri
|
34
|
+
|
35
|
+
def initialize(data)
|
36
|
+
# track_data could maybe use a better name. It's
|
37
|
+
# a base64 representation of a binary data representation
|
38
|
+
# of a track=
|
39
|
+
@track_data = data['track']
|
40
|
+
|
41
|
+
info = data['info']
|
42
|
+
@identifier = info['identifier']
|
43
|
+
@seekable = info['isSeekable']
|
44
|
+
@author = info['author']
|
45
|
+
@length = info['length']
|
46
|
+
@stream = info['isStream']
|
47
|
+
@position = info['position']
|
48
|
+
@title = info['title']
|
49
|
+
@uri = info['uri']
|
50
|
+
@source = info['source']
|
51
|
+
end
|
52
|
+
|
53
|
+
# Decode a track from base64 track data.
|
54
|
+
# @param [String] b64_data Base64 encoded track data, recieved from the Lavalink server.
|
55
|
+
def self.from_b64(b64_data)
|
56
|
+
data = Base64.decode64(b64_data)
|
57
|
+
flags, version = data.unpack('NC')
|
58
|
+
|
59
|
+
raise 'Unsupported track data' if (flags >> 30) != 1
|
60
|
+
|
61
|
+
# This is gross but it's easier than not doing it
|
62
|
+
case version
|
63
|
+
when 1
|
64
|
+
title, author, length, identifier, is_stream, source = data.unpack('@7Z*xZ*Q>xZ*CxZ*')
|
65
|
+
Track.new(
|
66
|
+
'track' => b64_data,
|
67
|
+
'info' => {
|
68
|
+
'title' => title,
|
69
|
+
'author' => author,
|
70
|
+
'length' => length,
|
71
|
+
'identifier' => identifier,
|
72
|
+
'isStream' => is_stream == 1,
|
73
|
+
'source' => source
|
74
|
+
}
|
75
|
+
)
|
76
|
+
when 2
|
77
|
+
title, author, length, identifier, is_stream, uri, source = data.unpack('@7Z*xZ*Q>xZ*CxxZ*xZ*xZ*')
|
78
|
+
Track.new(
|
79
|
+
'track' => b64_data,
|
80
|
+
'info' => {
|
81
|
+
'title' => title,
|
82
|
+
'author' => author,
|
83
|
+
'length' => length,
|
84
|
+
'identifier' => identifier,
|
85
|
+
'isStream' => is_stream == 1,
|
86
|
+
'source' => source,
|
87
|
+
'uri' => uri
|
88
|
+
}
|
89
|
+
)
|
90
|
+
else
|
91
|
+
raise 'Unsupported track version'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/caldera/node.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'caldera/model'
|
4
|
+
require 'caldera/player'
|
5
|
+
require 'caldera/websocket'
|
6
|
+
require 'event_emitter'
|
7
|
+
require 'net/http'
|
8
|
+
|
9
|
+
module Caldera
|
10
|
+
class Node
|
11
|
+
LOGGER = Logging.logger[self]
|
12
|
+
|
13
|
+
include EventEmitter
|
14
|
+
|
15
|
+
# @return [Caldera::Client]
|
16
|
+
attr_reader :client
|
17
|
+
|
18
|
+
# @return [Caldera::WebSocket]
|
19
|
+
attr_reader :websocket
|
20
|
+
|
21
|
+
# @return [Net::HTTP]
|
22
|
+
attr_reader :http
|
23
|
+
|
24
|
+
# @return [Caldera::Model::Stats]
|
25
|
+
attr_reader :stats
|
26
|
+
|
27
|
+
def initialize(rest_uri, ws_uri, auth, client)
|
28
|
+
@client = client
|
29
|
+
@authorization = auth
|
30
|
+
@websocket = WebSocket.new(ws_uri, auth, @client.num_shards, @client.user_id)
|
31
|
+
register_handlers
|
32
|
+
@stats = nil
|
33
|
+
@available = false
|
34
|
+
@http = Net::HTTP.new(rest_uri.host, rest_uri.port)
|
35
|
+
end
|
36
|
+
|
37
|
+
def start
|
38
|
+
LOGGER.info { "Connecting to #{@websocket.url}" }
|
39
|
+
@websocket.start
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop
|
43
|
+
LOGGER.info { "Disconnecting from #{@websocket.url}" }
|
44
|
+
@websocket.close
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_player(guild_id, session_id, event)
|
48
|
+
LOGGER.info { "Creating player for #{guild_id}" }
|
49
|
+
@websocket.send_json({
|
50
|
+
op: :voiceUpdate,
|
51
|
+
guildId: guild_id,
|
52
|
+
sessionId: session_id,
|
53
|
+
event: event
|
54
|
+
})
|
55
|
+
|
56
|
+
player = Player.new(guild_id, self, client)
|
57
|
+
@client.players[guild_id] = player
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_tracks(id)
|
61
|
+
resp = get('/loadtracks', query: { identifier: id })
|
62
|
+
Model::LoadTracks.new(resp)
|
63
|
+
end
|
64
|
+
|
65
|
+
def youtube_search(search_string)
|
66
|
+
load_tracks("ytsearch:#{search_string}")
|
67
|
+
end
|
68
|
+
|
69
|
+
def soundcloud_search(search_string)
|
70
|
+
load_tracks("scsearch:#{search_string}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def send_json(data)
|
74
|
+
@websocket.send_json(data)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def register_handlers
|
80
|
+
@websocket.on(:playerUpdate, &method(:handle_player_update))
|
81
|
+
@websocket.on(:stats, &method(:handle_stats))
|
82
|
+
@websocket.on(:event, &method(:handle_event))
|
83
|
+
@websocket.on(:open) { @available = true }
|
84
|
+
@websocket.on(:close) { @available = false }
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_player_update(data)
|
88
|
+
@client.get_player(data['guildId']).state = data['state']
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_stats(data)
|
92
|
+
data_without_op = data.clone
|
93
|
+
data_without_op.delete('op')
|
94
|
+
@stats = data_without_op
|
95
|
+
|
96
|
+
emit(:stats_update, Events::StatsEvent.new(data, self))
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_event(data)
|
100
|
+
type = transform_type(data['type'])
|
101
|
+
emit(type, data)
|
102
|
+
end
|
103
|
+
|
104
|
+
def transform_type(type)
|
105
|
+
(type[0] + type[1..-1].sub(/Event$/, '').gsub(/([A-Z])/, '_\1')).downcase
|
106
|
+
end
|
107
|
+
|
108
|
+
def get(path, query: {})
|
109
|
+
req_path = "#{path}?#{URI.encode_www_form(query)}"
|
110
|
+
resp = @http.get(req_path, { Authorization: @authorization })
|
111
|
+
|
112
|
+
case resp.code.to_i
|
113
|
+
when 200...300
|
114
|
+
JSON.load(resp.body)
|
115
|
+
else
|
116
|
+
# TODO
|
117
|
+
resp.error!
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'caldera/model'
|
4
|
+
require 'caldera/events'
|
5
|
+
require 'event_emitter'
|
6
|
+
require 'logging'
|
7
|
+
|
8
|
+
module Caldera
|
9
|
+
class Player
|
10
|
+
include EventEmitter
|
11
|
+
|
12
|
+
# @visibility private
|
13
|
+
LOGGER = Logging.logger[self]
|
14
|
+
|
15
|
+
# @return [String]
|
16
|
+
attr_reader :guild_id
|
17
|
+
|
18
|
+
# @return [Node] The node that owns this player.
|
19
|
+
attr_reader :node
|
20
|
+
|
21
|
+
# @return [Client]
|
22
|
+
attr_reader :client
|
23
|
+
|
24
|
+
# @return [Integer]
|
25
|
+
attr_reader :volume
|
26
|
+
|
27
|
+
# @return [true, false]
|
28
|
+
attr_reader :paused
|
29
|
+
|
30
|
+
# @return [Integer]
|
31
|
+
attr_reader :position
|
32
|
+
|
33
|
+
# @return [Time]
|
34
|
+
attr_reader :time
|
35
|
+
|
36
|
+
# @return [Track]
|
37
|
+
attr_reader :track
|
38
|
+
alias now_playing track
|
39
|
+
|
40
|
+
def initialize(guild_id, node, client)
|
41
|
+
@guild_id = guild_id
|
42
|
+
@node = node
|
43
|
+
@client = client
|
44
|
+
@volume = 100
|
45
|
+
@paused = false
|
46
|
+
@position = 0
|
47
|
+
@time = 0
|
48
|
+
|
49
|
+
register_node_handlers
|
50
|
+
end
|
51
|
+
|
52
|
+
# Play a track.
|
53
|
+
# @param [String, Track] Either a base64 encoded track, or a {Track} object.
|
54
|
+
# @param [Integer] start_time The time in milliseconds to begin playback at.
|
55
|
+
# @param [Integer] end_time The time in milliseconds to end at.
|
56
|
+
def play(track, start_time: 0, end_time: 0)
|
57
|
+
@paused = false
|
58
|
+
@track = track
|
59
|
+
|
60
|
+
send_packet(:play, {
|
61
|
+
track: track.is_a?(Model::Track) ? track.track_data : track,
|
62
|
+
startTime: start_time,
|
63
|
+
endTime: end_time,
|
64
|
+
noReplace: false
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
68
|
+
# Pause playback.
|
69
|
+
def pause
|
70
|
+
send_packet(:pause, {
|
71
|
+
pause: true
|
72
|
+
})
|
73
|
+
end
|
74
|
+
|
75
|
+
# Resume the player.
|
76
|
+
def unpause
|
77
|
+
send_packet(:pause, {
|
78
|
+
pause: false
|
79
|
+
})
|
80
|
+
end
|
81
|
+
|
82
|
+
# Seek to a position in the track.
|
83
|
+
# @param [Integer] position The position to seek to, in milliseconds.
|
84
|
+
def seek(position)
|
85
|
+
send_packet(:seek, {
|
86
|
+
position: position
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
90
|
+
# Set the volume of the player
|
91
|
+
# @param [Integer] level A value between 0 and 1000.
|
92
|
+
def volume(level)
|
93
|
+
send_packet(:volume, {
|
94
|
+
volume: level.clamp(0, 1000)
|
95
|
+
})
|
96
|
+
end
|
97
|
+
|
98
|
+
# Adjust the gain of bands.
|
99
|
+
# @example
|
100
|
+
# player.equalizer(1 => 0.25, 5 => -0.25, 10 => 0.0)
|
101
|
+
def equalizer(**bands)
|
102
|
+
send_packet(:equalizer, {
|
103
|
+
bands: bands.collect do |band,gain|
|
104
|
+
{ band: band.to_i, gain: gain.to_f }
|
105
|
+
end
|
106
|
+
})
|
107
|
+
end
|
108
|
+
|
109
|
+
# Destroy this player
|
110
|
+
def destroy
|
111
|
+
send_packet(:destroy)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @visibility private
|
115
|
+
def state=(new_state)
|
116
|
+
@time = Time.at(new_state['time'])
|
117
|
+
@position = new_state['position']
|
118
|
+
end
|
119
|
+
|
120
|
+
# See Node#load_tracks
|
121
|
+
def load_tracks(...)
|
122
|
+
@node.load_tracks(...)
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def send_packet(op, data={})
|
128
|
+
packet = { op: op, guildId: guild_id }.merge(data)
|
129
|
+
LOGGER.debug { "Sending packet to node: #{packet}"}
|
130
|
+
@node.send_json(packet)
|
131
|
+
end
|
132
|
+
|
133
|
+
def register_node_handlers
|
134
|
+
node.on(:track_start, &method(:handle_track_start))
|
135
|
+
node.on(:track_end, &method(:handle_track_end))
|
136
|
+
node.on(:track_exception, &method(:handle_track_exception))
|
137
|
+
node.on(:track_stuck, &method(:handle_track_stuck))
|
138
|
+
node.on(:websocket_closed, &method(:handle_websocket_closed))
|
139
|
+
end
|
140
|
+
|
141
|
+
def handle_track_start(data)
|
142
|
+
LOGGER.debug { "Track started for #{@guild_id}" }
|
143
|
+
emit(:track_start, Events::TrackStart.new(data, self))
|
144
|
+
end
|
145
|
+
|
146
|
+
def handle_track_end(data)
|
147
|
+
LOGGER.debug { "Track ended for #{@guild_id}" }
|
148
|
+
|
149
|
+
@track = nil
|
150
|
+
emit(:track_end, Events::TrackEnd.new(data, self))
|
151
|
+
end
|
152
|
+
|
153
|
+
def handle_track_exception(data)
|
154
|
+
LOGGER.debug { "Track exception for #{@guild_id}" }
|
155
|
+
@track = nil
|
156
|
+
emit(:track_exception, Events::TrackException.new(data, self))
|
157
|
+
end
|
158
|
+
|
159
|
+
def handle_track_stuck(data)
|
160
|
+
LOGGER.debug { "Track stuck for #{@guild_id}" }
|
161
|
+
@track = nil
|
162
|
+
emit(:track_stuck, Events::TrackStuck.new(data, self))
|
163
|
+
end
|
164
|
+
|
165
|
+
def handle_websocket_closed(data)
|
166
|
+
LOGGER.warn { "WebSocket closed for #{@guild_id}" }
|
167
|
+
@track = nil
|
168
|
+
emit(:websocket_closed, Events::WebSocketClosed.new(data, self))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'websocket/driver'
|
4
|
+
require 'permessage_deflate'
|
5
|
+
require 'socket'
|
6
|
+
require 'uri'
|
7
|
+
require 'logging'
|
8
|
+
require 'event_emitter'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
module Caldera
|
12
|
+
class WebSocket
|
13
|
+
include EventEmitter
|
14
|
+
|
15
|
+
attr_reader :thread, :uri
|
16
|
+
|
17
|
+
LOGGER = Logging.logger[self]
|
18
|
+
|
19
|
+
# Create a WebSocket that connects to the Lavalink server.
|
20
|
+
# @param [URI, String] uri The URI to connect with. (ex: `"ws://localhost:8080"`)
|
21
|
+
# @param [String] authorization Password matching the server config.
|
22
|
+
# @param [Integer] num_shards Total number of shards your bot is operating on.
|
23
|
+
# @param [String, Integer] user_id The user ID of the bot you are playing music with.
|
24
|
+
# @caldera.lavalink_docs https://github.com/Frederikam/Lavalink/blob/master/IMPLEMENTATION.md#opening-a-connection
|
25
|
+
def initialize(uri, authorization, num_shards, user_id)
|
26
|
+
@uri = uri.is_a?(URI::Generic) ? uri : URI.parse(uri)
|
27
|
+
@uri.scheme = 'ws'
|
28
|
+
|
29
|
+
create_driver
|
30
|
+
set_headers(authorization, num_shards, user_id)
|
31
|
+
register_handlers
|
32
|
+
end
|
33
|
+
|
34
|
+
# Start the connection to the Lavalink server.
|
35
|
+
def start
|
36
|
+
LOGGER.info { "Opening connection to #{url}" }
|
37
|
+
@tcp = TCPSocket.new(@uri.host || localhost, @uri.port)
|
38
|
+
@dead = false
|
39
|
+
create_thread
|
40
|
+
|
41
|
+
@driver.start
|
42
|
+
LOGGER.info { 'Driver started' }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Encode a hash to json, and send it over the websocket.
|
46
|
+
# @param [Hash] message
|
47
|
+
def send_json(message)
|
48
|
+
LOGGER.debug { "Sending message: #{message.inspect}" }
|
49
|
+
@driver.text(JSON.dump(message))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Write data to the socket
|
53
|
+
# @param [String] data
|
54
|
+
def write(data)
|
55
|
+
@tcp.write(data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Send a close frame
|
59
|
+
# @param [String] message The close reason
|
60
|
+
# @param [Integer] code The close code
|
61
|
+
def close(message: nil, code: 1000)
|
62
|
+
LOGGER.debug { "Sending close: (#{message.inspect}, #{code})" }
|
63
|
+
@driver.close(reason, code)
|
64
|
+
end
|
65
|
+
|
66
|
+
def url
|
67
|
+
@uri.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Construct a WebSocket::Driver instance.
|
73
|
+
def create_driver
|
74
|
+
@driver = ::WebSocket::Driver.client(self)
|
75
|
+
@driver.add_extension(PermessageDeflate)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Set the relevant headers for opening a connection.
|
79
|
+
# @param [String] authorization
|
80
|
+
# @param [Integer] num_shards
|
81
|
+
# @param [Integer, String] user_id
|
82
|
+
# @caldera.lavalink_docs [Opening a connection](https://github.com/Frederikam/Lavalink/blob/master/IMPLEMENTATION.md#opening-a-connection)
|
83
|
+
def set_headers(authorization, num_shards, user_id)
|
84
|
+
LOGGER.debug { 'Setting connection headers' }
|
85
|
+
@driver.set_header('Authorization', authorization)
|
86
|
+
@driver.set_header('Num-Shards', num_shards)
|
87
|
+
@driver.set_header('User-Id', user_id)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Register event handlers on the driver.
|
91
|
+
def register_handlers
|
92
|
+
LOGGER.debug { 'Registering driver handlers' }
|
93
|
+
@driver.on(:open, &method(:handle_open))
|
94
|
+
@driver.on(:message, &method(:handle_message))
|
95
|
+
@driver.on(:close, &method(:handle_close))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Begin the thread that feeds messages to the driver.
|
99
|
+
def create_thread
|
100
|
+
LOGGER.debug { 'Creating read thread' }
|
101
|
+
@thread = Thread.new do
|
102
|
+
@driver.parse(read_data) until @dead
|
103
|
+
LOGGER.debug { 'Read loop ending' }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Fired when the WS handshake has finished
|
108
|
+
def handle_open(_event)
|
109
|
+
LOGGER.info('WebSocket connected')
|
110
|
+
emit(:open)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Fired when a message frame has been received
|
114
|
+
def handle_message(event)
|
115
|
+
LOGGER.info("Received message: #{event.data.inspect}")
|
116
|
+
parsed = JSON.load(event.data)
|
117
|
+
emit(parsed['op'], parsed)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Fired when the WS connection has sent a close frame.
|
121
|
+
def handle_close(event)
|
122
|
+
LOGGER.warn { "Received a close frame: (#{event.reason}, #{event.code})" }
|
123
|
+
@dead = true
|
124
|
+
@thread.kill
|
125
|
+
emit(:close, { reason: event.reason, code: event.code })
|
126
|
+
end
|
127
|
+
|
128
|
+
# Read data from the TCP socket.
|
129
|
+
# @param [Integer] length The maximum length to read at once.
|
130
|
+
def read_data(length = 4096)
|
131
|
+
@tcp.readpartial(length)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: caldera
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Carey
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '12.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '12.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.93.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.93.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop-performance
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.25
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.25
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: event_emitter
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.2.6
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.2.6
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: logging
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.3'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: permessage_deflate
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.1.4
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.1.4
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: websocket-driver
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.7.3
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.7.3
|
125
|
+
description: Lavalink client for use with Discord libraries.
|
126
|
+
email:
|
127
|
+
- matthew.b.carey@gmailcom
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".travis.yml"
|
134
|
+
- CODE_OF_CONDUCT.md
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE.md
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- bin/console
|
140
|
+
- bin/setup
|
141
|
+
- caldera.gemspec
|
142
|
+
- lib/caldera.rb
|
143
|
+
- lib/caldera/client.rb
|
144
|
+
- lib/caldera/events.rb
|
145
|
+
- lib/caldera/model.rb
|
146
|
+
- lib/caldera/model/load_tracks.rb
|
147
|
+
- lib/caldera/model/playlist_info.rb
|
148
|
+
- lib/caldera/model/track.rb
|
149
|
+
- lib/caldera/node.rb
|
150
|
+
- lib/caldera/player.rb
|
151
|
+
- lib/caldera/version.rb
|
152
|
+
- lib/caldera/websocket.rb
|
153
|
+
homepage: https://github.com/swarley/caldera
|
154
|
+
licenses:
|
155
|
+
- MIT
|
156
|
+
metadata:
|
157
|
+
homepage_uri: https://github.com/swarley/caldera
|
158
|
+
source_code_uri: https://github.com/swarley/caldera
|
159
|
+
changelog_uri: https://github.com/swarley/caldera/blob/main/CHANGELOG.md
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: 2.5.0
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubygems_version: 3.1.4
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
178
|
+
summary: Lavalink client
|
179
|
+
test_files: []
|