From c804bd671ffe7b6b5e2114aa55b52f3a2519ffb2 Mon Sep 17 00:00:00 2001 From: Snowflake Date: Fri, 19 Feb 2021 03:25:31 +0545 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8New=20player=20features=20(#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Androz --- .gitignore | 1 + index.js | 3 +- package.json | 11 +- src/Extractors/Discord.js | 46 ++++ src/Extractors/Extractor.js | 7 + src/Extractors/Facebook.js | 148 ++++++++++++ src/Extractors/Vimeo.js | 68 ++++++ src/Player.js | 453 ++++++++++++++++++++++++++++++++---- src/Track.js | 2 +- src/Util.js | 58 ++++- typings/index.d.ts | 151 +++++++++++- 11 files changed, 884 insertions(+), 64 deletions(-) create mode 100644 src/Extractors/Discord.js create mode 100644 src/Extractors/Extractor.js create mode 100644 src/Extractors/Facebook.js create mode 100644 src/Extractors/Vimeo.js diff --git a/.gitignore b/.gitignore index a74d490..5013b61 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ package-lock.json yarn.lock docs .vscode +test \ No newline at end of file diff --git a/index.js b/index.js index 38280e7..409153f 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ -process.env.YTDL_NO_UPDATE = true; +process.env.YTDL_NO_UPDATE = true module.exports = { + Extractors: require('./src/Extractors/Extractor'), version: require('./package.json').version, Player: require('./src/Player') } diff --git a/package.json b/package.json index e059f01..41b9f7a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "typings/index.d.ts", "funding": "https://github.com/Androz2091/discord-player?sponsor=1", "scripts": { - "test": "node index.js", + "test": "cd test && node index.js", "generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" }, "repository": { @@ -33,13 +33,16 @@ "@types/node": "^14.14.7", "chalk": "^4.1.0", "discord-ytdl-core": "^5.0.0", + "jsdom": "^16.4.0", "merge-options": "^3.0.4", - "moment": "^2.27.0", "node-fetch": "^2.6.0", - "soundcloud-scraper": "^4.0.0", + "parse-ms": "^2.1.0", + "reverbnation-scraper": "^2.0.0", + "soundcloud-scraper": "^4.0.3", "spotify-url-info": "^2.2.0", + "xvdl": "^1.0.2", "youtube-sr": "^3.0.0", - "ytdl-core": "^4.4.2" + "ytdl-core": "^4.4.3" }, "devDependencies": { "@discordjs/opus": "^0.3.2", diff --git a/src/Extractors/Discord.js b/src/Extractors/Discord.js new file mode 100644 index 0000000..e780c4d --- /dev/null +++ b/src/Extractors/Discord.js @@ -0,0 +1,46 @@ +const https = require('https') + +class Discord { + constructor () { + throw new Error(`The ${this.constructor.name} class may not be instantiated!`) + } + + /** + * @typedef {import('stream').Readable} Readable + */ + + /** + * Downloads discord attachment + * @param {string} url Discord attachment url + * @returns {Promise} + */ + static async download (url) { + const data = await Discord.getInfo(url) + return data.stream + } + + /** + * Returns discord attachment info + * @param {string} url Attachment url + */ + static getInfo (url) { + return new Promise((resolve) => { + https.get(url, res => { + const data = { + title: res.req.path.split('/').pop(), + format: res.headers['content-type'], + size: !isNaN(res.headers['content-length']) ? Math.round((parseInt(res.headers['content-length']) / (1024 * 1024)) * 100) / 100 : 0, + sizeFormat: 'MB' + } + + Object.defineProperty(data, 'stream', { + get: () => res + }) + + resolve(data) + }) + }) + } +} + +module.exports = Discord diff --git a/src/Extractors/Extractor.js b/src/Extractors/Extractor.js new file mode 100644 index 0000000..461f482 --- /dev/null +++ b/src/Extractors/Extractor.js @@ -0,0 +1,7 @@ +module.exports = { + DiscordExtractor: require('./Discord'), + FacebookExtractor: require('./Facebook'), + ReverbnationExtractor: require('reverbnation-scraper'), + VimeoExtractor: require('./Vimeo'), + XVideosExtractor: require('xvdl').XVDL +} diff --git a/src/Extractors/Facebook.js b/src/Extractors/Facebook.js new file mode 100644 index 0000000..890171e --- /dev/null +++ b/src/Extractors/Facebook.js @@ -0,0 +1,148 @@ +const fetch = require('node-fetch').default +const { JSDOM } = require('jsdom') + +class Facebook { + constructor () { + throw new Error(`The ${this.constructor.name} class may not be instantiated!`) + } + + /** + * Validates facebook url + * @param {string} url URL to validate + */ + static validateURL (url) { + const REGEX = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/ + if (!url || typeof url !== 'string') return false + return REGEX.test(url) + } + + /** + * @typedef {import('stream').Readable} Readable + */ + + /** + * Downloads facebook video + * @param {string} url Video url to download + * @returns {Promise} + */ + static download (url) { + return new Promise(async (resolve, reject) => { + if (!Facebook.validateURL(url)) reject(new Error('Invalid url.')) + const info = await Facebook.getInfo(url) + if (!info || !info.streamURL) return reject(new Error('video not found')) + const link = info.streamURL + let req = require('https') + + if (link.startsWith('http://')) req = require('http') + + req.get(link, res => { + resolve(res) + }) + }) + } + + /** + * Fetches facebook video info + * @param {string} url Facebook video url + */ + static async getInfo (url) { + if (!Facebook.validateURL(url)) throw new Error('Invalid url.') + try { + const html = await Facebook._parseHTML(url) + const document = new JSDOM(html).window.document + const rawdata = document.querySelector('script[type="application/ld+json"]').innerHTML + const json = JSON.parse(rawdata) + + const obj = { + name: json.name, + title: document.querySelector('meta[property="og:title"]').attributes.item(1).value, + description: json.description, + rawVideo: json.contentUrl, + thumbnail: json.thumbnailUrl, + uploadedAt: new Date(json.uploadDate), + duration: Facebook.parseTime(json.duration), + interactionCount: json.interactionCount, + streamURL: json.url, + publishedAt: new Date(json.datePublished), + width: json.width, + height: json.height, + live: !!json.publication[0].isLiveBroadcast, + nsfw: !json.isFamilyFriendly, + genre: json.genre, + keywords: json.keywords ? json.keywords.split(', ') : [], + comments: json.commentCount, + size: json.contentSize, + quality: json.videoQuality, + author: { + type: json.author['@type'], + name: json.author.name, + url: json.author.url + }, + publisher: { + type: json.publisher['@type'], + name: json.publisher.name, + url: json.publisher.url, + avatar: json.publisher.logo.url + }, + url: html.split('",page_uri:"')[1].split('",')[0], + shares: html.split(',share_count:{')[1].split('},')[0].split(':')[1], + views: html.split(',video_view_count:')[1].split(',')[0] + } + + return obj + } catch { + return null + } + } + + /** + * Parses time in ms + * @param {string} duration Raw duration to parse + * @returns {string} + */ + static parseTime (duration) { + if (typeof duration !== 'string') return duration + let a = duration.match(/\d+/g) + + if (duration.indexOf('M') >= 0 && duration.indexOf('H') === -1 && duration.indexOf('S') === -1) { + a = [0, a[0], 0] + } + + if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) { + a = [a[0], 0, a[1]] + } + if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1 && duration.indexOf('S') === -1) { + a = [a[0], 0, 0] + } + + duration = 0 + + if (a.length === 3) { + duration = duration + parseInt(a[0]) * 3600 + duration = duration + parseInt(a[1]) * 60 + duration = duration + parseInt(a[2]) + } + + if (a.length === 2) { + duration = duration + parseInt(a[0]) * 60 + duration = duration + parseInt(a[1]) + } + + if (a.length === 1) { + duration = duration + parseInt(a[0]) + } + + return duration + } + + /** + * @ignore + * @param {string} url website url to parse html + */ + static async _parseHTML (url) { + const res = await fetch(url.replace('/m.', '/')) + return await res.text() + } +} + +module.exports = Facebook diff --git a/src/Extractors/Vimeo.js b/src/Extractors/Vimeo.js new file mode 100644 index 0000000..291a621 --- /dev/null +++ b/src/Extractors/Vimeo.js @@ -0,0 +1,68 @@ +const fetch = require('node-fetch').default + +class Vimeo { + constructor () { + throw new Error(`The ${this.constructor.name} class may not be instantiated!`) + } + + /** + * @typedef {import('stream').Readable} Readable + */ + + /** + * Downloads from vimeo + * @param {number} id Vimeo video id + * @returns {Promise} + */ + static download (id) { + return new Promise(async (resolve) => { + const info = await Vimeo.getInfo(id) + if (!info) return null + + const downloader = info.stream.url.startsWith('https://') ? require('https') : require('http') + + downloader.get(info.stream.url, res => { + resolve(res) + }) + }) + } + + /** + * Returns video info + * @param {number} id Video id + */ + static async getInfo (id) { + if (!id) throw new Error('Invalid id') + const url = `https://player.vimeo.com/video/${id}` + + try { + const res = await fetch(url) + const data = await res.text() + const json = JSON.parse(data.split('