├── README.image └── 1.png ├── README.md ├── app.js ├── download.js ├── package.json ├── renderer.js ├── router.js └── views ├── download.html ├── foot.html ├── form.html └── head.html /README.image/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MakhouT/treehouseDownloader/a7229413e8e0da73bc99708aae3d7a79d1c8f758/README.image/1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Not active any more 2 | Since the upgrade of treehouse it is not working any more - since I don't have a treehouse subscription any more - I am not able to work on this. However I am happy to pick this up in the future when I do get myself a treehouse subscription again or if someone lends me theirs for a week or so. 3 | 4 | #Treehouse downloader 5 | This tool will help you download videos from the website www.teamtreehouse.com if you have an active subscription and don't want to use the build in downloader with iTunes. 6 | The application will also create all the folders for you numbering them so you can see all the video's in the correct order. 7 | 8 | #How to start 9 | 1. Pull the project 10 | ``` git clone https://github.com/MakhouT/treehouseDownloader.git ``` 11 | 2. With Node.js installed ([Node.js website](https://nodejs.org/en/)) 12 | 3. Open the command prompt/terminal and navigate to the folder where the application is 13 | ```cd treehouseDownloader``` 14 | 4. Type ``` npm install ``` to install all the required packages 15 | 5. Run ```node app.js``` 16 | 6. In a browser browse to http://127.0.0.1:8080/ 17 | 18 | 19 | 7.1 Copy the course download link to download from iTunes and insert it in the input of the application, this link should start with 'itpc' and should end with you token corresponding to your account. This will download 1 course. 20 | 21 | ``` 22 | itpc://teamtreehouse.com/library/build-an-interactive-story-app.rss?feed_token=xxxxxxxx-b326-4ada-8ce4-ca456d6axxxx 23 | ``` 24 | 25 | 7.2 To download a full track paste in the url of the track and manually add "?tokencode", so it would look something like this 26 | ``` 27 | https://teamtreehouse.com/library/adobe-illustrator-for-web-design?xxxxxxxx-b326-4ada-8ce4-ca456d6axxxx 28 | ``` 29 | Note that downloading a full track is still instable. 30 | 31 | 8. Press download and have some patience until the video's are downloaded. 32 | 33 |
34 | It should create a folder for each chapter of the course and each video inside of it should be numbered so you can watch them in the correct order. 35 | If you find any bugs please report them so I can fix them. 36 | 37 | 38 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var router = require("./router.js"); 3 | var http = require("http"); 4 | 5 | //Create a basic server 6 | http.createServer(function(request, response){ 7 | router.home(request, response); 8 | }).listen(8080, "127.0.0.1"); 9 | console.log("Server up and running on 127.0.0.1:8080"); -------------------------------------------------------------------------------- /download.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var download = require("download"); 3 | var downloadStatus = require("download-status"); 4 | var https = require("https"); 5 | var xmlParser = require("xml2js").parseString; 6 | var mkdirp = require('mkdirp'); 7 | 8 | function downloadVideo(source, hd){ 9 | // Retrieve xml from treehouse 10 | https.get(source, function(response){ 11 | var xml = ""; 12 | 13 | // Put all the xml data in a variable 14 | response.on("data", function(chunk){ 15 | xml += chunk; 16 | }); 17 | 18 | // When that's done, filter the xml 19 | // And add all the links in an array 20 | response.on("end", function(){ 21 | xmlParser(xml, function (err, xmlObject) { 22 | var courseItem = xmlObject.rss.channel[0].item; 23 | var courseLinks = []; 24 | 25 | //I'm adding all the link 26 | courseItem.forEach(function(course){ 27 | courseLinks.push(course.enclosure[0].$.url + hd); 28 | }); 29 | 30 | var moduleNumbering = 0; 31 | var previousModuleFolder = ""; 32 | var courseFileNameNumbering; 33 | var firstVideoOfModule = true; 34 | 35 | courseLinks.forEach(function(course){ 36 | var courseDetails = course.replace("https:\/\/teamtreehouse.com\/library\/", "").split("\/"); 37 | var courseName = courseDetails[0]; 38 | var courseModule = courseDetails[1]; 39 | var courseVideo = courseDetails[2]; 40 | var courseFileNameNoNumbering = (courseDetails[3].substring(0, courseDetails[3].indexOf("?"))).replace("download", courseVideo); //The filename missing the filenumbering 41 | 42 | if(courseModule !== previousModuleFolder){ 43 | //Starting a new module 44 | moduleNumbering++; 45 | previousModuleFolder = courseModule; 46 | courseFileNameNumbering = 1; 47 | } 48 | 49 | //Add leading zero if numbers are from 1-9 50 | if(courseFileNameNumbering < 10){ 51 | courseFileNameNumbering = "0" + courseFileNameNumbering; 52 | } 53 | 54 | //Form the name of the file with numbering so you can see the video's in the correct order 55 | //Also removing the token and options from the originel filename 56 | var courseFileName = courseFileNameNumbering + ". " + courseFileNameNoNumbering; 57 | courseFileNameNumbering++; 58 | var directory = courseName + "\/" + moduleNumbering + ". " + courseModule; 59 | 60 | //Make all the directories and download the video's in the correct directory 61 | mkdirp.sync(directory); 62 | new download({mode: "755"}).get(course).rename(courseFileName).dest(directory).use(downloadStatus()).run(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | } 68 | 69 | module.exports.downloadVideo = downloadVideo; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treehouse", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "download": "^4.4.3", 14 | "download-status": "^2.2.1", 15 | "mkdirp": "^0.5.1", 16 | "xml2js": "^0.4.15" 17 | }, 18 | "devDependencies": { 19 | "x-ray": "^2.0.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var fs = require("fs"); 3 | 4 | //Renders the files that are passed as arguments 5 | function view(templateName, response){ 6 | var fileContents = fs.readFileSync("./views/" + templateName + ".html"); 7 | response.write(fileContents); 8 | } 9 | 10 | module.exports.view = view; -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var xray = require("x-ray"); 3 | var renderer = require("./renderer.js"); 4 | var downloadVideo = require("./download"); 5 | var queryString = require("querystring"); 6 | var url; 7 | var hd; 8 | 9 | function home(request, response){ 10 | if(request.url === "/"){ 11 | //Home page, this will show the actuall form to start a download 12 | if(request.method.toUpperCase() === "GET"){ 13 | response.writeHead(200, {"Content-Type": "text/html"}); 14 | renderer.view("head", response); 15 | renderer.view("form", response); 16 | renderer.view("foot", response); 17 | response.end(); 18 | } 19 | else{ 20 | //When you submit the form, we should start downloading the video 21 | // We take the data from the form parse it then send it to the download module 22 | request.on("data", function(postBody){ 23 | var query = queryString.parse(postBody.toString()); 24 | url = query.url.replace("itpc", "https"); 25 | downloadVideo.downloadVideo(url); 26 | }); 27 | } 28 | } 29 | else if(request.url === "/download" && request.method.toUpperCase() === "POST"){ 30 | //Received the url from the form, start the download 31 | request.on("data", function(postBody){ 32 | var query = queryString.parse(postBody.toString()); 33 | var protocol = query.url.substring(0,7); 34 | var hdOption = query.hd; 35 | 36 | //If HD option is enabled we add that option to the final url 37 | if(hdOption === "hd"){ 38 | hd = "&hd=yes"; 39 | } 40 | else{ 41 | hd = ""; 42 | } 43 | 44 | //if the protocol is itpc then we're downloading a single track 45 | //else we're downloading a whole track 46 | if(protocol === "itpc://"){ 47 | url = query.url.replace("itpc", "https"); 48 | downloadVideo.downloadVideo(url, hd); 49 | } 50 | else{ 51 | var token = query.url.substring(query.url.indexOf("?")+1); 52 | var trackUrl = query.url.substring(0, query.url.indexOf("?")); 53 | 54 | var x = xray(); 55 | x(trackUrl, "#track-steps li.card.course a.card-title", ["@href"])(function(err, data){ 56 | data.forEach(function(course){ 57 | var courseFromTrack = course + ".rss?feed_token=" + token; 58 | downloadVideo.downloadVideo(courseFromTrack, hd); 59 | }); 60 | }); 61 | } 62 | }); 63 | 64 | response.writeHead(200, {"Content-Type": "text/html"}); 65 | renderer.view("head", response); 66 | renderer.view("download", response); 67 | renderer.view("foot", response); 68 | } 69 | else{ 70 | //Any url that doesn't exist or shouldn't be accessed, redirect to the homepage 71 | response.writeHead(301, {Location: 'http://127.0.0.1:8080/'}); 72 | response.end(); 73 | } 74 | } 75 | 76 | module.exports.home = home; -------------------------------------------------------------------------------- /views/download.html: -------------------------------------------------------------------------------- 1 |

Downloading

-------------------------------------------------------------------------------- /views/foot.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
-------------------------------------------------------------------------------- /views/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Download videos 5 | 6 | 7 | 74 | 75 | --------------------------------------------------------------------------------