dodano obsłuugę linków ze spotify oraz soundcloud
This commit is contained in:
@@ -1 +1,6 @@
|
|||||||
DISCORD_TOKEN
|
DISCORD_TOKEN=
|
||||||
|
DISCORD_APP_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
GUILD_ID=
|
||||||
|
SPOTIFY_CLIENT_ID=
|
||||||
|
SPOTIFY_CLIENT_SECRET=
|
||||||
|
|||||||
1214
package-lock.json
generated
1214
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,12 @@
|
|||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"typescript": "^5.9.3"
|
"spotify-web-api-node": "^5.0.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"yt-search": "^2.13.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/spotify-web-api-node": "^5.0.11",
|
||||||
|
"@types/yt-search": "^2.10.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'dis
|
|||||||
import { connectToChannelByInteraction } from '../util/helpers';
|
import { connectToChannelByInteraction } from '../util/helpers';
|
||||||
import { requestSong } from '../playback';
|
import { requestSong } from '../playback';
|
||||||
import { getPlayMsg, msg_downloading, msg_play, msg_searching } from '../messages';
|
import { getPlayMsg, msg_downloading, msg_play, msg_searching } from '../messages';
|
||||||
import { search } from '../util/downloader';
|
import { search, spotifyToYouTube } from '../util/downloader';
|
||||||
|
|
||||||
const name = "play"
|
const name = "play"
|
||||||
|
|
||||||
@@ -23,7 +23,11 @@ async function execute(interaction: ChatInputCommandInteraction<CacheType>) {
|
|||||||
const input = interaction.options.getString("url")!;
|
const input = interaction.options.getString("url")!;
|
||||||
let url: string;
|
let url: string;
|
||||||
|
|
||||||
if (input.startsWith("http")) {
|
if (input.startsWith("https://open.spotify.com/")) {
|
||||||
|
await interaction.editReply(msg_searching(input));
|
||||||
|
url = await spotifyToYouTube(input)
|
||||||
|
}
|
||||||
|
else if (input.startsWith("http")) {
|
||||||
url = input
|
url = input
|
||||||
} else {
|
} else {
|
||||||
await interaction.editReply(msg_searching(input));
|
await interaction.editReply(msg_searching(input));
|
||||||
|
|||||||
13
src/main.ts
13
src/main.ts
@@ -1,11 +1,12 @@
|
|||||||
// Require the necessary discord.js classes
|
// Require the necessary discord.js classes
|
||||||
import { Client, Events, GatewayIntentBits, MessageFlags, REST, Routes } from 'discord.js';
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config()
|
||||||
|
import { Client, Events, GatewayIntentBits, MessageFlags, REST, Routes } from 'discord.js';
|
||||||
import { commands } from "./commands";
|
import { commands } from "./commands";
|
||||||
import { AudioPlayerState, createAudioPlayer } from '@discordjs/voice';
|
import { AudioPlayerState, createAudioPlayer } from '@discordjs/voice';
|
||||||
import { connectToChannel, playSong } from './util/helpers';
|
import { connectToChannel, playSong } from './util/helpers';
|
||||||
import { updatePlayer } from './playback';
|
import { updatePlayer } from './playback';
|
||||||
dotenv.config()
|
import SpotifyWebApi from 'spotify-web-api-node';
|
||||||
|
|
||||||
export const DATA_DIR = "./data";
|
export const DATA_DIR = "./data";
|
||||||
|
|
||||||
@@ -24,6 +25,12 @@ const client = new Client({
|
|||||||
GatewayIntentBits.GuildVoiceStates]
|
GatewayIntentBits.GuildVoiceStates]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const spotify = new SpotifyWebApi({
|
||||||
|
clientId: process.env.SPOTIFY_CLIENT_ID,
|
||||||
|
clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// When the client is ready, run this code (only once).
|
// When the client is ready, run this code (only once).
|
||||||
// The distinction between `client: Client<boolean>` and `readyClient: Client<true>` is important for TypeScript developers.
|
// The distinction between `client: Client<boolean>` and `readyClient: Client<true>` is important for TypeScript developers.
|
||||||
// It makes some properties non-nullable.
|
// It makes some properties non-nullable.
|
||||||
@@ -35,7 +42,7 @@ client.once(Events.ClientReady, (readyClient) => {
|
|||||||
async function registerCommands() {
|
async function registerCommands() {
|
||||||
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
|
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
|
||||||
const registry = Object.values(commands).map((cmd) => cmd.register().toJSON())
|
const registry = Object.values(commands).map((cmd) => cmd.register().toJSON())
|
||||||
console.log(registry)
|
console.log("komendy: \n", registry.map(r => `${r.name} : ${r.description}`).join("\n"))
|
||||||
await rest.put(Routes.applicationGuildCommands(
|
await rest.put(Routes.applicationGuildCommands(
|
||||||
process.env.DISCORD_APP_ID!,
|
process.env.DISCORD_APP_ID!,
|
||||||
process.env.GUILD_ID!
|
process.env.GUILD_ID!
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function msg_play(): InteractionEditReplyOptions {
|
|||||||
.setColor(PRIMARY_COLOR)
|
.setColor(PRIMARY_COLOR)
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: "Na Placku", value: current },
|
{ name: "Na Placku", value: current },
|
||||||
{ name: "Kolejka", value: queueSongs.length ? queueSongs.join("\n") : "Pusta Kolejka" }
|
{ name: "Kolejka", value: queueSongs.length ? queueSongs.join("\n") : "Pusto" }
|
||||||
)
|
)
|
||||||
.setTimestamp()
|
.setTimestamp()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,41 +1,65 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { DATA_DIR } from "../main";
|
import { DATA_DIR, spotify } from "../main";
|
||||||
|
import ytSearch from 'yt-search';
|
||||||
|
|
||||||
|
|
||||||
export async function getAudioFile(url: string): Promise<string> {
|
export async function getAudioFile(url: string): Promise<string> {
|
||||||
const id = extractId(url)
|
const id = await extractId(url)
|
||||||
|
console.log(`ID: ${id}`);
|
||||||
|
|
||||||
let path: string;
|
let path: string;
|
||||||
try {
|
try {
|
||||||
path = await findFileById(id)
|
path = await findFileById(id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await downloadYTVideo(url)
|
await downloadAudio(url)
|
||||||
|
console.log(`downloaded ${id}`)
|
||||||
path = await findFileById(id)
|
path = await findFileById(id)
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractId(url: string): string {
|
export async function extractId(url: string): Promise<string> {
|
||||||
|
if (url.startsWith("https://soundcloud")) { // soundcloud
|
||||||
|
const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp";
|
||||||
|
return await new Promise<string>((resolve, reject) => {
|
||||||
|
const p = spawn(ytDlpBin, [
|
||||||
|
"--print", "%(id)s",
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let out = ""
|
||||||
|
p.stderr.on("data", d => process.stderr.write(d));
|
||||||
|
p.stdout.on("data", d => out += d.toString());
|
||||||
|
p.on("close", code => code === 0 ? resolve(out.trim()) : reject(new Error("yt-dlp extract id failed")));
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url);
|
const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url);
|
||||||
if (!idMatch) throw new Error("Cannot extract video ID");
|
if (!idMatch) throw new Error("Cannot extract video ID");
|
||||||
const id = idMatch[1];
|
const id = idMatch[1];
|
||||||
console.log(`ID: ${id}`);
|
|
||||||
return id
|
return id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadYTVideo(url: string): Promise<void> {
|
export async function downloadAudio(url: string): Promise<void> {
|
||||||
const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp";
|
const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp";
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const p = spawn(ytDlpBin, [
|
const p = spawn(ytDlpBin, [
|
||||||
"-x",
|
"-x",
|
||||||
"--audio-format", "opus",
|
// "--audio-format", "opus",
|
||||||
"--audio-quality", "0",
|
"--audio-quality", "0",
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
"--paths", DATA_DIR,
|
"--paths", `temp:/tmp`,
|
||||||
|
"--paths", `home:${DATA_DIR}`,
|
||||||
url,
|
url,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
p.stderr.on("data", d => process.stderr.write(d));
|
p.stderr.on("data", d => process.stderr.write(d));
|
||||||
p.on("close", code => code === 0 ? resolve() : reject(new Error("yt-dlp failed")));
|
p.stdout.on("data", d => process.stdout.write(d))
|
||||||
|
p.on("close", code => {
|
||||||
|
console.log("Pobieranie exit code: ", code)
|
||||||
|
code === 0 ? resolve() : reject(new Error("yt-dlp failed"))
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +68,7 @@ export async function findFileById(id: string): Promise<string> {
|
|||||||
const find = spawn("find", [
|
const find = spawn("find", [
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
"-type", "f",
|
"-type", "f",
|
||||||
"-iname", `*${id}*.opus`,
|
"-iname", `*${id}*`,
|
||||||
"-exec", "readlink", "-f", "{}", ";",
|
"-exec", "readlink", "-f", "{}", ";",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -93,3 +117,21 @@ export function formatFilePath(path: string): string {
|
|||||||
.replace(/\[.*\].opus/, "")
|
.replace(/\[.*\].opus/, "")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function spotifyToYouTube(spotifyUrl: string) {
|
||||||
|
const data = await spotify.clientCredentialsGrant()
|
||||||
|
// console.log(data, {
|
||||||
|
// clientId: process.env.SPOTIFY_CLIENT_ID,
|
||||||
|
// clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
|
||||||
|
// }, spotify)
|
||||||
|
spotify.setAccessToken(data.body.access_token)
|
||||||
|
|
||||||
|
|
||||||
|
const trackId = spotifyUrl.split('/track/')[1].split('?')[0];
|
||||||
|
const track = await spotify.getTrack(trackId);
|
||||||
|
|
||||||
|
const query = `${track.body.name} ${track.body.artists[0].name}`;
|
||||||
|
const result = await ytSearch(query);
|
||||||
|
|
||||||
|
return result.videos[0].url;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user