mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-10-18 11:25:04 +02:00
♻️ Migrate to NestJS
This commit is contained in:
parent
cc6fd1e2d4
commit
6c5b282bc0
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir : __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
43
.gitignore
vendored
43
.gitignore
vendored
@ -1,4 +1,39 @@
|
|||||||
node_modules
|
# compiled output
|
||||||
package-lock.json
|
/dist
|
||||||
config.json
|
/node_modules
|
||||||
.prettierrc
|
.yarn
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
*.env
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
nodeLinker: node-modules
|
160
README.md
160
README.md
@ -1,145 +1,63 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/jellyfin.png" alt="Jellyfin Logo" width="80" height="80">
|
<a href="http://nestjs.com/" target="blank"><img src="https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true" width="200" alt="Nest Logo" /></a>
|
||||||
<h1 align="center">Jellyfin Discord Music Bot</h1>
|
|
||||||
<div align="center">
|
|
||||||
<span>A fork of the <a href="https://github.com/KGT1/jellyfin-discord-music-bot">original project</a> with improved readability and stability, compatible with Jellyfin 10.8.x</span>
|
|
||||||
</div>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# ✨ Features
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
- Simple Discord Bot that hooks into the [Jellyfin](http://github.com/jellyfin/jellyfin) API of your instance
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
- Request, pause and play songs directly from your Discord Server
|
|
||||||
- Interactive Media control message to control playback
|
|
||||||
|
|
||||||
# 🦾 About this fork
|
<p align="center">A simple <a href="https://discord.com" target="_blank">Discord</a> bot that enables you to broadcast your <a href="https://jellyfin.org/" target="_blank">Jellyfin Media Server</a> music collection to voice channels.</p>
|
||||||
The original version is decent for Jellyfin 10.6.x and before. After the breaking changes of 10.7 and 10.8, users were unable to stream music from their Jellyfin.
|
|
||||||
For this reason, I made this fork to address those changes to the API and improve the bot with my own ideas / features. Please check out the original project by [KGT1](https://github.com/KGT1).
|
|
||||||
|
|
||||||
I will gradually update documentation & code of the bot. Please wait patiently.
|
<p align="center">
|
||||||
|
<small>Thanky you KGT1 for starting this project! This is a fork of their original repository and re-uses some of their code.</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
<br/><br/><br/><br/><br/><br/><hr/><br/>
|
|
||||||
|
|
||||||
## Original README from https://github.com/KGT1/jellyfin-discord-music-bot
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<hr/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
Jellyfin Discord Music Bot is a Discord Bot for the [Jellyfin Media Server!](http://github.com/jellyfin/jellyfin)
|
|
||||||
|
|
||||||
### Capabilities
|
## ✨ Features
|
||||||
|
|
||||||
#### Play to
|
- Leighweight and extendable using the [Nest](https://github.com/nestjs/nest) framework
|
||||||
|
- Easy ussage with Discord commands system (eg. ``/play``, ``/pause``, ...)
|
||||||
|
- Fast configuration via environment variables
|
||||||
|
- Typesafe for faster and easier development
|
||||||
|
|
||||||
Just `summon` the Bot into your Channel, than choose the Bot in Jellyfin as the Device you want to cast to
|
## 🚀 Installation
|
||||||
|
|
||||||
![Image to Discord Play to Window](img/playtowindow.png)
|
|
||||||
|
|
||||||
and start playing you favourite Music
|
|
||||||
|
|
||||||
#### Interactive Play Message
|
|
||||||
|
|
||||||
When you start playing something you can easily controll the Bot with just clicking on the Buttons under the Play Message
|
|
||||||
|
|
||||||
![Image to Interactive Play Message](img/discordplaymessage.png)
|
|
||||||
|
|
||||||
#### Commands
|
|
||||||
|
|
||||||
Beware that you'll always need to add your prefix(default: ?) in front of the command.
|
|
||||||
|
|
||||||
Command | Description
|
|
||||||
------------ | -------------
|
|
||||||
summon | Join the channel the author of the message(now you can cast to the Bot from within Jellyfin)
|
|
||||||
disconnect | Disconnect from all current Voice Channels
|
|
||||||
play | Play the following item(can be the name of the song or the Stream URL)
|
|
||||||
add | Add the following item to the current playlist
|
|
||||||
pause/resume | Pause/Resume audio
|
|
||||||
seek | Where to Seek to in seconds or MM:SS
|
|
||||||
skip | Skip this Song
|
|
||||||
spawn | Spawns an Interactive Play Controller
|
|
||||||
help | Display the help message
|
|
||||||
|
|
||||||
#### Limitations
|
|
||||||
- No Playlist Repeat Mode.
|
|
||||||
- Multi Server support.
|
|
||||||
- [Playing Video Content](https://github.com/discordjs/discord.js/issues/4116) (if Discord ever adds this, I'll implement it into this Bot)
|
|
||||||
|
|
||||||
### Getting Started
|
|
||||||
You'll need a Discord Application for this Bot to work, as you will host it yourself.
|
|
||||||
|
|
||||||
[Generate an Api and bot here](https://discord.com/developers/applications/).
|
|
||||||
|
|
||||||
Click New Application.
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124506-bba00080-1706-11eb-820a-035039484ca2.png)
|
|
||||||
|
|
||||||
The Name of the application will be the bot's name.
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124528-d2deee00-1706-11eb-8a05-8b0542e1213a.png)
|
|
||||||
|
|
||||||
Go to the Bot tab.
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124557-ef7b2600-1706-11eb-8fed-2373df9a1eb7.png)
|
|
||||||
|
|
||||||
Generate the bot, and grab the token. Also, recommend making the bot private.
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124639-484abe80-1707-11eb-92f9-1182aad3d2d2.png)
|
|
||||||
|
|
||||||
Go to the OAuth2 page, click Bot Scope to get the url authorization link.
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124754-b68f8100-1707-11eb-9e16-f84401d108bf.png)
|
|
||||||
|
|
||||||
Authorize your room!
|
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/20715731/97124818-08380b80-1708-11eb-944a-f96395dcf6c1.png)
|
|
||||||
|
|
||||||
Next, join a voice channel and connect your bot with ?summon. This will connect your bot to the voice channel you're in and will create the device profile in Jellyfin.
|
|
||||||
|
|
||||||
![Image to Discord Play to Window](img/playtowindow.png)
|
|
||||||
|
|
||||||
From within Jellyfin, start playing content or from within Discord, use the bot commands to start enjoying music!
|
|
||||||
|
|
||||||
For official documentation to creating a bot.
|
|
||||||
|
|
||||||
[How to retrieve your token](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot)
|
|
||||||
|
|
||||||
[How to invite the Bot to your server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html#bot-invite-links)
|
|
||||||
|
|
||||||
### The simplest way to get started is using Docker:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
$ git clone https://github.com/manuel-rw/jellyfin-discord-music-bot.git
|
||||||
--name jellyfin-discord-music-bot \
|
$ cd jellyfin-discord-music-bot/
|
||||||
-e DISCORD_PREFIX="?" \
|
$ yarn
|
||||||
-e DISCORD_TOKEN="yourtokengoeshere" \
|
$ yarn start:prod
|
||||||
-e JELLYFIN_SERVER_ADDRESS="https://jellyfin.DOMAIN" \
|
|
||||||
-e JELLYFIN_USERNAME="" \
|
|
||||||
-e JELLYFIN_PASSWORD="" \
|
|
||||||
-e JELLYFIN_APP_NAME="Jellyfin Discord Music Bot" \
|
|
||||||
-e MESSAGE_UPDATE_INTERVAL="2000" \
|
|
||||||
--restart unless-stopped \
|
|
||||||
kgt1/jellyfin-discord-music-bot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
MESSAGE_UPDATE_INTERVAL is the amount of time in ms the play message gets updated with the current time
|
> Docker container comming soon
|
||||||
|
|
||||||
#### Alternatively you can run the Application natively with NodeJS:
|
## 💻 Development
|
||||||
|
|
||||||
Dependencies:
|
I'm open to any contributions to this project. You can start contributing using the following commands, after executing the installation commands:
|
||||||
|
|
||||||
- npm 6.14.6
|
|
||||||
- NodeJS v12.18.3
|
|
||||||
- ffmpeg 4.2.4
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/kgt1/jellyfin-discord-music-bot.git
|
# development
|
||||||
cd jellyfin-discord-music-bot
|
$ npm run start
|
||||||
npm install
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
```
|
```
|
||||||
edit config.json and add your token,server-address etc.
|
|
||||||
```bash
|
```bash
|
||||||
npm run start
|
# unit tests
|
||||||
```
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
### How to build
|
# test coverage
|
||||||
```
|
$ npm run test:cov
|
||||||
git clone https://github.com/kgt1/jellyfin-discord-music-bot.git
|
|
||||||
cd jellyfin-discord-music-bot
|
|
||||||
docker build -t YOUR_IMAGE_NAME .
|
|
||||||
```
|
```
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
5
nest-cli.json
Normal file
5
nest-cli.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src"
|
||||||
|
}
|
4088
package-lock.json
generated
Normal file
4088
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
106
package.json
106
package.json
@ -1,48 +1,78 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyfin-discord-music-bot",
|
"name": "jellyfin-discord-music-bot",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Jellyfin Discord Music Bot is a Discord Bot for the Jellyfin Media Server!",
|
"description": "",
|
||||||
"main": "src/index.js",
|
"author": "manuel-rw",
|
||||||
"scripts": {
|
"private": true,
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"start": "node src/index.js",
|
|
||||||
"postinstall": "npx patch-package",
|
|
||||||
"lint": "npx eslint src/ & npx eslint parseENV.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/KGT1/jellyfin-discord-music-bot.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"Jellyfin",
|
|
||||||
"Discord",
|
|
||||||
"Discord-Bot"
|
|
||||||
],
|
|
||||||
"author": "KGT1",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"scripts": {
|
||||||
"url": "https://github.com/KGT1/jellyfin-discord-music-bot/issues"
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/KGT1/jellyfin-discord-music-bot#readme",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.3.2",
|
"@discordjs/opus": "^0.9.0",
|
||||||
"chalk": "^4.1.0",
|
"@jellyfin/sdk": "^0.7.0",
|
||||||
"discord.js": "^12.3.1",
|
"@nestjs/common": "^9.0.0",
|
||||||
"jellyfin-apiclient": "1.7.0",
|
"@nestjs/config": "^2.2.0",
|
||||||
"loglevel": "^1.7.1",
|
"@nestjs/core": "^9.0.0",
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
"@nestjs/event-emitter": "^1.3.1",
|
||||||
"node-fetch": "^2.6.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"nodejs": "0.0.0",
|
"discord.js": "^14.7.1",
|
||||||
"window": "^4.2.7",
|
"jellyfin-apiclient": "^1.10.0",
|
||||||
"ws": "^7.3.1"
|
"joi": "^17.7.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rxjs": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"patch-package": "^6.4.7",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"eslint": "^7.9.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"eslint-config-standard": "^14.1.1",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"eslint-plugin-import": "^2.22.0",
|
"@types/express": "^4.17.13",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"@types/jest": "28.1.8",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"@types/node": "^16.0.0",
|
||||||
"eslint-plugin-standard": "^4.0.1"
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
"eslint": "^8.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"jest": "28.1.3",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"source-map-support": "^0.5.20",
|
||||||
|
"supertest": "^6.1.3",
|
||||||
|
"ts-jest": "28.0.8",
|
||||||
|
"ts-loader": "^9.2.3",
|
||||||
|
"ts-node": "^10.0.0",
|
||||||
|
"tsconfig-paths": "4.1.0",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,217 +0,0 @@
|
|||||||
const discordclientmanager = require("./discordclientmanager");
|
|
||||||
const CONFIG = require("../config.json");
|
|
||||||
const { secondsToHms, ticksToSeconds } = require("./util");
|
|
||||||
const log = require("loglevel");
|
|
||||||
|
|
||||||
const getProgressString = (percent) => {
|
|
||||||
// the min with of the discord window allows for this many chars
|
|
||||||
const NUMBER_OF_CHARS = 12;
|
|
||||||
let string = "";
|
|
||||||
for (let iX = 0; iX < NUMBER_OF_CHARS; iX++) {
|
|
||||||
if (percent > iX / NUMBER_OF_CHARS) {
|
|
||||||
string += "█";
|
|
||||||
} else {
|
|
||||||
string += "▒";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {String} string
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
// TODO do this with something like wcwidth
|
|
||||||
function getMaxWidthString (string) {
|
|
||||||
const NUMBER_OF_CHARS = 12;
|
|
||||||
if (string.length > NUMBER_OF_CHARS) {
|
|
||||||
return string.slice(0, NUMBER_OF_CHARS - 3) + "...";
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class InterActivePlayMessage {
|
|
||||||
// musicplayermessage
|
|
||||||
// probably should have done events instead of callbacks
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Object} message
|
|
||||||
* @param {String} title
|
|
||||||
* @param {String} artist
|
|
||||||
* @param {String} imageURL
|
|
||||||
* @param {String} itemURL
|
|
||||||
* @param {Number} ticksLength
|
|
||||||
* @param {Function} onPrevious
|
|
||||||
* @param {Function} onPausePlay
|
|
||||||
* @param {Function} onStop
|
|
||||||
* @param {Function} onNext
|
|
||||||
* @param {Function} onRepeat
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
ticksLength,
|
|
||||||
onPrevious,
|
|
||||||
onPausePlay,
|
|
||||||
onStop,
|
|
||||||
onNext,
|
|
||||||
onRepeat,
|
|
||||||
playlistLenth,
|
|
||||||
) {
|
|
||||||
this.ticksLength = ticksLength;
|
|
||||||
var exampleEmbed = {
|
|
||||||
color: 0x0099ff,
|
|
||||||
title: "Now Playing",
|
|
||||||
url: itemURL,
|
|
||||||
description: `\`\`${getMaxWidthString(title)}\`\` by \`\`${getMaxWidthString(
|
|
||||||
artist,
|
|
||||||
)}\`\``,
|
|
||||||
thumbnail: {
|
|
||||||
url: imageURL,
|
|
||||||
},
|
|
||||||
fields: [],
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
if (typeof CONFIG["interactive-seek-bar-update-intervall"] === "number") {
|
|
||||||
exampleEmbed.fields.push({
|
|
||||||
name: getProgressString(0 / this.ticksLength),
|
|
||||||
value: `${secondsToHms(0)} / ${secondsToHms(
|
|
||||||
ticksToSeconds(this.ticksLength),
|
|
||||||
)}`,
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (playlistLenth) {
|
|
||||||
exampleEmbed.fields.push({
|
|
||||||
name: `1 of ${playlistLenth}`,
|
|
||||||
value: "Playlist",
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
message.channel
|
|
||||||
.send({
|
|
||||||
embed: exampleEmbed,
|
|
||||||
})
|
|
||||||
.then((val) => {
|
|
||||||
this.musicplayermessage = val;
|
|
||||||
val.react("⏮️");
|
|
||||||
val.react("⏯️");
|
|
||||||
val.react("⏹️");
|
|
||||||
val.react("⏭️");
|
|
||||||
val.react("🔁");
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
function reactionchange(reaction, user, musicplayermessage) {
|
|
||||||
if (reaction.message.id === musicplayermessage.id && !user.bot) {
|
|
||||||
try {
|
|
||||||
switch (reaction._emoji.name) {
|
|
||||||
case "⏮️":
|
|
||||||
onPrevious();
|
|
||||||
break;
|
|
||||||
case "⏯️":
|
|
||||||
onPausePlay();
|
|
||||||
break;
|
|
||||||
case "⏹️":
|
|
||||||
onStop();
|
|
||||||
break;
|
|
||||||
case "⏭️":
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
case "🔁":
|
|
||||||
onRepeat();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
discordclientmanager
|
|
||||||
.getDiscordClient()
|
|
||||||
.on("messageReactionAdd", (reaction, user) => {
|
|
||||||
reactionchange(reaction, user, this.musicplayermessage);
|
|
||||||
});
|
|
||||||
discordclientmanager
|
|
||||||
.getDiscordClient()
|
|
||||||
.on("messageReactionRemove", (reaction, user) => {
|
|
||||||
reactionchange(reaction, user, this.musicplayermessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress(ticks) {
|
|
||||||
if (
|
|
||||||
typeof this.musicplayermessage !== "undefined" &&
|
|
||||||
typeof this.musicplayermessage.embeds[0] !== "undefined" &&
|
|
||||||
typeof this.musicplayermessage.embeds[0].fields[0] !== "undefined"
|
|
||||||
) {
|
|
||||||
this.musicplayermessage.embeds[0].fields[0] = {
|
|
||||||
name: getProgressString(ticks / this.ticksLength),
|
|
||||||
value: `${secondsToHms(ticksToSeconds(ticks))} / ${secondsToHms(
|
|
||||||
ticksToSeconds(this.ticksLength),
|
|
||||||
)}`,
|
|
||||||
inline: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.musicplayermessage.timestamp = new Date();
|
|
||||||
this.musicplayermessage.edit(this.musicplayermessage.embeds[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentSongMessage(
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
ticksLength,
|
|
||||||
playlistIndex,
|
|
||||||
playlistLenth,
|
|
||||||
) {
|
|
||||||
if (!this.musicplayermessage) {
|
|
||||||
log.error("Interactive play message was not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.musicplayermessage.embeds.length === 0) {
|
|
||||||
log.error("Interactive play message was unable to access embeds");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.musicplayermessage.embeds[0].url = itemURL;
|
|
||||||
this.musicplayermessage.embeds[0].description = `\`\`${getMaxWidthString(
|
|
||||||
title,
|
|
||||||
)}\`\` by \`\`${getMaxWidthString(artist)}\`\``;
|
|
||||||
this.musicplayermessage.embeds[0].thumbnail = { url: imageURL };
|
|
||||||
const indexOfPlaylistMessage =
|
|
||||||
this.musicplayermessage.embeds[0].fields.findIndex((element) => {
|
|
||||||
return element.value === "Playlist";
|
|
||||||
});
|
|
||||||
if (indexOfPlaylistMessage === -1) {
|
|
||||||
this.musicplayermessage.embeds[0].fields.push({
|
|
||||||
name: `${playlistIndex} of ${playlistLenth}`,
|
|
||||||
value: "Playlist",
|
|
||||||
inline: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.musicplayermessage.embeds[0].fields[
|
|
||||||
indexOfPlaylistMessage
|
|
||||||
].name = `${playlistIndex} of ${playlistLenth}`;
|
|
||||||
}
|
|
||||||
this.ticksLength = ticksLength;
|
|
||||||
|
|
||||||
this.musicplayermessage.timestamp = new Date();
|
|
||||||
this.musicplayermessage.edit(this.musicplayermessage.embeds[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.musicplayermessage.delete();
|
|
||||||
delete this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = InterActivePlayMessage;
|
|
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
src/app.controller.ts
Normal file
12
src/app.controller.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
31
src/app.module.ts
Normal file
31
src/app.module.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { DiscordClientModule } from './clients/discord/discord.module';
|
||||||
|
import { CommandHandlerModule } from './commands/handler/command-handler.module';
|
||||||
|
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
validationSchema: Joi.object({
|
||||||
|
DISCORD_CLIENT_TOKEN: Joi.string().required(),
|
||||||
|
JELLYFIN_SERVER_ADDRESS: Joi.string().required(),
|
||||||
|
JELLYFIN_AUTHENTICATION_USERNAME: Joi.string().required(),
|
||||||
|
JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
DiscordClientModule,
|
||||||
|
JellyfinClientModule,
|
||||||
|
CommandHandlerModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
9
src/app.service.ts
Normal file
9
src/app.service.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
constructor() {}
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
22
src/clients/discord/discord.module.ts
Normal file
22
src/clients/discord/discord.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { DiscordService } from "./discord.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [],
|
||||||
|
providers: [DiscordService],
|
||||||
|
exports: [DiscordService],
|
||||||
|
})
|
||||||
|
export class DiscordClientModule implements OnModuleInit, OnModuleDestroy {
|
||||||
|
|
||||||
|
constructor(private discordService: DiscordService) {}
|
||||||
|
onModuleDestroy() {
|
||||||
|
this.discordService.destroyClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.discordService.initializeClient();
|
||||||
|
this.discordService.registerEventHandlers();
|
||||||
|
this.discordService.connectAndLogin();
|
||||||
|
}
|
||||||
|
}
|
46
src/clients/discord/discord.service.ts
Normal file
46
src/clients/discord/discord.service.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
import { ActivityType, Client } from 'discord.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DiscordService {
|
||||||
|
private readonly logger = new Logger(DiscordService.name);
|
||||||
|
private client: Client;
|
||||||
|
|
||||||
|
constructor(private eventEmitter: EventEmitter2) {}
|
||||||
|
|
||||||
|
initializeClient() {
|
||||||
|
this.client = new Client({
|
||||||
|
intents: ['Guilds', 'GuildMessages', 'MessageContent'],
|
||||||
|
});
|
||||||
|
this.logger.debug('Initialized Discord client');
|
||||||
|
}
|
||||||
|
|
||||||
|
connectAndLogin() {
|
||||||
|
this.client.login(process.env.DISCORD_CLIENT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventHandlers() {
|
||||||
|
this.client.on('ready', () => {
|
||||||
|
this.logger.debug(`Connected as '${this.client.user.tag}' and ready!`);
|
||||||
|
this.eventEmitter.emit('client.discord.ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('messageCreate', async (message) => {
|
||||||
|
if (message.author.bot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.channel.send('nice');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyClient() {
|
||||||
|
this.client.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
22
src/clients/jellyfin/jellyfin.module.ts
Normal file
22
src/clients/jellyfin/jellyfin.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { JellyfinService } from "./jellyfin.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [],
|
||||||
|
providers: [JellyfinService],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class JellyfinClientModule implements OnModuleInit, OnModuleDestroy {
|
||||||
|
|
||||||
|
constructor(private jellyfinService: JellyfinService) {}
|
||||||
|
|
||||||
|
onModuleDestroy() {
|
||||||
|
this.jellyfinService.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.jellyfinService.init();
|
||||||
|
this.jellyfinService.authenticate();
|
||||||
|
}
|
||||||
|
}
|
55
src/clients/jellyfin/jellyfin.service.ts
Normal file
55
src/clients/jellyfin/jellyfin.service.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Api, Jellyfin } from '@jellyfin/sdk';
|
||||||
|
|
||||||
|
import { Constants } from 'src/utils/constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JellyfinService {
|
||||||
|
private readonly logger = new Logger(JellyfinService.name);
|
||||||
|
private jellyfin: Jellyfin;
|
||||||
|
private api: Api;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.jellyfin = new Jellyfin({
|
||||||
|
clientInfo: {
|
||||||
|
name: Constants.Metadata.ApplicationName,
|
||||||
|
version: Constants.Metadata.Version,
|
||||||
|
},
|
||||||
|
deviceInfo: {
|
||||||
|
id: 'test',
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api = this.jellyfin.createApi(process.env.JELLYFIN_SERVER_ADDRESS);
|
||||||
|
this.logger.debug('Created Jellyfin Client and Api');
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate() {
|
||||||
|
this.api
|
||||||
|
.authenticateUserByName(
|
||||||
|
process.env.JELLYFIN_AUTHENTICATION_USERNAME,
|
||||||
|
process.env.JELLYFIN_AUTHENTICATION_PASSWORD,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
this.logger.debug(
|
||||||
|
`Connected using user '${response.data.SessionInfo.UserId}'`,
|
||||||
|
);
|
||||||
|
}).catch((test) => {
|
||||||
|
this.logger.error(test);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.api === undefined) {
|
||||||
|
this.logger.warn(
|
||||||
|
'Jellyfin Api Client was unexpectitly undefined. Graceful destroy has failed',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.api.logout();
|
||||||
|
}
|
||||||
|
}
|
6
src/commands/abstractCommand.ts
Normal file
6
src/commands/abstractCommand.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
export abstract class Command {
|
||||||
|
abstract builder(): SlashCommandBuilder;
|
||||||
|
abstract execute(): void;
|
||||||
|
}
|
10
src/commands/handler/command-handler.module.ts
Normal file
10
src/commands/handler/command-handler.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DiscordClientModule } from '../../clients/discord/discord.module';
|
||||||
|
import { CommandHandlerService } from './command-handler.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DiscordClientModule],
|
||||||
|
controllers: [],
|
||||||
|
providers: [CommandHandlerService],
|
||||||
|
})
|
||||||
|
export class CommandHandlerModule {}
|
107
src/commands/handler/command-handler.service.ts
Normal file
107
src/commands/handler/command-handler.service.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { EmbedBuilder } from '@discordjs/builders';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
ApplicationCommand,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
SlashCommandSubcommandBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { DiscordService } from 'src/clients/discord/discord.service';
|
||||||
|
import { Command } from '../abstractCommand';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CommandHandlerService {
|
||||||
|
private logger: Logger = new Logger(CommandHandlerService.name);
|
||||||
|
|
||||||
|
constructor(private discordService: DiscordService) {}
|
||||||
|
|
||||||
|
@OnEvent('client.discord.ready')
|
||||||
|
async handleOnDiscordClientReady() {
|
||||||
|
var commands = [
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('play')
|
||||||
|
.setDescription('Immideatly play a track')
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('track')
|
||||||
|
.setDescription('the track name')
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('summon')
|
||||||
|
.setDescription('Join your current voice channel'),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('disconnect')
|
||||||
|
.setDescription('Disconnect from the current voice channel'),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('enqueue')
|
||||||
|
.setDescription('Enqueue a track to the current playlist')
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName('track')
|
||||||
|
.setDescription('the track name')
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('current')
|
||||||
|
.setDescription('Print the current track information'),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('pause')
|
||||||
|
.setDescription('Pause or resume the playback of the current track'),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('skip')
|
||||||
|
.setDescription('Skip the current track'),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('stop')
|
||||||
|
.setDescription(
|
||||||
|
'Stop playback entirely and clear the current playlist',
|
||||||
|
),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('help')
|
||||||
|
.setDescription('Get help for this Discord Bot'),
|
||||||
|
];
|
||||||
|
|
||||||
|
await this.discordService
|
||||||
|
.getClient()
|
||||||
|
.application.commands.set(commands.map((x) => x.toJSON()));
|
||||||
|
|
||||||
|
this.discordService
|
||||||
|
.getClient()
|
||||||
|
.on('interactionCreate', async (interaction) => {
|
||||||
|
if (!interaction.isChatInputCommand()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setAuthor({
|
||||||
|
name: 'Jellyfin Discord Bot',
|
||||||
|
iconURL:
|
||||||
|
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true',
|
||||||
|
url: 'https://github.com/manuel-rw/jellyfin-discord-music-bot',
|
||||||
|
})
|
||||||
|
.setTitle('Help Information')
|
||||||
|
.setDescription(
|
||||||
|
'Jellyfin Discord Music bot is an easy way to broadcast your music collection to a Discord voicechannel.',
|
||||||
|
)
|
||||||
|
.addFields([
|
||||||
|
{
|
||||||
|
name: 'Report an issue',
|
||||||
|
value:
|
||||||
|
'https://github.com/manuel-rw/jellyfin-discord-music-bot/issues/new/choose',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Source code',
|
||||||
|
value:
|
||||||
|
'https://github.com/manuel-rw/jellyfin-discord-music-bot',
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.toJSON(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
const Discord = require("discord.js");
|
|
||||||
|
|
||||||
var discordClient;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
discordClient = new Discord.Client();
|
|
||||||
}
|
|
||||||
function getDiscordClient() {
|
|
||||||
return discordClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getDiscordClient,
|
|
||||||
init,
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
var audioDispatcher;
|
|
||||||
|
|
||||||
function setAudioDispatcher(par) {
|
|
||||||
audioDispatcher = par;
|
|
||||||
}
|
|
||||||
function getAudioDispatcher() {
|
|
||||||
return audioDispatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setAudioDispatcher,
|
|
||||||
getAudioDispatcher,
|
|
||||||
};
|
|
64
src/index.js
64
src/index.js
@ -1,64 +0,0 @@
|
|||||||
const log = require("loglevel");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const CONFIG = require("../config.json");
|
|
||||||
|
|
||||||
const jellyfinClientManager = require("./jellyfinclientmanager");
|
|
||||||
|
|
||||||
const discordclientmanager = require("./discordclientmanager");
|
|
||||||
discordclientmanager.init();
|
|
||||||
const discordClient = discordclientmanager.getDiscordClient();
|
|
||||||
const { handleChannelMessage } = require("./messagehandler");
|
|
||||||
|
|
||||||
const prefix = require("loglevel-plugin-prefix");
|
|
||||||
const chalk = require("chalk");
|
|
||||||
const colors = {
|
|
||||||
TRACE: chalk.magenta,
|
|
||||||
DEBUG: chalk.cyan,
|
|
||||||
INFO: chalk.blue,
|
|
||||||
WARN: chalk.yellow,
|
|
||||||
ERROR: chalk.red,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.setLevel(CONFIG["log-level"]);
|
|
||||||
|
|
||||||
prefix.reg(log);
|
|
||||||
log.enableAll();
|
|
||||||
|
|
||||||
prefix.apply(log, {
|
|
||||||
format(level, name, timestamp) {
|
|
||||||
return `${chalk.gray(`[${timestamp}]`)} ${colors[level.toUpperCase()](
|
|
||||||
level
|
|
||||||
)} ${chalk.green(`${name}:`)}`;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
prefix.apply(log.getLogger("critical"), {
|
|
||||||
format(level, name, timestamp) {
|
|
||||||
return chalk.red.bold(`[${timestamp}] ${level} ${name}:`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
jellyfinClientManager.init();
|
|
||||||
// TODO Error Checking as the apiclients is inefficent
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.authenticateUserByName(
|
|
||||||
CONFIG["jellyfin-username"],
|
|
||||||
CONFIG["jellyfin-password"]
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.setAuthenticationInfo(response.AccessToken, response.SessionInfo.UserId);
|
|
||||||
});
|
|
||||||
|
|
||||||
discordClient.on("message", (message) => {
|
|
||||||
handleChannelMessage(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
discordClient.login(CONFIG.token);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
const InterActivePlayMessage = require("./InterActivePlayMessage");
|
|
||||||
const CONFIG = require("../config.json");
|
|
||||||
|
|
||||||
const log = require("loglevel");
|
|
||||||
|
|
||||||
var interactivePlayMessage;
|
|
||||||
|
|
||||||
var updateInterval;
|
|
||||||
|
|
||||||
const init = (
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
getProgress,
|
|
||||||
onPrevious,
|
|
||||||
onPausePlay,
|
|
||||||
onStop,
|
|
||||||
onNext,
|
|
||||||
onRepeat,
|
|
||||||
playlistLenth
|
|
||||||
) => {
|
|
||||||
if (typeof interactivePlayMessage !== "undefined") {
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
interactivePlayMessage = new InterActivePlayMessage(
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
getProgress,
|
|
||||||
onPrevious,
|
|
||||||
onPausePlay,
|
|
||||||
onStop,
|
|
||||||
onNext,
|
|
||||||
onRepeat,
|
|
||||||
playlistLenth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
if (typeof interactivePlayMessage !== "undefined") {
|
|
||||||
interactivePlayMessage.destroy();
|
|
||||||
interactivePlayMessage = undefined;
|
|
||||||
} else {
|
|
||||||
throw Error("No Interactive Message Found");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateInterval !== "undefined") {
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
updateInterval = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasMessage = () => {
|
|
||||||
if (typeof interactivePlayMessage === "undefined") {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Function} callback function to retrieve current ticks
|
|
||||||
*/
|
|
||||||
const startUpate = (callback) => {
|
|
||||||
if (
|
|
||||||
typeof CONFIG["interactive-seek-bar-update-intervall"] === "number" &&
|
|
||||||
CONFIG["interactive-seek-bar-update-intervall"] > 0
|
|
||||||
) {
|
|
||||||
updateInterval = setInterval(() => {
|
|
||||||
interactivePlayMessage.updateProgress(callback());
|
|
||||||
}, CONFIG["interactive-seek-bar-update-intervall"]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCurrentSongMessage = (
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
ticksLength,
|
|
||||||
playlistIndex,
|
|
||||||
playlistLenth
|
|
||||||
) => {
|
|
||||||
log.log(interactivePlayMessage);
|
|
||||||
|
|
||||||
if (typeof interactivePlayMessage !== "undefined") {
|
|
||||||
interactivePlayMessage.updateCurrentSongMessage(
|
|
||||||
title,
|
|
||||||
artist,
|
|
||||||
imageURL,
|
|
||||||
itemURL,
|
|
||||||
ticksLength,
|
|
||||||
playlistIndex,
|
|
||||||
playlistLenth
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw Error("No Interactive Message Found");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
destroy,
|
|
||||||
hasMessage,
|
|
||||||
startUpate,
|
|
||||||
updateCurrentSongMessage
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
const { ApiClient, Events } = require("jellyfin-apiclient");
|
|
||||||
const CONFIG = require("../config.json");
|
|
||||||
const os = require("os");
|
|
||||||
|
|
||||||
var jellyfinClient;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
jellyfinClient = new ApiClient(
|
|
||||||
CONFIG["server-address"],
|
|
||||||
CONFIG["jellyfin-app-name"],
|
|
||||||
"0.0.1",
|
|
||||||
os.hostname(),
|
|
||||||
os.hostname()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJellyfinClient() {
|
|
||||||
return jellyfinClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJellyfinEvents() {
|
|
||||||
return Events;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getJellyfinClient,
|
|
||||||
getJellyfinEvents,
|
|
||||||
init,
|
|
||||||
};
|
|
8
src/main.ts
Normal file
8
src/main.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
@ -1,308 +0,0 @@
|
|||||||
const CONFIG = require("../config.json");
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
const { checkJellyfinItemIDRegex } = require("./util");
|
|
||||||
const { hmsToSeconds, getDiscordEmbedError } = require("./util");
|
|
||||||
|
|
||||||
const discordclientmanager = require("./discordclientmanager");
|
|
||||||
const jellyfinClientManager = require("./jellyfinclientmanager");
|
|
||||||
const playbackmanager = require("./playbackmanager");
|
|
||||||
const websocketHanler = require("./websockethandler");
|
|
||||||
const discordClient = discordclientmanager.getDiscordClient();
|
|
||||||
|
|
||||||
var isSummendByPlay = false;
|
|
||||||
|
|
||||||
// random Color of the Jellyfin Logo Gradient
|
|
||||||
function getRandomDiscordColor() {
|
|
||||||
const random = Math.random();
|
|
||||||
function randomNumber(b, a) {
|
|
||||||
return (
|
|
||||||
Math.floor(random * Math.pow(Math.pow(b - a, 2), 1 / 2)) + (b > a ? a : b)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const GRANDIENT_START = "#AA5CC3";
|
|
||||||
const GRANDIENT_END = "#00A4DC";
|
|
||||||
|
|
||||||
let rS = GRANDIENT_START.slice(1, 3);
|
|
||||||
let gS = GRANDIENT_START.slice(3, 5);
|
|
||||||
let bS = GRANDIENT_START.slice(5, 7);
|
|
||||||
rS = parseInt(rS, 16);
|
|
||||||
gS = parseInt(gS, 16);
|
|
||||||
bS = parseInt(bS, 16);
|
|
||||||
|
|
||||||
let rE = GRANDIENT_END.slice(1, 3);
|
|
||||||
let gE = GRANDIENT_END.slice(3, 5);
|
|
||||||
let bE = GRANDIENT_END.slice(5, 7);
|
|
||||||
rE = parseInt(rE, 16);
|
|
||||||
gE = parseInt(gE, 16);
|
|
||||||
bE = parseInt(bE, 16);
|
|
||||||
|
|
||||||
return (
|
|
||||||
"#" +
|
|
||||||
("00" + randomNumber(rS, rE).toString(16)).substr(-2) +
|
|
||||||
("00" + randomNumber(gS, gE).toString(16)).substr(-2) +
|
|
||||||
("00" + randomNumber(bS, bE).toString(16)).substr(-2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Song Search, return the song itemID
|
|
||||||
async function searchForItemID(searchString) {
|
|
||||||
const response = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getSearchHints({
|
|
||||||
searchTerm: searchString,
|
|
||||||
includeItemTypes: "Audio,MusicAlbum,Playlist",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.TotalRecordCount < 1) {
|
|
||||||
throw Error("Found nothing");
|
|
||||||
} else {
|
|
||||||
switch (response.SearchHints[0].Type) {
|
|
||||||
case "Audio":
|
|
||||||
return [response.SearchHints[0].ItemId];
|
|
||||||
case "Playlist":
|
|
||||||
case "MusicAlbum": {
|
|
||||||
const resp = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getItems(jellyfinClientManager.getJellyfinClient().getCurrentUserId(), {
|
|
||||||
sortBy: "SortName",
|
|
||||||
sortOrder: "Ascending",
|
|
||||||
parentId: response.SearchHints[0].ItemId,
|
|
||||||
});
|
|
||||||
const itemArray = [];
|
|
||||||
resp.Items.forEach((element) => {
|
|
||||||
itemArray.push(element.Id);
|
|
||||||
});
|
|
||||||
return itemArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function summon(voiceChannel) {
|
|
||||||
voiceChannel.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
function summonMessage(message) {
|
|
||||||
if (!message.member.voice.channel) {
|
|
||||||
message.reply("please join a voice channel to summon me!");
|
|
||||||
} else if (message.channel.type === "dm") {
|
|
||||||
message.reply("no dms");
|
|
||||||
} else {
|
|
||||||
var desc = "**Joined Voice Channel** `";
|
|
||||||
desc = desc.concat(message.member.voice.channel.name).concat("`");
|
|
||||||
|
|
||||||
summon(message.member.voice.channel);
|
|
||||||
|
|
||||||
const vcJoin = new Discord.MessageEmbed()
|
|
||||||
.setColor(getRandomDiscordColor())
|
|
||||||
.setTitle("Joined Channel")
|
|
||||||
.setTimestamp()
|
|
||||||
.setDescription("<:loudspeaker:757929476993581117> " + desc);
|
|
||||||
message.channel.send(vcJoin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playThis(message) {
|
|
||||||
const indexOfItemID =
|
|
||||||
message.content.indexOf(CONFIG["discord-prefix"] + "play") +
|
|
||||||
(CONFIG["discord-prefix"] + "play").length +
|
|
||||||
1;
|
|
||||||
const argument = message.content.slice(indexOfItemID);
|
|
||||||
let items;
|
|
||||||
// check if play command was used with itemID
|
|
||||||
const regexresults = checkJellyfinItemIDRegex(argument);
|
|
||||||
if (regexresults) {
|
|
||||||
items = regexresults;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
items = await searchForItemID(argument);
|
|
||||||
} catch (e) {
|
|
||||||
const noSong = getDiscordEmbedError(e);
|
|
||||||
message.channel.send(noSong);
|
|
||||||
playbackmanager.stop(
|
|
||||||
isSummendByPlay
|
|
||||||
? discordClient.user.client.voice.connections.first()
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackmanager.startPlaying(
|
|
||||||
discordClient.user.client.voice.connections.first(),
|
|
||||||
items,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
isSummendByPlay,
|
|
||||||
);
|
|
||||||
playbackmanager.spawnPlayMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addThis(message) {
|
|
||||||
const indexOfItemID =
|
|
||||||
message.content.indexOf(CONFIG["discord-prefix"] + "add") +
|
|
||||||
(CONFIG["discord-prefix"] + "add").length +
|
|
||||||
1;
|
|
||||||
const argument = message.content.slice(indexOfItemID);
|
|
||||||
let items;
|
|
||||||
// check if play command was used with itemID
|
|
||||||
const regexresults = checkJellyfinItemIDRegex(argument);
|
|
||||||
if (regexresults) {
|
|
||||||
items = regexresults;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
items = await searchForItemID(argument);
|
|
||||||
} catch (e) {
|
|
||||||
const noSong = getDiscordEmbedError(e);
|
|
||||||
message.channel.send(noSong);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackmanager.addTracks(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChannelMessage(message) {
|
|
||||||
getRandomDiscordColor();
|
|
||||||
|
|
||||||
if (message.content.startsWith(CONFIG["discord-prefix"] + "summon")) {
|
|
||||||
isSummendByPlay = false;
|
|
||||||
|
|
||||||
websocketHanler.openSocket();
|
|
||||||
|
|
||||||
summonMessage(message);
|
|
||||||
} else if (
|
|
||||||
message.content.startsWith(CONFIG["discord-prefix"] + "disconnect")
|
|
||||||
) {
|
|
||||||
playbackmanager.stop();
|
|
||||||
jellyfinClientManager.getJellyfinClient().closeWebSocket();
|
|
||||||
discordClient.user.client.voice.connections.forEach((element) => {
|
|
||||||
element.disconnect();
|
|
||||||
});
|
|
||||||
var desc = "**Left Voice Channel** `";
|
|
||||||
desc = desc.concat(message.member.voice.channel.name).concat("`");
|
|
||||||
const vcJoin = new Discord.MessageEmbed()
|
|
||||||
.setColor(getRandomDiscordColor())
|
|
||||||
.setTitle("Left Channel")
|
|
||||||
.setTimestamp()
|
|
||||||
.setDescription("<:wave:757938481585586226> " + desc);
|
|
||||||
message.channel.send(vcJoin);
|
|
||||||
} else if (
|
|
||||||
message.content.startsWith(CONFIG["discord-prefix"] + "pause") ||
|
|
||||||
message.content.startsWith(CONFIG["discord-prefix"] + "resume")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
playbackmanager.playPause();
|
|
||||||
const noPlay = new Discord.MessageEmbed()
|
|
||||||
.setColor(0xff0000)
|
|
||||||
.setTitle("<:play_pause:757940598106882049> " + "Paused/Resumed.")
|
|
||||||
.setTimestamp();
|
|
||||||
message.channel.send(noPlay);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getDiscordEmbedError(error);
|
|
||||||
message.channel.send(errorMessage);
|
|
||||||
}
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "play")) {
|
|
||||||
if (discordClient.user.client.voice.connections.size < 1) {
|
|
||||||
summonMessage(message);
|
|
||||||
isSummendByPlay = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
playThis(message);
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "stop")) {
|
|
||||||
if (isSummendByPlay) {
|
|
||||||
if (discordClient.user.client.voice.connections.size > 0) {
|
|
||||||
playbackmanager.stop(discordClient.user.client.voice.connections.first());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playbackmanager.stop();
|
|
||||||
}
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "seek")) {
|
|
||||||
const indexOfArgument =
|
|
||||||
message.content.indexOf(CONFIG["discord-prefix"] + "seek") +
|
|
||||||
(CONFIG["discord-prefix"] + "seek").length +
|
|
||||||
1;
|
|
||||||
const argument = message.content.slice(indexOfArgument);
|
|
||||||
try {
|
|
||||||
playbackmanager.seek(hmsToSeconds(argument) * 10000000);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getDiscordEmbedError(error);
|
|
||||||
message.channel.send(errorMessage);
|
|
||||||
}
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "skip")) {
|
|
||||||
try {
|
|
||||||
playbackmanager.nextTrack();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getDiscordEmbedError(error);
|
|
||||||
message.channel.send(errorMessage);
|
|
||||||
}
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "add")) {
|
|
||||||
addThis(message);
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "spawn")) {
|
|
||||||
try {
|
|
||||||
playbackmanager.spawnPlayMessage(message);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getDiscordEmbedError(error);
|
|
||||||
message.channel.send(errorMessage);
|
|
||||||
}
|
|
||||||
} else if (message.content.startsWith(CONFIG["discord-prefix"] + "help")) {
|
|
||||||
/* eslint-disable quotes */
|
|
||||||
const reply = new Discord.MessageEmbed()
|
|
||||||
.setColor(getRandomDiscordColor())
|
|
||||||
.setTitle(
|
|
||||||
"<:musical_note:757938541123862638> " +
|
|
||||||
"Jellyfin Discord Music Bot" +
|
|
||||||
" <:musical_note:757938541123862638> ",
|
|
||||||
)
|
|
||||||
.addFields(
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}summon`,
|
|
||||||
value: "Join the channel the author of the message",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}disconnect`,
|
|
||||||
value: "Disconnect from all current Voice Channels",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}play`,
|
|
||||||
value: "Play the following item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}add`,
|
|
||||||
value: "Add the following item to the current playlist",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}pause/resume`,
|
|
||||||
value: "Pause/Resume audio",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}seek`,
|
|
||||||
value: "Where to Seek to in seconds or MM:SS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}skip`,
|
|
||||||
value: "Skip this Song",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}spawn`,
|
|
||||||
value: "Spawns an Interactive Play Controller",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${CONFIG["discord-prefix"]}help`,
|
|
||||||
value: "Display this help message",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `GitHub`,
|
|
||||||
value:
|
|
||||||
"Find the code for this bot at: https://github.com/KGT1/jellyfin-discord-music-bot",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
message.channel.send(reply);
|
|
||||||
/* eslint-enable quotes */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
handleChannelMessage,
|
|
||||||
};
|
|
@ -1,535 +0,0 @@
|
|||||||
const interactivemsghandler = require("./interactivemsghandler");
|
|
||||||
const CONFIG = require("../config.json");
|
|
||||||
const discordclientmanager = require("./discordclientmanager");
|
|
||||||
const log = require("loglevel");
|
|
||||||
|
|
||||||
const {
|
|
||||||
getAudioDispatcher,
|
|
||||||
setAudioDispatcher
|
|
||||||
} = require("./dispachermanager");
|
|
||||||
const { ticksToSeconds } = require("./util");
|
|
||||||
|
|
||||||
// this whole thing should be a class but its probably too late now.
|
|
||||||
|
|
||||||
var currentPlayingPlaylist;
|
|
||||||
var currentPlayingPlaylistIndex;
|
|
||||||
var isPaused;
|
|
||||||
var isRepeat;
|
|
||||||
var _disconnectOnFinish;
|
|
||||||
var _seek;
|
|
||||||
|
|
||||||
const jellyfinClientManager = require("./jellyfinclientmanager");
|
|
||||||
const { VoiceConnection } = require("discord.js");
|
|
||||||
|
|
||||||
function streamURLbuilder(itemID, bitrate) {
|
|
||||||
// so the server transcodes. Seems appropriate as it has the source file.(doesnt yet work i dont know why)
|
|
||||||
const supportedCodecs = "opus";
|
|
||||||
const supportedContainers = "ogg,opus";
|
|
||||||
return `${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.serverAddress()}/Audio/${itemID}/universal?UserId=${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getCurrentUserId()}&DeviceId=${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.deviceId()}&MaxStreamingBitrate=${bitrate}&Container=${supportedContainers}&AudioCodec=${supportedCodecs}&api_key=${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.accessToken()}&TranscodingContainer=ts&TranscodingProtocol=hls`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startPlaying(
|
|
||||||
voiceconnection = discordclientmanager
|
|
||||||
.getDiscordClient()
|
|
||||||
.user.client.voice.connections.first(),
|
|
||||||
itemIDPlaylist = currentPlayingPlaylist,
|
|
||||||
playlistIndex = currentPlayingPlaylistIndex,
|
|
||||||
seekTo,
|
|
||||||
disconnectOnFinish = _disconnectOnFinish
|
|
||||||
) {
|
|
||||||
log.debug(
|
|
||||||
"Start playing",
|
|
||||||
itemIDPlaylist[playlistIndex],
|
|
||||||
"with index",
|
|
||||||
playlistIndex,
|
|
||||||
"of list with length of",
|
|
||||||
itemIDPlaylist.length,
|
|
||||||
"in",
|
|
||||||
voiceconnection && voiceconnection.channel
|
|
||||||
? '"' +
|
|
||||||
voiceconnection.channel.name +
|
|
||||||
'" (' +
|
|
||||||
voiceconnection.channel.id +
|
|
||||||
")"
|
|
||||||
: "an unknown voice channel"
|
|
||||||
);
|
|
||||||
|
|
||||||
isPaused = false;
|
|
||||||
currentPlayingPlaylist = itemIDPlaylist;
|
|
||||||
currentPlayingPlaylistIndex = playlistIndex;
|
|
||||||
_disconnectOnFinish = disconnectOnFinish;
|
|
||||||
_seek = seekTo * 1000;
|
|
||||||
updatePlayMessage();
|
|
||||||
|
|
||||||
async function playasync() {
|
|
||||||
const url = streamURLbuilder(
|
|
||||||
itemIDPlaylist[playlistIndex],
|
|
||||||
voiceconnection.channel.bitrate
|
|
||||||
);
|
|
||||||
setAudioDispatcher(
|
|
||||||
voiceconnection.play(url, {
|
|
||||||
seek: seekTo
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (seekTo) {
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.reportPlaybackProgress(getProgressPayload());
|
|
||||||
} else {
|
|
||||||
jellyfinClientManager.getJellyfinClient().reportPlaybackStart({
|
|
||||||
userID: `${jellyfinClientManager.getJellyfinClient().getCurrentUserId()}`,
|
|
||||||
itemID: `${itemIDPlaylist[playlistIndex]}`,
|
|
||||||
canSeek: true,
|
|
||||||
playSessionId: getPlaySessionId(),
|
|
||||||
playMethod: getPlayMethod()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAudioDispatcher().on("finish", () => {
|
|
||||||
// report playback stop and start the same index again
|
|
||||||
if (isRepeat) {
|
|
||||||
reportPlaybackStoppedAndStartPlaying(
|
|
||||||
voiceconnection,
|
|
||||||
currentPlayingPlaylistIndex
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPlayingPlaylist.length < playlistIndex) {
|
|
||||||
if (disconnectOnFinish) {
|
|
||||||
stop(voiceconnection, currentPlayingPlaylist[playlistIndex - 1]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop(undefined, currentPlayingPlaylist[playlistIndex - 1]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// play the next song in the playlist
|
|
||||||
reportPlaybackStoppedAndStartPlaying(
|
|
||||||
voiceconnection,
|
|
||||||
currentPlayingPlaylistIndex + 1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
playasync().catch((rsn) => {
|
|
||||||
console.error(rsn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {VoiceConnection} voiceconnection - The voiceConnection where the bot should play
|
|
||||||
* @param {number} playlistIndex - The target playlist index
|
|
||||||
* @param {any} disconnectOnFinish
|
|
||||||
*/
|
|
||||||
const reportPlaybackStoppedAndStartPlaying = (
|
|
||||||
voiceconnection,
|
|
||||||
playlistIndex,
|
|
||||||
disconnectOnFinish
|
|
||||||
) => {
|
|
||||||
const stopPayload = getStopPayload();
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
"Repeat and sending following payload as reportPlaybackStopped to the server: ",
|
|
||||||
stopPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
jellyfinClientManager.getJellyfinClient().reportPlaybackStopped(stopPayload);
|
|
||||||
startPlaying(voiceconnection, undefined, playlistIndex, 0, disconnectOnFinish);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function spawnPlayMessage(message) {
|
|
||||||
if (!message.channel) {
|
|
||||||
log.error("Unable to send play message in channel");
|
|
||||||
log.debug(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
"Sending play message to channel",
|
|
||||||
message.channel.name,
|
|
||||||
"(" + message.channel.id + ")"
|
|
||||||
);
|
|
||||||
|
|
||||||
const itemIdDetails = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getItem(
|
|
||||||
jellyfinClientManager.getJellyfinClient().getCurrentUserId(),
|
|
||||||
getItemId()
|
|
||||||
);
|
|
||||||
const imageURL = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getImageUrl(itemIdDetails.AlbumId || getItemId(), { type: "Primary" });
|
|
||||||
try {
|
|
||||||
interactivemsghandler.init(
|
|
||||||
message,
|
|
||||||
itemIdDetails.Name,
|
|
||||||
itemIdDetails.Artists[0] || "VA",
|
|
||||||
imageURL,
|
|
||||||
`${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.serverAddress()}/web/index.html#!/details?id=${itemIdDetails.AlbumId}`,
|
|
||||||
itemIdDetails.RunTimeTicks,
|
|
||||||
ticksToSeconds(getPostitionTicks()) > 10 ? previousTrack : seek,
|
|
||||||
playPause,
|
|
||||||
() => {
|
|
||||||
stop(
|
|
||||||
_disconnectOnFinish
|
|
||||||
? discordclientmanager
|
|
||||||
.getDiscordClient()
|
|
||||||
.user.client.voice.connections.first()
|
|
||||||
: undefined
|
|
||||||
);
|
|
||||||
},
|
|
||||||
nextTrack,
|
|
||||||
() => {
|
|
||||||
setIsRepeat(!isRepeat);
|
|
||||||
},
|
|
||||||
currentPlayingPlaylist.length
|
|
||||||
);
|
|
||||||
if (typeof CONFIG["interactive-seek-bar-update-intervall"] === "number") {
|
|
||||||
interactivemsghandler.startUpate(getPostitionTicks);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updatePlayMessage() {
|
|
||||||
const itemId = getItemId();
|
|
||||||
|
|
||||||
if (!itemId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jellyfinItemDetails = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getItem(
|
|
||||||
jellyfinClientManager.getJellyfinClient().getCurrentUserId(),
|
|
||||||
getItemId()
|
|
||||||
);
|
|
||||||
|
|
||||||
const primaryAlbumCover = await jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.getImageUrl(jellyfinItemDetails.AlbumId || itemId, { type: "Primary" });
|
|
||||||
|
|
||||||
log.debug("Extracted primary Album cover url:", primaryAlbumCover);
|
|
||||||
|
|
||||||
try {
|
|
||||||
interactivemsghandler.updateCurrentSongMessage(
|
|
||||||
jellyfinItemDetails.Name,
|
|
||||||
jellyfinItemDetails.Artists[0] || "VA",
|
|
||||||
primaryAlbumCover,
|
|
||||||
`${jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.serverAddress()}/web/index.html#!/details?id=${
|
|
||||||
jellyfinItemDetails.AlbumId
|
|
||||||
}`,
|
|
||||||
jellyfinItemDetails.RunTimeTicks,
|
|
||||||
currentPlayingPlaylistIndex + 1,
|
|
||||||
currentPlayingPlaylist.length
|
|
||||||
);
|
|
||||||
} catch (exception) {
|
|
||||||
log.error("Exception during updating the current song message:", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Number} toSeek - where to seek in ticks
|
|
||||||
*/
|
|
||||||
function seek(toSeek = 0) {
|
|
||||||
log.debug("Seeking to: ", toSeek);
|
|
||||||
|
|
||||||
if (!getAudioDispatcher()) {
|
|
||||||
log.warn("Failed to seek because no song is playing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// start playing the same track but with a specified time
|
|
||||||
startPlaying(
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
ticksToSeconds(toSeek),
|
|
||||||
_disconnectOnFinish
|
|
||||||
);
|
|
||||||
|
|
||||||
// report change about playback progress to Jellyfin
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.reportPlaybackProgress(getProgressPayload());
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Array} trackItemIdsArray - array of itemIDs to be added
|
|
||||||
*/
|
|
||||||
function addTracks(trackItemIdsArray) {
|
|
||||||
currentPlayingPlaylist = currentPlayingPlaylist.concat(trackItemIdsArray);
|
|
||||||
log.debug(
|
|
||||||
"Added tracks of",
|
|
||||||
trackItemIdsArray.length,
|
|
||||||
"to the current playlist"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextTrack() {
|
|
||||||
log.debug("Going to the next track...");
|
|
||||||
|
|
||||||
if (!currentPlayingPlaylist) {
|
|
||||||
log.warn(
|
|
||||||
"Can't go to the next track, because there is currently nothing playing"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPlayingPlaylistIndex + 1 >= currentPlayingPlaylist.length) {
|
|
||||||
log.warn(
|
|
||||||
"Can't go to next track, because the current playing song is the last song."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reportPlaybackStoppedAndStartPlaying(
|
|
||||||
undefined,
|
|
||||||
currentPlayingPlaylistIndex + 1,
|
|
||||||
_disconnectOnFinish
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousTrack() {
|
|
||||||
log.debug("Going to the previous track...");
|
|
||||||
|
|
||||||
if (ticksToSeconds(getPostitionTicks()) > 10) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't go to the previous track when nothing is playing
|
|
||||||
if (!currentPlayingPlaylist) {
|
|
||||||
log.warn(
|
|
||||||
"Can't go to the previous track, because there's currently nothing playing"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPlayingPlaylistIndex - 1 < 0) {
|
|
||||||
log.warn(
|
|
||||||
"Can't go to the previous track, because this is the first track in the playlist"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reportPlaybackStoppedAndStartPlaying(
|
|
||||||
undefined,
|
|
||||||
currentPlayingPlaylistIndex - 1,
|
|
||||||
_disconnectOnFinish
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object=} disconnectVoiceConnection - Optional The voice Connection do disconnect from
|
|
||||||
*/
|
|
||||||
function stop(disconnectVoiceConnection, itemId = getItemId()) {
|
|
||||||
isPaused = true;
|
|
||||||
if (interactivemsghandler.hasMessage()) {
|
|
||||||
interactivemsghandler.destroy();
|
|
||||||
}
|
|
||||||
if (disconnectVoiceConnection) {
|
|
||||||
disconnectVoiceConnection.disconnect();
|
|
||||||
}
|
|
||||||
log.debug(
|
|
||||||
"stop playback and send following payload as reportPlaybackStopped to the server: ",
|
|
||||||
getStopPayload()
|
|
||||||
);
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.reportPlaybackStopped(getStopPayload());
|
|
||||||
if (getAudioDispatcher()) {
|
|
||||||
try {
|
|
||||||
getAudioDispatcher().destroy();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setAudioDispatcher(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pause() {
|
|
||||||
log.debug("Pausing the current track...");
|
|
||||||
isPaused = true;
|
|
||||||
|
|
||||||
// report to Jellyfin that the client has paused the track
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.reportPlaybackProgress(getProgressPayload());
|
|
||||||
|
|
||||||
// pause the track in the audio dispatcher
|
|
||||||
getAudioDispatcher().pause(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resume() {
|
|
||||||
log.debug("Resuming playback of the current track...");
|
|
||||||
|
|
||||||
isPaused = false;
|
|
||||||
|
|
||||||
// report to Jellyfin that the client has resumed playback
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinClient()
|
|
||||||
.reportPlaybackProgress(getProgressPayload());
|
|
||||||
|
|
||||||
// resume playback in the audio dispatcher
|
|
||||||
getAudioDispatcher().resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses the playback of the current track is playing or
|
|
||||||
* resumes the placback if the current track is paused
|
|
||||||
*/
|
|
||||||
function playPause() {
|
|
||||||
const audioDispatcher = getAudioDispatcher();
|
|
||||||
|
|
||||||
if (!audioDispatcher) {
|
|
||||||
log.warn(
|
|
||||||
"Can't toggle the playback of the current song because there is nothing playing right now"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDispatcher.paused) {
|
|
||||||
log.debug("Resuming playback because the current track is paused...");
|
|
||||||
resume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("Pausing the playback because the current track is playing...");
|
|
||||||
pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPostitionTicks() {
|
|
||||||
// this is very sketchy but i dont know how else to do it
|
|
||||||
return (
|
|
||||||
(_seek + getAudioDispatcher().streamTime - getAudioDispatcher().pausedTime) *
|
|
||||||
10000
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlayMethod() {
|
|
||||||
// TODO figure out how to figure this out
|
|
||||||
return "DirectPlay";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRepeatMode() {
|
|
||||||
if (isRepeat) {
|
|
||||||
return "RepeatOne";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "RepeatNone";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlaylistItemId() {
|
|
||||||
return getItemId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlaySessionId() {
|
|
||||||
// TODO: generate a unique identifier for identification at Jellyfin. This may cause conflicts when running multiple bots on the same Jellyfin server.
|
|
||||||
return "ae2436edc6b91b11d72aeaa67f84e0ea";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNowPLayingQueue() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
Id: getItemId(),
|
|
||||||
// as I curently dont support Playlists
|
|
||||||
PlaylistItemId: getPlaylistItemId()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCanSeek() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIsMuted() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVolumeLevel() {
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemId() {
|
|
||||||
if (typeof currentPlayingPlaylist !== "undefined") {
|
|
||||||
return currentPlayingPlaylist[currentPlayingPlaylistIndex];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIsPaused() {
|
|
||||||
// AudioDispacker Paused is to slow
|
|
||||||
|
|
||||||
if (isPaused === undefined) {
|
|
||||||
isPaused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isPaused;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setIsRepeat(arg) {
|
|
||||||
if (arg === undefined) {
|
|
||||||
if (!(isRepeat === undefined)) {
|
|
||||||
isRepeat = !isRepeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isRepeat = arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProgressPayload() {
|
|
||||||
const payload = {
|
|
||||||
CanSeek: getCanSeek(),
|
|
||||||
IsMuted: getIsMuted(),
|
|
||||||
IsPaused: getIsPaused(),
|
|
||||||
ItemId: getItemId(),
|
|
||||||
MediaSourceId: getItemId(),
|
|
||||||
NowPlayingQueue: getNowPLayingQueue(),
|
|
||||||
PlayMethod: getPlayMethod(),
|
|
||||||
PlaySessionId: getPlaySessionId(),
|
|
||||||
PlaylistItemId: getPlaylistItemId(),
|
|
||||||
PositionTicks: getPostitionTicks(),
|
|
||||||
RepeatMode: getRepeatMode(),
|
|
||||||
VolumeLevel: getVolumeLevel(),
|
|
||||||
EventName: "pauseplayupdate"
|
|
||||||
};
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStopPayload() {
|
|
||||||
return {
|
|
||||||
userId: jellyfinClientManager.getJellyfinClient().getCurrentUserId(),
|
|
||||||
itemId: getItemId(),
|
|
||||||
sessionID: getPlaySessionId(),
|
|
||||||
playSessionId: getPlaySessionId(),
|
|
||||||
positionTicks: getPostitionTicks()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
startPlaying,
|
|
||||||
stop,
|
|
||||||
playPause,
|
|
||||||
resume,
|
|
||||||
pause,
|
|
||||||
seek,
|
|
||||||
setIsRepeat,
|
|
||||||
nextTrack,
|
|
||||||
previousTrack,
|
|
||||||
addTracks,
|
|
||||||
getPostitionTicks,
|
|
||||||
spawnPlayMessage
|
|
||||||
};
|
|
3
src/types/env.ts
Normal file
3
src/types/env.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface EnvironmentVariablesType {
|
||||||
|
DISCORD_CLIENT_TOKEN: string;
|
||||||
|
}
|
55
src/util.js
55
src/util.js
@ -1,55 +0,0 @@
|
|||||||
function checkJellyfinItemIDRegex(strgintomatch) {
|
|
||||||
const regexresult = strgintomatch.match(/([0-9]|[a-f]){32}/);
|
|
||||||
if (regexresult) {
|
|
||||||
return [regexresult[0]];
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ticksToSeconds(ticks) {
|
|
||||||
return ticks / 10000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hmsToSeconds(str) {
|
|
||||||
var p = str.split(":");
|
|
||||||
var s = 0;
|
|
||||||
var m = 1;
|
|
||||||
|
|
||||||
while (p.length > 0) {
|
|
||||||
s += m * parseInt(p.pop(), 10);
|
|
||||||
m *= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function secondsToHms(totalSeconds) {
|
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
|
||||||
totalSeconds %= 3600;
|
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
|
||||||
let seconds = Math.floor(totalSeconds % 60);
|
|
||||||
seconds = seconds < 10 && seconds > 0 ? `0${seconds}` : `${seconds}`;
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}:${minutes}:${seconds}`;
|
|
||||||
} else {
|
|
||||||
return `${minutes}:${seconds}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDiscordEmbedError(e) {
|
|
||||||
const Discord = require("discord.js");
|
|
||||||
return new Discord.MessageEmbed()
|
|
||||||
.setColor(0xff0000)
|
|
||||||
.setTitle("Error!")
|
|
||||||
.setTimestamp()
|
|
||||||
.setDescription("<:x:757935515445231651> " + e);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkJellyfinItemIDRegex,
|
|
||||||
ticksToSeconds,
|
|
||||||
hmsToSeconds,
|
|
||||||
getDiscordEmbedError,
|
|
||||||
secondsToHms,
|
|
||||||
};
|
|
6
src/utils/constants.ts
Normal file
6
src/utils/constants.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const Constants = {
|
||||||
|
Metadata: {
|
||||||
|
Version: "0.0.1",
|
||||||
|
ApplicationName: "Discord Jellyfin Music Bot"
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
const jellyfinClientManager = require("./jellyfinclientmanager");
|
|
||||||
const playbackmanager = require("./playbackmanager");
|
|
||||||
const { ticksToSeconds } = require("./util");
|
|
||||||
|
|
||||||
function openSocket() {
|
|
||||||
jellyfinClientManager.getJellyfinClient().openWebSocket();
|
|
||||||
jellyfinClientManager.getJellyfinClient().reportCapabilities({
|
|
||||||
PlayableMediaTypes: "Audio",
|
|
||||||
SupportsMediaControl: true,
|
|
||||||
SupportedCommands: "SetRepeatMode,Play,Playstate",
|
|
||||||
});
|
|
||||||
jellyfinClientManager
|
|
||||||
.getJellyfinEvents()
|
|
||||||
.on(jellyfinClientManager.getJellyfinClient(), "message", (type, data) => {
|
|
||||||
if (data.MessageType === "Play") {
|
|
||||||
if (data.Data.PlayCommand === "PlayNow") {
|
|
||||||
playbackmanager.startPlaying(
|
|
||||||
undefined,
|
|
||||||
data.Data.ItemIds,
|
|
||||||
data.Data.StartIndex || 0,
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (data.MessageType === "Playstate") {
|
|
||||||
if (data.Data.Command === "PlayPause") {
|
|
||||||
playbackmanager.playPause();
|
|
||||||
} else if (data.Data.Command === "Stop") {
|
|
||||||
playbackmanager.stop();
|
|
||||||
} else if (data.Data.Command === "Seek") {
|
|
||||||
// because the server sends seek an privious track at same time so i have to do timing
|
|
||||||
setTimeout(async () => {
|
|
||||||
playbackmanager.seek(data.Data.SeekPositionTicks);
|
|
||||||
}, 20);
|
|
||||||
} else if (data.Data.Command === "NextTrack") {
|
|
||||||
try {
|
|
||||||
playbackmanager.nextTrack();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else if (data.Data.Command === "PreviousTrack") {
|
|
||||||
try {
|
|
||||||
if (ticksToSeconds(playbackmanager.getPostitionTicks()) < 10) {
|
|
||||||
playbackmanager.previousTrack();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
openSocket,
|
|
||||||
};
|
|
24
test/app.e2e-spec.ts
Normal file
24
test/app.e2e-spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user