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",
|
||||
"discord.js": "^14.25.1",
|
||||
"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 { requestSong } from '../playback';
|
||||
import { getPlayMsg, msg_downloading, msg_play, msg_searching } from '../messages';
|
||||
import { search } from '../util/downloader';
|
||||
import { search, spotifyToYouTube } from '../util/downloader';
|
||||
|
||||
const name = "play"
|
||||
|
||||
@@ -23,7 +23,11 @@ async function execute(interaction: ChatInputCommandInteraction<CacheType>) {
|
||||
const input = interaction.options.getString("url")!;
|
||||
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
|
||||
} else {
|
||||
await interaction.editReply(msg_searching(input));
|
||||
|
||||
13
src/main.ts
13
src/main.ts
@@ -1,11 +1,12 @@
|
||||
// Require the necessary discord.js classes
|
||||
import { Client, Events, GatewayIntentBits, MessageFlags, REST, Routes } from 'discord.js';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
import { Client, Events, GatewayIntentBits, MessageFlags, REST, Routes } from 'discord.js';
|
||||
import { commands } from "./commands";
|
||||
import { AudioPlayerState, createAudioPlayer } from '@discordjs/voice';
|
||||
import { connectToChannel, playSong } from './util/helpers';
|
||||
import { updatePlayer } from './playback';
|
||||
dotenv.config()
|
||||
import SpotifyWebApi from 'spotify-web-api-node';
|
||||
|
||||
export const DATA_DIR = "./data";
|
||||
|
||||
@@ -24,6 +25,12 @@ const client = new Client({
|
||||
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).
|
||||
// The distinction between `client: Client<boolean>` and `readyClient: Client<true>` is important for TypeScript developers.
|
||||
// It makes some properties non-nullable.
|
||||
@@ -35,7 +42,7 @@ client.once(Events.ClientReady, (readyClient) => {
|
||||
async function registerCommands() {
|
||||
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
|
||||
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(
|
||||
process.env.DISCORD_APP_ID!,
|
||||
process.env.GUILD_ID!
|
||||
|
||||
@@ -53,7 +53,7 @@ export function msg_play(): InteractionEditReplyOptions {
|
||||
.setColor(PRIMARY_COLOR)
|
||||
.addFields(
|
||||
{ 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()
|
||||
]
|
||||
|
||||
@@ -1,41 +1,65 @@
|
||||
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> {
|
||||
const id = extractId(url)
|
||||
const id = await extractId(url)
|
||||
console.log(`ID: ${id}`);
|
||||
|
||||
let path: string;
|
||||
try {
|
||||
path = await findFileById(id)
|
||||
} catch (e) {
|
||||
await downloadYTVideo(url)
|
||||
await downloadAudio(url)
|
||||
console.log(`downloaded ${id}`)
|
||||
path = await findFileById(id)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function extractId(url: string): string {
|
||||
const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url);
|
||||
if (!idMatch) throw new Error("Cannot extract video ID");
|
||||
const id = idMatch[1];
|
||||
console.log(`ID: ${id}`);
|
||||
return id
|
||||
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);
|
||||
if (!idMatch) throw new Error("Cannot extract video ID");
|
||||
const id = idMatch[1];
|
||||
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";
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const p = spawn(ytDlpBin, [
|
||||
"-x",
|
||||
"--audio-format", "opus",
|
||||
// "--audio-format", "opus",
|
||||
"--audio-quality", "0",
|
||||
"--no-playlist",
|
||||
"--paths", DATA_DIR,
|
||||
"--paths", `temp:/tmp`,
|
||||
"--paths", `home:${DATA_DIR}`,
|
||||
url,
|
||||
]);
|
||||
|
||||
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", [
|
||||
DATA_DIR,
|
||||
"-type", "f",
|
||||
"-iname", `*${id}*.opus`,
|
||||
"-iname", `*${id}*`,
|
||||
"-exec", "readlink", "-f", "{}", ";",
|
||||
]);
|
||||
|
||||
@@ -93,3 +117,21 @@ export function formatFilePath(path: string): string {
|
||||
.replace(/\[.*\].opus/, "")
|
||||
.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