mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-23 01:05:30 +01:00
Merge branch 'master' into feature/slash-commands
This commit is contained in:
commit
ed4e7b5ceb
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Lint & build
|
name: Lint & build
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
49
.github/workflows/pr.yml
vendored
49
.github/workflows/pr.yml
vendored
|
@ -3,23 +3,32 @@ name: PR Workflow
|
||||||
on: pull_request_target
|
on: pull_request_target
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release-snapshot:
|
||||||
name: Release snapshot
|
name: Release snapshot
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runner-platform:
|
||||||
|
- ubuntu-latest
|
||||||
|
- self-hosted
|
||||||
|
include:
|
||||||
|
- runner-platform: ubuntu-latest
|
||||||
|
build-arch: linux/amd64
|
||||||
|
tagged-platform: amd64
|
||||||
|
- runner-platform: self-hosted
|
||||||
|
build-arch: linux/arm64
|
||||||
|
tagged-platform: arm64
|
||||||
|
runs-on: ${{ matrix.runner-platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-prs-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-prs-
|
${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
|
@ -32,17 +41,37 @@ jobs:
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: codetheweb/muse:${{ github.event.pull_request.head.sha }}
|
tags: codetheweb/muse:${{ github.event.pull_request.head.sha }}-${{ matrix.tagged-platform }}
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
||||||
|
platforms: ${{ matrix.build-arch }}
|
||||||
|
|
||||||
|
combine-and-comment:
|
||||||
|
name: Combine platform tags and leave comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release-snapshot
|
||||||
|
steps:
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Combine tags
|
||||||
|
run: docker buildx imagetools create -t 'codetheweb/muse:pr-${{ github.event.number }}' -t 'codetheweb/muse:${{ github.event.pull_request.head.sha }}' 'codetheweb/muse:${{ github.event.pull_request.head.sha }}-arm64' 'codetheweb/muse:${{ github.event.pull_request.head.sha }}-amd64'
|
||||||
|
|
||||||
- name: Create comment
|
- name: Create comment
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
with:
|
with:
|
||||||
header: "pr-release"
|
header: "pr-release"
|
||||||
message: |
|
message: |
|
||||||
#### :package: A new release has been made for this pull request
|
#### :package: A new release has been made for this pull request.
|
||||||
|
|
||||||
To play around with this PR, pull `codetheweb/muse:${{ github.event.pull_request.head.sha }}`.
|
To play around with this PR, pull `codetheweb/muse:pr-${{ github.event.number }}` or `codetheweb/muse:${{ github.event.pull_request.head.sha }}`.
|
||||||
|
|
||||||
|
Images are available for x86_64 and ARM64.
|
||||||
|
|
||||||
> Latest commit: ${{ github.event.pull_request.head.sha }}
|
> Latest commit: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
140
.github/workflows/publish.yml
vendored
140
.github/workflows/publish.yml
vendored
|
@ -6,8 +6,92 @@ on:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
publish:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
runner-platform:
|
||||||
|
- ubuntu-latest
|
||||||
|
- self-hosted
|
||||||
|
include:
|
||||||
|
- runner-platform: ubuntu-latest
|
||||||
|
build-arch: linux/amd64
|
||||||
|
tagged-platform: amd64
|
||||||
|
- runner-platform: self-hosted
|
||||||
|
build-arch: linux/arm64
|
||||||
|
tagged-platform: arm64
|
||||||
|
runs-on: ${{ matrix.runner-platform }}
|
||||||
|
steps:
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: codetheweb/muse:${{ github.sha }}-${{ matrix.tagged-platform }}
|
||||||
|
platforms: ${{ matrix.build-arch }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
||||||
|
|
||||||
|
combine:
|
||||||
|
name: Combine platform tags
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: publish
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get tags
|
||||||
|
id: get-tags
|
||||||
|
uses: Surgo/docker-smart-tag-action@v1
|
||||||
|
with:
|
||||||
|
docker_image: codetheweb/muse
|
||||||
|
|
||||||
|
- name: Wait on lint & build
|
||||||
|
uses: lewagon/wait-on-check-action@v1.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.sha }}
|
||||||
|
check-name: 'Lint & build'
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
wait-interval: 10
|
||||||
|
|
||||||
|
- name: Combine tags
|
||||||
|
run: docker buildx imagetools create $(echo '${{ steps.get-tags.outputs.tag }}' | tr "," "\0" | xargs -0 printf -- '-t %s ') 'codetheweb/muse:${{ github.sha }}-arm64' 'codetheweb/muse:${{ github.sha }}-amd64'
|
||||||
|
|
||||||
|
- name: Update Docker Hub description
|
||||||
|
uses: peter-evans/dockerhub-description@v2.4.3
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
DOCKERHUB_REPOSITORY: codetheweb/muse
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: combine
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
@ -34,57 +118,3 @@ jobs:
|
||||||
draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }}
|
draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }}
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Set up Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get tags
|
|
||||||
id: get-tags
|
|
||||||
uses: Surgo/docker-smart-tag-action@v1
|
|
||||||
with:
|
|
||||||
docker_image: codetheweb/muse
|
|
||||||
|
|
||||||
- name: Wait on lint & build
|
|
||||||
uses: lewagon/wait-on-check-action@v1.1.1
|
|
||||||
with:
|
|
||||||
ref: ${{ github.sha }}
|
|
||||||
check-name: 'Lint & build'
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
wait-interval: 10
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.get-tags.outputs.tag }}
|
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
- name: Update Docker Hub description
|
|
||||||
uses: peter-evans/dockerhub-description@v2.4.3
|
|
||||||
env:
|
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
|
||||||
DOCKERHUB_REPOSITORY: codetheweb/muse
|
|
||||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Migrated to [Slash Commands](https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ)
|
- Migrated to [Slash Commands](https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ)
|
||||||
- The queue embed now automatically updates every 5 seconds (and has buttons for quick interactions)
|
- The queue embed now automatically updates every 5 seconds (and has buttons for quick interactions)
|
||||||
|
|
||||||
|
## [0.4.0] - 2022-01-17
|
||||||
|
### Added
|
||||||
|
- Playlists can now be shuffled as they are added to the queue, using the `shuffle` option to `play`.
|
||||||
|
|
||||||
|
## [0.3.2] - 2022-01-17
|
||||||
|
### Fixed
|
||||||
|
- The SQLite database path is now correctly generated on Windows
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Track lookups no longer fail silently (error is returned and logged)
|
||||||
|
|
||||||
|
## [0.3.1] - 2022-01-06
|
||||||
|
### Fixed
|
||||||
|
- Prisma client and migrations are no longer broken in built Docker images
|
||||||
|
|
||||||
|
## [0.3.0] - 2022-01-05
|
||||||
|
### Changed
|
||||||
|
- Migrated from Sequelize to Prisma. (#456)
|
||||||
|
- Bumped dependencies
|
||||||
|
|
||||||
## [0.2.1] - 2021-12-18
|
## [0.2.1] - 2021-12-18
|
||||||
### Added
|
### Added
|
||||||
- [release-it](https://www.npmjs.com/package/release-it): makes it easier to generate new tags and releases
|
- [release-it](https://www.npmjs.com/package/release-it): makes it easier to generate new tags and releases
|
||||||
|
@ -25,7 +45,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
[Unreleased]: https://github.com/codetheweb/muse/compare/v0.2.1...HEAD
|
[Unreleased]: https://github.com/codetheweb/muse/compare/v0.4.0...HEAD
|
||||||
|
[0.4.0]: https://github.com/codetheweb/muse/compare/v0.3.2...v0.4.0
|
||||||
|
[0.3.2]: https://github.com/codetheweb/muse/compare/v0.3.1...v0.3.2
|
||||||
|
[0.3.1]: https://github.com/codetheweb/muse/compare/v0.3.0...v0.3.1
|
||||||
|
[0.3.0]: https://github.com/codetheweb/muse/compare/v0.2.1...v0.3.0
|
||||||
[0.2.1]: https://github.com/codetheweb/muse/compare/v0.2.0...v0.2.1
|
[0.2.1]: https://github.com/codetheweb/muse/compare/v0.2.0...v0.2.1
|
||||||
[0.2.0]: https://github.com/codetheweb/muse/releases/tag/v0.2.0
|
[0.2.0]: https://github.com/codetheweb/muse/releases/tag/v0.2.0
|
||||||
[0.1.1]: https://github.com/codetheweb/muse/releases/tag/v0.1.1
|
[0.1.1]: https://github.com/codetheweb/muse/releases/tag/v0.1.1
|
||||||
|
|
14
Dockerfile
14
Dockerfile
|
@ -1,7 +1,9 @@
|
||||||
FROM node:16.13.0-alpine AS base
|
FROM node:16.13.0 AS base
|
||||||
|
|
||||||
# Install ffmpeg and build dependencies
|
# Install ffmpeg
|
||||||
RUN apk add --no-cache ffmpeg python2 make g++
|
RUN apt-get update && \
|
||||||
|
apt-get install -y ffmpeg && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
|
|
||||||
|
@ -22,12 +24,16 @@ FROM dependencies AS builder
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn build
|
RUN yarn prisma generate && yarn build
|
||||||
|
|
||||||
# Only copy essentials
|
# Only copy essentials
|
||||||
FROM base AS prod
|
FROM base AS prod
|
||||||
|
|
||||||
COPY --from=builder /usr/app/dist dist
|
COPY --from=builder /usr/app/dist dist
|
||||||
|
COPY --from=builder /usr/app/schema.prisma .
|
||||||
|
COPY --from=builder /usr/app/migrations migrations
|
||||||
|
|
||||||
|
RUN yarn prisma generate
|
||||||
|
|
||||||
ENV DATA_DIR /data
|
ENV DATA_DIR /data
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `FileCaches` (`hash` VARCHAR(255) UNIQUE PRIMARY KEY, `bytes` INTEGER, `accessedAt` DATETIME, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `KeyValueCaches` (`key` VARCHAR(255) UNIQUE PRIMARY KEY, `value` TEXT, `expiresAt` DATETIME, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Settings` (`guildId` VARCHAR(255) UNIQUE PRIMARY KEY, `prefix` VARCHAR(255), `channel` VARCHAR(255), `finishedSetup` TINYINT(1) DEFAULT 0, `playlistLimit` INTEGER DEFAULT '50', `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Shortcuts` (`id` INTEGER PRIMARY KEY, `guildId` VARCHAR(255), `authorId` VARCHAR(255), `shortcut` VARCHAR(255), `command` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "shortcuts_shortcut" ON "Shortcuts"("shortcut");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "shortcuts_guild_id" ON "Shortcuts"("guildId");
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to alter the column `finishedSetup` on the `Settings` table. The data in that column could be lost. The data in that column will be cast from `Unsupported("tinyint(1)")` to `Boolean`.
|
||||||
|
- Made the column `expiresAt` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `key` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `value` on table `KeyValueCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `authorId` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `command` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `guildId` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `id` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `shortcut` on table `Shortcuts` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `accessedAt` on table `FileCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `bytes` on table `FileCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `hash` on table `FileCaches` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `guildId` on table `Settings` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `prefix` on table `Settings` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_KeyValueCaches" (
|
||||||
|
"key" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"expiresAt" DATETIME NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_KeyValueCaches" ("createdAt", "expiresAt", "key", "updatedAt", "value") SELECT "createdAt", "expiresAt", "key", "updatedAt", "value" FROM "KeyValueCaches";
|
||||||
|
DROP TABLE "KeyValueCaches";
|
||||||
|
ALTER TABLE "new_KeyValueCaches" RENAME TO "KeyValueCache";
|
||||||
|
CREATE TABLE "new_Shortcuts" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT NOT NULL,
|
||||||
|
"shortcut" TEXT NOT NULL,
|
||||||
|
"command" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Shortcuts" ("authorId", "command", "createdAt", "guildId", "id", "shortcut", "updatedAt") SELECT "authorId", "command", "createdAt", "guildId", "id", "shortcut", "updatedAt" FROM "Shortcuts";
|
||||||
|
DROP TABLE "Shortcuts";
|
||||||
|
ALTER TABLE "new_Shortcuts" RENAME TO "Shortcut";
|
||||||
|
CREATE INDEX "shortcuts_shortcut" ON "Shortcut"("shortcut");
|
||||||
|
CREATE INDEX "shortcuts_guild_id" ON "Shortcut"("guildId");
|
||||||
|
CREATE TABLE "new_FileCaches" (
|
||||||
|
"hash" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"bytes" INTEGER NOT NULL,
|
||||||
|
"accessedAt" DATETIME NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_FileCaches" ("accessedAt", "bytes", "createdAt", "hash", "updatedAt") SELECT "accessedAt", "bytes", "createdAt", "hash", "updatedAt" FROM "FileCaches";
|
||||||
|
DROP TABLE "FileCaches";
|
||||||
|
ALTER TABLE "new_FileCaches" RENAME TO "FileCache";
|
||||||
|
CREATE TABLE "new_Settings" (
|
||||||
|
"guildId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"prefix" TEXT NOT NULL,
|
||||||
|
"channel" TEXT,
|
||||||
|
"finishedSetup" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"playlistLimit" INTEGER NOT NULL DEFAULT 50,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Settings" ("channel", "createdAt", "finishedSetup", "guildId", "playlistLimit", "prefix", "updatedAt") SELECT "channel", "createdAt", coalesce("finishedSetup", false) AS "finishedSetup", "guildId", coalesce("playlistLimit", 50) AS "playlistLimit", "prefix", "updatedAt" FROM "Settings";
|
||||||
|
DROP TABLE "Settings";
|
||||||
|
ALTER TABLE "new_Settings" RENAME TO "Setting";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Shortcut_guildId_shortcut_idx" ON "Shortcut"("guildId", "shortcut");
|
6
migrations/20220102176527_datetime_casting/migration.sql
Normal file
6
migrations/20220102176527_datetime_casting/migration.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-- Manual migration to cast DateTime Column from "2021-12-29 20:16:54.221 +00:00" format to timestamp
|
||||||
|
|
||||||
|
UPDATE FileCache SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT), accessedAt = CAST(strftime('%s', accessedAt) AS INT);
|
||||||
|
UPDATE KeyValueCache SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT), expiresAt = CAST(strftime('%s', expiresAt) AS INT);
|
||||||
|
UPDATE Setting SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT);
|
||||||
|
UPDATE Shortcut SET createdAt = CAST(strftime('%s', createdAt) AS INT) ,updatedAt = CAST(strftime('%s', updatedAt) AS INT);
|
3
migrations/migration_lock.toml
Normal file
3
migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
||||||
"watch": ["dist"],
|
"watch": ["dist"],
|
||||||
"exec": "npm start",
|
"exec": "npm run env:set-database-url -- node --experimental-json-modules dist/src/scripts/start.js",
|
||||||
"ext": "js"
|
"ext": "js"
|
||||||
}
|
}
|
||||||
|
|
23
package.json
23
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "muse",
|
"name": "muse",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"description": "🎧 a self-hosted Discord music bot that doesn't suck ",
|
"description": "🎧 a self-hosted Discord music bot that doesn't suck ",
|
||||||
"exports": "./dist/src/index.js",
|
"exports": "./dist/src/index.js",
|
||||||
"repository": "git@github.com:codetheweb/muse.git",
|
"repository": "git@github.com:codetheweb/muse.git",
|
||||||
|
@ -24,8 +24,13 @@
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"build:watch": "tsc --watch",
|
"build:watch": "tsc --watch",
|
||||||
"prepack": "npm run clean && npm run build",
|
"prepack": "npm run clean && npm run build",
|
||||||
"start": "node --experimental-json-modules dist/src/index.js",
|
"start": "npm run env:set-database-url -- node --experimental-json-modules dist/src/scripts/migrate-and-start.js",
|
||||||
|
"cache:clear-key-value": "npm run env:set-database-url node dist/src/scripts/cache-clear-key-value.js",
|
||||||
"dev": "concurrently nodemon 'tsc --watch'",
|
"dev": "concurrently nodemon 'tsc --watch'",
|
||||||
|
"migrations:generate": "npm run prisma:with-env migrate dev",
|
||||||
|
"migrations:run": "npm run prisma:with-env migrate deploy",
|
||||||
|
"prisma:with-env": "npm run env:set-database-url prisma",
|
||||||
|
"env:set-database-url": "node dist/src/scripts/run-with-database-url.js",
|
||||||
"release": "release-it"
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -47,6 +52,7 @@
|
||||||
"eslint-config-xo-typescript": "^0.44.0",
|
"eslint-config-xo-typescript": "^0.44.0",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
|
"prisma": "^3.7.0",
|
||||||
"release-it": "^14.11.8",
|
"release-it": "^14.11.8",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"type-fest": "^2.8.0",
|
"type-fest": "^2.8.0",
|
||||||
|
@ -80,13 +86,15 @@
|
||||||
"@discordjs/opus": "^0.7.0",
|
"@discordjs/opus": "^0.7.0",
|
||||||
"@discordjs/rest": "^0.1.0-canary.0",
|
"@discordjs/rest": "^0.1.0-canary.0",
|
||||||
"@discordjs/voice": "^0.7.5",
|
"@discordjs/voice": "^0.7.5",
|
||||||
|
"@prisma/client": "^3.7.0",
|
||||||
"@types/libsodium-wrappers": "^0.7.9",
|
"@types/libsodium-wrappers": "^0.7.9",
|
||||||
"array-shuffle": "^3.0.0",
|
"array-shuffle": "^3.0.0",
|
||||||
"debug": "^4.3.3",
|
"debug": "^4.3.3",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"discord-api-types": "^0.25.2",
|
"discord-api-types": "^0.26.1",
|
||||||
"discord.js": "^13.3.0",
|
"discord.js": "^13.5.0",
|
||||||
"dotenv": "^8.5.1",
|
"dotenv": "^8.5.1",
|
||||||
|
"execa": "^6.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"fs-capacitor": "^7.0.1",
|
"fs-capacitor": "^7.0.1",
|
||||||
"get-youtube-id": "^1.0.1",
|
"get-youtube-id": "^1.0.1",
|
||||||
|
@ -103,14 +111,11 @@
|
||||||
"p-limit": "^4.0.0",
|
"p-limit": "^4.0.0",
|
||||||
"p-queue": "^7.1.0",
|
"p-queue": "^7.1.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "6.11.0",
|
|
||||||
"sequelize-typescript": "^2.1.1",
|
|
||||||
"spotify-uri": "^2.2.0",
|
"spotify-uri": "^2.2.0",
|
||||||
"spotify-web-api-node": "^5.0.2",
|
"spotify-web-api-node": "^5.0.2",
|
||||||
"sqlite3": "^5.0.2",
|
|
||||||
"xbytes": "^1.7.0",
|
"xbytes": "^1.7.0",
|
||||||
"youtube.ts": "^0.2.2",
|
"youtube.ts": "^0.2.5",
|
||||||
"ytdl-core": "^4.9.1",
|
"ytdl-core": "^4.9.2",
|
||||||
"ytsr": "^3.5.3"
|
"ytsr": "^3.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
schema.prisma
Normal file
49
schema.prisma
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model FileCache {
|
||||||
|
hash String @id
|
||||||
|
bytes Int
|
||||||
|
accessedAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model KeyValueCache {
|
||||||
|
key String @id
|
||||||
|
value String
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Setting {
|
||||||
|
guildId String @id
|
||||||
|
prefix String
|
||||||
|
channel String?
|
||||||
|
finishedSetup Boolean @default(false)
|
||||||
|
playlistLimit Int @default(50)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Shortcut {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
guildId String
|
||||||
|
authorId String
|
||||||
|
shortcut String
|
||||||
|
command String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|
||||||
|
@@index([shortcut], map: "shortcuts_shortcut")
|
||||||
|
@@index([guildId], map: "shortcuts_guild_id")
|
||||||
|
@@index([guildId, shortcut])
|
||||||
|
}
|
|
@ -22,7 +22,10 @@ export default class {
|
||||||
private readonly commandsByName!: Collection<string, Command>;
|
private readonly commandsByName!: Collection<string, Command>;
|
||||||
private readonly commandsByButtonId!: Collection<string, Command>;
|
private readonly commandsByButtonId!: Collection<string, Command>;
|
||||||
|
|
||||||
constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config) config: Config) {
|
constructor(
|
||||||
|
@inject(TYPES.Client) client: Client,
|
||||||
|
@inject(TYPES.Config) config: Config,
|
||||||
|
) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.token = config.DISCORD_TOKEN;
|
this.token = config.DISCORD_TOKEN;
|
||||||
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;
|
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;
|
||||||
|
@ -61,7 +64,7 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (command.requiresVC && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
if (command.requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
||||||
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {CommandInteraction, GuildMember} from 'discord.js';
|
||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
import {Except} from 'type-fest';
|
import {Except} from 'type-fest';
|
||||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||||
|
import shuffle from 'array-shuffle';
|
||||||
import {inject, injectable} from 'inversify';
|
import {inject, injectable} from 'inversify';
|
||||||
import Command from '.';
|
import Command from '.';
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
|
@ -10,7 +11,7 @@ import PlayerManager from '../managers/player.js';
|
||||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels.js';
|
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels.js';
|
||||||
import errorMsg from '../utils/error-msg.js';
|
import errorMsg from '../utils/error-msg.js';
|
||||||
import GetSongs from '../services/get-songs.js';
|
import GetSongs from '../services/get-songs.js';
|
||||||
import Settings from '../models/settings.js';
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class implements Command {
|
export default class implements Command {
|
||||||
|
@ -23,7 +24,10 @@ export default class implements Command {
|
||||||
.setDescription('YouTube URL, Spotify URL, or search query'))
|
.setDescription('YouTube URL, Spotify URL, or search query'))
|
||||||
.addBooleanOption(option => option
|
.addBooleanOption(option => option
|
||||||
.setName('immediate')
|
.setName('immediate')
|
||||||
.setDescription('adds track to the front of the queue'));
|
.setDescription('adds track to the front of the queue'))
|
||||||
|
.addBooleanOption(option => option
|
||||||
|
.setName('shuffle')
|
||||||
|
.setDescription('shuffles the input if it\'s a playlist'));
|
||||||
|
|
||||||
public requiresVC = true;
|
public requiresVC = true;
|
||||||
|
|
||||||
|
@ -39,8 +43,13 @@ export default class implements Command {
|
||||||
public async executeFromInteraction(interaction: CommandInteraction): Promise<void> {
|
public async executeFromInteraction(interaction: CommandInteraction): Promise<void> {
|
||||||
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
|
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
|
||||||
|
|
||||||
const settings = await Settings.findByPk(interaction.guild!.id);
|
const settings = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}});
|
||||||
const {playlistLimit} = settings!;
|
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error('Could not find settings for guild');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {playlistLimit} = settings;
|
||||||
|
|
||||||
const player = this.playerManager.get(interaction.guild!.id);
|
const player = this.playerManager.get(interaction.guild!.id);
|
||||||
const wasPlayingSong = player.getCurrent() !== null;
|
const wasPlayingSong = player.getCurrent() !== null;
|
||||||
|
@ -67,8 +76,9 @@ export default class implements Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToFrontOfQueue = interaction.options.getBoolean('immediate');
|
const addToFrontOfQueue = interaction.options.getBoolean('immediate');
|
||||||
|
const shuffleAdditions = interaction.options.getBoolean('shuffle');
|
||||||
|
|
||||||
const newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
|
let newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
|
||||||
let extraMsg = '';
|
let extraMsg = '';
|
||||||
|
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
@ -139,6 +149,10 @@ export default class implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shuffleAdditions) {
|
||||||
|
newSongs = shuffle(newSongs);
|
||||||
|
}
|
||||||
|
|
||||||
newSongs.forEach(song => {
|
newSongs.forEach(song => {
|
||||||
player.add({...song, addedInChannelId: interaction.channel?.id}, {immediate: addToFrontOfQueue ?? false});
|
player.add({...song, addedInChannelId: interaction.channel?.id}, {immediate: addToFrontOfQueue ?? false});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {Message} from 'discord.js';
|
import {Message} from 'discord.js';
|
||||||
import {injectable} from 'inversify';
|
import {injectable} from 'inversify';
|
||||||
import {Shortcut, Settings} from '../models/index.js';
|
|
||||||
import errorMsg from '../utils/error-msg.js';
|
import errorMsg from '../utils/error-msg.js';
|
||||||
import Command from '.';
|
import Command from '.';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class implements Command {
|
export default class implements Command {
|
||||||
|
@ -18,7 +18,11 @@ export default class implements Command {
|
||||||
public async execute(msg: Message, args: string []): Promise<void> {
|
public async execute(msg: Message, args: string []): Promise<void> {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
// Get shortcuts for guild
|
// Get shortcuts for guild
|
||||||
const shortcuts = await Shortcut.findAll({where: {guildId: msg.guild!.id}});
|
const shortcuts = await prisma.shortcut.findMany({
|
||||||
|
where: {
|
||||||
|
guildId: msg.guild!.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (shortcuts.length === 0) {
|
if (shortcuts.length === 0) {
|
||||||
await msg.channel.send('no shortcuts exist');
|
await msg.channel.send('no shortcuts exist');
|
||||||
|
@ -26,7 +30,11 @@ export default class implements Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get prefix for guild
|
// Get prefix for guild
|
||||||
const settings = await Settings.findOne({where: {guildId: msg.guild!.id}});
|
const settings = await prisma.setting.findUnique({
|
||||||
|
where: {
|
||||||
|
guildId: msg.guild!.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return;
|
return;
|
||||||
|
@ -48,7 +56,12 @@ export default class implements Command {
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'set': {
|
case 'set': {
|
||||||
const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}});
|
const shortcut = await prisma.shortcut.findFirst({
|
||||||
|
where: {
|
||||||
|
guildId: msg.guild!.id,
|
||||||
|
shortcut: shortcutName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const command = args.slice(2).join(' ');
|
const command = args.slice(2).join(' ');
|
||||||
|
|
||||||
|
@ -60,10 +73,15 @@ export default class implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await shortcut.update(newShortcut);
|
await prisma.shortcut.update({
|
||||||
|
where: {
|
||||||
|
id: shortcut.id,
|
||||||
|
},
|
||||||
|
data: newShortcut,
|
||||||
|
});
|
||||||
await msg.channel.send('shortcut updated');
|
await msg.channel.send('shortcut updated');
|
||||||
} else {
|
} else {
|
||||||
await Shortcut.create(newShortcut);
|
await prisma.shortcut.create({data: newShortcut});
|
||||||
await msg.channel.send('shortcut created');
|
await msg.channel.send('shortcut created');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +90,12 @@ export default class implements Command {
|
||||||
|
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
// Check if shortcut exists
|
// Check if shortcut exists
|
||||||
const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}});
|
const shortcut = await prisma.shortcut.findFirst({
|
||||||
|
where: {
|
||||||
|
guildId: msg.guild!.id,
|
||||||
|
shortcut: shortcutName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!shortcut) {
|
if (!shortcut) {
|
||||||
await msg.channel.send(errorMsg('shortcut doesn\'t exist'));
|
await msg.channel.send(errorMsg('shortcut doesn\'t exist'));
|
||||||
|
@ -85,7 +108,11 @@ export default class implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await shortcut.destroy();
|
await prisma.shortcut.delete({
|
||||||
|
where: {
|
||||||
|
id: shortcut.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await msg.channel.send('shortcut deleted');
|
await msg.channel.send('shortcut deleted');
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,28 @@ import {Guild, TextChannel, Message, MessageReaction, User, ApplicationCommandDa
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import emoji from 'node-emoji';
|
import emoji from 'node-emoji';
|
||||||
import pEvent from 'p-event';
|
import pEvent from 'p-event';
|
||||||
import {Settings} from '../models/index.js';
|
|
||||||
import {chunk} from '../utils/arrays.js';
|
import {chunk} from '../utils/arrays.js';
|
||||||
import container from '../inversify.config.js';
|
import container from '../inversify.config.js';
|
||||||
import Command from '../commands';
|
import Command from '../commands';
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
import Config from '../services/config.js';
|
import Config from '../services/config.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
const DEFAULT_PREFIX = '!';
|
const DEFAULT_PREFIX = '!';
|
||||||
|
|
||||||
export default async (guild: Guild): Promise<void> => {
|
export default async (guild: Guild): Promise<void> => {
|
||||||
await Settings.upsert({guildId: guild.id, prefix: DEFAULT_PREFIX});
|
await prisma.setting.upsert({
|
||||||
|
where: {
|
||||||
|
guildId: guild.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
guildId: guild.id,
|
||||||
|
prefix: DEFAULT_PREFIX,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
prefix: DEFAULT_PREFIX,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const config = container.get<Config>(TYPES.Config);
|
const config = container.get<Config>(TYPES.Config);
|
||||||
|
|
||||||
|
@ -86,7 +97,15 @@ export default async (guild: Guild): Promise<void> => {
|
||||||
const prefixCharacter = prefixResponses.first()!.content;
|
const prefixCharacter = prefixResponses.first()!.content;
|
||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
await Settings.update({prefix: prefixCharacter, channel: chosenChannel.id}, {where: {guildId: guild.id}});
|
await prisma.setting.update({
|
||||||
|
where: {
|
||||||
|
guildId: guild.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
channel: chosenChannel.id,
|
||||||
|
prefix: prefixCharacter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Send welcome
|
// Send welcome
|
||||||
const boundChannel = guild.client.channels.cache.get(chosenChannel.id) as TextChannel;
|
const boundChannel = guild.client.channels.cache.get(chosenChannel.id) as TextChannel;
|
||||||
|
|
22
src/index.ts
22
src/index.ts
|
@ -1,28 +1,14 @@
|
||||||
import makeDir from 'make-dir';
|
import makeDir from 'make-dir';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {makeLines} from 'nodesplash';
|
|
||||||
import container from './inversify.config.js';
|
import container from './inversify.config.js';
|
||||||
import {TYPES} from './types.js';
|
import {TYPES} from './types.js';
|
||||||
import Bot from './bot.js';
|
import Bot from './bot.js';
|
||||||
import {sequelize} from './utils/db.js';
|
|
||||||
import Config from './services/config.js';
|
import Config from './services/config.js';
|
||||||
import FileCacheProvider from './services/file-cache.js';
|
import FileCacheProvider from './services/file-cache.js';
|
||||||
import metadata from '../package.json';
|
|
||||||
|
|
||||||
const bot = container.get<Bot>(TYPES.Bot);
|
const bot = container.get<Bot>(TYPES.Bot);
|
||||||
|
|
||||||
(async () => {
|
const startBot = async () => {
|
||||||
// Banner
|
|
||||||
console.log(makeLines({
|
|
||||||
user: 'codetheweb',
|
|
||||||
repository: 'muse',
|
|
||||||
version: metadata.version,
|
|
||||||
paypalUser: 'codetheweb',
|
|
||||||
githubSponsor: 'codetheweb',
|
|
||||||
madeByPrefix: 'Made with 🎶 by ',
|
|
||||||
}).join('\n'));
|
|
||||||
console.log('\n');
|
|
||||||
|
|
||||||
// Create data directories if necessary
|
// Create data directories if necessary
|
||||||
const config = container.get<Config>(TYPES.Config);
|
const config = container.get<Config>(TYPES.Config);
|
||||||
|
|
||||||
|
@ -30,9 +16,9 @@ const bot = container.get<Bot>(TYPES.Bot);
|
||||||
await makeDir(config.CACHE_DIR);
|
await makeDir(config.CACHE_DIR);
|
||||||
await makeDir(path.join(config.CACHE_DIR, 'tmp'));
|
await makeDir(path.join(config.CACHE_DIR, 'tmp'));
|
||||||
|
|
||||||
await sequelize.sync({alter: true});
|
|
||||||
|
|
||||||
await container.get<FileCacheProvider>(TYPES.FileCache).cleanup();
|
await container.get<FileCacheProvider>(TYPES.FileCache).cleanup();
|
||||||
|
|
||||||
await bot.listen();
|
await bot.listen();
|
||||||
})();
|
};
|
||||||
|
|
||||||
|
export {startBot};
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript';
|
|
||||||
|
|
||||||
@Table
|
|
||||||
export default class FileCache extends Model {
|
|
||||||
@PrimaryKey
|
|
||||||
@Column
|
|
||||||
hash!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
bytes!: number;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
accessedAt!: Date;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import FileCache from './file-cache.js';
|
|
||||||
import KeyValueCache from './key-value-cache.js';
|
|
||||||
import Settings from './settings.js';
|
|
||||||
import Shortcut from './shortcut.js';
|
|
||||||
|
|
||||||
export {
|
|
||||||
FileCache,
|
|
||||||
KeyValueCache,
|
|
||||||
Settings,
|
|
||||||
Shortcut,
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript';
|
|
||||||
import sequelize from 'sequelize';
|
|
||||||
|
|
||||||
@Table
|
|
||||||
export default class KeyValueCache extends Model {
|
|
||||||
@PrimaryKey
|
|
||||||
@Column
|
|
||||||
key!: string;
|
|
||||||
|
|
||||||
@Column(sequelize.TEXT)
|
|
||||||
value!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
expiresAt!: Date;
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import {Table, Column, PrimaryKey, Model, Default} from 'sequelize-typescript';
|
|
||||||
|
|
||||||
@Table
|
|
||||||
export default class Settings extends Model {
|
|
||||||
@PrimaryKey
|
|
||||||
@Column
|
|
||||||
guildId!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
prefix!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
channel!: string;
|
|
||||||
|
|
||||||
@Default(false)
|
|
||||||
@Column
|
|
||||||
finishedSetup!: boolean;
|
|
||||||
|
|
||||||
@Default(50)
|
|
||||||
@Column
|
|
||||||
playlistLimit!: number;
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import {Table, Column, PrimaryKey, Model, AutoIncrement, Index} from 'sequelize-typescript';
|
|
||||||
|
|
||||||
@Table
|
|
||||||
export default class Shortcut extends Model {
|
|
||||||
@PrimaryKey
|
|
||||||
@AutoIncrement
|
|
||||||
@Column
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
@Index
|
|
||||||
guildId!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
authorId!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
@Index
|
|
||||||
shortcut!: string;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
command!: string;
|
|
||||||
}
|
|
10
src/scripts/cache-clear-key-value.ts
Normal file
10
src/scripts/cache-clear-key-value.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import ora from 'ora';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const spinner = ora('Clearing key value cache...').start();
|
||||||
|
|
||||||
|
await prisma.keyValueCache.deleteMany({});
|
||||||
|
|
||||||
|
spinner.succeed('Key value cache cleared.');
|
||||||
|
})();
|
83
src/scripts/migrate-and-start.ts
Normal file
83
src/scripts/migrate-and-start.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// This script applies Prisma migrations
|
||||||
|
// and then starts Muse.
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import {execa, ExecaError} from 'execa';
|
||||||
|
import {promises as fs} from 'fs';
|
||||||
|
import Prisma from '@prisma/client';
|
||||||
|
import ora from 'ora';
|
||||||
|
import {startBot} from '../index.js';
|
||||||
|
import logBanner from '../utils/log-banner.js';
|
||||||
|
import {createDatabasePath} from '../utils/create-database-url.js';
|
||||||
|
import {DATA_DIR} from '../services/config.js';
|
||||||
|
|
||||||
|
const client = new Prisma.PrismaClient();
|
||||||
|
|
||||||
|
const migrateFromSequelizeToPrisma = async () => {
|
||||||
|
await execa('prisma', ['migrate', 'resolve', '--applied', '20220101155430_migrate_from_sequelize'], {preferLocal: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
const doesUserHaveExistingDatabase = async () => {
|
||||||
|
try {
|
||||||
|
await fs.access(createDatabasePath(DATA_DIR));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasDatabaseBeenMigratedToPrisma = async () => {
|
||||||
|
try {
|
||||||
|
await client.$queryRaw`SELECT COUNT(id) FROM _prisma_migrations`;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Prisma.Prisma.PrismaClientKnownRequestError && error.code === 'P2010') {
|
||||||
|
// Table doesn't exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Banner
|
||||||
|
logBanner();
|
||||||
|
|
||||||
|
const spinner = ora('Applying database migrations...').start();
|
||||||
|
|
||||||
|
if (await doesUserHaveExistingDatabase()) {
|
||||||
|
if (!(await hasDatabaseBeenMigratedToPrisma())) {
|
||||||
|
try {
|
||||||
|
await migrateFromSequelizeToPrisma();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as ExecaError).stderr) {
|
||||||
|
spinner.fail('Failed to apply database migrations (going from Sequelize to Prisma):');
|
||||||
|
console.error((error as ExecaError).stderr);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execa('prisma', ['migrate', 'deploy'], {preferLocal: true});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if ((error as ExecaError).stderr) {
|
||||||
|
spinner.fail('Failed to apply database migrations:');
|
||||||
|
console.error((error as ExecaError).stderr);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.succeed('Database migrations applied.');
|
||||||
|
|
||||||
|
await startBot();
|
||||||
|
})();
|
13
src/scripts/run-with-database-url.ts
Normal file
13
src/scripts/run-with-database-url.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {DATA_DIR} from '../services/config.js';
|
||||||
|
import createDatabaseUrl from '../utils/create-database-url.js';
|
||||||
|
import {execa} from 'execa';
|
||||||
|
|
||||||
|
process.env.DATABASE_URL = createDatabaseUrl(DATA_DIR);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await execa(process.argv[2], process.argv.slice(3), {
|
||||||
|
preferLocal: true,
|
||||||
|
stderr: process.stderr,
|
||||||
|
stdout: process.stdout,
|
||||||
|
});
|
||||||
|
})();
|
9
src/scripts/start.ts
Normal file
9
src/scripts/start.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// This script is mainly used during development.
|
||||||
|
// Starts Muse without applying database migrations.
|
||||||
|
import {startBot} from '../index.js';
|
||||||
|
import logBanner from '../utils/log-banner.js';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
logBanner();
|
||||||
|
await startBot();
|
||||||
|
})();
|
|
@ -1,4 +1,5 @@
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import 'reflect-metadata';
|
||||||
import {injectable} from 'inversify';
|
import {injectable} from 'inversify';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import xbytes from 'xbytes';
|
import xbytes from 'xbytes';
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {promises as fs, createWriteStream} from 'fs';
|
import {promises as fs, createWriteStream} from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {inject, injectable} from 'inversify';
|
import {inject, injectable} from 'inversify';
|
||||||
import sequelize from 'sequelize';
|
|
||||||
import {FileCache} from '../models/index.js';
|
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
import Config from './config.js';
|
import Config from './config.js';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import debug from '../utils/debug.js';
|
import debug from '../utils/debug.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
import {FileCache} from '@prisma/client';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class FileCacheProvider {
|
export default class FileCacheProvider {
|
||||||
|
@ -23,7 +23,11 @@ export default class FileCacheProvider {
|
||||||
* @param hash lookup key
|
* @param hash lookup key
|
||||||
*/
|
*/
|
||||||
async getPathFor(hash: string): Promise<string> {
|
async getPathFor(hash: string): Promise<string> {
|
||||||
const model = await FileCache.findByPk(hash);
|
const model = await prisma.fileCache.findUnique({
|
||||||
|
where: {
|
||||||
|
hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
throw new Error('File is not cached');
|
throw new Error('File is not cached');
|
||||||
|
@ -34,12 +38,23 @@ export default class FileCacheProvider {
|
||||||
try {
|
try {
|
||||||
await fs.access(resolvedPath);
|
await fs.access(resolvedPath);
|
||||||
} catch (_: unknown) {
|
} catch (_: unknown) {
|
||||||
await FileCache.destroy({where: {hash}});
|
await prisma.fileCache.delete({
|
||||||
|
where: {
|
||||||
|
hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
throw new Error('File is not cached');
|
throw new Error('File is not cached');
|
||||||
}
|
}
|
||||||
|
|
||||||
await model.update({accessedAt: new Date()});
|
await prisma.fileCache.update({
|
||||||
|
where: {
|
||||||
|
hash,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
accessedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return resolvedPath;
|
return resolvedPath;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +79,13 @@ export default class FileCacheProvider {
|
||||||
try {
|
try {
|
||||||
await fs.rename(tmpPath, finalPath);
|
await fs.rename(tmpPath, finalPath);
|
||||||
|
|
||||||
await FileCache.create({hash, bytes: stats.size, accessedAt: new Date()});
|
await prisma.fileCache.create({
|
||||||
|
data: {
|
||||||
|
hash,
|
||||||
|
accessedAt: new Date(),
|
||||||
|
bytes: stats.size,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug('Errored when moving a finished cache file:', error);
|
debug('Errored when moving a finished cache file:', error);
|
||||||
}
|
}
|
||||||
|
@ -100,14 +121,19 @@ export default class FileCacheProvider {
|
||||||
// Continue to evict until we're under the limit
|
// Continue to evict until we're under the limit
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
while (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) {
|
while (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) {
|
||||||
const oldest = await FileCache.findOne({
|
const oldest = await prisma.fileCache.findFirst({
|
||||||
order: [
|
orderBy: {
|
||||||
['accessedAt', 'ASC'],
|
accessedAt: 'asc',
|
||||||
],
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldest) {
|
if (oldest) {
|
||||||
await oldest.destroy();
|
await prisma.fileCache.delete({
|
||||||
|
where: {
|
||||||
|
hash: oldest.hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
await fs.unlink(path.join(this.config.CACHE_DIR, oldest.hash));
|
await fs.unlink(path.join(this.config.CACHE_DIR, oldest.hash));
|
||||||
debug(`${oldest.hash} has been evicted`);
|
debug(`${oldest.hash} has been evicted`);
|
||||||
numOfEvictedFiles++;
|
numOfEvictedFiles++;
|
||||||
|
@ -128,7 +154,11 @@ export default class FileCacheProvider {
|
||||||
// Check filesystem direction (do files exist on the disk but not in the database?)
|
// Check filesystem direction (do files exist on the disk but not in the database?)
|
||||||
for await (const dirent of await fs.opendir(this.config.CACHE_DIR)) {
|
for await (const dirent of await fs.opendir(this.config.CACHE_DIR)) {
|
||||||
if (dirent.isFile()) {
|
if (dirent.isFile()) {
|
||||||
const model = await FileCache.findByPk(dirent.name);
|
const model = await prisma.fileCache.findUnique({
|
||||||
|
where: {
|
||||||
|
hash: dirent.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
debug(`${dirent.name} was present on disk but was not in the database. Removing from disk.`);
|
debug(`${dirent.name} was present on disk but was not in the database. Removing from disk.`);
|
||||||
|
@ -145,7 +175,11 @@ export default class FileCacheProvider {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch {
|
} catch {
|
||||||
debug(`${model.hash} was present in database but was not on disk. Removing from database.`);
|
debug(`${model.hash} was present in database but was not on disk. Removing from database.`);
|
||||||
await model.destroy();
|
await prisma.fileCache.delete({
|
||||||
|
where: {
|
||||||
|
hash: model.hash,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,11 +190,12 @@ export default class FileCacheProvider {
|
||||||
* @returns the total size of the cache in bytes
|
* @returns the total size of the cache in bytes
|
||||||
*/
|
*/
|
||||||
private async getDiskUsageInBytes() {
|
private async getDiskUsageInBytes() {
|
||||||
const [{dataValues: {totalSizeBytes}}] = await FileCache.findAll({
|
const data = await prisma.fileCache.aggregate({
|
||||||
attributes: [
|
_sum: {
|
||||||
[sequelize.fn('sum', sequelize.col('bytes')), 'totalSizeBytes'],
|
bytes: true,
|
||||||
],
|
},
|
||||||
}) as unknown as [{dataValues: {totalSizeBytes: number}}];
|
});
|
||||||
|
const totalSizeBytes = data._sum.bytes ?? 0;
|
||||||
|
|
||||||
return totalSizeBytes;
|
return totalSizeBytes;
|
||||||
}
|
}
|
||||||
|
@ -176,24 +211,26 @@ export default class FileCacheProvider {
|
||||||
let models: FileCache[] = [];
|
let models: FileCache[] = [];
|
||||||
|
|
||||||
const fetchNextBatch = async () => {
|
const fetchNextBatch = async () => {
|
||||||
let where = {};
|
let where;
|
||||||
|
|
||||||
if (previousCreatedAt) {
|
if (previousCreatedAt) {
|
||||||
where = {
|
where = {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
[sequelize.Op.gt]: previousCreatedAt,
|
gt: previousCreatedAt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
models = await FileCache.findAll({
|
models = await prisma.fileCache.findMany({
|
||||||
where,
|
where,
|
||||||
limit,
|
orderBy: {
|
||||||
order: ['createdAt'],
|
createdAt: 'asc',
|
||||||
|
},
|
||||||
|
take: limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (models.length > 0) {
|
if (models.length > 0) {
|
||||||
previousCreatedAt = models[models.length - 1].createdAt as Date;
|
previousCreatedAt = models[models.length - 1].createdAt;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -42,59 +42,51 @@ export default class {
|
||||||
this.ytsrQueue = new PQueue({concurrency: 4});
|
this.ytsrQueue = new PQueue({concurrency: 4});
|
||||||
}
|
}
|
||||||
|
|
||||||
async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel | null> {
|
async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel> {
|
||||||
try {
|
const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
|
||||||
const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
|
ytsr,
|
||||||
ytsr,
|
query,
|
||||||
query,
|
{
|
||||||
{
|
limit: 10,
|
||||||
limit: 10,
|
},
|
||||||
},
|
{
|
||||||
{
|
expiresIn: ONE_HOUR_IN_SECONDS,
|
||||||
expiresIn: ONE_HOUR_IN_SECONDS,
|
},
|
||||||
},
|
));
|
||||||
));
|
|
||||||
|
|
||||||
let firstVideo: Video | undefined;
|
let firstVideo: Video | undefined;
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type === 'video') {
|
if (item.type === 'video') {
|
||||||
firstVideo = item;
|
firstVideo = item;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstVideo) {
|
|
||||||
throw new Error('No video found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.youtubeVideo(firstVideo.id);
|
|
||||||
} catch (_: unknown) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstVideo) {
|
||||||
|
throw new Error('No video found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.youtubeVideo(firstVideo.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel | null> {
|
async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel> {
|
||||||
try {
|
const videoDetails = await this.cache.wrap(
|
||||||
const videoDetails = await this.cache.wrap(
|
this.youtube.videos.get,
|
||||||
this.youtube.videos.get,
|
cleanUrl(url),
|
||||||
cleanUrl(url),
|
{
|
||||||
{
|
expiresIn: ONE_HOUR_IN_SECONDS,
|
||||||
expiresIn: ONE_HOUR_IN_SECONDS,
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: videoDetails.snippet.title,
|
title: videoDetails.snippet.title,
|
||||||
artist: videoDetails.snippet.channelTitle,
|
artist: videoDetails.snippet.channelTitle,
|
||||||
length: toSeconds(parse(videoDetails.contentDetails.duration)),
|
length: toSeconds(parse(videoDetails.contentDetails.duration)),
|
||||||
url: videoDetails.id,
|
url: videoDetails.id,
|
||||||
playlist: null,
|
playlist: null,
|
||||||
isLive: videoDetails.snippet.liveBroadcastContent === 'live',
|
isLive: videoDetails.snippet.liveBroadcastContent === 'live',
|
||||||
};
|
};
|
||||||
} catch (_: unknown) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
|
async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
|
||||||
|
@ -279,11 +271,7 @@ export default class {
|
||||||
return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs];
|
return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel | null> {
|
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel> {
|
||||||
try {
|
return this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
|
||||||
return await this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
|
|
||||||
} catch (_: unknown) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {injectable} from 'inversify';
|
import {injectable} from 'inversify';
|
||||||
import {KeyValueCache} from '../models/index.js';
|
import {prisma} from '../utils/db.js';
|
||||||
import debug from '../utils/debug.js';
|
import debug from '../utils/debug.js';
|
||||||
|
|
||||||
type Seconds = number;
|
type Seconds = number;
|
||||||
|
@ -29,7 +29,11 @@ export default class KeyValueCacheProvider {
|
||||||
throw new Error(`Cache key ${key} is too short.`);
|
throw new Error(`Cache key ${key} is too short.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedResult = await KeyValueCache.findByPk(key);
|
const cachedResult = await prisma.keyValueCache.findUnique({
|
||||||
|
where: {
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
if (new Date() < cachedResult.expiresAt) {
|
if (new Date() < cachedResult.expiresAt) {
|
||||||
|
@ -37,7 +41,11 @@ export default class KeyValueCacheProvider {
|
||||||
return JSON.parse(cachedResult.value) as F;
|
return JSON.parse(cachedResult.value) as F;
|
||||||
}
|
}
|
||||||
|
|
||||||
await cachedResult.destroy();
|
await prisma.keyValueCache.delete({
|
||||||
|
where: {
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Cache miss: ${key}`);
|
debug(`Cache miss: ${key}`);
|
||||||
|
@ -45,10 +53,21 @@ export default class KeyValueCacheProvider {
|
||||||
const result = await func(...options as any[]);
|
const result = await func(...options as any[]);
|
||||||
|
|
||||||
// Save result
|
// Save result
|
||||||
await KeyValueCache.upsert({
|
const value = JSON.stringify(result);
|
||||||
key,
|
const expiresAt = futureTimeToDate(expiresIn);
|
||||||
value: JSON.stringify(result),
|
await prisma.keyValueCache.upsert({
|
||||||
expiresAt: futureTimeToDate(expiresIn),
|
where: {
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
value,
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -61,6 +61,7 @@ export default class extends (EventEmitter as new () => TypedEmitter<PlayerEvent
|
||||||
const conn = joinVoiceChannel({
|
const conn = joinVoiceChannel({
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
guildId: channel.guild.id,
|
guildId: channel.guild.id,
|
||||||
|
// @ts-expect-error (see https://github.com/discordjs/voice/issues/166)
|
||||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,10 @@ export const getMostPopularVoiceChannel = (guild: Guild): [VoiceChannel, number]
|
||||||
|
|
||||||
for (const [_, channel] of guild.channels.cache) {
|
for (const [_, channel] of guild.channels.cache) {
|
||||||
if (channel.type === 'GUILD_VOICE') {
|
if (channel.type === 'GUILD_VOICE') {
|
||||||
const size = getSizeWithoutBots(channel as VoiceChannel);
|
const size = getSizeWithoutBots(channel);
|
||||||
|
|
||||||
voiceChannels.push({
|
voiceChannels.push({
|
||||||
channel: channel as VoiceChannel,
|
channel,
|
||||||
n: size,
|
n: size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
7
src/utils/create-database-url.ts
Normal file
7
src/utils/create-database-url.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {join} from 'path';
|
||||||
|
|
||||||
|
export const createDatabasePath = (directory: string) => join(directory, 'db.sqlite');
|
||||||
|
|
||||||
|
const createDatabaseUrl = (directory: string) => `file:${createDatabasePath(directory)}`;
|
||||||
|
|
||||||
|
export default createDatabaseUrl;
|
|
@ -1,12 +1,3 @@
|
||||||
import {Sequelize} from 'sequelize-typescript';
|
import Prisma from '@prisma/client';
|
||||||
import path from 'path';
|
|
||||||
import {DATA_DIR} from '../services/config.js';
|
|
||||||
import {FileCache, KeyValueCache, Settings, Shortcut} from '../models/index.js';
|
|
||||||
|
|
||||||
export const sequelize = new Sequelize({
|
export const prisma = new Prisma.PrismaClient();
|
||||||
dialect: 'sqlite',
|
|
||||||
database: 'muse',
|
|
||||||
storage: path.join(DATA_DIR, 'db.sqlite'),
|
|
||||||
models: [FileCache, KeyValueCache, Settings, Shortcut],
|
|
||||||
logging: false,
|
|
||||||
});
|
|
||||||
|
|
16
src/utils/log-banner.ts
Normal file
16
src/utils/log-banner.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {makeLines} from 'nodesplash';
|
||||||
|
import metadata from '../../package.json';
|
||||||
|
|
||||||
|
const logBanner = () => {
|
||||||
|
console.log(makeLines({
|
||||||
|
user: 'codetheweb',
|
||||||
|
repository: 'muse',
|
||||||
|
version: metadata.version,
|
||||||
|
paypalUser: 'codetheweb',
|
||||||
|
githubSponsor: 'codetheweb',
|
||||||
|
madeByPrefix: 'Made with 🎶 by ',
|
||||||
|
}).join('\n'));
|
||||||
|
console.log('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default logBanner;
|
Loading…
Reference in a new issue