├── .gitignore ├── .project ├── .travis.yml ├── LICENSE ├── README.md ├── app.js ├── bin └── lumus ├── config.js ├── helpers └── version.js ├── index.html ├── jobs ├── checker.js ├── decorator.js ├── filter.js ├── nexter.js ├── notifier.js ├── renamer.js ├── searcher.js ├── serviceDispatcher.js ├── subtitler.js └── torrenter.js ├── labels.js ├── models ├── item.js ├── music.js └── show.js ├── notifiers └── kodi.js ├── package.json ├── params.json ├── public ├── javascripts │ ├── music.js │ ├── search.js │ ├── show.js │ └── yify.js └── stylesheets │ └── style.less ├── routes ├── config.js ├── index.js ├── item.js ├── list.js ├── music.js ├── search.js ├── show.js ├── torrent.js └── user.js ├── searchers ├── isohuntSearcher.js ├── kickassSearcher.js └── tpbSearcher.js ├── subtitlers └── opensubtitler.js ├── test ├── jobs.checker.js ├── jobs.decorator.js ├── jobs.filter.js ├── routes.index.js ├── routes.item.js └── routes.list.js └── views ├── artist.jade ├── config.jade ├── error.jade ├── includes └── item.jade ├── index.jade ├── layout.jade ├── newReleases.jade ├── search.jade ├── show.jade └── torrents.jade /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /users.db 4 | ======= 5 | lib-cov 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | node_modules 20 | /config.json 21 | /config.v2.json 22 | /items.db 23 | /music.db 24 | /show.db 25 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | lumus 4 | 5 | 6 | 7 | 8 | 9 | 10 | org.nodeclipse.ui.NodeNature 11 | org.eclipse.wst.jsdt.core.jsNature 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 ziacik 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lumus 2 | ===== 3 | 4 | [![Build Status](https://travis-ci.org/ziacik/lumus.svg?branch=master)](https://travis-ci.org/ziacik/lumus) 5 | [![NPM version](https://badge.fury.io/js/lumus.svg)](http://badge.fury.io/js/lumus) 6 | 7 | Search for movies, shows and music and add them automatically to Transmission when they appear on torrents. Lightweight all-in-one alternative to SickBeard, CouchPotato and Headphones. Integration with Kodi. 8 | 9 | # Screenshots 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
Main ScreenSearch Results
Season SelectionAlbum Selection
Torrent ChooserSettings
37 | 38 | # Install on OSMC or Linux 39 | 40 | ## Quick start 41 | 42 | To install Transmission, use AppStore. Make sure you have `rpc-enabled` setting set to `true` and `rpc-authentication-required` set to `false` or `rpc-whitelist` configured correctly. Also, consider setting `umask` setting to `2`. 43 | 44 | To install and run Lumus, run this in console. 45 | `sudo apt-get -y --purge remove node` 46 | `sudo apt-get -y install nodejs npm git` 47 | `sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10` 48 | 49 | `sudo npm install -g lumus` 50 | `cd && mkdir Lumus && cd Lumus && lumus` 51 | 52 | Navigate to [http://[Your Device IP or HostName]:3001](http://[Your Device IP or HostName]:3001) 53 | 54 | ### To make it run as service 55 | 56 | Install and configure `forever-service`. 57 | `sudo npm install -g forever` 58 | `sudo npm install -g forever-service` 59 | `sudo update-rc.d lumus defaults` 60 | 61 | To start for the first time, you can use 62 | `sudo service lumus start` 63 | 64 | Check the logfile `/var/log/lumus.log` if something doesn't work as expected. 65 | 66 | # Install on Windows 67 | 68 | ## Install Transmission 69 | Windows support for Transmission is not yet official, but there is an unofficial port [here](http://sourceforge.net/projects/trqtw/). 70 | 71 | ## Install Node.js 72 | Download and install with [node.js installer](http://nodejs.org/download/). 73 | 74 | ## Install Lumus 75 | There is no windows installer yet, so you need to use command line. 76 | 77 | 1. Open command line 78 | `cmd` 79 | 80 | 2. Install Lumus 81 | `npm install -g lumus` 82 | 83 | 2. Run the application 84 | `lumus` 85 | 86 | 3. Browse the application url in your webbrowser. 87 | Use correct ip address or hostname instead of *localhost* if you installed the application on a dedicated server. 88 | [http://localhost:3001](http://localhost:3001) 89 | 90 | ## Install Kodi 91 | Optionally, you may want to install [Kodi](http://kodi.tv/) to get automatic library updates and notifications from Lumus. 92 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var routes = require('./routes'); 8 | var search = require('./routes/search'); 9 | var music = require('./routes/music'); 10 | var show = require('./routes/show'); 11 | var list = require('./routes/list'); 12 | var item = require('./routes/item'); 13 | var torrent = require('./routes/torrent'); 14 | var config = require('./routes/config'); 15 | var http = require('http'); 16 | var path = require('path'); 17 | var checker = require('./jobs/checker'); 18 | 19 | var app = express(); 20 | app.locals.moment = require('moment'); 21 | 22 | // all environments 23 | app.set('port', process.env.PORT || 3001); 24 | app.set('views', path.join(__dirname, 'views')); 25 | app.set('view engine', 'jade'); 26 | app.use(express.favicon()); 27 | app.use(express.logger('dev')); 28 | app.use(express.json()); 29 | app.use(express.urlencoded()); 30 | app.use(express.methodOverride()); 31 | app.use(express.cookieParser('your secret here')); 32 | app.use(express.session()); 33 | app.use(app.router); 34 | app.use(require('less-middleware')(path.join(__dirname, 'public'))); 35 | app.use(express.static(path.join(__dirname, 'public'))); 36 | 37 | // development only 38 | if ('development' == app.get('env')) { 39 | app.use(express.errorHandler()); 40 | } 41 | 42 | app.get('/', routes.index); 43 | app.get('/update', routes.update); 44 | app.get('/search', search.runSearch); 45 | app.get('/add', list.add); 46 | app.get('/list', list.list); 47 | app.get('/changeState', item.changeState); 48 | app.get('/remove', item.remove); 49 | app.get('/artist', music.artist); 50 | app.get('/artist/add', music.add); 51 | app.get('/show', show.show); 52 | app.get('/show/add', show.add); 53 | app.get('/torrent', torrent.list); 54 | app.get('/torrent/add', torrent.add); 55 | app.get('/config', config.form); 56 | app.get('/newReleases', function(req, res) { 57 | res.render('newReleases'); 58 | }); 59 | app.post('/config', config.save); 60 | 61 | if (typeof String.prototype.startsWith != 'function') { 62 | String.prototype.startsWith = function(str) { 63 | return this.substring(0, str.length) === str; 64 | } 65 | }; 66 | 67 | if (typeof String.prototype.endsWith != 'function') { 68 | String.prototype.endsWith = function(str) { 69 | return this.substring(this.length - str.length, this.length) === str; 70 | } 71 | }; 72 | 73 | http.createServer(app).listen(app.get('port'), function(){ 74 | console.log('Lumus server listening on port ' + app.get('port')); 75 | }); 76 | 77 | process.nextTick(checker); 78 | -------------------------------------------------------------------------------- /bin/lumus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@" 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var root = path.join(path.dirname(fs.realpathSync(__filename)), '..'); 7 | require(root + '/app.js'); 8 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var nconf = require('nconf'); 2 | var fs = require('fs'); 3 | var labels = require('./labels'); 4 | var Q = require('q'); 5 | 6 | module.exports = nconf; 7 | module.exports.Preference = Object.freeze({ 8 | required : 'required', 9 | preferred : 'preferred', 10 | optional : 'optional', 11 | disfavoured : 'disfavoured', 12 | unwanted : 'unwanted', 13 | }); 14 | 15 | var Preference = module.exports.Preference; 16 | 17 | nconf.use('file', { file : "config.v2.json"}); 18 | nconf.load(); 19 | 20 | nconf.defaults({ 21 | version: 0, 22 | checkInterval : 60, 23 | removeFinishedDays : 0, 24 | movieSettings : { 25 | maxSize : 6000, 26 | destinationDir : 'Movies', 27 | digitalAudioPreference : Preference.preferred, 28 | hdVideoPreference : Preference.preferred 29 | }, 30 | showSettings : { 31 | maxSize : 10000, 32 | destinationDir : 'Shows', 33 | digitalAudioPreference : Preference.preferred, 34 | hdVideoPreference : Preference.preferred 35 | }, 36 | musicSettings : { 37 | maxSize : 300, 38 | destinationDir : 'Music', 39 | losslessFormatPreference : Preference.optional 40 | }, 41 | }); 42 | 43 | var legacyHdVideoPreference1 = nconf.get('movieSettings:requireHD'); 44 | 45 | if (legacyHdVideoPreference1) { 46 | nconf.set('movieSettings:hdVideoPreference', legacyHdVideoPreference1); 47 | nconf.clear('movieSettings:requireHD'); 48 | } 49 | 50 | var legacyHdVideoPreference2 = nconf.get('showSettings:requireHD'); 51 | 52 | if (legacyHdVideoPreference2) { 53 | nconf.set('showSettings:hdVideoPreference', legacyHdVideoPreference2); 54 | nconf.clear('showSettings:requireHD'); 55 | } 56 | 57 | var legacyDigitalAudioPreference1 = nconf.get('movieSettings:requireDigitalSound'); 58 | 59 | if (legacyDigitalAudioPreference1) { 60 | nconf.set('movieSettings:digitalAudioPreference', legacyDigitalAudioPreference1); 61 | nconf.clear('movieSettings:requireDigitalSound'); 62 | } 63 | 64 | var legacyDigitalAudioPreference2 = nconf.get('showSettings:requireDigitalSound'); 65 | 66 | if (legacyDigitalAudioPreference2) { 67 | nconf.set('showSettings:digitalAudioPreference', legacyDigitalAudioPreference2); 68 | nconf.clear('showSettings:requireDigitalSound'); 69 | } 70 | 71 | var legacyLosslessFormatPreference = nconf.get('musicSettings:requireLossless'); 72 | 73 | if (legacyLosslessFormatPreference) { 74 | nconf.set('musicSettings:losslessFormatPreference', legacyLosslessFormatPreference); 75 | nconf.clear('musicSettings:requireLossless'); 76 | } 77 | 78 | if (legacyHdVideoPreference1 || legacyHdVideoPreference2 || legacyDigitalAudioPreference1 || legacyDigitalAudioPreference2 || legacyLosslessFormatPreference) { 79 | nconf.save(); 80 | } 81 | 82 | labels.add({ 83 | checkInterval : 'Check Interval [sec]', 84 | movieSettings : ' Movies', 85 | maxSize : 'Max Size [MB]', 86 | digitalAudioPreference : 'Digital Audio', 87 | hdVideoPreference : 'HD Video', 88 | showSettings : ' Shows', 89 | musicSettings : ' Music', 90 | losslessFormatPreference : 'Lossless Format', 91 | removeFinishedDays : 'Remove Finished Items After ? Days 0 is never', 92 | destinationDir : 'Directory' 93 | }); 94 | 95 | var nconfSave = Q.nbind(nconf.save, nconf); 96 | 97 | module.exports.save = function() { 98 | nconf.set('version', 2); 99 | return nconfSave(); 100 | } -------------------------------------------------------------------------------- /helpers/version.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var npm = require('npm'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var moment = require('moment'); 6 | 7 | var newVersionLastChecked; 8 | var lastNewVersion; 9 | 10 | //TODO Review 11 | module.exports.update = function() { 12 | var load = Q.nbind(npm.load, npm); 13 | return load().then(function() { 14 | console.log('update'); 15 | var update = Q.nbind(npm.commands.update, npm.commands); 16 | return update([]); 17 | }).then(function(data) { 18 | console.log('Update result', data); 19 | }); 20 | } 21 | 22 | module.exports.newVersion = function() { 23 | var today = moment().format('YYYY-MM-DD'); 24 | 25 | if (newVersionLastChecked === today) { 26 | return Q(lastNewVersion); 27 | } 28 | 29 | var load = Q.nbind(npm.load, npm); 30 | return load().then(function() { 31 | var view = Q.nbind(npm.commands.view, npm.commands); 32 | return view(["lumus", "version"]); 33 | }).then(function(data) { 34 | var version; 35 | 36 | for (var i = 0; i < data.length; i++) { 37 | var record = data[i]; 38 | 39 | if (!record) { 40 | continue; 41 | } 42 | 43 | for (var property in record) { 44 | if (record.hasOwnProperty(property) && record[property].version) { 45 | version = record[property].version; 46 | break; 47 | } 48 | } 49 | } 50 | 51 | newVersionLastChecked = today; 52 | 53 | if (version !== module.exports.myVersion) { 54 | lastNewVersion = version; 55 | return version; 56 | } else { 57 | lastNewVersion = undefined; 58 | return Q(undefined); 59 | } 60 | }); 61 | } 62 | 63 | var getMyVersion = function() { 64 | var packageJson = fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'); 65 | var packageInfo = JSON.parse(packageJson); 66 | return packageInfo.version; 67 | } 68 | 69 | module.exports.myVersion = getMyVersion(); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | Lumus by ziacik 17 | 18 | 19 | 20 |
21 |
22 |

Lumus

23 |

SickBeard, CouchPotato and Headphones lightweight alternative for automated downloading movies, shows and music with torrents written in node.js.

24 | View project onGitHub 25 |
26 |
27 | 28 |
29 |
30 |
31 |

32 | How to install on Raspbmc / Linux

33 | 34 |

First we need to install a torrent downloader called transmission. Skip this step if you already have transmission configured.

35 | 36 |

37 | Transmission

38 | 39 |

We only need to install a daemon (service running at background) instead of full-featured client. Thanks to that, less memory will be used.

40 | 41 |
    42 |
  1. Install transmission
    $ sudo apt-get install transmission-daemon

  2. 43 |
  3. Set it to run at startup
    $ sudo update-rc.d transmission-daemon defaults

  4. 44 |
  5. Stop the deamon
    $ sudo /etc/init.d/transmission-daemon stop

  6. 45 |
  7. 46 |

    Configure
    $ sudo nano /etc/transmission-daemon/settings.json

    47 | 48 |

    You may also use vi if you prefer, or any other editor.
    $ sudo vi /etc/transmission-daemon/settings.json

    49 | 50 |

    You should at least change these settings:

    51 | 52 |
      53 |
    • 54 | download-dir to some generic download directory such as ~/Downloads (lumus will move the files to destination directories from here),
    • 55 |
    • 56 | incomplete-dir to some generic incomplete downloads directory such as ~/Downloading,
    • 57 |
    • 58 | incomplete-dir-enabled to true,
    • 59 |
    • 60 | rpc-enabled to true.
    • 61 |
    62 |
  8. 63 |

64 | Node.js

65 | 66 |

This is a required step so that the application can run.

67 | 68 |

sudo apt-get install nodejs npm
69 | Answer Y if asked.

70 | 71 |

72 | Lumus

73 | 74 |
    75 |
  1. Clone lumus sources
    git clone https://github.com/ziacik/lumus.git

  2. 76 |
  3. Go to the lumus directory 77 | cd lumus

  4. 78 |
  5. Install dependencies
    npm install

  6. 79 |
  7. Run
    node app.js

  8. 80 |
  9. Browse to configure
    http://localhost:3001

  10. 81 |

82 | Notes

83 | 84 |
    85 |
  • This howto is incomplete.
  • 86 |
  • This project is a work in progress.
  • 87 |
  • This tool does not download any content. It only provides a convenient search interface to transmission client.
  • 88 |
89 |
90 | 91 | 105 |
106 |
107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /jobs/checker.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var util = require('util'); 3 | var config = require('../config'); 4 | var torrenter = require('./torrenter'); 5 | var searcher = require('./searcher'); 6 | var renamer = require('./renamer'); 7 | var subtitler = require('./subtitler'); 8 | var notifier = require('./notifier'); 9 | var nexter = require('./nexter'); 10 | var kodi = require('../notifiers/kodi'); 11 | var opensubtitler = require('../subtitlers/opensubtitler'); 12 | var tpbSearcher = require('../searchers/tpbSearcher'); 13 | //var kickassSearcher = require('../searchers/kickassSearcher'); 14 | 15 | var Item = require('../models/item').Item; 16 | var ItemStates = require('../models/item').ItemStates; 17 | var ItemTypes = require('../models/item').ItemTypes; 18 | 19 | var configure = function() { 20 | var configuration = config.get(); 21 | 22 | if (configuration.notifier.kodi.use) { 23 | notifier.use(kodi); 24 | } else { 25 | notifier.unuse(kodi); 26 | } 27 | 28 | if (configuration.subtitler.opensubtitler.use) { 29 | subtitler.use(opensubtitler); 30 | } else { 31 | subtitler.unuse(opensubtitler); 32 | } 33 | 34 | if (configuration.searcher.tpbSearcher.use) { 35 | searcher.use(tpbSearcher); 36 | } else { 37 | searcher.unuse(tpbSearcher); 38 | } 39 | 40 | // if (configuration.searcher.kickassSearcher.use) { 41 | // searcher.use(kickassSearcher); 42 | // } else { 43 | // searcher.unuse(kickassSearcher); 44 | // } 45 | }; 46 | 47 | function isMusic(item) { 48 | return item.type === ItemTypes.music; 49 | } 50 | 51 | function isMovie(item) { 52 | return item.type === ItemTypes.movie; 53 | } 54 | 55 | function isShow(item) { 56 | return item.type === ItemTypes.show; 57 | } 58 | 59 | var finish = function(item) { 60 | item.state = ItemStates.finished; 61 | delete item.searchResults; 62 | return item.save(); 63 | } 64 | 65 | function isAfterSubtitlerRetryLimit(item) { 66 | return false; //TODO Or? 67 | } 68 | 69 | function check() { 70 | return checkActive().then(checkFinished).catch(function(err) { 71 | util.error(err.stack || err); 72 | }); 73 | } 74 | 75 | function checkOne(item) { 76 | if (item.state === ItemStates.wanted) { 77 | return searcher.findAndAdd(item); 78 | } else if (item.state === ItemStates.snatched) { 79 | return torrenter.checkFinished(item); 80 | } else if (item.state === ItemStates.downloaded) { 81 | return renamer.rename(item); 82 | } else if (item.state === ItemStates.renamed) { 83 | return notifier.updateLibrary(item); 84 | } else if (item.state === ItemStates.libraryUpdated) { 85 | if (isMusic(item)) { 86 | return finish(item); 87 | } else { 88 | return subtitler.findSubtitles(item); 89 | } 90 | } else if (item.state === ItemStates.subtitlerFailed) { 91 | if (isAfterSubtitlerRetryLimit(item)) { 92 | return finish(item); 93 | } else { 94 | return subtitler.findSubtitles(item); 95 | } 96 | } else if (item.state === ItemStates.subtitled) { 97 | return finish(item); 98 | } else { 99 | throw new Error('Invalid state ' + item.state); 100 | } 101 | }; 102 | 103 | function checkActive() { 104 | return Item.find({ 105 | state : { 106 | $nin : [ItemStates.finished, ItemStates.renameFailed, ItemStates.libraryUpdateFailed] 107 | }, 108 | $not : { 109 | nextCheck : { $gt : new Date().toJSON() } 110 | } 111 | }).then(function(items) { 112 | var itemCheckers = items.map(function(item) { 113 | return Q.fcall(checkOne, item); 114 | }); 115 | 116 | return Q.allSettled(itemCheckers); /// Or should I run it sequentially? Probably yes. 117 | }).then(function() { 118 | setTimeout(check, config.get().checkInterval * 1000); 119 | }).catch(function(error) { 120 | util.error(error); 121 | setTimeout(check, config.get().checkInterval * 1000); 122 | }); 123 | } 124 | 125 | function checkFinished() { 126 | return checkFinishedToRemove().then(checkFinishedToNext); 127 | } 128 | 129 | function checkFinishedToNext() { 130 | return Item.find({ 131 | state : ItemStates.finished, 132 | type : { $in : [ItemTypes.show] }, 133 | next : { $exists : false}, 134 | $not : { nextCheck : { $gt : new Date().toJSON() } } 135 | }).then(function(items) { 136 | return Q.allSettled(items.map(function(item) { 137 | return nexter.checkNext(item); 138 | })); 139 | }).catch(function(error) { 140 | util.error(error.stack || error); 141 | }); 142 | } 143 | 144 | function checkFinishedToRemove() { 145 | if (!config.get().removeFinishedDays) { 146 | return Q(); 147 | } 148 | 149 | var now = new Date(); 150 | var deleteDate = new Date(now); 151 | deleteDate.setDate(now.getDate() - config.get().removeFinishedDays); 152 | 153 | return Item.find({state : ItemStates.finished, createdAt : {$lt : deleteDate.toJSON()}}) 154 | .then(function(items) { 155 | return Q.allSettled(items.map(function(item) { 156 | return item.remove(); 157 | })); 158 | }).catch(function(error) { 159 | util.error(error.stack || error); 160 | }); 161 | } 162 | 163 | configure(); 164 | 165 | module.exports = check; 166 | module.exports.configure = configure; -------------------------------------------------------------------------------- /jobs/decorator.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var config = require('../config'); 3 | var subtitler = require('./subtitler'); 4 | var Item = require('../models/item').Item; 5 | var ItemStates = require('../models/item').ItemStates; 6 | var ItemTypes = require('../models/item').ItemTypes; 7 | 8 | var getDecoratorFunction = function(item) { 9 | if (item.type === ItemTypes.movie) { 10 | return movieDecorator; 11 | } else if (item.type === ItemTypes.show) { 12 | return showDecorator; 13 | } else if (item.type === ItemTypes.music) { 14 | return musicDecorator; 15 | } 16 | }; 17 | 18 | module.exports.all = function(item, results) { 19 | if (item.type === ItemTypes.music) { 20 | return doDecoration(item, results); 21 | } else { 22 | return subtitler.listSubtitles(item).then(function(subtitles) { 23 | return doDecoration(item, results, subtitles); 24 | }); 25 | } 26 | }; 27 | 28 | var doDecoration = function(item, results, subtitles) { 29 | var decoratorFunction = getDecoratorFunction(item); 30 | 31 | var promises = results.map(function(result) { 32 | return Q.fcall(decoratorFunction, item, result, subtitles); 33 | }); 34 | 35 | return Q.all(promises).then(function(dummy) { 36 | return Q(results); 37 | }); 38 | } 39 | 40 | var descriptionChecker = function(result) { 41 | return Q.when(result.getDescription()); 42 | }; 43 | 44 | var hasDigitalAudio = function(description) { 45 | var digitalAudioKeywords = ['DTS', 'AC3', 'AC-3']; 46 | for (var i = 0; i < digitalAudioKeywords.length; i++) { 47 | isGoodKeywords = description.indexOf(digitalAudioKeywords[i]) >= 0; 48 | if (isGoodKeywords) 49 | break; 50 | } 51 | 52 | return isGoodKeywords; 53 | }; 54 | 55 | var isHD = function(result, description) { 56 | var isFake720 = /(720\s*[x*]\s*[1-6][0-9][0-9])|(width[^\w]*720)/i.test(description) 57 | 58 | if (isFake720) { 59 | return false; 60 | } 61 | 62 | if (/(720)|(1080)/.test(result.title)) { 63 | return true; 64 | } 65 | 66 | if (/(720)|(1080)/.test(description)) { 67 | return true; 68 | } 69 | 70 | return false; 71 | }; 72 | 73 | var getScore = function(result, setting, points) { 74 | var preference = config.get()[result.type + 'Settings'][setting + 'Preference']; 75 | 76 | if (preference === config.Preference.optional) { 77 | return 0; 78 | } 79 | 80 | var capitalizedSetting = setting.charAt(0).toUpperCase() + setting.substring(1); 81 | 82 | var shouldHaveIt = preference === config.Preference.required || preference === config.Preference.preferred; 83 | var haveIt = result['has' + capitalizedSetting] || result['is' + capitalizedSetting] || false; 84 | 85 | return (haveIt === shouldHaveIt) ? points : 0; 86 | }; 87 | 88 | var releaseNameMatcher = function(releaseName) { 89 | return function(subtitleRecord) { 90 | if (!releaseName || !subtitleRecord.MovieReleaseName) { 91 | return false; 92 | } 93 | 94 | var lowerCaseReleaseName = releaseName.toLowerCase(); 95 | var subtitleReleaseName = subtitleRecord.MovieReleaseName.toLowerCase(); 96 | 97 | if (subtitleReleaseName === lowerCaseReleaseName) { 98 | return true; 99 | } 100 | 101 | if (stripSceneTags(subtitleReleaseName) === stripSceneTags(lowerCaseReleaseName)) { 102 | return true; 103 | } 104 | 105 | return false; 106 | }; 107 | }; 108 | 109 | var stripSceneTags = function(releaseName) { 110 | return releaseName.replace(/[.](limited|proper|internal|festival)([^a-zA-Z]|$)/, '\$2'); 111 | }; 112 | 113 | var movieDecorator = showDecorator = function(item, result, subtitles) { 114 | result.hasSubtitles = subtitles.some(releaseNameMatcher(result.releaseName)); 115 | 116 | return descriptionChecker(result).then(function(description) { 117 | result.type = item.type; 118 | result.hasDigitalAudio = hasDigitalAudio(description); 119 | result.hasHdVideo = isHD(result, description); 120 | result.score = result.hasSubtitles ? 1 : 0; 121 | result.score += result.verified ? 1 : 0; 122 | result.score += getScore(result, 'digitalAudio', 3); 123 | result.score += getScore(result, 'hdVideo', 3); 124 | }); 125 | }; 126 | 127 | var musicDecorator = function(item, result) { 128 | result.type = item.type; 129 | result.isLosslessFormat = (/FLAC/i).test(result.title); 130 | result.score = getScore(result, 'losslessFormat', 1); 131 | return result; 132 | }; -------------------------------------------------------------------------------- /jobs/filter.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var config = require('../config'); 3 | var Item = require('../models/item').Item; 4 | var ItemStates = require('../models/item').ItemStates; 5 | var ItemTypes = require('../models/item').ItemTypes; 6 | 7 | var getFilterFunction = function(item) { 8 | if (item.type === ItemTypes.movie) { 9 | return movieFilter; 10 | } else if (item.type === ItemTypes.show) { 11 | return showFilter; 12 | } else if (item.type === ItemTypes.music) { 13 | return musicFilter; 14 | } 15 | }; 16 | 17 | module.exports.first = function(item, results) { 18 | if (results.length === 0) { 19 | return undefined; 20 | } 21 | 22 | var filterFunction = getFilterFunction(item); 23 | 24 | var deferred = Q.defer(); 25 | var resultIndex = 0; 26 | 27 | function loop() { 28 | var result = results[resultIndex]; 29 | resultIndex++; 30 | 31 | Q.when(filterFunction(item, result), function(isSuccess) { 32 | if (isSuccess) { 33 | deferred.resolve(result); 34 | } else if (resultIndex >= results.length) { 35 | deferred.resolve(undefined); 36 | } else { 37 | loop(); 38 | } 39 | }, function(error) { 40 | deferred.reject(error); 41 | }); 42 | } 43 | 44 | Q.nextTick(loop); 45 | 46 | return deferred.promise; 47 | } 48 | 49 | module.exports.all = function(item, results) { 50 | var filterFunction = getFilterFunction(item); 51 | var promises = results.map(function(result) { 52 | return Q.fcall(filterFunction, item, result); 53 | }); 54 | return Q.all(promises).then(function(dummy) { 55 | return Q(results); 56 | }); 57 | }; 58 | 59 | var isUsedAlready = function(item, magnetLink) { 60 | if (!item.torrentLinks) { 61 | return false; 62 | } 63 | 64 | for (i = 0; i < item.torrentLinks.length; i++) { 65 | if (item.torrentLinks[i] === magnetLink) { 66 | return true; 67 | } 68 | } 69 | 70 | return false; 71 | }; 72 | 73 | var genericFilter = function(item, result) { 74 | if (isUsedAlready(item, result.magnetLink)) { 75 | result.info = 'Already used.'; 76 | return false; 77 | } 78 | 79 | if (result.seeds == 0) { 80 | result.info = 'No seeders.'; 81 | return false; 82 | } 83 | 84 | if (!yearFilter(item, result)) { 85 | result.info = 'Wrong year.'; 86 | return false; 87 | } 88 | 89 | return true; 90 | } 91 | 92 | var digitalAudioFilter = function(result) { 93 | if (!result.hasDigitalAudio && config.get()[result.type + 'Settings'].digitalAudioPreference === config.Preference.required) { 94 | result.info = 'Doesn\'t have digital audio.'; 95 | return false; 96 | } 97 | 98 | if (result.hasDigitalAudio && config.get()[result.type + 'Settings'].digitalAudioPreference === config.Preference.unwanted) { 99 | result.info = 'Has digital audio.'; 100 | return false; 101 | } 102 | 103 | return true; 104 | }; 105 | 106 | var hdVideoFilter = function(result) { 107 | if (!result.hasHdVideo && config.get()[result.type + 'Settings'].hdVideoPreference === config.Preference.required) { 108 | result.info = 'Is not HD.'; 109 | return false; 110 | } 111 | 112 | if (result.hasHdVideo && config.get()[result.type + 'Settings'].hdVideoPreference === config.Preference.unwanted) { 113 | result.info = 'Is HD.'; 114 | return false; 115 | } 116 | 117 | return true; 118 | }; 119 | 120 | var losslessFilter = function(result) { 121 | if (!result.isLosslessFormat && config.get().musicSettings.losslessFormatPreference === config.Preference.required) { 122 | result.info = 'Is not lossless.'; 123 | return false; 124 | } 125 | 126 | if (result.isLosslessFormat && config.get().musicSettings.losslessFormatPreference === config.Preference.unwanted) { 127 | result.info = 'Is lossless.'; 128 | return false; 129 | } 130 | 131 | return true; 132 | }; 133 | 134 | 135 | var yearFilter = function(item, result) { 136 | if (!item.year) { 137 | return true; 138 | } 139 | 140 | var yearMatch = result.title.match(/[^0-9](((19)|(20))[0-9][0-9])[^0-9]/); 141 | 142 | if (yearMatch && yearMatch[1] != item.year) { 143 | return false; 144 | } 145 | 146 | return true; 147 | }; 148 | 149 | var movieFilter = function(item, result) { 150 | if (!genericFilter(item, result)) { 151 | return false; 152 | } 153 | 154 | if (result.size > config.get().movieSettings.maxSize) { 155 | result.info = 'Size exceeded the limit.'; 156 | return false; 157 | } 158 | 159 | if (!digitalAudioFilter(result)) { 160 | return false; 161 | } 162 | 163 | if (!hdVideoFilter(result)) { 164 | return false; 165 | } 166 | 167 | return true; 168 | }; 169 | 170 | var showFilter = function(item, result) { 171 | if (!genericFilter(item, result)) { 172 | return false; 173 | } 174 | 175 | var correctSeasonRegex = new RegExp('season\\W*0*' + item.no + '(?![0-9])', 'i'); 176 | var isCorrectSeason = correctSeasonRegex.test(result.title); 177 | 178 | if (!isCorrectSeason) { 179 | result.info = 'Wrong season.'; 180 | return false; 181 | } 182 | 183 | if (result.size > config.get().showSettings.maxSize) { 184 | result.info = 'Size exceeded the limit.'; 185 | return false; 186 | } 187 | 188 | if (!digitalAudioFilter(result)) { 189 | return false; 190 | } 191 | 192 | if (!hdVideoFilter(result)) { 193 | return false; 194 | } 195 | 196 | return true; 197 | }; 198 | 199 | var musicFilter = function(item, result) { 200 | if (!genericFilter(item, result)) { 201 | return false; 202 | } 203 | 204 | if (result.size > config.get().musicSettings.maxSize) { 205 | result.info = 'Size exceeded the limit.'; 206 | return false; 207 | } 208 | 209 | 210 | if (!losslessFilter(result)) { 211 | return false; 212 | } 213 | 214 | return true; 215 | }; -------------------------------------------------------------------------------- /jobs/nexter.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var config = require('../config'); 3 | var searcher = require('./searcher'); 4 | var Item = require('../models/item').Item; 5 | var ItemStates = require('../models/item').ItemStates; 6 | var ItemTypes = require('../models/item').ItemTypes; 7 | 8 | module.exports.checkNext = function(item) { 9 | if (item.next) { 10 | return Q(); 11 | } 12 | 13 | if (item.type === ItemTypes.show) { 14 | return checkNextShow(item); 15 | } else if (item.type === ItemTypes.music) { 16 | return checkNextMusic(item); 17 | } 18 | 19 | return Q(); 20 | }; 21 | 22 | var checkNextShow = function(item) { 23 | var itemNext = new Item(); 24 | itemNext.name = item.name; 25 | itemNext.type = item.type; 26 | itemNext.externalId = item.externalId; 27 | itemNext.no = +item.no + 1; //FIXME why original no is a string 28 | 29 | return searcher.findIfExists(itemNext).then(function(result) { 30 | if (result) { 31 | item.next = itemNext.no; 32 | return item.save(); 33 | } else { 34 | item.rescheduleNextDay(); 35 | } 36 | }); 37 | }; 38 | 39 | var checkNextMusic = function(item) { 40 | item.rescheduleNextDay(); 41 | return Q(); 42 | }; -------------------------------------------------------------------------------- /jobs/notifier.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var ItemStates = require('../models/item').ItemStates; 4 | var labels = require('../labels'); 5 | var serviceDispatcher = require('./serviceDispatcher'); 6 | 7 | labels.add({ notifier : ' Notification & Library' }); 8 | 9 | module.exports = new serviceDispatcher.ServiceDispatcher(); 10 | 11 | module.exports.notifySnatched = function(item) { 12 | return this.forAll(function(service) { 13 | return service.notifySnatched(item); 14 | }); 15 | } 16 | 17 | module.exports.notifyDownloaded = function(item) { 18 | return this.forAll(function(service) { 19 | return service.notifyDownloaded(item); 20 | }); 21 | } 22 | 23 | module.exports.updateLibrary = function(item) { 24 | return this.forAll(function(service) { 25 | return service.updateLibrary(item).then(function() { 26 | item.state = ItemStates.libraryUpdated; 27 | return item.save(); 28 | }); 29 | }).catch(function(error) { 30 | util.error(error.stack || error); 31 | item.stateInfo = error.message || error; 32 | item.state = ItemStates.libraryUpdateFailed; 33 | item.rescheduleNextHour(); 34 | }); 35 | } -------------------------------------------------------------------------------- /jobs/renamer.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var util = require('util'); 3 | var config = require('../config'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var mkdirp = require('mkdirp'); 7 | var tvdb = new (require("node-tvdb"))("6E61D6699D0B1CB0"); 8 | var ItemTypes = require('../models/item').ItemTypes; 9 | var ItemStates = require('../models/item').ItemStates; 10 | 11 | module.exports.rename = function(item) { 12 | util.log("Renaming " + item.name); 13 | 14 | var itemName = item.name; 15 | var promise; 16 | 17 | if (item.type === ItemTypes.show && item.externalId) { 18 | promise = getShowNameAndRename(item); 19 | } else { 20 | promise = renameTo(item, item.name); 21 | } 22 | 23 | return promise.catch(function(error) { 24 | util.error(error.stack || error); 25 | item.stateInfo = error.message || error; 26 | item.state = ItemStates.renameFailed; 27 | item.save(); 28 | }); 29 | } 30 | 31 | function doRename(item, destinationDir) { 32 | var rmdir = Q.denodeify(require('rimraf')); 33 | var mkdir = Q.denodeify(require('mkdirp')); 34 | var rename = Q.denodeify(fs.rename); 35 | 36 | var promise = 37 | mkdir(destinationDir) 38 | .then(function() { 39 | return rename(item.downloadDir, destinationDir).fail(function(error) { 40 | if (error.code === 'ENOTEMPTY') { 41 | return rmdir(destinationDir).then(function() { 42 | return rename(item.downloadDir, destinationDir); 43 | }); 44 | } else { 45 | throw error; 46 | } 47 | }); 48 | }).then(function() { 49 | item.renamedDir = destinationDir; 50 | item.state = ItemStates.renamed; 51 | return item.save(); 52 | }); 53 | 54 | return promise; 55 | } 56 | 57 | function renameTo(item, itemName) { 58 | var destinationDir = path.join(config.get()[item.type + 'Settings'].destinationDir, itemName); 59 | 60 | if (item.type === ItemTypes.show) 61 | destinationDir = path.join(destinationDir, 'Season ' + item.no); 62 | 63 | return doRename(item, destinationDir); 64 | } 65 | 66 | function getShowNameAndRename(item) { 67 | return Q.nbind(tvdb.getSeriesByRemoteId, tvdb)(item.externalId).then(function(response) { 68 | var itemName = item.name; 69 | 70 | if (response.SeriesName) { 71 | itemName = response.SeriesName; 72 | } else if (response[0] && response[0].SeriesName) { 73 | itemName = response[0].SeriesName; 74 | } 75 | 76 | return renameTo(item, itemName); 77 | }); 78 | } -------------------------------------------------------------------------------- /jobs/searcher.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Q = require('q'); 3 | var ItemStates = require('../models/item').ItemStates; 4 | var torrenter = require('./torrenter'); 5 | var serviceDispatcher = require('./serviceDispatcher'); 6 | var filter = require('./filter'); 7 | var decorator = require('./decorator'); 8 | var labels = require('../labels'); 9 | 10 | module.exports = new serviceDispatcher.ServiceDispatcher(); 11 | 12 | labels.add({ 13 | searcher : ' Searchers' 14 | }); 15 | 16 | module.exports.findIfExists = function(item) { 17 | return module.exports.findAll(item) 18 | .then(function(results) { 19 | item.searchResults = results; 20 | 21 | if (results.length === 0) { 22 | return Q(false); 23 | } else { 24 | return filter.first(item, results); 25 | } 26 | }).catch(function(errors) { 27 | if (!errors.length) { 28 | errors = [ errors ]; 29 | } 30 | 31 | var stack = errors.map(function(error) { 32 | return error.stack || error.message; 33 | }).join(require('os').EOL); 34 | 35 | var messages = errors.map(function(error) { 36 | return error.message || error; 37 | }).join(', '); 38 | 39 | util.error('Error in searcher. Caused by: ' + stack); 40 | }); 41 | }; 42 | 43 | module.exports.findAndAdd = function(item) { 44 | return module.exports.findAll(item) 45 | .then(function(results) { 46 | item.searchResults = results; 47 | 48 | if (results.length === 0) { 49 | item.stateInfo = "No results."; 50 | } else { 51 | return filter.first(item, results); 52 | } 53 | }).then(function(result) { 54 | if (result) { 55 | return torrenter.add(item, result.magnetLink, result.torrentInfoUrl); 56 | } else { 57 | if (!item.stateInfo) { 58 | item.stateInfo = "No result matched filters."; 59 | } 60 | item.rescheduleNextDay(); 61 | } 62 | }) 63 | .catch(function(errors) { 64 | if (!errors.length) { 65 | errors = [ errors ]; 66 | } 67 | 68 | var stack = errors.map(function(error) { 69 | return error.stack || error.message; 70 | }).join(require('os').EOL); 71 | 72 | var messages = errors.map(function(error) { 73 | return error.message || error; 74 | }).join(', '); 75 | 76 | util.error('Error in searcher. Caused by: ' + stack); 77 | 78 | item.stateInfo = messages; 79 | item.rescheduleNextHour(); 80 | }); 81 | } 82 | 83 | module.exports.findAll = function(item) { 84 | return this.forAll(function(service) { 85 | return service.searchFor(item).then(function(results) { 86 | return decorator.all(item, results); 87 | }).then(function(results) { 88 | return filter.all(item, results); 89 | }).then(function(results) { 90 | results.forEach(function(result) { 91 | result.serviceName = service.name; 92 | }); 93 | return results; 94 | }); 95 | }).then(function(serviceResults) { 96 | var results = serviceResults.reduce(function(previous, current, index, array) { 97 | return previous.concat(current); 98 | }, []); 99 | 100 | results.sort(function(result1, result2) { 101 | if (result1.score != result2.score) { 102 | return result2.score - result1.score; 103 | } 104 | 105 | if (result1.seeds != result2.seeds) { 106 | return result2.seeds - result1.seeds; 107 | } 108 | 109 | if (result1.leechs != result2.leechs) { 110 | return result2.leechs - result1.leechs; 111 | } 112 | 113 | return 0; 114 | }); 115 | 116 | return results; 117 | }); 118 | }; -------------------------------------------------------------------------------- /jobs/serviceDispatcher.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var util = require('util'); 3 | 4 | module.exports.ServiceDispatcher = function() { 5 | this.services = []; 6 | }; 7 | 8 | module.exports.ServiceDispatcher.prototype.use = function(service) { 9 | this.services.push(service); 10 | }; 11 | 12 | module.exports.ServiceDispatcher.prototype.unuse = function(service) { 13 | var serviceIndex = this.services.indexOf(service); 14 | 15 | if (serviceIndex >= 0) { 16 | this.services.splice(serviceIndex, 1); 17 | } 18 | }; 19 | 20 | module.exports.ServiceDispatcher.prototype.forAll = function(command) { 21 | var promises = this.services.map(function(service) { 22 | return Q.fcall(command, service); 23 | }); 24 | return Q.all(promises); 25 | }; 26 | 27 | module.exports.ServiceDispatcher.prototype.untilSuccess = function(command, isSuccess) { 28 | var deferred = Q.defer(); 29 | var serviceIndex = 0; 30 | var services = this.services; 31 | 32 | function loop() { 33 | if (services.length <= serviceIndex) { 34 | return Q.resolve(undefined); 35 | } 36 | 37 | var service = services[serviceIndex]; 38 | serviceIndex++; 39 | 40 | Q.when(command(service), function(result) { 41 | if (result && (!isSuccess || isSuccess(result))) { 42 | deferred.resolve(result); 43 | } else if (serviceIndex < services.length) { 44 | loop(); 45 | } else { 46 | deferred.resolve(undefined); 47 | } 48 | }, deferred.reject); 49 | } 50 | 51 | Q.nextTick(loop); 52 | 53 | return deferred.promise; 54 | } -------------------------------------------------------------------------------- /jobs/subtitler.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var labels = require('../labels'); 3 | var util = require('util'); 4 | var Q = require('q'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var ItemStates = require('../models/item').ItemStates; 8 | 9 | config.add('subtitler', { type : 'literal', store : {'subtitler:languages' : 'eng', 'subtitler:shouldSearchByName' : true}}); 10 | labels.add({ 11 | subtitler : ' Subtitling', 12 | languages : 'Subtitle Languages Use comma to separate multiple languages', 13 | shouldSearchByName : 'Search By File Name if nothing found with exact match' 14 | }); 15 | 16 | module.exports = new (require('./serviceDispatcher').ServiceDispatcher)(); 17 | 18 | function setSubtitlerFail(item, why) { 19 | item.state = ItemStates.subtitlerFailed; 20 | item.stateInfo = why; 21 | item.subtitlerFailCount = 1 + (item.subtitlerFailCount || 0); 22 | item.rescheduleNextDay(); 23 | } 24 | 25 | function setSubtitlerSuccess(item) { 26 | item.state = ItemStates.subtitled; 27 | delete item.stateInfo; 28 | item.save(); 29 | } 30 | 31 | function getPathsForSubtitling(item) { 32 | var readdir = Q.denodeify(fs.readdir); 33 | var extPattern = /[.](avi|mkv|mp4|mpeg4|mpg4|mpg|mpeg|divx|xvid)$/i; 34 | 35 | return readdir(item.renamedDir).then(function(files) { 36 | var relevantFiles = files.filter(function(file) { 37 | return extPattern.test(file); 38 | }); 39 | 40 | var relevantPaths = relevantFiles.map(function(file) { 41 | return path.join(item.renamedDir, file); 42 | }); 43 | 44 | return relevantPaths; 45 | }); 46 | } 47 | 48 | module.exports.listSubtitles = function(item) { 49 | return this.untilSuccess(function(service) { 50 | return service.listSubtitles(item); 51 | }); 52 | }; 53 | 54 | module.exports.findSubtitles = function(item) { 55 | var self = this; 56 | var completeness = 0; 57 | var pathCount = 0; 58 | 59 | return getPathsForSubtitling(item) 60 | .then(function(paths) { 61 | pathCount = paths.length; 62 | 63 | if (paths.length == 0) { 64 | return false; 65 | } 66 | 67 | return self.untilSuccess(function(service) { 68 | return service.findSubtitles(item, paths); 69 | }, function isSuccess(results) { 70 | var isSuccess = true; 71 | 72 | for (var i = results.length - 1; i >= 0; i--) { 73 | if (results[i]) { 74 | paths.splice(i, 1); 75 | completeness++; 76 | } else { 77 | isSuccess = false; 78 | } 79 | } 80 | 81 | return isSuccess; 82 | }); 83 | }).then(function(overallResult) { 84 | if (overallResult) { 85 | setSubtitlerSuccess(item); 86 | } else { 87 | setSubtitlerFail(item, completeness + " of " + pathCount + ' subtitles found'); 88 | } 89 | }).catch(function(error) { 90 | util.error(error.stack || error); 91 | item.stateInfo = error.message || error; 92 | item.rescheduleNextHour(); 93 | }); 94 | }; -------------------------------------------------------------------------------- /jobs/torrenter.js: -------------------------------------------------------------------------------- 1 | var Transmission = require('transmission'); 2 | var Q = require('q'); 3 | 4 | var url = require('url'); 5 | 6 | var path = require('path'); 7 | var config = require('../config'); 8 | var labels = require('../labels'); 9 | var notifier = require('./notifier'); 10 | 11 | var Item = require('../models/item').Item; 12 | var ItemStates = require('../models/item').ItemStates; 13 | 14 | config.add('transmission', { type : 'literal', store : { 'downloader:transmission:url' : 'http://localhost:9091', 'downloader:removeTorrent' : true }}); 15 | labels.add({ 16 | downloader : ' Downloaders', 17 | transmission : 'Transmission', 18 | removeTorrent : 'Remove Torrent from downloader when finished', 19 | 'downloader:transmission:url' : 'Transmission Url' 20 | }); 21 | 22 | var convertErr = function(err) { 23 | if (err.code === 'ECONNREFUSED') { 24 | return new Error('Unable to add item to torrent client. Is it running?'); 25 | } else { 26 | return err; 27 | } 28 | } 29 | 30 | var checkFinished = function(item) { 31 | var deferred = Q.defer(); 32 | var transmission = getTransmission(); 33 | 34 | transmission.get(item.torrentHash, function(err, result) { 35 | if (err) { 36 | deferred.reject(convertErr(err)); 37 | return; 38 | } 39 | 40 | if (result.torrents.length == 0) { 41 | item.state = ItemStates.wanted; 42 | item.save().then(deferred.resolve, deferred.reject); 43 | return; 44 | } 45 | 46 | var torrent = result.torrents[0]; 47 | 48 | if (torrent.isFinished) { 49 | finishItem(item, torrent).then(function() { 50 | if (config.get().downloader.removeTorrent) { 51 | return removeTorrent(item); 52 | } 53 | }).then(deferred.resolve, deferred.reject); 54 | } else { 55 | deferred.resolve(); 56 | }; 57 | }); 58 | 59 | return deferred.promise; 60 | } 61 | 62 | var removeTorrent = function(item, removeData) { 63 | if (!item.torrentHash) { 64 | return Q(undefined); 65 | } 66 | 67 | var transmission = getTransmission(); 68 | var remove = Q.nbind(transmission.remove, transmission); 69 | 70 | return remove([item.torrentHash], removeData) 71 | } 72 | 73 | var finishItem = function(item, torrent) { 74 | var filesInfo = torrent.files; 75 | 76 | var maxLength = 0; 77 | var maxFileInfo; 78 | 79 | for (var i = 0; i < filesInfo.length; i++) { 80 | var fileInfo = filesInfo[i]; 81 | if (fileInfo.length > maxLength) { 82 | maxLength = fileInfo.length; 83 | maxFileInfo = fileInfo; 84 | } 85 | } 86 | 87 | var fileDir = path.dirname(maxFileInfo.name); 88 | var fileName = path.basename(maxFileInfo.name); 89 | 90 | item.downloadDir = path.join(torrent.downloadDir, fileDir); 91 | item.mainFile = fileName; 92 | 93 | item.state = ItemStates.downloaded; 94 | 95 | return item.save().then(function() { 96 | if (notifier) 97 | return notifier.notifyDownloaded(item); 98 | }); 99 | } 100 | 101 | var _transmission; 102 | var _transmissionUrl; 103 | 104 | 105 | var getTransmission = function() { 106 | if (_transmission && config.get().downloader.transmission.url === _transmissionUrl) 107 | return _transmission; 108 | 109 | var transmissionUrl = config.get().downloader.transmission.url; 110 | 111 | var parsedUrl = url.parse(transmissionUrl, false, true); 112 | var transmission = new Transmission({ 113 | host : parsedUrl.hostname, 114 | port : parsedUrl.port 115 | }); 116 | 117 | _transmission = transmission; 118 | _transmissionUrl = transmissionUrl; 119 | 120 | return transmission; 121 | } 122 | 123 | var add = function(item, magnetLink, torrentPageUrl) { 124 | var transmission = getTransmission(); 125 | var deferred = Q.defer(); 126 | 127 | /// Item already has a torrent, remove it first (with data too). 128 | removeTorrent(item, true); 129 | 130 | transmission.addUrl(magnetLink, function(err, result) { 131 | if (err) { 132 | err = convertErr(err); 133 | 134 | item.rescheduleNextHour(); 135 | deferred.reject(err); 136 | 137 | return; 138 | } 139 | 140 | item.state = ItemStates.snatched; 141 | item.stateInfo = null; 142 | item.torrentHash = result.hashString; 143 | item.torrentInfoUrl = torrentPageUrl; 144 | 145 | if (!item.torrentLinks) { 146 | item.torrentLinks = []; 147 | } 148 | 149 | item.torrentLinks.push(magnetLink); 150 | 151 | if (notifier) 152 | notifier.notifySnatched(item); 153 | 154 | console.log('Success. Torrent hash ' + item.torrentHash + '.'); 155 | 156 | item.planNextCheck(1); /// To cancel possible postpone. 157 | 158 | item.save().then(deferred.resolve, deferred.reject); 159 | }); 160 | 161 | return deferred.promise; 162 | }; 163 | 164 | 165 | module.exports.add = add; 166 | module.exports.checkFinished = checkFinished; 167 | module.exports.removeTorrent = removeTorrent; 168 | -------------------------------------------------------------------------------- /labels.js: -------------------------------------------------------------------------------- 1 | var labels = {}; 2 | 3 | module.exports.add = function(translationMap) { 4 | for (var property in translationMap) { 5 | if (translationMap.hasOwnProperty(property)) { 6 | labels[property] = translationMap[property]; 7 | } 8 | } 9 | } 10 | 11 | module.exports.get = function() { 12 | for (var i = 0; i < arguments.length; i++) { 13 | var translation = labels[arguments[i]]; 14 | if (translation) { 15 | return translation; 16 | } 17 | } 18 | return arguments[0]; 19 | } -------------------------------------------------------------------------------- /models/item.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | 3 | var ItemTypes = Object.freeze({ 4 | movie:"movie", 5 | show:"show", 6 | music:"music" 7 | }); 8 | 9 | var ItemTypeIcons = Object.freeze({ 10 | movie:"film", 11 | show:"tv", 12 | music:"music" 13 | }); 14 | 15 | var ItemStates = Object.freeze({ 16 | wanted:"wanted", 17 | snatched:"snatched", 18 | downloaded:"downloaded", 19 | renamed:"renamed", 20 | renameFailed:"renameFailed", 21 | libraryUpdated:"libraryUpdated", 22 | libraryUpdateFailed:"libraryUpdateFailed", 23 | subtitled:"subtitled", 24 | subtitlerFailed:"subtitlerFailed", 25 | finished:"finished" 26 | }); 27 | 28 | var Datastore = require('nedb'); 29 | 30 | if (typeof db == 'undefined') 31 | db = {}; 32 | 33 | db.items = new Datastore({ filename : "items.db", autoload : true }); 34 | db.items.persistence.setAutocompactionInterval(3600000); /// Compact every hour. 35 | 36 | function Item() { 37 | this.name = null; 38 | this.type = null; 39 | this.no = null; 40 | this.year = null; 41 | this.externalId = null; 42 | this.state = ItemStates.wanted; 43 | this.createdAt = new Date().toJSON(); 44 | this.torrentLinks = []; 45 | this.searchTerm = null; 46 | this.searchResults = null; 47 | 48 | Item.setupMethods(this); 49 | } 50 | 51 | Item.setupAggregateMethods = function(items) { 52 | items.hasWaiting = function() { 53 | return items.some(function(item) { 54 | return item.isWaiting(); 55 | }); 56 | }; 57 | 58 | items.hasDownloading = function() { 59 | return items.some(function(item) { 60 | return item.isDownloading(); 61 | }); 62 | }; 63 | 64 | items.hasWaitingForSubtitles = function() { 65 | return items.some(function(item) { 66 | return item.isWaitingForSubtitles(); 67 | }); 68 | }; 69 | 70 | items.hasFinished = function() { 71 | return items.some(function(item) { 72 | return item.isFinished(); 73 | }); 74 | }; 75 | 76 | items.hasFailed = function() { 77 | return items.some(function(item) { 78 | return item.isFailed(); 79 | }); 80 | }; 81 | } 82 | 83 | Item.setupMethods = function(item) { 84 | item.ensureSearchTerm = function() { 85 | if (item.searchTerm) { 86 | return; 87 | } 88 | 89 | if (item.type === ItemTypes.show) { 90 | item.searchTerm = item.name + ' Season ' + item.no + ' Complete'; 91 | } else { 92 | item.searchTerm = item.name; 93 | } 94 | }; 95 | 96 | item.getDisplayName = function() { 97 | if (item.type === ItemTypes.show) 98 | return item.name + " Season " + item.no; 99 | 100 | return item.name; 101 | }; 102 | 103 | item.remove = function() { 104 | var deferred = Q.defer(); 105 | 106 | db.items.remove({_id : item._id}, {}, function(err) { 107 | if (err) { 108 | util.error(err.stack || err); 109 | deferred.reject(err); 110 | } else { 111 | deferred.resolve(); 112 | } 113 | }); 114 | 115 | return deferred.promise; 116 | } 117 | 118 | item.save = function() { 119 | return Item.save(item); 120 | }; 121 | 122 | item.planNextCheck = function(seconds) { 123 | item.nextCheck = new Date(new Date().getTime() + seconds * 1000).toJSON(); 124 | }; 125 | 126 | item.rescheduleNextDay = function() { 127 | item.planNextCheck(60 * 60 * 24); 128 | return item.save(); 129 | }; 130 | 131 | item.rescheduleNextHour = function() { 132 | item.planNextCheck(60 * 60); 133 | return item.save(); 134 | }; 135 | 136 | item.isWaiting = function() { 137 | return item.state === ItemStates.wanted; 138 | }; 139 | 140 | item.isDownloading = function() { 141 | return item.state === ItemStates.snatched; 142 | }; 143 | 144 | item.isWaitingForSubtitles = function() { 145 | return item.state === ItemStates.renamed || item.state === ItemStates.libraryUpdated || item.state === ItemStates.subtitlerFailed || item.state === ItemStates.subtitled; 146 | }; 147 | 148 | item.isFinished = function() { 149 | return item.state === ItemStates.finished; 150 | }; 151 | 152 | item.isFailed = function() { 153 | return item.state === ItemStates.renameFailed || item.state === ItemStates.libraryUpdateFailed; 154 | }; 155 | }; 156 | 157 | Item.save = function(item) { 158 | var deferred = Q.defer(); 159 | 160 | if (item._id) { 161 | db.items.update({_id : item._id}, item, {}, function(err) { 162 | if (err) { 163 | util.error(err.stack || err); 164 | deferred.reject(err); 165 | } else { 166 | deferred.resolve(item); 167 | } 168 | }); 169 | } else { 170 | db.items.insert(item, function(err, newDoc) { 171 | item._id = newDoc._id; 172 | if (err) { 173 | util.error(err.stack || err); 174 | deferred.reject(err); 175 | } else { 176 | deferred.resolve(item); 177 | } 178 | }); 179 | } 180 | 181 | return deferred.promise; 182 | }; 183 | 184 | Item.getAll = function() { 185 | return Item.find({}, { createdAt: -1 }); 186 | }; 187 | 188 | Item.findOne = function(what) { 189 | var deferred = Q.defer(); 190 | 191 | db.items.findOne(what, function(err, item) { 192 | if (err) { 193 | util.error(err.stack || err); 194 | deferred.reject(err); 195 | return; 196 | } 197 | 198 | if (item) { 199 | Item.setupMethods(item); 200 | } 201 | 202 | deferred.resolve(item); 203 | }); 204 | 205 | return deferred.promise; 206 | }; 207 | 208 | Item.find = function(byWhat, sortBy) { 209 | var deferred = Q.defer(); 210 | 211 | var query = db.items.find(byWhat); 212 | 213 | if (sortBy) { 214 | query = query.sort(sortBy); 215 | } 216 | 217 | query.exec(function(err, items) { 218 | if (err) { 219 | util.error(err.stack || err); 220 | deferred.reject(err); 221 | return; 222 | } 223 | 224 | Item.setupAggregateMethods(items); 225 | 226 | items.forEach(function(item) { 227 | Item.setupMethods(item); 228 | }); 229 | 230 | deferred.resolve(items); 231 | }); 232 | 233 | return deferred.promise; 234 | }; 235 | 236 | Item.findById = function(id) { 237 | return Item.findOne({_id : id}); 238 | }; 239 | 240 | Item.removeById = function(id) { 241 | var deferred = Q.defer(); 242 | db.items.remove({_id : id}, function(err) { 243 | if (err) { 244 | deferred.reject(err); 245 | return; 246 | } 247 | deferred.resolve(); 248 | }); 249 | return deferred.promise; 250 | }; 251 | 252 | module.exports.Item = Item; 253 | module.exports.ItemTypes = ItemTypes; 254 | module.exports.ItemStates = ItemStates; 255 | module.exports.ItemTypeIcons = ItemTypeIcons; 256 | -------------------------------------------------------------------------------- /models/music.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb'); 2 | 3 | if (typeof db == 'undefined') 4 | db = {}; 5 | 6 | db.music = new Datastore("music.db"); 7 | db.music.loadDatabase(); 8 | 9 | db.music.persistence.setAutocompactionInterval(3600000); /// Compact every hour. 10 | 11 | function Music() { 12 | this.artist = null; 13 | this.albums = []; 14 | 15 | Music.setupMethods(this); 16 | } 17 | 18 | Music.setupMethods = function(music) { 19 | music.save = function(done) { 20 | if (!done) 21 | throw "Sorry, done callback is required."; //TODO don't require callback 22 | 23 | if (music._id) { 24 | db.music.update({_id : music._id}, music, {}, done); 25 | } else { 26 | db.music.insert(music, function(err, newDoc) { 27 | music._id = newDoc._id; 28 | done(err); 29 | }); 30 | } 31 | }; 32 | 33 | music.planNextCheck = function(seconds) { 34 | music.nextCheck = new Date(new Date().getTime() + seconds * 1000).toJSON(); 35 | }; 36 | }; 37 | 38 | Music.getAll = function(done) { 39 | db.music.find({}, function(err, music) { 40 | if (err) { 41 | console.log(err); 42 | done(err, null); 43 | } 44 | 45 | for (index in music) { 46 | var music = music[index]; 47 | Music.setupMethods(music); 48 | } 49 | 50 | done(null, music); 51 | }); 52 | }; 53 | 54 | Music.findOne = function(what, done) { 55 | db.music.findOne(what, function(err, music) { 56 | if (music) 57 | Music.setupMethods(music); 58 | 59 | done(err, music); 60 | }); 61 | }; 62 | 63 | Music.find = function(byWhat, done) { 64 | db.music.find(byWhat, function(err, music) { 65 | if (err) { 66 | console.log(err); 67 | done(err, null); 68 | } 69 | 70 | for (index in music) { 71 | var music = music[index]; 72 | Music.setupMethods(music); 73 | } 74 | 75 | done(null, music); 76 | }); 77 | }; 78 | 79 | Music.findById = function(id, done) { 80 | Music.findOne({_id : id}, done); 81 | }; 82 | 83 | Music.removeById = function(id, done) { 84 | db.music.remove({_id : id}, done); 85 | }; 86 | 87 | module.exports.Music = Music; -------------------------------------------------------------------------------- /models/show.js: -------------------------------------------------------------------------------- 1 | var Datastore = require('nedb'); 2 | var Q = require('q'); 3 | 4 | if (typeof db == 'undefined') 5 | db = {}; 6 | 7 | db.show = new Datastore("show.db"); 8 | db.show.loadDatabase(); 9 | 10 | db.show.persistence.setAutocompactionInterval(3600000); /// Compact every hour. 11 | 12 | function Show() { 13 | this.name = null; 14 | this.seasons = []; 15 | 16 | Show.setupMethods(this); 17 | } 18 | 19 | Show.setupMethods = function(show) { 20 | show.save = function() { 21 | var deferred = Q.defer(); 22 | 23 | if (show._id) { 24 | db.show.update({_id : show._id}, show, {}, function(err) { 25 | if (err) { 26 | deferred.reject(new Error(err)); 27 | } else { 28 | deferred.resolve(show); 29 | } 30 | }); 31 | } else { 32 | db.show.insert(show, function(err, newDoc) { 33 | if (err) { 34 | deferred.reject(new Error(err)); 35 | } else { 36 | show._id = newDoc._id; 37 | deferred.resolve(show); 38 | } 39 | }); 40 | } 41 | 42 | return deferred.promise; 43 | }; 44 | 45 | show.planNextCheck = function(seconds) { 46 | show.nextCheck = new Date(new Date().getTime() + seconds * 1000).toJSON(); 47 | }; 48 | }; 49 | 50 | Show.getAll = function(done) { 51 | db.show.find({}, function(err, show) { 52 | if (err) { 53 | console.log(err); 54 | done(err, null); 55 | } 56 | 57 | for (index in show) { 58 | var show = show[index]; 59 | Show.setupMethods(show); 60 | } 61 | 62 | done(null, show); 63 | }); 64 | }; 65 | 66 | Show.findOne = function(what) { 67 | var deferred = Q.defer(); 68 | db.show.findOne(what, function (err, show) { 69 | if (err) { 70 | deferred.reject(new Error(err)); 71 | } else { 72 | if (show) { 73 | Show.setupMethods(show) 74 | } 75 | deferred.resolve(show); 76 | } 77 | }); 78 | return deferred.promise; 79 | }; 80 | 81 | Show.find = function(byWhat, done) { 82 | db.show.find(byWhat, function(err, show) { 83 | if (err) { 84 | console.log(err); 85 | done(err, null); 86 | } 87 | 88 | for (index in show) { 89 | var show = show[index]; 90 | Show.setupMethods(show); 91 | } 92 | 93 | done(null, show); 94 | }); 95 | }; 96 | 97 | Show.findById = function(id) { 98 | return Show.findOne({_id : id}); 99 | }; 100 | 101 | Show.removeById = function(id, done) { 102 | db.show.remove({_id : id}, done); 103 | }; 104 | 105 | module.exports.Show = Show; -------------------------------------------------------------------------------- /notifiers/kodi.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var kodi = require('kodi-ws'); 3 | var config = require('../config'); 4 | var labels = require('../labels'); 5 | 6 | var Item = require('../models/item').Item; 7 | var ItemTypes = require('../models/item').ItemTypes; 8 | 9 | config.add('kodi', { type : 'literal', store : {'notifier:kodi:use' : true, 'notifier:kodi:host' : 'localhost', 'notifier:kodi:port' : '9090' }}); 10 | labels.add({ 11 | kodi : 'Kodi', 12 | 'notifier:kodi:url' : 'Kodi Url Deprecated, please use Host and Port to EventServer', 13 | 'notifier:kodi:host' : 'Kodi Host', 14 | 'notifier:kodi:port' : 'Kodi Port EventServer' 15 | }); 16 | 17 | /// Kodi-ws library uses Promise which was introduced in node ~ v0.11 or so, so let's define it for legacy. 18 | if (!global.Promise) { 19 | global.Promise = function(resolveRejectFunction) { 20 | var deferred = Q.defer(); 21 | resolveRejectFunction(deferred.resolve, deferred.reject); 22 | return deferred.promise; 23 | } 24 | } 25 | 26 | module.exports.notifySnatched = function(item) { 27 | return connect().then(function(connection) { 28 | return connection.GUI.ShowNotification('Snatched', item.name, 'info'); 29 | }); 30 | } 31 | 32 | module.exports.notifyDownloaded = function(item) { 33 | return connect().then(function(connection) { 34 | return connection.GUI.ShowNotification('Downloaded', item.name, 'info'); 35 | }); 36 | } 37 | 38 | module.exports.updateLibrary = function(item) { 39 | if (item.type === ItemTypes.music) { 40 | return updateAudioLibrary(item); 41 | } else { 42 | return updateVideoLibrary(item); 43 | } 44 | } 45 | 46 | var updateAudioLibrary = function(item) { 47 | return connect().then(function(connection) { 48 | return connection.AudioLibrary.Scan(); 49 | }); 50 | }; 51 | 52 | var updateVideoLibrary = function(item) { 53 | return connect().then(function(connection) { 54 | return connection.VideoLibrary.Scan(); 55 | }); 56 | }; 57 | 58 | var connect = function() { 59 | var options = config.get().notifier.kodi; 60 | return kodi(options.host, parseInt(options.port)); 61 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lumus", 3 | "author": "František Žiačik ", 4 | "version": "0.6.0", 5 | "description": "Search for movies, shows and music and add them automatically to Transmission when they appear on torrents. Lightweight all-in-one alternative to SickBeard, CouchPotato and Headphones.", 6 | "keywords": [ 7 | "lumus", 8 | "torrent", 9 | "search", 10 | "movies", 11 | "shows", 12 | "music" 13 | ], 14 | "homepage": "http://getlumus.com", 15 | "preferGlobal": true, 16 | "bin": { 17 | "lumus": "./bin/lumus" 18 | }, 19 | "dependencies": { 20 | "cheerio": "*", 21 | "express": "3.4.x", 22 | "jade": "*", 23 | "kodi-ws": "^2.2.0", 24 | "less-middleware": "*", 25 | "mkdirp": "*", 26 | "moment": "2.6.x", 27 | "nconf": "*", 28 | "nedb": "^1.2.0", 29 | "node-tvdb": "~0.4.2", 30 | "npm": "~2.7.1", 31 | "opensubtitles-client": "^2.2.0", 32 | "q": "~1.1.2", 33 | "request": "^2.51.0", 34 | "rimraf": "*", 35 | "thepiratebay": "^0.2.2", 36 | "transmission": "~0.4.0" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git://github.com/ziacik/lumus.git" 41 | }, 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "http://github.com/ziacik/lumus/issues" 45 | }, 46 | "devDependencies": { 47 | "chai": "^2.2.0", 48 | "chai-as-promised": "^4.3.0", 49 | "mocha": "^2.2.1", 50 | "proxyquire": "^1.4.0", 51 | "sinon": "^1.14.1", 52 | "sinon-chai": "^2.7.0" 53 | }, 54 | "scripts": { 55 | "test": "mocha", 56 | "start": "node app.js" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | {"name":"Lumus","tagline":"SickBeard, CouchPotato and Headphones lightweight alternative for automated downloading movies, shows and music with torrents written in node.js.","body":"How to install on Raspbmc / Linux\r\n-----\r\n\r\nFirst we need to install a torrent downloader called [transmission](http://www.transmissionbt.com/). Skip this step if you already have transmission configured.\r\n\r\n### Transmission\r\n\r\nWe only need to install a daemon (service running at background) instead of full-featured client. Thanks to that, less memory will be used.\r\n\r\n1. Install transmission \r\n\t`$ sudo apt-get install transmission-daemon`\r\n\t\r\n2. Set it to run at startup \r\n\t`$ sudo update-rc.d transmission-daemon defaults`\r\n\t\r\n3. Stop the deamon \r\n\t`$ sudo /etc/init.d/transmission-daemon stop`\r\n\t\r\n4. Configure \r\n\t`$ sudo nano /etc/transmission-daemon/settings.json`\r\n\t\r\n\tYou may also use `vi` if you prefer, or any other editor. \r\n\t`$ sudo vi /etc/transmission-daemon/settings.json`\r\n\t\r\n\tYou should at least change these settings:\r\n\t* `download-dir` to some generic download directory such as ~/Downloads (lumus will move the files to destination directories from here),\r\n\t* `incomplete-dir` to some generic incomplete downloads directory such as ~/Downloading,\r\n\t* `incomplete-dir-enabled` to `true`,\r\n\t* `rpc-enabled` to `true`.\r\n\t\r\n### Node.js\r\nThis is a required step so that the application can run.\r\n\r\n`sudo apt-get install nodejs npm` \r\nAnswer `Y` if asked.\r\n\r\n### Lumus\r\n\r\n1. Clone lumus sources \r\n`git clone https://github.com/ziacik/lumus.git`\r\n\r\n2. Go to the lumus directory\r\n`cd lumus`\r\n\r\n3. Install dependencies \r\n`npm install`\r\n\r\n4. Run \r\n`node app.js`\r\n\r\n5. Browse to configure \r\n[http://localhost:3001](http://localhost:3001)\r\n\r\nNotes\r\n-----\r\n\r\n* This howto is incomplete.\r\n* This project is a work in progress.\r\n* This tool does not download any content. It only provides a convenient search interface to transmission client.","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} -------------------------------------------------------------------------------- /public/javascripts/music.js: -------------------------------------------------------------------------------- 1 | function getParameter(name) { 2 | return decodeURIComponent( 3 | (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1] 4 | ); 5 | } 6 | 7 | function showError(e, errorDivId, what) { 8 | var message = e.statusText + ' (' + e.status + ')'; 9 | 10 | if (e.status === 404) 11 | message = what + ' info not found.'; 12 | 13 | $(errorDivId).text(message); 14 | $(errorDivId).show(); 15 | 16 | console.log('Error getting info.'); 17 | console.log(e); 18 | } 19 | 20 | function findAlbums(artistId, artistName) { 21 | $.getJSON("http://musicbrainz.org/ws/2/release-group?artist=" + artistId + "&fmt=json", function(data) { 22 | listAlbums(artistId, artistName, data["release-groups"]); 23 | }).fail(function(e) { 24 | showError(e, '#error', 'Show'); 25 | }); 26 | } 27 | 28 | function sortByYear(results) { 29 | results.sort(function (result1, result2) { 30 | var year1 = result1["first-release-date"]; 31 | year1 = year1.substr(0, year1.indexOf('-')) || year1; 32 | 33 | var year2 = result2["first-release-date"]; 34 | year2 = year2.substr(0, year2.indexOf('-')) || year2; 35 | 36 | result1.Year = year1; 37 | result2.Year = year2; 38 | 39 | return (year1 < year2) ? 1 : -1; 40 | }); 41 | } 42 | 43 | function listAlbums(artistId, artistName, results) { 44 | sortByYear(results); 45 | 46 | $.each(results, function(key, val) { 47 | var name = val.name + (val.disambiguation ? " (" + val.disambiguation + ")" : ""); 48 | 49 | if (val["first-release-date"]) { 50 | var type = val["primary-type"]; 51 | 52 | if (type === "Album") 53 | $(getAlbumItem(artistId, artistName, val)).appendTo("#albums"); 54 | else 55 | $(getAlbumItem(artistId, artistName, val)).appendTo("#other"); 56 | } 57 | }); 58 | } 59 | 60 | function getAlbumItem(artistId, artistName, albumInfo) { 61 | var item = 62 | "

" + 63 | albumInfo.title + " (" + albumInfo["primary-type"] + ", " + albumInfo.Year + ")" + 64 | " " + 65 | "" + 66 | "" + 67 | " " + 68 | "" + 69 | "

"; 70 | return item; 71 | } 72 | 73 | $(document).ready(function(){ 74 | var artistId = getParameter("artistId"); 75 | var artistName = getParameter("name"); 76 | findAlbums(artistId, artistName); 77 | }); 78 | -------------------------------------------------------------------------------- /public/javascripts/search.js: -------------------------------------------------------------------------------- 1 | var templates = []; 2 | var noResultCount = 0; 3 | 4 | function getParameter(name) { 5 | return decodeURIComponent( 6 | (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1] 7 | ); 8 | } 9 | 10 | function showError(which, err) { 11 | stopSpinner(which); 12 | $('#' + which + 'ErrorText').text(' Failed ' + which + ' search: ' + err); 13 | $('#' + which + 'Error').show(); 14 | } 15 | 16 | function stopSpinner(which) { 17 | $('#' + which + 'Spinner').hide(); 18 | } 19 | 20 | function noResult(which) { 21 | if (which) { 22 | $('#' + which).hide(); 23 | noResultCount++; 24 | } 25 | 26 | if (noResultCount === 3) { 27 | $('#noResult').show(); 28 | } 29 | } 30 | 31 | function findShowsAndMovies(what) { 32 | $.getJSON("http://www.omdbapi.com/?i=&s=" + what, function(data) { 33 | stopSpinner('movie'); 34 | stopSpinner('show'); 35 | sortByYear(data.Search); 36 | listMovies(data.Search); 37 | listShows(data.Search); 38 | }).fail(function(jqxhr, textStatus, error) { 39 | showError('movie', textStatus + (error ? ', ' + error : '')); 40 | showError('show', textStatus + (error ? ', ' + error : '')); 41 | }); 42 | } 43 | 44 | function findMusic(what) { 45 | $.getJSON("http://musicbrainz.org/ws/2/artist?query=%22" + what + "%22&fmt=json", function(data) { 46 | stopSpinner('music'); 47 | listArtists(data.artists); 48 | }).fail(function(jqxhr, textStatus, error) { 49 | showError('music', textStatus + (error ? ', ' + error : '')); 50 | }); 51 | } 52 | 53 | function sortByYear(results) { 54 | if (results) { 55 | results.sort(function (result1, result2) { 56 | return (result1.Year < result2.Year) ? 1 : -1; 57 | }); 58 | } 59 | } 60 | 61 | function listMovies(results) { 62 | var hadResult = false; 63 | 64 | if (results) { 65 | var movieResults = $.grep(results, function(result) { 66 | return result.Type === "movie"; 67 | }); 68 | 69 | $.each(movieResults, function(key, val) { 70 | hadResult = true; 71 | $(getItem("movie", val)).appendTo("#movies"); 72 | }); 73 | } 74 | 75 | if (!hadResult) { 76 | noResult('movies'); 77 | } 78 | } 79 | 80 | function listShows(results) { 81 | var hadResult = false; 82 | 83 | if (results) { 84 | var showResults = $.grep(results, function(result) { 85 | return result.Type === "series"; 86 | }); 87 | 88 | $.each(showResults, function(key, val) { 89 | hadResult = true; 90 | $(getItem("show", val)).appendTo("#shows"); 91 | }); 92 | } 93 | 94 | if (!hadResult) { 95 | noResult('shows'); 96 | } 97 | } 98 | 99 | function listArtists(results) { 100 | var hadResult = false; 101 | 102 | if (results) { 103 | $.each(results, function(key, val) { 104 | hadResult = true; 105 | $(getItem("music", val)).appendTo("#music"); 106 | }); 107 | } 108 | 109 | if (!hadResult) { 110 | noResult('music'); 111 | } 112 | } 113 | 114 | function getItem(type, info) { 115 | return templates[type](info); 116 | } 117 | 118 | $(document).ready(function(){ 119 | templates['movie'] = Handlebars.compile($("#movie-template").html()); 120 | templates['show'] = Handlebars.compile($("#show-template").html()); 121 | templates['music'] = Handlebars.compile($("#music-template").html()); 122 | 123 | var what = getParameter("what"); 124 | findShowsAndMovies(what); 125 | findMusic(what); 126 | }); 127 | -------------------------------------------------------------------------------- /public/javascripts/show.js: -------------------------------------------------------------------------------- 1 | function getParameter(name) { 2 | return decodeURIComponent( 3 | (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1] 4 | ); 5 | } 6 | 7 | function showError(e, errorDivId, what) { 8 | var message = e.statusText + ' (' + e.status + ')'; 9 | 10 | if (e.status === 404) 11 | message = what + ' info not found.'; 12 | 13 | $(errorDivId).text(message); 14 | $(errorDivId).show(); 15 | 16 | console.log('Error getting info.'); 17 | console.log(e); 18 | } 19 | 20 | function showInfo(showId) { 21 | $.getJSON("http://api.trakt.tv/show/summary.json/18607462d7b7bfd44d68a4721c732900/" + showId + "?callback=?", function(info) { 22 | $("#poster").attr("src", info.images.poster.replace('.jpg', '-300.jpg')); 23 | $("#basicInfo").text(info.year + ", " + info.runtime + "min" + ", " + info.status); 24 | $("#overview").text(info.overview); 25 | }).fail(function(e) { 26 | showError(e, '#error', 'Show'); 27 | }); 28 | } 29 | 30 | function findSeasons(showId, showName) { 31 | $.getJSON("http://api.trakt.tv/show/seasons.json/18607462d7b7bfd44d68a4721c732900/" + showId + "?callback=?", function(seasonInfos) { 32 | listSeasons(showId, showName, seasonInfos); 33 | }).fail(function(e) { 34 | showError(e, '#error2', 'Season'); 35 | }); 36 | } 37 | 38 | function sort(results) { 39 | results.sort(function (result1, result2) { 40 | return (result1.season < result2.season) ? -1 : 1; 41 | }); 42 | } 43 | 44 | function listSeasons(showId, showName, results) { 45 | sort(results); 46 | 47 | $.each(results, function(key, val) { 48 | $(getSeasonItem(showId, showName, val)).appendTo("#seasons"); 49 | }); 50 | } 51 | 52 | function getSeasonItem(showId, showName, seasonInfo) { 53 | var item = 54 | "

Season " + 55 | seasonInfo.season + " (" + seasonInfo.episodes + " episodes)" + 56 | " " + 57 | "" + 58 | "

"; 59 | return item; 60 | } 61 | 62 | $(document).ready(function(){ 63 | var showId = getParameter("showId"); 64 | var showName = getParameter("name"); 65 | showInfo(showId); 66 | findSeasons(showId, showName); 67 | }); 68 | -------------------------------------------------------------------------------- /public/javascripts/yify.js: -------------------------------------------------------------------------------- 1 | function findYify() { 2 | $.getJSON('https://yts.re/api/list.json?rating=6&sort=date&limit=50', function(data) { //TODO rating configure 3 | listMovies(data.MovieList); 4 | }); 5 | } 6 | 7 | function listMovies(results) { 8 | var usedTitles = []; 9 | 10 | var movieResults = $.grep(results, function(result) { 11 | if (usedTitles.indexOf(result.MovieTitleClean) >= 0) 12 | return false; 13 | 14 | if (result.MovieYear > 2010) { //TODO 15 | usedTitles.push(result.MovieTitleClean); 16 | return true; 17 | } 18 | 19 | return false; 20 | }); 21 | 22 | $.each(movieResults, function(key, val) { 23 | $(getItem(val.MovieTitleClean, val.MovieYear, val.MovieRating, val.Genre, val.MovieUrl, val.CoverImage)).appendTo("#results"); 24 | }); 25 | } 26 | 27 | function getItem(name, year, rating, genre, infoUrl, imageUrl) { 28 | var ref = 'add'; 29 | var yearQuery = '&year=' + year; 30 | var yearInfo = year ? ' (' + year + ')' : ''; 31 | var ratingInfo = rating ? ' (' + rating + ')' : ''; 32 | var genreInfo = genre ? ' (' + genre + ')' : ''; 33 | 34 | var item = 35 | "

" + 36 | "" + name + yearInfo + ratingInfo + genreInfo + "" + 37 | " " + 38 | "" + 39 | "

"; 40 | return item; 41 | } 42 | 43 | $(document).ready(function(){ 44 | findYify(); 45 | }); 46 | -------------------------------------------------------------------------------- /public/stylesheets/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /routes/config.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var labels = require('../labels'); 3 | var checker = require('../jobs/checker'); 4 | 5 | exports.form = function(req, res) { 6 | res.render('config', { 7 | config : config.get(), 8 | labels : labels 9 | }); 10 | }; 11 | 12 | var recursiveUpdate = function(req, configObject, propertyPrefix) { 13 | if (propertyPrefix === undefined) { 14 | propertyPrefix = ''; 15 | } 16 | 17 | for (var propertyName in configObject) { 18 | if (configObject.hasOwnProperty(propertyName)) { 19 | var type = typeof configObject[propertyName]; 20 | var fullPropertyName = propertyPrefix + propertyName; 21 | var postedValue = req.body[fullPropertyName]; 22 | 23 | if (type === 'boolean') { 24 | config.set(fullPropertyName, postedValue === 'on'); 25 | } else if (type === 'number') { 26 | if (postedValue) { 27 | config.set(fullPropertyName, parseInt(postedValue)); 28 | } else { 29 | config.clear(fullPropertyName); 30 | } 31 | } else if (type === 'object') { 32 | recursiveUpdate(req, configObject[propertyName], fullPropertyName + ':'); 33 | } else { 34 | config.set(fullPropertyName, postedValue); 35 | } 36 | } 37 | } 38 | } 39 | 40 | exports.save = function(req, res) { 41 | recursiveUpdate(req, config.get()); 42 | 43 | var isFirstSave = config.get().version === 0; 44 | 45 | config.save().then(function() { 46 | checker.configure(); 47 | 48 | if (isFirstSave) { 49 | res.redirect('/'); 50 | } else { 51 | res.redirect('/config?success'); 52 | } 53 | }).catch(function(error) { 54 | res.render('error', { 55 | error: error 56 | }); 57 | }); 58 | }; -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var util = require('util'); 3 | var Item = require('../models/item').Item; 4 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 5 | var version = require('../helpers/version'); 6 | 7 | /* 8 | * GET home page. 9 | */ 10 | 11 | exports.index = function(req, res){ 12 | if (config.get().version === 0) { 13 | res.redirect('/config'); 14 | } else { 15 | return Item.getAll().then(function(items) { 16 | return version.newVersion().then(function(result) { 17 | res.render('index', { 18 | title : 'lumus', 19 | myVersion : version.myVersion, 20 | newVersion : result, 21 | items : items, 22 | icons : ItemTypeIcons 23 | }); 24 | }).catch(function(error) { 25 | util.error(error.stack || error); 26 | res.render('index', { 27 | title : 'lumus', 28 | myVersion : '?', 29 | newVersion : '?', 30 | items : items, 31 | icons : ItemTypeIcons 32 | }); 33 | }); 34 | }) 35 | .catch(function(error) { 36 | util.error(error.stack || error); 37 | res.render('error', { error: error }); 38 | }); 39 | }; 40 | }; 41 | 42 | exports.update = function(req, res) { 43 | version.update().then(function() { 44 | res.redirect('/'); 45 | }).catch(function(error) { 46 | util.error(error.stack || error); 47 | res.render('error', { error: error }); 48 | }); 49 | }; -------------------------------------------------------------------------------- /routes/item.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Q = require('q'); 3 | var notifier = require('../jobs/notifier'); 4 | var torrenter = require('../jobs/torrenter'); 5 | 6 | var Item = require('../models/item').Item; 7 | var ItemTypes = require('../models/item').ItemTypes; 8 | var ItemStates = require('../models/item').ItemStates; 9 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 10 | 11 | exports.changeState = function(req, res) { 12 | return Item.findById(req.query.id).then(function(item) { 13 | item.stateInfo = undefined; 14 | item.state = req.query.state; //TODO vulnerability (validate) 15 | 16 | if (item.state !== ItemStates.finished) { 17 | item.planNextCheck(1); /// To cancel possible postpone. 18 | } 19 | 20 | if (item.state === ItemStates.finished) { 21 | delete item.searchResults; 22 | } 23 | 24 | return item.save() 25 | .then(function() { 26 | res.redirect('/'); 27 | }); 28 | }) 29 | .catch(function(error) { 30 | util.error(error.stack || error); 31 | res.render('error', { error : error }); 32 | }); 33 | }; 34 | 35 | 36 | exports.remove = function(req, res) { 37 | return Item.findById(req.query.id).then(function(item) { 38 | if (item) { 39 | return Item.removeById(req.query.id).then(function() { 40 | return torrenter.removeTorrent(item, true).then(function() { 41 | return Q(undefined); 42 | }).catch(function(error) { 43 | util.error(error.stack || error); 44 | }); 45 | }); 46 | } 47 | }).then(function() { 48 | res.redirect('/'); 49 | }).catch(function(error) { 50 | util.error(error.stack || error); 51 | res.render('error', { error : error }); 52 | }); 53 | } 54 | 55 | exports.add = function(req, res) { 56 | var item = new Item(); 57 | item.name = req.query.name; 58 | item.type = req.query.type; 59 | item.year = req.query.year; 60 | item.no = req.query.no; 61 | item.externalId = req.query.externalId; 62 | 63 | return item 64 | .save() 65 | .then(function() { 66 | res.redirect('/'); 67 | }) 68 | .catch(function(error) { 69 | util.error(error.stack || error); 70 | res.render('error', { error: error }); 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /routes/list.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var itemRoute = require('./item'); 3 | var Item = require('../models/item').Item; 4 | var ItemTypes = require('../models/item').ItemTypes; 5 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 6 | 7 | exports.list = function(req, res) { 8 | return Item.getAll().then(function(items) { 9 | res.render('list', { 10 | items: items, 11 | icons: ItemTypeIcons 12 | }); 13 | }) 14 | .catch(function(error) { 15 | util.error(error.stack || error); 16 | res.render('error', { 17 | error: error 18 | }); 19 | }); 20 | }; 21 | 22 | exports.add = function(req, res) { 23 | return itemRoute.add(req, res); 24 | }; -------------------------------------------------------------------------------- /routes/music.js: -------------------------------------------------------------------------------- 1 | var Music = require('../models/music').Music; 2 | 3 | exports.add = function(req, res) { 4 | Music.findOne({ artistId : req.query.artistId }, function(err, music) { 5 | if (!music) { 6 | music = new Music(); 7 | music.artist = req.query.name; 8 | music.artistId = req.query.artistId; 9 | 10 | music.save(function(err) { 11 | if (err) { 12 | res.redirect('/error'); //TODO zle, loop 13 | return; 14 | } 15 | 16 | res.redirect('/artist?id=' + music._id + '&artistId=' + music.artistId + '&name=' + music.artist); 17 | }); 18 | } else { 19 | res.redirect('/artist?id=' + music._id + '&artistId=' + music.artistId + '&name=' + music.artist); 20 | } 21 | }); 22 | }; 23 | 24 | exports.artist = function(req, res) { 25 | Music.findById(req.query.id, function(err, item) { 26 | if (err) { 27 | res.render('error', { 28 | error: err 29 | }); 30 | } else { 31 | res.render('artist', { 32 | item: item 33 | }); 34 | } 35 | }); 36 | }; -------------------------------------------------------------------------------- /routes/search.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET search. 4 | */ 5 | 6 | exports.runSearch = function(req, res){ 7 | res.render('search', { title: 'Lumus' }); 8 | }; -------------------------------------------------------------------------------- /routes/show.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var Show = require('../models/show').Show; 3 | var TvdbClient = require("node-tvdb"); 4 | var tvdb = new TvdbClient("6E61D6699D0B1CB0"); 5 | 6 | exports.add = function(req, res) { 7 | Show.findOne({ showId : req.query.showId }) 8 | .then(function(show) { 9 | if (!show) { 10 | show = new Show(); 11 | show.name = req.query.name; 12 | show.showId = req.query.showId; 13 | return show.save(); 14 | } else { 15 | return Q(show); 16 | } 17 | }).then(function(show) { 18 | res.redirect('/show?id=' + show._id + '&showId=' + show.showId + '&name=' + encodeURIComponent(show.name)); 19 | }) 20 | .catch(function(error) { 21 | res.render('error', { 22 | error: error 23 | }); 24 | }); 25 | }; 26 | 27 | exports.show = function(req, res) { 28 | var i; 29 | 30 | Show.findOne({ showId : req.query.showId }) 31 | .then(function(item) { 32 | i = item; 33 | return getShowInfo(item); 34 | }) 35 | .then(function(info) { 36 | reduceShowInfo(info); 37 | res.render('show', { 38 | item: i, 39 | info: info 40 | }); 41 | }) 42 | .catch(function(error) { 43 | res.render('error', { 44 | error: error 45 | }); 46 | }); 47 | }; 48 | 49 | var getShowInfo = function(item) { 50 | console.log('Looking for item ' + item.showId + ' in tvdb.') 51 | return tvdb.getSeriesByRemoteId(item.showId).then(function (info) { 52 | if (!info) { 53 | throw 'Could not find season info about this show.'; 54 | } 55 | return tvdb.getSeriesAllById(info.seriesid); 56 | }); 57 | } 58 | 59 | var reduceShowInfo = function(fullInfo) { 60 | if (fullInfo[0] && fullInfo[0].SeriesName) { 61 | fullInfo = fullInfo[0]; 62 | } 63 | 64 | if (!Array.isArray(fullInfo.Episodes)) { 65 | fullInfo.Episodes = [ fullInfo.Episodes ]; 66 | } 67 | 68 | var seasonInfo = fullInfo.Episodes.reduce(function(info, episode, index, array) { 69 | if (episode.SeasonNumber !== '0') { 70 | var seasonItem = findByNo(info, episode.SeasonNumber); 71 | var episodeYear = getYear(episode.FirstAired); 72 | 73 | if (!seasonItem) { 74 | seasonItem = { 75 | No : episode.SeasonNumber, 76 | Year : episodeYear 77 | }; 78 | 79 | info.push(seasonItem); 80 | } else { 81 | if (seasonItem.Year > episodeYear) { 82 | seasonItem.Year = episodeYear; 83 | } 84 | } 85 | } 86 | 87 | return info; 88 | }, []); 89 | 90 | fullInfo.seasons = seasonInfo; 91 | delete fullInfo.Episodes; 92 | } 93 | 94 | var findByNo = function(data, no) { 95 | for (var i = 0; i < data.length; i++) { 96 | if (data[i].No == no) { 97 | return data[i]; 98 | } 99 | } 100 | 101 | return null; 102 | } 103 | 104 | var getYear = function(date) { 105 | if (!date) { 106 | return 'unknown'; 107 | } else { 108 | return date.substr(0, date.indexOf('-')); 109 | } 110 | } -------------------------------------------------------------------------------- /routes/torrent.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Item = require('../models/item').Item; 3 | var searcher = require('../jobs/searcher'); 4 | var torrenter = require('../jobs/torrenter'); 5 | 6 | exports.list = function(req, res){ 7 | return Item.findById(req.query.id).then(function(item) { 8 | if (!item) { 9 | res.send(404); 10 | } else { 11 | var searchNowClicked = req.query.reset || req.query.what; 12 | 13 | if (req.query.reset) { 14 | delete item.searchTerm; 15 | } else if (req.query.what && item.searchTerm !== req.query.what) { 16 | item.searchTerm = req.query.what; 17 | } 18 | 19 | if (item.searchResults && !searchNowClicked) { 20 | res.render('torrents', { item : item, results : item.searchResults, searchTerm : item.searchTerm || item.name }); 21 | } else { 22 | return searcher 23 | .findAll(item) 24 | .then(function(decoratedResults) { 25 | item.searchResults = decoratedResults; 26 | item.save(); 27 | res.render('torrents', { item : item, results : decoratedResults, searchTerm : item.searchTerm || item.name }); 28 | }); 29 | }; 30 | } 31 | }) 32 | .catch(function(error) { 33 | util.error(error.stack || error); 34 | res.render('error', { error : error }); 35 | }); 36 | }; 37 | 38 | exports.add = function(req, res) { 39 | return Item.findById(req.query.id).then(function(item) { 40 | return torrenter 41 | .add(item, req.query.magnet, req.query.page) 42 | .then(function() { 43 | res.redirect('/'); 44 | }); 45 | }) 46 | .catch(function(error) { 47 | util.error(error.stack || error); 48 | res.render('error', { error : error }); 49 | }); 50 | } -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET users listing. 4 | */ 5 | 6 | exports.list = function(req, res){ 7 | res.send("respond with a resource"); 8 | }; -------------------------------------------------------------------------------- /searchers/isohuntSearcher.js: -------------------------------------------------------------------------------- 1 | var nodeIsohunt = require('node-isohunt'); 2 | 3 | var searchFor = function(item, callback) { 4 | console.log('Searching for ' + item.name + ' with isohunt'); 5 | var opts = { 6 | ihq : item.name, 7 | start : 1, // Optional. Starting row number in paging through results set. 8 | // First page have start=1, not 0. Defaults to 1. 9 | rows : 3, // Optional. Results to return, starting from parameter "start". 10 | // Defaults to 100. Upper limit of 100. 11 | sort : 'seeds', // Optional. Defaults to composite ranking (over all factors 12 | // such as age, query relevance, seed/leechers counts and 13 | // votes). Parameter takes only values of "seeds", "age" or 14 | // "size", where seeds sorting is combination of 15 | // seeds+leechers. Sort order defaults to descending. 16 | order : 'desc' // Optional, can be either "asc" or "desc". Defaults to 17 | // descending, in conjunction with sort parameter. 18 | }; 19 | // obs: start+rows have maximum possible limit of 1000. 20 | 21 | nodeIsohunt(opts, function(data) { 22 | console.log('Done searching for ' + item.name + ' with isohunt'); 23 | 24 | callback(data); 25 | /*var t = data.items.list.map(function(each) { 26 | return { 27 | title : each.title, 28 | link : each.link, 29 | torrentUrl : each.enclosure_url, 30 | size : each.size 31 | }; 32 | }); 33 | 34 | console.log(t);*/ 35 | //TODO: ERR? 36 | }, function(err) { 37 | callback(undefined, err); 38 | }); 39 | } 40 | 41 | module.exports.searchFor = searchFor; -------------------------------------------------------------------------------- /searchers/kickassSearcher.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var request = require('request'); 3 | var cheerio = require('cheerio'); 4 | var config = require('../config'); 5 | var labels = require('../labels'); 6 | 7 | var kat = require('kat-api'); 8 | var ItemTypes = require('../models/item').ItemTypes; 9 | 10 | module.exports.name = 'Kickass Torrents'; 11 | 12 | config.add('kickassSearcher', { type : 'literal', store : {'searcher:kickassSearcher:use' : true }}); 13 | //TODO url 14 | labels.add({ kickassSearcher : module.exports.name }); 15 | 16 | 17 | module.exports.searchFor = function(item) { 18 | return kat.search({ 19 | query : getSearchTerm(item), 20 | category : getCategory(item), 21 | sort_by : 'seeders', 22 | order : 'desc' 23 | }).then(function(data) { 24 | return data.results.map(convertDataItemToResult); 25 | }).catch(function(err) { 26 | if (err.message === 'No results' || err.message === 'No data') { 27 | return []; 28 | } else { 29 | throw err; 30 | } 31 | }); 32 | }; 33 | 34 | var getSearchTerm = function(item) { 35 | item.ensureSearchTerm(); 36 | return item.searchTerm.replace(/-/g, '"-"'); 37 | }; 38 | 39 | var convertDataItemToResult = function(dataItem) { 40 | var result = {}; 41 | result.title = dataItem.title; 42 | result.magnetLink = dataItem.magnet; 43 | result.torrentInfoUrl = dataItem.link; 44 | result.size = dataItem.size / 1048576 | 0; 45 | result.seeds = dataItem.seeds; 46 | result.leechs = dataItem.leechs; 47 | result.verified = 1 === dataItem.verified; 48 | result.releaseName = guessReleaseName(dataItem); 49 | result.getDescription = function() { 50 | return getDescription(dataItem.link); 51 | }; 52 | return result; 53 | }; 54 | 55 | var guessReleaseName = function(dataItem) { 56 | return dataItem.title.replace(/\s*[.]?\[[a-zA-Z]+\]\s*$/, ''); 57 | }; 58 | 59 | var getDescription = function(link) { 60 | var deferred = Q.defer(); 61 | 62 | request({ 63 | method: 'GET', 64 | uri: link, 65 | gzip: true 66 | }, function(error, response, body) { 67 | if (error) { 68 | return deferred.reject(error); 69 | } 70 | 71 | $ = cheerio.load(body); 72 | var description = $('#desc').text(); 73 | deferred.resolve(description); 74 | }); 75 | 76 | return deferred.promise; 77 | } 78 | 79 | var getCategory = function(item) { 80 | if (item.type === ItemTypes.movie) { 81 | return "movies"; 82 | } else if (item.type === ItemTypes.show) { 83 | return "tv"; 84 | } else if (item.type === ItemTypes.music) { 85 | return "music"; 86 | } 87 | }; -------------------------------------------------------------------------------- /searchers/tpbSearcher.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var request = require('request'); 3 | var cheerio = require('cheerio'); 4 | var config = require('../config'); 5 | var labels = require('../labels'); 6 | 7 | var tpb = require('thepiratebay'); 8 | var ItemTypes = require('../models/item').ItemTypes; 9 | 10 | module.exports.name = 'The Pirate Bay'; 11 | 12 | config.add('tpbSearcher', { 13 | type : 'literal', 14 | store : { 15 | 'searcher:tpbSearcher:use' : true, 16 | 'searcher:tpbSearcher:url' : 'https://thepiratebay.la' 17 | } 18 | }); 19 | 20 | labels.add({ 21 | tpbSearcher : module.exports.name, 22 | 'searcher:tpbSearcher:url' : 'Base URL' 23 | }); 24 | 25 | module.exports.searchFor = function(item) { 26 | var searchTerm = getSearchTerm(item); 27 | 28 | tpb.setUrl(config.get().searcher.tpbSearcher.url); 29 | 30 | return tpb.search(searchTerm, { 31 | category : getCategory(item), 32 | orderBy : '7' 33 | }).then(function(searchResults) { 34 | return searchResults.map(convertDataItemToResult); 35 | }); 36 | }; 37 | 38 | var getSearchTerm = function(item) { 39 | item.ensureSearchTerm(); 40 | return item.searchTerm.replace(/-/g, '"-"'); 41 | }; 42 | 43 | var convertSize = function(sizeStr) { 44 | var sizeMatches = sizeStr.match(/([0-9]+[.]?[0-9]*)[^0-9]+(KiB|MiB|GiB)/); // todo rewise 45 | 46 | if (sizeMatches == null || sizeMatches.length < 3) { 47 | throw 'Unable to determine size from "' + sizeStr + '".'; 48 | } 49 | 50 | var size = parseFloat(sizeMatches[1]); 51 | 52 | if (sizeMatches[2] === 'GiB') 53 | size = size * 1000; 54 | else if (sizeMatches[2] === 'KiB') 55 | size = size / 1000; 56 | 57 | return size; 58 | }; 59 | 60 | var convertDataItemToResult = function(dataItem) { 61 | var result = {}; 62 | result.title = dataItem.name; 63 | result.magnetLink = dataItem.magnetLink; 64 | result.torrentInfoUrl = dataItem.link; 65 | result.size = convertSize(dataItem.size); 66 | result.seeds = dataItem.seeders; 67 | result.leechs = dataItem.leechers; 68 | result.releaseName = guessReleaseName(dataItem); 69 | result.getDescription = function() { 70 | return getDescription(dataItem.link); 71 | }; 72 | return result; 73 | }; 74 | 75 | var guessReleaseName = function(dataItem) { 76 | return dataItem.name.replace(/\s*[.]?\[[a-zA-Z]+\]\s*$/, ''); 77 | }; 78 | 79 | var getDescription = function(link) { 80 | var deferred = Q.defer(); 81 | 82 | request({ 83 | method: 'GET', 84 | uri: link, 85 | gzip: true 86 | }, function(error, response, body) { 87 | if (error) { 88 | return deferred.reject(error); 89 | } 90 | 91 | $ = cheerio.load(body); 92 | var description = $('.nfo').text(); 93 | deferred.resolve(description); 94 | }); 95 | 96 | return deferred.promise; 97 | } 98 | 99 | var getCategory = function(item) { 100 | if (item.type === ItemTypes.movie) { 101 | if (config.get().movieSettings.hdVideoPreference === config.Preference.required) { 102 | return "207"; 103 | } else { 104 | return "200"; 105 | } 106 | } else if (item.type === ItemTypes.show) { 107 | if (config.get().showSettings.hdVideoPreference === config.Preference.required) { 108 | return "208"; 109 | } else if (config.get().showSettings.hdVideoPreference === config.Preference.unwanted) { 110 | return "205"; 111 | } else { 112 | return "200"; 113 | } 114 | } else if (item.type === ItemTypes.music) { 115 | if (config.get().musicSettings.losslessFormatPreference === config.Preference.required) { 116 | return "104"; 117 | } else { 118 | return "100"; 119 | } 120 | } 121 | }; -------------------------------------------------------------------------------- /subtitlers/opensubtitler.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var util = require('util'); 3 | var path = require('path'); 4 | var config = require('../config'); 5 | var labels = require('../labels'); 6 | 7 | var openSubtitles = require('opensubtitles-client'); 8 | var Item = require('../models/item').Item; 9 | var ItemStates = require('../models/item').ItemStates; 10 | var ItemTypes = require('../models/item').ItemTypes; 11 | 12 | config.add('opensubtitler', { type : 'literal', store : {'subtitler:opensubtitler:use' : true}}); 13 | labels.add({ opensubtitler : 'OpenSubtitles.org' }); 14 | 15 | module.exports.name = 'OpenSubtitles.org'; 16 | 17 | module.exports.findSubtitles = function(item, filePaths) { 18 | return openSubtitles.api.login() 19 | .then(function(token) { 20 | 21 | var promises = filePaths.map(function(filePath) { 22 | return Q.fcall(findSubtitlesOne, token, item, filePath); 23 | }); 24 | 25 | return Q.all(promises).then(function(results) { 26 | openSubtitles.api.logout(token).done(); 27 | return results; 28 | }); 29 | }); 30 | }; 31 | 32 | function findSubtitlesOne(token, item, filePath) { 33 | var deferred = Q.defer(); 34 | 35 | openSubtitles.api.searchForFile(token, config.get().subtitler.languages, filePath) 36 | .then(function(results) { 37 | if (results && results.length) { 38 | return results; 39 | } else if (config.get().subtitler.shouldSearchByName) { 40 | var fileName = path.basename(filePath); 41 | return openSubtitles.api.searchForTag(token, config.get().subtitler.languages, fileName); 42 | } 43 | }).then(function(results) { 44 | if (results && results.length) { 45 | var downloaderDomain = require('domain').create() 46 | downloaderDomain.on('error', function(err){ 47 | console.error('Subtlter download error.', err.stack || err.message); 48 | deferred.reject(err); 49 | }); 50 | 51 | downloaderDomain.run(function() { 52 | openSubtitles.downloader.download(results, 1, filePath, function() { 53 | deferred.resolve(true); 54 | }); 55 | }); 56 | } else { 57 | deferred.resolve(false); 58 | } 59 | }).catch(function(error) { 60 | deferred.reject(error); 61 | }); 62 | 63 | return deferred.promise; 64 | } 65 | 66 | module.exports.listSubtitles = function(item) { 67 | var token; 68 | var imdbId = parseInt(item.externalId.replace('tt', '')); 69 | 70 | return openSubtitles.api.login() 71 | .then(function(tok) { 72 | token = tok; 73 | return openSubtitles.api.search(token, config.get().subtitler.languages, { imdbid : imdbId }); 74 | }).then(function(results) { 75 | openSubtitles.api.logout(token).done(); 76 | return results; 77 | }).catch(function(error) { 78 | var newError = new Error('Error listing subtitles'); 79 | newError.causedBy = error; 80 | throw newError; 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /test/jobs.checker.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var sinon = require('sinon'); 4 | var sinonChai = require("sinon-chai"); 5 | 6 | var Q = require('q'); 7 | var Item = require('../models/item').Item; 8 | var ItemStates = require('../models/item').ItemStates; 9 | 10 | var config = require('../config'); 11 | var checker = require('../jobs/checker'); 12 | var subtitler = require('../jobs/subtitler'); 13 | var searcher = require('../jobs/searcher'); 14 | var torrenter = require('../jobs/torrenter'); 15 | var renamer = require('../jobs/renamer'); 16 | var notifier = require('../jobs/notifier'); 17 | 18 | chai.should(); 19 | chai.use(chaiAsPromised); 20 | chai.use(sinonChai); 21 | 22 | subtitler.hasSubtitlesForName = function() { 23 | return Q(true); 24 | } 25 | 26 | var staticConfig = config.get(); 27 | 28 | config.get = function() { 29 | return staticConfig; 30 | } 31 | 32 | describe('checker', function() { 33 | var clock; 34 | 35 | beforeEach(function () { 36 | config.get().removeFinishedDays = 0; 37 | clock = sinon.useFakeTimers(); 38 | }); 39 | 40 | afterEach(function () { 41 | clock.restore(); 42 | }); 43 | 44 | it('should search for wanted movie', function() { 45 | searcher.findAndAdd = sinon.spy(); 46 | Item.find = function() { 47 | return Q([{ type : 'movie', state : ItemStates.wanted }]); 48 | }; 49 | return checker().should.be.fulfilled.then(function() { 50 | return searcher.findAndAdd.should.have.been.called; 51 | }); 52 | }); 53 | 54 | it('should search for wanted show', function() { 55 | searcher.findAndAdd = sinon.spy(); 56 | Item.find = function() { 57 | return Q([{ type : 'show', state : ItemStates.wanted }]); 58 | }; 59 | return checker().should.be.fulfilled.then(function() { 60 | return searcher.findAndAdd.should.have.been.called; 61 | }); 62 | }); 63 | 64 | it('should search for wanted music', function() { 65 | searcher.findAndAdd = sinon.spy(); 66 | Item.find = function() { 67 | return Q([{ type : 'music', state : ItemStates.wanted }]); 68 | }; 69 | return checker().should.be.fulfilled.then(function() { 70 | return searcher.findAndAdd.should.have.been.called; 71 | }); 72 | }); 73 | 74 | it('should check if torrent finished for snatched movie', function() { 75 | torrenter.checkFinished = sinon.spy(); 76 | var item = { type : 'movie', state : ItemStates.snatched }; 77 | Item.find = function() { 78 | return Q([item]); 79 | }; 80 | return checker().should.be.fulfilled.then(function() { 81 | return torrenter.checkFinished.should.have.been.calledWith(item); 82 | }); 83 | }); 84 | 85 | it('should check if torrent finished for snatched show', function() { 86 | torrenter.checkFinished = sinon.spy(); 87 | var item = { type : 'show', state : ItemStates.snatched }; 88 | Item.find = function() { 89 | return Q([item]); 90 | }; 91 | return checker().should.be.fulfilled.then(function() { 92 | return torrenter.checkFinished.should.have.been.calledWith(item); 93 | }); 94 | }); 95 | 96 | it('should check if torrent finished for snatched music', function() { 97 | torrenter.checkFinished = sinon.spy(); 98 | var item = { type : 'music', state : ItemStates.snatched }; 99 | Item.find = function() { 100 | return Q([item]); 101 | }; 102 | return checker().should.be.fulfilled.then(function() { 103 | return torrenter.checkFinished.should.have.been.calledWith(item); 104 | }); 105 | }); 106 | 107 | it('should rename downloaded movie', function() { 108 | renamer.rename = sinon.spy(); 109 | var item = { type : 'movie', state : ItemStates.downloaded }; 110 | Item.find = function() { 111 | return Q([item]); 112 | }; 113 | return checker().should.be.fulfilled.then(function() { 114 | return renamer.rename.should.have.been.calledWith(item); 115 | }); 116 | }); 117 | 118 | it('should rename downloaded show', function() { 119 | renamer.rename = sinon.spy(); 120 | var item = { type : 'show', state : ItemStates.downloaded }; 121 | Item.find = function() { 122 | return Q([item]); 123 | }; 124 | return checker().should.be.fulfilled.then(function() { 125 | return renamer.rename.should.have.been.calledWith(item); 126 | }); 127 | }); 128 | 129 | it('should rename downloaded music', function() { 130 | renamer.rename = sinon.spy(); 131 | var item = { type : 'music', state : ItemStates.downloaded }; 132 | Item.find = function() { 133 | return Q([item]); 134 | }; 135 | return checker().should.be.fulfilled.then(function() { 136 | return renamer.rename.should.have.been.calledWith(item); 137 | }); 138 | }); 139 | 140 | it('should update library for renamed movie', function() { 141 | notifier.updateLibrary = sinon.spy(); 142 | var item = { type : 'movie', state : ItemStates.renamed }; 143 | Item.find = function() { 144 | return Q([item]); 145 | }; 146 | return checker().should.be.fulfilled.then(function() { 147 | return notifier.updateLibrary.should.have.been.calledWith(item); 148 | }); 149 | }); 150 | 151 | it('should update library for renamed show', function() { 152 | notifier.updateLibrary = sinon.spy(); 153 | var item = { type : 'show', state : ItemStates.renamed }; 154 | Item.find = function() { 155 | return Q([item]); 156 | }; 157 | return checker().should.be.fulfilled.then(function() { 158 | return notifier.updateLibrary.should.have.been.calledWith(item); 159 | }); 160 | }); 161 | 162 | it('should update library for renamed music', function() { 163 | notifier.updateLibrary = sinon.spy(); 164 | var item = { type : 'music', state : ItemStates.renamed }; 165 | Item.find = function() { 166 | return Q([item]); 167 | }; 168 | return checker().should.be.fulfilled.then(function() { 169 | return notifier.updateLibrary.should.have.been.calledWith(item); 170 | }); 171 | }); 172 | 173 | it('should find subtitles for libraryUpdated movie', function() { 174 | subtitler.findSubtitles = sinon.spy(); 175 | var item = { type : 'movie', state : ItemStates.libraryUpdated }; 176 | Item.find = function() { 177 | return Q([item]); 178 | }; 179 | return checker().should.be.fulfilled.then(function() { 180 | return subtitler.findSubtitles.should.have.been.calledWith(item); 181 | }); 182 | }); 183 | 184 | it('should find subtitles for libraryUpdated show', function() { 185 | subtitler.findSubtitles = sinon.spy(); 186 | var item = { type : 'show', state : ItemStates.libraryUpdated }; 187 | Item.find = function() { 188 | return Q([item]); 189 | }; 190 | return checker().should.be.fulfilled.then(function() { 191 | return subtitler.findSubtitles.should.have.been.calledWith(item); 192 | }); 193 | }); 194 | 195 | it('should finish libraryUpdated music', function() { 196 | var item = { type : 'music', state : ItemStates.libraryUpdated }; 197 | item.save = sinon.spy(); 198 | Item.find = function() { 199 | return Q([item]); 200 | }; 201 | return checker().should.be.fulfilled.then(function() { 202 | return item.save.should.have.been.called; 203 | }); 204 | }); 205 | 206 | it('should finish subtitled movie', function() { 207 | var item = { type : 'movie', state : ItemStates.subtitled }; 208 | item.save = sinon.spy(); 209 | Item.find = function() { 210 | return Q([item]); 211 | }; 212 | return checker().should.be.fulfilled.then(function() { 213 | return item.save.should.have.been.called; 214 | }); 215 | }); 216 | 217 | it('should finish subtitled show', function() { 218 | var item = { type : 'show', state : ItemStates.subtitled }; 219 | item.save = sinon.spy(); 220 | Item.find = function() { 221 | return Q([item]); 222 | }; 223 | return checker().should.be.fulfilled.then(function() { 224 | return item.save.should.have.been.called; 225 | }); 226 | }); 227 | 228 | it('should remove finished item if configured', function() { 229 | var item = { type : 'show', state : ItemStates.subtitled }; 230 | config.get().removeFinishedDays = 1; 231 | item.remove = sinon.spy(); 232 | Item.find = function() { 233 | return Q([item]); 234 | }; 235 | return checker().should.be.fulfilled.then(function() { 236 | return item.remove.should.have.been.called; 237 | }); 238 | }); 239 | 240 | it('should schedule next check on success', function() { 241 | var item = { type : 'show', state : ItemStates.subtitled, save : function() {} }; 242 | var count = 0; 243 | Item.find = function() { 244 | count++; 245 | return Q([item]); 246 | }; 247 | return checker().should.be.fulfilled.then(function() { 248 | clock.tick(config.get().checkInterval * 1000); 249 | return count.should.equal(3); 250 | }); 251 | }); 252 | 253 | it('should schedule next check on error', function() { 254 | var errThrown = false; 255 | var item = { type : 'show', state : ItemStates.subtitled, save : function() { errThrown = true; throw new Error('Err'); } }; 256 | var count = 0; 257 | Item.find = function() { 258 | count++; 259 | return Q([item]); 260 | }; 261 | return checker().should.be.fulfilled.then(function() { 262 | clock.tick(config.get().checkInterval * 1000); 263 | errThrown.should.equal(true); 264 | return count.should.equal(3); 265 | }); 266 | }); 267 | 268 | it('should schedule next check on no items', function() { 269 | var count = 0; 270 | Item.find = function() { 271 | count++; 272 | return Q([]); 273 | }; 274 | return checker().should.be.fulfilled.then(function() { 275 | clock.tick(config.get().checkInterval * 1000); 276 | return count.should.equal(3); 277 | }); 278 | }); 279 | 280 | it('should not schedule next check too early', function() { 281 | var item = { type : 'show', state : ItemStates.subtitled, save : function() {} }; 282 | var count = 0; 283 | Item.find = function() { 284 | count++; 285 | return Q([item]); 286 | }; 287 | return checker().should.be.fulfilled.then(function() { 288 | clock.tick(config.get().checkInterval * 1000 - 1); 289 | return count.should.equal(2); 290 | }); 291 | }); 292 | 293 | }); -------------------------------------------------------------------------------- /test/jobs.decorator.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var extend = require('util')._extend; 4 | 5 | var Q = require('q'); 6 | 7 | var config = require('../config'); 8 | var decorator = require('../jobs/decorator') 9 | var subtitler = require('../jobs/subtitler'); 10 | 11 | chai.use(chaiAsPromised); 12 | chai.should(); 13 | 14 | subtitler.listSubtitles = function() { 15 | return Q([{ MovieReleaseName: 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo' }]); 16 | } 17 | 18 | var staticConfig = config.get(); 19 | 20 | config.get = function() { 21 | return staticConfig; 22 | } 23 | 24 | describe('decorator', function() { 25 | before(function() { 26 | config.get().movieSettings.digitalAudioPreference = config.Preference.required; 27 | config.get().movieSettings.hdVideoPreference = config.Preference.preferred; 28 | config.get().showSettings.digitalAudioPreference = config.Preference.optional; 29 | config.get().showSettings.hdVideoPreference = config.Preference.preferred; 30 | }), 31 | it('should correctly decorate hd ac3 movie', function() { 32 | var item = { type : 'movie', externalId : 'tt1727587' }; 33 | 34 | var results = [{ 35 | title : '.720p', 36 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 37 | verified : true, 38 | getDescription : function() { 39 | return 'Test AC-3 something'; 40 | } 41 | }]; 42 | 43 | var expected = extend({ 44 | type : 'movie', 45 | hasHdVideo : true, 46 | hasDigitalAudio : true, 47 | hasSubtitles : true, 48 | score : 8 49 | }, results[0]); 50 | 51 | return decorator.all(item, results).should.become([expected]); 52 | }); 53 | 54 | it('should correctly decorate fake hd movie', function() { 55 | var item = { type : 'movie', externalId : 'tt1727587' }; 56 | 57 | var results = [{ 58 | title : 'Sintel.720p', 59 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 60 | verified : false, 61 | getDescription : function() { 62 | return 'Test 720 x 400 something'; 63 | } 64 | }]; 65 | 66 | var expected = extend({ 67 | type : 'movie', 68 | hasHdVideo : false, 69 | hasDigitalAudio : false, 70 | hasSubtitles : true, 71 | score : 1 72 | }, results[0]); 73 | 74 | return decorator.all(item, results).should.become([expected]); 75 | }); 76 | 77 | it('should correctly decorate fake hd movie 2', function() { 78 | var item = { type : 'movie', externalId : 'tt1727587' }; 79 | 80 | var results = [{ 81 | title : 'Sintel.720p', 82 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 83 | verified : false, 84 | getDescription : function() { 85 | return 'Test width 720px ' + 86 | 'Height 300px something'; 87 | } 88 | }]; 89 | 90 | var expected = extend({ 91 | type : 'movie', 92 | hasHdVideo : false, 93 | hasDigitalAudio : false, 94 | hasSubtitles : true, 95 | score : 1 96 | }, results[0]); 97 | 98 | return decorator.all(item, results).should.become([expected]); 99 | }); 100 | 101 | it('should correctly decorate hd dts show', function() { 102 | var item = { type : 'show', externalId : 'tt1727587' }; 103 | 104 | var results = [{ 105 | title : 'Sintel.720p', 106 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 107 | verified : true, 108 | getDescription : function() { 109 | return 'Test DTS something'; 110 | } 111 | }]; 112 | 113 | var expected = extend({ 114 | type : 'show', 115 | hasHdVideo : true, 116 | hasDigitalAudio : true, 117 | hasSubtitles : true, 118 | score : 5 119 | }, results[0]); 120 | 121 | return decorator.all(item, results).should.become([expected]); 122 | }); 123 | 124 | it('should correctly decorate fake hd show', function() { 125 | var item = { type : 'show', externalId : 'tt1727587' }; 126 | 127 | var results = [{ 128 | title : 'Sintel.1080p', 129 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 130 | verified : false, 131 | getDescription : function() { 132 | return 'Test 720 x 600 something'; 133 | } 134 | }]; 135 | 136 | var expected = extend({ 137 | type : 'show', 138 | hasHdVideo : false, 139 | hasDigitalAudio : false, 140 | hasSubtitles : true, 141 | score : 1 142 | }, results[0]); 143 | 144 | return decorator.all(item, results).should.become([expected]); 145 | }); 146 | 147 | it('should correctly decorate fake hd show 2', function() { 148 | var item = { type : 'show', externalId : 'tt1727587' }; 149 | 150 | var results = [{ 151 | title : 'Sintel.1080p', 152 | releaseName : 'Sintel.2010.HDRip.XviD.AC3-ViSiON_MaCo', 153 | verified : false, 154 | getDescription : function() { 155 | return 'Test width 720 ' + 156 | 'Height 300px something'; 157 | } 158 | }]; 159 | 160 | var expected = extend({ 161 | type : 'show', 162 | hasHdVideo : false, 163 | hasDigitalAudio : false, 164 | hasSubtitles : true, 165 | score : 1 166 | }, results[0]); 167 | 168 | return decorator.all(item, results).should.become([expected]); 169 | }); 170 | 171 | it('should correctly decorate mp3', function() { 172 | var item = { type : 'music' }; 173 | 174 | var results = [{ 175 | title : 'Sintel', 176 | verified : true, 177 | getDescription : function() { 178 | return 'Test FLAC something'; 179 | } 180 | }]; 181 | 182 | var expected = extend({ 183 | type : 'music', 184 | isLosslessFormat : false, 185 | score : 0 186 | }, results[0]); 187 | 188 | return decorator.all(item, results).should.become([expected]); 189 | }); 190 | 191 | it('should correctly decorate mp3 when preferred', function() { 192 | var item = { type : 'music' }; 193 | config.get().musicSettings.losslessFormatPreference = config.Preference.disfavoured; 194 | 195 | var results = [{ 196 | title : 'Sintel', 197 | verified : true, 198 | getDescription : function() { 199 | return 'Test FLAC something'; 200 | } 201 | }]; 202 | 203 | var expected = extend({ 204 | type : 'music', 205 | isLosslessFormat : false, 206 | score : 1 207 | }, results[0]); 208 | 209 | return decorator.all(item, results).should.become([expected]); 210 | }); 211 | 212 | it('should correctly decorate flac when preferred', function() { 213 | var item = { type : 'music' }; 214 | config.get().musicSettings.losslessFormatPreference = config.Preference.preferred; 215 | 216 | var results = [{ 217 | title : 'Sintel.FLAC', 218 | verified : true, 219 | getDescription : function() { 220 | return 'Test FLAC something'; 221 | } 222 | }]; 223 | 224 | var expected = extend({ 225 | type : 'music', 226 | isLosslessFormat : true, 227 | score : 1 228 | }, results[0]); 229 | 230 | return decorator.all(item, results).should.become([expected]); 231 | }); 232 | 233 | it('should correctly decorate flac when unwanted', function() { 234 | var item = { type : 'music' }; 235 | config.get().musicSettings.losslessFormatPreference = config.Preference.unwanted; 236 | 237 | var results = [{ 238 | title : 'Sintel.FLAC', 239 | verified : true, 240 | getDescription : function() { 241 | return 'Test FLAC something'; 242 | } 243 | }]; 244 | 245 | var expected = extend({ 246 | type : 'music', 247 | isLosslessFormat : true, 248 | score : 0 249 | }, results[0]); 250 | 251 | return decorator.all(item, results).should.become([expected]); 252 | }); 253 | }); -------------------------------------------------------------------------------- /test/jobs.filter.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var extend = require('util')._extend; 4 | 5 | var Q = require('q'); 6 | 7 | var config = require('../config'); 8 | var filter = require('../jobs/filter') 9 | var subtitler = require('../jobs/subtitler'); 10 | 11 | chai.use(chaiAsPromised); 12 | chai.should(); 13 | 14 | subtitler.hasSubtitlesForName = function() { 15 | return Q(true); 16 | } 17 | 18 | var staticConfig = config.get(); 19 | 20 | config.get = function() { 21 | return staticConfig; 22 | } 23 | 24 | describe('filter', function() { 25 | before(function() { 26 | config.get().movieSettings.digitalAudioPreference = config.Preference.optional; 27 | config.get().movieSettings.hdVideoPreference = config.Preference.optional; 28 | }), 29 | 30 | it('should remove non-hd movie when required', function() { 31 | config.get().movieSettings.hdVideoPreference = config.Preference.required; 32 | var item = { type : 'movie' }; 33 | 34 | var results = [{ 35 | type : 'movie', 36 | title : 'Something.NotHd', 37 | hasHdVideo : false, 38 | hasDigitalAudio : true, 39 | verified : true, 40 | getDescription : function() { 41 | return 'Test AC-3 something'; 42 | } 43 | }]; 44 | 45 | return filter.first(item, results).should.become(undefined); 46 | }); 47 | 48 | it('should not remove non-hd movie when not required', function() { 49 | config.get().movieSettings.hdVideoPreference = config.Preference.preferred; 50 | var item = { type : 'movie' }; 51 | 52 | var results = [{ 53 | type : 'movie', 54 | title : 'Something.NotHd', 55 | hasHdVideo : false, 56 | hasDigitalAudio : true, 57 | verified : true, 58 | getDescription : function() { 59 | return 'Test AC-3 something'; 60 | } 61 | }]; 62 | 63 | return filter.first(item, results).should.become(results[0]); 64 | }); 65 | 66 | it('should remove hd movie when unwanted', function() { 67 | config.get().movieSettings.hdVideoPreference = config.Preference.unwanted; 68 | var item = { type : 'movie' }; 69 | 70 | var results = [{ 71 | type : 'movie', 72 | title : 'Something.NotHd', 73 | hasHdVideo : true, 74 | hasDigitalAudio : true, 75 | verified : true, 76 | getDescription : function() { 77 | return 'Test AC-3 something'; 78 | } 79 | }]; 80 | 81 | return filter.first(item, results).should.become(undefined); 82 | }); 83 | 84 | it('should not remove hd movie when not unwanted', function() { 85 | config.get().movieSettings.hdVideoPreference = config.Preference.optional; 86 | var item = { type : 'movie' }; 87 | 88 | var results = [{ 89 | type : 'movie', 90 | title : 'Something.NotHd', 91 | hasHdVideo : true, 92 | hasDigitalAudio : true, 93 | verified : true, 94 | getDescription : function() { 95 | return 'Test AC-3 something'; 96 | } 97 | }]; 98 | 99 | return filter.first(item, results).should.become(results[0]); 100 | }); 101 | 102 | it('should remove non-dts movie when required', function() { 103 | config.get().movieSettings.digitalAudioPreference = config.Preference.required; 104 | 105 | var item = { type : 'movie' }; 106 | 107 | var results = [{ 108 | type : 'movie', 109 | title : 'Something.NotHd', 110 | hasHdVideo : false, 111 | hasDigitalAudio : false, 112 | verified : true, 113 | getDescription : function() { 114 | return 'Test AC-3 something'; 115 | } 116 | }]; 117 | 118 | return filter.first(item, results).should.become(undefined); 119 | }); 120 | 121 | it('should not remove non-dts movie when not required', function() { 122 | config.get().movieSettings.digitalAudioPreference = config.Preference.preferred; 123 | 124 | var item = { type : 'movie' }; 125 | 126 | var results = [{ 127 | type : 'movie', 128 | title : 'Something.NotHd', 129 | hasHdVideo : false, 130 | hasDigitalAudio : true, 131 | verified : true, 132 | getDescription : function() { 133 | return 'Test AC-3 something'; 134 | } 135 | }]; 136 | 137 | return filter.first(item, results).should.become(results[0]); 138 | }); 139 | 140 | it('should remove dts movie when unwanted', function() { 141 | config.get().movieSettings.digitalAudioPreference = config.Preference.unwanted; 142 | 143 | var item = { type : 'movie' }; 144 | 145 | var results = [{ 146 | type : 'movie', 147 | title : 'Something.NotHd', 148 | hasHdVideo : false, 149 | hasDigitalAudio : true, 150 | verified : true, 151 | getDescription : function() { 152 | return 'Test AC-3 something'; 153 | } 154 | }]; 155 | 156 | return filter.first(item, results).should.become(undefined); 157 | }); 158 | 159 | it('should not remove dts movie when not unwanted', function() { 160 | config.get().movieSettings.digitalAudioPreference = config.Preference.disfavoured; 161 | 162 | var item = { type : 'movie' }; 163 | 164 | var results = [{ 165 | type : 'movie', 166 | title : 'Something.NotHd', 167 | hasHdVideo : false, 168 | hasDigitalAudio : true, 169 | verified : true, 170 | getDescription : function() { 171 | return 'Test AC-3 something'; 172 | } 173 | }]; 174 | 175 | return filter.first(item, results).should.become(results[0]); 176 | }); 177 | 178 | it('should remove non-lossless music when required', function() { 179 | config.get().musicSettings.losslessFormatPreference = config.Preference.required; 180 | 181 | var item = { type : 'music' }; 182 | 183 | var results = [{ 184 | type : 'music', 185 | title : 'Something.NotHd', 186 | isLosslessFormat : false, 187 | verified : true, 188 | getDescription : function() { 189 | return 'Test AC-3 something'; 190 | } 191 | }]; 192 | 193 | return filter.first(item, results).should.become(undefined); 194 | }); 195 | 196 | it('should not remove non-lossless music when not required', function() { 197 | config.get().musicSettings.losslessFormatPreference = config.Preference.preferred; 198 | 199 | var item = { type : 'music' }; 200 | 201 | var results = [{ 202 | type : 'music', 203 | title : 'Something.NotHd', 204 | isLosslessFormat : false, 205 | verified : true, 206 | getDescription : function() { 207 | return 'Test AC-3 something'; 208 | } 209 | }]; 210 | 211 | return filter.first(item, results).should.become(results[0]); 212 | }); 213 | 214 | it('should remove lossless music when unwanted', function() { 215 | config.get().musicSettings.losslessFormatPreference = config.Preference.unwanted; 216 | 217 | var item = { type : 'music' }; 218 | 219 | var results = [{ 220 | type : 'music', 221 | title : 'Something.NotHd', 222 | isLosslessFormat : true, 223 | verified : true, 224 | getDescription : function() { 225 | return 'Test AC-3 something'; 226 | } 227 | }]; 228 | 229 | return filter.first(item, results).should.become(undefined); 230 | }); 231 | 232 | it('should not remove lossless music when not unwanted', function() { 233 | config.get().musicSettings.losslessFormatPreference = config.Preference.disfavoured; 234 | 235 | var item = { type : 'music' }; 236 | 237 | var results = [{ 238 | type : 'music', 239 | title : 'Something.NotHd', 240 | isLosslessFormat : true, 241 | verified : true, 242 | getDescription : function() { 243 | return 'Test AC-3 something'; 244 | } 245 | }]; 246 | 247 | return filter.first(item, results).should.become(results[0]); 248 | }); 249 | 250 | 251 | it('should remove non-hd show when required', function() { 252 | config.get().showSettings.hdVideoPreference = config.Preference.required; 253 | var item = { type : 'show', no : 1 }; 254 | 255 | var results = [{ 256 | type : 'show', 257 | title : 'Something Season 1.NotHd', 258 | hasHdVideo : false, 259 | hasDigitalAudio : true, 260 | verified : true, 261 | getDescription : function() { 262 | return 'Test AC-3 something'; 263 | } 264 | }]; 265 | 266 | return filter.first(item, results).should.become(undefined); 267 | }); 268 | 269 | it('should not remove non-hd show when not required', function() { 270 | config.get().showSettings.hdVideoPreference = config.Preference.preferred; 271 | var item = { type : 'show', no : 1 }; 272 | 273 | var results = [{ 274 | type : 'show', 275 | title : 'Something Season 1.NotHd', 276 | hasHdVideo : false, 277 | hasDigitalAudio : true, 278 | verified : true, 279 | getDescription : function() { 280 | return 'Test AC-3 something'; 281 | } 282 | }]; 283 | 284 | return filter.first(item, results).should.become(results[0]); 285 | }); 286 | 287 | it('should remove hd show when unwanted', function() { 288 | config.get().showSettings.hdVideoPreference = config.Preference.unwanted; 289 | var item = { type : 'show', no : 1 }; 290 | 291 | var results = [{ 292 | type : 'show', 293 | title : 'Something Season 1.NotHd', 294 | hasHdVideo : true, 295 | hasDigitalAudio : true, 296 | verified : true, 297 | getDescription : function() { 298 | return 'Test AC-3 something'; 299 | } 300 | }]; 301 | 302 | return filter.first(item, results).should.become(undefined); 303 | }); 304 | 305 | it('should not remove hd show when not unwanted', function() { 306 | config.get().showSettings.hdVideoPreference = config.Preference.optional; 307 | var item = { type : 'show', no : 1 }; 308 | 309 | var results = [{ 310 | type : 'show', 311 | title : 'Something Season 1.NotHd', 312 | hasHdVideo : true, 313 | hasDigitalAudio : true, 314 | verified : true, 315 | getDescription : function() { 316 | return 'Test AC-3 something'; 317 | } 318 | }]; 319 | 320 | return filter.first(item, results).should.become(results[0]); 321 | }); 322 | 323 | it('should remove non-dts show when required', function() { 324 | config.get().showSettings.digitalAudioPreference = config.Preference.required; 325 | 326 | var item = { type : 'show', no : 1 }; 327 | 328 | var results = [{ 329 | type : 'show', 330 | title : 'Something Season 1.NotHd', 331 | hasHdVideo : false, 332 | hasDigitalAudio : false, 333 | verified : true, 334 | getDescription : function() { 335 | return 'Test AC-3 something'; 336 | } 337 | }]; 338 | 339 | return filter.first(item, results).should.become(undefined); 340 | }); 341 | 342 | it('should not remove non-dts show when not required', function() { 343 | config.get().showSettings.digitalAudioPreference = config.Preference.preferred; 344 | 345 | var item = { type : 'show', no : 1 }; 346 | 347 | var results = [{ 348 | type : 'show', 349 | title : 'Something Season 1.NotHd', 350 | hasHdVideo : false, 351 | hasDigitalAudio : true, 352 | verified : true, 353 | getDescription : function() { 354 | return 'Test AC-3 something'; 355 | } 356 | }]; 357 | 358 | return filter.first(item, results).should.become(results[0]); 359 | }); 360 | 361 | it('should remove dts show when unwanted', function() { 362 | config.get().showSettings.digitalAudioPreference = config.Preference.unwanted; 363 | 364 | var item = { type : 'show', no : 1 }; 365 | 366 | var results = [{ 367 | type : 'show', 368 | title : 'Something Season 1.NotHd', 369 | hasHdVideo : false, 370 | hasDigitalAudio : true, 371 | verified : true, 372 | getDescription : function() { 373 | return 'Test AC-3 something'; 374 | } 375 | }]; 376 | 377 | return filter.first(item, results).should.become(undefined); 378 | }); 379 | 380 | it('should not remove dts show when not unwanted', function() { 381 | config.get().showSettings.digitalAudioPreference = config.Preference.disfavoured; 382 | 383 | var item = { type : 'show', no : 1 }; 384 | 385 | var results = [{ 386 | type : 'show', 387 | title : 'Something Season 1.NotHd', 388 | hasHdVideo : false, 389 | hasDigitalAudio : true, 390 | verified : true, 391 | getDescription : function() { 392 | return 'Test AC-3 something'; 393 | } 394 | }]; 395 | 396 | return filter.first(item, results).should.become(results[0]); 397 | }); 398 | 399 | it('should remove show from wrong season', function() { 400 | var item = { type : 'show', no : 1 }; 401 | 402 | var results = [{ 403 | type : 'show', 404 | title : 'Something Seasons 1-2.NotHd', 405 | hasHdVideo : false, 406 | hasDigitalAudio : true, 407 | verified : true, 408 | getDescription : function() { 409 | return 'Test AC-3 something'; 410 | } 411 | }]; 412 | 413 | return filter.first(item, results).should.become(undefined); 414 | }); 415 | 416 | it('should remove movie exceeding size limit', function() { 417 | var item = { type : 'movie' }; 418 | 419 | var results = [{ 420 | type : 'movie', 421 | title : 'Something.NotHd', 422 | hasHdVideo : false, 423 | hasDigitalAudio : true, 424 | size : 10000, 425 | verified : true, 426 | getDescription : function() { 427 | return 'Test AC-3 something'; 428 | } 429 | }]; 430 | 431 | return filter.first(item, results).should.become(undefined); 432 | }); 433 | 434 | it('should remove show exceeding size limit', function() { 435 | var item = { type : 'show', no : 1 }; 436 | 437 | var results = [{ 438 | type : 'show', 439 | title : 'Something Season 1.NotHd', 440 | hasHdVideo : false, 441 | hasDigitalAudio : true, 442 | size : 100000, 443 | verified : true, 444 | getDescription : function() { 445 | return 'Test AC-3 something'; 446 | } 447 | }]; 448 | 449 | return filter.first(item, results).should.become(undefined); 450 | }); 451 | 452 | it('should remove music exceeding size limit', function() { 453 | var item = { type : 'music' }; 454 | 455 | var results = [{ 456 | type : 'music', 457 | title : 'Something Season 1.NotHd', 458 | size : 10000, 459 | verified : true, 460 | getDescription : function() { 461 | return 'Test AC-3 something'; 462 | } 463 | }]; 464 | 465 | return filter.first(item, results).should.become(undefined); 466 | }); 467 | 468 | it('should remove movie with 0 seeders', function() { 469 | var item = { type : 'movie' }; 470 | 471 | var results = [{ 472 | type : 'movie', 473 | title : 'Something.NotHd', 474 | seeds : 0, 475 | hasHdVideo : false, 476 | hasDigitalAudio : true, 477 | verified : true, 478 | getDescription : function() { 479 | return 'Test AC-3 something'; 480 | } 481 | }]; 482 | 483 | return filter.first(item, results).should.become(undefined); 484 | }); 485 | 486 | it('should remove show with 0 seeders', function() { 487 | var item = { type : 'show', no : 1 }; 488 | 489 | var results = [{ 490 | type : 'show', 491 | title : 'Something Season 1.NotHd', 492 | seeds : 0, 493 | hasHdVideo : false, 494 | hasDigitalAudio : true, 495 | verified : true, 496 | getDescription : function() { 497 | return 'Test AC-3 something'; 498 | } 499 | }]; 500 | 501 | return filter.first(item, results).should.become(undefined); 502 | }); 503 | 504 | it('should remove music with 0 seeders', function() { 505 | var item = { type : 'music' }; 506 | 507 | var results = [{ 508 | type : 'music', 509 | title : 'Something Season 1.NotHd', 510 | seeds : 0, 511 | verified : true, 512 | getDescription : function() { 513 | return 'Test AC-3 something'; 514 | } 515 | }]; 516 | 517 | return filter.first(item, results).should.become(undefined); 518 | }); 519 | 520 | it('should remove movie that was already used', function() { 521 | var item = { type : 'movie', torrentLinks : ['abc'] }; 522 | 523 | var results = [{ 524 | type : 'movie', 525 | title : 'Something.NotHd', 526 | magnetLink : 'abc', 527 | hasHdVideo : false, 528 | hasDigitalAudio : true, 529 | verified : true, 530 | getDescription : function() { 531 | return 'Test AC-3 something'; 532 | } 533 | }]; 534 | 535 | return filter.first(item, results).should.become(undefined); 536 | }); 537 | 538 | it('should remove show that was already used', function() { 539 | var item = { type : 'show', no : 1, torrentLinks : ['abc'] }; 540 | 541 | var results = [{ 542 | type : 'show', 543 | title : 'Something Season 1.NotHd', 544 | magnetLink : 'abc', 545 | hasHdVideo : false, 546 | hasDigitalAudio : true, 547 | verified : true, 548 | getDescription : function() { 549 | return 'Test AC-3 something'; 550 | } 551 | }]; 552 | 553 | return filter.first(item, results).should.become(undefined); 554 | }); 555 | 556 | it('should remove music that was already used', function() { 557 | var item = { type : 'music', torrentLinks : ['abc'] }; 558 | 559 | var results = [{ 560 | type : 'music', 561 | title : 'Something Season 1.NotHd', 562 | magnetLink : 'abc', 563 | verified : true, 564 | getDescription : function() { 565 | return 'Test AC-3 something'; 566 | } 567 | }]; 568 | 569 | return filter.first(item, results).should.become(undefined); 570 | }); 571 | 572 | 573 | }); -------------------------------------------------------------------------------- /test/routes.index.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var sinon = require('sinon'); 4 | var sinonChai = require("sinon-chai"); 5 | 6 | var Q = require('q'); 7 | var Item = require('../models/item').Item; 8 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 9 | 10 | var config = require('../config'); 11 | var index = require('../routes/index'); 12 | var version = require('../helpers/version'); 13 | 14 | chai.should(); 15 | chai.use(chaiAsPromised); 16 | chai.use(sinonChai); 17 | 18 | var staticConfig = config.get(); 19 | 20 | config.get = function() { 21 | return staticConfig; 22 | } 23 | 24 | describe('routes.index', function() { 25 | var items = [{ type : 'movie' }]; 26 | var res = { 27 | render : sinon.spy() 28 | }; 29 | 30 | before(function () { 31 | config.get().version = 1; 32 | Item.find = function() { 33 | return Q(items); 34 | }; 35 | version.myVersion = 'My version'; 36 | version.newVersion = sinon.stub().returns(Q('Some new version')); 37 | }); 38 | 39 | it('should successfully get index', function() { 40 | return index.index(undefined, res).should.be.fulfilled.then(function() { 41 | return res.render.should.have.been.calledWith('index', { 42 | title : 'lumus', 43 | myVersion : 'My version', 44 | newVersion : 'Some new version', 45 | items : items, 46 | icons : ItemTypeIcons 47 | }); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/routes.item.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var sinon = require('sinon'); 4 | var sinonChai = require("sinon-chai"); 5 | var util = require('util'); 6 | 7 | var Q = require('q'); 8 | var Item = require('../models/item').Item; 9 | var ItemStates = require('../models/item').ItemStates; 10 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 11 | 12 | var config = require('../config'); 13 | var itemRoutes = require('../routes/item'); 14 | var torrenter = require('../jobs/torrenter'); 15 | 16 | chai.should(); 17 | chai.use(chaiAsPromised); 18 | chai.use(sinonChai); 19 | 20 | var staticConfig = config.get(); 21 | 22 | config.get = function() { 23 | return staticConfig; 24 | } 25 | 26 | describe('routes.item', function() { 27 | var item; 28 | var res; 29 | 30 | beforeEach(function () { 31 | item = { type : 'movie' }; 32 | Item.setupMethods(item); 33 | item.save = sinon.spy(item.save); 34 | item.planNextCheck = sinon.spy(item.planNextCheck); 35 | Item.findById = function(id) { 36 | return id === 123 ? Q(item) : Q(undefined); 37 | }; 38 | Item.save = sinon.stub().returns(Q(this)); 39 | Item.removeById = sinon.stub().returns(Q(undefined)); 40 | torrenter.removeTorrent = sinon.stub().returns(Q(undefined)); 41 | res = { 42 | render : sinon.spy(), 43 | redirect : sinon.spy() 44 | }; 45 | util.error = sinon.spy(); 46 | }); 47 | 48 | it('should add item', function() { 49 | return itemRoutes.add({query : { name : 'Test', type : 'movie', year : 2015, externalId : 'tt000000' } }, res).should.be.fulfilled.then(function() { 50 | return Item.save.should.have.been.calledWithMatch({ name : 'Test', state : ItemStates.wanted }); 51 | }).then(function() { 52 | return res.redirect.should.have.been.calledWith('/'); 53 | }); 54 | }); 55 | 56 | it('should change item', function() { 57 | return itemRoutes.changeState({query : { id : 123, state : ItemStates.downloaded } }, res).should.be.fulfilled.then(function() { 58 | return item.planNextCheck.should.have.been.called; 59 | }).then(function() { 60 | return Item.save.should.have.been.calledWithMatch({ state : ItemStates.downloaded }); 61 | }).then(function() { 62 | return res.redirect.should.have.been.calledWith('/'); 63 | }); 64 | }); 65 | 66 | it('should remove item', function() { 67 | return itemRoutes.remove({query : {id : 123} }, res).should.be.fulfilled.then(function() { 68 | return Item.removeById.should.have.been.calledWith(123); 69 | }).then(function() { 70 | return torrenter.removeTorrent.should.have.been.calledWith(item, true); 71 | }).then(function() { 72 | return res.redirect.should.have.been.calledWith('/'); 73 | }); 74 | }); 75 | 76 | it('should not fail from invalid item', function() { 77 | return itemRoutes.remove({query : {id : 0} }, res).should.be.fulfilled.then(function() { 78 | return Item.removeById.should.not.have.been.called; 79 | }).then(function() { 80 | return torrenter.removeTorrent.should.not.have.been.called; 81 | }).then(function() { 82 | return util.error.should.not.have.been.called; 83 | }).then(function() { 84 | return res.redirect.should.have.been.calledWith('/'); 85 | }); 86 | }); 87 | 88 | it('should not fail from torrent removal inability', function() { 89 | torrenter.removeTorrent = function() { return Q.fcall(function() { throw new Error('Unable to connect'); }); }; 90 | 91 | return itemRoutes.remove({query : {id : 123} }, res).should.be.fulfilled.then(function() { 92 | return Item.removeById.should.have.been.calledWith(123); 93 | }).then(function() { 94 | return util.error.should.have.been.called; 95 | }).then(function() { 96 | return res.redirect.should.have.been.calledWith('/'); 97 | }); 98 | }); 99 | }); -------------------------------------------------------------------------------- /test/routes.list.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var chaiAsPromised = require("chai-as-promised"); 3 | var sinon = require('sinon'); 4 | var sinonChai = require("sinon-chai"); 5 | var util = require('util'); 6 | 7 | var Q = require('q'); 8 | var Item = require('../models/item').Item; 9 | var ItemStates = require('../models/item').ItemStates; 10 | var ItemTypeIcons = require('../models/item').ItemTypeIcons; 11 | 12 | var config = require('../config'); 13 | var listRoutes = require('../routes/list'); 14 | 15 | chai.should(); 16 | chai.use(chaiAsPromised); 17 | chai.use(sinonChai); 18 | 19 | var staticConfig = config.get(); 20 | 21 | config.get = function() { 22 | return staticConfig; 23 | } 24 | 25 | describe('routes.list', function() { 26 | var item; 27 | var res; 28 | 29 | beforeEach(function () { 30 | item = { type : 'movie' }; 31 | Item.setupMethods(item); 32 | item.save = sinon.spy(item.save); 33 | item.planNextCheck = sinon.spy(item.planNextCheck); 34 | Item.find = function(condition, sort) { 35 | return Object.getOwnPropertyNames(condition).length === 0 ? Q([item]) : Q([]); 36 | }; 37 | Item.save = sinon.stub().returns(Q(this)); 38 | res = { 39 | render : sinon.spy(), 40 | redirect : sinon.spy() 41 | }; 42 | util.error = sinon.spy(); 43 | }); 44 | 45 | it('should add item', function() { 46 | return listRoutes.add({query : { name : 'Test', type : 'movie', year : 2015, externalId : 'tt000000' } }, res).should.be.fulfilled.then(function() { 47 | return Item.save.should.have.been.calledWithMatch({ name : 'Test', state : ItemStates.wanted }); 48 | }).then(function() { 49 | return res.redirect.should.have.been.calledWith('/'); 50 | }); 51 | }); 52 | 53 | it('should list items', function() { 54 | return listRoutes.list({}, res).should.be.fulfilled.then(function() { 55 | return res.render.should.have.been.calledWith('list', { 56 | items : [ item ], 57 | icons : require('../models/item').ItemTypeIcons 58 | }); 59 | }); 60 | }); 61 | }); -------------------------------------------------------------------------------- /views/artist.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headScript 4 | script(src="/javascripts/music.js") 5 | 6 | block content 7 | .row 8 | .col-sm-3 9 | img#poster(style='width: 100%;margin-top: 2em') 10 | 11 | .col-sm-6 12 | h1 13 | +fa('music', item.artist) 14 | 15 | hr 16 | h2 Albums 17 | #error.alert.alert-info(style='display:none') 18 | #albums 19 | 20 | hr 21 | h2 Other 22 | #error2.alert.alert-info(style='display:none') 23 | #other 24 | 25 | hr 26 | +buttonPrimary('/', 'Back') -------------------------------------------------------------------------------- /views/config.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | include includes/item 3 | 4 | mixin renderObject(property, value, path) 5 | - var pathJoined = path ? path.join(':') : property 6 | div(id=pathJoined + 'Div') 7 | each innerValue, innerProperty in value 8 | if typeof innerValue != 'object' 9 | +render(innerProperty, innerValue, path ? path.concat([ innerProperty ]) : [ innerProperty ]) 10 | 11 | each innerValue, innerProperty in value 12 | if typeof innerValue == 'object' 13 | +render(innerProperty, innerValue, path ? path.concat([ innerProperty ]) : [ innerProperty ]) 14 | 15 | 16 | mixin render(property, value, path) 17 | - var pathJoined = path ? path.join(':') : property 18 | - var jqueryPathJoined = path ? path.join('\\\\:') : property 19 | - var label = labels.get(pathJoined, property) 20 | if property == 'type' || property == 'version' || property == 'use' 21 | else if property.endsWith('Preference') 22 | .form-group 23 | label!= label 24 | select.form-control(id=pathJoined, name=pathJoined) 25 | option(value = 'required', selected = value === 'required')= 'Required' 26 | option(value = 'preferred', selected = value === 'preferred')= 'Preferred' 27 | option(value = 'optional', selected = value === 'optional')= 'Optional' 28 | option(value = 'disfavoured', selected = value === 'disfavoured')= 'Disfavoured' 29 | option(value = 'unwanted', selected = value === 'unwanted')= 'Unwanted' 30 | 31 | else if typeof value == 'object' 32 | if path.length == 1 33 | hr 34 | h2(id=pathJoined + 'Head')!= label 35 | else if path.length == 2 36 | h3(id=pathJoined + 'Head')!= label 37 | else if path.length == 3 38 | h4(id=pathJoined + 'Head')!= label 39 | 40 | +renderObject(property, value, path) 41 | 42 | if value.use != undefined 43 | script(type='text/javascript'). 44 | $('##{jqueryPathJoined}Head').html('
'); 45 | $('##{jqueryPathJoined}\\:use').change(function() { 46 | $('##{jqueryPathJoined}Div').toggle(this.checked); 47 | }).change(); 48 | 49 | 50 | else if typeof value == 'boolean' 51 | +check(pathJoined, label, value) 52 | else if typeof value == 'number' 53 | +input('number', pathJoined, label, value) 54 | else 55 | +input('text', pathJoined, label, value) 56 | 57 | block content 58 | .col-sm-6.col-sm-offset-3 59 | h1 60 | +fa('wrench', 'Settings') 61 | 62 | form(action='/config', method='POST') 63 | +renderObject('config', config) 64 | 65 | +faButtonSubmit('save', 'Save') 66 | if config.version != 0 67 | | 68 | +buttonPrimary('/', 'Back') 69 | #bottom   -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .row 5 | .col-sm-3 6 | img#poster(style='width: 100%;margin-top: 2em') 7 | 8 | .col-sm-6 9 | h1 10 | +fa('ban', 'Error') 11 | 12 | p 13 | strong Sorry, there was an error processing your request. 14 | p= error 15 | 16 | hr 17 | +buttonPrimary('/', 'Back') -------------------------------------------------------------------------------- /views/includes/item.jade: -------------------------------------------------------------------------------- 1 | mixin item(item) 2 | div 3 | strong 4 | +fa(icons[item.type], item.getDisplayName()) 5 | if item.externalId 6 | |   7 | if item.type === 'show' || item.type === 'movie' 8 | +faLinkBlank('external-link', 'http://www.imdb.com/title/' + item.externalId + '/') 9 | else 10 | +faLinkBlank('external-link', 'http://musicbrainz.org/release-group/' + item.externalId) 11 | div 12 | span.state(style='float:right') 13 | if item.stateInfo 14 | a(href="#", title="#{item.stateInfo}") 15 | +fa('info-circle') 16 | | 17 | = item.state 18 | if item.torrentInfoUrl 19 | a(href=item.torrentInfoUrl, target='_blank'): +fa('download', 'Info') 20 | |  or  21 | if item.state === 'wanted' 22 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Retry search') 23 | if item.state === 'snatched' 24 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 25 | if item.state === 'subtitled' 26 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 27 | if item.state === 'renameFailed' 28 | a(href='/changeState?id=' + item._id + '&state=downloaded'): +fa('refresh', 'Retry rename') 29 | |  or  30 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 31 | if item.state === 'libraryUpdated' 32 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 33 | if item.state === 'libraryUpdateFailed' 34 | a(href='/changeState?id=' + item._id + '&state=renamed'): +fa('refresh', 'Retry library update') 35 | |  or  36 | a(href='/changeState?id=' + item._id + '&state=libraryUpdated'): +fa('chevron-right', 'Skip library update') 37 | if item.state === 'renamed' 38 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 39 | |  or  40 | a(href='/changeState?id=' + item._id + '&state=finished'): +fa('check', 'Set finished') 41 | if item.state === 'subtitlerFailed' 42 | a(href='/changeState?id=' + item._id + '&state=libraryUpdated'): +fa('refresh', 'Retry subtitles') 43 | |  or  44 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 45 | |  or  46 | a(href='/changeState?id=' + item._id + '&state=finished'): +fa('check', 'Set finished') 47 | if item.state === 'finished' 48 | a(href='/changeState?id=' + item._id + '&state=wanted'): +fa('refresh', 'Try other release') 49 | if item.next 50 | |  or  51 | if item.type === 'show' 52 | a(href='/add?type=show&no=' + item.next + '&externalId=' + item.externalId + '&name=' + encodeURIComponent(item.name)): +fa('plus', 'Add next season') 53 | else if item.type === 'music' 54 | a(href='/add?type=music&externalId=' + item.next + '&name=' + encodeURIComponent(item.nextName)): +fa('plus', 'Add next album') 55 | |  or  56 | a(href='/remove?id=' + item._id): +fa('times', 'Remove') 57 | |  or  58 | a(href='/torrent?id=' + item._id): +fa('list', 'Choose') 59 | hr(style='margin-top: 5px; margin-bottom: 5px') -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | include includes/item 3 | 4 | block content 5 | .row 6 | .col-sm-6.col-sm-offset-3 7 | h1 8 | +fa('search', 'Lumus') 9 | 10 | small 11 | | Version #{myVersion} 12 | if newVersion 13 | em 14 | |  | new version available #{newVersion}  15 | //a(href='/update') Update 16 | 17 | .row 18 | .col-sm-6.col-sm-offset-3 19 | form(action='/search', method='GET') 20 | +inputWithPlaceholder('text', 'what', 'Enter Movie, Show, Artist or Group Name') 21 | button#search.btn.btn-warning(type='submit') 22 | +fa('search', 'Search') 23 | .dropdown#menu(style='float:right') 24 | a.btn.btn-default.dropdown-toggle(href='#', data-toggle='dropdown', type='button', title='Menu') 25 | +fa('bars') 26 | ul.dropdown-menu(role='menu', aria-labelledby='menu') 27 | li(role='presentation') 28 | +faLink('wrench', '/config', 'Settings') 29 | 30 | if items.hasWaiting() 31 | h2 Waiting 32 | each item in items 33 | if item.isWaiting() 34 | +item(item) 35 | 36 | if items.hasDownloading() 37 | h2 Downloading 38 | each item in items 39 | if item.isDownloading() 40 | +item(item) 41 | 42 | if items.hasWaitingForSubtitles() 43 | h2 Waiting For Subtitles 44 | each item in items 45 | if item.isWaitingForSubtitles() 46 | +item(item) 47 | 48 | if items.hasFailed() 49 | h2 Failed 50 | each item in items 51 | if item.isFailed() 52 | +item(item) 53 | 54 | if items.hasFinished() 55 | h2 Finished 56 | each item in items 57 | if item.isFinished() 58 | +item(item) 59 | .col-sm-3 60 | small Please support development by making a donation 61 | br 62 | br 63 | form(action='https://www.paypal.com/cgi-bin/webscr', method='POST', target='blank') 64 | input(type='hidden', name='cmd' value='_s-xclick') 65 | input(type='hidden', name='encrypted', value='-----BEGIN PKCS7-----MIIHPwYJKoZIhvcNAQcEoIIHMDCCBywCAQExggEwMIIBLAIBADCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwDQYJKoZIhvcNAQEBBQAEgYBW3RJMJT+FiPiVkOe7uviEgu5HoKsYMvYkSRJH1xzlI+1L/5nfsiYquLbGJaEn7xIQ2b2Ipz7S6Ur8YqW70fs0HUY/fvY4zzmJsnXgkEropyHagFYaTUevg/QRh0tEINae/fPg6plAvcxjfz9HZUprourb76yjmQYo6rTHXx4aWjELMAkGBSsOAwIaBQAwgbwGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIsrTbpfQDTuaAgZgf+Z8PKfQ4axhNLCxMw8sSGSYjaGfwgxlApQrY8EWt0AhBePxLEVY0b9BzMgZTqrck5PN6aZyPSRMGvHEkkssDczKk7GqE1CoVBsQVxI2HKF+1w1dj832EvChq+7LPBRDBpJ4WMWU64HkiiGHbjn5UaR06pCw23tbnrwOWXOx+2uhbLgP/RH2VgYe+NRrUB2krFnqwmPCT9aCCA4cwggODMIIC7KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTAeFw0wNDAyMTMxMDEzMTVaFw0zNTAyMTMxMDEzMTVaMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwUdO3fxEzEtcnI7ZKZL412XvZPugoni7i7D7prCe0AtaHTc97CYgm7NsAtJyxNLixmhLV8pyIEaiHXWAh8fPKW+R017+EmXrr9EaquPmsVvTywAAE1PMNOKqo2kl4Gxiz9zZqIajOm1fZGWcGS0f5JQ2kBqNbvbg2/Za+GJ/qwUCAwEAAaOB7jCB6zAdBgNVHQ4EFgQUlp98u8ZvF71ZP1LXChvsENZklGswgbsGA1UdIwSBszCBsIAUlp98u8ZvF71ZP1LXChvsENZklGuhgZSkgZEwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLUGF5UGFsIEluYy4xEzARBgNVBAsUCmxpdmVfY2VydHMxETAPBgNVBAMUCGxpdmVfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAgV86VpqAWuXvX6Oro4qJ1tYVIT5DgWpE692Ag422H7yRIr/9j/iKG4Thia/Oflx4TdL+IFJBAyPK9v6zZNZtBgPBynXb048hsP16l2vi0k5Q2JKiPDsEfBhGI+HnxLXEaUWAcVfCsQFvd2A1sxRr67ip5y2wwBelUecP3AjJ+YcxggGaMIIBlgIBATCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE1MDUxNDIxMTcwMlowIwYJKoZIhvcNAQkEMRYEFJdtFwd/iM6Ol3N3KaiQ7GRxF5H1MA0GCSqGSIb3DQEBAQUABIGAPYzxzRqVlx1GKsMXuXq0FemiUXGzcW8BWGqzkBaZ/v88jQYKiqKgLd8OIIwontUR38wSv6IbT6V6uH/hzl7Dh+ftczKEGT0sN9gMpZ9xTVywEebE1/k911ni5I/0w0tNTosGHxYHAL2OqrZr1GRS8uNmLEuZNVnwqp2vvLecw7A=-----END PKCS7-----') 66 | button.btn.btn-primary.btn-sm(type='submit') 67 | +fa('paypal', 'PayPal') 68 | hr 69 | small 70 | +fa('bitcoin', 'Bitcoin') 71 | br 72 | code 73 | | 1MqjdRkVpG7CzRgYZAuABdA3NcpH4gajho 74 | 75 | hr 76 | small 77 | | Found a bug? Want a feature? 78 | br 79 | +faLinkBlank('github', 'https://github.com/ziacik/lumus/issues/new', 'File an issue') -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | mixin fa(type, text, title) 4 | span(class='fa fa-'+type, title=title) 5 | if text 6 | | 7 | = text 8 | 9 | mixin faSpinner(text) 10 | i(class='fa fa-circle-o-notch fa-spin') 11 | if text 12 | | 13 | = text 14 | 15 | mixin faLink(faType, href, text, title) 16 | a(href=href) 17 | +fa(faType, text, title) 18 | 19 | mixin faLinkBlank(faType, href, text, title) 20 | a(href=href, target='_blank') 21 | +fa(faType, text, title) 22 | 23 | mixin faButtonSmall(faType, href, text, title) 24 | a.btn.btn-default.btn-xs(href=href, title=title) 25 | +fa(faType, text) 26 | 27 | mixin faButtonDefault(faType, href, text, title) 28 | a.btn.btn-default(href=href, title=title) 29 | +fa(faType, text) 30 | 31 | mixin faButtonPrimary(faType, href, text, title) 32 | a.btn.btn-primary(href=href, title=title) 33 | +fa(faType, text) 34 | 35 | mixin buttonPrimary(href, text, title) 36 | a.btn.btn-primary(href=href, title=title)= text 37 | 38 | mixin faButtonSubmit(faType, text, title) 39 | button.btn.btn-warning(type='submit', title=title) 40 | +fa(faType, text) 41 | 42 | mixin buttonSubmit(text, title) 43 | button.btn.btn-warning(type='submit', title=title)= text 44 | 45 | mixin buttonHome() 46 | a.btn.btn-primary(href='/', title='Home')= 'Home' 47 | 48 | mixin inputHidden(id, value) 49 | input(type='hidden', name=id, id=id, value=value) 50 | 51 | mixin inputRequired(type, id, text, value) 52 | .form-group 53 | label!= text 54 | input.form-control(type=type, name=id, id=id, required, value=value) 55 | 56 | mixin input(type, id, text, value) 57 | .form-group 58 | label!= text 59 | input.form-control(type=type, name=id, id=id, value=value) 60 | 61 | mixin inputWithPlaceholder(type, id, text, value) 62 | .form-group 63 | input.form-control(type=type, name=id, id=id, value=value, placeholder=text) 64 | 65 | mixin check(id, text, value) 66 | .checkbox 67 | label 68 | if value 69 | input(type='checkbox', name=id, id=id, checked='checked') 70 | else 71 | input(type='checkbox', name=id, id=id) 72 | != text 73 | 74 | 75 | html 76 | head 77 | title= title 78 | meta(name='viewport', content='width=device-width, initial-scale=1') 79 | link(href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", rel="stylesheet") 80 | link(href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css", rel="stylesheet") 81 | script(src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js') 82 | script(src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js") 83 | script(src="//builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v2.0.0.js") 84 | block headScript 85 | body 86 | .container 87 | block content 88 | -------------------------------------------------------------------------------- /views/newReleases.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headScript 4 | script(src="/javascripts/yify.js") 5 | 6 | block content 7 | .col-sm-6.col-sm-offset-3 8 | h1 9 | +fa('fire', 'New Releases') 10 | 11 | hr 12 | #results 13 | 14 | hr 15 | +buttonPrimary('/', 'Back') -------------------------------------------------------------------------------- /views/search.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headScript 4 | script(src="/javascripts/search.js") 5 | 6 | script. 7 | Handlebars.registerHelper('uri', function(value, value2OrOptions, optionsOrNothing) { 8 | if (value2OrOptions && optionsOrNothing) { 9 | value = value + ' (' + value2OrOptions + ')'; 10 | } 11 | 12 | return new Handlebars.SafeString(encodeURIComponent(value)); 13 | }); 14 | 15 | script#show-template(type="text/x-handlebars-template") 16 | p 17 | +fa('tv', '{{ Title }} ({{ Year }})') 18 | |   19 | +faLinkBlank('external-link', 'http://www.imdb.com/title/{{ imdbID }}/') 20 | |   21 | a.btn.btn-default.btn-xs(href='/show/add?type=show&name={{uri Title}}&showId={{ imdbID }}') 22 | span.glyphicon.glyphicon-plus 23 | 24 | 25 | script#movie-template(type="text/x-handlebars-template") 26 | p 27 | +fa('film', '{{ Title }} ({{ Year }})') 28 | |   29 | +faLinkBlank('external-link', 'http://www.imdb.com/title/{{ imdbID }}/') 30 | |   31 | a.btn.btn-default.btn-xs(href='/add?type=movie&name={{uri Title}}&year={{ Year }}&externalId={{ imdbID }}') 32 | span.glyphicon.glyphicon-plus 33 | 34 | script#music-template(type="text/x-handlebars-template") 35 | p 36 | +fa('music', '{{ name }}{{#if disambiguation}} ({{ disambiguation }}){{/if}}') 37 | |   38 | +faLinkBlank('external-link', 'http://musicbrainz.org/artist/{{ id }}') 39 | |   40 | a.btn.btn-default.btn-xs(href='/artist/add?type=music&name={{uri name disambiguation}}&artistId={{ id }}') 41 | span.glyphicon.glyphicon-plus 42 | 43 | block content 44 | .row 45 | .col-sm-3 46 | 47 | .col-sm-6 48 | h1 49 | +fa('search', 'Search Results') 50 | 51 | #movies 52 | hr 53 | #movieSpinner 54 | +faSpinner('Searching for movies...') 55 | #movieError(style='display:none') 56 | +fa('ban') 57 | span#movieErrorText 58 | #shows 59 | hr 60 | #showSpinner 61 | +faSpinner('Searching for shows...') 62 | #showError(style='display:none') 63 | +fa('ban') 64 | span#showErrorText 65 | #music 66 | hr 67 | #musicSpinner 68 | +faSpinner('Searching for music...') 69 | #musicError(style='display:none') 70 | +fa('ban') 71 | span#musicErrorText 72 | 73 | #noResult(style='display:none') 74 | hr 75 | +fa('info-circle', 'Nothing found.') 76 | 77 | hr 78 | +buttonHome() -------------------------------------------------------------------------------- /views/show.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block headScript 4 | // TODO will probably remove unless client-side api is found for shows script(src="/javascripts/show.js") 5 | 6 | block content 7 | .row 8 | .col-sm-3 9 | img#poster(style='width: 100%;margin-top: 2em') 10 | 11 | .col-sm-6 12 | h1 13 | +fa('tv', item.name) 14 | 15 | img#poster.img-responsive(src="http://thetvdb.com/banners/" + info.banner) 16 | 17 | #basicInfo.small 18 | p= info.Network + " • " + info.Runtime + " min" + " • " + "Rating " + info.Rating + " • " + info.Status 19 | p= info.Genre.substring(1, info.Genre.length - 1).replace(/\|/g, ' • ') 20 | 21 | p#overview.text-justify= info.Overview 22 | 23 | h2 Seasons 24 | 25 | #seasons 26 | for season in info.seasons 27 | p 28 | +fa('tv', 'Season ' + season.No + ' (' + season.Year + ')') 29 | span   30 | a.btn.btn-default.btn-xs(href="/add?type=show&name=" + encodeURIComponent(item.name) + "&no=" + season.No + "&externalId=" + item.showId, title="Add") 31 | span.glyphicon.glyphicon-plus -------------------------------------------------------------------------------- /views/torrents.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .row 5 | .col-sm-3 6 | img#poster(style='width: 100%;margin-top: 2em') 7 | 8 | .col-sm-6 9 | h1 10 | +fa('th-large', item.name) 11 | 12 | form(action='/torrent', method='GET') 13 | +inputHidden('id', item._id) 14 | +input('text', 'what', 'Search Term', searchTerm) 15 | +faButtonSubmit('search', 'Search Now') 16 | button#reset.btn(type='submit', name='reset', value='1', style='margin-left:1em') 17 | +fa('undo', 'Reset') 18 | 19 | hr 20 | 21 | each result in results 22 | div 23 | strong 24 | a(href=result.torrentInfoUrl, target='_blank')= result.title 25 | 26 | span.state(style='float:right') 27 | +faButtonDefault('download', '/torrent/add?id=' + item._id + '&magnet=' + encodeURIComponent(result.magnetLink) + '&page=' + encodeURIComponent(result.torrentInfoUrl), '', 'Download') 28 | 29 | div 30 | small 31 | em 32 | = result.serviceName 33 | 34 | div 35 | span.state 36 | +fa('cube', result.size + ' MB', 'Size') 37 | span.state 38 | |   39 | +fa('arrow-up', result.seeds + '', 'Seeders') 40 | span.state 41 | |   42 | +fa('arrow-down', result.leechs + '', 'Leechers') 43 | if result.verified 44 | span.state 45 | |   46 | +fa('check', 'Verified', 'Verified') 47 | if result.hasSubtitles 48 | span.state 49 | |   50 | +fa('check', 'Subtitles', 'Subtitles') 51 | if result.hasHdVideo 52 | span.state 53 | |   54 | +fa('check', 'HD Video', 'HD Video') 55 | if result.hasDigitalAudio 56 | span.state 57 | |   58 | +fa('check', 'Digital Audio', 'Digital Audio') 59 | if result.isLossless 60 | span.state 61 | |   62 | +fa('check', 'Lossless', 'Lossless') 63 | 64 | div 65 | = result.info 66 | 67 | hr 68 | hr 69 | +buttonHome() 70 | --------------------------------------------------------------------------------