├── .github ├── disgolink.png └── workflows │ └── lint.yml ├── .gitignore ├── LICENSE ├── README.md ├── _examples ├── discordgo │ ├── commands.go │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── main.go │ ├── player_handlers.go │ └── queue.go └── disgo │ ├── bot.go │ ├── commands.go │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── main.go │ ├── player_handlers.go │ └── queue.go ├── disgolink ├── client.go ├── client_config.go ├── event.go ├── info.go ├── load_result.go ├── node.go ├── player.go ├── plugin.go └── rest_client.go ├── go.mod ├── go.sum └── lavalink ├── duration.go ├── duration_test.go ├── error.go ├── filters.go ├── info.go ├── load_result.go ├── messages.go ├── player.go ├── player_update.go ├── playlist.go ├── plugin.go ├── search_type.go ├── session.go ├── stats.go ├── timestamp.go └── track.go /.github/disgolink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disgoorg/disgolink/d10bc670c3daaf59c9df48ec96ac1c1ac9d77f67/.github/disgolink.png -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | gobuild: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/setup-go@v3 10 | with: 11 | go-version: 1.21 12 | - uses: actions/checkout@v3 13 | - name: go build 14 | run: go build -v ./... 15 | 16 | gotest: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.21 22 | - uses: actions/checkout@v3 23 | - name: go build 24 | run: go test -v ./... 25 | 26 | golangci: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/setup-go@v3 30 | with: 31 | go-version: 1.21 32 | - uses: actions/checkout@v3 33 | - name: golangci-lint 34 | uses: golangci/golangci-lint-action@v3 35 | with: 36 | version: latest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | go.work 4 | go.work.sum -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/disgoorg/disgolink.svg)](https://pkg.go.dev/github.com/disgoorg/disgolink) 2 | [![Go Report](https://goreportcard.com/badge/github.com/disgoorg/disgolink/v3)](https://goreportcard.com/report/github.com/disgoorg/disgolink) 3 | [![Go Version](https://img.shields.io/github/go-mod/go-version/disgoorg/disgolink?filename=go.mod)](https://golang.org/doc/devel/release.html) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/disgoorg/disgolink/blob/master/LICENSE) 5 | [![Disgolink Version](https://img.shields.io/github/v/release/disgoorg/disgolink?label=release)](https://github.com/disgoorg/disgolink/releases/latest) 6 | [![Support Discord](https://discord.com/api/guilds/817327181659111454/widget.png)](https://discord.gg/NFmvZYmZMF) 7 | 8 | discord gopher 9 | 10 | # DisGoLink 11 | 12 | DisGoLink is a [Lavalink](https://github.com/freyacodes/Lavalink) Client written in [Golang](https://golang.org/) which supports the latest Lavalink 4.0.0+ release and the new plugin system. 13 | 14 | While DisGoLink can be used with any [Discord](https://discord.com) Library [DisGo](https://github.com/disgoorg/disgo) is the best fit for it as usage with other Libraries can be a bit annoying due to different [Snowflake](https://github.com/disgoorg/snowflake) implementations. 15 | 16 | * [DiscordGo](https://github.com/bwmarrin/discordgo) `string` 17 | * [Arikawa](https://github.com/diamondburned/arikawa) `type Snowflake uint64` 18 | * [Disgord](https://github.com/andersfylling/disgord) `type Snowflake uint64` 19 | * [DisGo](https://github.com/disgoorg/disgo) `type ID uint64` 20 | 21 | This Library uses the [Disgo Snowflake](https://github.com/disgoorg/snowflake) package like DisGo 22 | 23 | ## Getting Started 24 | 25 | ### Installing 26 | 27 | ```sh 28 | go get github.com/disgoorg/disgolink/v3 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Setup 34 | 35 | First create a new lavalink instance. You can do this either with 36 | 37 | ```go 38 | import ( 39 | "github.com/disgoorg/snowflake/v2" 40 | "github.com/disgoorg/disgolink/v3/disgolink" 41 | ) 42 | 43 | var userID = snowflake.ID(1234567890) 44 | lavalinkClient := disgolink.New(userID) 45 | ``` 46 | 47 | You also need to forward the `VOICE_STATE_UPDATE` and `VOICE_SERVER_UPDATE` events to DisGoLink. 48 | Just register an event listener for those events with your library and call `lavalinkClient.OnVoiceStateUpdate` (make sure to only forward your bots voice update event!) and `lavalinkClient.OnVoiceServerUpdate` 49 | 50 | 51 | For DisGo this would look like this 52 | ```go 53 | client, err := disgo.New(Token, 54 | bot.WithEventListenerFunc(b.onVoiceStateUpdate), 55 | bot.WithEventListenerFunc(b.onVoiceServerUpdate), 56 | ) 57 | 58 | func onVoiceStateUpdate(event *events.GuildVoiceStateUpdate) { 59 | // filter all non bot voice state updates out 60 | if event.VoiceState.UserID != client.ApplicationID() { 61 | return 62 | } 63 | lavalinkClient.OnVoiceStateUpdate(context.TODO(), event.VoiceState.GuildID, event.VoiceState.ChannelID, event.VoiceState.SessionID) 64 | } 65 | 66 | func onVoiceServerUpdate(event *events.VoiceServerUpdate) { 67 | lavalinkClient.OnVoiceServerUpdate(context.TODO(), event.GuildID, event.Token, *event.Endpoint) 68 | } 69 | ``` 70 | 71 | Then you add your lavalink nodes. This directly connects to the nodes and is a blocking call 72 | ```go 73 | node, err := lavalinkClient.AddNode(context.TODO(), lavalink.NodeConfig{ 74 | Name: "test", // a unique node name 75 | Address: "localhost:2333", 76 | Password: "youshallnotpass", 77 | Secure: false, // ws or wss 78 | SessionID: "", // only needed if you want to resume a previous lavalink session 79 | }) 80 | ``` 81 | 82 | after this you can play songs from lavalinks supported sources. 83 | 84 | ### Loading a track 85 | 86 | To play a track you first need to resolve the song. For this you need to call the Lavalink rest `loadtracks` endpoint which returns a result with various track instances. Those tracks can then be played. 87 | ```go 88 | query := "ytsearch:Rick Astley - Never Gonna Give You Up" 89 | 90 | var toPlay *lavalink.Track 91 | lavalinkClient.BestNode().LoadTracksHandler(context.TODO(), query, disgolink.NewResultHandler( 92 | func(track lavalink.Track) { 93 | // Loaded a single track 94 | toPlay = &track 95 | }, 96 | func(playlist lavalink.Playlist) { 97 | // Loaded a playlist 98 | }, 99 | func(tracks []lavalink.Track) { 100 | // Loaded a search result 101 | }, 102 | func() { 103 | // nothing matching the query found 104 | }, 105 | func(err error) { 106 | // something went wrong while loading the track 107 | }, 108 | )) 109 | ``` 110 | 111 | ### Playing a track 112 | 113 | To play a track we first need to connect to the voice channel. 114 | Connecting to a voice channel differs with every lib but here are some quick usages with some 115 | ```go 116 | // DisGo 117 | err := client.UpdateVoiceState(context.TODO(), guildID, channelID, false, false) 118 | 119 | // DiscordGo 120 | err := session.ChannelVoiceJoinManual(guildID, channelID, false, false) 121 | ``` 122 | 123 | after this you can get/create your player and play the track 124 | ```go 125 | player := lavalinkClient.Player("guild_id") // This will either return an existing or new player 126 | 127 | // toPlay is from result handler in the example above 128 | err := player.Update(context.TODO(), lavalink.WithTrack(*toPlay)) 129 | ``` 130 | now audio should start playing 131 | 132 | ### Listening for events 133 | 134 | You can listen for following lavalink events 135 | * `PlayerUpdateMessage` Emitted every x seconds (default 5) with the current player state 136 | * `PlayerPause` Emitted when the player is paused 137 | * `PlayerResume` Emitted when the player is resumed 138 | * `TrackStart` Emitted when a track starts playing 139 | * `TrackEnd` Emitted when a track ends 140 | * `TrackException` Emitted when a track throws an exception 141 | * `TrackStuck` Emitted when a track gets stuck 142 | * `WebsocketClosed` Emitted when the voice gateway connection to lavalink is closed 143 | 144 | for this add and event listener for each event to your `Client` instance when you create it or with `Client.AddEventListener` 145 | ```go 146 | lavalinkClient := disgolink.New(userID, 147 | disgolink.WithListenerFunc(onPlayerUpdate), 148 | disgolink.WithListenerFunc(onPlayerPause), 149 | disgolink.WithListenerFunc(onPlayerResume), 150 | disgolink.WithListenerFunc(onTrackStart), 151 | disgolink.WithListenerFunc(onTrackEnd), 152 | disgolink.WithListenerFunc(onTrackException), 153 | disgolink.WithListenerFunc(onTrackStuck), 154 | disgolink.WithListenerFunc(onWebSocketClosed), 155 | ) 156 | 157 | func onPlayerUpdate(player disgolink.Player, event lavalink.PlayerUpdateMessage) { 158 | // do something with the event 159 | } 160 | 161 | func onPlayerPause(player disgolink.Player, event lavalink.PlayerPauseEvent) { 162 | // do something with the event 163 | } 164 | 165 | func onPlayerResume(player disgolink.Player, event lavalink.PlayerResumeEvent) { 166 | // do something with the event 167 | } 168 | 169 | func onTrackStart(player disgolink.Player, event lavalink.TrackStartEvent) { 170 | // do something with the event 171 | } 172 | 173 | func onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) { 174 | // do something with the event 175 | } 176 | 177 | func onTrackException(player disgolink.Player, event lavalink.TrackExceptionEvent) { 178 | // do something with the event 179 | } 180 | 181 | func onTrackStuck(player disgolink.Player, event lavalink.TrackStuckEvent) { 182 | // do something with the event 183 | } 184 | 185 | func onWebSocketClosed(player disgolink.Player, event lavalink.WebSocketClosedEvent) { 186 | // do something with the event 187 | } 188 | ``` 189 | 190 | ### Plugins 191 | 192 | Lavalink added [plugins](https://lavalink.dev/plugins.html) in `v3.5` . DisGoLink exposes a similar API for you to use. With that you can create plugins which require server & client work. 193 | To see what you can do with plugins see [here](disgolink/plugin.go) 194 | 195 | You register plugins when creating the client instance like this 196 | ```go 197 | lavalinkClient := disgolink.New(userID, disgolink.WithPlugins(yourPlugin)) 198 | ``` 199 | 200 | Here is a list of plugins(you can pr your own to here): 201 | * [SponsorBlock](https://github.com/disgoorg/sponsorblock-plugin) support for [Lavalink Sponsorblock-Plugin](https://github.com/topi314/SponsorBlock-Plugin) 202 | * [LavaQueue](https://github.com/disgoorg/lavaqueue-plugin) support for [Lavalink LavaQueue-Plugin](https://github.com/topi314/LavaQueue) 203 | * [LavaSrc](https://github.com/disgoorg/sponsorblock-plugin) support for [Lavalink LavaSrc-Plugin](https://github.com/topi314/LavaSrc) 204 | * [LavaLyrics](https://github.com/disgoorg/lavalyrics-plugin) support for [Lavalink LavaLyrics-Plugin](https://github.com/topi314/LavaLyrics) 205 | * [LavaSearch](https://github.com/disgoorg/lavasearch-plugin) support for [Lavalink LavaSearch-Plugin](https://github.com/topi314/LavaSearch) 206 | 207 | ## Examples 208 | 209 | You can find examples under 210 | * disgo: [_example](https://github.com/disgoorg/disgolink/tree/v2/_examples/disgo) 211 | * discordgo: [_examples](https://github.com/disgoorg/disgolink/tree/v2/_examples/discordgo) 212 | 213 | ## Troubleshooting 214 | 215 | For help feel free to open an issue or reach out on [Discord](https://discord.gg/NFmvZYmZMF) 216 | 217 | ## Contributing 218 | 219 | Contributions are welcomed but for bigger changes please first reach out via [Discord](https://discord.gg/NFmvZYmZMF) or create an issue to discuss your intentions and ideas. 220 | 221 | ## License 222 | 223 | Distributed under the [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/disgoorg/disgolink/blob/master/LICENSE). See LICENSE for more information. 224 | -------------------------------------------------------------------------------- /_examples/discordgo/commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | "github.com/disgoorg/log" 8 | ) 9 | 10 | var commands = []*discordgo.ApplicationCommand{ 11 | { 12 | Name: "play", 13 | Description: "Plays a song", 14 | Options: []*discordgo.ApplicationCommandOption{ 15 | { 16 | Type: discordgo.ApplicationCommandOptionString, 17 | Name: "identifier", 18 | Description: "The song link or search query", 19 | Required: true, 20 | }, 21 | }, 22 | }, 23 | { 24 | Name: "pause", 25 | Description: "Pauses the current song", 26 | }, 27 | { 28 | Name: "now-playing", 29 | Description: "Shows the current playing song", 30 | }, 31 | { 32 | Name: "stop", 33 | Description: "Stops the current song and stops the player", 34 | }, 35 | { 36 | Name: "players", 37 | Description: "Shows all active players", 38 | }, 39 | { 40 | Name: "shuffle", 41 | Description: "Shuffles the current queue", 42 | }, 43 | { 44 | Name: "queue", 45 | Description: "Shows the current queue", 46 | }, 47 | { 48 | Name: "clear-queue", 49 | Description: "Clears the current queue", 50 | }, 51 | { 52 | Name: "queue-type", 53 | Description: "Sets the queue type", 54 | Options: []*discordgo.ApplicationCommandOption{ 55 | { 56 | Type: discordgo.ApplicationCommandOptionString, 57 | Name: "type", 58 | Description: "The queue type", 59 | Required: true, 60 | Choices: []*discordgo.ApplicationCommandOptionChoice{ 61 | { 62 | Name: "default", 63 | Value: "default", 64 | }, 65 | { 66 | Name: "repeat-track", 67 | Value: "repeat-track", 68 | }, 69 | { 70 | Name: "repeat-queue", 71 | Value: "repeat-queue", 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | 79 | func registerCommands(s *discordgo.Session) { 80 | if _, err := s.ApplicationCommandBulkOverwrite(s.State.User.ID, GuildId, commands); err != nil { 81 | log.Warn("Failed to register commands", slog.Any("err", err)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /_examples/discordgo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/disgoorg/disgolink/v3/_examples/discordgo 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.0 6 | 7 | require ( 8 | github.com/bwmarrin/discordgo v0.28.1 9 | github.com/disgoorg/disgolink/v3 v3.0.3 10 | github.com/disgoorg/json v1.2.0 11 | github.com/disgoorg/log v1.2.1 12 | github.com/disgoorg/snowflake/v2 v2.0.3 13 | ) 14 | 15 | require ( 16 | github.com/gorilla/websocket v1.5.3 // indirect 17 | golang.org/x/crypto v0.31.0 // indirect 18 | golang.org/x/sys v0.28.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /_examples/discordgo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= 2 | github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/disgoorg/disgolink/v3 v3.0.3 h1:9eHV5lVAGtPzDHjp3IJJ5xdWcFEjc24VJZWsA1341Wo= 6 | github.com/disgoorg/disgolink/v3 v3.0.3/go.mod h1:34D/dfdfrj08fSjtdKSXYz1TsMcjrX2RKoSpdxG3lHo= 7 | github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= 8 | github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= 9 | github.com/disgoorg/log v1.2.1 h1:kZYAWkUBcGy4LbZcgYtgYu49xNVLy+xG5Uq3yz5VVQs= 10 | github.com/disgoorg/log v1.2.1/go.mod h1:hhQWYTFTnIGzAuFPZyXJEi11IBm9wq+/TVZt/FEwX0o= 11 | github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= 12 | github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= 13 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 14 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 15 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 19 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 21 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 22 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 23 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 24 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 26 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 29 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /_examples/discordgo/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | "github.com/disgoorg/json" 10 | "github.com/disgoorg/snowflake/v2" 11 | 12 | "github.com/disgoorg/disgolink/v3/disgolink" 13 | "github.com/disgoorg/disgolink/v3/lavalink" 14 | ) 15 | 16 | func (b *Bot) shuffle(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 17 | queue := b.Queues.Get(event.GuildID) 18 | if queue == nil { 19 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 20 | Type: discordgo.InteractionResponseChannelMessageWithSource, 21 | Data: &discordgo.InteractionResponseData{ 22 | Content: "No player found", 23 | }, 24 | }) 25 | } 26 | 27 | queue.Shuffle() 28 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 29 | Type: discordgo.InteractionResponseChannelMessageWithSource, 30 | Data: &discordgo.InteractionResponseData{ 31 | Content: "Queue shuffled", 32 | }, 33 | }) 34 | } 35 | 36 | func (b *Bot) queueType(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 37 | queue := b.Queues.Get(event.GuildID) 38 | if queue == nil { 39 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 40 | Type: discordgo.InteractionResponseChannelMessageWithSource, 41 | Data: &discordgo.InteractionResponseData{ 42 | Content: "No player found", 43 | }, 44 | }) 45 | } 46 | 47 | queue.Type = QueueType(data.Options[0].Value.(string)) 48 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 49 | Type: discordgo.InteractionResponseChannelMessageWithSource, 50 | Data: &discordgo.InteractionResponseData{ 51 | Content: fmt.Sprintf("Queue type set to `%s`", queue.Type), 52 | }, 53 | }) 54 | } 55 | 56 | func (b *Bot) clearQueue(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 57 | queue := b.Queues.Get(event.GuildID) 58 | if queue == nil { 59 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 60 | Type: discordgo.InteractionResponseChannelMessageWithSource, 61 | Data: &discordgo.InteractionResponseData{ 62 | Content: "No player found", 63 | }, 64 | }) 65 | } 66 | 67 | queue.Clear() 68 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 69 | Type: discordgo.InteractionResponseChannelMessageWithSource, 70 | Data: &discordgo.InteractionResponseData{ 71 | Content: "Queue cleared", 72 | }, 73 | }) 74 | } 75 | 76 | func (b *Bot) queue(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 77 | queue := b.Queues.Get(event.GuildID) 78 | if queue == nil { 79 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 80 | Type: discordgo.InteractionResponseChannelMessageWithSource, 81 | Data: &discordgo.InteractionResponseData{ 82 | Content: "No player found", 83 | }, 84 | }) 85 | } 86 | 87 | if len(queue.Tracks) == 0 { 88 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 89 | Type: discordgo.InteractionResponseChannelMessageWithSource, 90 | Data: &discordgo.InteractionResponseData{ 91 | Content: "No tracks in queue", 92 | }, 93 | }) 94 | } 95 | 96 | var tracks string 97 | for i, track := range queue.Tracks { 98 | tracks += fmt.Sprintf("%d. [`%s`](<%s>)\n", i+1, track.Info.Title, *track.Info.URI) 99 | } 100 | 101 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 102 | Type: discordgo.InteractionResponseChannelMessageWithSource, 103 | Data: &discordgo.InteractionResponseData{ 104 | Content: fmt.Sprintf("Queue `%s`:\n%s", queue.Type, tracks), 105 | }, 106 | }) 107 | } 108 | 109 | func (b *Bot) pause(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 110 | player := b.Lavalink.ExistingPlayer(snowflake.MustParse(event.GuildID)) 111 | if player == nil { 112 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 113 | Type: discordgo.InteractionResponseChannelMessageWithSource, 114 | Data: &discordgo.InteractionResponseData{ 115 | Content: "No player found", 116 | }, 117 | }) 118 | } 119 | 120 | if err := player.Update(context.TODO(), lavalink.WithPaused(!player.Paused())); err != nil { 121 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 122 | Type: discordgo.InteractionResponseChannelMessageWithSource, 123 | Data: &discordgo.InteractionResponseData{ 124 | Content: fmt.Sprintf("Error while pausing: `%s`", err), 125 | }, 126 | }) 127 | } 128 | 129 | status := "playing" 130 | if player.Paused() { 131 | status = "paused" 132 | } 133 | 134 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 135 | Type: discordgo.InteractionResponseChannelMessageWithSource, 136 | Data: &discordgo.InteractionResponseData{ 137 | Content: fmt.Sprintf("Player is now %s", status), 138 | }, 139 | }) 140 | } 141 | 142 | func (b *Bot) stop(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 143 | player := b.Lavalink.ExistingPlayer(snowflake.MustParse(event.GuildID)) 144 | if player == nil { 145 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 146 | Type: discordgo.InteractionResponseChannelMessageWithSource, 147 | Data: &discordgo.InteractionResponseData{ 148 | Content: "No player found", 149 | }, 150 | }) 151 | } 152 | 153 | if err := b.Session.ChannelVoiceJoinManual(event.GuildID, "", false, false); err != nil { 154 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 155 | Type: discordgo.InteractionResponseChannelMessageWithSource, 156 | Data: &discordgo.InteractionResponseData{ 157 | Content: fmt.Sprintf("Error while disconnecting: `%s`", err), 158 | }, 159 | }) 160 | } 161 | 162 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 163 | Type: discordgo.InteractionResponseChannelMessageWithSource, 164 | Data: &discordgo.InteractionResponseData{ 165 | Content: "Player stopped", 166 | }, 167 | }) 168 | } 169 | 170 | func (b *Bot) nowPlaying(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 171 | player := b.Lavalink.ExistingPlayer(snowflake.MustParse(event.GuildID)) 172 | if player == nil { 173 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 174 | Type: discordgo.InteractionResponseChannelMessageWithSource, 175 | Data: &discordgo.InteractionResponseData{ 176 | Content: "No player found", 177 | }, 178 | }) 179 | } 180 | 181 | track := player.Track() 182 | if track == nil { 183 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 184 | Type: discordgo.InteractionResponseChannelMessageWithSource, 185 | Data: &discordgo.InteractionResponseData{ 186 | Content: "No track found", 187 | }, 188 | }) 189 | } 190 | 191 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 192 | Type: discordgo.InteractionResponseChannelMessageWithSource, 193 | Data: &discordgo.InteractionResponseData{ 194 | Content: fmt.Sprintf("Now playing: [`%s`](<%s>)\n\n %s / %s", track.Info.Title, *track.Info.URI, formatPosition(player.Position()), formatPosition(track.Info.Length)), 195 | }, 196 | }) 197 | } 198 | 199 | func formatPosition(position lavalink.Duration) string { 200 | if position == 0 { 201 | return "0:00" 202 | } 203 | return fmt.Sprintf("%d:%02d", position.Minutes(), position.SecondsPart()) 204 | } 205 | 206 | func (b *Bot) play(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error { 207 | identifier := data.Options[0].StringValue() 208 | if !urlPattern.MatchString(identifier) && !searchPattern.MatchString(identifier) { 209 | identifier = lavalink.SearchTypeYouTube.Apply(identifier) 210 | } 211 | 212 | voiceState, err := b.Session.State.VoiceState(event.GuildID, event.Member.User.ID) 213 | if err != nil { 214 | return b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 215 | Type: discordgo.InteractionResponseChannelMessageWithSource, 216 | Data: &discordgo.InteractionResponseData{ 217 | Content: fmt.Sprintf("Error while getting voice state: `%s`", err), 218 | }, 219 | }) 220 | } 221 | 222 | if err := b.Session.InteractionRespond(event.Interaction, &discordgo.InteractionResponse{ 223 | Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, 224 | }); err != nil { 225 | return err 226 | } 227 | 228 | player := b.Lavalink.Player(snowflake.MustParse(event.GuildID)) 229 | queue := b.Queues.Get(event.GuildID) 230 | 231 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 232 | defer cancel() 233 | 234 | var toPlay *lavalink.Track 235 | b.Lavalink.BestNode().LoadTracksHandler(ctx, identifier, disgolink.NewResultHandler( 236 | func(track lavalink.Track) { 237 | _, _ = b.Session.InteractionResponseEdit(event.Interaction, &discordgo.WebhookEdit{ 238 | Content: json.Ptr(fmt.Sprintf("Loading track: [`%s`](<%s>)", track.Info.Title, *track.Info.URI)), 239 | }) 240 | if player.Track() == nil { 241 | toPlay = &track 242 | } else { 243 | queue.Add(track) 244 | } 245 | }, 246 | func(playlist lavalink.Playlist) { 247 | _, _ = b.Session.InteractionResponseEdit(event.Interaction, &discordgo.WebhookEdit{ 248 | Content: json.Ptr(fmt.Sprintf("Loaded playlist: `%s` with `%d` tracks", playlist.Info.Name, len(playlist.Tracks))), 249 | }) 250 | if player.Track() == nil { 251 | toPlay = &playlist.Tracks[0] 252 | queue.Add(playlist.Tracks[1:]...) 253 | } else { 254 | queue.Add(playlist.Tracks...) 255 | } 256 | }, 257 | func(tracks []lavalink.Track) { 258 | _, _ = b.Session.InteractionResponseEdit(event.Interaction, &discordgo.WebhookEdit{ 259 | Content: json.Ptr(fmt.Sprintf("Loaded search result: [`%s`](<%s>)", tracks[0].Info.Title, *tracks[0].Info.URI)), 260 | }) 261 | if player.Track() == nil { 262 | toPlay = &tracks[0] 263 | } else { 264 | queue.Add(tracks[0]) 265 | } 266 | }, 267 | func() { 268 | _, _ = b.Session.InteractionResponseEdit(event.Interaction, &discordgo.WebhookEdit{ 269 | Content: json.Ptr(fmt.Sprintf("Nothing found for: `%s`", identifier)), 270 | }) 271 | }, 272 | func(err error) { 273 | _, _ = b.Session.InteractionResponseEdit(event.Interaction, &discordgo.WebhookEdit{ 274 | Content: json.Ptr(fmt.Sprintf("Error while looking up query: `%s`", err)), 275 | }) 276 | }, 277 | )) 278 | if toPlay == nil { 279 | return nil 280 | } 281 | 282 | if err := b.Session.ChannelVoiceJoinManual(event.GuildID, voiceState.ChannelID, false, false); err != nil { 283 | return err 284 | } 285 | 286 | return player.Update(context.TODO(), lavalink.WithTrack(*toPlay)) 287 | } 288 | -------------------------------------------------------------------------------- /_examples/discordgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "regexp" 8 | "strconv" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/bwmarrin/discordgo" 13 | "github.com/disgoorg/snowflake/v2" 14 | 15 | "github.com/disgoorg/log" 16 | 17 | "github.com/disgoorg/disgolink/v3/disgolink" 18 | ) 19 | 20 | var ( 21 | urlPattern = regexp.MustCompile("^https?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]?") 22 | searchPattern = regexp.MustCompile(`^(.{2})search:(.+)`) 23 | 24 | Token = os.Getenv("TOKEN") 25 | GuildId = os.Getenv("GUILD_ID") 26 | 27 | NodeName = os.Getenv("NODE_NAME") 28 | NodeAddress = os.Getenv("NODE_ADDRESS") 29 | NodePassword = os.Getenv("NODE_PASSWORD") 30 | NodeSecure, _ = strconv.ParseBool(os.Getenv("NODE_SECURE")) 31 | ) 32 | 33 | type Bot struct { 34 | Session *discordgo.Session 35 | Lavalink disgolink.Client 36 | Handlers map[string]func(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error 37 | Queues *QueueManager 38 | } 39 | 40 | func main() { 41 | log.SetFlags(log.LstdFlags | log.Lshortfile) 42 | log.SetLevel(log.LevelInfo) 43 | log.Info("starting discordgo example...") 44 | log.Info("discordgo version: ", discordgo.VERSION) 45 | log.Info("disgolink version: ", disgolink.Version) 46 | 47 | b := &Bot{ 48 | Queues: &QueueManager{ 49 | queues: make(map[string]*Queue), 50 | }, 51 | } 52 | 53 | session, err := discordgo.New("Bot " + Token) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | b.Session = session 58 | 59 | session.State.TrackVoice = true 60 | session.Identify.Intents = discordgo.IntentGuilds | discordgo.IntentsGuildVoiceStates 61 | 62 | session.AddHandler(b.onApplicationCommand) 63 | session.AddHandler(b.onVoiceStateUpdate) 64 | session.AddHandler(b.onVoiceServerUpdate) 65 | 66 | if err = session.Open(); err != nil { 67 | log.Fatal(err) 68 | } 69 | defer session.Close() 70 | 71 | registerCommands(session) 72 | 73 | b.Lavalink = disgolink.New(snowflake.MustParse(session.State.User.ID), 74 | disgolink.WithListenerFunc(b.onPlayerPause), 75 | disgolink.WithListenerFunc(b.onPlayerResume), 76 | disgolink.WithListenerFunc(b.onTrackStart), 77 | disgolink.WithListenerFunc(b.onTrackEnd), 78 | disgolink.WithListenerFunc(b.onTrackException), 79 | disgolink.WithListenerFunc(b.onTrackStuck), 80 | disgolink.WithListenerFunc(b.onWebSocketClosed), 81 | ) 82 | b.Handlers = map[string]func(event *discordgo.InteractionCreate, data discordgo.ApplicationCommandInteractionData) error{ 83 | "play": b.play, 84 | "pause": b.pause, 85 | "now-playing": b.nowPlaying, 86 | "stop": b.stop, 87 | "queue": b.queue, 88 | "clear-queue": b.clearQueue, 89 | "queue-type": b.queueType, 90 | "shuffle": b.shuffle, 91 | } 92 | 93 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 94 | defer cancel() 95 | node, err := b.Lavalink.AddNode(ctx, disgolink.NodeConfig{ 96 | Name: NodeName, 97 | Address: NodeAddress, 98 | Password: NodePassword, 99 | Secure: NodeSecure, 100 | }) 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | version, err := node.Version(ctx) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | log.Infof("node version: %s", version) 109 | 110 | log.Info("DiscordGo example is now running. Press CTRL-C to exit.") 111 | s := make(chan os.Signal, 1) 112 | signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 113 | <-s 114 | } 115 | 116 | func (b *Bot) onApplicationCommand(session *discordgo.Session, event *discordgo.InteractionCreate) { 117 | data := event.ApplicationCommandData() 118 | 119 | handler, ok := b.Handlers[data.Name] 120 | if !ok { 121 | log.Info("unknown command: ", data.Name) 122 | return 123 | } 124 | if err := handler(event, data); err != nil { 125 | log.Error("error handling command: ", err) 126 | } 127 | } 128 | 129 | func (b *Bot) onVoiceStateUpdate(session *discordgo.Session, event *discordgo.VoiceStateUpdate) { 130 | if event.UserID != session.State.User.ID { 131 | return 132 | } 133 | 134 | var channelID *snowflake.ID 135 | if event.ChannelID != "" { 136 | id := snowflake.MustParse(event.ChannelID) 137 | channelID = &id 138 | } 139 | b.Lavalink.OnVoiceStateUpdate(context.TODO(), snowflake.MustParse(event.GuildID), channelID, event.SessionID) 140 | if event.ChannelID == "" { 141 | b.Queues.Delete(event.GuildID) 142 | } 143 | } 144 | 145 | func (b *Bot) onVoiceServerUpdate(session *discordgo.Session, event *discordgo.VoiceServerUpdate) { 146 | b.Lavalink.OnVoiceServerUpdate(context.TODO(), snowflake.MustParse(event.GuildID), event.Token, event.Endpoint) 147 | } 148 | -------------------------------------------------------------------------------- /_examples/discordgo/player_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/disgoorg/log" 8 | 9 | "github.com/disgoorg/disgolink/v3/disgolink" 10 | "github.com/disgoorg/disgolink/v3/lavalink" 11 | ) 12 | 13 | func (b *Bot) onPlayerPause(player disgolink.Player, event lavalink.PlayerPauseEvent) { 14 | fmt.Printf("onPlayerPause: %v\n", event) 15 | } 16 | 17 | func (b *Bot) onPlayerResume(player disgolink.Player, event lavalink.PlayerResumeEvent) { 18 | fmt.Printf("onPlayerResume: %v\n", event) 19 | } 20 | 21 | func (b *Bot) onTrackStart(player disgolink.Player, event lavalink.TrackStartEvent) { 22 | fmt.Printf("onTrackStart: %v\n", event) 23 | } 24 | 25 | func (b *Bot) onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) { 26 | fmt.Printf("onTrackEnd: %v\n", event) 27 | 28 | if !event.Reason.MayStartNext() { 29 | return 30 | } 31 | 32 | queue := b.Queues.Get(event.GuildID().String()) 33 | var ( 34 | nextTrack lavalink.Track 35 | ok bool 36 | ) 37 | switch queue.Type { 38 | case QueueTypeNormal: 39 | nextTrack, ok = queue.Next() 40 | 41 | case QueueTypeRepeatTrack: 42 | nextTrack = event.Track 43 | 44 | case QueueTypeRepeatQueue: 45 | queue.Add(event.Track) 46 | nextTrack, ok = queue.Next() 47 | } 48 | 49 | if !ok { 50 | return 51 | } 52 | if err := player.Update(context.TODO(), lavalink.WithTrack(nextTrack)); err != nil { 53 | log.Error("Failed to play next track: ", err) 54 | } 55 | } 56 | 57 | func (b *Bot) onTrackException(player disgolink.Player, event lavalink.TrackExceptionEvent) { 58 | fmt.Printf("onTrackException: %v\n", event) 59 | } 60 | 61 | func (b *Bot) onTrackStuck(player disgolink.Player, event lavalink.TrackStuckEvent) { 62 | fmt.Printf("onTrackStuck: %v\n", event) 63 | } 64 | 65 | func (b *Bot) onWebSocketClosed(player disgolink.Player, event lavalink.WebSocketClosedEvent) { 66 | fmt.Printf("onWebSocketClosed: %v\n", event) 67 | } 68 | -------------------------------------------------------------------------------- /_examples/discordgo/queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/disgoorg/disgolink/v3/lavalink" 8 | ) 9 | 10 | func init() { 11 | rand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | type QueueType string 15 | 16 | const ( 17 | QueueTypeNormal QueueType = "normal" 18 | QueueTypeRepeatTrack QueueType = "repeat_track" 19 | QueueTypeRepeatQueue QueueType = "repeat_queue" 20 | ) 21 | 22 | func (q QueueType) String() string { 23 | switch q { 24 | case QueueTypeNormal: 25 | return "Normal" 26 | case QueueTypeRepeatTrack: 27 | return "Repeat Track" 28 | case QueueTypeRepeatQueue: 29 | return "Repeat Queue" 30 | default: 31 | return "unknown" 32 | } 33 | } 34 | 35 | type Queue struct { 36 | Tracks []lavalink.Track 37 | Type QueueType 38 | } 39 | 40 | func (q *Queue) Shuffle() { 41 | rand.Shuffle(len(q.Tracks), func(i, j int) { 42 | q.Tracks[i], q.Tracks[j] = q.Tracks[j], q.Tracks[i] 43 | }) 44 | } 45 | 46 | func (q *Queue) Add(track ...lavalink.Track) { 47 | q.Tracks = append(q.Tracks, track...) 48 | } 49 | 50 | func (q *Queue) Next() (lavalink.Track, bool) { 51 | if len(q.Tracks) == 0 { 52 | return lavalink.Track{}, false 53 | } 54 | track := q.Tracks[0] 55 | q.Tracks = q.Tracks[1:] 56 | return track, true 57 | } 58 | 59 | func (q *Queue) Clear() { 60 | q.Tracks = make([]lavalink.Track, 0) 61 | } 62 | 63 | type QueueManager struct { 64 | queues map[string]*Queue 65 | } 66 | 67 | func (q *QueueManager) Get(guildID string) *Queue { 68 | queue, ok := q.queues[guildID] 69 | if !ok { 70 | queue = &Queue{ 71 | Tracks: make([]lavalink.Track, 0), 72 | Type: QueueTypeNormal, 73 | } 74 | q.queues[guildID] = queue 75 | } 76 | return queue 77 | } 78 | 79 | func (q *QueueManager) Delete(guildID string) { 80 | delete(q.queues, guildID) 81 | } 82 | -------------------------------------------------------------------------------- /_examples/disgo/bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/disgoorg/disgo/bot" 8 | "github.com/disgoorg/disgo/discord" 9 | "github.com/disgoorg/disgo/events" 10 | "github.com/disgoorg/disgolink/v3/disgolink" 11 | "github.com/disgoorg/snowflake/v2" 12 | ) 13 | 14 | func newBot() *Bot { 15 | return &Bot{ 16 | Queues: &QueueManager{ 17 | queues: make(map[snowflake.ID]*Queue), 18 | }, 19 | } 20 | } 21 | 22 | type Bot struct { 23 | Client bot.Client 24 | Lavalink disgolink.Client 25 | Handlers map[string]func(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error 26 | Queues *QueueManager 27 | } 28 | 29 | func (b *Bot) onApplicationCommand(event *events.ApplicationCommandInteractionCreate) { 30 | data := event.SlashCommandInteractionData() 31 | 32 | handler, ok := b.Handlers[data.CommandName()] 33 | if !ok { 34 | slog.Info("unknown command", slog.String("command", data.CommandName())) 35 | return 36 | } 37 | if err := handler(event, data); err != nil { 38 | slog.Error("error handling command", slog.Any("err", err)) 39 | } 40 | } 41 | 42 | func (b *Bot) onVoiceStateUpdate(event *events.GuildVoiceStateUpdate) { 43 | if event.VoiceState.UserID != b.Client.ApplicationID() { 44 | return 45 | } 46 | b.Lavalink.OnVoiceStateUpdate(context.TODO(), event.VoiceState.GuildID, event.VoiceState.ChannelID, event.VoiceState.SessionID) 47 | if event.VoiceState.ChannelID == nil { 48 | b.Queues.Delete(event.VoiceState.GuildID) 49 | } 50 | } 51 | 52 | func (b *Bot) onVoiceServerUpdate(event *events.VoiceServerUpdate) { 53 | b.Lavalink.OnVoiceServerUpdate(context.TODO(), event.GuildID, event.Token, *event.Endpoint) 54 | } 55 | -------------------------------------------------------------------------------- /_examples/disgo/commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/disgoorg/disgo/bot" 7 | "github.com/disgoorg/disgo/discord" 8 | "github.com/disgoorg/disgo/handler" 9 | "github.com/disgoorg/json" 10 | "github.com/disgoorg/snowflake/v2" 11 | 12 | "github.com/disgoorg/disgolink/v3/lavalink" 13 | ) 14 | 15 | var commands = []discord.ApplicationCommandCreate{ 16 | discord.SlashCommandCreate{ 17 | Name: "play", 18 | Description: "Plays a song", 19 | Options: []discord.ApplicationCommandOption{ 20 | discord.ApplicationCommandOptionString{ 21 | Name: "identifier", 22 | Description: "The song link or search query", 23 | Required: true, 24 | }, 25 | discord.ApplicationCommandOptionString{ 26 | Name: "source", 27 | Description: "The source to search on", 28 | Required: false, 29 | Choices: []discord.ApplicationCommandOptionChoiceString{ 30 | { 31 | Name: "YouTube", 32 | Value: string(lavalink.SearchTypeYouTube), 33 | }, 34 | { 35 | Name: "YouTube Music", 36 | Value: string(lavalink.SearchTypeYouTubeMusic), 37 | }, 38 | { 39 | Name: "SoundCloud", 40 | Value: string(lavalink.SearchTypeSoundCloud), 41 | }, 42 | { 43 | Name: "Deezer", 44 | Value: "dzsearch", 45 | }, 46 | { 47 | Name: "Deezer ISRC", 48 | Value: "dzisrc", 49 | }, 50 | { 51 | Name: "Spotify", 52 | Value: "spsearch", 53 | }, 54 | { 55 | Name: "AppleMusic", 56 | Value: "amsearch", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | discord.SlashCommandCreate{ 63 | Name: "pause", 64 | Description: "Pauses the current song", 65 | }, 66 | discord.SlashCommandCreate{ 67 | Name: "now-playing", 68 | Description: "Shows the current playing song", 69 | }, 70 | discord.SlashCommandCreate{ 71 | Name: "stop", 72 | Description: "Stops the current song and stops the player", 73 | }, 74 | discord.SlashCommandCreate{ 75 | Name: "disconnect", 76 | Description: "Disconnects the player", 77 | }, 78 | discord.SlashCommandCreate{ 79 | Name: "bass-boost", 80 | Description: "Enables or disables bass boost", 81 | Options: []discord.ApplicationCommandOption{ 82 | discord.ApplicationCommandOptionBool{ 83 | Name: "enabled", 84 | Description: "Whether bass boost should be enabled or disabled", 85 | Required: true, 86 | }, 87 | }, 88 | }, 89 | discord.SlashCommandCreate{ 90 | Name: "players", 91 | Description: "Shows all active players", 92 | }, 93 | discord.SlashCommandCreate{ 94 | Name: "skip", 95 | Description: "Skips the current song", 96 | Options: []discord.ApplicationCommandOption{ 97 | discord.ApplicationCommandOptionInt{ 98 | Name: "amount", 99 | Description: "The amount of songs to skip", 100 | Required: false, 101 | }, 102 | }, 103 | }, 104 | discord.SlashCommandCreate{ 105 | Name: "volume", 106 | Description: "Sets the volume of the player", 107 | Options: []discord.ApplicationCommandOption{ 108 | discord.ApplicationCommandOptionInt{ 109 | Name: "volume", 110 | Description: "The volume to set", 111 | Required: true, 112 | MaxValue: json.Ptr(1000), 113 | MinValue: json.Ptr(0), 114 | }, 115 | }, 116 | }, 117 | discord.SlashCommandCreate{ 118 | Name: "seek", 119 | Description: "Seeks to a specific position in the current song", 120 | Options: []discord.ApplicationCommandOption{ 121 | discord.ApplicationCommandOptionInt{ 122 | Name: "position", 123 | Description: "The position to seek to", 124 | Required: true, 125 | }, 126 | discord.ApplicationCommandOptionInt{ 127 | Name: "unit", 128 | Description: "The unit of the position", 129 | Required: false, 130 | Choices: []discord.ApplicationCommandOptionChoiceInt{ 131 | { 132 | Name: "Milliseconds", 133 | Value: int(lavalink.Millisecond), 134 | }, 135 | { 136 | Name: "Seconds", 137 | Value: int(lavalink.Second), 138 | }, 139 | { 140 | Name: "Minutes", 141 | Value: int(lavalink.Minute), 142 | }, 143 | { 144 | Name: "Hours", 145 | Value: int(lavalink.Hour), 146 | }, 147 | }, 148 | }, 149 | }, 150 | }, 151 | discord.SlashCommandCreate{ 152 | Name: "shuffle", 153 | Description: "Shuffles the current queue", 154 | }, 155 | } 156 | 157 | func registerCommands(client bot.Client) { 158 | if err := handler.SyncCommands(client, commands, []snowflake.ID{GuildID}); err != nil { 159 | slog.Error("error while registering commands", slog.Any("err", err)) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /_examples/disgo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/disgoorg/disgolink/v3/_examples/disgo 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/disgoorg/disgo v0.18.15 7 | github.com/disgoorg/disgolink/v3 v3.0.4 8 | github.com/disgoorg/json v1.2.0 9 | github.com/disgoorg/snowflake/v2 v2.0.3 10 | ) 11 | 12 | require ( 13 | github.com/gorilla/websocket v1.5.3 // indirect 14 | github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect 15 | golang.org/x/crypto v0.36.0 // indirect 16 | golang.org/x/sys v0.31.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /_examples/disgo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/disgoorg/disgo v0.18.15 h1:T24I/NdUUody4FDvb8YkhSxHtsgRKD8Ui5Vi5PXnIrQ= 4 | github.com/disgoorg/disgo v0.18.15/go.mod h1:dXYVH059d6aK7mI+Nh/3svSRWedNd09P7C2VX3RqbJY= 5 | github.com/disgoorg/disgolink/v3 v3.0.4 h1:ymSb9PPbgvA1zQBkecnopRBB+ybJyqizLxP+SCoRfpM= 6 | github.com/disgoorg/disgolink/v3 v3.0.4/go.mod h1:UjHfrC4NT4vzibG3GyqtY5l3aMzFwfkU+B3RiW3AQQ8= 7 | github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= 8 | github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= 9 | github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= 10 | github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= 11 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 12 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= 16 | github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= 17 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 18 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 20 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 21 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 22 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /_examples/disgo/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/disgoorg/disgo/discord" 9 | "github.com/disgoorg/disgo/events" 10 | "github.com/disgoorg/json" 11 | 12 | "github.com/disgoorg/disgolink/v3/disgolink" 13 | "github.com/disgoorg/disgolink/v3/lavalink" 14 | ) 15 | 16 | var bassBoost = &lavalink.Equalizer{ 17 | 0: 0.2, 18 | 1: 0.15, 19 | 2: 0.1, 20 | 3: 0.05, 21 | 4: 0.0, 22 | 5: -0.05, 23 | 6: -0.1, 24 | 7: -0.1, 25 | 8: -0.1, 26 | 9: -0.1, 27 | 10: -0.1, 28 | 11: -0.1, 29 | 12: -0.1, 30 | 13: -0.1, 31 | 14: -0.1, 32 | } 33 | 34 | func (b *Bot) shuffle(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 35 | queue := b.Queues.Get(*event.GuildID()) 36 | if queue == nil { 37 | return event.CreateMessage(discord.MessageCreate{ 38 | Content: "No player found", 39 | }) 40 | } 41 | 42 | queue.Shuffle() 43 | return event.CreateMessage(discord.MessageCreate{ 44 | Content: "Queue shuffled", 45 | }) 46 | } 47 | 48 | func (b *Bot) volume(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 49 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 50 | if player == nil { 51 | return event.CreateMessage(discord.MessageCreate{ 52 | Content: "No player found", 53 | }) 54 | } 55 | 56 | volume := data.Int("volume") 57 | if err := player.Update(context.TODO(), lavalink.WithVolume(volume)); err != nil { 58 | return event.CreateMessage(discord.MessageCreate{ 59 | Content: fmt.Sprintf("Error while setting volume: `%s`", err), 60 | }) 61 | } 62 | 63 | return event.CreateMessage(discord.MessageCreate{ 64 | Content: fmt.Sprintf("Volume set to `%d`", volume), 65 | }) 66 | } 67 | 68 | func (b *Bot) seek(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 69 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 70 | if player == nil { 71 | return event.CreateMessage(discord.MessageCreate{ 72 | Content: "No player found", 73 | }) 74 | } 75 | 76 | position := data.Int("position") 77 | unit, ok := data.OptInt("unit") 78 | if !ok { 79 | unit = 1 80 | } 81 | finalPosition := lavalink.Duration(position * unit) 82 | if err := player.Update(context.TODO(), lavalink.WithPosition(finalPosition)); err != nil { 83 | return event.CreateMessage(discord.MessageCreate{ 84 | Content: fmt.Sprintf("Error while seeking: `%s`", err), 85 | }) 86 | } 87 | 88 | return event.CreateMessage(discord.MessageCreate{ 89 | Content: fmt.Sprintf("Seeked to `%s`", formatPosition(finalPosition)), 90 | }) 91 | } 92 | 93 | func (b *Bot) bassBoost(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 94 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 95 | if player == nil { 96 | return event.CreateMessage(discord.MessageCreate{ 97 | Content: "No player found", 98 | }) 99 | } 100 | 101 | enabled := data.Bool("enabled") 102 | filters := player.Filters() 103 | if enabled { 104 | filters.Equalizer = bassBoost 105 | } else { 106 | filters.Equalizer = nil 107 | } 108 | 109 | if err := player.Update(context.TODO(), lavalink.WithFilters(filters)); err != nil { 110 | return event.CreateMessage(discord.MessageCreate{ 111 | Content: fmt.Sprintf("Error while setting bass boost: `%s`", err), 112 | }) 113 | } 114 | 115 | return event.CreateMessage(discord.MessageCreate{ 116 | Content: fmt.Sprintf("Bass boost set to `%t`", enabled), 117 | }) 118 | } 119 | 120 | func (b *Bot) skip(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 121 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 122 | queue := b.Queues.Get(*event.GuildID()) 123 | if player == nil || queue == nil { 124 | return event.CreateMessage(discord.MessageCreate{ 125 | Content: "No player found", 126 | }) 127 | } 128 | 129 | amount, ok := data.OptInt("amount") 130 | if !ok { 131 | amount = 1 132 | } 133 | 134 | track, ok := queue.Skip(amount) 135 | if !ok { 136 | return event.CreateMessage(discord.MessageCreate{ 137 | Content: "No tracks in queue", 138 | }) 139 | } 140 | 141 | if err := player.Update(context.TODO(), lavalink.WithTrack(track)); err != nil { 142 | return event.CreateMessage(discord.MessageCreate{ 143 | Content: fmt.Sprintf("Error while skipping track: `%s`", err), 144 | }) 145 | } 146 | 147 | return event.CreateMessage(discord.MessageCreate{ 148 | Content: "Skipped track", 149 | }) 150 | } 151 | 152 | func (b *Bot) queueType(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 153 | queue := b.Queues.Get(*event.GuildID()) 154 | if queue == nil { 155 | return event.CreateMessage(discord.MessageCreate{ 156 | Content: "No player found", 157 | }) 158 | } 159 | 160 | queue.Type = QueueType(data.String("type")) 161 | return event.CreateMessage(discord.MessageCreate{ 162 | Content: fmt.Sprintf("Queue type set to `%s`", queue.Type), 163 | }) 164 | } 165 | 166 | func (b *Bot) clearQueue(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 167 | queue := b.Queues.Get(*event.GuildID()) 168 | if queue == nil { 169 | return event.CreateMessage(discord.MessageCreate{ 170 | Content: "No player found", 171 | }) 172 | } 173 | 174 | queue.Clear() 175 | return event.CreateMessage(discord.MessageCreate{ 176 | Content: "Queue cleared", 177 | }) 178 | } 179 | 180 | func (b *Bot) queue(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 181 | queue := b.Queues.Get(*event.GuildID()) 182 | if queue == nil { 183 | return event.CreateMessage(discord.MessageCreate{ 184 | Content: "No player found", 185 | }) 186 | } 187 | 188 | if len(queue.Tracks) == 0 { 189 | return event.CreateMessage(discord.MessageCreate{ 190 | Content: "No tracks in queue", 191 | }) 192 | } 193 | 194 | var tracks string 195 | for i, track := range queue.Tracks { 196 | tracks += fmt.Sprintf("%d. [`%s`](<%s>)\n", i+1, track.Info.Title, *track.Info.URI) 197 | } 198 | 199 | return event.CreateMessage(discord.MessageCreate{ 200 | Content: fmt.Sprintf("Queue `%s`:\n%s", queue.Type, tracks), 201 | }) 202 | } 203 | 204 | func (b *Bot) players(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 205 | var description string 206 | b.Lavalink.ForPlayers(func(player disgolink.Player) { 207 | description += fmt.Sprintf("GuildID: `%s`\n", player.GuildID()) 208 | }) 209 | 210 | return event.CreateMessage(discord.MessageCreate{ 211 | Content: fmt.Sprintf("Players:\n%s", description), 212 | }) 213 | } 214 | 215 | func (b *Bot) pause(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 216 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 217 | if player == nil { 218 | return event.CreateMessage(discord.MessageCreate{ 219 | Content: "No player found", 220 | }) 221 | } 222 | 223 | if err := player.Update(context.TODO(), lavalink.WithPaused(!player.Paused())); err != nil { 224 | return event.CreateMessage(discord.MessageCreate{ 225 | Content: fmt.Sprintf("Error while pausing: `%s`", err), 226 | }) 227 | } 228 | 229 | status := "playing" 230 | if player.Paused() { 231 | status = "paused" 232 | } 233 | return event.CreateMessage(discord.MessageCreate{ 234 | Content: fmt.Sprintf("Player is now %s", status), 235 | }) 236 | } 237 | 238 | func (b *Bot) stop(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 239 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 240 | if player == nil { 241 | return event.CreateMessage(discord.MessageCreate{ 242 | Content: "No player found", 243 | }) 244 | } 245 | 246 | if err := player.Update(context.TODO(), lavalink.WithNullTrack()); err != nil { 247 | return event.CreateMessage(discord.MessageCreate{ 248 | Content: fmt.Sprintf("Error while stopping: `%s`", err), 249 | }) 250 | } 251 | 252 | return event.CreateMessage(discord.MessageCreate{ 253 | Content: "Player stopped", 254 | }) 255 | } 256 | 257 | func (b *Bot) disconnect(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 258 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 259 | if player == nil { 260 | return event.CreateMessage(discord.MessageCreate{ 261 | Content: "No player found", 262 | }) 263 | } 264 | 265 | if err := b.Client.UpdateVoiceState(context.TODO(), *event.GuildID(), nil, false, false); err != nil { 266 | return event.CreateMessage(discord.MessageCreate{ 267 | Content: fmt.Sprintf("Error while disconnecting: `%s`", err), 268 | }) 269 | } 270 | 271 | return event.CreateMessage(discord.MessageCreate{ 272 | Content: "Player disconnected", 273 | }) 274 | } 275 | 276 | func (b *Bot) nowPlaying(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 277 | player := b.Lavalink.ExistingPlayer(*event.GuildID()) 278 | if player == nil { 279 | return event.CreateMessage(discord.MessageCreate{ 280 | Content: "No player found", 281 | }) 282 | } 283 | 284 | track := player.Track() 285 | if track == nil { 286 | return event.CreateMessage(discord.MessageCreate{ 287 | Content: "No track found", 288 | }) 289 | } 290 | 291 | return event.CreateMessage(discord.MessageCreate{ 292 | Content: fmt.Sprintf("Now playing: [`%s`](<%s>)\n\n %s / %s", track.Info.Title, *track.Info.URI, formatPosition(player.Position()), formatPosition(track.Info.Length)), 293 | }) 294 | } 295 | 296 | func formatPosition(position lavalink.Duration) string { 297 | if position == 0 { 298 | return "0:00" 299 | } 300 | return fmt.Sprintf("%d:%02d", position.Minutes(), position.SecondsPart()) 301 | } 302 | 303 | func (b *Bot) play(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error { 304 | identifier := data.String("identifier") 305 | if source, ok := data.OptString("source"); ok { 306 | identifier = lavalink.SearchType(source).Apply(identifier) 307 | } else if !urlPattern.MatchString(identifier) && !searchPattern.MatchString(identifier) { 308 | identifier = lavalink.SearchTypeYouTube.Apply(identifier) 309 | } 310 | 311 | voiceState, ok := b.Client.Caches().VoiceState(*event.GuildID(), event.User().ID) 312 | if !ok { 313 | return event.CreateMessage(discord.MessageCreate{ 314 | Content: "You need to be in a voice channel to use this command", 315 | }) 316 | } 317 | 318 | if err := event.DeferCreateMessage(false); err != nil { 319 | return err 320 | } 321 | 322 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 323 | defer cancel() 324 | 325 | var toPlay *lavalink.Track 326 | b.Lavalink.BestNode().LoadTracksHandler(ctx, identifier, disgolink.NewResultHandler( 327 | func(track lavalink.Track) { 328 | _, _ = b.Client.Rest().UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.MessageUpdate{ 329 | Content: json.Ptr(fmt.Sprintf("Loaded track: [`%s`](<%s>)", track.Info.Title, *track.Info.URI)), 330 | }) 331 | toPlay = &track 332 | }, 333 | func(playlist lavalink.Playlist) { 334 | _, _ = b.Client.Rest().UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.MessageUpdate{ 335 | Content: json.Ptr(fmt.Sprintf("Loaded playlist: `%s` with `%d` tracks", playlist.Info.Name, len(playlist.Tracks))), 336 | }) 337 | toPlay = &playlist.Tracks[0] 338 | }, 339 | func(tracks []lavalink.Track) { 340 | _, _ = b.Client.Rest().UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.MessageUpdate{ 341 | Content: json.Ptr(fmt.Sprintf("Loaded search result: [`%s`](<%s>)", tracks[0].Info.Title, *tracks[0].Info.URI)), 342 | }) 343 | toPlay = &tracks[0] 344 | }, 345 | func() { 346 | _, _ = b.Client.Rest().UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.MessageUpdate{ 347 | Content: json.Ptr(fmt.Sprintf("Nothing found for: `%s`", identifier)), 348 | }) 349 | }, 350 | func(err error) { 351 | _, _ = b.Client.Rest().UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.MessageUpdate{ 352 | Content: json.Ptr(fmt.Sprintf("Error while looking up query: `%s`", err)), 353 | }) 354 | }, 355 | )) 356 | if toPlay == nil { 357 | return nil 358 | } 359 | 360 | if err := b.Client.UpdateVoiceState(context.TODO(), *event.GuildID(), voiceState.ChannelID, false, false); err != nil { 361 | return err 362 | } 363 | 364 | return b.Lavalink.Player(*event.GuildID()).Update(context.TODO(), lavalink.WithTrack(*toPlay)) 365 | } 366 | -------------------------------------------------------------------------------- /_examples/disgo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "os/signal" 8 | "regexp" 9 | "strconv" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/disgoorg/disgo" 14 | "github.com/disgoorg/disgo/bot" 15 | "github.com/disgoorg/disgo/cache" 16 | "github.com/disgoorg/disgo/discord" 17 | "github.com/disgoorg/disgo/events" 18 | "github.com/disgoorg/disgo/gateway" 19 | "github.com/disgoorg/snowflake/v2" 20 | 21 | "github.com/disgoorg/disgolink/v3/disgolink" 22 | ) 23 | 24 | var ( 25 | urlPattern = regexp.MustCompile("^https?://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]?") 26 | searchPattern = regexp.MustCompile(`^(.{2})search:(.+)`) 27 | 28 | Token = os.Getenv("TOKEN") 29 | GuildID = snowflake.GetEnv("GUILD_ID") 30 | 31 | NodeName = os.Getenv("NODE_NAME") 32 | NodeAddress = os.Getenv("NODE_ADDRESS") 33 | NodePassword = os.Getenv("NODE_PASSWORD") 34 | NodeSecure, _ = strconv.ParseBool(os.Getenv("NODE_SECURE")) 35 | ) 36 | 37 | func main() { 38 | slog.Info("starting disgo example...") 39 | slog.Info("disgo version", slog.String("version", disgo.Version)) 40 | slog.Info("disgolink version: ", slog.String("version", disgolink.Version)) 41 | slog.SetLogLoggerLevel(slog.LevelDebug) 42 | 43 | b := newBot() 44 | 45 | client, err := disgo.New(Token, 46 | bot.WithGatewayConfigOpts( 47 | gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates), 48 | ), 49 | bot.WithCacheConfigOpts( 50 | cache.WithCaches(cache.FlagVoiceStates), 51 | ), 52 | bot.WithEventListenerFunc(b.onApplicationCommand), 53 | bot.WithEventListenerFunc(b.onVoiceStateUpdate), 54 | bot.WithEventListenerFunc(b.onVoiceServerUpdate), 55 | ) 56 | if err != nil { 57 | slog.Error("error while building disgo client", slog.Any("err", err)) 58 | os.Exit(1) 59 | } 60 | b.Client = client 61 | 62 | registerCommands(client) 63 | 64 | b.Lavalink = disgolink.New(client.ApplicationID(), 65 | disgolink.WithListenerFunc(b.onPlayerPause), 66 | disgolink.WithListenerFunc(b.onPlayerResume), 67 | disgolink.WithListenerFunc(b.onTrackStart), 68 | disgolink.WithListenerFunc(b.onTrackEnd), 69 | disgolink.WithListenerFunc(b.onTrackException), 70 | disgolink.WithListenerFunc(b.onTrackStuck), 71 | disgolink.WithListenerFunc(b.onWebSocketClosed), 72 | disgolink.WithListenerFunc(b.onUnknownEvent), 73 | ) 74 | b.Handlers = map[string]func(event *events.ApplicationCommandInteractionCreate, data discord.SlashCommandInteractionData) error{ 75 | "play": b.play, 76 | "pause": b.pause, 77 | "now-playing": b.nowPlaying, 78 | "stop": b.stop, 79 | "players": b.players, 80 | "queue": b.queue, 81 | "clear-queue": b.clearQueue, 82 | "queue-type": b.queueType, 83 | "shuffle": b.shuffle, 84 | "seek": b.seek, 85 | "volume": b.volume, 86 | "skip": b.skip, 87 | "bass-boost": b.bassBoost, 88 | } 89 | 90 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 91 | defer cancel() 92 | if err = client.OpenGateway(ctx); err != nil { 93 | slog.Error("failed to open gateway", slog.Any("err", err)) 94 | os.Exit(1) 95 | } 96 | defer client.Close(context.TODO()) 97 | 98 | node, err := b.Lavalink.AddNode(ctx, disgolink.NodeConfig{ 99 | Name: NodeName, 100 | Address: NodeAddress, 101 | Password: NodePassword, 102 | Secure: NodeSecure, 103 | }) 104 | if err != nil { 105 | slog.Error("failed to add node", slog.Any("err", err)) 106 | os.Exit(1) 107 | } 108 | version, err := node.Version(ctx) 109 | if err != nil { 110 | slog.Error("failed to get node version", slog.Any("err", err)) 111 | os.Exit(1) 112 | } 113 | 114 | slog.Info("DisGo example is now running. Press CTRL-C to exit.", slog.String("node_version", version), slog.String("node_session_id", node.SessionID())) 115 | s := make(chan os.Signal, 1) 116 | signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 117 | <-s 118 | } 119 | -------------------------------------------------------------------------------- /_examples/disgo/player_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/disgoorg/disgolink/v3/disgolink" 8 | "github.com/disgoorg/disgolink/v3/lavalink" 9 | ) 10 | 11 | func (b *Bot) onPlayerPause(player disgolink.Player, event lavalink.PlayerPauseEvent) { 12 | slog.Info("player paused", slog.Any("event", event)) 13 | } 14 | 15 | func (b *Bot) onPlayerResume(player disgolink.Player, event lavalink.PlayerResumeEvent) { 16 | slog.Info("player resumed", slog.Any("event", event)) 17 | } 18 | 19 | func (b *Bot) onTrackStart(player disgolink.Player, event lavalink.TrackStartEvent) { 20 | slog.Info("track started", slog.Any("event", event)) 21 | } 22 | 23 | func (b *Bot) onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) { 24 | if !event.Reason.MayStartNext() { 25 | return 26 | } 27 | 28 | queue := b.Queues.Get(event.GuildID()) 29 | var ( 30 | nextTrack lavalink.Track 31 | ok bool 32 | ) 33 | switch queue.Type { 34 | case QueueTypeNormal: 35 | nextTrack, ok = queue.Next() 36 | 37 | case QueueTypeRepeatTrack: 38 | nextTrack = event.Track 39 | 40 | case QueueTypeRepeatQueue: 41 | queue.Add(event.Track) 42 | nextTrack, ok = queue.Next() 43 | } 44 | 45 | if !ok { 46 | return 47 | } 48 | if err := player.Update(context.TODO(), lavalink.WithTrack(nextTrack)); err != nil { 49 | slog.Error("Failed to play next track", slog.Any("err", err)) 50 | } 51 | } 52 | 53 | func (b *Bot) onTrackException(player disgolink.Player, event lavalink.TrackExceptionEvent) { 54 | slog.Info("track exception", slog.Any("event", event)) 55 | } 56 | 57 | func (b *Bot) onTrackStuck(player disgolink.Player, event lavalink.TrackStuckEvent) { 58 | slog.Info("track stuck", slog.Any("event", event)) 59 | } 60 | 61 | func (b *Bot) onWebSocketClosed(player disgolink.Player, event lavalink.WebSocketClosedEvent) { 62 | slog.Info("websocket closed", slog.Any("event", event)) 63 | } 64 | 65 | func (b *Bot) onUnknownEvent(p disgolink.Player, e lavalink.UnknownEvent) { 66 | slog.Info("unknown event", slog.Any("event", e.Type()), slog.String("data", string(e.Data))) 67 | } 68 | -------------------------------------------------------------------------------- /_examples/disgo/queue.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/disgoorg/snowflake/v2" 8 | 9 | "github.com/disgoorg/disgolink/v3/lavalink" 10 | ) 11 | 12 | func init() { 13 | rand.Seed(time.Now().UnixNano()) 14 | } 15 | 16 | type QueueType string 17 | 18 | const ( 19 | QueueTypeNormal QueueType = "normal" 20 | QueueTypeRepeatTrack QueueType = "repeat_track" 21 | QueueTypeRepeatQueue QueueType = "repeat_queue" 22 | ) 23 | 24 | func (q QueueType) String() string { 25 | switch q { 26 | case QueueTypeNormal: 27 | return "Normal" 28 | case QueueTypeRepeatTrack: 29 | return "Repeat Track" 30 | case QueueTypeRepeatQueue: 31 | return "Repeat Queue" 32 | default: 33 | return "unknown" 34 | } 35 | } 36 | 37 | type Queue struct { 38 | Tracks []lavalink.Track 39 | Type QueueType 40 | } 41 | 42 | func (q *Queue) Shuffle() { 43 | rand.Shuffle(len(q.Tracks), func(i, j int) { 44 | q.Tracks[i], q.Tracks[j] = q.Tracks[j], q.Tracks[i] 45 | }) 46 | } 47 | 48 | func (q *Queue) Add(track ...lavalink.Track) { 49 | q.Tracks = append(q.Tracks, track...) 50 | } 51 | 52 | func (q *Queue) Next() (lavalink.Track, bool) { 53 | if len(q.Tracks) == 0 { 54 | return lavalink.Track{}, false 55 | } 56 | track := q.Tracks[0] 57 | q.Tracks = q.Tracks[1:] 58 | return track, true 59 | } 60 | 61 | func (q *Queue) Skip(amount int) (lavalink.Track, bool) { 62 | if len(q.Tracks) == 0 { 63 | return lavalink.Track{}, false 64 | } 65 | if amount > len(q.Tracks) { 66 | amount = len(q.Tracks) 67 | } 68 | q.Tracks = q.Tracks[amount:] 69 | return q.Tracks[0], true 70 | } 71 | 72 | func (q *Queue) Clear() { 73 | q.Tracks = make([]lavalink.Track, 0) 74 | } 75 | 76 | type QueueManager struct { 77 | queues map[snowflake.ID]*Queue 78 | } 79 | 80 | func (q *QueueManager) Get(guildID snowflake.ID) *Queue { 81 | queue, ok := q.queues[guildID] 82 | if !ok { 83 | queue = &Queue{ 84 | Tracks: make([]lavalink.Track, 0), 85 | Type: QueueTypeNormal, 86 | } 87 | q.queues[guildID] = queue 88 | } 89 | return queue 90 | } 91 | 92 | func (q *QueueManager) Delete(guildID snowflake.ID) { 93 | delete(q.queues, guildID) 94 | } 95 | -------------------------------------------------------------------------------- /disgolink/client.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "runtime/debug" 8 | "sync" 9 | 10 | "github.com/disgoorg/snowflake/v2" 11 | 12 | "github.com/disgoorg/disgolink/v3/lavalink" 13 | ) 14 | 15 | type Client interface { 16 | AddNode(ctx context.Context, config NodeConfig) (Node, error) 17 | ForNodes(nodeFunc func(node Node)) 18 | Node(name string) Node 19 | BestNode() Node 20 | RemoveNode(name string) 21 | 22 | Player(guildID snowflake.ID) Player 23 | PlayerOnNode(node Node, guildID snowflake.ID) Player 24 | ExistingPlayer(guildID snowflake.ID) Player 25 | RemovePlayer(guildID snowflake.ID) 26 | ForPlayers(playerFunc func(player Player)) 27 | 28 | EmitEvent(player Player, event lavalink.Message) 29 | AddListeners(listeners ...EventListener) 30 | RemoveListeners(listeners ...EventListener) 31 | 32 | AddPlugins(plugins ...Plugin) 33 | ForPlugins(pluginFunc func(plugin Plugin)) 34 | RemovePlugins(plugins ...Plugin) 35 | 36 | UserID() snowflake.ID 37 | Close() 38 | 39 | OnVoiceServerUpdate(ctx context.Context, guildID snowflake.ID, token string, endpoint string) 40 | OnVoiceStateUpdate(ctx context.Context, guildID snowflake.ID, channelID *snowflake.ID, sessionID string) 41 | } 42 | 43 | func New(userID snowflake.ID, opts ...ConfigOpt) Client { 44 | cfg := DefaultConfig() 45 | cfg.Apply(opts) 46 | cfg.Logger = cfg.Logger.With(slog.String("name", "disgolink_client")) 47 | 48 | return &clientImpl{ 49 | logger: cfg.Logger, 50 | httpClient: cfg.HTTPClient, 51 | userID: userID, 52 | nodes: map[string]Node{}, 53 | players: map[snowflake.ID]Player{}, 54 | listeners: cfg.Listeners, 55 | plugins: cfg.Plugins, 56 | } 57 | } 58 | 59 | var _ Client = (*clientImpl)(nil) 60 | 61 | type clientImpl struct { 62 | logger *slog.Logger 63 | httpClient *http.Client 64 | userID snowflake.ID 65 | 66 | nodesMu sync.Mutex 67 | nodes map[string]Node 68 | 69 | playersMu sync.Mutex 70 | players map[snowflake.ID]Player 71 | 72 | listenersMu sync.Mutex 73 | listeners []EventListener 74 | 75 | pluginsMu sync.Mutex 76 | plugins []Plugin 77 | } 78 | 79 | func (c *clientImpl) AddNode(ctx context.Context, config NodeConfig) (Node, error) { 80 | node := &nodeImpl{ 81 | logger: c.logger.With(slog.String("name", "disgolink_node"), slog.String("node_name", config.Name)), 82 | config: config, 83 | lavalink: c, 84 | status: StatusDisconnected, 85 | } 86 | node.rest = &restClientImpl{ 87 | logger: c.logger.With(slog.String("name", "disgolink_rest_client"), slog.String("node_name", config.Name)), 88 | node: node, 89 | httpClient: c.httpClient, 90 | } 91 | if err := node.Open(ctx); err != nil { 92 | return nil, err 93 | } 94 | 95 | c.nodesMu.Lock() 96 | defer c.nodesMu.Unlock() 97 | c.nodes[config.Name] = node 98 | return node, nil 99 | } 100 | 101 | func (c *clientImpl) ForNodes(nodeFunc func(node Node)) { 102 | c.nodesMu.Lock() 103 | defer c.nodesMu.Unlock() 104 | for i := range c.nodes { 105 | nodeFunc(c.nodes[i]) 106 | } 107 | } 108 | 109 | func (c *clientImpl) Node(name string) Node { 110 | c.nodesMu.Lock() 111 | defer c.nodesMu.Unlock() 112 | return c.nodes[name] 113 | } 114 | 115 | func (c *clientImpl) BestNode() Node { 116 | c.nodesMu.Lock() 117 | defer c.nodesMu.Unlock() 118 | var bestNode Node 119 | for _, node := range c.nodes { 120 | if bestNode == nil || node.Stats().Better(bestNode.Stats()) { 121 | bestNode = node 122 | } 123 | } 124 | return bestNode 125 | } 126 | 127 | func (c *clientImpl) RemoveNode(name string) { 128 | c.nodesMu.Lock() 129 | defer c.nodesMu.Unlock() 130 | if node, ok := c.nodes[name]; ok { 131 | node.Close() 132 | delete(c.nodes, name) 133 | } 134 | } 135 | 136 | func (c *clientImpl) Player(guildID snowflake.ID) Player { 137 | return c.PlayerOnNode(c.BestNode(), guildID) 138 | } 139 | 140 | func (c *clientImpl) PlayerOnNode(node Node, guildID snowflake.ID) Player { 141 | c.playersMu.Lock() 142 | defer c.playersMu.Unlock() 143 | if player, ok := c.players[guildID]; ok { 144 | return player 145 | } 146 | 147 | player := NewPlayer(c.logger, c, node, guildID) 148 | c.ForPlugins(func(plugin Plugin) { 149 | if pl, ok := plugin.(PluginEventHandler); ok { 150 | pl.OnNewPlayer(player) 151 | } 152 | }) 153 | c.players[guildID] = player 154 | return player 155 | } 156 | 157 | func (c *clientImpl) ExistingPlayer(guildID snowflake.ID) Player { 158 | c.playersMu.Lock() 159 | defer c.playersMu.Unlock() 160 | return c.players[guildID] 161 | } 162 | 163 | func (c *clientImpl) RemovePlayer(guildID snowflake.ID) { 164 | c.playersMu.Lock() 165 | defer c.playersMu.Unlock() 166 | delete(c.players, guildID) 167 | } 168 | 169 | func (c *clientImpl) ForPlayers(playerFunc func(player Player)) { 170 | c.playersMu.Lock() 171 | defer c.playersMu.Unlock() 172 | for _, player := range c.players { 173 | playerFunc(player) 174 | } 175 | } 176 | 177 | func (c *clientImpl) EmitEvent(player Player, event lavalink.Message) { 178 | c.listenersMu.Lock() 179 | defer c.listenersMu.Unlock() 180 | 181 | defer func() { 182 | if r := recover(); r != nil { 183 | c.logger.Error("recovered from panic in event listener", slog.Any("r", r), slog.String("stack", string(debug.Stack()))) 184 | return 185 | } 186 | }() 187 | for _, listener := range c.listeners { 188 | listener.OnEvent(player, event) 189 | } 190 | } 191 | 192 | func (c *clientImpl) AddListeners(listeners ...EventListener) { 193 | c.listenersMu.Lock() 194 | defer c.listenersMu.Unlock() 195 | c.listeners = append(c.listeners, listeners...) 196 | } 197 | 198 | func (c *clientImpl) RemoveListeners(listeners ...EventListener) { 199 | c.listenersMu.Lock() 200 | defer c.listenersMu.Unlock() 201 | for _, listener := range listeners { 202 | for i, ln := range c.listeners { 203 | if ln == listener { 204 | c.listeners = append(c.listeners[:i], c.listeners[i+1:]...) 205 | } 206 | } 207 | } 208 | } 209 | 210 | func (c *clientImpl) AddPlugins(plugins ...Plugin) { 211 | c.pluginsMu.Lock() 212 | defer c.pluginsMu.Unlock() 213 | c.plugins = append(c.plugins, plugins...) 214 | } 215 | 216 | func (c *clientImpl) ForPlugins(pluginFunc func(plugin Plugin)) { 217 | c.pluginsMu.Lock() 218 | defer c.pluginsMu.Unlock() 219 | for _, plugin := range c.plugins { 220 | pluginFunc(plugin) 221 | } 222 | } 223 | 224 | func (c *clientImpl) RemovePlugins(plugins ...Plugin) { 225 | c.pluginsMu.Lock() 226 | defer c.pluginsMu.Unlock() 227 | for _, plugin := range plugins { 228 | for i, pl := range c.plugins { 229 | if pl == plugin { 230 | c.plugins = append(c.plugins[:i], c.plugins[i+1:]...) 231 | } 232 | } 233 | } 234 | } 235 | 236 | func (c *clientImpl) UserID() snowflake.ID { 237 | return c.userID 238 | } 239 | 240 | func (c *clientImpl) Close() { 241 | c.nodesMu.Lock() 242 | defer c.nodesMu.Unlock() 243 | for _, node := range c.nodes { 244 | node.Close() 245 | } 246 | } 247 | 248 | func (c *clientImpl) OnVoiceServerUpdate(ctx context.Context, guildID snowflake.ID, token string, endpoint string) { 249 | c.Player(guildID).OnVoiceServerUpdate(ctx, token, endpoint) 250 | } 251 | 252 | func (c *clientImpl) OnVoiceStateUpdate(ctx context.Context, guildID snowflake.ID, channelID *snowflake.ID, sessionID string) { 253 | c.Player(guildID).OnVoiceStateUpdate(ctx, channelID, sessionID) 254 | } 255 | -------------------------------------------------------------------------------- /disgolink/client_config.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/disgoorg/disgolink/v3/lavalink" 9 | ) 10 | 11 | func DefaultConfig() *Config { 12 | return &Config{ 13 | Logger: slog.Default(), 14 | HTTPClient: &http.Client{Timeout: 10 * time.Second}, 15 | } 16 | } 17 | 18 | type Config struct { 19 | Logger *slog.Logger 20 | HTTPClient *http.Client 21 | Listeners []EventListener 22 | Plugins []Plugin 23 | } 24 | 25 | type ConfigOpt func(config *Config) 26 | 27 | func (c *Config) Apply(opts []ConfigOpt) { 28 | for _, opt := range opts { 29 | opt(c) 30 | } 31 | } 32 | 33 | // WithLogger lets you inject your own logger implementing log.Logger 34 | func WithLogger(logger *slog.Logger) ConfigOpt { 35 | return func(config *Config) { 36 | config.Logger = logger 37 | } 38 | } 39 | 40 | func WithHTTPClient(httpClient *http.Client) ConfigOpt { 41 | return func(config *Config) { 42 | config.HTTPClient = httpClient 43 | } 44 | } 45 | 46 | func WithListeners(listeners ...EventListener) ConfigOpt { 47 | return func(config *Config) { 48 | config.Listeners = append(config.Listeners, listeners...) 49 | } 50 | } 51 | 52 | func WithListenerFunc[E lavalink.Message](listenerFunc func(p Player, e E)) ConfigOpt { 53 | return WithListeners(NewListenerFunc(listenerFunc)) 54 | } 55 | 56 | func WithPlugins(plugins ...Plugin) ConfigOpt { 57 | return func(config *Config) { 58 | config.Plugins = append(config.Plugins, plugins...) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /disgolink/event.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import "github.com/disgoorg/disgolink/v3/lavalink" 4 | 5 | type EventListener interface { 6 | OnEvent(player Player, event lavalink.Message) 7 | } 8 | 9 | func NewListenerFunc[E lavalink.Message](f func(p Player, e E)) EventListener { 10 | return &listenerFunc[E]{f: f} 11 | } 12 | 13 | type listenerFunc[E lavalink.Message] struct { 14 | f func(p Player, e E) 15 | } 16 | 17 | func (l *listenerFunc[E]) OnEvent(p Player, e lavalink.Message) { 18 | if event, ok := e.(E); ok { 19 | l.f(p, event) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /disgolink/info.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | const ( 8 | Name = "disgolink" 9 | Module = "github.com/disgoorg/disgolink/v3" 10 | GitHub = "https://github.com/disgoorg/disgo" 11 | ) 12 | 13 | var ( 14 | Version = getVersion() 15 | ) 16 | 17 | func getVersion() string { 18 | bi, ok := debug.ReadBuildInfo() 19 | if ok { 20 | for _, dep := range bi.Deps { 21 | if dep.Path == Module { 22 | return dep.Version 23 | } 24 | } 25 | } 26 | return "unknown" 27 | } 28 | -------------------------------------------------------------------------------- /disgolink/load_result.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import "github.com/disgoorg/disgolink/v3/lavalink" 4 | 5 | type AudioLoadResultHandler interface { 6 | TrackLoaded(track lavalink.Track) 7 | PlaylistLoaded(playlist lavalink.Playlist) 8 | SearchResultLoaded(tracks []lavalink.Track) 9 | NoMatches() 10 | LoadFailed(err error) 11 | } 12 | 13 | var _ AudioLoadResultHandler = (*FunctionalResultHandler)(nil) 14 | 15 | func NewResultHandler(trackLoaded func(track lavalink.Track), playlistLoaded func(playlist lavalink.Playlist), searchResultLoaded func(tracks []lavalink.Track), noMatches func(), loadFailed func(err error)) AudioLoadResultHandler { 16 | return FunctionalResultHandler{ 17 | trackLoaded: trackLoaded, 18 | playlistLoaded: playlistLoaded, 19 | searchResultLoaded: searchResultLoaded, 20 | noMatches: noMatches, 21 | loadFailed: loadFailed, 22 | } 23 | } 24 | 25 | type FunctionalResultHandler struct { 26 | trackLoaded func(track lavalink.Track) 27 | playlistLoaded func(playlist lavalink.Playlist) 28 | searchResultLoaded func(tracks []lavalink.Track) 29 | noMatches func() 30 | loadFailed func(err error) 31 | } 32 | 33 | func (h FunctionalResultHandler) TrackLoaded(track lavalink.Track) { 34 | if h.trackLoaded != nil { 35 | h.trackLoaded(track) 36 | } 37 | } 38 | func (h FunctionalResultHandler) PlaylistLoaded(playlist lavalink.Playlist) { 39 | if h.playlistLoaded != nil { 40 | h.playlistLoaded(playlist) 41 | } 42 | } 43 | func (h FunctionalResultHandler) SearchResultLoaded(tracks []lavalink.Track) { 44 | if h.searchResultLoaded != nil { 45 | h.searchResultLoaded(tracks) 46 | } 47 | } 48 | func (h FunctionalResultHandler) NoMatches() { 49 | if h.noMatches != nil { 50 | h.noMatches() 51 | } 52 | } 53 | func (h FunctionalResultHandler) LoadFailed(err error) { 54 | if h.loadFailed != nil { 55 | h.loadFailed(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /disgolink/node.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net" 9 | "net/http" 10 | "sync" 11 | "time" 12 | 13 | "github.com/gorilla/websocket" 14 | 15 | "github.com/disgoorg/disgolink/v3/lavalink" 16 | ) 17 | 18 | type Status string 19 | 20 | // Indicates how far along the client is to connecting 21 | const ( 22 | StatusConnecting Status = "CONNECTING" 23 | StatusConnected Status = "CONNECTED" 24 | StatusReconnecting Status = "RECONNECTING" 25 | StatusDisconnected Status = "DISCONNECTED" 26 | ) 27 | 28 | var ErrNodeAlreadyConnected = errors.New("node already connected") 29 | 30 | var _ Node = (*nodeImpl)(nil) 31 | 32 | type Node interface { 33 | Lavalink() Client 34 | Config() NodeConfig 35 | Rest() RestClient 36 | 37 | Stats() lavalink.Stats 38 | Status() Status 39 | SessionID() string 40 | 41 | Version(ctx context.Context) (string, error) 42 | Info(ctx context.Context) (*lavalink.Info, error) 43 | Update(ctx context.Context, update lavalink.SessionUpdate) error 44 | LoadTracks(ctx context.Context, identifier string) (*lavalink.LoadResult, error) 45 | LoadTracksHandler(ctx context.Context, identifier string, handler AudioLoadResultHandler) 46 | 47 | DecodeTrack(ctx context.Context, encodedTrack string) (*lavalink.Track, error) 48 | DecodeTracks(ctx context.Context, encodedTracks []string) ([]lavalink.Track, error) 49 | 50 | Open(ctx context.Context) error 51 | Close() 52 | } 53 | 54 | type NodeConfig struct { 55 | Name string `json:"name"` 56 | Address string `json:"address"` 57 | Password string `json:"password"` 58 | Secure bool `json:"secure"` 59 | SessionID string `json:"session_id"` 60 | } 61 | 62 | func (c NodeConfig) RestURL() string { 63 | scheme := "http" 64 | if c.Secure { 65 | scheme += "s" 66 | } 67 | 68 | return fmt.Sprintf("%s://%s", scheme, c.Address) 69 | } 70 | 71 | func (c NodeConfig) WsURL() string { 72 | scheme := "ws" 73 | if c.Secure { 74 | scheme += "s" 75 | } 76 | 77 | return fmt.Sprintf("%s://%s%s", scheme, c.Address, EndpointWebSocket) 78 | } 79 | 80 | type nodeImpl struct { 81 | logger *slog.Logger 82 | lavalink Client 83 | config NodeConfig 84 | rest RestClient 85 | 86 | conn *websocket.Conn 87 | connMu sync.Mutex 88 | 89 | status Status 90 | stats lavalink.Stats 91 | sessionID string 92 | } 93 | 94 | func (n *nodeImpl) Lavalink() Client { 95 | return n.lavalink 96 | } 97 | 98 | func (n *nodeImpl) Config() NodeConfig { 99 | return n.config 100 | } 101 | 102 | func (n *nodeImpl) Rest() RestClient { 103 | return n.rest 104 | } 105 | 106 | func (n *nodeImpl) Status() Status { 107 | return n.status 108 | } 109 | 110 | func (n *nodeImpl) Stats() lavalink.Stats { 111 | return n.stats 112 | } 113 | 114 | func (n *nodeImpl) SessionID() string { 115 | return n.sessionID 116 | } 117 | 118 | func (n *nodeImpl) Version(ctx context.Context) (string, error) { 119 | return n.rest.Version(ctx) 120 | } 121 | 122 | func (n *nodeImpl) Info(ctx context.Context) (*lavalink.Info, error) { 123 | return n.rest.Info(ctx) 124 | } 125 | 126 | func (n *nodeImpl) Update(ctx context.Context, update lavalink.SessionUpdate) error { 127 | session, err := n.rest.UpdateSession(ctx, n.sessionID, update) 128 | if session != nil && session.Resuming { 129 | n.config.SessionID = n.sessionID 130 | } 131 | return err 132 | } 133 | 134 | func (n *nodeImpl) LoadTracks(ctx context.Context, identifier string) (*lavalink.LoadResult, error) { 135 | return n.rest.LoadTracks(ctx, identifier) 136 | } 137 | 138 | func (n *nodeImpl) LoadTracksHandler(ctx context.Context, identifier string, handler AudioLoadResultHandler) { 139 | result, err := n.LoadTracks(ctx, identifier) 140 | if err != nil { 141 | handler.LoadFailed(err) 142 | return 143 | } 144 | 145 | switch d := result.Data.(type) { 146 | case lavalink.Track: 147 | handler.TrackLoaded(d) 148 | 149 | case lavalink.Playlist: 150 | handler.PlaylistLoaded(d) 151 | 152 | case lavalink.Search: 153 | handler.SearchResultLoaded(d) 154 | 155 | case lavalink.Empty: 156 | handler.NoMatches() 157 | 158 | case lavalink.Exception: 159 | handler.LoadFailed(d) 160 | } 161 | } 162 | 163 | func (n *nodeImpl) syncPlayers(ctx context.Context) error { 164 | players, err := n.rest.Players(ctx, n.sessionID) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | for _, player := range players { 170 | p := n.lavalink.PlayerOnNode(n, player.GuildID) 171 | if p == nil { 172 | continue 173 | } 174 | p.Restore(player) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (n *nodeImpl) DecodeTrack(ctx context.Context, encodedTrack string) (*lavalink.Track, error) { 181 | return n.rest.DecodeTrack(ctx, encodedTrack) 182 | } 183 | 184 | func (n *nodeImpl) DecodeTracks(ctx context.Context, encodedTracks []string) ([]lavalink.Track, error) { 185 | return n.rest.DecodeTracks(ctx, encodedTracks) 186 | } 187 | 188 | func (n *nodeImpl) Open(ctx context.Context) error { 189 | return n.reconnectTry(ctx, 0, false) 190 | } 191 | 192 | func (n *nodeImpl) open(ctx context.Context, reconnecting bool) error { 193 | n.logger.Debug("opening connection to node...") 194 | 195 | n.connMu.Lock() 196 | defer n.connMu.Unlock() 197 | if n.conn != nil { 198 | return ErrNodeAlreadyConnected 199 | } 200 | 201 | if reconnecting { 202 | n.status = StatusReconnecting 203 | } else { 204 | n.status = StatusConnecting 205 | } 206 | 207 | header := http.Header{ 208 | "Authorization": []string{n.config.Password}, 209 | "User-Id": []string{n.lavalink.UserID().String()}, 210 | "Client-Name": []string{fmt.Sprintf("%s/%s", Name, Version)}, 211 | } 212 | if n.config.SessionID != "" { 213 | header.Add("Session-Id", n.config.SessionID) 214 | } 215 | 216 | conn, _, err := websocket.DefaultDialer.DialContext(ctx, n.config.WsURL(), header) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | _, data, err := conn.ReadMessage() 222 | if err != nil { 223 | return err 224 | } 225 | 226 | message, err := lavalink.UnmarshalMessage(data) 227 | if err != nil { 228 | return fmt.Errorf("failed to unmarshal ready message. error: %w", err) 229 | } 230 | ready, ok := message.(lavalink.ReadyMessage) 231 | if !ok { 232 | return fmt.Errorf("expected ready message but got %T", message) 233 | } 234 | 235 | n.sessionID = ready.SessionID 236 | if n.config.SessionID != "" { 237 | if ready.Resumed { 238 | n.logger.InfoContext(ctx, "successfully resumed session", slog.String("session_id", n.config.SessionID)) 239 | if err = n.syncPlayers(ctx); err != nil { 240 | n.logger.Warn("failed to sync players: ", slog.Any("err", err)) 241 | } 242 | } else { 243 | n.logger.Warn("failed to resume session", slog.String("session_id", n.config.SessionID)) 244 | } 245 | } 246 | n.status = StatusConnected 247 | 248 | conn.SetCloseHandler(func(code int, text string) error { 249 | return nil 250 | }) 251 | 252 | n.conn = conn 253 | 254 | go n.listen(conn) 255 | 256 | n.Lavalink().ForPlugins(func(plugin Plugin) { 257 | if pl, ok := plugin.(PluginEventHandler); ok { 258 | pl.OnNodeOpen(n) 259 | } 260 | }) 261 | 262 | return nil 263 | } 264 | 265 | func (n *nodeImpl) Close() { 266 | n.Lavalink().ForPlugins(func(plugin Plugin) { 267 | if pl, ok := plugin.(PluginEventHandler); ok { 268 | pl.OnNodeClose(n) 269 | } 270 | }) 271 | n.status = StatusDisconnected 272 | if n.conn != nil { 273 | _ = n.conn.Close() 274 | n.conn = nil 275 | } 276 | 277 | } 278 | 279 | func (n *nodeImpl) reconnectTry(ctx context.Context, try int, reconnecting bool) error { 280 | delay := time.Duration(try) * 2 * time.Second 281 | if delay > 30*time.Second { 282 | delay = 30 * time.Second 283 | } 284 | 285 | timer := time.NewTimer(delay) 286 | defer timer.Stop() 287 | select { 288 | case <-ctx.Done(): 289 | timer.Stop() 290 | return ctx.Err() 291 | case <-timer.C: 292 | } 293 | 294 | if err := n.open(ctx, reconnecting); err != nil { 295 | if errors.Is(err, ErrNodeAlreadyConnected) { 296 | return err 297 | } 298 | n.logger.ErrorContext(ctx, "failed to reconnect node", slog.Any("err", err), slog.Int("try", try)) 299 | n.status = StatusDisconnected 300 | return n.reconnectTry(ctx, try+1, reconnecting) 301 | } 302 | return nil 303 | } 304 | 305 | func (n *nodeImpl) reconnect() { 306 | if err := n.reconnectTry(context.Background(), 0, true); err != nil { 307 | n.logger.Error("failed to reopen node", slog.Any("err", err)) 308 | } 309 | } 310 | 311 | func (n *nodeImpl) listen(conn *websocket.Conn) { 312 | defer n.logger.Debug("exiting listen goroutine") 313 | loop: 314 | for { 315 | _, data, err := conn.ReadMessage() 316 | if err != nil { 317 | n.connMu.Lock() 318 | sameConnection := n.conn == conn 319 | n.connMu.Unlock() 320 | 321 | if !sameConnection { 322 | return 323 | } 324 | 325 | reconnect := true 326 | if errors.Is(err, net.ErrClosed) { 327 | reconnect = false 328 | } 329 | 330 | n.Close() 331 | if reconnect { 332 | go n.reconnect() 333 | } 334 | break loop 335 | } 336 | 337 | n.logger.Debug("received message", slog.String("data", string(data))) 338 | 339 | n.Lavalink().ForPlugins(func(plugin Plugin) { 340 | if pl, ok := plugin.(PluginEventHandler); ok { 341 | pl.OnNodeMessageIn(n, data) 342 | } 343 | }) 344 | 345 | m, err := lavalink.UnmarshalMessage(data) 346 | if err != nil { 347 | n.logger.Error("error while unmarshalling ws data", slog.Any("err", err)) 348 | return 349 | } 350 | 351 | switch message := m.(type) { 352 | case lavalink.UnknownMessage: 353 | n.Lavalink().ForPlugins(func(plugin Plugin) { 354 | if pl, ok := plugin.(OpPlugin); ok { 355 | pl.OnOpInvocation(n, message.Data) 356 | } 357 | }) 358 | 359 | case lavalink.StatsMessage: 360 | n.stats = lavalink.Stats(message) 361 | n.lavalink.EmitEvent(nil, m) 362 | 363 | case lavalink.PlayerUpdateMessage: 364 | player := n.lavalink.ExistingPlayer(message.GuildID) 365 | if player == nil { 366 | continue 367 | } 368 | player.OnPlayerUpdate(message.State) 369 | n.lavalink.EmitEvent(player, m) 370 | 371 | case lavalink.Event: 372 | player := n.lavalink.ExistingPlayer(message.GuildID()) 373 | if player == nil { 374 | continue 375 | } 376 | player.OnEvent(message) 377 | n.lavalink.EmitEvent(player, m) 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /disgolink/player.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/disgoorg/snowflake/v2" 10 | 11 | "github.com/disgoorg/disgolink/v3/lavalink" 12 | ) 13 | 14 | var ErrPlayerNoNode = errors.New("player has no node") 15 | 16 | type Player interface { 17 | GuildID() snowflake.ID 18 | ChannelID() *snowflake.ID 19 | Track() *lavalink.Track 20 | Paused() bool 21 | Position() lavalink.Duration 22 | State() lavalink.PlayerState 23 | Volume() int 24 | Filters() lavalink.Filters 25 | 26 | Update(ctx context.Context, opts ...lavalink.PlayerUpdateOpt) error 27 | Destroy(ctx context.Context) error 28 | 29 | Lavalink() Client 30 | Node() Node 31 | 32 | Restore(player lavalink.Player) 33 | OnEvent(event lavalink.Event) 34 | OnPlayerUpdate(state lavalink.PlayerState) 35 | OnVoiceServerUpdate(ctx context.Context, token string, endpoint string) 36 | OnVoiceStateUpdate(ctx context.Context, channelID *snowflake.ID, sessionID string) 37 | } 38 | 39 | func NewPlayer(logger *slog.Logger, lavalink Client, node Node, guildID snowflake.ID) Player { 40 | return &playerImpl{ 41 | logger: logger.With(slog.String("name", "disgolink_player"), slog.Int64("guild_id", int64(guildID))), 42 | lavalink: lavalink, 43 | node: node, 44 | guildID: guildID, 45 | volume: 100, 46 | } 47 | } 48 | 49 | type playerImpl struct { 50 | logger *slog.Logger 51 | node Node 52 | lavalink Client 53 | 54 | guildID snowflake.ID 55 | channelID *snowflake.ID 56 | track *lavalink.Track 57 | volume int 58 | paused bool 59 | state lavalink.PlayerState 60 | voice lavalink.VoiceState 61 | filters lavalink.Filters 62 | } 63 | 64 | func (p *playerImpl) GuildID() snowflake.ID { 65 | return p.guildID 66 | } 67 | 68 | func (p *playerImpl) ChannelID() *snowflake.ID { 69 | return p.channelID 70 | } 71 | 72 | func (p *playerImpl) Track() *lavalink.Track { 73 | return p.track 74 | } 75 | 76 | func (p *playerImpl) Paused() bool { 77 | return p.paused 78 | } 79 | 80 | func (p *playerImpl) Position() lavalink.Duration { 81 | if p.track == nil { 82 | return 0 83 | } 84 | position := p.state.Position 85 | if p.paused { 86 | return position 87 | } 88 | position += lavalink.Duration(time.Now().UnixMilli() - p.state.Time.UnixMilli()) 89 | if position > p.track.Info.Length { 90 | position = p.track.Info.Length 91 | } else if position < 0 { 92 | position = 0 93 | } 94 | return position 95 | } 96 | 97 | func (p *playerImpl) State() lavalink.PlayerState { 98 | return p.state 99 | } 100 | 101 | func (p *playerImpl) Volume() int { 102 | return p.volume 103 | } 104 | 105 | func (p *playerImpl) Filters() lavalink.Filters { 106 | return p.filters 107 | } 108 | 109 | func (p *playerImpl) Update(ctx context.Context, opts ...lavalink.PlayerUpdateOpt) error { 110 | if p.node == nil { 111 | return ErrPlayerNoNode 112 | } 113 | 114 | update := lavalink.DefaultPlayerUpdate() 115 | update.Apply(opts) 116 | 117 | updatedPlayer, err := p.node.Rest().UpdatePlayer(ctx, p.node.SessionID(), p.guildID, *update) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | p.volume = updatedPlayer.Volume 123 | 124 | p.voice = updatedPlayer.Voice 125 | p.filters = updatedPlayer.Filters 126 | 127 | // dispatch artificial player resume/pause event 128 | if update.Paused != nil { 129 | var event lavalink.Event 130 | if p.paused && !*update.Paused { 131 | event = lavalink.PlayerResumeEvent{ 132 | GuildID_: p.guildID, 133 | } 134 | } else if !p.paused && *update.Paused { 135 | event = lavalink.PlayerPauseEvent{ 136 | GuildID_: p.guildID, 137 | } 138 | } 139 | p.paused = updatedPlayer.Paused 140 | go p.OnEvent(event) 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (p *playerImpl) Destroy(ctx context.Context) error { 147 | if p.node == nil { 148 | return ErrPlayerNoNode 149 | } 150 | 151 | err := p.node.Rest().DestroyPlayer(ctx, p.node.SessionID(), p.guildID) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | // check if this player already got destroyed 157 | if player := p.lavalink.ExistingPlayer(p.guildID); player == nil { 158 | return nil 159 | } 160 | 161 | p.lavalink.ForPlugins(func(plugin Plugin) { 162 | if pl, ok := plugin.(PluginEventHandler); ok { 163 | pl.OnDestroyPlayer(p) 164 | } 165 | }) 166 | 167 | p.lavalink.RemovePlayer(p.guildID) 168 | 169 | return nil 170 | } 171 | 172 | func (p *playerImpl) Node() Node { 173 | if p.node == nil { 174 | p.node = p.lavalink.BestNode() 175 | } 176 | return p.node 177 | } 178 | 179 | func (p *playerImpl) Lavalink() Client { 180 | return p.lavalink 181 | } 182 | 183 | func (p *playerImpl) Restore(player lavalink.Player) { 184 | p.track = player.Track 185 | p.state = player.State 186 | p.paused = player.Paused 187 | p.voice = player.Voice 188 | p.filters = player.Filters 189 | p.volume = player.Volume 190 | } 191 | 192 | func (p *playerImpl) OnEvent(event lavalink.Event) { 193 | switch e := event.(type) { 194 | case lavalink.UnknownEvent: 195 | p.lavalink.ForPlugins(func(plugin Plugin) { 196 | if pl, ok := plugin.(EventPlugin); ok && pl.Event() == e.Type() { 197 | pl.OnEventInvocation(p, e.Data) 198 | } 199 | if pl, ok := plugin.(EventPlugins); ok { 200 | for _, pls := range pl.EventPlugins() { 201 | if pls.Event() == e.Type() { 202 | pls.OnEventInvocation(p, e.Data) 203 | } 204 | } 205 | } 206 | }) 207 | case lavalink.PlayerPauseEvent: 208 | p.paused = true 209 | 210 | case lavalink.PlayerResumeEvent: 211 | p.paused = false 212 | 213 | case lavalink.TrackStartEvent: 214 | p.track = &e.Track 215 | 216 | case lavalink.TrackEndEvent: 217 | p.track = nil 218 | 219 | case lavalink.WebSocketClosedEvent: 220 | p.voice = lavalink.VoiceState{} 221 | p.state.Connected = false 222 | } 223 | } 224 | 225 | func (p *playerImpl) OnPlayerUpdate(state lavalink.PlayerState) { 226 | p.state = state 227 | } 228 | 229 | func (p *playerImpl) OnVoiceServerUpdate(ctx context.Context, token string, endpoint string) { 230 | if _, err := p.Node().Rest().UpdatePlayer(ctx, p.node.SessionID(), p.guildID, lavalink.PlayerUpdate{ 231 | Voice: &lavalink.VoiceState{ 232 | Token: token, 233 | Endpoint: endpoint, 234 | SessionID: p.voice.SessionID, 235 | }, 236 | }); err != nil { 237 | p.logger.ErrorContext(ctx, "error while sending voice server update", slog.Any("err", err)) 238 | } 239 | p.voice.Token = token 240 | p.voice.Endpoint = endpoint 241 | } 242 | 243 | func (p *playerImpl) OnVoiceStateUpdate(ctx context.Context, channelID *snowflake.ID, sessionID string) { 244 | if channelID == nil { 245 | p.channelID = nil 246 | if err := p.Destroy(ctx); err != nil { 247 | p.logger.ErrorContext(ctx, "error while destroying player", slog.Any("err", err)) 248 | } 249 | p.lavalink.RemovePlayer(p.guildID) 250 | return 251 | } 252 | p.channelID = channelID 253 | p.voice.SessionID = sessionID 254 | } 255 | -------------------------------------------------------------------------------- /disgolink/plugin.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import "github.com/disgoorg/disgolink/v3/lavalink" 4 | 5 | type Plugin interface { 6 | Name() string 7 | Version() string 8 | } 9 | 10 | type OpPlugin interface { 11 | Op() lavalink.Op 12 | OnOpInvocation(node Node, data []byte) 13 | } 14 | 15 | type EventPlugin interface { 16 | Event() lavalink.EventType 17 | OnEventInvocation(player Player, data []byte) 18 | } 19 | 20 | type EventPlugins interface { 21 | EventPlugins() []EventPlugin 22 | } 23 | 24 | type PluginEventHandler interface { 25 | OnNodeOpen(node Node) 26 | OnNodeClose(node Node) 27 | OnNodeMessageIn(node Node, data []byte) 28 | OnNewPlayer(player Player) 29 | OnDestroyPlayer(player Player) 30 | } 31 | -------------------------------------------------------------------------------- /disgolink/rest_client.go: -------------------------------------------------------------------------------- 1 | package disgolink 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "net/url" 12 | 13 | "github.com/disgoorg/snowflake/v2" 14 | 15 | "github.com/disgoorg/disgolink/v3/lavalink" 16 | ) 17 | 18 | type Endpoint string 19 | 20 | func (e Endpoint) Format(a ...any) string { 21 | return fmt.Sprintf(string(e), a...) 22 | } 23 | 24 | var ( 25 | EndpointBase Endpoint = "/v4" 26 | EndpointVersion Endpoint = "/version" 27 | EndpointInfo = EndpointBase + "/info" 28 | EndpointStats = EndpointBase + "/stats" 29 | 30 | EndpointUpdateSession = EndpointBase + "/sessions/%s" 31 | EndpointPlayers = EndpointBase + "/sessions/%s/players" 32 | EndpointPlayer = EndpointBase + "/sessions/%s/players/%s" 33 | EndpointUpdatePlayer = EndpointBase + "/sessions/%s/players/%s?noReplace=%t" 34 | EndpointDestroyPlayer = EndpointBase + "/sessions/%s/players/%s" 35 | 36 | EndpointLoadTracks = EndpointBase + "/loadtracks?identifier=%s" 37 | EndpointDecodeTrack = EndpointBase + "/decodetrack?track=%s" 38 | EndpointDecodeTracks = EndpointBase + "/decodetracks" 39 | 40 | EndpointWebSocket = EndpointBase + "/websocket" 41 | ) 42 | 43 | type RestClient interface { 44 | // Do executes a http.Request and replaces the host and scheme with the node's config. It also sets the Authorization header to the node's password. It returns the http.Response or an error 45 | Do(rq *http.Request) (*http.Response, error) 46 | 47 | Version(ctx context.Context) (string, error) 48 | Info(ctx context.Context) (*lavalink.Info, error) 49 | Stats(ctx context.Context) (*lavalink.Stats, error) 50 | 51 | UpdateSession(ctx context.Context, sessionID string, sessionUpdate lavalink.SessionUpdate) (*lavalink.Session, error) 52 | 53 | Players(ctx context.Context, sessionID string) ([]lavalink.Player, error) 54 | Player(ctx context.Context, sessionID string, guildID snowflake.ID) (*lavalink.Player, error) 55 | UpdatePlayer(ctx context.Context, sessionID string, guildID snowflake.ID, playerUpdate lavalink.PlayerUpdate) (*lavalink.Player, error) 56 | DestroyPlayer(ctx context.Context, sessionID string, guildID snowflake.ID) error 57 | 58 | LoadTracks(ctx context.Context, identifier string) (*lavalink.LoadResult, error) 59 | DecodeTrack(ctx context.Context, encodedTrack string) (*lavalink.Track, error) 60 | DecodeTracks(ctx context.Context, encodedTracks []string) ([]lavalink.Track, error) 61 | } 62 | 63 | type restClientImpl struct { 64 | logger *slog.Logger 65 | node Node 66 | httpClient *http.Client 67 | } 68 | 69 | func (c *restClientImpl) Version(ctx context.Context) (string, error) { 70 | _, rawBody, err := c.do(ctx, http.MethodGet, string(EndpointVersion), nil) 71 | if err != nil { 72 | return "", err 73 | } 74 | return string(rawBody), nil 75 | } 76 | 77 | func (c *restClientImpl) Info(ctx context.Context) (info *lavalink.Info, err error) { 78 | err = c.doJSON(ctx, http.MethodGet, string(EndpointInfo), nil, &info) 79 | return 80 | } 81 | 82 | func (c *restClientImpl) Stats(ctx context.Context) (stats *lavalink.Stats, err error) { 83 | err = c.doJSON(ctx, http.MethodGet, string(EndpointStats), nil, &stats) 84 | return 85 | } 86 | 87 | func (c *restClientImpl) UpdateSession(ctx context.Context, sessionID string, sessionUpdate lavalink.SessionUpdate) (session *lavalink.Session, err error) { 88 | err = c.doJSON(ctx, http.MethodPatch, EndpointUpdateSession.Format(sessionID), sessionUpdate, &session) 89 | return 90 | } 91 | 92 | func (c *restClientImpl) Players(ctx context.Context, sessionID string) (players []lavalink.Player, err error) { 93 | err = c.doJSON(ctx, http.MethodGet, EndpointPlayers.Format(sessionID), nil, &players) 94 | return 95 | } 96 | 97 | func (c *restClientImpl) Player(ctx context.Context, sessionID string, guildID snowflake.ID) (player *lavalink.Player, err error) { 98 | err = c.doJSON(ctx, http.MethodGet, EndpointPlayer.Format(sessionID, guildID), nil, &player) 99 | return 100 | } 101 | 102 | func (c *restClientImpl) UpdatePlayer(ctx context.Context, sessionID string, guildID snowflake.ID, playerUpdate lavalink.PlayerUpdate) (player *lavalink.Player, err error) { 103 | err = c.doJSON(ctx, http.MethodPatch, EndpointUpdatePlayer.Format(sessionID, guildID, playerUpdate.NoReplace), playerUpdate, &player) 104 | return 105 | } 106 | 107 | func (c *restClientImpl) DestroyPlayer(ctx context.Context, sessionID string, guildID snowflake.ID) error { 108 | _, _, err := c.do(ctx, http.MethodDelete, EndpointDestroyPlayer.Format(sessionID, guildID), nil) 109 | return err 110 | } 111 | 112 | func (c *restClientImpl) LoadTracks(ctx context.Context, identifier string) (result *lavalink.LoadResult, err error) { 113 | err = c.doJSON(ctx, http.MethodGet, EndpointLoadTracks.Format(url.QueryEscape(identifier)), nil, &result) 114 | return 115 | } 116 | 117 | func (c *restClientImpl) DecodeTrack(ctx context.Context, encodedTrack string) (track *lavalink.Track, err error) { 118 | err = c.doJSON(ctx, http.MethodGet, EndpointDecodeTrack.Format(url.QueryEscape(encodedTrack)), nil, &track) 119 | return 120 | } 121 | 122 | func (c *restClientImpl) DecodeTracks(ctx context.Context, encodedTracks []string) (tracks []lavalink.Track, err error) { 123 | err = c.doJSON(ctx, http.MethodPost, string(EndpointDecodeTracks), encodedTracks, &tracks) 124 | return 125 | } 126 | 127 | func (c *restClientImpl) Do(rq *http.Request) (*http.Response, error) { 128 | rq.Header.Set("Authorization", c.node.Config().Password) 129 | rq.URL.Host = c.node.Config().Address 130 | if c.node.Config().Secure { 131 | rq.URL.Scheme = "https" 132 | } else { 133 | rq.URL.Scheme = "http" 134 | } 135 | return c.httpClient.Do(rq) 136 | } 137 | 138 | func (c *restClientImpl) do(ctx context.Context, method string, path string, rqBody []byte) (int, []byte, error) { 139 | rq, err := http.NewRequestWithContext(ctx, method, c.node.Config().RestURL()+path, bytes.NewReader(rqBody)) 140 | if err != nil { 141 | return 0, nil, err 142 | } 143 | rq.Header.Set("Authorization", c.node.Config().Password) 144 | if len(rqBody) > 0 { 145 | rq.Header.Set("Content-Type", "application/json") 146 | } 147 | 148 | c.logger.DebugContext(ctx, "sending request", slog.String("method", method), slog.String("path", path), slog.String("body", string(rqBody))) 149 | 150 | rs, err := c.httpClient.Do(rq) 151 | if err != nil { 152 | return 0, nil, err 153 | } 154 | 155 | defer rs.Body.Close() 156 | rawBody, err := io.ReadAll(rs.Body) 157 | c.logger.DebugContext(ctx, "received response", slog.String("path", path), slog.Int("status_code", rs.StatusCode), slog.String("body", string(rawBody))) 158 | if err != nil { 159 | return rs.StatusCode, nil, fmt.Errorf("failed to read response body: %w", err) 160 | } 161 | 162 | if rs.StatusCode >= http.StatusBadRequest { 163 | var lavalinkErr lavalink.Error 164 | if err = json.Unmarshal(rawBody, &lavalinkErr); err != nil { 165 | return rs.StatusCode, rawBody, fmt.Errorf("error while unmarshalling disgolink error: %w", err) 166 | } 167 | return rs.StatusCode, nil, lavalinkErr 168 | } 169 | 170 | return rs.StatusCode, rawBody, nil 171 | } 172 | 173 | func (c *restClientImpl) doJSON(ctx context.Context, method string, path string, rqBody any, rsBody any) error { 174 | var rawRqBody []byte 175 | if rqBody != nil { 176 | var err error 177 | rawRqBody, err = json.Marshal(rqBody) 178 | if err != nil { 179 | return fmt.Errorf("failed to marshal request body: %w", err) 180 | } 181 | } 182 | statusCode, rawBody, err := c.do(ctx, method, path, rawRqBody) 183 | if err != nil { 184 | return err 185 | } 186 | if statusCode != http.StatusNoContent { 187 | if err = json.Unmarshal(rawBody, rsBody); err != nil { 188 | return fmt.Errorf("failed to unmarshal response body: %w", err) 189 | } 190 | } 191 | return json.Unmarshal(rawBody, rsBody) 192 | } 193 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/disgoorg/disgolink/v3 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/disgoorg/json v1.2.0 7 | github.com/disgoorg/snowflake/v2 v2.0.3 8 | github.com/gorilla/websocket v1.5.3 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= 4 | github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= 5 | github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= 6 | github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= 7 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 8 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 12 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /lavalink/duration.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type Duration int64 8 | 9 | const ( 10 | Millisecond Duration = 1 11 | Second = 1000 * Millisecond 12 | Minute = 60 * Second 13 | Hour = 60 * Minute 14 | Day = 24 * Hour 15 | ) 16 | 17 | func (d Duration) Milliseconds() int64 { 18 | return int64(d) 19 | } 20 | 21 | func (d Duration) MillisecondsPart() int64 { 22 | return int64(Duration(d.Milliseconds()) % 1000) 23 | } 24 | 25 | func (d Duration) Seconds() int64 { 26 | return int64(d / Second) 27 | } 28 | 29 | func (d Duration) SecondsPart() int64 { 30 | return int64(Duration(d.Seconds()) % 60) 31 | } 32 | 33 | func (d Duration) Minutes() int64 { 34 | return int64(d / Minute) 35 | } 36 | 37 | func (d Duration) MinutesPart() int64 { 38 | return int64(Duration(d.Minutes()) % 60) 39 | } 40 | 41 | func (d Duration) Hours() int64 { 42 | return int64(d / Hour) 43 | } 44 | 45 | func (d Duration) HoursPart() int64 { 46 | return int64(Duration(d.Hours()) % 24) 47 | } 48 | 49 | func (d Duration) Days() int64 { 50 | return int64(d / Day) 51 | } 52 | 53 | func (d Duration) String() string { 54 | if d == 0 { 55 | return "0ms" 56 | } 57 | var str string 58 | if days := d.Days(); days > 0 { 59 | str += strconv.FormatInt(days, 10) + "d" 60 | } 61 | if hours := d.HoursPart(); hours > 0 { 62 | str += strconv.FormatInt(hours, 10) + "h" 63 | } 64 | if minutes := d.MinutesPart(); minutes > 0 { 65 | str += strconv.FormatInt(minutes, 10) + "m" 66 | } 67 | if seconds := d.SecondsPart(); seconds > 0 { 68 | str += strconv.FormatInt(seconds, 10) + "s" 69 | } 70 | if milliseconds := d.MillisecondsPart(); milliseconds > 0 { 71 | str += strconv.FormatInt(milliseconds, 10) + "ms" 72 | } 73 | return str 74 | } 75 | -------------------------------------------------------------------------------- /lavalink/duration_test.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDuration_Milliseconds(t *testing.T) { 10 | milliseconds := Millisecond * 2 11 | assert.Equal(t, int64(2), milliseconds.Milliseconds()) 12 | } 13 | 14 | func TestDuration_MillisecondsPart(t *testing.T) { 15 | seconds := Duration(2345) 16 | assert.Equal(t, int64(345), seconds.MillisecondsPart()) 17 | } 18 | 19 | func TestDuration_Seconds(t *testing.T) { 20 | seconds := Second * 2 21 | assert.Equal(t, int64(2), seconds.Seconds()) 22 | } 23 | 24 | func TestDuration_SecondsPart(t *testing.T) { 25 | seconds := Duration(2345) 26 | assert.Equal(t, int64(2), seconds.SecondsPart()) 27 | } 28 | 29 | func TestDuration_Minutes(t *testing.T) { 30 | minutes := Minute * 2 31 | assert.Equal(t, int64(2), minutes.Minutes()) 32 | } 33 | 34 | func TestDuration_MinutesPart(t *testing.T) { 35 | minutes := Duration(123456) 36 | assert.Equal(t, int64(2), minutes.MinutesPart()) 37 | } 38 | 39 | func TestDuration_Hours(t *testing.T) { 40 | hours := Hour * 2 41 | assert.Equal(t, int64(2), hours.Hours()) 42 | } 43 | 44 | func TestDuration_HoursPart(t *testing.T) { 45 | hours := Duration(7234567) 46 | assert.Equal(t, int64(2), hours.HoursPart()) 47 | } 48 | 49 | func TestDuration_Days(t *testing.T) { 50 | days := Day * 2 51 | assert.Equal(t, int64(2), days.Days()) 52 | } 53 | -------------------------------------------------------------------------------- /lavalink/error.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Error struct { 8 | Timestamp Timestamp `json:"timestamp"` 9 | Status int `json:"status"` 10 | StatusError string `json:"error"` 11 | Trace string `json:"trace"` 12 | Message string `json:"message"` 13 | Path string `json:"path"` 14 | } 15 | 16 | func (e Error) Error() string { 17 | if e.Message != "" { 18 | return e.Message 19 | } 20 | return fmt.Sprintf("%s: %d - %s", e.Path, e.Status, e.StatusError) 21 | } 22 | -------------------------------------------------------------------------------- /lavalink/filters.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | var DefaultFilters = []string{"volume", "equalizer", "timescale", "tremolo", "vibrato", "rotation", "karaoke", "distortion", "channelMix", "lowPass"} 8 | 9 | type Filters struct { 10 | Volume *Volume `json:"volume,omitempty"` 11 | Equalizer *Equalizer `json:"equalizer,omitempty"` 12 | Timescale *Timescale `json:"timescale,omitempty"` 13 | Tremolo *Tremolo `json:"tremolo,omitempty"` 14 | Vibrato *Vibrato `json:"vibrato,omitempty"` 15 | Rotation *Rotation `json:"rotation,omitempty"` 16 | Karaoke *Karaoke `json:"karaoke,omitempty"` 17 | Distortion *Distortion `json:"distortion,omitempty"` 18 | ChannelMix *ChannelMix `json:"channelMix,omitempty"` 19 | LowPass *LowPass `json:"lowPass,omitempty"` 20 | PluginFilters map[string]any `json:"pluginFilters,omitempty"` 21 | } 22 | 23 | type LowPass struct { 24 | Smoothing float64 `json:"smoothing"` 25 | } 26 | 27 | type ChannelMix struct { 28 | LeftToLeft float32 `json:"leftToLeft"` 29 | LeftToRight float32 `json:"leftToRight"` 30 | RightToLeft float32 `json:"rightToLeft"` 31 | RightToRight float32 `json:"rightToRight"` 32 | } 33 | 34 | type Distortion struct { 35 | SinOffset float32 `json:"sinOffset"` 36 | SinScale float32 `json:"sinScale"` 37 | CosOffset float32 `json:"cosOffset"` 38 | CosScale float32 `json:"cosScale"` 39 | TanOffset float32 `json:"tanOffset"` 40 | TanScale float32 `json:"tanScale"` 41 | Offset float32 `json:"offset"` 42 | Scale float32 `json:"scale"` 43 | } 44 | 45 | type Vibrato struct { 46 | Frequency float32 `json:"frequency"` 47 | Depth float32 `json:"depth"` 48 | } 49 | 50 | type Karaoke struct { 51 | Level float32 `json:"level"` 52 | MonoLevel float32 `json:"monoLevel"` 53 | FilterBand float32 `json:"filterBand"` 54 | FilterWidth float32 `json:"filterWidth"` 55 | } 56 | 57 | type Rotation struct { 58 | RotationHz int `json:"rotationHz"` 59 | } 60 | 61 | type Timescale struct { 62 | Speed float64 `json:"speed"` 63 | Pitch float64 `json:"pitch"` 64 | Rate float64 `json:"rate"` 65 | } 66 | 67 | type Tremolo struct { 68 | Frequency float32 `json:"frequency"` 69 | Depth float32 `json:"depth"` 70 | } 71 | 72 | type Volume float32 73 | 74 | type Equalizer [15]float32 75 | 76 | type EqBand struct { 77 | Band int `json:"band"` 78 | Gain float32 `json:"gain"` 79 | } 80 | 81 | func (e *Equalizer) UnmarshalJSON(data []byte) error { 82 | var bands [15]EqBand 83 | if err := json.Unmarshal(data, &bands); err != nil { 84 | return err 85 | } 86 | for _, band := range bands { 87 | e[band.Band] = band.Gain 88 | } 89 | return nil 90 | } 91 | 92 | // MarshalJSON marshals the map as object array 93 | func (e Equalizer) MarshalJSON() ([]byte, error) { 94 | var bands [15]EqBand 95 | for band, gain := range e { 96 | bands[band] = EqBand{ 97 | Band: band, 98 | Gain: gain, 99 | } 100 | } 101 | return json.Marshal(bands) 102 | } 103 | -------------------------------------------------------------------------------- /lavalink/info.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | type Info struct { 4 | Version Version `json:"version"` 5 | BuildTime Timestamp `json:"buildTime"` 6 | Git Git `json:"git"` 7 | JVM string `json:"jvm"` 8 | Lavaplayer string `json:"lavaplayer"` 9 | SourceManagers []string `json:"sourceManagers"` 10 | Filters []string `json:"filters"` 11 | Plugins []Plugin `json:"plugins"` 12 | } 13 | 14 | type Version struct { 15 | Semver string `json:"semver"` 16 | Major int `json:"major"` 17 | Minor int `json:"minor"` 18 | Patch int `json:"patch"` 19 | PreRelease string `json:"preRelease"` 20 | } 21 | 22 | type Git struct { 23 | Branch string `json:"branch"` 24 | Commit string `json:"commit"` 25 | CommitTime Timestamp `json:"commitTime"` 26 | } 27 | -------------------------------------------------------------------------------- /lavalink/load_result.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/disgoorg/json" 7 | ) 8 | 9 | type LoadType string 10 | 11 | const ( 12 | LoadTypeTrack LoadType = "track" 13 | LoadTypePlaylist LoadType = "playlist" 14 | LoadTypeSearch LoadType = "search" 15 | LoadTypeEmpty LoadType = "empty" 16 | LoadTypeError LoadType = "error" 17 | ) 18 | 19 | type LoadResultData interface { 20 | loadResultData() 21 | } 22 | 23 | type LoadResult struct { 24 | LoadType LoadType `json:"loadType"` 25 | Data LoadResultData `json:"data"` 26 | } 27 | 28 | func (r *LoadResult) UnmarshalJSON(data []byte) error { 29 | var raw struct { 30 | LoadType LoadType `json:"loadType"` 31 | Data json.RawMessage `json:"data"` 32 | } 33 | if err := json.Unmarshal(data, &raw); err != nil { 34 | return err 35 | } 36 | r.LoadType = raw.LoadType 37 | switch raw.LoadType { 38 | case LoadTypeTrack: 39 | var track Track 40 | if err := json.Unmarshal(raw.Data, &track); err != nil { 41 | return err 42 | } 43 | r.Data = track 44 | case LoadTypePlaylist: 45 | var playlist Playlist 46 | if err := json.Unmarshal(raw.Data, &playlist); err != nil { 47 | return err 48 | } 49 | r.Data = playlist 50 | case LoadTypeSearch: 51 | var search Search 52 | if err := json.Unmarshal(raw.Data, &search); err != nil { 53 | return err 54 | } 55 | r.Data = search 56 | case LoadTypeEmpty: 57 | r.Data = Empty{} 58 | case LoadTypeError: 59 | var exception Exception 60 | if err := json.Unmarshal(raw.Data, &exception); err != nil { 61 | return err 62 | } 63 | r.Data = exception 64 | default: 65 | return fmt.Errorf("unknown load type %q", raw.LoadType) 66 | } 67 | return nil 68 | } 69 | 70 | var _ error = (*Exception)(nil) 71 | 72 | type Search []Track 73 | 74 | func (Search) loadResultData() {} 75 | 76 | type Empty struct{} 77 | 78 | func (Empty) loadResultData() {} 79 | 80 | type Exception struct { 81 | Message string `json:"message"` 82 | Severity Severity `json:"severity"` 83 | Cause string `json:"cause"` 84 | CauseStackTrace string `json:"causeStackTrace"` 85 | } 86 | 87 | func (Exception) loadResultData() {} 88 | 89 | func (e Exception) Error() string { 90 | return fmt.Sprintf("%s: %s", e.Severity, e.Message) 91 | } 92 | 93 | type Severity string 94 | 95 | const ( 96 | SeverityCommon Severity = "common" 97 | SeveritySuspicious Severity = "suspicious" 98 | SeverityFault Severity = "fault" 99 | ) 100 | -------------------------------------------------------------------------------- /lavalink/messages.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "github.com/disgoorg/json" 5 | "github.com/disgoorg/snowflake/v2" 6 | ) 7 | 8 | type Op string 9 | 10 | const ( 11 | OpReady Op = "ready" 12 | OpStats Op = "stats" 13 | OpPlayerUpdate Op = "playerUpdate" 14 | OpEvent Op = "event" 15 | ) 16 | 17 | type EventType string 18 | 19 | const ( 20 | EventTypeTrackStart EventType = "TrackStartEvent" 21 | EventTypeTrackEnd EventType = "TrackEndEvent" 22 | EventTypeTrackException EventType = "TrackExceptionEvent" 23 | EventTypeTrackStuck EventType = "TrackStuckEvent" 24 | EventTypeWebSocketClosed EventType = "WebSocketClosedEvent" 25 | EventTypePlayerPause EventType = "PlayerPauseEvent" // not actually sent by lavalink 26 | EventTypePlayerResume EventType = "PlayerResumeEvent" // not actually sent by lavalink 27 | ) 28 | 29 | func UnmarshalMessage(data []byte) (Message, error) { 30 | var v struct { 31 | Op Op `json:"op"` 32 | Event EventType `json:"type"` 33 | } 34 | if err := json.Unmarshal(data, &v); err != nil { 35 | return nil, err 36 | } 37 | 38 | var ( 39 | message Message 40 | err error 41 | ) 42 | 43 | switch v.Op { 44 | case OpReady: 45 | var m ReadyMessage 46 | err = json.Unmarshal(data, &m) 47 | message = m 48 | case OpStats: 49 | var m StatsMessage 50 | err = json.Unmarshal(data, &m) 51 | message = m 52 | case OpPlayerUpdate: 53 | var m PlayerUpdateMessage 54 | err = json.Unmarshal(data, &m) 55 | message = m 56 | case OpEvent: 57 | switch v.Event { 58 | case EventTypeTrackStart: 59 | var m TrackStartEvent 60 | err = json.Unmarshal(data, &m) 61 | message = m 62 | case EventTypeTrackEnd: 63 | var m TrackEndEvent 64 | err = json.Unmarshal(data, &m) 65 | message = m 66 | case EventTypeTrackException: 67 | var m TrackExceptionEvent 68 | err = json.Unmarshal(data, &m) 69 | message = m 70 | case EventTypeTrackStuck: 71 | var m TrackStuckEvent 72 | err = json.Unmarshal(data, &m) 73 | message = m 74 | case EventTypeWebSocketClosed: 75 | var m WebSocketClosedEvent 76 | err = json.Unmarshal(data, &m) 77 | message = m 78 | case EventTypePlayerPause: 79 | var m PlayerPauseEvent 80 | err = json.Unmarshal(data, &m) 81 | message = m 82 | case EventTypePlayerResume: 83 | var m PlayerResumeEvent 84 | err = json.Unmarshal(data, &m) 85 | message = m 86 | default: 87 | var m UnknownEvent 88 | err = json.Unmarshal(data, &m) 89 | message = m 90 | } 91 | default: 92 | var m UnknownMessage 93 | err = json.Unmarshal(data, &m) 94 | message = m 95 | } 96 | if err != nil { 97 | return nil, err 98 | } 99 | return message, nil 100 | } 101 | 102 | type Message interface { 103 | Op() Op 104 | } 105 | 106 | type ReadyMessage struct { 107 | Resumed bool `json:"resumed"` 108 | SessionID string `json:"sessionId"` 109 | } 110 | 111 | func (ReadyMessage) Op() Op { return OpReady } 112 | 113 | type PlayerUpdateMessage struct { 114 | State PlayerState `json:"state"` 115 | GuildID snowflake.ID `json:"guildId"` 116 | } 117 | 118 | func (PlayerUpdateMessage) Op() Op { return OpPlayerUpdate } 119 | 120 | type StatsMessage Stats 121 | 122 | func (StatsMessage) Op() Op { return OpStats } 123 | 124 | type UnknownMessage struct { 125 | Op_ Op `json:"op"` 126 | Data json.RawMessage `json:"-"` 127 | } 128 | 129 | func (m *UnknownMessage) UnmarshalJSON(data []byte) error { 130 | type unknownMessage UnknownMessage 131 | if err := json.Unmarshal(data, (*unknownMessage)(m)); err != nil { 132 | return err 133 | } 134 | m.Data = data 135 | return nil 136 | } 137 | 138 | func (m UnknownMessage) MarshalJSON() ([]byte, error) { 139 | return m.Data, nil 140 | } 141 | 142 | func (m UnknownMessage) Op() Op { return m.Op_ } 143 | 144 | type Event interface { 145 | Op() Op 146 | Type() EventType 147 | GuildID() snowflake.ID 148 | } 149 | 150 | type TrackStartEvent struct { 151 | Track Track `json:"track"` 152 | GuildID_ snowflake.ID `json:"guildId"` 153 | } 154 | 155 | func (TrackStartEvent) Op() Op { return OpEvent } 156 | func (TrackStartEvent) Type() EventType { return EventTypeTrackStart } 157 | func (e TrackStartEvent) GuildID() snowflake.ID { return e.GuildID_ } 158 | 159 | type TrackEndEvent struct { 160 | Track Track `json:"track"` 161 | Reason TrackEndReason `json:"reason"` 162 | GuildID_ snowflake.ID `json:"guildId"` 163 | } 164 | 165 | func (TrackEndEvent) Op() Op { return OpEvent } 166 | func (TrackEndEvent) Type() EventType { return EventTypeTrackEnd } 167 | func (e TrackEndEvent) GuildID() snowflake.ID { return e.GuildID_ } 168 | 169 | type TrackEndReason string 170 | 171 | const ( 172 | TrackEndReasonFinished TrackEndReason = "finished" 173 | TrackEndReasonLoadFailed TrackEndReason = "loadFailed" 174 | TrackEndReasonStopped TrackEndReason = "stopped" 175 | TrackEndReasonReplaced TrackEndReason = "replaced" 176 | TrackEndReasonCleanup TrackEndReason = "cleanup" 177 | ) 178 | 179 | func (e TrackEndReason) MayStartNext() bool { 180 | switch e { 181 | case TrackEndReasonFinished, TrackEndReasonLoadFailed: 182 | return true 183 | default: 184 | return false 185 | } 186 | } 187 | 188 | type TrackExceptionEvent struct { 189 | Track Track `json:"track"` 190 | Exception Exception `json:"exception"` 191 | GuildID_ snowflake.ID `json:"guildId"` 192 | } 193 | 194 | func (TrackExceptionEvent) Op() Op { return OpEvent } 195 | func (TrackExceptionEvent) Type() EventType { return EventTypeTrackException } 196 | func (e TrackExceptionEvent) GuildID() snowflake.ID { return e.GuildID_ } 197 | 198 | type TrackStuckEvent struct { 199 | Track Track `json:"track"` 200 | Threshold Duration `json:"thresholdMs"` 201 | GuildID_ snowflake.ID `json:"guildId"` 202 | } 203 | 204 | func (TrackStuckEvent) Op() Op { return OpEvent } 205 | func (TrackStuckEvent) Type() EventType { return EventTypeTrackStuck } 206 | func (e TrackStuckEvent) GuildID() snowflake.ID { return e.GuildID_ } 207 | 208 | type WebSocketClosedEvent struct { 209 | Code int `json:"code"` 210 | Reason string `json:"reason"` 211 | ByRemote bool `json:"byRemote"` 212 | GuildID_ snowflake.ID `json:"guildId"` 213 | } 214 | 215 | func (WebSocketClosedEvent) Op() Op { return OpEvent } 216 | func (WebSocketClosedEvent) Type() EventType { return EventTypeWebSocketClosed } 217 | func (e WebSocketClosedEvent) GuildID() snowflake.ID { return e.GuildID_ } 218 | 219 | type PlayerPauseEvent struct { 220 | GuildID_ snowflake.ID `json:"guildId"` 221 | } 222 | 223 | func (PlayerPauseEvent) Op() Op { return OpEvent } 224 | func (PlayerPauseEvent) Type() EventType { return EventTypePlayerPause } 225 | func (e PlayerPauseEvent) GuildID() snowflake.ID { return e.GuildID_ } 226 | 227 | type PlayerResumeEvent struct { 228 | GuildID_ snowflake.ID `json:"guildId"` 229 | } 230 | 231 | func (PlayerResumeEvent) Op() Op { return OpEvent } 232 | func (PlayerResumeEvent) Type() EventType { return EventTypePlayerResume } 233 | func (e PlayerResumeEvent) GuildID() snowflake.ID { return e.GuildID_ } 234 | 235 | type UnknownEvent struct { 236 | Type_ EventType `json:"type"` 237 | GuildID_ snowflake.ID `json:"guildId"` 238 | Data json.RawMessage `json:"-"` 239 | } 240 | 241 | func (e *UnknownEvent) UnmarshalJSON(data []byte) error { 242 | type unknownEvent UnknownEvent 243 | if err := json.Unmarshal(data, (*unknownEvent)(e)); err != nil { 244 | return err 245 | } 246 | e.Data = data 247 | return nil 248 | } 249 | 250 | func (e UnknownEvent) MarshalJSON() ([]byte, error) { 251 | return e.Data, nil 252 | } 253 | 254 | func (UnknownEvent) Op() Op { return OpEvent } 255 | func (e UnknownEvent) Type() EventType { return e.Type_ } 256 | func (e UnknownEvent) GuildID() snowflake.ID { return e.GuildID_ } 257 | -------------------------------------------------------------------------------- /lavalink/player.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "github.com/disgoorg/snowflake/v2" 5 | ) 6 | 7 | type Players []Player 8 | 9 | type Player struct { 10 | GuildID snowflake.ID `json:"guildId"` 11 | Track *Track `json:"track"` 12 | Volume int `json:"volume"` 13 | Paused bool `json:"paused"` 14 | State PlayerState `json:"state"` 15 | Voice VoiceState `json:"voice"` 16 | Filters Filters `json:"filters"` 17 | } 18 | 19 | type VoiceState struct { 20 | Token string `json:"token"` 21 | Endpoint string `json:"endpoint"` 22 | SessionID string `json:"sessionId"` 23 | } 24 | 25 | type PlayerState struct { 26 | Time Timestamp `json:"time"` 27 | Position Duration `json:"position"` 28 | Connected bool `json:"connected"` 29 | Ping int `json:"ping"` 30 | } 31 | -------------------------------------------------------------------------------- /lavalink/player_update.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import "github.com/disgoorg/json" 4 | 5 | func DefaultPlayerUpdate() *PlayerUpdate { 6 | return &PlayerUpdate{} 7 | } 8 | 9 | type PlayerUpdateTrack struct { 10 | Encoded *json.Nullable[string] `json:"encoded,omitempty"` 11 | Identifier *string `json:"identifier,omitempty"` 12 | UserData any `json:"userData,omitempty"` 13 | } 14 | 15 | type PlayerUpdate struct { 16 | Track *PlayerUpdateTrack `json:"track,omitempty"` 17 | Position *Duration `json:"position,omitempty"` 18 | EndTime *Duration `json:"endTime,omitempty"` 19 | Volume *int `json:"volume,omitempty"` 20 | Paused *bool `json:"paused,omitempty"` 21 | Voice *VoiceState `json:"voice,omitempty"` 22 | Filters *Filters `json:"filters,omitempty"` 23 | NoReplace bool `json:"-"` 24 | } 25 | 26 | type PlayerUpdateOpt func(update *PlayerUpdate) 27 | 28 | func (u *PlayerUpdate) Apply(opts []PlayerUpdateOpt) { 29 | for _, opt := range opts { 30 | opt(u) 31 | } 32 | } 33 | 34 | func WithNoReplace(noReplace bool) PlayerUpdateOpt { 35 | return func(update *PlayerUpdate) { 36 | update.NoReplace = noReplace 37 | } 38 | } 39 | 40 | func WithTrack(track Track) PlayerUpdateOpt { 41 | return func(update *PlayerUpdate) { 42 | WithEncodedTrack(track.Encoded)(update) 43 | WithTrackUserData(track.UserData)(update) 44 | } 45 | } 46 | 47 | func WithEncodedTrack(encodedTrack string) PlayerUpdateOpt { 48 | return func(update *PlayerUpdate) { 49 | if update.Track == nil { 50 | update.Track = &PlayerUpdateTrack{} 51 | } 52 | update.Track.Encoded = json.NewNullablePtr(encodedTrack) 53 | } 54 | } 55 | 56 | func WithNullTrack() PlayerUpdateOpt { 57 | return func(update *PlayerUpdate) { 58 | if update.Track == nil { 59 | update.Track = &PlayerUpdateTrack{} 60 | } 61 | update.Track.Encoded = json.NullPtr[string]() 62 | } 63 | } 64 | 65 | func WithTrackIdentifier(identifier string) PlayerUpdateOpt { 66 | return func(update *PlayerUpdate) { 67 | if update.Track == nil { 68 | update.Track = &PlayerUpdateTrack{} 69 | } 70 | update.Track.Identifier = &identifier 71 | } 72 | } 73 | 74 | func WithTrackUserData(userData any) PlayerUpdateOpt { 75 | return func(update *PlayerUpdate) { 76 | if update.Track == nil { 77 | update.Track = &PlayerUpdateTrack{} 78 | } 79 | update.Track.UserData = userData 80 | } 81 | } 82 | 83 | func WithPosition(position Duration) PlayerUpdateOpt { 84 | return func(update *PlayerUpdate) { 85 | update.Position = &position 86 | } 87 | } 88 | 89 | func WithEndTime(endTime Duration) PlayerUpdateOpt { 90 | return func(update *PlayerUpdate) { 91 | update.EndTime = &endTime 92 | } 93 | } 94 | 95 | func WithVolume(volume int) PlayerUpdateOpt { 96 | return func(update *PlayerUpdate) { 97 | update.Volume = &volume 98 | } 99 | } 100 | 101 | func WithPaused(paused bool) PlayerUpdateOpt { 102 | return func(update *PlayerUpdate) { 103 | update.Paused = &paused 104 | } 105 | } 106 | 107 | func WithVoice(voice VoiceState) PlayerUpdateOpt { 108 | return func(update *PlayerUpdate) { 109 | update.Voice = &voice 110 | } 111 | } 112 | 113 | func WithFilters(filters Filters) PlayerUpdateOpt { 114 | return func(update *PlayerUpdate) { 115 | update.Filters = &filters 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lavalink/playlist.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | type Playlist struct { 4 | Info PlaylistInfo `json:"info"` 5 | PluginInfo RawData `json:"pluginInfo"` 6 | Tracks []Track `json:"tracks"` 7 | } 8 | 9 | func (Playlist) loadResultData() {} 10 | 11 | type PlaylistInfo struct { 12 | Name string `json:"name"` 13 | SelectedTrack int `json:"selectedTrack"` 14 | } 15 | -------------------------------------------------------------------------------- /lavalink/plugin.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "github.com/disgoorg/json" 5 | ) 6 | 7 | type Plugins []Plugin 8 | 9 | type Plugin struct { 10 | Name string `json:"name"` 11 | Version string `json:"version"` 12 | } 13 | 14 | type RawData json.RawMessage 15 | 16 | func (p RawData) String() string { 17 | return string(p) 18 | } 19 | 20 | func (p RawData) Unmarshal(v any) error { 21 | return json.Unmarshal(p, v) 22 | } 23 | 24 | func (p *RawData) UnmarshalJSON(data []byte) error { 25 | return json.Unmarshal(data, (*json.RawMessage)(p)) 26 | } 27 | 28 | func (p RawData) MarshalJSON() ([]byte, error) { 29 | return json.Marshal(json.RawMessage(p)) 30 | } 31 | -------------------------------------------------------------------------------- /lavalink/search_type.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | type SearchType string 4 | 5 | // search prefixes 6 | const ( 7 | SearchTypeYouTube SearchType = "ytsearch" 8 | SearchTypeYouTubeMusic SearchType = "ytmsearch" 9 | SearchTypeSoundCloud SearchType = "scsearch" 10 | ) 11 | 12 | func (t SearchType) Apply(searchString string) string { 13 | return string(t) + ":" + searchString 14 | } 15 | -------------------------------------------------------------------------------- /lavalink/session.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | type Session struct { 4 | Resuming bool `json:"resuming"` 5 | Timeout int `json:"timeout"` 6 | } 7 | 8 | type SessionUpdate struct { 9 | Resuming *bool `json:"resuming,omitempty"` 10 | Timeout *int `json:"timeout,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /lavalink/stats.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | type Stats struct { 4 | Players int `json:"players"` 5 | PlayingPlayers int `json:"playingPlayers"` 6 | Uptime Duration `json:"uptime"` 7 | Memory Memory `json:"memory"` 8 | CPU CPU `json:"cpu"` 9 | FrameStats *FrameStats `json:"frameStats"` 10 | } 11 | 12 | func (s Stats) Better(stats Stats) bool { 13 | sLoad := int(s.CPU.SystemLoad / float64(s.CPU.Cores) * 100) 14 | statsLoad := int(stats.CPU.SystemLoad / float64(stats.CPU.Cores) * 100) 15 | 16 | return sLoad > statsLoad 17 | } 18 | 19 | type Memory struct { 20 | Free int `json:"free"` 21 | Used int `json:"used"` 22 | Allocated int `json:"allocated"` 23 | Reservable int `json:"reservable"` 24 | } 25 | 26 | type CPU struct { 27 | Cores int `json:"cores"` 28 | SystemLoad float64 `json:"systemLoad"` 29 | LavalinkLoad float64 `json:"lavalinkLoad"` 30 | } 31 | 32 | type FrameStats struct { 33 | Sent int `json:"sent"` 34 | Nulled int `json:"nulled"` 35 | Deficit int `json:"deficit"` 36 | } 37 | -------------------------------------------------------------------------------- /lavalink/timestamp.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | func Now() Timestamp { 9 | return Timestamp{ 10 | Time: time.Now(), 11 | } 12 | } 13 | 14 | type Timestamp struct { 15 | time.Time 16 | } 17 | 18 | func (t Timestamp) MarshalJSON() ([]byte, error) { 19 | return []byte(strconv.FormatInt(t.UnixMilli(), 10)), nil 20 | } 21 | 22 | func (t *Timestamp) UnmarshalJSON(data []byte) error { 23 | // Ignore null, like in the main JSON package. 24 | if string(data) == "null" { 25 | return nil 26 | } 27 | 28 | timestamp, err := strconv.ParseInt(string(data), 10, 64) 29 | if err != nil { 30 | return err 31 | } 32 | *t = Timestamp{Time: time.UnixMilli(timestamp)} 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /lavalink/track.go: -------------------------------------------------------------------------------- 1 | package lavalink 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/disgoorg/json" 10 | ) 11 | 12 | var ( 13 | _ driver.Valuer = (*Track)(nil) 14 | _ sql.Scanner = (*Track)(nil) 15 | ) 16 | 17 | type Track struct { 18 | Encoded string `json:"encoded"` 19 | Info TrackInfo `json:"info"` 20 | PluginInfo RawData `json:"pluginInfo"` 21 | UserData RawData `json:"userData"` 22 | } 23 | 24 | // WithUserData returns a copy of the Track with the given userData. 25 | func (t Track) WithUserData(userData any) (Track, error) { 26 | userDataRaw, err := json.Marshal(userData) 27 | if err != nil { 28 | return t, fmt.Errorf("failed to marshal userData: %w", err) 29 | } 30 | t.UserData = userDataRaw 31 | return t, nil 32 | } 33 | 34 | func (Track) loadResultData() {} 35 | 36 | func (t Track) Value() (driver.Value, error) { 37 | return json.Marshal(t) 38 | } 39 | 40 | func (t *Track) Scan(value interface{}) error { 41 | b, ok := value.([]byte) 42 | if !ok { 43 | return errors.New("type assertion to []byte failed") 44 | } 45 | 46 | return json.Unmarshal(b, &t) 47 | } 48 | 49 | type TrackInfo struct { 50 | Identifier string `json:"identifier"` 51 | Author string `json:"author"` 52 | Length Duration `json:"length"` 53 | IsStream bool `json:"isStream"` 54 | Title string `json:"title"` 55 | URI *string `json:"uri"` 56 | SourceName string `json:"sourceName"` 57 | Position Duration `json:"position"` 58 | ArtworkURL *string `json:"artworkUrl"` 59 | ISRC *string `json:"isrc"` 60 | } 61 | --------------------------------------------------------------------------------