diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 652dfd5..45105aa 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -25,4 +25,4 @@ Please delete options that are not relevant.
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my feature works
-- [ ] I have checked my changes haven't lowered code coverage
+
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index a8715b0..eab9c4f 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -26,17 +26,21 @@ jobs:
${{ runner.os }}-pubspec-
- name: Install dependencies
+ working-directory: packages/nyxx_lavalink
run: dart pub get
- name: Generate docs
+ working-directory: packages/nyxx_lavalink
run: dart doc
- name: Extract branch name
+ working-directory: packages/nyxx_lavalink
shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
id: extract_branch
- name: Deploy nyxx dev docs
+ working-directory: packages/nyxx_lavalink
uses: easingthemes/ssh-deploy@v2.1.5
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_SERVER_KEY }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index a4ed848..48a1ec9 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -6,8 +6,12 @@ on:
- main
jobs:
- nyxx_publish:
+ publish:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ package: [lavalink, nyxx_lavalink]
+
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -20,9 +24,10 @@ jobs:
restore-keys: |
${{ runner.os }}-pubspec-
- - name: 'publish nyxx package to pub.dev'
+ - name: 'publish ${{ matrix.package }} package to pub.dev'
id: publish
uses: k-paxian/dart-package-publisher@master
+ working-directory: packages/${{ matrix.package }}
with:
skipTests: true
force: true
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index ece288b..ee0d4b1 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -11,6 +11,10 @@ jobs:
runs-on: ubuntu-latest
env:
TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
+ strategy:
+ matrix:
+ package: [lavalink, nyxx_lavalink]
+
steps:
- name: Setup Dart Action
uses: dart-lang/setup-dart@v1
@@ -27,9 +31,11 @@ jobs:
${{ runner.os }}-pubspec-
- name: Install dependencies
+ working-directory: packages/${{ matrix.package }}
run: dart pub get
- name: Analyze project source
+ working-directory: packages/${{ matrix.package }}
run: dart analyze
format:
@@ -37,6 +43,10 @@ jobs:
runs-on: ubuntu-latest
env:
TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
+ strategy:
+ matrix:
+ package: [lavalink, nyxx_lavalink]
+
steps:
- name: Setup Dart Action
uses: dart-lang/setup-dart@v1
@@ -53,9 +63,11 @@ jobs:
${{ runner.os }}-pubspec-
- name: Install dependencies
+ working-directory: packages/${{ matrix.package }}
run: dart pub get
- name: Format
+ working-directory: packages/${{ matrix.package }}
run: dart format --set-exit-if-changed -l 160 ./lib
tests:
@@ -64,6 +76,10 @@ jobs:
runs-on: ubuntu-latest
env:
TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
+ strategy:
+ matrix:
+ package: [lavalink, nyxx_lavalink]
+
steps:
- name: Setup Dart Action
uses: dart-lang/setup-dart@v1
@@ -80,7 +96,9 @@ jobs:
${{ runner.os }}-pubspec-
- name: Install dependencies
+ working-directory: packages/${{ matrix.package }}
run: dart pub get
- name: Unit tests
+ working-directory: packages/${{ matrix.package }}
run: dart run test test/unit/**
diff --git a/.gitignore b/.gitignore
index 295e586..63a682b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@ docs/
.project
.pub
**/build
-**/packages
*.dart.js
*.part.js
*.js.deps
@@ -37,3 +36,4 @@ test-*.dart
coverage.json
lcov.info
pubspec.lock
+application.yml.example
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 8b96ec0..0000000
--- a/Makefile
+++ /dev/null
@@ -1,40 +0,0 @@
-.PHONY: help
-help:
- @fgrep -h "##" $(MAKEFILE_LIST) | sed -e 's/\(\:.*\#\#\)/\:\ /' | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
-
-.PHONY: app-check ## Run basic format checks and then generate code coverage
-app-check: format-check generate-coverage
-
-.PHONY: format-check ## Check basic format
-format-check: format analyze
-
-.PHONY: generate-coverage
-generate-coverage: integration-tests unit-tests coverage-format coverage-gen-html ## Run all test and generate html code coverage
-
-.PHONY: integration-tests
-integration-tests: ## Run integration tests with coverage
- (timeout 20s dart run test --coverage="coverage" --timeout=none test/integration ; exit 0)
-
-.PHONY: unit-tests
-unit-tests: ## Run unit tests with coverage
- (timeout 10s dart run test --coverage="coverage" --timeout=none test/unit ; exit 0)
-
-.PHONY: coverage-format
-coverage-format: ## Format dart coverage output to lcov
- dart run coverage:format_coverage --lcov --in=coverage --out=coverage/coverage.lcov --packages=.packages --report-on=lib
-
-.PHONY: coverage-gen-html
-coverage-gen-html: ## Generate html coverage from lcov data
- genhtml coverage/coverage.lcov -o coverage/coverage_gen
-
-.PHONY: format
-format: ## Run dart format
- dart format --set-exit-if-changed -l 160 ./lib
-
-.PHONY: format-apply
-format-apply: ## Run dart format
- dart format --fix -l 160 ./lib
-
-.PHONY: analyze
-analyze: ## Run dart analyze
- dart analyze
diff --git a/README.md b/README.md
deleted file mode 100644
index 32bac54..0000000
--- a/README.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# nyxx_lavalink
-
-[![Discord Shield](https://discordapp.com/api/guilds/846136758470443069/widget.png?style=shield)](https://discord.gg/nyxx)
-[![pub](https://img.shields.io/pub/v/nyxx.svg)](https://pub.dartlang.org/packages/nyxx_lavalink)
-[![documentation](https://img.shields.io/badge/Documentation-nyxx_interactions-yellow.svg)](https://www.dartdocs.org/documentation/nyxx_lavalink/latest/)
-
-Simple, robust framework for creating discord bots for Dart language.
-
-
-
-### Features
-
-- **Lavalink support**
- Nyxx allows you to create music bots by adding support to [Lavalink](https://github.com/freyacodes/Lavalink) API
-- **Fine Control**
- Nyxx allows you to control every outgoing HTTP request or WebSocket message.
-
-## Quick example
-
-Lavalink
-```dart
-void main() async {
- final bot = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged);
-
- final guildId = Snowflake("GUILD_ID");
- final channelId = Snowflake("CHANNEL_ID");
-
- final cluster = ICluster.createCluster(bot, Snowflake("BOT_ID"));
- await cluster.addNode(NodeOptions());
-
- bot.eventsWs.onMessageReceived.listen((event) async {
- if (event.message.content == "!join") {
- final channel = await bot.fetchChannel(channelId);
-
- cluster.getOrCreatePlayerNode(guildId);
-
- channel.connect();
- } else {
- final node = cluster.getOrCreatePlayerNode(guildId);
-
- final searchResults = await node.searchTracks(event.message.content);
-
- node.play(guildId, searchResults.tracks[0]).queue();
- }
- });
-}
-```
-
-## Other nyxx packages
-
-- [nyxx](https://github.com/nyxx-discord/nyxx)
-- [nyxx_interactions](https://github.com/nyxx-discord/nyxx_interactions)
-- [nyxx_extensions](https://github.com/nyxx-discord/nyxx_extensions)
-- [nyxx_commander](https://github.com/nyxx-discord/nyxx_commander)
-- [nyxx_pagination](https://github.com/nyxx-discord/nyxx_pagination)
-
-## More examples
-
-Nyxx examples can be found [here](https://github.com/nyxx-discord/nyxx_lavalink/tree/dev/example).
-
-### Example bots
-- [Running on Dart](https://github.com/l7ssha/running_on_dart)
-
-## Documentation, help and examples
-
-**Dartdoc documentation for latest stable version is hosted on [pub](https://www.dartdocs.org/documentation/nyxx_lavalink/latest/)**
-
-#### [Docs and wiki](https://nyxx.l7ssha.xyz)
-You can read docs and wiki articles for latest stable version on my website. This website also hosts docs for latest
-dev changes to framework (`dev` branch)
-
-#### [Official nyxx discord server](https://discord.gg/nyxx)
-If you need assistance in developing bot using nyxx you can join official nyxx discord guild.
-
-#### [Discord API docs](https://discordapp.com/developers/docs/intro)
-Discord API documentation features rich descriptions about all topics that nyxx covers.
-
-#### [Discord API Guild](https://discord.gg/discord-api)
-The unofficial guild for Discord Bot developers. To get help with nyxx check `#dart_nyxx` channel.
-
-#### [Dartdocs](https://www.dartdocs.org/documentation/nyxx_lavalink/latest/)
-The dartdocs page will always have the documentation for the latest release.
-
-## Contributing to Nyxx
-
-Read [contributing document](https://github.com/nyxx-discord/nyxx_lavalink/blob/dev/CONTRIBUTING.md)
-
-## Credits
-
-* [Hackzzila's](https://github.com/Hackzzila) for [nyx](https://github.com/Hackzzila/nyx).
diff --git a/README.md b/README.md
new file mode 120000
index 0000000..3978739
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+packages/nyxx_lavalink/README.md
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
deleted file mode 100644
index 53ec678..0000000
--- a/analysis_options.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-include: package:lints/recommended.yaml
-
-linter:
- rules:
- unrelated_type_equality_checks: false
- implementation_imports: false
-
-analyzer:
- exclude: [build/**, example/**]
- language:
- strict-raw-types: true
- strong-mode:
- implicit-casts: false
diff --git a/example/example.dart b/example/example.dart
deleted file mode 100644
index a25b37a..0000000
--- a/example/example.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-import "dart:io";
-
-import "package:nyxx_lavalink/nyxx_lavalink.dart";
-import "package:nyxx/nyxx.dart";
-
-void main() async {
- final client = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged)
- ..registerPlugin(Logging()) // Default logging plugin
- ..registerPlugin(CliIntegration()) // Cli integration for nyxx allows stopping application via SIGTERM and SIGKILl
- ..registerPlugin(IgnoreExceptions()) // Plugin that handles uncaught exceptions that may occur
- ..connect();
-
- final cluster = ICluster.createCluster(client, Snowflake("YOUR_BOT_ID"));
-
- // This is a really simple example, so we'll define the guild and
- // the channel where the bot will play music on
- final guildId = Snowflake("GUILD_ID_HERE");
- final channelId = Snowflake("CHANNEL_ID_HERE");
-
- // Add your lava link nodes. Empty constructor assumes default settings to lavalink.
- await cluster.addNode(NodeOptions());
-
- await for (final msg in client.eventsWs.onMessageReceived) {
- if(msg.message.content == "!join") {
- final channel = await client.fetchChannel(channelId);
-
- // Create lava link node for guild
- cluster.getOrCreatePlayerNode(guildId);
-
- // Connect to channel
- channel.connect();
- } else if(msg.message.content == "!queue") {
- // Fetch node for guild
- final node = cluster.getOrCreatePlayerNode(guildId);
-
- // get player for guild
- final player = node.players[guildId];
-
- print(player!.queue);
- } else if (msg.message.content == "!skip") {
- final node = cluster.getOrCreatePlayerNode(guildId);
-
- // skip the current track, if it's the last on the queue, the
- // player will stop automatically
- node.skip(guildId);
- } else if(msg.message.content == "!nodes") {
- print("${cluster.connectedNodes.length} available nodes");
- } else if (msg.message.content == "!update") {
- final node = cluster.getOrCreatePlayerNode(guildId);
-
- node.updateOptions(NodeOptions());
- } else {
- // Any other message will be processed as potential title to play lava link
- final node = cluster.getOrCreatePlayerNode(guildId);
-
- // search for given query using lava link
- final searchResults = await node.searchTracks(msg.message.content);
-
- // add found song to queue and play
- node.play(guildId, searchResults.tracks[0]).queue();
- }
- }
-}
diff --git a/lib/nyxx_lavalink.dart b/lib/nyxx_lavalink.dart
deleted file mode 100644
index c8eb41b..0000000
--- a/lib/nyxx_lavalink.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-library nyxx_lavalink;
-
-export 'src/model/base_event.dart' show IBaseEvent;
-export 'src/model/exception.dart' show ILavalinkException;
-export 'src/model/guild_player.dart' show IGuildPlayer;
-export 'src/model/play_parameters.dart' show IPlayParameters;
-export 'src/model/player_update.dart' show IPlayerUpdateEvent, IPlayerUpdateStateEvent;
-export 'src/model/search_platform.dart' show SearchPlatform;
-export 'src/model/stats.dart' show IStatsEvent, ICpuStats, IFrameStats, IMemoryStats;
-export 'src/model/track.dart' show IQueuedTrack, IPlaylistInfo, ITrack, ITrackInfo, ITracks;
-export 'src/model/track_end.dart' show ITrackEndEvent;
-export 'src/model/track_exception.dart' show ITrackExceptionEvent;
-export 'src/model/track_start.dart' show ITrackStartEvent;
-export 'src/model/track_stuck.dart' show ITrackStuckEvent;
-export 'src/model/websocket_closed.dart' show IWebSocketClosedEvent;
-export 'src/node/node.dart' show INode;
-export 'src/node/node_options.dart' show NodeOptions;
-
-export 'src/cluster.dart' show ICluster;
-export 'src/cluster_exception.dart' show ClusterException;
-export 'src/event_dispatcher.dart' show IEventDispatcher;
-export 'src/http_exception.dart' show HttpException;
diff --git a/lib/src/cluster.dart b/lib/src/cluster.dart
deleted file mode 100644
index 8f1e1cc..0000000
--- a/lib/src/cluster.dart
+++ /dev/null
@@ -1,298 +0,0 @@
-import 'dart:collection';
-import 'dart:isolate';
-
-import 'package:logging/logging.dart';
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-import 'package:nyxx_lavalink/src/node/node_options.dart';
-import 'package:nyxx_lavalink/src/node/node_runner.dart';
-import 'package:nyxx_lavalink/src/model/guild_player.dart';
-import 'package:nyxx_lavalink/src/cluster_exception.dart';
-
-import 'event_dispatcher.dart';
-
-abstract class ICluster implements Disposable {
- /// A reference to the client
- INyxx get client;
-
- /// The client id provided to lavalink;
- Snowflake get clientId;
-
- /// Returns a map with the nodes connected to lavalink cluster
- UnmodifiableMapView get connectedNodes;
-
- /// Returns a map with the nodes that are actually disconnected from lavalink
- UnmodifiableMapView get disconnectedNodes;
-
- /// Dispatcher of all lavalink events
- late final IEventDispatcher eventDispatcher;
-
- /// Get the best available node, it is recommended to use [getOrCreatePlayerNode] instead
- /// as this won't create the player itself if it doesn't exists
- INode get bestNode;
-
- /// Attempts to get the node containing a player for a specific guild id
- ///
- /// if the player doesn't exist, then the best node is retrieved and the player created
- INode getOrCreatePlayerNode(Snowflake guildId);
-
- /// Attempts to retrieve a node disconnected from lavalink by its id,
- /// this method does not work with nodes that have exceeded the maximum
- /// reconnect attempts as those get removed from cluster
- INode? getDisconnectedNode(int nodeId);
-
- /// Adds and initializes a node
- Future addNode(NodeOptions options);
-
- static ICluster createCluster(INyxxWebsocket client, Snowflake clientId) => Cluster(client, clientId);
-}
-
-/// Cluster of lavalink nodes
-class Cluster implements ICluster {
- /// A reference to the client
- @override
- final INyxxWebsocket client;
-
- /// The client id provided to lavalink;
- @override
- final Snowflake clientId;
-
- /// All available nodes, ordered by node id
- final Map nodes = {};
-
- /// Returns a map with the nodes connected to lavalink cluster
- @override
- UnmodifiableMapView get connectedNodes => UnmodifiableMapView(nodes);
-
- /// Nodes that are currently connecting to server, when a node gets connected
- /// it will be moved to [_nodes], and when reconnecting will be moved here again
- final Map connectingNodes = {};
-
- /// Returns a map with the nodes that are actually disconnected from lavalink
- @override
- UnmodifiableMapView get disconnectedNodes => UnmodifiableMapView(connectingNodes);
-
- /// A map to keep the assigned node id for each player
- final Map nodeLocations = {};
-
- /// The last id assigned to a node, this is used to avoid repeating ids
- /// since if we use a repeated id, the existing node would be overwritten and lost
- int _lastId = 0;
-
- final _receivePort = ReceivePort();
- late final Stream _receiveStream;
-
- final logger = Logger("Lavalink");
-
- @override
- late final IEventDispatcher eventDispatcher;
-
- Future _addNode(NodeOptions nodeOptions, int nodeId) async {
- await Isolate.spawn(handleNode, _receivePort.sendPort);
-
- final isolateSendPort = await _receiveStream.firstWhere((element) => element is SendPort) as SendPort;
-
- nodeOptions.clientId = clientId;
- nodeOptions.nodeId = nodeId;
-
- isolateSendPort.send(nodeOptions.toJson());
-
- // Say the node to start the connection
- isolateSendPort.send({"cmd": "CONNECT"});
-
- final node = Node.fromOptions(this, nodeOptions, isolateSendPort);
-
- connectingNodes[nodeId] = node;
- }
-
- void _handleNodeMessage(dynamic message) {
- if (message is SendPort) {
- return;
- }
-
- final map = message as Map;
-
- logger.finer("Receved data from node ${map["nodeId"]}, data: $map");
-
- switch (map["cmd"]) {
- case "DISPATCH":
- (eventDispatcher as EventDispatcher).dispatchEvent(map);
- break;
-
- case "LOG":
- {
- Level? level;
-
- switch (map["level"]) {
- case "INFO":
- level = Level.INFO;
- break;
-
- case "WARNING":
- level = Level.WARNING;
- break;
- }
-
- logger.log(level!, map["message"]);
- }
- break;
-
- case "EXITED":
- {
- final nodeId = map["nodeId"]! as int;
- nodes.remove(nodeId);
- connectingNodes.remove(nodeId);
-
- logger.info("[Node $nodeId] Exited");
- }
- break;
-
- case "CONNECTED":
- {
- final node = connectingNodes.remove(map["nodeId"] as int);
-
- if (node != null) {
- nodes[node.options.nodeId] = node;
-
- logger.info("[Node ${map["nodeId"]}] Connected to lavalink");
- }
- }
- break;
-
- case "DISCONNECTED":
- {
- final node = nodes.remove(map["nodeId"] as int);
-
- if (node == null) {
- return;
- }
- connectingNodes[node.options.nodeId] = node;
-
- // this makes possible for a player to be moved to another node
- node.players.forEach((guildId, _) => nodeLocations.remove(guildId));
-
- // Also delete the players, so them can be created again on another node
- node.clearPlayers();
-
- logger.info("[Node ${map["nodeId"]}] Disconnected from lavalink");
- }
- break;
- }
- }
-
- void _registerEvents() {
- client.eventsWs.onVoiceServerUpdate.listen((event) async {
- final node = nodes[nodeLocations[event.guild.id]];
- if (node == null) {
- return;
- }
-
- final player = node.players[event.guild.id];
- if (player == null) {
- return;
- }
-
- (player as GuildPlayer).handleServerUpdate(event);
- });
-
- client.eventsWs.onVoiceStateUpdate.listen((event) async {
- if (event.raw["d"]["user_id"] != clientId.toString()) {
- return;
- }
- if (event.state.guild == null) {
- return;
- }
-
- final node = nodes[nodeLocations[event.state.guild!.id]];
- if (node == null) {
- return;
- }
-
- final player = node.players[event.state.guild!.id];
- if (player == null) {
- return;
- }
-
- (player as GuildPlayer).handleStateUpdate(event);
- });
- }
-
- /// Get the best available node, it is recommended to use [getOrCreatePlayerNode] instead
- /// as this won't create the player itself if it doesn't exists
- @override
- INode get bestNode {
- if (nodes.isEmpty) {
- throw ClusterException("No available nodes");
- }
- if (nodes.length == 1) {
- return nodes.values.first;
- }
-
- /// Node id of the node who has fewer players
- int? minNodeId;
-
- /// Number of players the node has
- int? minNodePlayers;
-
- nodes.forEach((id, node) {
- if (minNodeId == null && minNodePlayers == null) {
- minNodeId = id;
- minNodePlayers = node.players.length;
- } else {
- if (node.players.length < minNodePlayers!) {
- minNodeId = id;
- minNodePlayers = node.players.length;
- }
- }
- });
-
- return nodes[minNodeId]!;
- }
-
- /// Attempts to get the node containing a player for a specific guild id
- ///
- /// if the player doesn't exist, then the best node is retrieved and the player created
- @override
- INode getOrCreatePlayerNode(Snowflake guildId) {
- final nodePreview = nodeLocations.containsKey(guildId) ? nodes[nodeLocations[guildId]] : bestNode;
-
- final node = nodePreview ?? bestNode;
-
- if (!node.players.containsKey(guildId)) {
- node.createPlayer(guildId);
- }
-
- return node;
- }
-
- /// Attempts to retrieve a node disconnected from lavalink by its id,
- /// this method does not work with nodes that have exceeded the maximum
- /// reconnect attempts as those get removed from cluster
- @override
- INode? getDisconnectedNode(int nodeId) => connectingNodes[nodeId];
-
- /// Adds and initializes a node
- @override
- Future addNode(NodeOptions options) async {
- /// Set a tiny delay so we can ensure we don't repeat ids
- await Future.delayed(const Duration(milliseconds: 50));
-
- _lastId += 1;
- await _addNode(options, _lastId);
- }
-
- /// Creates a new cluster ready to start adding connections
- Cluster(this.client, this.clientId) {
- _registerEvents();
-
- eventDispatcher = EventDispatcher(this);
-
- _receiveStream = _receivePort.asBroadcastStream();
- _receiveStream.listen(_handleNodeMessage);
- }
-
- @override
- Future dispose() async {
- await eventDispatcher.dispose();
- }
-}
diff --git a/lib/src/cluster_exception.dart b/lib/src/cluster_exception.dart
deleted file mode 100644
index 33cae47..0000000
--- a/lib/src/cluster_exception.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-/// An exception related to cluster functions
-class ClusterException implements Exception {
- /// The actual error description
- final String error;
-
- ClusterException(this.error);
-
- @override
- String toString() => "Lavalink cluster error: $error";
-}
diff --git a/lib/src/event_dispatcher.dart b/lib/src/event_dispatcher.dart
deleted file mode 100644
index 0efed4a..0000000
--- a/lib/src/event_dispatcher.dart
+++ /dev/null
@@ -1,155 +0,0 @@
-import 'dart:async';
-
-import 'package:nyxx/nyxx.dart';
-import 'cluster.dart';
-
-import 'model/stats.dart';
-import 'model/track.dart';
-import 'model/track_end.dart';
-import 'model/track_start.dart';
-import 'model/track_stuck.dart';
-import 'model/track_exception.dart';
-import 'model/player_update.dart';
-import 'model/websocket_closed.dart';
-import 'node/node.dart';
-
-abstract class IEventDispatcher implements Disposable {
- /// Emitted when stats are sent from lavalink
- Stream get onStatsReceived;
-
- /// Emitted when a player gets updated
- Stream get onPlayerUpdate;
-
- /// Emitted when a track starts playing
- Stream get onTrackStart;
-
- /// Emitted when a track ends playing
- Stream get onTrackEnd;
-
- /// Emitted when a track gets an exception during playback
- Stream get onTrackException;
-
- /// Emitted when a track gets stuck
- Stream get onTrackStuck;
-
- /// Emitted when a web socket is closed
- Stream get onWebSocketClosed;
-}
-
-class EventDispatcher implements IEventDispatcher {
- final Cluster cluster;
-
- final StreamController onStatsReceivedController = StreamController.broadcast();
- final StreamController onPlayerUpdateController = StreamController.broadcast();
- final StreamController onTrackStartController = StreamController.broadcast();
- final StreamController onTrackEndController = StreamController.broadcast();
- final StreamController onTrackExceptionController = StreamController.broadcast();
- final StreamController onTrackStuckController = StreamController.broadcast();
- final StreamController onWebSocketClosedController = StreamController.broadcast();
-
- /// Emitted when stats are sent from lavalink
- @override
- late final Stream onStatsReceived;
-
- /// Emitted when a player gets updated
- @override
- late final Stream onPlayerUpdate;
-
- /// Emitted when a track starts playing
- @override
- late final Stream onTrackStart;
-
- /// Emitted when a track ends playing
- @override
- late final Stream onTrackEnd;
-
- /// Emitted when a track gets an exception during playback
- @override
- late final Stream onTrackException;
-
- /// Emitted when a track gets stuck
- @override
- late final Stream onTrackStuck;
-
- /// Emitted when a web socket is closed
- @override
- late final Stream onWebSocketClosed;
-
- EventDispatcher(this.cluster) {
- onStatsReceived = onStatsReceivedController.stream;
- onPlayerUpdate = onPlayerUpdateController.stream;
- onTrackStart = onTrackStartController.stream;
- onTrackEnd = onTrackEndController.stream;
- onTrackException = onTrackExceptionController.stream;
- onTrackStuck = onTrackStuckController.stream;
- onWebSocketClosed = onWebSocketClosedController.stream;
- }
-
- void dispatchEvent(Map json) {
- final node = cluster.nodes[json["nodeId"]];
-
- if (node == null) {
- return;
- }
-
- cluster.logger.fine("[Node ${json["nodeId"]}] Dispatching ${json["event"]}");
-
- switch (json["event"]) {
- case "TrackStartEvent":
- onTrackStartController.add(TrackStartEvent(cluster.client, node, json["data"] as Map));
- break;
-
- case "TrackEndEvent":
- {
- final trackEnd = TrackEndEvent(cluster.client, node, json["data"] as Map);
-
- onTrackEndController.add(trackEnd);
-
- (node as Node).handleTrackEnd(trackEnd);
- }
- break;
-
- case "TrackExceptionEvent":
- onTrackExceptionController.add(TrackExceptionEvent(cluster.client, node, json["data"] as Map));
- break;
-
- case "TrackStuckEvent":
- onTrackStuckController.add(TrackStuckEvent(cluster.client, node, json["data"] as Map));
- break;
-
- case "WebSocketClosedEvent":
- onWebSocketClosedController.add(WebSocketClosedEvent(cluster.client, node, json["data"] as Map));
- break;
-
- case "stats":
- final stats = StatsEvent(cluster.client, node, json["data"] as Map);
-
- // Put the stats into the node
- (node as Node).stats = stats;
-
- onStatsReceivedController.add(stats);
- break;
-
- case "playerUpdate":
- final update = PlayerUpdateEvent(cluster.client, node, json["data"] as Map);
-
- if (update.state.position != null) {
- final _node = node as Node;
- // Update the position of the currently playing track of the corresponding player.
- (_node.players[update.guildId]?.nowPlaying?.track.info as TrackInfo?)?.position = update.state.position!;
- }
-
- onPlayerUpdateController.add(update);
- break;
- }
- }
-
- @override
- Future dispose() async {
- await onStatsReceivedController.close();
- await onPlayerUpdateController.close();
- await onTrackStartController.close();
- await onTrackEndController.close();
- await onWebSocketClosedController.close();
- }
-}
diff --git a/lib/src/http_exception.dart b/lib/src/http_exception.dart
deleted file mode 100644
index 7adf4cd..0000000
--- a/lib/src/http_exception.dart
+++ /dev/null
@@ -1,11 +0,0 @@
-/// An exception that can be thrown when using
-/// [Node.searchTracks] or [Node.autoSearch] if the request fails
-class HttpException implements Exception {
- /// The status code of the request
- final int code;
-
- HttpException(this.code);
-
- @override
- String toString() => "Lavalink server responded with $code code";
-}
diff --git a/lib/src/model/base_event.dart b/lib/src/model/base_event.dart
deleted file mode 100644
index 2cadbb8..0000000
--- a/lib/src/model/base_event.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IBaseEvent {
- /// A reference to the current client
- INyxx get client;
-
- /// A reference to the node this event belongs to
- INode get node;
-}
-
-/// Base event class which all events must inherit
-class BaseEvent implements IBaseEvent {
- /// A reference to the current client
- @override
- final INyxx client;
-
- /// A reference to the node this event belongs to
- @override
- final INode node;
-
- /// Creates a new base event instance
- BaseEvent(this.client, this.node);
-}
diff --git a/lib/src/model/exception.dart b/lib/src/model/exception.dart
deleted file mode 100644
index f3a72e5..0000000
--- a/lib/src/model/exception.dart
+++ /dev/null
@@ -1,43 +0,0 @@
-/// A exception object that can be sent by lavalink at certain endpoints
-abstract class ILavalinkException {
- /// Exception message
- String? get message;
-
- /// The error message
- String? get error;
-
- /// The cause of the exception
- String? get cause;
-
- /// Exception severity
- String? get severity;
-}
-
-/// A exception object that can be sent by lavalink at certain endpoints
-class LavalinkException implements ILavalinkException {
- /// Exception message
- @override
- late final String? message;
-
- /// The error message
- @override
- late final String? error;
-
- /// The cause of the exception
- @override
- late final String? cause;
-
- /// Exception severity
- @override
- late final String? severity;
-
- LavalinkException(Map json) {
- if (json.containsKey("exception")) {
- message = json["exception"]["message"] as String?;
- severity = json["exception"]["severity"] as String?;
- cause = json["exception"]["cause"] as String?;
- } else {
- error = json["error"] as String?;
- }
- }
-}
diff --git a/lib/src/model/guild_player.dart b/lib/src/model/guild_player.dart
deleted file mode 100644
index d5c7f87..0000000
--- a/lib/src/model/guild_player.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/track.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IGuildPlayer {
- /// Track queue
- List get queue;
-
- /// The currently playing track
- IQueuedTrack? get nowPlaying;
-
- /// Guild where this player operates on
- Snowflake get guildId;
-}
-
-/// A player of a specific guild
-class GuildPlayer implements IGuildPlayer {
- /// Track queue
- @override
- List queue = [];
-
- /// The currently playing track
- @override
- QueuedTrack? nowPlaying;
-
- /// Guild where this player operates on
- @override
- final Snowflake guildId;
-
- /// A map to combine server state and server update events to send them to lavalink
- final Map _serverUpdate = {};
-
- /// A reference to the parent node
- final INode _nodeRef;
-
- GuildPlayer(this._nodeRef, this.guildId);
-
- void _dispatchVoiceUpdate() {
- if (_serverUpdate.containsKey("sessionId") && _serverUpdate.containsKey("event")) {
- (_nodeRef as Node).sendPayload("voiceUpdate", guildId, _serverUpdate);
- }
- }
-
- void handleServerUpdate(IVoiceServerUpdateEvent event) {
- _serverUpdate["event"] = {"token": event.token, "endpoint": event.endpoint, "guildId": guildId.toString()};
-
- _dispatchVoiceUpdate();
- }
-
- void handleStateUpdate(IVoiceStateUpdateEvent event) {
- _serverUpdate["sessionId"] = event.state.sessionId;
-
- _dispatchVoiceUpdate();
- }
-}
diff --git a/lib/src/model/play_parameters.dart b/lib/src/model/play_parameters.dart
deleted file mode 100644
index 0fe7639..0000000
--- a/lib/src/model/play_parameters.dart
+++ /dev/null
@@ -1,101 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/model/track.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IPlayParameters {
- /// The track to play
- ITrack get track;
-
- /// The guild where the track will be played
- Snowflake get guildId;
-
- /// Whether to replace the track or not
- bool get replace;
-
- /// The time at where the track will start to play
- Duration get startTime;
-
- /// The time at where the track will stop playing
- Duration? get endTime;
-
- /// The requester of the track
- Snowflake? get requester;
-
- /// The channel where this track was requested
- Snowflake? get channelId;
-
- /// Forces the song to start playing
- void startPlaying();
-
- /// Puts the track on the queue and starts playing if necessary
- void queue();
-}
-
-/// Parameters to start playing a track
-class PlayParameters implements IPlayParameters {
- final INode _node;
-
- /// The track to play
- @override
- final ITrack track;
-
- /// The guild where the track will be played
- @override
- final Snowflake guildId;
-
- /// Whether to replace the track or not
- @override
- bool replace;
-
- /// The time at where the track will start to play
- @override
- Duration startTime;
-
- /// The time at where the track will stop playing
- @override
- Duration? endTime;
-
- /// The requester of the track
- @override
- Snowflake? requester;
-
- /// The channel where this track was requested
- @override
- Snowflake? channelId;
-
- /// Create a new play parameters object, it is recommended to create this
- /// through [Node.play]
- PlayParameters(this._node, this.track, this.guildId, this.replace, this.startTime, this.endTime, this.requester, this.channelId);
-
- /// Forces the song to start playing
- @override
- void startPlaying() {
- if (endTime == null) {
- (_node as Node).sendPayload("play", guildId, {"track": track.track, "noReplace": !replace, "startTime": startTime.inMilliseconds});
-
- return;
- }
-
- (_node as Node).sendPayload("play", guildId, {"track": track.track, "noReplace": !replace, "startTime": startTime, "endTime": endTime!.inMilliseconds});
- }
-
- /// Puts the track on the queue and starts playing if necessary
- @override
- void queue() {
- final player = (_node as Node).players[guildId];
-
- if (player == null) {
- return;
- }
-
- final queuedTrack = QueuedTrack(track, startTime, endTime, requester, channelId);
-
- // Whether if the node should start playing the track
- final shouldPlay = player.nowPlaying == null && player.queue.isEmpty;
- player.queue.add(queuedTrack);
-
- if (shouldPlay) {
- (_node as Node).playNext(guildId);
- }
- }
-}
diff --git a/lib/src/model/player_update.dart b/lib/src/model/player_update.dart
deleted file mode 100644
index 16b9309..0000000
--- a/lib/src/model/player_update.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IPlayerUpdateEvent implements IBaseEvent {
- /// State of the current player
- IPlayerUpdateStateEvent get state;
-
- /// Guild id where player comes from
- Snowflake get guildId;
-}
-
-/// Player update event dispatched by lavalink at player progression
-class PlayerUpdateEvent extends BaseEvent implements IPlayerUpdateEvent {
- /// State of the current player
- @override
- late final IPlayerUpdateStateEvent state;
-
- /// Guild id where player comes from
- @override
- late final Snowflake guildId;
-
- PlayerUpdateEvent(INyxx client, INode node, Map json) : super(client, node) {
- guildId = Snowflake(json["guildId"]);
- state = PlayerUpdateStateEvent(json["state"]["time"] as int, json["state"]["position"] as int?);
- }
-}
-
-abstract class IPlayerUpdateStateEvent {
- /// The timestamp of the player
- int get time;
-
- /// The position where the current track is now on
- int? get position;
-}
-
-/// The state of a player at a given moment
-class PlayerUpdateStateEvent implements IPlayerUpdateStateEvent {
- /// The timestamp of the player
- @override
- final int time;
-
- /// The position where the current track is now on
- @override
- final int? position;
-
- PlayerUpdateStateEvent(this.time, this.position);
-}
diff --git a/lib/src/model/search_platform.dart b/lib/src/model/search_platform.dart
deleted file mode 100644
index d944d19..0000000
--- a/lib/src/model/search_platform.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-/// Search platforms supported by Lavalink
-class SearchPlatform extends IEnum {
- /// Youtube
- static const youtube = SearchPlatform._create("ytsearch");
-
- /// Youtube Music
- static const youtubeMusic = SearchPlatform._create("ytmsearch");
-
- /// SoundCloud
- static const soundcloud = SearchPlatform._create("scsearch");
-
- const SearchPlatform._create(String value) : super(value);
-}
diff --git a/lib/src/model/stats.dart b/lib/src/model/stats.dart
deleted file mode 100644
index be485c4..0000000
--- a/lib/src/model/stats.dart
+++ /dev/null
@@ -1,164 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IStatsEvent implements IBaseEvent {
- /// Number of playing players
- int get playingPlayers;
-
- ///Memory usage stats
- IMemoryStats get memory;
-
- /// Frame sending stats
- IFrameStats? get frameStats;
-
- /// Total amount of players
- int get players;
-
- /// Cpu usage stats
- ICpuStats get cpu;
-
- /// Server uptime
- int get uptime;
-}
-
-/// Stats update event dispatched by lavalink
-class StatsEvent extends BaseEvent implements IStatsEvent {
- /// Number of playing players
- @override
- late final int playingPlayers;
-
- ///Memory usage stats
- @override
- late final IMemoryStats memory;
-
- /// Frame sending stats
- @override
- late final IFrameStats? frameStats;
-
- /// Total amount of players
- @override
- late final int players;
-
- /// Cpu usage stats
- @override
- late final ICpuStats cpu;
-
- /// Server uptime
- @override
- late final int uptime;
-
- StatsEvent(INyxx client, INode node, Map json) : super(client, node) {
- playingPlayers = json["playingPlayers"] as int;
- players = json["players"] as int;
- uptime = json["uptime"] as int;
- memory = MemoryStats(json["memory"] as Map);
- frameStats = json["frameStats"] == null ? null : FrameStats(json["frameStats"] as Map);
- cpu = CpuStats(json["cpu"] as Map);
- }
-}
-
-abstract class IFrameStats {
- /// Sent frames
- int get sent;
-
- /// Deficit frames
- int get deficit;
-
- /// Nulled frames
- int get nulled;
-}
-
-/// Stats about frame sending to discord
-class FrameStats implements IFrameStats {
- /// Sent frames
- @override
- late final int sent;
-
- /// Deficit frames
- @override
- late final int deficit;
-
- /// Nulled frames
- @override
- late final int nulled;
-
- FrameStats(Map json) {
- sent = json["sent"] as int;
- deficit = json["deficit"] as int;
- nulled = json["nulled"] as int;
- }
-}
-
-abstract class ICpuStats {
- /// Amount of available cores on the cpu
- int get cores;
-
- /// The total load of the machine where lavalink is running on
- num get systemLoad;
-
- /// The total load of lavalink server
- num get lavalinkLoad;
-}
-
-/// Cpu usage stats
-class CpuStats implements ICpuStats {
- /// Amount of available cores on the cpu
- @override
- late final int cores;
-
- /// The total load of the machine where lavalink is running on
- @override
- late final num systemLoad;
-
- /// The total load of lavalink server
- @override
- late final num lavalinkLoad;
-
- CpuStats(Map json) {
- cores = json["cores"] as int;
- systemLoad = json["systemLoad"] as num;
- lavalinkLoad = json["lavalinkLoad"] as num;
- }
-}
-
-abstract class IMemoryStats {
- /// Reservable memory
- int get reservable;
-
- /// Used memory
- int get used;
-
- /// Free/unused memory
- int get free;
-
- /// Total allocated memory
- int get allocated;
-}
-
-/// Memory usage stats
-class MemoryStats implements IMemoryStats {
- /// Reservable memory
- @override
- late final int reservable;
-
- /// Used memory
- @override
- late final int used;
-
- /// Free/unused memory
- @override
- late final int free;
-
- /// Total allocated memory
- @override
- late final int allocated;
-
- MemoryStats(Map json) {
- reservable = json["reservable"] as int;
- used = json["used"] as int;
- free = json["free"] as int;
- allocated = json["allocated"] as int;
- }
-}
diff --git a/lib/src/model/track.dart b/lib/src/model/track.dart
deleted file mode 100644
index 64a77fc..0000000
--- a/lib/src/model/track.dart
+++ /dev/null
@@ -1,212 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/exception.dart';
-
-abstract class IQueuedTrack {
- /// The actual track
- ITrack get track;
-
- /// Where should start lavalink playing the track
- Duration get startTime;
-
- /// If the track should stop playing before finish and where
- Duration? get endTime;
-
- /// The requester of the track
- Snowflake? get requester;
-
- /// The channel where this track was requested
- Snowflake? get channelId;
-}
-
-/// Represents a track already on a player queue
-class QueuedTrack implements IQueuedTrack {
- /// The actual track
- @override
- final ITrack track;
-
- /// Where should start lavalink playing the track
- @override
- final Duration startTime;
-
- /// If the track should stop playing before finish and where
- @override
- final Duration? endTime;
-
- /// The requester of the track
- @override
- final Snowflake? requester;
-
- /// The channel where this track was requested
- @override
- final Snowflake? channelId;
-
- /// Create a new QueuedTrack instance
- QueuedTrack(this.track, this.startTime, this.endTime, this.requester, this.channelId);
-}
-
-abstract class ITrack {
- /// Base64 encoded track
- String get track;
-
- /// Optional information about the track
- ITrackInfo? get info;
-}
-
-/// Lavalink track object
-class Track implements ITrack {
- /// Base64 encoded track
- @override
- late final String track;
-
- /// Optional information about the track
- @override
- late final ITrackInfo? info;
-
- /// Create a new track instance
- Track(this.track, this.info);
-
- Track.fromJson(Map json) {
- if (json.containsKey("info")) {
- info = TrackInfo(json["info"] as Map);
- }
-
- track = json["track"] as String;
- }
-}
-
-abstract class ITrackInfo {
- /// Track identifier
- String get identifier;
-
- /// If the track is seekable (if it's a streaming it's not)
- bool get seekable;
-
- /// The author of the track
- String get author;
-
- /// The length of the track
- int get length;
-
- /// Whether the track is a streaming or not
- bool get stream;
-
- /// Position returned by lavalink
- int get position;
-
- /// The title of the track
- String get title;
-
- /// Url of the track
- String get uri;
-}
-
-/// Track details
-class TrackInfo implements ITrackInfo {
- /// Track identifier
- @override
- late final String identifier;
-
- /// If the track is seekable (if it's a streaming it's not)
- @override
- late final bool seekable;
-
- /// The author of the track
- @override
- late final String author;
-
- /// The length of the track
- @override
- late final int length;
-
- /// Whether the track is a streaming or not
- @override
- late final bool stream;
-
- /// Position returned by lavalink
- @override
- late int position;
-
- /// The title of the track
- @override
- late final String title;
-
- /// Url of the track
- @override
- late final String uri;
-
- TrackInfo(Map json) {
- identifier = json["identifier"] as String;
- seekable = json["isSeekable"] as bool;
- author = json["author"] as String;
- length = json["length"] as int;
- stream = json["isStream"] as bool;
- position = json["position"] as int;
- title = json["title"] as String;
- uri = json["uri"] as String;
- }
-}
-
-abstract class IPlaylistInfo {
- /// Name of the playlist
- String? get name;
-
- /// Currently selected track
- int? get selectedTrack;
-}
-
-/// Playlist info
-class PlaylistInfo implements IPlaylistInfo {
- /// Name of the playlist
- @override
- late final String? name;
-
- /// Currently selected track
- @override
- late final int? selectedTrack;
-
- PlaylistInfo(Map json) {
- name = json["name"] as String?;
- selectedTrack = json["selectedTrack"] as int?;
- }
-}
-
-abstract class ITracks {
- /// Information about loaded playlist
- IPlaylistInfo get playlistInfo;
-
- /// Load type (track, playlist, etc)
- String get loadType;
-
- /// Loaded tracks
- List get tracks;
-
- /// Occurred exception (if occurred)
- ILavalinkException? get exception;
-}
-
-/// Object returned from lavalink when searching
-class Tracks implements ITracks {
- /// Information about loaded playlist
- @override
- late final IPlaylistInfo playlistInfo;
-
- /// Load type (track, playlist, etc)
- @override
- late final String loadType;
-
- /// Loaded tracks
- @override
- late final List tracks;
-
- /// Occurred exception (if occurred)
- @override
- late final ILavalinkException? exception;
-
- Tracks(Map json) {
- playlistInfo = PlaylistInfo(json["playlistInfo"] as Map);
- loadType = json["loadType"] as String;
- tracks = (json["tracks"] as List).map((t) => Track.fromJson(t as Map)).toList();
- exception = json["exception"] == null ? null : LavalinkException(json["exception"] as Map);
- }
-}
diff --git a/lib/src/model/track_end.dart b/lib/src/model/track_end.dart
deleted file mode 100644
index 212b3f1..0000000
--- a/lib/src/model/track_end.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class ITrackEndEvent implements IBaseEvent {
- /// Reason to the track to end
- String get reason;
-
- /// Base64 encoded track
- String get track;
-
- /// Guild where the track ended
- Snowflake get guildId;
-}
-
-/// Object sent when a track ends playing
-class TrackEndEvent extends BaseEvent implements ITrackEndEvent {
- /// Reason to the track to end
- @override
- late final String reason;
-
- /// Base64 encoded track
- @override
- late final String track;
-
- /// Guild where the track ended
- @override
- late final Snowflake guildId;
-
- TrackEndEvent(INyxx client, INode node, Map json) : super(client, node) {
- reason = json["reason"] as String;
- track = json["track"] as String;
- guildId = Snowflake(json["guildId"] as String);
- }
-}
diff --git a/lib/src/model/track_exception.dart b/lib/src/model/track_exception.dart
deleted file mode 100644
index 1da6bc2..0000000
--- a/lib/src/model/track_exception.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/model/exception.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class ITrackExceptionEvent implements IBaseEvent {
- /// Base64 encoded track
- String get track;
-
- /// The occurred error
- ILavalinkException get exception;
-
- /// Guild id where the track got an exception
- Snowflake get guildId;
-}
-
-/// Object sent when a track gets an exception while playing
-class TrackExceptionEvent extends BaseEvent implements ITrackExceptionEvent {
- /// Base64 encoded track
- @override
- late final String track;
-
- /// The occurred error
- @override
- late final ILavalinkException exception;
-
- /// Guild id where the track got an exception
- @override
- late final Snowflake guildId;
-
- TrackExceptionEvent(INyxx client, INode node, Map json) : super(client, node) {
- track = json["track"] as String;
- exception = LavalinkException(json);
- guildId = Snowflake(json["guildId"]);
- }
-}
diff --git a/lib/src/model/track_start.dart b/lib/src/model/track_start.dart
deleted file mode 100644
index 7b15eda..0000000
--- a/lib/src/model/track_start.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class ITrackStartEvent implements IBaseEvent {
- /// Track start type (if its replaced or not the track)
- String get startType;
-
- /// Base64 encoded track
- String get track;
-
- /// Guild where the track started
- Snowflake get guildId;
-}
-
-/// Object sent when a track starts playing
-class TrackStartEvent extends BaseEvent implements ITrackStartEvent {
- /// Track start type (if its replaced or not the track)
- @override
- late final String startType;
-
- /// Base64 encoded track
- @override
- late final String track;
-
- /// Guild where the track started
- @override
- late final Snowflake guildId;
-
- TrackStartEvent(INyxx client, INode node, Map json) : super(client, node) {
- startType = json["type"] as String;
- track = json["track"] as String;
- guildId = Snowflake(json["guildId"]);
- }
-}
diff --git a/lib/src/model/track_stuck.dart b/lib/src/model/track_stuck.dart
deleted file mode 100644
index 281c4d0..0000000
--- a/lib/src/model/track_stuck.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class ITrackStuckEvent implements IBaseEvent {
- /// Base64 encoded track
- String get track;
-
- /// The wait threshold that was exceeded for this event to trigger
- int get thresholdMs;
-
- /// Guild where the track got stuck
- Snowflake get guildId;
-}
-
-/// Object sent when a track gets stuck when playing
-class TrackStuckEvent extends BaseEvent implements ITrackStuckEvent {
- /// Base64 encoded track
- @override
- late final String track;
-
- /// The wait threshold that was exceeded for this event to trigger
- @override
- late final int thresholdMs;
-
- /// Guild where the track got stuck
- @override
- late final Snowflake guildId;
-
- TrackStuckEvent(INyxx client, INode node, Map json) : super(client, node) {
- track = json["track"] as String;
- thresholdMs = json["thresholdMs"] as int;
- guildId = Snowflake(json["guildId"] as String);
- }
-}
diff --git a/lib/src/model/websocket_closed.dart b/lib/src/model/websocket_closed.dart
deleted file mode 100644
index d95559a..0000000
--- a/lib/src/model/websocket_closed.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-import 'package:nyxx_lavalink/src/model/base_event.dart';
-import 'package:nyxx_lavalink/src/node/node.dart';
-
-abstract class IWebSocketClosedEvent implements IBaseEvent {
- /// Guild where the websocket has closed
- Snowflake get guildId;
-
- /// Close code
- int get code;
-
- /// Reason why the socket closed
- String get reason;
-
- /// If the connection was closed by discord
- bool get byRemote;
-}
-
-/// Web socket closed event from lavalink
-class WebSocketClosedEvent extends BaseEvent implements IWebSocketClosedEvent {
- /// Guild where the websocket has closed
- @override
- late final Snowflake guildId;
-
- /// Close code
- @override
- late final int code;
-
- /// Reason why the socket closed
- @override
- late final String reason;
-
- /// If the connection was closed by discord
- @override
- late final bool byRemote;
-
- WebSocketClosedEvent(INyxx client, INode node, Map json) : super(client, node) {
- guildId = Snowflake(json["guildId"]);
- code = json["code"] as int;
- reason = json["reason"] as String;
- byRemote = json["byRemote"] as bool;
- }
-}
diff --git a/lib/src/node/node.dart b/lib/src/node/node.dart
deleted file mode 100644
index cadc38b..0000000
--- a/lib/src/node/node.dart
+++ /dev/null
@@ -1,344 +0,0 @@
-import 'dart:collection';
-import 'dart:convert';
-import 'dart:isolate';
-
-import 'package:http/http.dart';
-import 'package:nyxx/nyxx.dart';
-import 'package:nyxx_lavalink/src/http_exception.dart';
-import 'package:nyxx_lavalink/src/model/play_parameters.dart';
-import 'package:nyxx_lavalink/src/model/search_platform.dart';
-import 'package:nyxx_lavalink/src/model/track_end.dart';
-import 'package:nyxx_lavalink/src/node/node_options.dart';
-import 'package:nyxx_lavalink/src/model/guild_player.dart';
-import 'package:nyxx_lavalink/src/model/stats.dart';
-import 'package:nyxx_lavalink/src/model/track.dart';
-import 'package:nyxx_lavalink/src/cluster.dart';
-
-abstract class INode {
- /// Node options, such as host, port, etc..
- NodeOptions get options;
-
- /// Returns a map with all the players the node currently has
- UnmodifiableMapView get players;
-
- /// Returns the last stats received by this node
- IStatsEvent? get stats;
-
- /// Destroys a player
- void destroy(Snowflake guildId);
-
- /// Stops a player
- void stop(Snowflake guildId);
-
- /// Skips a track, starting the next one if available or stopping the player if not
- void skip(Snowflake guildId);
-
- /// Set the pause state of a player
- ///
- /// this method is internally used by [resume] and [pause]
- void setPause(Snowflake guildId, bool pauseState);
-
- /// Seeks for a given time at the currently playing track
- void seek(Snowflake guildId, Duration time);
-
- /// Sets the volume for a guild player, [volume] should be a number between 1 to 1000
- void volume(Snowflake guildId, int volume);
-
- /// Pauses a guild player
- void pause(Snowflake guildId);
-
- /// Resumes the track playback of a guild player
- void resume(Snowflake guildId);
-
- /// Clears all the players this node handles
- void clearPlayers();
-
- /// Searches a given query over the lavalink api and returns the results
- Future searchTracks(String query);
-
- /// Searches a provided query on selected platform (YouTube by default),
- /// if the query is a link it's searched directly by the link
- Future autoSearch(
- String query, {
- SearchPlatform platform = SearchPlatform.youtube,
- });
-
- /// Get the [PlayParameters] object for a specific track
- IPlayParameters play(Snowflake guildId, ITrack track,
- {bool replace = false, Duration startTime = const Duration(), Duration? endTime, Snowflake? requester, Snowflake? channelId});
-
- /// Shuts down the node
- void shutdown();
-
- /// Create a new player for a specific guild
- IGuildPlayer createPlayer(Snowflake guildId);
-
- /// Updates the [NodeOptions] property of the node, also reconnects the
- /// websocket to the new options
- void updateOptions(NodeOptions newOptions);
-
- /// Tells the node to disconnect from lavalink server
- void disconnect();
-
- /// Tells the node to reconnect to lavalink server
- void reconnect();
-}
-
-/// Represents an active and running lavalink node
-class Node implements INode {
- /// Node options, such as host, port, etc..
- @override
- NodeOptions options;
-
- /// A map with guild ids as keys and players as values
- final Map _players = {};
-
- /// Returns a map with all the players the node currently has
- @override
- UnmodifiableMapView get players => UnmodifiableMapView(_players);
-
- /// Returns the last stats received by this node
- @override
- IStatsEvent? stats;
-
- /// Http client used with this node
- final Client _httpClient = Client();
-
- final SendPort _nodeSendPort;
- late String _httpUri;
- late Map _defaultHeaders;
- final ICluster _cluster;
-
- /// A regular expression to avoid searching when a link is provided
- final RegExp _urlRegex = RegExp(r"https?://(?:www\.)?.+");
-
- /// Build a new Node
- Node.fromOptions(this._cluster, this.options, this._nodeSendPort) {
- _httpUri = options.ssl ? "https://${options.host}:${options.port}" : "http://${options.host}:${options.port}";
-
- _defaultHeaders = {"Authorization": options.password, "Num-Shards": options.shards.toString(), "User-Id": options.clientId.toString()};
- }
-
- /// Clears all the players this node handles
- @override
- void clearPlayers() {
- _players.clear();
- }
-
- void sendPayload(String op, Snowflake guildId, [Map? data]) async {
- if (data == null) {
- _nodeSendPort.send({
- "cmd": "SEND",
- "data": {
- "op": op,
- "guildId": guildId.toString(),
- }
- });
- } else {
- _nodeSendPort.send({
- "cmd": "SEND",
- "data": {"op": op, "guildId": guildId.toString(), ...data}
- });
- }
- }
-
- void playNext(Snowflake guildId) async {
- final player = _players[guildId];
-
- if (player == null) {
- return;
- }
-
- final track = player.queue.first;
-
- player.nowPlaying = track;
-
- if (track.endTime == null) {
- sendPayload("play", guildId, {
- "track": track.track.track,
- "noReplace": false,
- "startTime": track.startTime.inMilliseconds,
- });
- } else {
- sendPayload("play", guildId,
- {"track": track.track.track, "noReplace": false, "startTime": track.startTime.inMilliseconds, "endTime": track.endTime!.inMilliseconds});
- }
- }
-
- void handleTrackEnd(ITrackEndEvent event) {
- if (!(event.reason == "FINISHED")) {
- return;
- }
-
- final player = _players[event.guildId];
-
- if (player == null) {
- return;
- }
-
- player.queue.removeAt(0);
- player.nowPlaying = null;
-
- if (player.queue.isEmpty) {
- return;
- }
-
- playNext(event.guildId);
- }
-
- /// Destroys a player
- @override
- void destroy(Snowflake guildId) {
- sendPayload("destroy", guildId);
-
- // delete the actual player
- _players.remove(guildId);
-
- // delete the relationship between this node and the player so
- // if this guild creates a new player, it can be assigned to other node
- (_cluster as Cluster).nodeLocations.remove(guildId);
- }
-
- /// Stops a player
- @override
- void stop(Snowflake guildId) {
- final player = _players[guildId];
-
- if (player == null) {
- return;
- }
-
- player.queue.clear();
- player.nowPlaying = null;
-
- sendPayload("stop", guildId);
- }
-
- /// Skips a track, starting the next one if available or stopping the player if not
- @override
- void skip(Snowflake guildId) {
- final player = _players[guildId];
-
- if (player == null) {
- return;
- }
-
- if (player.queue.isEmpty) {
- return;
- } else if (player.queue.length == 1) {
- stop(guildId);
- return;
- } else {
- player.queue.removeAt(0);
- playNext(guildId);
- }
- }
-
- /// Set the pause state of a player
- ///
- /// this method is internally used by [resume] and [pause]
- @override
- void setPause(Snowflake guildId, bool pauseState) {
- sendPayload("pause", guildId, {"pause": pauseState});
- }
-
- /// Seeks for a given time at the currently playing track
- @override
- void seek(Snowflake guildId, Duration time) {
- sendPayload("seek", guildId, {"position": time.inMilliseconds});
- }
-
- /// Sets the volume for a guild player, [volume] should be a number between 1 to 1000
- @override
- void volume(Snowflake guildId, int volume) {
- final trimmed = volume.clamp(0, 1000);
-
- sendPayload("volume", guildId, {"volume": trimmed});
- }
-
- /// Pauses a guild player
- @override
- void pause(Snowflake guildId) {
- setPause(guildId, true);
- }
-
- /// Resumes the track playback of a guild player
- @override
- void resume(Snowflake guildId) {
- setPause(guildId, false);
- }
-
- /// Searches a given query over the lavalink api and returns the results
- @override
- Future searchTracks(String query) async {
- final response = await _httpClient.get(Uri.parse("$_httpUri/loadtracks?identifier=$query"), headers: _defaultHeaders);
-
- if (!(response.statusCode == 200)) {
- throw HttpException(response.statusCode);
- }
-
- return Tracks(jsonDecode(utf8.decode(response.bodyBytes)) as Map);
- }
-
- /// Searches a provided query on selected platform (YouTube by default),
- /// if the query is a link it's searched directly by the link
- @override
- Future autoSearch(
- String query, {
- SearchPlatform platform = SearchPlatform.youtube,
- }) async {
- if (_urlRegex.hasMatch(query)) {
- return searchTracks(query);
- }
-
- return searchTracks("${platform.value}:$query");
- }
-
- /// Get the [PlayParameters] object for a specific track
- @override
- IPlayParameters play(Snowflake guildId, ITrack track,
- {bool replace = false, Duration startTime = const Duration(), Duration? endTime, Snowflake? requester, Snowflake? channelId}) =>
- PlayParameters(this, track, guildId, replace, startTime, endTime, requester, channelId);
-
- /// Shuts down the node
- @override
- void shutdown() {
- _nodeSendPort.send({"cmd": "SHUTDOWN"});
- }
-
- /// Create a new player for a specific guild
- @override
- IGuildPlayer createPlayer(Snowflake guildId) {
- final player = GuildPlayer(this, guildId);
-
- _players[guildId] = player;
- (_cluster as Cluster).nodeLocations[guildId] = options.nodeId;
-
- return player;
- }
-
- /// Updates the [NodeOptions] property of the node, also reconnects the
- /// websocket to the new options
- @override
- void updateOptions(NodeOptions newOptions) {
- // Set the node id and client id before sending it to the isolate
- newOptions.clientId = options.clientId;
- newOptions.nodeId = options.nodeId;
-
- _nodeSendPort.send({"cmd": "UPDATE", "data": newOptions.toJson()});
-
- options = newOptions;
- }
-
- /// Tells the node to disconnect from lavalink server
- @override
- void disconnect() {
- _nodeSendPort.send({"cmd": "DISCONNECT"});
- }
-
- /// Tells the node to reconnect to lavalink server
- @override
- void reconnect() {
- _nodeSendPort.send({"cmd": "RECONNECT"});
- }
-}
diff --git a/lib/src/node/node_options.dart b/lib/src/node/node_options.dart
deleted file mode 100644
index ef87fbd..0000000
--- a/lib/src/node/node_options.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'package:nyxx/nyxx.dart';
-
-/// Class containing all node options needed to establish and mantain a connection
-/// with lavalink server
-class NodeOptions {
- /// Host where lavalink is running
- late final String host;
-
- /// Port used by lavalink rest & socket
- late final int port;
-
- /// Whether to use a tls connection or not
- late final bool ssl;
-
- /// Password to connect to the server
- late final String password;
-
- /// Shards the bot is operating on
- late final int shards;
-
- /// Max connect attempts before shutting down a node
- late final int maxConnectAttempts;
-
- /// How much time should the node wait before trying to reconnect
- /// to lavalink server again
- late final Duration delayBetweenReconnections;
-
- /// Client id
- late final Snowflake clientId;
-
- /// Node id, you **must** not set this yourself
- late final int nodeId;
-
- late final String clientName;
-
- /// Constructor to build a new node builder
- NodeOptions(
- {this.host = "localhost",
- this.port = 2333,
- this.ssl = false,
- this.password = "youshallnotpass",
- this.shards = 1,
- this.maxConnectAttempts = 5,
- this.delayBetweenReconnections = const Duration(seconds: 5),
- this.clientName = "nyxx_lavalink"});
-
- NodeOptions.fromJson(Map json) {
- host = json["host"] as String;
- port = json["port"] as int;
- ssl = json["ssl"] as bool;
- password = json["password"] as String;
- shards = json["shards"] as int;
- clientId = Snowflake(json["clientId"] as int);
- nodeId = json["nodeId"] as int;
- maxConnectAttempts = json["maxConnectAttempts"] as int;
- delayBetweenReconnections = Duration(milliseconds: json["delayBetweenReconnections"] as int);
- clientName = json["clientName"] as String;
- }
-
- Map toJson() => {
- "host": host,
- "port": port,
- "ssl": ssl,
- "password": password,
- "shards": shards,
- "clientId": clientId.id,
- "nodeId": nodeId,
- "maxConnectAttempts": maxConnectAttempts,
- "delayBetweenReconnections": delayBetweenReconnections.inMilliseconds,
- "clientName": clientName
- };
-}
diff --git a/lib/src/node/node_runner.dart b/lib/src/node/node_runner.dart
deleted file mode 100644
index 03f05c3..0000000
--- a/lib/src/node/node_runner.dart
+++ /dev/null
@@ -1,155 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
-
-import 'dart:isolate';
-import 'node_options.dart';
-
-/*
- The actual node runner
- Following nyxx design, the node communicates with the cluster using json
- First message will always be the [NodeOptions] data
-
- Can receive:
- * SEND - Sends a given json payload directly to the server through the web socket
- * CONNECT - Starts the connection to lavalink server
- * UPDATE - Updates the current node data
- * RECONNECT - Reconnects to lavalink server
- * DISCONNECT - Disconnects from lavalink server
- * SHUTDOWN - Shuts down the node and kills the isolate
-
- Can send:
- * DISPATCH - Dispatch the given event
- * DISCONNECTED - WebSocket disconnected
- * CONNECTED - WebSocket connected
- * ERROR - An error occurred
- * EXITED - Node shutdown itself
- * LOG - Log something
-*/
-Future handleNode(SendPort clusterPort) async {
- WebSocket? socket;
- StreamSubscription? socketStream;
-
- // First thing to do is to return a send port to the cluster to communicate with the node
- final receivePort = ReceivePort();
- final receiveStream = receivePort.asBroadcastStream();
- clusterPort.send(receivePort.sendPort);
-
- var node = NodeOptions.fromJson(await receiveStream.first as Map);
-
- void process(Map json) {
- if (json["op"] == "event") {
- clusterPort.send({"cmd": "DISPATCH", "nodeId": node.nodeId, "event": json["type"], "data": json});
- } else {
- clusterPort.send({"cmd": "DISPATCH", "nodeId": node.nodeId, "event": json["op"], "data": json});
- }
- }
-
- Future connect() async {
- final address = node.ssl ? "wss://${node.host}:${node.port}" : "ws://${node.host}:${node.port}";
- var actualAttempt = 1;
-
- while (actualAttempt <= node.maxConnectAttempts) {
- try {
- clusterPort.send({
- "cmd": "LOG",
- "nodeId": node.nodeId,
- "level": "INFO",
- "message": "[Node ${node.nodeId}] Trying to connect to lavalink ($actualAttempt/${node.maxConnectAttempts})"
- });
-
- await WebSocket.connect(address, headers: {
- "Authorization": node.password,
- "Num-Shards": node.shards,
- "User-Id": node.clientId.id,
- "Client-Name": node.clientName,
- }).then((ws) {
- clusterPort.send({"cmd": "CONNECTED", "nodeId": node.nodeId});
-
- socket = ws;
-
- socketStream = socket!.listen(
- (data) {
- process(jsonDecode(data as String) as Map);
- },
- onDone: () async {
- clusterPort.send({"cmd": "DISCONNECTED", "nodeId": node.nodeId});
- await connect();
-
- return;
- },
- cancelOnError: true,
- onError: (err) {
- clusterPort.send({"cmd": "ERROR", "nodeId": node.nodeId, "code": socket!.closeCode, "reason": socket!.closeReason});
- });
-
- return;
- });
-
- return;
- // ignore: avoid_catches_without_on_clauses
- } catch (e) {
- clusterPort
- .send({"cmd": "LOG", "nodeId": node.nodeId, "level": "WARNING", "message": "[Node ${node.nodeId}] Error while trying to connect to lavalink; $e"});
- }
-
- clusterPort.send({"cmd": "LOG", "nodeId": node.nodeId, "level": "WARNING", "message": "[Node ${node.nodeId}] Failed to connect to lavalink, retrying"});
-
- actualAttempt += 1;
-
- await Future.delayed(node.delayBetweenReconnections);
- }
-
- clusterPort.send({"cmd": "EXITED", "nodeId": node.nodeId});
- }
-
- Future disconnect() async {
- await socket?.close(1000);
- await socketStream?.cancel();
-
- clusterPort.send({"cmd": "DISCONNECTED", "nodeId": node.nodeId});
- }
-
- Future reconnect() async {
- await disconnect();
- await Future.delayed(const Duration(milliseconds: 300));
- await connect();
- }
-
- await for (final msg in receiveStream) {
- switch (msg["cmd"]) {
- case "SEND":
- socket?.add(jsonEncode(msg["data"]));
- break;
-
- case "CONNECT":
- await connect();
- break;
-
- case "UPDATE":
- node = NodeOptions.fromJson(msg["data"] as Map);
- await reconnect();
- break;
-
- case "DISCONNECT":
- await disconnect();
- break;
-
- case "RECONNECT":
- await reconnect();
- break;
-
- case "SHUTDOWN":
- {
- clusterPort.send({"cmd": "EXITED", "nodeId": node.nodeId});
- await disconnect();
- receivePort.close();
- Isolate.current.kill(priority: Isolate.immediate);
- }
- break;
-
- default:
- break;
- }
- }
-}
diff --git a/packages/lavalink/CHANGELOG.md b/packages/lavalink/CHANGELOG.md
new file mode 100644
index 0000000..effe43c
--- /dev/null
+++ b/packages/lavalink/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/LICENSE b/packages/lavalink/LICENSE
similarity index 100%
rename from LICENSE
rename to packages/lavalink/LICENSE
diff --git a/packages/lavalink/README.md b/packages/lavalink/README.md
new file mode 100644
index 0000000..8ffdc9b
--- /dev/null
+++ b/packages/lavalink/README.md
@@ -0,0 +1,5 @@
+## lavalink
+
+A dart wrapper for the [Lavalink API](https://github.com/lavalink-devs/Lavalink).
+
+If you're looking to create a Discord bot that uses Lavalink, check out [nyxx](https://pub.dev/packages/nyxx) and [nyxx_lavalink](https://pub.dev/packages/nyxx_lavalink), which provide access to the Discord and Lavalink APIs in a unified way.
diff --git a/packages/lavalink/analysis_options.yaml b/packages/lavalink/analysis_options.yaml
new file mode 100644
index 0000000..dee8927
--- /dev/null
+++ b/packages/lavalink/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+# Uncomment the following section to specify additional rules.
+
+# linter:
+# rules:
+# - camel_case_types
+
+# analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# For more information about the core and recommended set of lints, see
+# https://dart.dev/go/core-lints
+
+# For additional information about configuring this file, see
+# https://dart.dev/guides/language/analysis-options
diff --git a/packages/lavalink/build.yaml b/packages/lavalink/build.yaml
new file mode 100644
index 0000000..2024e49
--- /dev/null
+++ b/packages/lavalink/build.yaml
@@ -0,0 +1,7 @@
+targets:
+ $default:
+ builders:
+ json_serializable:
+ options:
+ field_rename: none
+ create_to_json: false
diff --git a/packages/lavalink/example/example.dart b/packages/lavalink/example/example.dart
new file mode 100644
index 0000000..75c717a
--- /dev/null
+++ b/packages/lavalink/example/example.dart
@@ -0,0 +1,13 @@
+import 'package:lavalink/lavalink.dart';
+
+void main() async {
+ final client = await LavalinkClient.connect(
+ Uri.http('localhost:2333'),
+ password: 'youshallnotpass',
+ userId: '1',
+ );
+
+ print(await client.getVersion());
+
+ await client.close();
+}
diff --git a/packages/lavalink/lib/lavalink.dart b/packages/lavalink/lib/lavalink.dart
new file mode 100644
index 0000000..3210185
--- /dev/null
+++ b/packages/lavalink/lib/lavalink.dart
@@ -0,0 +1,24 @@
+/// A wrapper for the Lavalink API.
+library;
+
+export 'src/errors.dart';
+export 'src/connection.dart';
+export 'src/client.dart';
+export 'src/models/track.dart';
+export 'src/models/stats.dart';
+export 'src/models/route_planner.dart';
+export 'src/models/player.dart';
+export 'src/models/player_state.dart';
+export 'src/models/loaded_track_result.dart';
+export 'src/models/info.dart';
+export 'src/models/filters.dart';
+export 'src/messages/stats.dart';
+export 'src/messages/ready.dart';
+export 'src/messages/player_update.dart';
+export 'src/messages/message.dart';
+export 'src/messages/event.dart';
+export 'src/messages/events/websocket_closed.dart';
+export 'src/messages/events/track_stuck.dart';
+export 'src/messages/events/track_start.dart';
+export 'src/messages/events/track_exception.dart';
+export 'src/messages/events/track_end.dart';
diff --git a/packages/lavalink/lib/src/client.dart b/packages/lavalink/lib/src/client.dart
new file mode 100644
index 0000000..c060526
--- /dev/null
+++ b/packages/lavalink/lib/src/client.dart
@@ -0,0 +1,272 @@
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:lavalink/src/connection.dart';
+import 'package:lavalink/src/errors.dart';
+import 'package:lavalink/src/models/filters.dart';
+import 'package:lavalink/src/models/info.dart';
+import 'package:lavalink/src/models/loaded_track_result.dart';
+import 'package:lavalink/src/models/player.dart';
+import 'package:lavalink/src/models/route_planner.dart';
+import 'package:lavalink/src/models/stats.dart';
+import 'package:lavalink/src/models/track.dart';
+
+/// A Lavalink client that does not create a session on the server and can only make certain HTTP
+/// requests.
+class HttpLavalinkClient {
+ /// The URI relative to which API routes will be resolved.
+ final Uri base;
+
+ /// The password to use for authentication.
+ final String password;
+
+ /// The name of this client.
+ final String clientName;
+
+ /// The HTTP client used by this client.
+ final http.Client httpClient = http.Client();
+
+ HttpLavalinkClient({
+ required this.base,
+ required this.password,
+ this.clientName = LavalinkClient.defaultClientName,
+ });
+
+ Future _executeSafe(
+ String method,
+ String endpoint, {
+ bool trace = false,
+ Object? body,
+ Map? queryParameters,
+ }) async {
+ // Avoid resetting the path of the base URI
+ if (endpoint.startsWith('/')) endpoint = endpoint.substring(1);
+
+ final uri = base.resolveUri(Uri(
+ path: endpoint,
+ queryParameters: {
+ if (trace) 'trace': 'true',
+ ...?queryParameters,
+ },
+ ));
+
+ final request = http.Request(method, uri)
+ ..headers['Authorization'] = password
+ ..headers['Content-Type'] = 'application/json';
+ if (body != null) request.bodyBytes = utf8.encode(jsonEncode(body));
+
+ final response = await http.Response.fromStream(await httpClient.send(request));
+ final bodyText = utf8.decode(response.bodyBytes);
+
+ if (response.statusCode >= 400) {
+ final parsedBody = jsonDecode(bodyText);
+
+ throw LavalinkException(
+ timestamp: DateTime.fromMicrosecondsSinceEpoch(parsedBody['timestamp'] as int),
+ status: parsedBody['status'] as int,
+ error: parsedBody['error'] as String,
+ trace: parsedBody['trace'] as String?,
+ message: parsedBody['message'] as String,
+ path: parsedBody['path'] as String,
+ );
+ }
+
+ return bodyText;
+ }
+
+ /// Load one or more tracks from an identifier.
+ Future loadTrack(String identifier) async {
+ final response = jsonDecode(await _executeSafe(
+ 'GET',
+ '/v4/loadtracks',
+ queryParameters: {'identifier': identifier},
+ ));
+ return LoadResult.fromJson(response as Map);
+ }
+
+ /// Decode a track from its encoded form.
+ Future