├── .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 | [](https://travis-ci.org/ziacik/lumus)
5 | [](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 | Main Screen |
14 | Search Results |
15 |
16 |
17 |  |
18 |  |
19 |
20 |
21 | Season Selection |
22 | Album Selection |
23 |
24 |
25 |  |
26 |  |
27 |
28 |
29 | Torrent Chooser |
30 | Settings |
31 |
32 |
33 |  |
34 |  |
35 |
36 |
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 | Install transmission
$ sudo apt-get install transmission-daemon
43 | Set it to run at startup
$ sudo update-rc.d transmission-daemon defaults
44 | Stop the deamon
$ sudo /etc/init.d/transmission-daemon stop
45 | -
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 |
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 | Clone lumus sources
git clone https://github.com/ziacik/lumus.git
76 | Go to the lumus directory
77 | cd lumus
78 | Install dependencies
npm install
79 | Run
node app.js
80 | Browse to configure
http://localhost:3001
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 |
--------------------------------------------------------------------------------