├── index.js
├── lib
├── video-url-link.js
├── util.js
├── twitter
│ └── index.js
└── youtube
│ ├── index.js
│ └── formats.js
├── test
├── twitter.js
└── youtube.js
├── package.json
├── LICENSE
├── .gitignore
└── README.md
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./lib/video-url-link');
--------------------------------------------------------------------------------
/lib/video-url-link.js:
--------------------------------------------------------------------------------
1 | exports.youtube = require('./youtube');
2 | exports.twitter = require('./twitter');
--------------------------------------------------------------------------------
/test/twitter.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const videoUrlLink = require('..');
3 |
4 | describe('videoUrlLink.twitter', () => {
5 | const urls = [
6 | 'https://twitter.com/taylorswift13/status/1121935013484867585',
7 | 'https://twitter.com/blakelively/status/1045768952872398848'
8 | ]
9 | urls.forEach(function (url) {
10 | it('twitter.getInfo ' + url, (done) => {
11 | videoUrlLink.twitter.getInfo(url, { }, (error, info) => {
12 | console.log(info);
13 | done(error);
14 | });
15 | });
16 | });
17 | });
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 |
3 | exports.getReqOpt = (options) => {
4 | let defaultOptions = {
5 | gzip: true,
6 | method: 'GET',
7 | timeout: 5000,
8 | headers: {
9 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
10 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
11 | },
12 | jar: true
13 | };
14 | return _.extend(defaultOptions, options);
15 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-url-link",
3 | "version": "0.1.5",
4 | "description": "Get Video Download URL, Catch Video From Any Site.",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "test": "mocha --timeout 20000"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/catcto/video-url-link.git"
12 | },
13 | "keywords": [
14 | "video downloader",
15 | "youtube",
16 | "facebook",
17 | "twitter"
18 | ],
19 | "author": "catcto (https://github.com/catcto)",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/catcto/video-url-link/issues"
23 | },
24 | "homepage": "https://github.com/catcto/video-url-link#readme",
25 | "devDependencies": {
26 | "mocha": "^10.2.0"
27 | },
28 | "dependencies": {
29 | "cheerio": "^1.0.0-rc.12",
30 | "lodash": "^4.17.21",
31 | "request": "^2.88.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alan Wu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/test/youtube.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const videoUrlLink = require('..');
3 |
4 | describe('videoUrlLink.youtube', () => {
5 | const urls = [
6 | 'https://www.youtube.com/watch?v=ftlvreFtA2A',
7 | 'https://m.youtube.com/watch?v=ftlvreFtA2A',
8 | 'https://youtu.be/ftlvreFtA2A',
9 | 'https://www.youtube.com/v/ftlvreFtA2A',
10 | 'https://www.youtube.com/embed/ftlvreFtA2A',
11 | 'https://music.youtube.com/watch?v=ftlvreFtA2A',
12 | 'https://gaming.youtube.com/watch?v=ftlvreFtA2A',
13 | ]
14 |
15 | urls.forEach((url) => {
16 | it('youtube.getID ' + url, () => {
17 | let id = videoUrlLink.youtube.getID(url);
18 | assert.equal(id, 'ftlvreFtA2A');
19 | });
20 | })
21 |
22 | it('youtube.getInfo', (done) => {
23 | videoUrlLink.youtube.getInfo('https://www.youtube.com/watch?v=x7OCFcgf504', { hl: 'en', timeout: 15000, proxy: 'http://127.0.0.1:18880'}, (error, info) => {
24 | if (error) {
25 | done(error);
26 | } else {
27 | console.log(JSON.stringify(info, null, 4));
28 | done();
29 | }
30 | });
31 | });
32 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # video-url-link
2 |
3 | This module is used to get the video download url link from the website.
4 |
5 | Module in pure javascript for node.js
6 |
7 | Supported Sites: YouTube, Twitter
8 |
9 | ## Installation
10 |
11 | This is a [Node.js](https://nodejs.org/en/) module available through the
12 | [npm registry](https://www.npmjs.com/).
13 |
14 | Before installing, [download and install Node.js](https://nodejs.org/en/download/).
15 | Node.js 8.0 or higher is required.
16 |
17 | Installation is done using the
18 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
19 |
20 | ```bash
21 | $ npm install video-url-link
22 | ```
23 |
24 | ## Usage
25 |
26 | ```js
27 | const videoUrlLink = require('video-url-link');
28 | ```
29 |
30 | ### Get YouTube Info
31 |
32 | videoUrlLink.youtube.getInfo(url, [options], callback(error, info))
33 |
34 | ```js
35 | videoUrlLink.youtube.getInfo('https://youtu.be/{ID}', { hl: 'en' }, (error, info) => {
36 | if (error) {
37 | console.error(error);
38 | } else {
39 | console.log(info.details);
40 | console.log(info.formats);
41 | }
42 | });
43 | ```
44 |
45 | ### Get Twitter Info
46 |
47 | videoUrlLink.twitter.getInfo(url, [options], callback(error, info))
48 |
49 | ```js
50 | videoUrlLink.twitter.getInfo('https://twitter.com/{@}/status/{ID}', {}, (error, info) => {
51 | if (error) {
52 | console.error(error);
53 | } else {
54 | console.log(info.full_text);
55 | console.log(info.variants);
56 | }
57 | });
58 | ```
59 |
60 | ## Supported Sites
61 |
62 | | Site | URL | Video? | Details? |
63 | | :--- | :--- | :--- | :--- |
64 | | YouTube | | ✓ | ✓ |
65 | | Twitter | | ✓ | ✓ |
66 |
67 | ## Tests
68 |
69 | Tests are written with [mocha](https://mochajs.org)
70 |
71 | ```bash
72 | npm test
73 | ```
--------------------------------------------------------------------------------
/lib/twitter/index.js:
--------------------------------------------------------------------------------
1 | const Util = require('util');
2 | const request = require('request').defaults({ jar: true });
3 | var _ = require('lodash');
4 | const AUTHORIZATION = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
5 | const API_GUEST = 'https://api.twitter.com/1.1/guest/activate.json';
6 | const API_TIMELINE = 'https://api.twitter.com/2/timeline/conversation/%s.json?tweet_mode=extended'
7 |
8 | /**
9 | * Get twitter ID
10 | *
11 | * @param {string} url
12 | * @return {string}
13 | */
14 | exports.getID = (url) => {
15 | var regex = /twitter\.com\/[^/]+\/status\/(\d+)/;
16 | var matches = regex.exec(url);
17 | return matches && matches[1];
18 | };
19 |
20 | /**
21 | * Get twitter Info
22 | *
23 | * @param {string} url
24 | * @param {Object} options
25 | * @param {Function(Error, Object)} callback
26 | */
27 | exports.getInfo = (url, options, callback) => {
28 | if (typeof options === 'function') callback = options, options = {};
29 | const id = exports.getID(url);
30 | if (id) {
31 | req({
32 | url: API_GUEST,
33 | method: 'POST',
34 | }, options, (error, body) => {
35 | if (error) {
36 | callback(error);
37 | } else {
38 | let guest_token;
39 | try {
40 | guest_token = JSON.parse(body).guest_token;
41 | } catch (errInfo) {
42 | return callback(new Error(errInfo));
43 | }
44 | req({
45 | url: Util.format(API_TIMELINE, id),
46 | method: 'GET',
47 | headers: {
48 | 'x-guest-token': guest_token
49 | }
50 | }, options, (error, body) => {
51 | if (error) {
52 | callback(error);
53 | } else {
54 | try {
55 | const info = JSON.parse(body);
56 | callback(null, {
57 | full_text: info['globalObjects']['tweets'][id]['full_text'],
58 | variants: info['globalObjects']['tweets'][id]['extended_entities']['media'][0]['video_info']['variants']
59 | });
60 | } catch (errInfo) {
61 | return callback(new Error(errInfo));
62 | }
63 | }
64 | });
65 | }
66 | });
67 | } else {
68 | callback(new Error('Not a twitter URL'));
69 | }
70 | }
71 |
72 | function req(opt, options, callback) {
73 | opt = _.defaultsDeep({
74 | headers: {
75 | 'authorization': AUTHORIZATION
76 | }
77 | }, opt, options);
78 | request(opt, (error, response, body) => {
79 | if (error) {
80 | callback(error);
81 | } else {
82 | if (response.statusCode == 200 && body) {
83 | callback(null, body);
84 | } else {
85 | callback(new Error('twitter API error'));
86 | }
87 | }
88 | });
89 | }
--------------------------------------------------------------------------------
/lib/youtube/index.js:
--------------------------------------------------------------------------------
1 | const querystring = require('querystring');
2 | const Url = require('url');
3 | const request = require('request');
4 | const util = require('../util');
5 | const FORMATS = require('./formats');
6 | const VIDEO_URL = 'https://www.youtube.com/watch?v=';
7 | const EMBED_URL = 'https://www.youtube.com/embed/';
8 | const VIDEO_EURL = 'https://youtube.googleapis.com/v/';
9 | const INFO_PROTOCOL = 'https';
10 | const INFO_HOST = 'www.youtube.com';
11 | const INFO_PATH = '/get_video_info';
12 | const KEYS_TO_SPLIT = [
13 | 'fmt_list',
14 | 'fexp',
15 | 'watermark'
16 | ];
17 |
18 | /**
19 | * Validate youtube ID
20 | *
21 | * @param {string} id
22 | * @return {boolean}
23 | */
24 | const idRegex = /^[a-zA-Z0-9-_]{11}$/;
25 | exports.validateID = (id) => {
26 | return idRegex.test(id);
27 | };
28 |
29 | /**
30 | * Get youtube ID
31 | *
32 | * URL formats
33 | * - https://www.youtube.com/watch?v={ID}
34 | * - https://m.youtube.com/watch?v={ID}
35 | * - https://youtu.be/{ID}
36 | * - https://www.youtube.com/v/{ID}
37 | * - https://www.youtube.com/embed/{ID}
38 | * - https://music.youtube.com/watch?v={ID}
39 | * - https://gaming.youtube.com/watch?v={ID}
40 | *
41 | * @param {string} url
42 | * @return {string}
43 | */
44 | const queryHost = new Set([
45 | 'youtube.com',
46 | 'www.youtube.com',
47 | 'm.youtube.com',
48 | 'music.youtube.com',
49 | 'gaming.youtube.com',
50 | ]);
51 | const pathHost = new Set([
52 | 'youtu.be',
53 | 'youtube.com',
54 | 'www.youtube.com',
55 | ]);
56 | exports.getID = (url) => {
57 | const urlObj = Url.parse(url, true);
58 | let id = null;
59 | if (urlObj.query.v && queryHost.has(urlObj.hostname)) {
60 | id = urlObj.query.v;
61 | } else if (pathHost.has(urlObj.hostname)) {
62 | try {
63 | const paths = urlObj.pathname.split('/');
64 | id = paths[paths.length - 1];
65 | } catch (error) {
66 | console.error(error);
67 | }
68 | }
69 | if (!exports.validateID(id)) {
70 | console.error('Not a youtube URL');
71 | }
72 | return id;
73 | };
74 |
75 | /**
76 | * @param {Object} info
77 | * @return {Array.