diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py
index 1b7f1636..b80ddcc1 100644
--- a/couchpotato/core/_base/clientscript/main.py
+++ b/couchpotato/core/_base/clientscript/main.py
@@ -34,6 +34,8 @@ class ClientScript(Plugin):
'scripts/library/question.js',
'scripts/library/scrollspy.js',
'scripts/library/spin.js',
+ 'scripts/library/Array.stableSort.js',
+ 'scripts/library/async.js',
'scripts/couchpotato.js',
'scripts/api.js',
'scripts/library/history.js',
diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py
index 2c97e1b4..87b05335 100644
--- a/couchpotato/core/_base/scheduler/main.py
+++ b/couchpotato/core/_base/scheduler/main.py
@@ -31,8 +31,8 @@ class Scheduler(Plugin):
pass
def doShutdown(self):
- super(Scheduler, self).doShutdown()
self.stop()
+ return super(Scheduler, self).doShutdown()
def stop(self):
if self.started:
diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
index 0577c783..860ad514 100644
--- a/couchpotato/core/_base/updater/static/updater.js
+++ b/couchpotato/core/_base/updater/static/updater.js
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate();
else {
App.unBlockPage();
- App.fireEvent('message', 'No updates available');
+ App.on('message', 'No updates available');
}
}
})
diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py
index 026a56c6..684ea45e 100755
--- a/couchpotato/core/downloaders/rtorrent/__init__.py
+++ b/couchpotato/core/downloaders/rtorrent/__init__.py
@@ -58,14 +58,6 @@ config = [{
'advanced': True,
'description': 'Also remove the leftover files.',
},
- {
- 'name': 'append_label',
- 'label': 'Append Label',
- 'default': False,
- 'advanced': True,
- 'type': 'bool',
- 'description': 'Append label to download location. Requires you to set the download location above.',
- },
{
'name': 'paused',
'type': 'bool',
diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py
index d7ae589f..8381f0a2 100755
--- a/couchpotato/core/downloaders/rtorrent/main.py
+++ b/couchpotato/core/downloaders/rtorrent/main.py
@@ -125,9 +125,7 @@ class rTorrent(Downloader):
if self.conf('label'):
torrent.set_custom(1, self.conf('label'))
- if self.conf('directory') and self.conf('append_label'):
- torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
- elif self.conf('directory'):
+ if self.conf('directory'):
torrent.set_directory(self.conf('directory'))
# Set Ratio Group
diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py
index 1db1b8a3..9ae5fcb3 100644
--- a/couchpotato/core/downloaders/utorrent/main.py
+++ b/couchpotato/core/downloaders/utorrent/main.py
@@ -77,6 +77,7 @@ class uTorrent(Downloader):
else:
info = bdecode(filedata)["info"]
torrent_hash = sha1(benc(info)).hexdigest().upper()
+
torrent_filename = self.createFileName(data, filedata, movie)
if data.get('seed_ratio'):
@@ -93,9 +94,9 @@ class uTorrent(Downloader):
# Send request to uTorrent
if data.get('protocol') == 'torrent_magnet':
- self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
+ self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), directory)
else:
- self.utorrent_api.add_torrent_file(torrent_filename, filedata)
+ self.utorrent_api.add_torrent_file(torrent_filename, filedata, directory)
# Change settings of added torrent
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py
index e6a249d5..1ba83863 100644
--- a/couchpotato/core/media/__init__.py
+++ b/couchpotato/core/media/__init__.py
@@ -38,7 +38,7 @@ class MediaBase(Plugin):
def notifyFront():
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
- fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
+ fireEvent('notify.frontend', type = '%s.update' % media.type, data = media.to_dict(self.default_dict))
db.expire_all()
return notifyFront
diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py
index 87afb82a..68ae5314 100644
--- a/couchpotato/core/media/_base/media/main.py
+++ b/couchpotato/core/media/_base/media/main.py
@@ -34,7 +34,7 @@ class MediaPlugin(MediaBase):
for title in media.library.titles:
if title.default: default_title = title.title
- fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
+ fireEvent('notify.frontend', type = '%s.busy' % media.type, data = {'id': x})
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
diff --git a/couchpotato/core/media/movie/_base/static/list.js b/couchpotato/core/media/movie/_base/static/list.js
index aaa8be12..db598b20 100644
--- a/couchpotato/core/media/movie/_base/static/list.js
+++ b/couchpotato/core/media/movie/_base/static/list.js
@@ -52,8 +52,8 @@ var MovieList = new Class({
self.getMovies();
- App.addEvent('movie.added', self.movieAdded.bind(self))
- App.addEvent('movie.deleted', self.movieDeleted.bind(self))
+ App.on('movie.added', self.movieAdded.bind(self))
+ App.on('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
@@ -65,6 +65,7 @@ var MovieList = new Class({
movie.destroy();
delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1);
+ self.total_movies--;
}
})
}
@@ -75,6 +76,7 @@ var MovieList = new Class({
movieAdded: function(notification){
var self = this;
+ self.fireEvent('movieAdded', notification);
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0);
self.createMovie(notification.data, 'top');
@@ -390,6 +392,7 @@ var MovieList = new Class({
self.movies.erase(movie);
movie.destroy();
self.setCounter(self.counter_count-1);
+ self.total_movies--;
});
self.calculateSelected();
diff --git a/couchpotato/core/media/movie/_base/static/movie.actions.js b/couchpotato/core/media/movie/_base/static/movie.actions.js
index f6e0f542..e3591f34 100644
--- a/couchpotato/core/media/movie/_base/static/movie.actions.js
+++ b/couchpotato/core/media/movie/_base/static/movie.actions.js
@@ -126,7 +126,9 @@ MA.Release = new Class({
else
self.showHelper();
- App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
+ App.on('movie.searcher.ended', function(notification){
+ if(self.movie.data.id != notification.data.id) return;
+
self.releases = null;
if(self.options_container){
self.options_container.destroy();
@@ -250,12 +252,14 @@ MA.Release = new Class({
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
-
+
var update_handle = function(notification) {
- var q = self.movie.quality.getElement('.q_id' + release.quality_id),
+ if(notification.data.id != release.id) return;
+
+ var q = self.movie.quality.getElement('.q_id' + release.quality_id),
status = Status.get(release.status_id),
- new_status = Status.get(notification.data);
-
+ new_status = Status.get(notification.data.status_id);
+
release.status_id = new_status.id
release.el.set('class', 'item ' + new_status.identifier);
@@ -272,7 +276,7 @@ MA.Release = new Class({
}
}
- App.addEvent('release.update_status.' + release.id, update_handle);
+ App.on('release.update_status', update_handle);
});
@@ -285,7 +289,7 @@ MA.Release = new Class({
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
-
+
var nr = self.next_release,
lr = self.last_release;
diff --git a/couchpotato/core/media/movie/_base/static/movie.css b/couchpotato/core/media/movie/_base/static/movie.css
index c013bd80..a88a2077 100644
--- a/couchpotato/core/media/movie/_base/static/movie.css
+++ b/couchpotato/core/media/movie/_base/static/movie.css
@@ -1036,7 +1036,7 @@
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
- direction: rtl;
+ direction: ltr;
vertical-align: middle;
}
diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js
index a865325b..bc258451 100644
--- a/couchpotato/core/media/movie/_base/static/movie.js
+++ b/couchpotato/core/media/movie/_base/static/movie.js
@@ -23,23 +23,49 @@ var Movie = new Class({
addEvents: function(){
var self = this;
- App.addEvent('movie.update.'+self.data.id, function(notification){
+ self.global_events = {}
+
+ // Do refresh with new data
+ self.global_events['movie.update'] = function(notification){
+ if(self.data.id != notification.data.id) return;
+
self.busy(false);
self.removeView();
self.update.delay(2000, self, notification);
- });
+ }
+ App.on('movie.update', self.global_events['movie.update']);
+ // Add spinner on load / search
['movie.busy', 'movie.searcher.started'].each(function(listener){
- App.addEvent(listener+'.'+self.data.id, function(notification){
- if(notification.data)
+ self.global_events[listener] = function(notification){
+ if(notification.data && self.data.id == notification.data.id)
self.busy(true)
- });
+ }
+ App.on(listener, self.global_events[listener]);
})
- App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
- if(notification.data)
+ // Remove spinner
+ self.global_events['movie.searcher.ended'] = function(notification){
+ if(notification.data && self.data.id == notification.data.id)
self.busy(false)
- });
+ }
+ App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
+
+ // Reload when releases have updated
+ self.global_events['release.update_status'] = function(notification){
+ var data = notification.data
+ if(data && self.data.id == data.movie_id){
+
+ if(!self.data.releases)
+ self.data.releases = [];
+
+ self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
+ self.updateReleases();
+ }
+ }
+
+ App.on('release.update_status', self.global_events['release.update_status']);
+
},
destroy: function(){
@@ -52,9 +78,8 @@ var Movie = new Class({
self.list.checkIfEmpty();
// Remove events
- App.removeEvents('movie.update.'+self.data.id);
- ['movie.busy', 'movie.searcher.started'].each(function(listener){
- App.removeEvents(listener+'.'+self.data.id);
+ self.global_events.each(function(handle, listener){
+ App.off(listener, handle);
})
},
@@ -179,21 +204,7 @@ var Movie = new Class({
});
// Add releases
- if(self.data.releases)
- self.data.releases.each(function(release){
-
- var q = self.quality.getElement('.q_id'+ release.quality_id),
- status = Status.get(release.status_id);
-
- if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
- var q = self.addQuality(release.quality_id)
-
- if (status && q && !q.hasClass(status.identifier)){
- q.addClass(status.identifier);
- q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
- }
-
- });
+ self.updateReleases();
Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
@@ -203,6 +214,26 @@ var Movie = new Class({
},
+ updateReleases: function(){
+ var self = this;
+ if(!self.data.releases || self.data.releases.length == 0) return;
+
+ self.data.releases.each(function(release){
+
+ var q = self.quality.getElement('.q_id'+ release.quality_id),
+ status = Status.get(release.status_id);
+
+ if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
+ var q = self.addQuality(release.quality_id)
+
+ if (status && q && !q.hasClass(status.identifier)){
+ q.addClass(status.identifier);
+ q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
+ }
+
+ });
+ },
+
addQuality: function(quality_id){
var self = this;
diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py
index 93441c59..f80f63fe 100644
--- a/couchpotato/core/media/movie/searcher/main.py
+++ b/couchpotato/core/media/movie/searcher/main.py
@@ -148,7 +148,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
fireEvent('movie.delete', movie['id'], single = True)
return
- fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
+ fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
ret = False
@@ -202,7 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
- fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
+ fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
return ret
diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py
index 4c0d0992..63d2075e 100644
--- a/couchpotato/core/notifications/base.py
+++ b/couchpotato/core/notifications/base.py
@@ -17,7 +17,7 @@ class Notification(Provider):
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
- 'core.message',
+ 'core.message.important',
]
dont_listen_to = []
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index 04acf284..cd63c2cb 100644
--- a/couchpotato/core/notifications/core/main.py
+++ b/couchpotato/core/notifications/core/main.py
@@ -21,6 +21,12 @@ class CoreNotifier(Notification):
m_lock = None
+ listen_to = [
+ 'renamer.after', 'movie.snatched',
+ 'updater.available', 'updater.updated',
+ 'core.message', 'core.message.important',
+ ]
+
def __init__(self):
super(CoreNotifier, self).__init__()
@@ -121,7 +127,10 @@ class CoreNotifier(Notification):
for message in messages:
if message.get('time') > last_check:
- fireEvent('core.message', message = message.get('message'), data = message)
+ message['sticky'] = True # Always sticky core messages
+
+ message_type = 'core.message.important' if message.get('important') else 'core.message'
+ fireEvent(message_type, message = message.get('message'), data = message)
if last_check < message.get('time'):
last_check = message.get('time')
diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
index e485976e..a0c3b15c 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
// Listener
App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true]));
- App.addEvent('notification', self.notify.bind(self));
- App.addEvent('message', self.showMessage.bind(self));
+ App.on('notification', self.notify.bind(self));
+ App.on('message', self.showMessage.bind(self));
// Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self));
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
, 'top');
self.notifications.include(result);
- if(result.data.important !== undefined && !result.read){
+ if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
var sticky = true
- App.fireEvent('message', [result.message, sticky, result])
+ App.trigger('message', [result.message, sticky, result])
}
else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
// Process data
if(json){
Array.each(json.result, function(result){
- App.fireEvent(result.type, result);
+ App.trigger(result.type, result);
if(result.message && result.read === undefined)
self.showMessage(result.message);
})
diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py
index c67ac97d..41a4323b 100644
--- a/couchpotato/core/notifications/email/main.py
+++ b/couchpotato/core/notifications/email/main.py
@@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from email.mime.text import MIMEText
+from email.utils import formatdate, make_msgid
import smtplib
import traceback
@@ -30,6 +31,8 @@ class Email(Notification):
message['Subject'] = self.default_title
message['From'] = from_address
message['To'] = to_address
+ message['Date'] = formatdate(localtime = 1)
+ message['Message-ID'] = make_msgid()
try:
# Open the SMTP connection, via SSL if requested
diff --git a/couchpotato/core/notifications/pushbullet/__init__.py b/couchpotato/core/notifications/pushbullet/__init__.py
new file mode 100644
index 00000000..e61a44e3
--- /dev/null
+++ b/couchpotato/core/notifications/pushbullet/__init__.py
@@ -0,0 +1,39 @@
+from .main import Pushbullet
+
+def start():
+ return Pushbullet()
+
+config = [{
+ 'name': 'pushbullet',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'pushbullet',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'api_key',
+ 'label': 'User API Key'
+ },
+ {
+ 'name': 'devices',
+ 'default': '',
+ 'advanced': True,
+ 'description': 'IDs of devices to send notifications to, empty = all devices'
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/pushbullet/main.py b/couchpotato/core/notifications/pushbullet/main.py
new file mode 100644
index 00000000..2e6db29d
--- /dev/null
+++ b/couchpotato/core/notifications/pushbullet/main.py
@@ -0,0 +1,86 @@
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+import base64
+import json
+
+log = CPLog(__name__)
+
+
+class Pushbullet(Notification):
+
+ url = 'https://api.pushbullet.com/api/%s'
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ devices = self.getDevices()
+ if devices is None:
+ return False
+
+ # Get all the device IDs linked to this user
+ if not len(devices):
+ response = self.request('devices')
+ if not response:
+ return False
+
+ devices += [device.get('id') for device in response['devices']]
+
+ successful = 0
+ for device in devices:
+ response = self.request(
+ 'pushes',
+ cache = False,
+ device_id = device,
+ type = 'note',
+ title = self.default_title,
+ body = toUnicode(message)
+ )
+
+ if response:
+ successful += 1
+ else:
+ log.error('Unable to push notification to Pushbullet device with ID %s' % device)
+
+ return successful == len(devices)
+
+ def getDevices(self):
+ devices = [d.strip() for d in self.conf('devices').split(',')]
+
+ # Remove empty items
+ devices = [d for d in devices if len(d)]
+
+ # Break on any ids that aren't integers
+ valid_devices = []
+
+ for device_id in devices:
+ d = tryInt(device_id, None)
+
+ if not d:
+ log.error('Device ID "%s" is not valid', device_id)
+ return None
+
+ valid_devices.append(d)
+
+ return valid_devices
+
+ def request(self, method, cache = True, **kwargs):
+ try:
+ base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
+
+ headers = {
+ "Authorization": "Basic %s" % base64string
+ }
+
+ if cache:
+ return self.getJsonData(self.url % method, headers = headers, params = kwargs)
+ else:
+ data = self.urlopen(self.url % method, headers = headers, params = kwargs)
+ return json.loads(data)
+
+ except Exception, ex:
+ log.error('Pushbullet request failed')
+ log.debug(ex)
+
+ return None
diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py
index e8ccaf7e..87207615 100644
--- a/couchpotato/core/plugins/manage/main.py
+++ b/couchpotato/core/plugins/manage/main.py
@@ -79,6 +79,7 @@ class Manage(Plugin):
try:
directories = self.directories()
+ directories.sort()
added_identifiers = []
# Add some progress
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index 0c0636e6..3aaa77ea 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -2,7 +2,7 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode, ss
-from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
+from couchpotato.core.helpers.variable import mergeDicts, getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType
@@ -19,14 +19,14 @@ class QualityPlugin(Plugin):
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']},
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
- {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
+ {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r')]},
- {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
- {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
- {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
- {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
+ {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
+ {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]},
+ {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]},
+ {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]},
+ {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
+ {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -50,6 +50,8 @@ class QualityPlugin(Plugin):
addEvent('app.initialize', self.fill, priority = 10)
+ addEvent('app.test', self.doTest)
+
def preReleases(self):
return self.pre_releases
@@ -165,9 +167,10 @@ class QualityPlugin(Plugin):
if not extra: extra = {}
# Create hash for cache
- cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
+ cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files])
cached = self.getCache(cache_key)
- if cached and len(extra) == 0: return cached
+ if cached and len(extra) == 0:
+ return cached
qualities = self.all()
@@ -228,11 +231,6 @@ class QualityPlugin(Plugin):
if len(set(words) & set(alt)) == len(alt):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type)
- elif len(set(words) & set(alt)) > 0:
- partial = list(set(words) & set(alt))[0]
- if len(partial) > 2:
- log.debug('Found %s via partial %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
- score += points.get(tag_type) / 3
if (isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower()):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
@@ -285,3 +283,33 @@ class QualityPlugin(Plugin):
if add_score != 0:
for allow in quality.get('allow', []):
score[allow] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5
+
+ def doTest(self):
+
+ tests = {
+ 'Movie Name (1999)-DVD-Rip.avi': 'dvdrip',
+ 'Movie Name 1999 720p Bluray.mkv': '720p',
+ 'Movie Name 1999 BR-Rip 720p.avi': 'brrip',
+ 'Movie Name 1999 720p Web Rip.avi': 'scr',
+ 'Movie Name 1999 Web DL.avi': 'brrip',
+ 'Movie.Name.1999.1080p.WEBRip.H264-Group': 'scr',
+ 'Movie.Name.1999.DVDRip-Group': 'dvdrip',
+ 'Movie.Name.1999.DVD-Rip-Group': 'dvdrip',
+ 'Movie.Name.1999.DVD-R-Group': 'dvdr',
+ }
+
+ correct = 0
+ for name in tests:
+ success = self.guess([name]).get('identifier') == tests[name]
+ if not success:
+ log.error('%s failed check, thinks it\'s %s', (name, self.guess([name]).get('identifier')))
+
+ correct += success
+
+ if correct == len(tests):
+ log.info('Quality test successful')
+ return True
+ else:
+ log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests)))
+
+
diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py
index 009a60e9..a74e853b 100644
--- a/couchpotato/core/plugins/release/main.py
+++ b/couchpotato/core/plugins/release/main.py
@@ -446,6 +446,6 @@ class Release(Plugin):
db.commit()
#Update all movie info as there is no release update function
- fireEvent('notify.frontend', type = 'release.update_status.%s' % rel.id, data = status.get('id'))
+ fireEvent('notify.frontend', type = 'release.update_status', data = rel.to_dict())
return True
diff --git a/couchpotato/core/providers/torrent/torrentpotato/__init__.py b/couchpotato/core/providers/torrent/torrentpotato/__init__.py
new file mode 100644
index 00000000..5054f98c
--- /dev/null
+++ b/couchpotato/core/providers/torrent/torrentpotato/__init__.py
@@ -0,0 +1,66 @@
+from .main import TorrentPotato
+
+def start():
+ return TorrentPotato()
+
+config = [{
+ 'name': 'torrentpotato',
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'list': 'torrent_providers',
+ 'name': 'TorrentPotato',
+ 'order': 10,
+ 'description': 'CouchPotato torrent provider. Checkout the wiki page about this provider for more info.',
+ 'wizard': True,
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'type': 'enabler',
+ 'default': False,
+ },
+ {
+ 'name': 'use',
+ 'default': ''
+ },
+ {
+ 'name': 'host',
+ 'default': '',
+ 'description': 'The url path of your TorrentPotato provider.',
+ },
+ {
+ 'name': 'extra_score',
+ 'advanced': True,
+ 'label': 'Extra Score',
+ 'default': '0',
+ 'description': 'Starting score for each release found via this provider.',
+ },
+ {
+ 'name': 'name',
+ 'label': 'Username',
+ 'default': '',
+ },
+ {
+ 'name': 'seed_ratio',
+ 'label': 'Seed ratio',
+ 'default': '1',
+ 'description': 'Will not be (re)moved until this seed ratio is met.',
+ },
+ {
+ 'name': 'seed_time',
+ 'label': 'Seed time',
+ 'default': '40',
+ 'description': 'Will not be (re)moved until this seed time (in hours) is met.',
+ },
+ {
+ 'name': 'pass_key',
+ 'default': ',',
+ 'label': 'Pass Key',
+ 'description': 'Can be found on your profile page',
+ 'type': 'combined',
+ 'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'],
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/torrent/torrentpotato/main.py b/couchpotato/core/providers/torrent/torrentpotato/main.py
new file mode 100644
index 00000000..a76c0c8f
--- /dev/null
+++ b/couchpotato/core/providers/torrent/torrentpotato/main.py
@@ -0,0 +1,129 @@
+from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
+from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
+from couchpotato.core.logger import CPLog
+from couchpotato.core.providers.base import ResultList
+from couchpotato.core.providers.torrent.base import TorrentProvider
+from urlparse import urlparse
+import re
+import traceback
+
+log = CPLog(__name__)
+
+
+class TorrentPotato(TorrentProvider):
+
+ urls = {}
+ limits_reached = {}
+
+ http_time_between_calls = 1 # Seconds
+
+ def search(self, movie, quality):
+ hosts = self.getHosts()
+
+ results = ResultList(self, movie, quality, imdb_results = True)
+
+ for host in hosts:
+ if self.isDisabled(host):
+ continue
+
+ self._searchOnHost(host, movie, quality, results)
+
+ return results
+
+ def _searchOnHost(self, host, movie, quality, results):
+
+ arguments = tryUrlencode({
+ 'user': host['name'],
+ 'passkey': host['pass_key'],
+ 'imdbid': movie['library']['identifier']
+ })
+ url = '%s?%s' % (host['host'], arguments)
+
+ torrents = self.getJsonData(url, cache_timeout = 1800)
+
+ if torrents:
+ try:
+ if torrents.get('error'):
+ log.error('%s: %s', (torrents.get('error'), host['host']))
+ elif torrents.get('results'):
+ for torrent in torrents.get('results', []):
+ results.append({
+ 'id': torrent.get('torrent_id'),
+ 'protocol': 'torrent' if re.match('^(http|https|ftp)://.*$', torrent.get('download_url')) else 'torrent_magnet',
+ 'provider_extra': urlparse(host['host']).hostname or host['host'],
+ 'name': toUnicode(torrent.get('release_name')),
+ 'url': torrent.get('download_url'),
+ 'detail_url': torrent.get('details_url'),
+ 'size': torrent.get('size'),
+ 'score': host['extra_score'],
+ 'seeders': torrent.get('seeders'),
+ 'leechers': torrent.get('leechers'),
+ 'seed_ratio': host['seed_ratio'],
+ 'seed_time': host['seed_time'],
+ })
+
+ except:
+ log.error('Failed getting results from %s: %s', (host['host'], traceback.format_exc()))
+
+ def getHosts(self):
+
+ uses = splitString(str(self.conf('use')), clean = False)
+ hosts = splitString(self.conf('host'), clean = False)
+ names = splitString(self.conf('name'), clean = False)
+ seed_times = splitString(self.conf('seed_time'), clean = False)
+ seed_ratios = splitString(self.conf('seed_ratio'), clean = False)
+ pass_keys = splitString(self.conf('pass_key'), clean = False)
+ extra_score = splitString(self.conf('extra_score'), clean = False)
+
+ list = []
+ for nr in range(len(hosts)):
+
+ try: key = pass_keys[nr]
+ except: key = ''
+
+ try: host = hosts[nr]
+ except: host = ''
+
+ try: name = names[nr]
+ except: name = ''
+
+ try: ratio = seed_ratios[nr]
+ except: ratio = ''
+
+ try: seed_time = seed_times[nr]
+ except: seed_time = ''
+
+ list.append({
+ 'use': uses[nr],
+ 'host': host,
+ 'name': name,
+ 'seed_ratio': tryFloat(ratio),
+ 'seed_time': tryInt(seed_time),
+ 'pass_key': key,
+ 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
+ })
+
+ return list
+
+ def belongsTo(self, url, provider = None, host = None):
+
+ hosts = self.getHosts()
+
+ for host in hosts:
+ result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider)
+ if result:
+ return result
+
+ def isDisabled(self, host = None):
+ return not self.isEnabled(host)
+
+ def isEnabled(self, host = None):
+
+ # Return true if at least one is enabled and no host is given
+ if host is None:
+ for host in self.getHosts():
+ if self.isEnabled(host):
+ return True
+ return False
+
+ return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index 59fac34b..eae865f4 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -11,6 +11,12 @@
pages: [],
block: [],
+ initialize: function(){
+ var self = this;
+
+ self.global_events = {};
+ },
+
setup: function(options) {
var self = this;
self.setOptions(options);
@@ -30,7 +36,7 @@
History.addEvent('change', self.openPage.bind(self));
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
-
+
// Check if device is touchenabled
self.touch_device = 'ontouchstart' in window || navigator.msMaxTouchPoints;
if(self.touch_device)
@@ -55,7 +61,7 @@
History.push(url);
}
},
-
+
isMac: function(){
return Browser.Platform.mac
},
@@ -111,7 +117,7 @@
}
})
];
-
+
setting_links.each(function(a){
self.block.more.addLink(a)
});
@@ -336,6 +342,66 @@
})
)
);
+ },
+
+ /*
+ * Global events
+ */
+ on: function(name, handle){
+ var self = this;
+
+ if(!self.global_events[name])
+ self.global_events[name] = [];
+
+ self.global_events[name].push(handle);
+
+ },
+
+ trigger: function(name, args, on_complete){
+ var self = this;
+
+ if(!self.global_events[name]){ return; }
+
+ if(!on_complete && typeOf(args) == 'function'){
+ on_complete = args;
+ args = {};
+ }
+
+ // Create parallel callback
+ var callbacks = [];
+ self.global_events[name].each(function(handle, nr){
+
+ callbacks.push(function(callback){
+ var results = handle(args || {});
+ callback(null, results || null);
+ });
+
+ });
+
+ // Fire events
+ async.parallel(callbacks, function(err, results){
+ if(err) p(err);
+
+ if(on_complete)
+ on_complete(results);
+ });
+
+ },
+
+ off: function(name, handle){
+ var self = this;
+
+ if(!self.global_events[name]) return;
+
+ // Remove single
+ if(handle){
+ self.global_events[name] = self.global_events[name].erase(handle);
+ }
+ // Reset full event
+ else {
+ self.global_events[name] = [];
+ }
+
}
});
@@ -503,7 +569,7 @@ function randomString(length, extra) {
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
}
});
- return this.sort(comparer);
+ return this.stableSort(comparer);
}
});
diff --git a/couchpotato/static/scripts/library/Array.stableSort.js b/couchpotato/static/scripts/library/Array.stableSort.js
new file mode 100644
index 00000000..062c7566
--- /dev/null
+++ b/couchpotato/static/scripts/library/Array.stableSort.js
@@ -0,0 +1,56 @@
+/*
+---
+
+script: Array.stableSort.js
+
+description: Add a stable sort algorithm for all browsers
+
+license: MIT-style license.
+
+authors:
+ - Yorick Sijsling
+
+requires:
+ core/1.3: '*'
+
+provides:
+ - [Array.stableSort, Array.mergeSort]
+
+...
+*/
+
+(function() {
+
+ var defaultSortFunction = function(a, b) {
+ return a > b ? 1 : (a < b ? -1 : 0);
+ }
+
+ Array.implement({
+
+ stableSort: function(compare) {
+ // I would love some real feature recognition. Problem is that an unstable algorithm sometimes/often gives the same result as an unstable algorithm.
+ return (Browser.chrome || Browser.firefox2 || Browser.opera9) ? this.mergeSort(compare) : this.sort(compare);
+ },
+
+ mergeSort: function(compare, token) {
+ compare = compare || defaultSortFunction;
+ if (this.length > 1) {
+ // Split and sort both parts
+ var right = this.splice(Math.floor(this.length / 2)).mergeSort(compare);
+ var left = this.splice(0).mergeSort(compare); // 'this' is now empty.
+
+ // Merge parts together
+ while (left.length > 0 || right.length > 0) {
+ this.push(
+ right.length === 0 ? left.shift()
+ : left.length === 0 ? right.shift()
+ : compare(left[0], right[0]) > 0 ? right.shift()
+ : left.shift());
+ }
+ }
+ return this;
+ }
+
+ });
+})();
+
diff --git a/couchpotato/static/scripts/library/async.js b/couchpotato/static/scripts/library/async.js
new file mode 100644
index 00000000..cb6320d6
--- /dev/null
+++ b/couchpotato/static/scripts/library/async.js
@@ -0,0 +1,955 @@
+/*global setImmediate: false, setTimeout: false, console: false */
+(function () {
+
+ var async = {};
+
+ // global on the server, window in the browser
+ var root, previous_async;
+
+ root = this;
+ if (root != null) {
+ previous_async = root.async;
+ }
+
+ async.noConflict = function () {
+ root.async = previous_async;
+ return async;
+ };
+
+ function only_once(fn) {
+ var called = false;
+ return function() {
+ if (called) throw new Error("Callback was already called.");
+ called = true;
+ fn.apply(root, arguments);
+ }
+ }
+
+ //// cross-browser compatiblity functions ////
+
+ var _each = function (arr, iterator) {
+ if (arr.forEach) {
+ return arr.forEach(iterator);
+ }
+ for (var i = 0; i < arr.length; i += 1) {
+ iterator(arr[i], i, arr);
+ }
+ };
+
+ var _map = function (arr, iterator) {
+ if (arr.map) {
+ return arr.map(iterator);
+ }
+ var results = [];
+ _each(arr, function (x, i, a) {
+ results.push(iterator(x, i, a));
+ });
+ return results;
+ };
+
+ var _reduce = function (arr, iterator, memo) {
+ if (arr.reduce) {
+ return arr.reduce(iterator, memo);
+ }
+ _each(arr, function (x, i, a) {
+ memo = iterator(memo, x, i, a);
+ });
+ return memo;
+ };
+
+ var _keys = function (obj) {
+ if (Object.keys) {
+ return Object.keys(obj);
+ }
+ var keys = [];
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ keys.push(k);
+ }
+ }
+ return keys;
+ };
+
+ //// exported async module functions ////
+
+ //// nextTick implementation with browser-compatible fallback ////
+ if (typeof process === 'undefined' || !(process.nextTick)) {
+ if (typeof setImmediate === 'function') {
+ async.nextTick = function (fn) {
+ // not a direct alias for IE10 compatibility
+ setImmediate(fn);
+ };
+ async.setImmediate = async.nextTick;
+ }
+ else {
+ async.nextTick = function (fn) {
+ setTimeout(fn, 0);
+ };
+ async.setImmediate = async.nextTick;
+ }
+ }
+ else {
+ async.nextTick = process.nextTick;
+ if (typeof setImmediate !== 'undefined') {
+ async.setImmediate = setImmediate;
+ }
+ else {
+ async.setImmediate = async.nextTick;
+ }
+ }
+
+ async.each = function (arr, iterator, callback) {
+ callback = callback || function () {};
+ if (!arr.length) {
+ return callback();
+ }
+ var completed = 0;
+ _each(arr, function (x) {
+ iterator(x, only_once(function (err) {
+ if (err) {
+ callback(err);
+ callback = function () {};
+ }
+ else {
+ completed += 1;
+ if (completed >= arr.length) {
+ callback(null);
+ }
+ }
+ }));
+ });
+ };
+ async.forEach = async.each;
+
+ async.eachSeries = function (arr, iterator, callback) {
+ callback = callback || function () {};
+ if (!arr.length) {
+ return callback();
+ }
+ var completed = 0;
+ var iterate = function () {
+ iterator(arr[completed], function (err) {
+ if (err) {
+ callback(err);
+ callback = function () {};
+ }
+ else {
+ completed += 1;
+ if (completed >= arr.length) {
+ callback(null);
+ }
+ else {
+ iterate();
+ }
+ }
+ });
+ };
+ iterate();
+ };
+ async.forEachSeries = async.eachSeries;
+
+ async.eachLimit = function (arr, limit, iterator, callback) {
+ var fn = _eachLimit(limit);
+ fn.apply(null, [arr, iterator, callback]);
+ };
+ async.forEachLimit = async.eachLimit;
+
+ var _eachLimit = function (limit) {
+
+ return function (arr, iterator, callback) {
+ callback = callback || function () {};
+ if (!arr.length || limit <= 0) {
+ return callback();
+ }
+ var completed = 0;
+ var started = 0;
+ var running = 0;
+
+ (function replenish () {
+ if (completed >= arr.length) {
+ return callback();
+ }
+
+ while (running < limit && started < arr.length) {
+ started += 1;
+ running += 1;
+ iterator(arr[started - 1], function (err) {
+ if (err) {
+ callback(err);
+ callback = function () {};
+ }
+ else {
+ completed += 1;
+ running -= 1;
+ if (completed >= arr.length) {
+ callback();
+ }
+ else {
+ replenish();
+ }
+ }
+ });
+ }
+ })();
+ };
+ };
+
+
+ var doParallel = function (fn) {
+ return function () {
+ var args = Array.prototype.slice.call(arguments);
+ return fn.apply(null, [async.each].concat(args));
+ };
+ };
+ var doParallelLimit = function(limit, fn) {
+ return function () {
+ var args = Array.prototype.slice.call(arguments);
+ return fn.apply(null, [_eachLimit(limit)].concat(args));
+ };
+ };
+ var doSeries = function (fn) {
+ return function () {
+ var args = Array.prototype.slice.call(arguments);
+ return fn.apply(null, [async.eachSeries].concat(args));
+ };
+ };
+
+
+ var _asyncMap = function (eachfn, arr, iterator, callback) {
+ var results = [];
+ arr = _map(arr, function (x, i) {
+ return {index: i, value: x};
+ });
+ eachfn(arr, function (x, callback) {
+ iterator(x.value, function (err, v) {
+ results[x.index] = v;
+ callback(err);
+ });
+ }, function (err) {
+ callback(err, results);
+ });
+ };
+ async.map = doParallel(_asyncMap);
+ async.mapSeries = doSeries(_asyncMap);
+ async.mapLimit = function (arr, limit, iterator, callback) {
+ return _mapLimit(limit)(arr, iterator, callback);
+ };
+
+ var _mapLimit = function(limit) {
+ return doParallelLimit(limit, _asyncMap);
+ };
+
+ // reduce only has a series version, as doing reduce in parallel won't
+ // work in many situations.
+ async.reduce = function (arr, memo, iterator, callback) {
+ async.eachSeries(arr, function (x, callback) {
+ iterator(memo, x, function (err, v) {
+ memo = v;
+ callback(err);
+ });
+ }, function (err) {
+ callback(err, memo);
+ });
+ };
+ // inject alias
+ async.inject = async.reduce;
+ // foldl alias
+ async.foldl = async.reduce;
+
+ async.reduceRight = function (arr, memo, iterator, callback) {
+ var reversed = _map(arr, function (x) {
+ return x;
+ }).reverse();
+ async.reduce(reversed, memo, iterator, callback);
+ };
+ // foldr alias
+ async.foldr = async.reduceRight;
+
+ var _filter = function (eachfn, arr, iterator, callback) {
+ var results = [];
+ arr = _map(arr, function (x, i) {
+ return {index: i, value: x};
+ });
+ eachfn(arr, function (x, callback) {
+ iterator(x.value, function (v) {
+ if (v) {
+ results.push(x);
+ }
+ callback();
+ });
+ }, function (err) {
+ callback(_map(results.sort(function (a, b) {
+ return a.index - b.index;
+ }), function (x) {
+ return x.value;
+ }));
+ });
+ };
+ async.filter = doParallel(_filter);
+ async.filterSeries = doSeries(_filter);
+ // select alias
+ async.select = async.filter;
+ async.selectSeries = async.filterSeries;
+
+ var _reject = function (eachfn, arr, iterator, callback) {
+ var results = [];
+ arr = _map(arr, function (x, i) {
+ return {index: i, value: x};
+ });
+ eachfn(arr, function (x, callback) {
+ iterator(x.value, function (v) {
+ if (!v) {
+ results.push(x);
+ }
+ callback();
+ });
+ }, function (err) {
+ callback(_map(results.sort(function (a, b) {
+ return a.index - b.index;
+ }), function (x) {
+ return x.value;
+ }));
+ });
+ };
+ async.reject = doParallel(_reject);
+ async.rejectSeries = doSeries(_reject);
+
+ var _detect = function (eachfn, arr, iterator, main_callback) {
+ eachfn(arr, function (x, callback) {
+ iterator(x, function (result) {
+ if (result) {
+ main_callback(x);
+ main_callback = function () {};
+ }
+ else {
+ callback();
+ }
+ });
+ }, function (err) {
+ main_callback();
+ });
+ };
+ async.detect = doParallel(_detect);
+ async.detectSeries = doSeries(_detect);
+
+ async.some = function (arr, iterator, main_callback) {
+ async.each(arr, function (x, callback) {
+ iterator(x, function (v) {
+ if (v) {
+ main_callback(true);
+ main_callback = function () {};
+ }
+ callback();
+ });
+ }, function (err) {
+ main_callback(false);
+ });
+ };
+ // any alias
+ async.any = async.some;
+
+ async.every = function (arr, iterator, main_callback) {
+ async.each(arr, function (x, callback) {
+ iterator(x, function (v) {
+ if (!v) {
+ main_callback(false);
+ main_callback = function () {};
+ }
+ callback();
+ });
+ }, function (err) {
+ main_callback(true);
+ });
+ };
+ // all alias
+ async.all = async.every;
+
+ async.sortBy = function (arr, iterator, callback) {
+ async.map(arr, function (x, callback) {
+ iterator(x, function (err, criteria) {
+ if (err) {
+ callback(err);
+ }
+ else {
+ callback(null, {value: x, criteria: criteria});
+ }
+ });
+ }, function (err, results) {
+ if (err) {
+ return callback(err);
+ }
+ else {
+ var fn = function (left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ };
+ callback(null, _map(results.sort(fn), function (x) {
+ return x.value;
+ }));
+ }
+ });
+ };
+
+ async.auto = function (tasks, callback) {
+ callback = callback || function () {};
+ var keys = _keys(tasks);
+ if (!keys.length) {
+ return callback(null);
+ }
+
+ var results = {};
+
+ var listeners = [];
+ var addListener = function (fn) {
+ listeners.unshift(fn);
+ };
+ var removeListener = function (fn) {
+ for (var i = 0; i < listeners.length; i += 1) {
+ if (listeners[i] === fn) {
+ listeners.splice(i, 1);
+ return;
+ }
+ }
+ };
+ var taskComplete = function () {
+ _each(listeners.slice(0), function (fn) {
+ fn();
+ });
+ };
+
+ addListener(function () {
+ if (_keys(results).length === keys.length) {
+ callback(null, results);
+ callback = function () {};
+ }
+ });
+
+ _each(keys, function (k) {
+ var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
+ var taskCallback = function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (args.length <= 1) {
+ args = args[0];
+ }
+ if (err) {
+ var safeResults = {};
+ _each(_keys(results), function(rkey) {
+ safeResults[rkey] = results[rkey];
+ });
+ safeResults[k] = args;
+ callback(err, safeResults);
+ // stop subsequent errors hitting callback multiple times
+ callback = function () {};
+ }
+ else {
+ results[k] = args;
+ async.setImmediate(taskComplete);
+ }
+ };
+ var requires = task.slice(0, Math.abs(task.length - 1)) || [];
+ var ready = function () {
+ return _reduce(requires, function (a, x) {
+ return (a && results.hasOwnProperty(x));
+ }, true) && !results.hasOwnProperty(k);
+ };
+ if (ready()) {
+ task[task.length - 1](taskCallback, results);
+ }
+ else {
+ var listener = function () {
+ if (ready()) {
+ removeListener(listener);
+ task[task.length - 1](taskCallback, results);
+ }
+ };
+ addListener(listener);
+ }
+ });
+ };
+
+ async.waterfall = function (tasks, callback) {
+ callback = callback || function () {};
+ if (tasks.constructor !== Array) {
+ var err = new Error('First argument to waterfall must be an array of functions');
+ return callback(err);
+ }
+ if (!tasks.length) {
+ return callback();
+ }
+ var wrapIterator = function (iterator) {
+ return function (err) {
+ if (err) {
+ callback.apply(null, arguments);
+ callback = function () {};
+ }
+ else {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var next = iterator.next();
+ if (next) {
+ args.push(wrapIterator(next));
+ }
+ else {
+ args.push(callback);
+ }
+ async.setImmediate(function () {
+ iterator.apply(null, args);
+ });
+ }
+ };
+ };
+ wrapIterator(async.iterator(tasks))();
+ };
+
+ var _parallel = function(eachfn, tasks, callback) {
+ callback = callback || function () {};
+ if (tasks.constructor === Array) {
+ eachfn.map(tasks, function (fn, callback) {
+ if (fn) {
+ fn(function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (args.length <= 1) {
+ args = args[0];
+ }
+ callback.call(null, err, args);
+ });
+ }
+ }, callback);
+ }
+ else {
+ var results = {};
+ eachfn.each(_keys(tasks), function (k, callback) {
+ tasks[k](function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (args.length <= 1) {
+ args = args[0];
+ }
+ results[k] = args;
+ callback(err);
+ });
+ }, function (err) {
+ callback(err, results);
+ });
+ }
+ };
+
+ async.parallel = function (tasks, callback) {
+ _parallel({ map: async.map, each: async.each }, tasks, callback);
+ };
+
+ async.parallelLimit = function(tasks, limit, callback) {
+ _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback);
+ };
+
+ async.series = function (tasks, callback) {
+ callback = callback || function () {};
+ if (tasks.constructor === Array) {
+ async.mapSeries(tasks, function (fn, callback) {
+ if (fn) {
+ fn(function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (args.length <= 1) {
+ args = args[0];
+ }
+ callback.call(null, err, args);
+ });
+ }
+ }, callback);
+ }
+ else {
+ var results = {};
+ async.eachSeries(_keys(tasks), function (k, callback) {
+ tasks[k](function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (args.length <= 1) {
+ args = args[0];
+ }
+ results[k] = args;
+ callback(err);
+ });
+ }, function (err) {
+ callback(err, results);
+ });
+ }
+ };
+
+ async.iterator = function (tasks) {
+ var makeCallback = function (index) {
+ var fn = function () {
+ if (tasks.length) {
+ tasks[index].apply(null, arguments);
+ }
+ return fn.next();
+ };
+ fn.next = function () {
+ return (index < tasks.length - 1) ? makeCallback(index + 1): null;
+ };
+ return fn;
+ };
+ return makeCallback(0);
+ };
+
+ async.apply = function (fn) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function () {
+ return fn.apply(
+ null, args.concat(Array.prototype.slice.call(arguments))
+ );
+ };
+ };
+
+ var _concat = function (eachfn, arr, fn, callback) {
+ var r = [];
+ eachfn(arr, function (x, cb) {
+ fn(x, function (err, y) {
+ r = r.concat(y || []);
+ cb(err);
+ });
+ }, function (err) {
+ callback(err, r);
+ });
+ };
+ async.concat = doParallel(_concat);
+ async.concatSeries = doSeries(_concat);
+
+ async.whilst = function (test, iterator, callback) {
+ if (test()) {
+ iterator(function (err) {
+ if (err) {
+ return callback(err);
+ }
+ async.whilst(test, iterator, callback);
+ });
+ }
+ else {
+ callback();
+ }
+ };
+
+ async.doWhilst = function (iterator, test, callback) {
+ iterator(function (err) {
+ if (err) {
+ return callback(err);
+ }
+ if (test()) {
+ async.doWhilst(iterator, test, callback);
+ }
+ else {
+ callback();
+ }
+ });
+ };
+
+ async.until = function (test, iterator, callback) {
+ if (!test()) {
+ iterator(function (err) {
+ if (err) {
+ return callback(err);
+ }
+ async.until(test, iterator, callback);
+ });
+ }
+ else {
+ callback();
+ }
+ };
+
+ async.doUntil = function (iterator, test, callback) {
+ iterator(function (err) {
+ if (err) {
+ return callback(err);
+ }
+ if (!test()) {
+ async.doUntil(iterator, test, callback);
+ }
+ else {
+ callback();
+ }
+ });
+ };
+
+ async.queue = function (worker, concurrency) {
+ if (concurrency === undefined) {
+ concurrency = 1;
+ }
+ function _insert(q, data, pos, callback) {
+ if(data.constructor !== Array) {
+ data = [data];
+ }
+ _each(data, function(task) {
+ var item = {
+ data: task,
+ callback: typeof callback === 'function' ? callback : null
+ };
+
+ if (pos) {
+ q.tasks.unshift(item);
+ } else {
+ q.tasks.push(item);
+ }
+
+ if (q.saturated && q.tasks.length === concurrency) {
+ q.saturated();
+ }
+ async.setImmediate(q.process);
+ });
+ }
+
+ var workers = 0;
+ var q = {
+ tasks: [],
+ concurrency: concurrency,
+ saturated: null,
+ empty: null,
+ drain: null,
+ push: function (data, callback) {
+ _insert(q, data, false, callback);
+ },
+ unshift: function (data, callback) {
+ _insert(q, data, true, callback);
+ },
+ process: function () {
+ if (workers < q.concurrency && q.tasks.length) {
+ var task = q.tasks.shift();
+ if (q.empty && q.tasks.length === 0) {
+ q.empty();
+ }
+ workers += 1;
+ var next = function () {
+ workers -= 1;
+ if (task.callback) {
+ task.callback.apply(task, arguments);
+ }
+ if (q.drain && q.tasks.length + workers === 0) {
+ q.drain();
+ }
+ q.process();
+ };
+ var cb = only_once(next);
+ worker(task.data, cb);
+ }
+ },
+ length: function () {
+ return q.tasks.length;
+ },
+ running: function () {
+ return workers;
+ }
+ };
+ return q;
+ };
+
+ async.cargo = function (worker, payload) {
+ var working = false,
+ tasks = [];
+
+ var cargo = {
+ tasks: tasks,
+ payload: payload,
+ saturated: null,
+ empty: null,
+ drain: null,
+ push: function (data, callback) {
+ if(data.constructor !== Array) {
+ data = [data];
+ }
+ _each(data, function(task) {
+ tasks.push({
+ data: task,
+ callback: typeof callback === 'function' ? callback : null
+ });
+ if (cargo.saturated && tasks.length === payload) {
+ cargo.saturated();
+ }
+ });
+ async.setImmediate(cargo.process);
+ },
+ process: function process() {
+ if (working) return;
+ if (tasks.length === 0) {
+ if(cargo.drain) cargo.drain();
+ return;
+ }
+
+ var ts = typeof payload === 'number'
+ ? tasks.splice(0, payload)
+ : tasks.splice(0);
+
+ var ds = _map(ts, function (task) {
+ return task.data;
+ });
+
+ if(cargo.empty) cargo.empty();
+ working = true;
+ worker(ds, function () {
+ working = false;
+
+ var args = arguments;
+ _each(ts, function (data) {
+ if (data.callback) {
+ data.callback.apply(null, args);
+ }
+ });
+
+ process();
+ });
+ },
+ length: function () {
+ return tasks.length;
+ },
+ running: function () {
+ return working;
+ }
+ };
+ return cargo;
+ };
+
+ var _console_fn = function (name) {
+ return function (fn) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ fn.apply(null, args.concat([function (err) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (typeof console !== 'undefined') {
+ if (err) {
+ if (console.error) {
+ console.error(err);
+ }
+ }
+ else if (console[name]) {
+ _each(args, function (x) {
+ console[name](x);
+ });
+ }
+ }
+ }]));
+ };
+ };
+ async.log = _console_fn('log');
+ async.dir = _console_fn('dir');
+ /*async.info = _console_fn('info');
+ async.warn = _console_fn('warn');
+ async.error = _console_fn('error');*/
+
+ async.memoize = function (fn, hasher) {
+ var memo = {};
+ var queues = {};
+ hasher = hasher || function (x) {
+ return x;
+ };
+ var memoized = function () {
+ var args = Array.prototype.slice.call(arguments);
+ var callback = args.pop();
+ var key = hasher.apply(null, args);
+ if (key in memo) {
+ callback.apply(null, memo[key]);
+ }
+ else if (key in queues) {
+ queues[key].push(callback);
+ }
+ else {
+ queues[key] = [callback];
+ fn.apply(null, args.concat([function () {
+ memo[key] = arguments;
+ var q = queues[key];
+ delete queues[key];
+ for (var i = 0, l = q.length; i < l; i++) {
+ q[i].apply(null, arguments);
+ }
+ }]));
+ }
+ };
+ memoized.memo = memo;
+ memoized.unmemoized = fn;
+ return memoized;
+ };
+
+ async.unmemoize = function (fn) {
+ return function () {
+ return (fn.unmemoized || fn).apply(null, arguments);
+ };
+ };
+
+ async.times = function (count, iterator, callback) {
+ var counter = [];
+ for (var i = 0; i < count; i++) {
+ counter.push(i);
+ }
+ return async.map(counter, iterator, callback);
+ };
+
+ async.timesSeries = function (count, iterator, callback) {
+ var counter = [];
+ for (var i = 0; i < count; i++) {
+ counter.push(i);
+ }
+ return async.mapSeries(counter, iterator, callback);
+ };
+
+ async.compose = function (/* functions... */) {
+ var fns = Array.prototype.reverse.call(arguments);
+ return function () {
+ var that = this;
+ var args = Array.prototype.slice.call(arguments);
+ var callback = args.pop();
+ async.reduce(fns, args, function (newargs, fn, cb) {
+ fn.apply(that, newargs.concat([function () {
+ var err = arguments[0];
+ var nextargs = Array.prototype.slice.call(arguments, 1);
+ cb(err, nextargs);
+ }]))
+ },
+ function (err, results) {
+ callback.apply(that, [err].concat(results));
+ });
+ };
+ };
+
+ var _applyEach = function (eachfn, fns /*args...*/) {
+ var go = function () {
+ var that = this;
+ var args = Array.prototype.slice.call(arguments);
+ var callback = args.pop();
+ return eachfn(fns, function (fn, cb) {
+ fn.apply(that, args.concat([cb]));
+ },
+ callback);
+ };
+ if (arguments.length > 2) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ return go.apply(this, args);
+ }
+ else {
+ return go;
+ }
+ };
+ async.applyEach = doParallel(_applyEach);
+ async.applyEachSeries = doSeries(_applyEach);
+
+ async.forever = function (fn, callback) {
+ function next(err) {
+ if (err) {
+ if (callback) {
+ return callback(err);
+ }
+ throw err;
+ }
+ fn(next);
+ }
+ next();
+ };
+
+ // AMD / RequireJS
+ if (typeof define !== 'undefined' && define.amd) {
+ define([], function () {
+ return async;
+ });
+ }
+ // Node.js
+ else if (typeof module !== 'undefined' && module.exports) {
+ module.exports = async;
+ }
+ // included directly via
+"""
+ soup = BeautifulSoup(doc, "xml")
+ # lxml would have stripped this while parsing, but we can add
+ # it later.
+ soup.script.string = 'console.log("< < hey > > ");'
+ encoded = soup.encode()
+ self.assertTrue(b"< < hey > >" in encoded)
+
+ def test_can_parse_unicode_document(self):
+ markup = u'