26 Commits

Author SHA1 Message Date
c27d1f70da chore: version bump to 0.10.5
Some checks are pending
Rust / build (push) Waiting to run
Release Plz / Release-plz (push) Waiting to run
2024-08-15 20:13:42 +02:00
73b885e5d6 fix: version requirements for crates.io 2024-08-15 20:11:35 +02:00
8c0eb62d41 fix: yt-dlp errors 2024-08-15 20:05:52 +02:00
5bc86aca2b Merge pull request #15 from eRgo35/release-plz-2024-08-13T13-12-37Z
Some checks failed
Rust / build (push) Has been cancelled
Release Plz / Release-plz (push) Has been cancelled
chore: release v0.10.4
2024-08-13 15:25:20 +02:00
Michał Czyż
57a281a1d3 chore: release 2024-08-13 15:21:37 +02:00
2f4045d4a9 fix: added wip info for some commands 2024-08-13 15:20:54 +02:00
aa215daf28 chore: cargo-dist action setup 2024-08-13 15:11:07 +02:00
7fde11f615 chore: repo added to Cargo.toml 2024-08-13 15:07:50 +02:00
c8f86a2f9c chore: using cargo-dist for package building 2024-08-13 15:06:32 +02:00
d9b6c8fc4b Merge pull request #14 from eRgo35/release-plz-2024-08-13T12-51-00Z
chore: release v0.10.3
2024-08-13 14:56:39 +02:00
Michał Czyż
051ae857a7 chore: release 2024-08-13 14:51:01 +02:00
08fbdc828c ci: release-plz 2024-08-13 14:50:20 +02:00
aeb10d9baf ops: default branch yeet 2024-08-12 20:58:01 +02:00
af247aa6d5 ci: release please setup 2024-08-12 20:01:06 +02:00
eae957c6d7 Create ci.yml 2024-08-12 19:33:07 +02:00
5399f3ba6d oop 2024-08-12 19:31:51 +02:00
076dfd0a93 fixed release to build arm64 2024-08-12 19:08:59 +02:00
212abd9bdd chore: release 0.10.2 2024-08-12 18:46:53 +02:00
8d787f2a11 fix: crates bumped 2024-08-12 18:46:21 +02:00
486f403d10 chore: release 0.10.1 2024-08-12 18:20:24 +02:00
bfd301c9c2 fix: deps broken 2024-08-12 18:14:09 +02:00
670b8dd30e gh: actions 2024-08-12 18:13:59 +02:00
c607aaf403 updated image in README.md 2024-08-12 13:37:10 +02:00
f41fa7e09b updated README.md 2024-08-12 13:36:36 +02:00
10b7d4d7e3 queue system improved 2024-08-08 20:46:22 +02:00
772d5d2a97 spotify-parser is now a submodule 2024-08-05 16:03:52 +02:00
43 changed files with 666 additions and 2604 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: eRgo35

22
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "senpai" ]
pull_request:
branches: [ "senpai" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

27
.github/workflows/release-plz.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Release Plz
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- senpai
jobs:
release-plz:
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

281
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,281 @@
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.20.0/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/cargo-dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by cargo-dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to cargo dist
# - install-dist: expression to run to install cargo-dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
run: ${{ matrix.install_dist }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "cargo dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

9
.gitmodules vendored
View File

@@ -1,9 +0,0 @@
[submodule "libs/songbird"]
path = libs/songbird
url = git@github.com:eRgo35/songbird.git
[submodule "libs/serenity"]
path = libs/serenity
url = git@github.com:eRgo35/serenity.git
[submodule "libs/poise"]
path = libs/poise
url = git@github.com:eRgo35/poise.git

23
CHANGELOG.md Normal file
View File

@@ -0,0 +1,23 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.10.4](https://github.com/eRgo35/lyra/compare/v0.10.3...v0.10.4) - 2024-08-13
### Fixed
- added wip info for some commands
### Other
- cargo-dist action setup
- repo added to Cargo.toml
- using cargo-dist for package building
## [0.10.3](https://github.com/eRgo35/lyra/compare/v0.10.2...v0.10.3) - 2024-08-13
### Other
- release-plz
# Changelog

62
Cargo.lock generated
View File

@@ -149,21 +149,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -642,17 +627,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fancy-regex"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.0" version = "2.1.0"
@@ -1292,18 +1266,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "lib-spotify-parser"
version = "1.0.0"
dependencies = [
"regex",
"reqwest 0.12.5",
"scraper",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.155"
@@ -1349,12 +1311,10 @@ dependencies = [
[[package]] [[package]]
name = "lyra" name = "lyra"
version = "0.9.1" version = "0.10.5"
dependencies = [ dependencies = [
"dotenv", "dotenv",
"fancy-regex",
"json", "json",
"lib-spotify-parser",
"once_cell", "once_cell",
"openssl", "openssl",
"owoify", "owoify",
@@ -1366,6 +1326,7 @@ dependencies = [
"serde_json", "serde_json",
"serenity", "serenity",
"songbird", "songbird",
"spotify-parser",
"symphonia", "symphonia",
"tokio", "tokio",
"tracing", "tracing",
@@ -2827,9 +2788,8 @@ dependencies = [
[[package]] [[package]]
name = "songbird" name = "songbird"
version = "0.4.3" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/eRgo35/songbird#64a9b6ba5e591a1324390aa896b661633f4baa24"
checksum = "338dd182f9f084f583c4c0db38588e28a34778ecec288208cf0b61c378ac90d1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"audiopus", "audiopus",
@@ -2886,6 +2846,20 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "spotify-parser"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7b9d1d990125af02c6f611f2e8e34fd979310d43ae7ac5bd2cd357b87b13352"
dependencies = [
"regex",
"reqwest 0.12.5",
"scraper",
"serde",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "stable-vec" name = "stable-vec"
version = "0.4.1" version = "0.4.1"

View File

@@ -1,19 +1,19 @@
[package] [package]
name = "lyra" name = "lyra"
version = "0.9.1" version = "0.10.5"
authors = ["Michał Czyż <mike@c2yz.com>"] authors = ["Michał Czyż <mike@c2yz.com>"]
edition = "2021" edition = "2021"
description = "A featureful Discord bot written in Rust." description = "A featureful Discord bot written in Rust."
documentation = "https://lyra.c2yz.com/docs" documentation = "https://github.com/eRgo35/lyra"
repository = "https://github.com/eRgo35/lyra"
readme = "README.md" readme = "README.md"
homepage = "https://lyra.c2yz.com" homepage = "https://lyra.c2yz.com"
license-file = "LICENSE.md" license-file = "LICENSE.md"
keywords = ["discord", "bot", "rust", "music", "featureful"] keywords = ["discord", "bot", "rust", "music", "featureful"]
[dependencies] [dependencies]
lib-spotify-parser = { path = "./libs/spotify-parser" } spotify-parser = "1.0.1"
dotenv = "0.15.0" dotenv = "0.15.0"
fancy-regex = "0.13.0"
json = "0.12.4" json = "0.12.4"
openssl = { version = "0.10.66", features = ["vendored"] } openssl = { version = "0.10.66", features = ["vendored"] }
owoify = "0.1.5" owoify = "0.1.5"
@@ -23,9 +23,9 @@ regex = "1.10.6"
reqwest = { version = "0.11.27", features = ["json"] } reqwest = { version = "0.11.27", features = ["json"] }
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122" serde_json = "1.0.122"
songbird = { version = "0.4.3", default-features = true, features = [ songbird = { version = "~0.4.3", default-features = true, features = [
"builtin-queue", "builtin-queue",
] } ], git = "https://github.com/eRgo35/songbird" }
serenity = { default-features = true, features = [ serenity = { default-features = true, features = [
"cache", "cache",
"framework", "framework",
@@ -34,7 +34,6 @@ serenity = { default-features = true, features = [
"http", "http",
"rustls_backend", "rustls_backend",
], version = "0.12" } ], version = "0.12" }
symphonia = { version = "0.5.4", features = [ symphonia = { version = "0.5.4", features = [
"aac", "aac",
"adpcm", "adpcm",
@@ -53,3 +52,23 @@ once_cell = "1.19.0"
[patch.crates-io.serenity-voice-model] [patch.crates-io.serenity-voice-model]
git = "https://github.com/serenity-rs/serenity" git = "https://github.com/serenity-rs/serenity"
branch = "current" branch = "current"
# The profile that 'cargo dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.20.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["x86_64-unknown-linux-gnu"]
# Path that installers should place binaries in
install-path = "CARGO_HOME"
# Whether to install an updater program
install-updater = false

View File

@@ -1,52 +1,82 @@
# Lyra <h2 align="center">
<a href="https://lyra.c2yz.com" target="blank_">
![](assets/lyra-256.png) <img height="256" alt="Lyra" src="assets/lyra-256.png" />
</a>
Lyra is a music bot written in Rust. <br />
Lyra: a discord music bot written in rust :crab:
## Getting Started </h2>
Lyra is an open source, discord music bot written in Rust. Lyra is an open source, discord music bot written in Rust.
The idea behind this project is to allow a user to self-host one's own instance of the bot. The idea behind this project is to allow a user to self-host one's own instance of the bot.
User no longer has to rely on 3rd-parties to provide them a service.
The bot can be run on a desktop, a vps or a phone as it's just a simple binary.
User no longer has to rely on 3rd parties to provide them an invite link. Currently bot is still heavily in development!
The bot can be run even on a desktop or a phone because after compilation, it's just a simple binary. ## Getting started
As of now, the bot supports spotify url track recognition through a separate nodejs script. I plan to write the actual parser inside the bot iteself but as of now I postponed it into future release. Before you start, you need to create a discord bot and get a token.
You can do it [here](https://discord.com/developers/applications).
Slash commands are still work in progress! Currently bot is still heavily in development! After you create a bot, you need to invite it to your server.
## Setting up Then, head to download section and download the latest release (or compile it yourself).
After you download the binary, you need to create a `.env` file in the same directory as the binary.
Example can be found in `.env.example` file.
```
DISCORD_TOKEN=<YOUR_DISCORD_TOKEN>
PREFIX=<YOUR_PREFIX>
```
DISCORD_TOKEN is the token you got from discord developers page and PREFIX is the prefix you want to use for your bot.
Bot by default reacts only to the prefix. To enable slash commands, while the bot is running type `:register` in the chat (where `:` is your bot prefix).
## Features
- Music playback
- Audio effects (soon)
- Some multipurpose commands
- Slash commands
- Self-hosted
## Compilation
To compile the source code on your own, you need `rust` and `cargo` To compile the source code on your own, you need `rust` and `cargo`
To run a dev version use To run a dev version, `cd` into the project directory and type
```bash ```bash
$ cargo run $ cargo run
``` ```
To build a production version use To build a production version use
```bash ```bash
$ cargo build --release $ cargo build --release
``` ```
If you need an ARM version and just don't want to wait for ages for the program to compile, use If you need a version for a different system or architecture, you can use `cross` crate
```bash ```bash
$ cross build -r --target aarch64-unknown-linux-gnu $ cross build -r --target aarch64-unknown-linux-gnu
``` ```
To run a program, just type To run a program, just type
```bash ```bash
$ ./lyra $ ./lyra
``` ```
if you want to disown it from the shell, I recommend using the script I provided in `scripts` folder Remember to provide a `.env` file in the same directory as the binary.
If you want to disown the bot from the shell, I recommend using the script I provided in `scripts` folder
## Commands ## Commands
As of now, working commands are: As of now, the commands are:
``` ```
Music: Music:
@@ -60,7 +90,7 @@ Music:
/repeat Loops currently playing song provided amount of times; aliases: repeat, loop, while, for /repeat Loops currently playing song provided amount of times; aliases: repeat, loop, while, for
/resume Resumes currently paused song /resume Resumes currently paused song
/seek Seeks a track by provided seconds /seek Seeks a track by provided seconds
/skip Skips the currently playing song /skip Skips the currently playing song; aliases: skip, :skipper:
/stop Stops playback and destroys the queue; aliases: stop, end /stop Stops playback and destroys the queue; aliases: stop, end
/volume Changes output volume /volume Changes output volume
/effect Plays one of available audio effects /effect Plays one of available audio effects
@@ -69,13 +99,22 @@ Music:
Tools: Tools:
/ai Asks AI /ai Asks AI
/dice Rolls a dice /dice Rolls a dice
/dictionary Explains provided query
/ip Shows IP information
/metar Prints metar for provided airport
/owoify Owoifies whatever you want uwu /owoify Owoifies whatever you want uwu
/ping Pings you backs with a response time /ping Pings you backs with a response time
/posix Prints current time in POSIX format /posix Prints current time in POSIX format
/qr Creates a qr code from text /qr Creates a qr code from text
/taf Returns taf for provided airport
/uptime Checks how long the bot has been running
/verse Reference Bible by verse /verse Reference Bible by verse
/weather Shows weather for provided location
Help: Help:
/help Prints this help message; aliases: help, huh, welp /help Prints this help message; aliases: help, huh, welp
Use /help command for more info on a command.
You can edit you message to the bot and the bot will edit its response.
``` ```

Submodule libs/poise deleted from 575025909b

Submodule libs/serenity deleted from 658b6a7261

Submodule libs/songbird deleted from 2d7dc29fd6

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
[package]
name = "lib-spotify-parser"
version = "1.0.0"
edition = "2021"
[dependencies]
regex = "1.10.6"
reqwest = { version = "0.12.5", features = ["blocking", "rustls-tls"] }
scraper = "0.19.1"
serde = "1.0.204"
serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["macros"] }

View File

@@ -1,14 +0,0 @@
use std::fmt;
#[derive(Debug)]
pub enum ParseError {
InvalidUrl,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Invalid URL")
}
}
impl std::error::Error for ParseError {}

View File

@@ -1,154 +0,0 @@
mod error;
mod parser;
mod retriever;
use error::ParseError;
use parser::{parse_list, parse_single, USER_AGENT};
use regex::Regex;
use reqwest;
use retriever::{retrieve_async_track, retrieve_async_tracks, retrieve_track, retrieve_tracks};
pub fn retrieve_url(url: &str) -> Result<Vec<String>, ParseError> {
let spotify_regex: Regex = Regex::new(r"https?:\/\/(?:embed\.|open\.)spotify\.com\/(track|album|playlist)\/([a-zA-Z0-9]+)(?:\?si=[\w-]+)?").unwrap();
if let Some(captures) = spotify_regex.captures(url) {
let category = captures.get(1).unwrap().as_str();
let id = captures.get(2).unwrap().as_str();
match category {
"track" => {
let track = retrieve_track(category, id).unwrap();
Ok(vec![track])
}
"playlist" | "album" => {
let tracks = retrieve_tracks(category, id).unwrap();
Ok(tracks)
}
_ => Err(ParseError::InvalidUrl),
}
} else {
Err(ParseError::InvalidUrl)
}
}
pub async fn retrieve_async_url(url: &str) -> Result<Vec<String>, ParseError> {
let spotify_regex: Regex = Regex::new(r"https?:\/\/(?:embed\.|open\.)spotify\.com\/(track|album|playlist)\/([a-zA-Z0-9]+)(?:\?si=[\w-]+)?").unwrap();
if let Some(captures) = spotify_regex.captures(url) {
let category = captures.get(1).unwrap().as_str();
let id = captures.get(2).unwrap().as_str();
match category {
"track" => {
let track = retrieve_async_track(category, id).await.unwrap();
Ok(vec![track])
}
"playlist" | "album" => {
let tracks = retrieve_async_tracks(category, id).await.unwrap();
Ok(tracks)
}
_ => Err(ParseError::InvalidUrl),
}
} else {
Err(ParseError::InvalidUrl)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRACK: &str = "https://open.spotify.com/track/4PTG3Z6ehGkBFwjybzWkR8?si=e0a8c8ada8284e43";
const MULTIPLE_ARTISTS_TRACK: &str =
"https://open.spotify.com/track/1O0SdrryPeGp6eSSUibdgo?si=4ae58febe9e74eae";
const PLAYLIST: &str =
"https://open.spotify.com/playlist/37i9dQZF1DZ06evO05tE88?si=e0c6f44d176f44e6";
const ALBUM: &str =
"https://open.spotify.com/album/6eUW0wxWtzkFdaEFsTJto6?si=_grLtlNySNyfJTZr8tP44Q";
#[test]
fn check_track() {
let track: Vec<&str> = vec!["Rick Astley - Never Gonna Give You Up"];
assert_eq!(retrieve_url(TRACK).unwrap(), track);
}
#[test]
fn check_multiple_artists_track() {
let track: Vec<&str> = vec!["Will o' the wisp, Rick Astley - Blood on My Tie"];
assert_eq!(retrieve_url(MULTIPLE_ARTISTS_TRACK).unwrap(), track);
}
#[test]
fn check_playlist() {
let playlist: Vec<&str> = vec![
"Rick Astley - Never Gonna Give You Up",
"Rick Astley - Take Me to Your Heart (2023 Remaster)",
"Rick Astley - Cry for Help - Single Edit",
"Rick Astley - Never Gonna Stop",
"Rick Astley - Together Forever",
"Rick Astley - Hold Me in Your Arms (7\" Version)",
"Rick Astley - Angels on My Side",
"New Kids On The Block, Salt-N-Pepa, Rick Astley, En Vogue - Bring Back The Time",
"Rick Astley - Whenever You Need Somebody",
"Rick Astley - She Wants to Dance with Me (2023 Remaster)",
"Rick Astley - Dippin My Feet",
"Rick Astley - My Arms Keep Missing You",
"Rick Astley - Don't Say Goodbye",
"Rick Astley - Dance",
"Rick Astley - Giving Up On Love (7'' Pop Version)",
"Rick Astley - Never Gonna Give You Up (Cake Mix)",
"Rick Astley - It Would Take a Strong Strong Man",
"Rick Astley - Driving Me Crazy",
"Rick Astley - Beautiful Life",
"Rick Astley - I Don't Want to Lose Her",
"Rick Astley - Keep Singing",
"Rick Astley - Forever and More",
"Rick Astley - Hopelessly",
"Rick Astley - When I Fall in Love",
"Rick Astley - Every One of Us",
"Rick Astley - High Enough",
"Rick Astley - Ain't Too Proud to Beg (2023 Remaster)",
"Rick Astley - I'll Never Let You Down",
"Trevor Horn, Rick Astley - Owner Of A Lonely Heart",
"Rick Astley - Letting Go",
"Rick Astley - Never Knew Love",
"Rick Astley - Lights Out - Radio Edit",
"Rick Astley - Try",
"Rick Astley - Dial My Number (2023 Remaster)",
"Rick Astley - Wish Away",
"Rick Astley - Giant",
"Rick Astley - Move Right Out",
"Rick Astley - Till Then (Time Stands Still) (2023 Remaster)",
"Rick Astley - Pray with Me",
"Rick Astley - I Don't Want to Be Your Lover",
"Will o' the wisp, Rick Astley - Blood on My Tie",
"Rick Astley - Can't Help Falling in Love",
"Rick Astley - I Like the Sun",
"Rick Astley - She Makes Me",
"Rick Astley - Body and Soul",
"Rick Astley - (They Long to Be) Close to You",
"Rick Astley - Unwanted (Official Song from the Podcast)",
"Rick Astley - Last Night on Earth",
"Rick Astley - Everlong - Acoustic Version",
"Rick Astley - Superman",
];
assert_eq!(retrieve_url(PLAYLIST).unwrap(), playlist);
}
#[test]
fn check_album() {
let album: Vec<&str> = vec![
"Rick Astley - Never Gonna Give You Up",
"Rick Astley - Whenever You Need Somebody",
"Rick Astley - Together Forever",
"Rick Astley - It Would Take a Strong Strong Man",
"Rick Astley - The Love Has Gone",
"Rick Astley - Don't Say Goodbye",
"Rick Astley - Slipping Away",
"Rick Astley - No More Looking for Love",
"Rick Astley - You Move Me",
"Rick Astley - When I Fall in Love",
];
assert_eq!(retrieve_url(ALBUM).unwrap(), album);
}
}

View File

@@ -1,70 +0,0 @@
use scraper::{Html, Selector};
use serde_json::Value;
use std::error::Error;
#[derive(Debug, Clone)]
pub(crate) struct SpotifyTrack {
pub title: String,
pub artist: String,
}
pub(crate) const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
pub(crate) fn parse_single(content: String) -> Result<SpotifyTrack, Box<dyn Error>> {
let document = Html::parse_document(&content);
let selector = Selector::parse("#__NEXT_DATA__").unwrap();
if let Some(script_element) = document.select(&selector).next() {
let json_str = script_element.inner_html();
let json_value: Value = serde_json::from_str(&json_str)?;
let metadata = &json_value["props"]["pageProps"]["state"]["data"]["entity"];
// println!("{metadata:?}");
let title = metadata["title"].as_str().unwrap().to_string();
let artists = metadata["artists"].as_array().unwrap();
let artists_list: Vec<String> = artists
.iter()
.map(|artist| artist["name"].as_str().unwrap().to_string())
.collect();
let artist: String = artists_list.join(", ").to_string();
let track = SpotifyTrack { title, artist };
Ok(track)
} else {
Err("Could not find element".into())
}
}
pub(crate) fn parse_list(content: String) -> Result<Vec<SpotifyTrack>, Box<dyn Error>> {
let document = Html::parse_document(&content);
let selector = Selector::parse("#__NEXT_DATA__").unwrap();
if let Some(script_element) = document.select(&selector).next() {
let json_str = script_element.inner_html();
let json_value: Value = serde_json::from_str(&json_str)?;
let metadata = &json_value["props"]["pageProps"]["state"]["data"]["entity"]["trackList"]
.as_array()
.unwrap();
// println!("{metadata:?}");
let tracks: Vec<SpotifyTrack> = metadata
.iter()
.map(|track| {
let title = track["title"].as_str().unwrap().to_string();
let artist = track["subtitle"].as_str().unwrap().to_string();
SpotifyTrack { title, artist }
})
.collect();
// println!("{tracks:?}");
Ok(tracks)
} else {
Err("Could not find element".into())
}
}

View File

@@ -1,85 +0,0 @@
use crate::*;
pub fn retrieve_track(category: &str, id: &str) -> Result<String, String> {
let embed_url = format!("https://embed.spotify.com/?uri=spotify:{}:{}", category, id);
let client = reqwest::blocking::Client::builder()
.use_rustls_tls()
.user_agent(USER_AGENT)
.build()
.unwrap();
let response = client.get(&embed_url).send().unwrap();
let content = response.text().unwrap();
let parsed_content = parse_single(content).unwrap();
Ok(format!(
"{} - {}",
parsed_content.artist, parsed_content.title
))
}
pub fn retrieve_tracks(category: &str, id: &str) -> Result<Vec<String>, String> {
let embed_url = format!("https://embed.spotify.com/?uri=spotify:{}:{}", category, id);
let client = reqwest::blocking::Client::builder()
.use_rustls_tls()
.user_agent(USER_AGENT)
.build()
.unwrap();
let response = client.get(&embed_url).send().unwrap();
let content = response.text().unwrap();
let parsed_content = parse_list(content).unwrap();
let tracks: Vec<String> = parsed_content
.iter()
.map(|track| format!("{} - {}", track.artist, track.title))
.collect();
Ok(tracks)
}
pub async fn retrieve_async_track(category: &str, id: &str) -> Result<String, String> {
let embed_url = format!("https://embed.spotify.com/?uri=spotify:{}:{}", category, id);
let client = reqwest::Client::builder()
.use_rustls_tls()
.user_agent(USER_AGENT)
.build()
.unwrap();
let response = client.get(&embed_url).send().await.unwrap();
let content = response.text().await.unwrap();
let parsed_content = parse_single(content).unwrap();
Ok(format!(
"{} - {}",
parsed_content.artist, parsed_content.title
))
}
pub async fn retrieve_async_tracks(category: &str, id: &str) -> Result<Vec<String>, String> {
let embed_url = format!("https://embed.spotify.com/?uri=spotify:{}:{}", category, id);
let client = reqwest::Client::builder()
.use_rustls_tls()
.user_agent(USER_AGENT)
.build()
.unwrap();
let response = client.get(&embed_url).send().await.unwrap();
let content = response.text().await.unwrap();
let parsed_content = parse_list(content).unwrap();
let tracks: Vec<String> = parsed_content
.iter()
.map(|track| format!("{} - {}", track.artist, track.title))
.collect();
Ok(tracks)
}

View File

@@ -1,4 +1,3 @@
pub mod embeds; pub mod embeds;
pub mod kashi;
pub mod music; pub mod music;
pub mod tools; pub mod tools;

View File

@@ -1,3 +0,0 @@
pub mod kashi;
pub use kashi::kashi;

View File

@@ -1,17 +0,0 @@
use crate::{Context, Error};
/// Kashi integration platform (WIP)
#[poise::command(
prefix_command,
slash_command,
category = "Kashi"
)]
pub async fn kashi(
ctx: Context<'_>
) -> Result<(), Error> {
let response = format!("Kashi platform is currently under construction!");
ctx.say(response).await?;
Ok(())
}

View File

@@ -15,7 +15,7 @@ use poise::CreateReply;
pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> { pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -27,7 +27,7 @@ pub async fn join(ctx: Context<'_>) -> Result<(), Error> {
} }
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -15,12 +15,12 @@ use poise::CreateReply;
pub async fn leave(ctx: Context<'_>) -> Result<(), Error> { pub async fn leave(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
if !manager.get(guild_id).is_some() { if manager.get(guild_id).is_none() {
let msg = "I am not in a voice channel!"; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap())) ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?; .await?;

View File

@@ -15,7 +15,7 @@ use poise::CreateReply;
pub async fn mute(ctx: Context<'_>) -> Result<(), Error> { pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn pause(ctx: Context<'_>) -> Result<(), Error> { pub async fn pause(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -1,23 +1,24 @@
use crate::commands::music::metadata::Metadata; use crate::commands::music::metadata::Metadata;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::{commands::embeds::error_embed, Context, Error}; use crate::{commands::embeds::error_embed, Context, Error};
use fancy_regex::Regex;
use lib_spotify_parser;
use poise::serenity_prelude::{ use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp, Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
}; };
use poise::CreateReply; use poise::CreateReply;
use regex::Regex as Regex_Classic; use regex::Regex as Regex_Classic;
use reqwest::Client;
use serenity::all::GuildId;
use songbird::events::TrackEvent; use songbird::events::TrackEvent;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl}; use songbird::input::{Compose, YoutubeDl};
use songbird::tracks::{TrackHandle, TrackQueue}; use songbird::tracks::TrackQueue;
use songbird::Call;
use spotify_parser;
use std::collections::VecDeque;
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Plays a song; \ /// Plays a song; \
/// you can search by query or paste an url; \ /// you can search by query or paste an url; \
/// aliases: play, p, enqueue /// aliases: play, p, enqueue
@@ -31,25 +32,15 @@ pub async fn play(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Provide a query or an url"] #[description = "Provide a query or an url"]
#[rest] #[rest]
mut song: String, song: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let regex_spotify = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap();
let regex_youtube =
Regex_Classic::new(r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#)
.unwrap();
let regex_youtube_playlist = Regex::new(
r"^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com)).*(youtu.be\/|list=)([^#&?]*).*",
)
.unwrap();
let regex_spotify_playlist = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:(album|playlist)\/|\?uri=spotify:playlist:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap();
let is_playlist = regex_youtube_playlist.is_match(&song).unwrap()
|| regex_spotify_playlist.is_match(&song).unwrap();
let is_spotify =
regex_spotify.is_match(&song).unwrap() || regex_spotify_playlist.is_match(&song).unwrap();
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let http_client = ctx.data().http_client.clone();
let manager = songbird::get(ctx.serenity_context()).await.unwrap().clone();
let mut rest_playlist: VecDeque<String> = VecDeque::new();
if manager.get(guild_id).is_none() {
let channel_id = ctx let channel_id = ctx
.guild() .guild()
.unwrap() .unwrap()
@@ -63,125 +54,131 @@ pub async fn play(
let msg = "I am not in a voice channel!"; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap())) ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?; .await?;
return Ok(()); return Ok(());
} }
}; };
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await { if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_playlist && is_spotify { rest_playlist = handle_play(ctx, song, handler, http_client.clone())
let tracks: Vec<String> = lib_spotify_parser::retrieve_async_url(&song).await.unwrap(); .await
.unwrap();
for (index, url) in tracks.clone().iter().enumerate() { } else {
if url.is_empty() { let msg = "Failed to join the voice channel!";
break; ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
let src = YoutubeDl::new_ytdl_like( } else {
"yt-dlp", let handler = manager.get(guild_id).unwrap();
http_client.clone(), let mut handler = handler.lock().await;
format!("ytsearch:{}", url.to_string()), handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
); rest_playlist = handle_play(ctx, song, handler, http_client.clone())
.await
.unwrap();
}
handle_playlist(rest_playlist, manager, guild_id, http_client)
.await
.unwrap();
Ok(())
}
async fn handle_play<'a>(
ctx: Context<'a>,
song: String,
mut handler: tokio::sync::MutexGuard<'a, Call>,
http_client: Client,
) -> Result<VecDeque<String>, Error> {
let mut results = parse_data(song).await;
let src: YoutubeDl =
YoutubeDl::new_ytdl_like("yt-dlp", http_client, results.pop_front().unwrap());
ctx.send(
CreateReply::default().embed(
generate_embed(ctx, src.clone(), handler.queue(), results.clone())
.await
.unwrap(),
),
)
.await?;
let aux_metadata = src.clone().aux_metadata().await.unwrap(); let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track handler
.enqueue_input(src.clone().into())
.await
.typemap() .typemap()
.write() .write()
.await .await
.insert::<Metadata>(aux_metadata); .insert::<Metadata>(aux_metadata);
if index == 0 { Ok(results)
let embed = generate_playlist_embed(ctx, track, tracks.len()).await; }
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
}
}
return Ok(()); async fn parse_data(data: String) -> VecDeque<String> {
} let tracks = spotify_parser::retrieve_async_url(&data)
.await
.unwrap_or(vec![data])
.iter()
.flat_map(|track| {
if track.contains("?list=") {
let regex_youtube = Regex_Classic::new(
r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#,
)
.unwrap();
if is_playlist { let list = Command::new("yt-dlp")
let raw_list = Command::new("yt-dlp") .args(["-j", "--flat-playlist", track])
.args(["-j", "--flat-playlist", &song])
.output() .output()
.expect("failed to execute process") .expect("Failed to execute process")
.stdout; .stdout;
let list = String::from_utf8(list).unwrap();
let list = String::from_utf8(raw_list.clone()).expect("Invalid UTF-8"); regex_youtube
let urls: Vec<String> = regex_youtube
.captures_iter(&list) .captures_iter(&list)
.map(|capture| capture[1].to_string()) .map(|capture| capture.get(1).unwrap().as_str().to_string())
.collect::<Vec<String>>()
} else if track.starts_with("http") {
vec![track.clone()]
} else {
vec![format!("ytsearch:{}", track)]
}
})
.collect(); .collect();
for (index, url) in urls.clone().iter().enumerate() { tracks
if url.is_empty() { }
break;
} async fn handle_playlist(
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), url.to_string()); playlist: VecDeque<String>,
manager: std::sync::Arc<songbird::Songbird>,
guild_id: GuildId,
http_client: Client,
) -> Result<(), Error> {
for song in playlist {
if manager.get(guild_id).is_some() {
let handler = manager.get(guild_id).unwrap();
let mut handler = handler.lock().await;
let src: YoutubeDl =
YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), song.clone());
let aux_metadata = src.clone().aux_metadata().await.unwrap(); let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track handler
.enqueue_input(src.clone().into())
.await
.typemap() .typemap()
.write() .write()
.await .await
.insert::<Metadata>(aux_metadata); .insert::<Metadata>(aux_metadata);
if index == 0 {
let embed = generate_playlist_embed(ctx, track, urls.len()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
} }
} }
return Ok(());
}
if is_spotify {
song = format!(
"ytsearch:{}",
lib_spotify_parser::retrieve_async_url(&song)
.await
.unwrap()
.first()
.unwrap()
);
}
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone(), handler.queue()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let aux_metadata = src.clone().aux_metadata().await.unwrap();
let track = handler.enqueue_input(src.clone().into()).await;
let _ = track
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
}
Ok(()) Ok(())
} }
@@ -189,6 +186,7 @@ async fn generate_embed(
ctx: Context<'_>, ctx: Context<'_>,
src: YoutubeDl, src: YoutubeDl,
queue: &TrackQueue, queue: &TrackQueue,
results: VecDeque<String>,
) -> Result<CreateEmbed, Error> { ) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap(); let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata { let AuxMetadata {
@@ -199,17 +197,21 @@ async fn generate_embed(
duration, duration,
.. ..
} = metadata; } = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let mut description = format!("Song added to queue @ {}", queue.len() + 1); let mut description = format!("Enqueued @ {}", queue.len() + 1);
let mut tracks = "Tracks enqueued";
if queue.len() == 0 { if results.len() == 1 {
description = format!("Playing now!"); tracks = "Track enqueued";
}
if queue.is_empty() {
description = "Playing now!".to_string();
} }
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Track enqueued").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new(tracks).icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap()) .title(title.unwrap())
.url(source_url.unwrap()) .url(source_url.unwrap())
@@ -226,59 +228,7 @@ async fn generate_embed(
) )
.field("DJ", ctx.author().name.clone(), true) .field("DJ", ctx.author().name.clone(), true)
.description(description) .description(description)
.timestamp(timestamp) .timestamp(Timestamp::now())
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}
async fn generate_playlist_embed(
ctx: Context<'_>,
track: TrackHandle,
queue_length: usize,
) -> Result<CreateEmbed, Error> {
let meta_typemap = track.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Enqueued tracks: {}", queue_length - 1);
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playlist enqueued").icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97))
.title(title.as_ref().unwrap())
.url(source_url.as_ref().unwrap())
.thumbnail(
thumbnail
.as_ref()
.unwrap_or(&ctx.cache().current_user().face()),
)
.field(
"Artist",
artist.as_ref().unwrap_or(&"Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer( .footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()) CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()), .icon_url(ctx.cache().current_user().face()),

View File

@@ -16,7 +16,7 @@ const QUEUE_DISPLAY_LENGTH: usize = 10;
pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { pub async fn queue(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -21,7 +21,7 @@ pub async fn repeat(
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn resume(ctx: Context<'_>) -> Result<(), Error> { pub async fn resume(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -14,7 +14,7 @@ pub async fn seek(
) -> Result<(), Error> { ) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -17,7 +17,7 @@ use songbird::{input::AuxMetadata, tracks::TrackHandle};
pub async fn skip(ctx: Context<'_>) -> Result<(), Error> { pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -30,23 +30,18 @@ pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let track = track_raw.get(1); let track = track_raw.get(1);
let queue_length = queue.len() - 1; let queue_length = queue.len() - 1;
let response; let mut response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
match track { if let Some(track) = track {
Some(track) => {
response = CreateReply::default().embed( response = CreateReply::default().embed(
generate_embed(ctx, track.clone(), queue_length) generate_embed(ctx, track.clone(), queue_length)
.await .await
.unwrap(), .unwrap(),
); );
}
None => {
response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
}
}; };
ctx.send(response).await?; ctx.send(response).await?;

View File

@@ -34,7 +34,7 @@ pub async fn effect(
.expect("Guaranteed to exist in the typemap.") .expect("Guaranteed to exist in the typemap.")
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird Voice placed at init") .expect("Songbird Voice placed at init")
.clone(); .clone();
@@ -85,7 +85,7 @@ async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed,
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!"); let description = "Playing now!";
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face()))

View File

@@ -41,7 +41,7 @@ pub async fn stream(
.expect("Guaranteed to exist in the typemap.") .expect("Guaranteed to exist in the typemap.")
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird Voice placed at init") .expect("Songbird Voice placed at init")
.clone(); .clone();
@@ -92,7 +92,7 @@ async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed,
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Playing now!"); let description = "Playing now!";
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author( .author(

View File

@@ -10,7 +10,7 @@ use poise::CreateReply;
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -9,7 +9,7 @@ use poise::CreateReply;
pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> { pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();

View File

@@ -15,7 +15,7 @@ pub async fn ai(
#[rest] #[rest]
prompt: String, prompt: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let iamsorry = vec![ let iamsorry = &[
"I'm sorry, but as an AI language model, I must follow ethical guidelines, and I cannot engage in harmful, malicious, or offensive behavior.", "I'm sorry, but as an AI language model, I must follow ethical guidelines, and I cannot engage in harmful, malicious, or offensive behavior.",
"I'm sorry, but as an AI language model, I may not always be perfect and can make mistakes or provide inaccurate information. Please verify important details from reliable sources.", "I'm sorry, but as an AI language model, I may not always be perfect and can make mistakes or provide inaccurate information. Please verify important details from reliable sources.",
"I'm sorry, but as an AI language model, I can't engage in real-time conversations or remember previous interactions with users.", "I'm sorry, but as an AI language model, I can't engage in real-time conversations or remember previous interactions with users.",
@@ -27,19 +27,17 @@ pub async fn ai(
println!("Funny prompts: {}", prompt); println!("Funny prompts: {}", prompt);
let response; let response = {
let _ = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
response = rng.gen_range(0..iamsorry.len()); rng.gen_range(0..iamsorry.len())
}; };
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
ctx.send( ctx.send(
CreateReply::default().embed( CreateReply::default().embed(
embed(ctx, "AI Response:", "", &format!("{}", iamsorry[response])) embed(ctx, "AI Response:", "", iamsorry[response])
.await .await
.unwrap(), .unwrap(),
), ),

View File

@@ -7,12 +7,9 @@ use crate::{commands::embeds::embed, Context, Error};
/// Rolls a dice /// Rolls a dice
#[poise::command(prefix_command, slash_command, category = "Tools")] #[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn dice(ctx: Context<'_>) -> Result<(), Error> { pub async fn dice(ctx: Context<'_>) -> Result<(), Error> {
let dice; let dice = {
let _ = {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
rng.gen_range(1..=6)
dice = rng.gen_range(1..7);
}; };
ctx.send( ctx.send(

View File

@@ -5,7 +5,9 @@ use crate::{commands::embeds::embed, Context, Error};
/// Shows IP information /// Shows IP information
#[poise::command(prefix_command, slash_command, category = "Tools")] #[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ip(ctx: Context<'_>) -> Result<(), Error> { pub async fn ip(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap())) ctx.send(
CreateReply::default().embed(embed(ctx, "IP Info", "Work in progress", "").await.unwrap()),
)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -5,7 +5,9 @@ use crate::{commands::embeds::embed, Context, Error};
/// Prints metar for provided airport /// Prints metar for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")] #[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn metar(ctx: Context<'_>) -> Result<(), Error> { pub async fn metar(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap())) ctx.send(
CreateReply::default().embed(embed(ctx, "Metar", "Work in progress", "").await.unwrap()),
)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -5,7 +5,9 @@ use crate::{commands::embeds::embed, Context, Error};
/// Returns taf for provided airport /// Returns taf for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")] #[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn taf(ctx: Context<'_>) -> Result<(), Error> { pub async fn taf(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap())) ctx.send(
CreateReply::default().embed(embed(ctx, "Taf", "Work in progress", "").await.unwrap()),
)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -10,7 +10,9 @@ pub async fn weather(
#[rest] #[rest]
_location: String, _location: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap())) ctx.send(
CreateReply::default().embed(embed(ctx, "Weather", "Work in progress", "").await.unwrap()),
)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -9,7 +9,6 @@ use tracing::{error, info, warn};
mod commands; mod commands;
mod http; mod http;
use crate::commands::kashi;
use crate::commands::music; use crate::commands::music;
use crate::commands::tools; use crate::commands::tools;
use crate::http::HttpKey; use crate::http::HttpKey;
@@ -17,7 +16,9 @@ use crate::http::HttpKey;
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
pub struct Data; pub struct Data {
pub http_client: HttpClient,
}
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
match error { match error {
@@ -45,7 +46,6 @@ async fn main() {
let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!"); let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!");
let commands = vec![ let commands = vec![
kashi::kashi(),
music::deafen(), music::deafen(),
music::join(), music::join(),
music::leave(), music::leave(),
@@ -141,7 +141,9 @@ async fn main() {
) )
.await?; .await?;
Ok(Data {}) Ok(Data {
http_client: HttpClient::new(),
})
}) })
}) })
.options(options) .options(options)