├── .gitignore
├── src
├── icon128.png
├── icon16.png
├── icon32.png
├── icon48.png
├── manifest.json
├── bradlys-ytd-injector.js
└── bradlys-ytd.js
├── .gitattributes
├── LICENSE
├── changelog
├── contributing.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | promo/*
3 | breaks/
4 |
--------------------------------------------------------------------------------
/src/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradlys/monochromatic-panda/HEAD/src/icon128.png
--------------------------------------------------------------------------------
/src/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradlys/monochromatic-panda/HEAD/src/icon16.png
--------------------------------------------------------------------------------
/src/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradlys/monochromatic-panda/HEAD/src/icon32.png
--------------------------------------------------------------------------------
/src/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradlys/monochromatic-panda/HEAD/src/icon48.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Bradly's YouTube Downloader",
4 | "version": "0.0.4.0",
5 | "description": "Download YouTube videos without pain",
6 | "homepage_url": "http://bradly.me/",
7 | "permissions": ["http://*.youtube.com/*", "https://*.ytimg.com/*"],
8 | "content_security_policy": "default-src 'self' ",
9 | "content_scripts": [
10 | {
11 | "matches": ["https://*.youtube.com/*"],
12 | "js": ["bradlys-ytd-injector.js"],
13 | "run_at": "document_end",
14 | "all_frames": true
15 | }
16 | ],
17 | "icons": {
18 | "16": "icon16.png",
19 | "32": "icon32.png",
20 | "48": "icon48.png",
21 | "128": "icon128.png"
22 | },
23 |
24 | "web_accessible_resources": ["bradlys-ytd.js"]
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Bradly Schlenker
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 |
--------------------------------------------------------------------------------
/changelog:
--------------------------------------------------------------------------------
1 |
2 | UPDATE 0.0.2.4:
3 | Updated URL encoding methodology. Filenames should be saved properly rather than with a bunch of %u0402's and so forth. Old method didn't work well with non-ASCII characters.
4 |
5 | UPDATE 0.0.2.1:
6 | No new tab will appear for regular video downloads. New tabs will appear for alternative/streaming formats still.
7 |
8 | UPDATE 0.0.2.0:
9 | Changed method of decrypting youtube signatures! Hopefully I don't have to do updates every few days for this extension anymore! Let me know if this ends up failing you. I'll rollback to previous strategy if it doesn't.
10 |
11 | UPDATE 0.0.0.8:
12 | Resolved issue with download button not appearing when clicking on related videos and when using playlists.
13 |
14 | UPDATE 0.0.0.4, 0.0.0.5, 0.0.0.6, 0.0.0.7, 0.0.0.8, 0.0.0.9, 0.0.1.0, 0.0.1.1, 0.0.2.2, 0.0.2.3, 0.0.2.5, 0.0.2.6:
15 | Updated signature decrypting algorithm.
16 |
17 | UPDATE 0.0.0.3:
18 | Added alternative video formats. These are mostly streaming audio only or streaming video only formats and, personally, I wouldn't bother with them unless you have a very specific purpose. Click the "Alternative Formats (experimental) -->" link to see them. You'll have to figure out a way to play them but you can. (Open VLC, use network streaming, and copy+paste the link in.)
19 |
20 | UPDATE 0.0.0.2:
21 | Updated algorithm for figuring out correct signatures. YouTube will update and change this often, thus breaking the extension for certain videos.
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | Please ensure your pull request adheres to the following guidelines:
4 |
5 | - [Read this.](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)
6 | - Search previous suggestions before making a new one, as yours may be a duplicate.
7 | - Make an individual pull request for each suggestion.
8 | - All new functions should be documented and have jsdoc compatible documentation.
9 | - Keep descriptions short and simple, but descriptive.
10 | - Start the description with a capital and end with a full stop/period.
11 | - Check your spelling and grammar.
12 | - Use similar method naming and syntax to existing codebase.
13 | - Make sure your text editor is set to remove trailing whitespace and always leave a new line at the end of any file.
14 |
15 | Thank you for your improvements, criticisms, and suggestions! Anything to push forward.
16 |
17 | ## Needs
18 |
19 | Unless otherwise shown, there are no tests or build tools. This is something that needs to be addressed immediately and is the top priority of this project. As it would be wise to have unit tests and Selenium tests to verify the correctness of the extension. It would be nice to have build tools to streamline the build process (simple as it stands but could get complex in the future) and minimize the code for distribution. However, the first step to this is to rewrite the code into testable code. As it stands, it's not very testable. This is the main reason for unit tests not existing.
20 |
21 | There is also the desire for a more streamlined UI, error reporting (automatic or done by the user), and some kind of MP3 link (possibly using a third party service but only if desired by the client).
22 |
23 | Overall, many thoughts but so few developers! Tests are of utmost importance though and, without them, it is hard for anyone to contribute to the codebase without fear of destroying the entire thing.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bradly's YouTube Downloader
2 | Google Chrome Extension for Downloading YouTube Videos
3 |
4 | ## Introduction
5 |
6 | Download YouTube videos without pain. Inserts a dropdown button with links to directly download the current video from YouTube in multiple formats. Easily integrates with the UI and does not interfere or rely on third party resources.
7 |
8 | As of December 20th, 2015, Google removed the extension from their web store.
9 |
10 | If there are any issues, please do not hesitate to create an issue here on Github so that I can fix it! Always follow this repository for any updates as I do sincerely try to update this as I can.
11 |
12 | ## Usage
13 |
14 | Go to a video on YouTube, click the Download button, and click on a format that you desire. The download will begin and come directly from YouTube's servers.
15 |
16 | ## Motivation
17 |
18 | The motivation behind this extension was to allow for users to easily and safely download YouTube videos. There were already extensions and websites available that allowed for the downloading of YouTube videos but they came at a great cost in convenience or safety. None of them did it without taking your information, inserting advertisements into the page, or trying to insert malware into your computer. This is where Bradly's YouTube Downloader comes in and saves the day!
19 |
20 | ## Installation
21 |
22 | Enable Developer Mode in Google Chrome and install the extension, [as described here](https://developer.chrome.com/extensions/getstarted#unpacked).
23 |
24 | A detailed video is [available here](https://www.youtube.com/watch?v=wRBKYiumhQI) that details error reporting, installation, updating, and a bit of troubleshooting.
25 |
26 | * I've since removed the dist folder and you should now use the src folder and use the unpacked files there.
27 |
28 | ## Contributing
29 |
30 | Contributions are always welcome!
31 | Please read the [contribution guidelines](contributing.md) first.
32 |
33 | ## License
34 |
35 | [MIT (c) 2015 Bradly Schlenker](LICENSE)
36 |
--------------------------------------------------------------------------------
/src/bradlys-ytd-injector.js:
--------------------------------------------------------------------------------
1 | let URLToDecryptionFunction = {};
2 |
3 | /**
4 | * Takes in a URL to a text type file and returns the content once the promise resolves.
5 | * @param {string} url
6 | * @returns {Promise}
7 | */
8 | function getContent(url) {
9 | return new Promise(function(resolve, reject) {
10 | let request = new XMLHttpRequest();
11 | request.onreadystatechange = function() {
12 | if (request.readyState === 4) {
13 | if (request.status === 200) {
14 | resolve(request.responseText);
15 | } else {
16 | reject('');
17 | }
18 | }
19 | };
20 | request.open("GET", url);
21 | request.send();
22 | });
23 | }
24 |
25 | /**
26 | * Gets the URL to a file that matches the needle from document.scripts
27 | * @returns {string} Will return the url string or will return empty if not found
28 | */
29 | function getScriptURL(needle) {
30 | let haystacks = document.scripts;
31 | // try to get a url for a script that has needle in it
32 | for (let i of haystacks) {
33 | let haystack = haystacks[i].src;
34 | if (haystack && haystack.indexOf(needle) !== -1) {
35 | return haystack;
36 | }
37 | }
38 | return '';
39 | }
40 |
41 | function getDecryptionFunctionName(haystack) {
42 | // Use two different methods for getting the decryption function name. These vary but generally have these traits.
43 | // look for something like: `signature", functionName(` <-- capture functionName
44 | let gen2 = /signature['"]\s*,\s*([a-zA-Z0-9$]+)\(/;
45 | // look for something like: `.sig||functionName(` <-- capture functionName
46 | let gen1 = /\.sig\|\|([a-zA-Z0-9$]+)\(/;
47 | let functionName = haystack.match(gen1);
48 | if (!functionName) {
49 | functionName = haystack.match(gen2);
50 | }
51 | if (!functionName) return '';
52 | return functionName[1];
53 | }
54 |
55 | function getFunction(needle, haystack) {
56 | // group 1 is the function declaration up to but not including params. (3 different attempts) But don't capture it
57 | // group 2 is the params declaration
58 | // group 3 is the code for the function
59 | // JS doesn't support named capture groups (annoying!)
60 | let escaped_needle = regexEscapeString(needle);
61 | let functionCaptureRegex = `
62 | (?:function\\s+${ escaped_needle } | [{;,]\\s*${ escaped_needle }\\s*=\\s*function | var\\s+${ escaped_needle }\\s*=\\s*function)\\s*
63 | \\(([^)]*)\\)
64 | \\s*{([^}]+)}`.replace(/\s/g, ''); // JS has no free-spacing mode (also annoying!)
65 |
66 | let match = haystack.match(new RegExp(functionCaptureRegex), 'g');
67 | if (!match) return getObject(needle, haystack);
68 |
69 | let params = match[1]; // Although labeled params - this is usually 1 parameter with YouTube code.
70 | let code = match[2];
71 | let needleFunction = `var ${ needle } = function(${ params }) { ${ code } }`;
72 |
73 | let subFunction = getFirstSubFunction(needleFunction, params, needle, haystack);
74 | // if no subfunctions inside then we can just pass back the needleFunction we made
75 | if (!subFunction) {
76 | return needleFunction;
77 | }
78 |
79 | // otherwise, we need to add the subfunction code to the inside of the code.
80 | // Basically, put it in the same scope while retaining function names.
81 | return `var ${ needle } = function(${ params }) {
82 | ${ subFunction }
83 | ${ code }
84 | };`;
85 | }
86 |
87 | /**
88 | * Given a haystack and needle, get the declaration of the needle in the haystack. Presuming it's an object declaration.
89 | * @param {string} needle
90 | * @param {string} haystack
91 | * @returns {string}
92 | */
93 | function getObject(needle, haystack) {
94 | let escaped_needle = regexEscapeString(needle);
95 | let match = haystack.match(new RegExp("(var " + escaped_needle + "={[\\S\\s]*?(?=}};)}};)"), 'm');
96 | if (!match) return '';
97 | return match[1];
98 | }
99 |
100 | /**
101 | * Obtains the first subfunction reference out of the provided haystack and then
102 | * returns that subfunction's declaration.
103 | *
104 | * WARNING: This is not a very generic method - as it is specifically tuned for YouTube.
105 | * Unlike getFunction which is more generic.
106 | *
107 | * @param {string} haystack haystack to look for subfunctions in
108 | * @param {string} haystackParameter function
109 | * @param {string} needle function to look for
110 | * @param {string} script script to search
111 | * @returns {string}
112 | */
113 | function getFirstSubFunction(haystack, haystackParameter, needle, script) {
114 | haystackParameter = regexEscapeString(haystackParameter);
115 | // We know that the code generally is like `XX.YY(haystackParameter, arg2);` - Not bulletproof but good enough
116 | let firstSubFunctionName = haystack.match(new RegExp("([\\w$]+)\\.[\\w$]+\\(" + haystackParameter + "[^)]*\\)", 'm'));
117 | if (!firstSubFunctionName) return '';
118 |
119 | // Need to look for the function in the entire script
120 | let subFunction = getFunction(firstSubFunctionName[1], script);
121 | if (!subFunction) return '';
122 |
123 | return subFunction;
124 | }
125 |
126 | /**
127 | * Take a string and return the regex safe version of it.
128 | * Which is to say, a version that can be inserted into regexp as is.
129 | *
130 | * @param string
131 | * @returns {string}
132 | */
133 | function regexEscapeString(string) {
134 | return string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
135 | }
136 |
137 | /**
138 | * Starts the process of putting the decryption signature into state for use by other processes.
139 | *
140 | * @param {string} url optional parameter is the link to the JS file to get the decryption signature from
141 | * @returns {Promise} Promise will resolve when decryption is done or fail if a problem occurs.
142 | */
143 | function putDecryptionSignatureIntoState(url) {
144 | if (!url) {
145 | // try to get a script that has html5player in the url
146 | url = getScriptURL('html5player');
147 | // if not that then try to get a script that has player in the url
148 | if (!url) url = getScriptURL('player');
149 | if (!url) return new Promise(function(resolve, reject) { reject(false); });
150 | }
151 | let p = getContent(url);
152 | return p.then(function(text) {
153 | let functionName = getDecryptionFunctionName(text);
154 | if (!functionName) return false;
155 |
156 | let func = getFunction(functionName, text);
157 | if (!func) return false;
158 |
159 | // put the function into state
160 | URLToDecryptionFunction[url] = func;
161 | return url;
162 | });
163 | }
164 |
165 | /**
166 | * Add event listener to DOM to know when the injected script needs the decryption scheme.
167 | */
168 | document.addEventListener('BYTD_connectExtension', function(e) {
169 | if (e.detail) {
170 | let p = putDecryptionSignatureIntoState(e.detail);
171 | p.then(function (url) {
172 | if (!url) return;
173 | // since we know the URL now and know that this is in state, we can add the function to the DOM.
174 | let scriptElement = document.createElement('script');
175 | // Get function name and then wrap the function in decrypt_signature so that we can call it consistently.
176 | let func = URLToDecryptionFunction[url];
177 | let funcName = func.slice(4, func.indexOf('=')-1);
178 |
179 | scriptElement.innerText = `decrypt_signature = function(zzzz) {
180 | ${ func }
181 | return ${ funcName }(zzzz);
182 | }`;
183 | scriptElement.onload = function() { this.parentNode.removeChild(this);};
184 | (document.head||document.documentElement).appendChild(scriptElement);
185 | });
186 | }
187 | }, false);
188 |
189 | // inject our download button creator script into the user's current DOM
190 | let s = document.createElement('script');
191 | s.src = chrome.extension.getURL('bradlys-ytd.js');
192 | s.onload = function() {
193 | this.parentNode.removeChild(this);
194 | };
195 | (document.head||document.documentElement).appendChild(s);
196 |
--------------------------------------------------------------------------------
/src/bradlys-ytd.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | //Every 500ms, see if we can create the youtube downloader element
3 | setInterval(function () {
4 | try {
5 | createYouTubeDownloader();
6 | } catch (e) {
7 | BYTDERRORS.addError(e);
8 | }
9 | }, 500);
10 |
11 | const BUTTON_APPEND_SELECTOR = '#menu-container > #menu > ytd-menu-renderer';
12 | const VIDEO_TITLE_SELECTOR = '.title.style-scope.ytd-video-primary-info-renderer';
13 | const BRADLYS_YOUTUBE_DOWNLOADER_BUTTON_SELECTOR = '#bradlys-youtube-downloader';
14 | const BRADLYS_YOUTUBE_DOWNLOADER_UL_SELECTOR = '#bradlys-youtube-downloader-ul';
15 | const BRADLYS_YOUTUBE_DOWNLOADER_ID = 'bradlys-youtube-downloader';
16 | const BRADLYS_YOUTUBE_DOWNLOADER_ERRORS_SELECTOR = '#bradlys-youtube-downloader .bytd-error';
17 | const ALTERNATIVE_FORMATS_MENU_NAME = 'Alternative Formats (Experimental) -->';
18 | let LAST_LOCATION_HREF = '';
19 |
20 | //logging all errors that occur within the program
21 | let BYTDERRORS = {
22 | errors: [],
23 | addError: function (err) {
24 | this.errors.push(err);
25 | //addErrorToView(err);
26 | }
27 | };
28 |
29 | class Item {
30 | constructor(text, parent) {
31 | if (typeof text !== 'string' && text !== undefined) {
32 | throw 'Non-String type provided for Text.';
33 | }
34 | this._text = text;
35 | let randomInt = 'bytd' + intGen.next().replace('.', '').replace('+', '');
36 | let selector = '#' + randomInt;
37 | while (document.querySelector(selector)) {
38 | randomInt = 'bytd' + intGen.next().replace('.', '').replace('+', '');
39 | selector = '#' + randomInt;
40 | }
41 | this._id = randomInt;
42 | //this is where things get weird
43 | if (parent === undefined || parent instanceof Menu) {
44 | this._parent = parent;
45 | } else {
46 | throw 'Incorrect type provided for parent.';
47 | }
48 | }
49 |
50 | getText() {
51 | return this._text ? this._text : '';
52 | }
53 |
54 | getId() {
55 | return this._id;
56 | }
57 |
58 | setId(id) {
59 | this._id = id;
60 | }
61 |
62 | getParent() {
63 | return this._parent;
64 | }
65 |
66 | getHTML() {
67 | let id = this.getId();
68 | let text = this.getText();
69 | let linkTemplate =
70 | `
71 | ${text}
72 |
`;
73 | return linkTemplate;
74 | }
75 | }
76 |
77 | class Link extends Item {
78 | constructor(text, url, parent) {
79 | super(text, parent);
80 | if (typeof url === 'boolean') {
81 | if (url !== false) {
82 | throw 'True Boolean value provided for URL.';
83 | }
84 | } else if (typeof url === 'string') {
85 | if (url.length < 0) {
86 | throw 'Length of 0 String provided for URL.';
87 | }
88 | } else {
89 | throw 'Invalid type provided for URL.';
90 | }
91 | this._url = url;
92 | }
93 |
94 | getURL() {
95 | return this._url;
96 | }
97 |
98 | getHTML() {
99 | let id = this.getId();
100 | let text = this.getText();
101 | let url = this.getURL();
102 | return `
103 |