diff --git a/src/modules/misc/yt.js b/src/modules/misc/yt.js index 3179bd5..2c1ef6a 100644 --- a/src/modules/misc/yt.js +++ b/src/modules/misc/yt.js @@ -14,23 +14,25 @@ yt.usage = "[search term]"; yt.callback = async function (msg, line) { if (!line) return "Arguments are required."; - const req = await fetch(`${hf.config.piped_api}/search?q=${encodeURIComponent(line)}&filter=videos`).then((x) => - x.json() - ); + const req = await fetch( + `https://www.googleapis.com/youtube/v3/search?key=${ + hf.apikeys.google + }&maxResults=5&part=snippet&type=video&q=${encodeURIComponent(line)}` + ).then((res) => res.json()); const topVid = req.items[0]; - let out = `**${safeString(parseHtmlEntities(topVid.title))}** | \`${safeString( - parseHtmlEntities(topVid.uploaderName) - )}\`\nhttps://youtube.com${topVid.url}\n\n**__See Also:__**\n`; + let out = `**${safeString(parseHtmlEntities(topVid.snippet.title))}** | \`${safeString( + parseHtmlEntities(topVid.snippet.channelTitle) + )}\`\nhttps://youtu.be/${topVid.id.videoId}\n\n**__See Also:__**\n`; for (let i = 1; i < 5; i++) { const vid = req.items[i]; if (!vid) continue; - out += `- **${safeString(parseHtmlEntities(vid.title))}** | By: \`${safeString( - parseHtmlEntities(vid.uploaderName) - )}\` | \n`; + out += `- **${safeString(parseHtmlEntities(vid.snippet.title))}** | By: \`${safeString( + parseHtmlEntities(vid.snippet.channelTitle) + )}\` | \n`; } return out; @@ -60,15 +62,17 @@ fyt.usage = "[search term]"; fyt.callback = async function (msg, line) { if (!line) return "Arguments are required."; - const req = await fetch(`${hf.config.piped_api}/search?q=${encodeURIComponent(line)}&filter=videos`).then((x) => - x.json() - ); + const req = await fetch( + `https://www.googleapis.com/youtube/v3/search?key=${ + hf.apikeys.google + }&maxResults=2&part=snippet&type=video&q=${encodeURIComponent(line)}` + ).then((res) => res.json()); const vid = req.items[0]; - return `**${safeString(parseHtmlEntities(vid.title))}** | \`${safeString( - parseHtmlEntities(vid.uploaderName) - )}\`\nhttps://youtube.com${vid.url}`; + return `**${safeString(parseHtmlEntities(vid.snippet.title))}** | \`${safeString( + parseHtmlEntities(vid.snippet.channelTitle) + )}\`\nhttps://youtu.be/${vid.id.videoId}`; }; hf.registerCommand(fyt); diff --git a/src/modules/music.js b/src/modules/music.js index a417fba..bc4bc81 100644 --- a/src/modules/music.js +++ b/src/modules/music.js @@ -1,6 +1,5 @@ const {Collection} = require("@projectdysnomia/dysnomia"); -const {Readable} = require("node:stream"); const ffprobe = require("node-ffprobe"); const Command = require("#lib/command.js"); @@ -55,20 +54,18 @@ async function processPlaylist(url, type, shuffle = false, limit = -1, offset = const playlistId = url.match(REGEX_YOUTUBE_PLAYLIST)?.[4] ?? url.match(REGEX_YOUTUBE_PLAYLIST_SHORT)?.[0]; if (!playlistId) return null; - const baseUrl = "/playlists/" + playlistId; + const baseUrl = `https://www.googleapis.com/youtube/v3/playlistItems?key=${hf.apikeys.google}&part=snippet&playlistId=${playlistId}&maxResults=50`; - const data = await fetch(hf.config.piped_api + baseUrl).then((res) => res.json()); + const data = await fetch(baseUrl).then((res) => res.json()); - playlist = data.relatedStreams; + playlist = data.items; - let pageToken = data.nextpage; - while (pageToken?.startsWith("{")) { - const pageData = await fetch( - hf.config.piped_api + "/nextpage" + baseUrl + "&nextpage=" + encodeURIComponent(pageToken) - ).then((res) => res.json()); - if (pageData.nextpage) pageToken = pageData.nextpage; + let pageToken = data.nextPageToken; + while (pageToken != null) { + const pageData = await fetch(baseUrl + "&pageToken=" + encodeURIComponent(pageToken)).then((res) => res.json()); + if (pageData.nextPageToken) pageToken = pageData.nextPageToken; - playlist = [...playlist, ...pageData.relatedStreams]; + playlist = [...playlist, ...pageData.items]; } } else if (type === "sc") { const clientId = await getSoundcloudClientID(); @@ -176,7 +173,6 @@ async function createVoiceConnection(guild_id, voice_id, text_id) { return state; } -const REGEX_HLS_AUDIO_TRACK = /#EXT-X-MEDIA:URI="(.+?)",TYPE=AUDIO,/; async function enqueue({guild_id, voice_id, text_id, url, type, addedBy, suppress = false, queueNext = false}) { if (!url) return; @@ -189,7 +185,8 @@ async function enqueue({guild_id, voice_id, text_id, url, type, addedBy, suppres media, stream = false; - if (type == "yt") { + // this is only whitelisted because residential ip being used + if (type == "yt" && hf.config.yt_whitelist.includes(guild_id)) { let info; let id = url; try { @@ -201,27 +198,53 @@ async function enqueue({guild_id, voice_id, text_id, url, type, addedBy, suppres id = uri.searchParams.get("v"); } } - info = await fetch(`${hf.config.piped_api}/streams/${id}`).then((res) => res.json()); + info = await fetch( + `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=${id}&fields=items%2Fsnippet&key=${hf.apikeys.google}` + ) + .then((res) => res.json()) + .then((data) => data?.items?.[0]); } catch (err) { await textChannel.createMessage({ content: `:warning: Failed to get metadata: \`\`\`\n${err}\n\`\`\``, }); } - title = info?.title; - length = info?.duration * 1000; - thumbnail = info?.thumbnailUrl; + title = info?.snippet?.title; - const hlsUrl = new URL(info.hls); - const hlsBase = await fetch(info.hls) - .then((res) => res.text()) - .then((data) => data.replaceAll("/api/manifest/", `https://${hlsUrl.hostname}/api/manifest/`)); + const thumbnails = info?.snippet?.thumbnails; + thumbnail = thumbnails?.maxres?.url ?? thumbnails?.standard?.url; - media = Readable.from( - await fetch(hlsBase.match(REGEX_HLS_AUDIO_TRACK)[1]) - .then((res) => res.text()) - .then((data) => data.replaceAll("/videoplayback/", `https://${hlsUrl.hostname}/videoplayback/`)) - ); + const [, h, m, s] = info.contentDetails.duration.match(/P.*?T(\d+H)?(\d+M)?(\d+S)/); + length = + ((h != null ? parseInt(h.replace("H", "")) * 60 * 60 : 0) + + (m != null ? parseInt(m.replace("M", "")) * 60 : 0) + + parseInt(s.replace("S", ""))) * + 1000; + + let stream; + if (!connection.connection.playing) { + try { + stream = await fetch(hf.config.cobalt_api, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Api-Key ${hf.apikeys.cobalt}`, + }, + body: JSON.stringify({ + url: `https://youtu.be/${id}`, + downloadMode: "audio", + filenameStyle: "basic", + }), + }).then((res) => res.json()); + } catch (err) { + await textChannel.createMessage({ + content: `:warning: Failed to get stream: \`\`\`\n${err}\n\`\`\``, + }); + } + } + + media = stream?.url; } else if (type == "sc") { if (url?.startsWith("sc:")) url = url.replace(/^sc:/, "https://soundcloud.com/"); const client_id = await getSoundcloudClientID(); @@ -314,7 +337,7 @@ async function enqueue({guild_id, voice_id, text_id, url, type, addedBy, suppres } else { if (!media) { textChannel.createMessage({ - content: `:warning: No usable media was found for \`${url}\`. May possibly be due to region restrictions or paywalls.`, + content: type == "yt" ? "youtube :b:roke" : `:warning: No usable media was found for \`${url}\`.`, }); return; } @@ -367,18 +390,23 @@ async function enqueue({guild_id, voice_id, text_id, url, type, addedBy, suppres } async function youtubeSearch(msg, str) { - const {items} = await fetch(`${hf.config.piped_api}/search?q=${encodeURIComponent(str)}&filter=videos`).then((x) => - x.json() - ); + const {items} = await fetch( + `https://www.googleapis.com/youtube/v3/search?key=${ + hf.apikeys.google + }&maxResults=5&part=snippet&type=video&q=${encodeURIComponent(str)}` + ).then((res) => res.json()); - const selection = items.map((item) => ({ - value: "https://youtube.com" + item.url, - key: item.url.replace("/watch?v=", ""), - display: `${parseHtmlEntities(item.title).substring(0, 99)}${parseHtmlEntities(item.title).length > 99 ? "…" : ""}`, - description: `from ${parseHtmlEntities(item.uploaderName).substring(0, 95)}${ - parseHtmlEntities(item.uploaderName).length > 95 ? "…" : "" - }`, - })); + const selection = items.map((item) => { + const title = parseHtmlEntities(item.snippet.title); + const channel = parseHtmlEntities(item.snippet.channelTitle); + + return { + value: "https://youtu.be/" + item.id.videoId, + key: item.id.videoId, + display: `${title.substring(0, 99)}${title.length > 99 ? "…" : ""}`, + description: `from ${channel.substring(0, 95)}${channel.length > 95 ? "…" : ""}`, + }; + }); try { return await selectionMessage(msg, "Search results:", selection);