├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.sample.js ├── package.json ├── sync.js └── tvshowdir.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/put-io-sync.iml 2 | .idea/scopes/scope_settings.xml 3 | node_modules/ 4 | config.js 5 | .idea/jsLibraryMappings.xml 6 | .idea/codeStyleSettings.xml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2 4 | 5 | - added support for aria2 RPC with concurrent downloads 6 | 7 | ## 0.1 8 | 9 | - Initial release 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Max Winde 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # put.io sync 2 | 3 | Synchronze directories from put.io to your local file system. 4 | 5 | This script is intended to run as a cron job. put.io sync will. 6 | 7 | put.io sync uses the put.io API v2. 8 | 9 | - recreate your put.io file structure locally 10 | - delete all files that allready have been downloaded from put.io 11 | - delete empty directories from put.io 12 | 13 | After downloading an file the script can optionaly notify you via Pushpin. 14 | 15 | # Requirements 16 | 17 | - node.js 0.10 or later (might work with older versions, havent tested it) 18 | - [aria2](http://aria2.sourceforge.net/) download manager 19 | 20 | # Installation 21 | 22 | - `npm install` 23 | - `cp config.sample.js config.js` 24 | - you need to enter some API keys into the config.js file. look at the links inside this file 25 | 26 | aria2 can run in two modes: in command line mode or in rpc-server mode. rpc mode is now the default mode. You can turn it of be changing aria2c.useRPC to false in the config. 27 | 28 | To learn how to setup aria2 in RPC mode read more [here](http://www.albertdelafuente.com/doku.php/wiki/dev/raspi/aria2c-raspi). 29 | 30 | # Usage 31 | 32 | Syncing directories from put.io to a local directory call: 33 | 34 | `node sync.js -d put.io-directory-id -l /path/to/your/local/sync/dir` 35 | 36 | If you want to sync TV shows you might so using the -s parameter. The script expects a subdirectory for every TV Show and will try to move TV shows directly to the correct dir. 37 | 38 | `node sync.js -d put.io-tvshow-directory -s /path/to/your/tv-shows -l /path/were/all/the/rest/should/got/to` 39 | -------------------------------------------------------------------------------- /config.sample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'putIo': { 3 | 'oauth2key': 'register an app here https://put.io/v2/oauth2/register and enter the generated oauth2 token here' 4 | }, 5 | 'aria2c': { 6 | 'path': 'aria2c', 7 | 'rpcHost': '127.0.0.1:6800', // you might run into problems if you use 'localhost' here 8 | 'useRPC': true 9 | }, 10 | 'pushpin': { 11 | 'enabled': false, 12 | 'userkey': 'user key from here: https://pushover.net/ ', 13 | 'appkey': 'create an key here: https://pushover.net/apps/build ' 14 | } 15 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "put.io-v2": "git://github.com/343max/put.io.js-v2.git", 4 | "argv": "*", 5 | "underscore": "*", 6 | "execSync": "*", 7 | "node-pushover": "*", 8 | "xmlhttprequest": "*", 9 | "request": "*", 10 | "longjohn": "*" 11 | } 12 | } -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | var PutIO = require('put.io-v2'); 2 | var argv = require( 'argv' ); 3 | var _ = require('underscore'); 4 | var fs = require('fs'); 5 | var execSync = require('execSync'); 6 | var Pushover = require('node-pushover'); 7 | var request = require('request'); 8 | var TVShowMatcher = require('./tvshowdir'); 9 | var config = require('./config'); 10 | require('longjohn'); 11 | 12 | var push = null; 13 | if (config.pushpin.enabled) { 14 | push = new Pushover({ 15 | token: config.pushpin.appkey, 16 | user: config.pushpin.userkey 17 | }); 18 | } else { 19 | push = { 20 | send: function() {} 21 | }; 22 | } 23 | 24 | var api = new PutIO(config.putIo.oauth2key); 25 | 26 | var args = argv.option([{ 27 | name: 'directory-id', 28 | short: 'd', 29 | type: 'int', 30 | description: 'id of the directory to sync' 31 | }, { 32 | name: 'local-path', 33 | short: 'l', 34 | type: 'path', 35 | description: 'local dir to sync to' 36 | }, { 37 | name: 'tvshow-dir', 38 | short: 's', 39 | type: 'path', 40 | description: '(optional) local filepath of your TV show dir' 41 | }]).run(); 42 | 43 | var directoryId = args.options['directory-id'] || 0; 44 | var localPath = args.options['local-path']; 45 | var tvShowDir = args.options['tvshow-dir']; 46 | var matcher = null; 47 | if (tvShowDir) { 48 | matcher = TVShowMatcher(tvShowDir); 49 | } else { 50 | matcher = function() {}; 51 | } 52 | 53 | function deleteShowIfCompleted(api, fileNode, stat) { 54 | if (stat && stat.size == fileNode.size) { 55 | // this file was allready downloaded - so we might delete it 56 | console.log('deleting ' + fileNode.name + ' from put.io'); 57 | api.files.delete(fileNode.id); 58 | return true; 59 | }; 60 | 61 | return false; 62 | } 63 | 64 | function sendRPCRequest(methodName, params) { 65 | if (!params) params = []; 66 | 67 | request.post({ 68 | url: 'http://' + config.aria2c.rpcHost + '/jsonrpc', 69 | json: { 70 | "jsonrpc":"2.0", 71 | "method":methodName, 72 | "params": params, 73 | "id":"1", 74 | "timeout": 5000 75 | } 76 | }, function(error, response, body) { 77 | if (error && error.code == 'ECONNREFUSED') { 78 | console.error('connection refused to aria2c rpc at ' + config.aria2c.rpcHost); 79 | console.error('could it be you forgot to start aria2c --enable-rpc ?'); 80 | } 81 | 82 | if (body && body.error) { 83 | console.error('aria2c response: ' + body.error.message); 84 | } 85 | } 86 | ); 87 | } 88 | 89 | function listDir(directoryId, localPath, isChildDir) { 90 | api.files.list(directoryId, function gotPutIoListing(data) { 91 | if (data.files.length == 0) { 92 | if (isChildDir) { 93 | console.log('deleting empty directory from put.io'); 94 | api.files.delete(directoryId); 95 | } 96 | } else { 97 | fs.mkdir(localPath, 0766, function dirCreated() { 98 | _.each(data.files, function eachFile(fileNode) { 99 | var localFilePath = localPath + '/' + fileNode.name; 100 | 101 | if (fileNode.content_type == 'application/x-directory') { 102 | listDir(fileNode.id, localFilePath, true); 103 | } else { 104 | var fileDir = localPath; 105 | var tvshow = matcher(fileNode.name); 106 | 107 | if (tvshow) fileDir = tvshow.path; 108 | 109 | var finalPath = fileDir + '/' + fileNode.name; 110 | 111 | fs.stat(finalPath, function gotFileStat(err, stat) { 112 | if (deleteShowIfCompleted(api, fileNode, stat)) { 113 | return; 114 | } 115 | 116 | if (config.aria2c.rpcHost && config.aria2c.useRPC) { 117 | console.log('adding ' + localFilePath + ' to the download queue...'); 118 | sendRPCRequest('aria2.addUri', [ [ api.files.download(fileNode.id) ], { dir: fileDir } ]); 119 | 120 | if (tvshow) { 121 | push.send('put.io sync', 'Began download of an episode of ' + tvshow.name); 122 | } else { 123 | push.send('put.io sync', 'Began download of ' + fileNode.name); 124 | } 125 | 126 | } else { 127 | var shellCommand = config.aria2c.path + ' -d "' + fileDir + '" "' + api.files.download(fileNode.id) + '"'; 128 | 129 | console.log('downloading ' + localFilePath + '...'); 130 | console.log(shellCommand); 131 | var result = execSync.stdout(shellCommand); 132 | 133 | var afterStat = fs.statSync(finalPath); 134 | deleteShowIfCompleted(api, fileNode, afterStat); 135 | 136 | if (fileNode.size > 20 * 1024 * 1024) { 137 | if (tvshow) { 138 | push.send('put.io sync', 'Downloaded an episode of ' + tvshow.name); 139 | } else { 140 | push.send('put.io sync', 'Downloaded ' + fileNode.name); 141 | } 142 | } 143 | 144 | } 145 | }); 146 | } 147 | }); 148 | }); 149 | } 150 | }); 151 | } 152 | 153 | var lockFile = '/tmp/putiosync-' + directoryId + '.lock'; 154 | 155 | if (fs.existsSync(lockFile)) { 156 | console.log('Process already running. If it is not the delete ' + lockFile); 157 | } else { 158 | process.on('exit', function() { 159 | fs.unlinkSync(lockFile); 160 | }); 161 | fs.open(lockFile, 'w', 0666, function(err, fd) { 162 | fs.closeSync(fd); 163 | }); 164 | listDir(directoryId, localPath, false); 165 | 166 | } 167 | 168 | -------------------------------------------------------------------------------- /tvshowdir.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var _ = require('underscore'); 3 | 4 | module.exports = function BestDir(path) { 5 | var dirs = fs.readdirSync(path); 6 | 7 | var cleanupName = function(name) { 8 | return name.toLowerCase().replace(/[^A-Za-z0-9]/g, ''); 9 | } 10 | 11 | var maps = _.map(dirs, function(dirname) { 12 | return { 13 | 'name': dirname, 14 | 'slug': cleanupName(dirname), 15 | 'path': path + '/' + dirname 16 | }; 17 | }); 18 | 19 | return function(filename) { 20 | filename = cleanupName(filename); 21 | 22 | var bestMatch = null; 23 | 24 | _.each(maps, function(tvshow) { 25 | if (filename.indexOf(tvshow.slug) == -1) return; 26 | if (!bestMatch) { 27 | bestMatch = tvshow; 28 | } else if (tvshow.slug.length > bestMatch.slug.length) { 29 | bestMatch = tvshow 30 | } 31 | }); 32 | 33 | return bestMatch; 34 | } 35 | } 36 | --------------------------------------------------------------------------------