Browse Source

Merge branch 'refs/heads/develop' into desktop

tags/build/2.0.0.pre1
Ruud 13 years ago
parent
commit
5d4efb60cf
  1. 4
      .gitignore
  2. 6
      CouchPotato.py
  3. 7
      README.md
  4. 2
      couchpotato/core/_base/updater/main.py
  5. 38
      couchpotato/core/downloaders/base.py
  6. 18
      couchpotato/core/downloaders/blackhole/main.py
  7. 8
      couchpotato/core/downloaders/sabnzbd/__init__.py
  8. 106
      couchpotato/core/downloaders/sabnzbd/main.py
  9. 5
      couchpotato/core/helpers/encoding.py
  10. 2
      couchpotato/core/notifications/core/static/notification.js
  11. 9
      couchpotato/core/notifications/nmj/main.py
  12. 18
      couchpotato/core/notifications/notifymyandroid/main.py
  13. 14
      couchpotato/core/notifications/xbmc/main.py
  14. 26
      couchpotato/core/plugins/movie/static/movie.css
  15. 155
      couchpotato/core/plugins/movie/static/movie.js
  16. 1
      couchpotato/core/plugins/movie/static/search.css
  17. 4
      couchpotato/core/plugins/release/main.py
  18. 25
      couchpotato/core/plugins/renamer/__init__.py
  19. 90
      couchpotato/core/plugins/renamer/main.py
  20. 10
      couchpotato/core/plugins/scanner/main.py
  21. 80
      couchpotato/core/plugins/searcher/main.py
  22. 1
      couchpotato/core/plugins/status/main.py
  23. 2
      couchpotato/core/plugins/userscript/main.py
  24. 21
      couchpotato/core/plugins/userscript/template.js
  25. 2
      couchpotato/core/providers/movie/_modifier/main.py
  26. 2
      couchpotato/core/providers/movie/imdbapi/main.py
  27. 2
      couchpotato/core/providers/nzb/mysterbin/__init__.py
  28. 2
      couchpotato/core/providers/nzb/mysterbin/main.py
  29. 1
      couchpotato/core/providers/nzb/newzbin/__init__.py
  30. 7
      couchpotato/core/providers/nzb/newznab/main.py
  31. 2
      couchpotato/core/providers/nzb/nzbclub/__init__.py
  32. 2
      couchpotato/core/providers/nzb/nzbclub/main.py
  33. 2
      couchpotato/core/providers/nzb/nzbindex/__init__.py
  34. 2
      couchpotato/core/providers/nzb/nzbindex/main.py
  35. 1
      couchpotato/core/providers/nzb/nzbmatrix/__init__.py
  36. 5
      couchpotato/core/providers/nzb/nzbmatrix/main.py
  37. 40
      couchpotato/core/providers/nzb/nzbsrus/__init__.py
  38. 104
      couchpotato/core/providers/nzb/nzbsrus/main.py
  39. 1
      couchpotato/core/providers/torrent/kickasstorrents/__init__.py
  40. 29
      couchpotato/core/providers/torrent/kickasstorrents/main.py
  41. 36
      couchpotato/core/providers/torrent/passthepopcorn/__init__.py
  42. 254
      couchpotato/core/providers/torrent/passthepopcorn/main.py
  43. 4
      couchpotato/core/providers/torrent/publichd/__init__.py
  44. 22
      couchpotato/core/providers/torrent/publichd/main.py
  45. 1
      couchpotato/core/providers/torrent/sceneaccess/__init__.py
  46. 2
      couchpotato/core/providers/torrent/sceneaccess/main.py
  47. 1
      couchpotato/core/providers/torrent/scenehd/__init__.py
  48. 2
      couchpotato/core/providers/torrent/scenehd/main.py
  49. 2
      couchpotato/core/providers/torrent/thepiratebay/__init__.py
  50. 4
      couchpotato/core/providers/torrent/thepiratebay/main.py
  51. 1
      couchpotato/core/providers/torrent/torrentleech/__init__.py
  52. 2
      couchpotato/core/providers/torrent/torrentleech/main.py
  53. 6
      couchpotato/core/providers/trailer/hdtrailers/main.py
  54. 24
      couchpotato/core/providers/userscript/allocine/main.py
  55. 29
      couchpotato/core/providers/userscript/rottentomatoes/main.py
  56. 16
      couchpotato/core/settings/model.py
  57. 37
      couchpotato/static/scripts/couchpotato.js
  58. 11
      couchpotato/static/scripts/page/about.js
  59. 14
      couchpotato/static/scripts/page/wanted.js
  60. 29
      couchpotato/static/style/page/settings.css
  61. 3
      couchpotato/templates/_desktop.html

4
.gitignore

@ -1,3 +1,5 @@
*.pyc
/data/
/_source/
/_source/
.project
.pydevproject

6
CouchPotato.py

@ -9,7 +9,7 @@ import socket
import subprocess
import sys
import traceback
import time
# Root path
base_path = dirname(os.path.abspath(__file__))
@ -96,6 +96,10 @@ class Loader(object):
except:
self.log.critical(traceback.format_exc())
# Release log files and shutdown logger
logging.shutdown()
time.sleep(3)
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
subprocess.Popen(args)
except:

7
README.md

@ -7,11 +7,12 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s
## Running from Source
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
Windows:
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
* Install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/)
* Install [Python 2.7](http://www.python.org/download/releases/2.7.3/)
* Then install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/)
* If you come and ask on the forums 'why directory selection no work?', I will kill a kitten, also this is because you need PyWin32
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.

2
couchpotato/core/_base/updater/main.py

@ -305,7 +305,7 @@ class SourceUpdater(BaseUpdater):
if not os.path.isdir(dirname):
self.makeDir(dirname)
os.rename(fromfile, tofile)
shutil.move(fromfile, tofile)
try:
existing_files.remove(tofile)
except ValueError:

38
couchpotato/core/downloaders/base.py

@ -1,9 +1,12 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
import random
import re
log = CPLog(__name__)
@ -12,14 +15,25 @@ class Downloader(Plugin):
type = []
torrent_sources = [
'http://torrage.com/torrent/%s.torrent',
'http://torrage.ws/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent',
]
def __init__(self):
addEvent('download', self.download)
addEvent('download.status', self.getDownloadStatus)
def download(self, data = {}, movie = {}, manual = False, filedata = None):
pass
def download(self, data = {}):
def getDownloadStatus(self, data = {}, movie = {}):
pass
def createNzbName(self, data, movie):
return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
@ -41,6 +55,26 @@ class Downloader(Plugin):
return is_correct
def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
sources = self.torrent_sources
random.shuffle(sources)
for source in sources:
try:
filedata = self.urlopen(source % torrent_hash, show_error = False)
return filedata
except:
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
def isDisabled(self, manual):
return not self.isEnabled(manual)

18
couchpotato/core/downloaders/blackhole/main.py

@ -8,10 +8,12 @@ log = CPLog(__name__)
class Blackhole(Downloader):
type = ['nzb', 'torrent']
type = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
if self.isDisabled(manual) or \
(not self.isCorrectType(data.get('type')) or \
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
return
directory = self.conf('directory')
@ -20,8 +22,16 @@ class Blackhole(Downloader):
else:
try:
if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available!')
return False
try:
if data.get('type') == 'torrent_magnet':
filedata = self.magnetToTorrent(data.get('url'))
data['type'] = 'torrent'
except:
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available: %s', data.get('url'))
return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))

8
couchpotato/core/downloaders/sabnzbd/__init__.py

@ -35,11 +35,17 @@ config = [{
},
{
'name': 'manual',
'default': 0,
'default': False,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
],
}
],

106
couchpotato/core/downloaders/sabnzbd/main.py

@ -3,6 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
import traceback
import json
log = CPLog(__name__)
@ -39,14 +40,14 @@ class Sabnzbd(Downloader):
try:
if params.get('mode') is 'addfile':
data = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
else:
data = self.urlopen(url, timeout = 60, show_error = False)
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error(traceback.format_exc())
log.error('Failed sending release: %s', traceback.format_exc())
return False
result = data.strip()
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
return False
@ -61,3 +62,100 @@ class Sabnzbd(Downloader):
else:
log.error("Unknown error: " + result[:40])
return False
def getDownloadStatus(self, data = {}, movie = {}):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
return
nzbname = self.createNzbName(data, movie)
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
# Go through Queue
params = {
'apikey': self.conf('api_key'),
'mode': 'queue',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed checking status: %s', traceback.format_exc())
return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing json status: %s', traceback.format_exc())
return False
for slot in history['queue']['slots']:
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
# Go through history items
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed getting history: %s', traceback.format_exc())
return
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing history json: %s', traceback.format_exc())
return
for slot in history['history']['slots']:
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
# Delete failed download
if self.conf('delete_failed', default = True):
log.info('%s failed downloading, deleting...', slot['name'])
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
return 'failed'
else:
return slot['status'].lower()
return 'not_found'

5
couchpotato/core/helpers/encoding.py

@ -2,6 +2,7 @@ from couchpotato.core.logger import CPLog
from string import ascii_letters, digits
from urllib import quote_plus
import re
import traceback
import unicodedata
log = CPLog(__name__)
@ -30,8 +31,8 @@ def toUnicode(original, *args):
return ek(original, *args)
except:
raise
except UnicodeDecodeError:
log.error('Unable to decode value: %s... ', repr(original)[:20])
except:
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)

2
couchpotato/core/notifications/core/static/notification.js

@ -216,7 +216,7 @@ var NotificationBase = new Class({
},
testButtonName: function(fieldset){
var name = fieldset.getElement('h2').get('text');
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span")); //.get('text');
return 'Test '+name;
}

9
couchpotato/core/notifications/nmj/main.py

@ -19,7 +19,7 @@ class NMJ(Notification):
def __init__(self):
addEvent('renamer.after', self.addToLibrary)
addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig)
def autoConfig(self):
@ -76,7 +76,7 @@ class NMJ(Notification):
mount = self.conf('mount')
database = self.conf('database')
if self.mount:
if mount:
log.debug('Try to mount network drive via url: %s', (mount))
try:
data = self.urlopen(mount)
@ -114,3 +114,8 @@ class NMJ(Notification):
def failed(self):
return jsonified({'success': False})
def test(self):
return jsonified({'success': self.addToLibrary()})

18
couchpotato/core/notifications/notifymyandroid/main.py

@ -15,14 +15,22 @@ class NotifyMyAndroid(Notification):
nma.addkey(keys)
nma.developerkey(self.conf('dev_key'))
# hacky fix for the event type
# hacky fix for the event type
# as it seems to be part of the message now
self.event = message.split(' ')[0]
response = nma.push(self.default_title, self.event , message, self.conf('priority'), batch_mode = len(keys) > 1)
response = nma.push(
application = self.default_title,
event = self.event,
description = message,
priority = self.conf('priority'),
batch_mode = len(keys) > 1
)
successful = 0
for key in keys:
if not response[str(key)]['code'] == u'200':
log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
else:
successful += 1
return response
return successful == len(keys)

14
couchpotato/core/notifications/xbmc/main.py

@ -13,11 +13,15 @@ class XBMC(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
for host in [x.strip() for x in self.conf('host').split(",")]:
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host)
self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host)
return True
hosts = [x.strip() for x in self.conf('host').split(",")]
successful = 0
for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
successful += 1
return successful == len(hosts)*2
def send(self, command, host):

26
couchpotato/core/plugins/movie/static/movie.css

@ -340,6 +340,32 @@
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container {
padding: 5px 10px;
text-align: center;
}
.movies .movie .try_container a {
margin: 0 5px;
padding: 2px 5px;
}
.movies .movie .releases .next_release {
border-left: 6px solid #2aa300;
}
.movies .movie .releases .next_release > :first-child {
margin-left: -6px;
}
.movies .movie .releases .last_release {
border-left: 6px solid #ffa200;
}
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
.movies .load_more {
display: block;

155
couchpotato/core/plugins/movie/static/movie.js

@ -140,28 +140,33 @@ var Movie = new Class({
self.profile.getTypes().each(function(type){
var q = self.addQuality(type.quality_id || type.get('quality_id'));
if(type.finish == true || type.get('finish'))
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
q.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
});
// Add done releases
Array.each(self.data.releases, function(release){
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 == 'done'))
var q = self.addQuality(release.quality_id)
if (status && q)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
}
});
Object.each(self.options.actions, function(action, key){
self.actions.adopt(
self.action[key.toLowerCase()] = new self.options.actions[key](self)
)
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
if(action.el)
self.actions.adopt(action)
});
if(!self.data.library.rating)
@ -175,7 +180,8 @@ var Movie = new Class({
var q = Quality.getQuality(quality_id);
return new Element('span', {
'text': q.label,
'class': 'q_'+q.identifier + ' q_id' + q.id
'class': 'q_'+q.identifier + ' q_id' + q.id,
'title': ''
}).inject(self.quality);
},
@ -280,6 +286,31 @@ var MovieAction = new Class({
this.el.removeClass('disable')
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.movie, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
movie = $(self.movie),
s = movie.getSize()
return;
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': movie
})
},
toElement: function(){
return this.el || null
}
@ -298,19 +329,11 @@ var IMDBAction = new Class({
self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'events': {
'click': self.gotoIMDB.bind(self)
}
'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank'
});
if(!self.id) self.disable();
},
gotoIMDB: function(e){
var self = this;
(e).preventDefault();
window.open('http://www.imdb.com/title/'+self.id+'/');
}
});
@ -318,13 +341,10 @@ var IMDBAction = new Class({
var ReleaseAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
@ -332,15 +352,33 @@ var ReleaseAction = new Class({
}
});
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
},
show: function(e){
var self = this;
(e).preventDefault();
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table')
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
// Header
@ -354,29 +392,35 @@ var ReleaseAction = new Class({
new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container)
Array.each(self.movie.data.releases, function(release){
self.movie.data.releases.sortBy('-info.score').each(function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info;
try {
var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value;
} catch(e){}
if( status.identifier == 'ignored' || status.identifier == 'failed'){
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
// Create release
new Element('div', {
'class': 'item '+status.identifier,
'class': 'item '+status.identifier +
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.label || 'n/a'}),
new Element('span.size', {'text': (self.get(release, 'size'))}),
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}),
details_url ? new Element('a.info.icon', {
'href': details_url,
release.info['detail_url'] ? new Element('a.info.icon', {
'href': release.info['detail_url'],
'target': '_blank'
}) : null,
new Element('a.download.icon', {
@ -400,17 +444,37 @@ var ReleaseAction = new Class({
).inject(self.release_container)
});
self.trynext_container.adopt(
new Element('span.or', {
'text': 'Download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
'events': {
'click': self.trySameRelease.bind(self)
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': 'or'
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('span.or', {
'text': 'or pick one below'
})] : null
)
}
self.movie.slide('in', self.options_container);
},
get: function(release, type){
var self = this;
return (release.info.filter(function(info){
return type == info.identifier
}).pick() || {}).value || 'n/a'
return release.info[type] || 'n/a'
},
download: function(release){
@ -444,6 +508,25 @@ var ReleaseAction = new Class({
}
})
},
tryNextRelease: function(movie_id){
var self = this;
if(self.last_release)
self.ignore(self.last_release);
if(self.next_release)
self.download(self.next_release);
},
trySameRelease: function(movie_id){
var self = this;
if(self.last_release)
self.download(self.last_release);
}
});

1
couchpotato/core/plugins/movie/static/search.css

@ -186,7 +186,6 @@
.movie_result .info h2 span {
padding: 0 5px;
content: ")";
}
.movie_result .info h2 span:before { content: "("; }

4
couchpotato/core/plugins/release/main.py

@ -170,7 +170,9 @@ class Release(Plugin):
# Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
item['download'] = provider.download
if item['type'] != 'torrent_magnet':
item['download'] = provider.download
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}},

25
couchpotato/core/plugins/renamer/__init__.py

@ -74,6 +74,22 @@ config = [{
'default': False,
},
{
'advanced': True,
'name': 'run_every',
'label': 'Run every',
'default': 1,
'type': 'int',
'unit': 'min(s)',
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
},
{
'advanced': True,
'name': 'next_on_failed',
'default': True,
'type': 'bool',
'description': 'Try the next best release for a movie after a download failed.',
},
{
'name': 'move_leftover',
'type': 'bool',
'description': 'Move all leftover file after renaming, to the movie folder.',
@ -86,15 +102,6 @@ config = [{
'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
},
{
'advanced': True,
'name': 'run_every',
'label': 'Run every',
'default': 1,
'type': 'int',
'unit': 'min(s)',
'description': 'Search for new movies inside the folder every X minutes.',
},
],
}, {
'tab': 'renamer',

90
couchpotato/core/plugins/renamer/main.py

@ -6,13 +6,13 @@ from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile
from couchpotato.core.settings.model import Library, File, Profile, Release
from couchpotato.environment import Env
import errno
import os
import re
import shutil
import traceback
import errno
log = CPLog(__name__)
@ -28,9 +28,11 @@ class Renamer(Plugin):
})
addEvent('renamer.scan', self.scan)
addEvent('renamer.check_snatched', self.checkSnatched)
addEvent('app.load', self.scan)
fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every'))
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
def scanView(self):
@ -314,6 +316,7 @@ class Renamer(Plugin):
break
# Remove files
delete_folders = []
for src in remove_files:
if isinstance(src, File):
@ -327,10 +330,19 @@ class Renamer(Plugin):
try:
if os.path.isfile(src):
os.remove(src)
parent_dir = os.path.normpath(os.path.dirname(src))
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
delete_folders.append(parent_dir)
except:
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
self.tagDir(group, 'failed_remove')
# Delete leftover folder from older releases
for delete_folder in delete_folders:
self.deleteEmptyFolder(delete_folder, show_error = False)
# Rename all files marked
group['renamed_files'] = []
for src in rename_files:
@ -464,8 +476,9 @@ class Renamer(Plugin):
def replaceDoubles(self, string):
return string.replace(' ', ' ').replace(' .', '.')
def deleteEmptyFolder(self, folder):
def deleteEmptyFolder(self, folder, show_error = True):
loge = log.error if show_error else log.debug
for root, dirs, files in os.walk(folder):
for dir_name in dirs:
@ -474,9 +487,74 @@ class Renamer(Plugin):
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
loge('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
try:
os.rmdir(folder)
except:
log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
done_status = fireEvent('status.get', 'done', single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
if rels:
log.debug('Checking status snatched releases...')
scan_required = False
for rel in rels:
# Get current selected title
default_title = ''
for title in rel.movie.library.titles:
if title.default: default_title = title.title
# Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
db.commit()
continue
item = {}
for info in rel.info:
item[info.identifier] = info.value
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
if not downloadstatus:
log.debug('Download status functionality is not implemented for active downloaders.')
scan_required = True
else:
log.debug('Download status: %s' , downloadstatus)
if downloadstatus == 'failed':
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
db.commit()
log.info('Download of %s failed.', item['name'])
elif downloadstatus == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
elif downloadstatus == 'not_found':
log.info('%s not found in downloaders', item['name'])
rel.status_id = ignored_status.get('id')
db.commit()
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
if scan_required:
fireEvent('renamer.scan')

10
couchpotato/core/plugins/scanner/main.py

@ -303,7 +303,15 @@ class Scanner(Plugin):
break
if file_too_new:
log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time.ctime(file_time[0]), identifier))
try:
time_string = time.ctime(file_time[0])
except:
try:
time_string = time.ctime(file_time[1])
except:
time_string = 'unknown'
log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time_string, identifier))
# Delete the unsorted list
del group['unsorted_files']

80
couchpotato/core/plugins/searcher/main.py

@ -1,7 +1,9 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import md5, getImdb, getTitle
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
@ -21,15 +23,24 @@ class Searcher(Plugin):
in_progress = False
def __init__(self):
addEvent('searcher.all', self.all_movies)
addEvent('searcher.all', self.allMovies)
addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
addEvent('searcher.try_next_release', self.tryNextRelease)
addApiView('searcher.try_next', self.tryNextReleaseView, docs = {
'desc': 'Marks the snatched results as ignored and try the next best release',
'params': {
'id': {'desc': 'The id of the movie'},
},
})
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def all_movies(self):
def allMovies(self):
if self.in_progress:
log.info('Search already in progress')
@ -138,7 +149,7 @@ class Searcher(Plugin):
for info in nzb:
try:
if not isinstance(nzb[info], (str, unicode, int, long)):
if not isinstance(nzb[info], (str, unicode, int, long, float)):
continue
rls_info = ReleaseInfo(
@ -239,7 +250,6 @@ class Searcher(Plugin):
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
imdb_results = kwargs.get('imdb_results', False)
single_category = kwargs.get('single_category', False)
retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
@ -272,7 +282,7 @@ class Searcher(Plugin):
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single_category = single_category):
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False
@ -317,14 +327,10 @@ class Searcher(Plugin):
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
# Get the nfo and see if it contains the proper imdb url
if self.checkNFO(nzb['name'], movie['library']['identifier']):
return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}):
name = nzb['name']
size = nzb.get('size', 0)
@ -355,9 +361,6 @@ class Searcher(Plugin):
if found.get(allowed):
del found[allowed]
if (len(found) == 0 and single_category):
return False
return not (found.get(preferred_quality['identifier']) and len(found) == 1)
def checkIMDB(self, haystack, imdbId):
@ -398,19 +401,6 @@ class Searcher(Plugin):
return False
def checkNFO(self, check_name, imdb_id):
cache_key = 'srrdb.com %s' % simplifyString(check_name)
nfo = self.getCache(cache_key)
if not nfo:
try:
nfo = self.urlopen('http://www.srrdb.com/showfile.php?release=%s' % check_name, show_error = False)
self.setCache(cache_key, nfo)
except:
pass
return nfo and getImdb(nfo) == imdb_id
def couldBeReleased(self, wanted_quality, dates, pre_releases):
now = int(time.time())
@ -439,3 +429,37 @@ class Searcher(Plugin):
return False
def tryNextReleaseView(self):
trynext = self.tryNextRelease(getParam('id'))
return jsonified({
'success': trynext
})
def tryNextRelease(self, movie_id, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
try:
db = get_session()
rels = db.query(Release).filter_by(
status_id = snatched_status.get('id'),
movie_id = movie_id
).all()
for rel in rels:
rel.status_id = ignored_status.get('id')
db.commit()
movie_dict = fireEvent('movie.get', movie_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
fireEvent('searcher.single', movie_dict)
return True
except:
log.error('Failed searching for next release: %s', traceback.format_exc())
return False

1
couchpotato/core/plugins/status/main.py

@ -19,6 +19,7 @@ class StatusPlugin(Plugin):
'downloaded': 'Downloaded',
'wanted': 'Wanted',
'snatched': 'Snatched',
'failed': 'Failed',
'deleted': 'Deleted',
'ignored': 'Ignored',
}

2
couchpotato/core/plugins/userscript/main.py

@ -15,7 +15,7 @@ log = CPLog(__name__)
class Userscript(Plugin):
version = 2
version = 3
def __init__(self):
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)

21
couchpotato/core/plugins/userscript/template.js

@ -1,6 +1,7 @@
// ==UserScript==
// @name CouchPotato UserScript
// @description Add movies like a real CouchPotato
// @grant none
// @version {{version}}
// @match {{host}}*
@ -44,21 +45,19 @@ function create() {
return A;
}
if (typeof GM_addStyle == 'undefined'){
GM_addStyle = function(css) {
var head = document.getElementsByTagName('head')[0],
style = document.createElement('style');
if (!head)
return;
var addStyle = function(css) {
var head = document.getElementsByTagName('head')[0],
style = document.createElement('style');
if (!head)
return;
style.type = 'text/css';
style.textContent = css;
head.appendChild(style);
}
style.type = 'text/css';
style.textContent = css;
head.appendChild(style);
}
// Styles
GM_addStyle('\
addStyle('\
#cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \
#cp_popup.opened { width: 492px; } \
#cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \

2
couchpotato/core/providers/movie/_modifier/main.py

@ -31,7 +31,7 @@ class MovieResultModifier(Plugin):
order.append(imdb)
if item.get('via_imdb'):
if order.index(imdb):
if order.count(imdb):
order.remove(imdb)
order.insert(0, imdb)

2
couchpotato/core/providers/movie/imdbapi/main.py

@ -27,7 +27,7 @@ class IMDBAPI(MovieProvider):
name_year = fireEvent('scanner.name_year', q, single = True)
if not q or not name_year.get('name'):
if not q or not name_year or (name_year and not name_year.get('name')):
return []
cache_key = 'imdbapi.cache.%s' % q

2
couchpotato/core/providers/nzb/mysterbin/__init__.py

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'Mysterbin',
'description': '',
'description': 'Free provider, less accurate. See <a href="http://www.mysterbin.com/">Mysterbin</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/nzb/mysterbin/main.py

@ -88,7 +88,7 @@ class Mysterbin(NZBProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)

1
couchpotato/core/providers/nzb/newzbin/__init__.py

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'newzbin',
'description': 'See <a href="https://www.newzbin2.es/">Newzbin</a>',
'wizard': True,
'options': [
{

7
couchpotato/core/providers/nzb/newznab/main.py

@ -96,13 +96,12 @@ class Newznab(NZBProvider, RSS):
url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments)
cache_key = 'newznab.%s.%s.%s' % (host['host'], movie['library']['identifier'], cat_id[0])
single_cat = (len(cat_id) == 1 and cat_id[0] != self.cat_backup_id)
results = self.createItems(url, cache_key, host, single_cat = single_cat, movie = movie, quality = quality)
results = self.createItems(url, cache_key, host, movie = movie, quality = quality)
return results
def createItems(self, url, cache_key, host, single_cat = False, movie = None, quality = None, for_feed = False):
def createItems(self, url, cache_key, host, movie = None, quality = None, for_feed = False):
results = []
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
@ -146,7 +145,7 @@ class Newznab(NZBProvider, RSS):
if not for_feed:
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True)
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)

2
couchpotato/core/providers/nzb/nzbclub/__init__.py

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'NZBClub',
'description': '',
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/nzb/nzbclub/main.py

@ -86,7 +86,7 @@ class NZBClub(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)

2
couchpotato/core/providers/nzb/nzbindex/__init__.py

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbindex',
'description': 'Free provider, but less accurate.',
'description': 'Free provider, less accurate. See <a href="http://www.nzbindex.nl/">NZBIndex</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/nzb/nzbindex/main.py

@ -93,7 +93,7 @@ class NzbIndex(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)

1
couchpotato/core/providers/nzb/nzbmatrix/__init__.py

@ -11,6 +11,7 @@ config = [{
'subtab': 'providers',
'name': 'nzbmatrix',
'label': 'NZBMatrix',
'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>',
'wizard': True,
'options': [
{

5
couchpotato/core/providers/nzb/nzbmatrix/main.py

@ -22,7 +22,7 @@ class NZBMatrix(NZBProvider, RSS):
cat_ids = [
([50], ['bd50']),
([42, 53], ['720p', '1080p']),
([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([2, 9], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([54], ['brrip']),
([1], ['dvdr']),
]
@ -49,7 +49,6 @@ class NZBMatrix(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments)
cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids)
single_cat = True
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
if data:
@ -86,7 +85,7 @@ class NZBMatrix(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True)
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)

40
couchpotato/core/providers/nzb/nzbsrus/__init__.py

@ -0,0 +1,40 @@
from .main import Nzbsrus
def start():
return Nzbsrus()
config = [{
'name': 'nzbsrus',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbsrus',
'label': 'Nzbsrus',
'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'userid',
'label': 'User ID',
},
{
'name': 'api_key',
'default': '',
'label': 'Api Key',
},
{
'name': 'english_only',
'default': 1,
'type': 'bool',
'label': 'English only',
'description': 'Only search for English spoken movies on Nzbsrus',
},
],
},
],
}]

104
couchpotato/core/providers/nzb/nzbsrus/main.py

@ -0,0 +1,104 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
import time
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class Nzbsrus(NZBProvider, RSS):
urls = {
'download': 'https://www.nzbsrus.com/nzbdownload_rss.php/%s',
'detail': 'https://www.nzbsrus.com/nzbdetails.php?id=%s',
'search': 'https://www.nzbsrus.com/api.php?extended=1&xml=1&listname={date,grabs}',
}
cat_ids = [
([90, 45, 51], ['720p', '1080p', 'brrip', 'bd50', 'dvdr']),
([48, 51], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
]
cat_backup_id = 240
def search(self, movie, quality):
results = []
if self.isDisabled():
return results
cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))])
arguments = tryUrlencode({
'searchtext': 'imdb:' + movie['library']['identifier'][2:],
'uid': self.conf('userid'),
'key': self.conf('api_key'),
'age': Env.setting('retention', section = 'nzb'),
})
# check for english_only
if self.conf('english_only'):
arguments += "&lang0=1&lang3=1&lang1=1"
url = "%s&%s&%s" % (self.urls['search'], arguments , cat_id_string)
cache_key = 'nzbsrus_1.%s.%s' % (movie['library'].get('identifier'), cat_id_string)
single_cat = True
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
if data:
try:
try:
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'results/result')
except Exception, e:
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
title = self.getTextElement(nzb, "name")
if 'error' in title.lower(): continue
id = self.getTextElement(nzb, "id")
size = int(round(int(self.getTextElement(nzb, "size")) / 1048576))
age = int(round((time.time() - int(self.getTextElement(nzb, "postdate"))) / 86400))
new = {
'id': id,
'type': 'nzb',
'provider': self.getName(),
'name': title,
'age': age,
'size': size,
'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, "key"),
'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "addtext"),
'check_nzb': True,
}
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from Nzbsrus.com')
return results
def download(self, url = '', nzb_id = ''):
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
def getApiExt(self):
return '/%s/' % (self.conf('userid'))

1
couchpotato/core/providers/torrent/kickasstorrents/__init__.py

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'KickAssTorrents',
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
'options': [
{
'name': 'enabled',

29
couchpotato/core/providers/torrent/kickasstorrents/main.py

@ -3,8 +3,6 @@ from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import StringIO
import gzip
import re
import traceback
@ -14,10 +12,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider):
urls = {
'test': 'http://www.kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html',
'search': 'http://www.kat.ph/i%s/',
'download': 'http://torcache.net/',
'test': 'http://kat.ph/',
'detail': 'http://kat.ph/%s',
'search': 'http://kat.ph/i%s/',
}
cat_ids = [
@ -60,11 +57,10 @@ class KickAssTorrents(TorrentProvider):
continue
new = {
'type': 'torrent',
'type': 'torrent_magnet',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'download': self.download,
'score': 0,
}
@ -77,9 +73,8 @@ class KickAssTorrents(TorrentProvider):
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
new['id'] = temp.get('id')[-8:]
new['name'] = link.text
new['url'] = td.find_all('a', 'idownload')[1]['href']
if new['url'][:2] == '//':
new['url'] = 'http:%s' % new['url']
new['url'] = td.find('a', 'imagnet')['href']
new['detail_url'] = self.urls['detail'] % link['href'][1:]
new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
@ -95,7 +90,7 @@ class KickAssTorrents(TorrentProvider):
new['score'] += fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = False, single = True)
imdb_results = True, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
@ -129,13 +124,3 @@ class KickAssTorrents(TorrentProvider):
age += tryInt(nr) * mult
return tryInt(age)
def download(self, url = '', nzb_id = ''):
compressed_data = self.urlopen(url = url, headers = {'Referer': 'http://kat.ph/'})
compressedstream = StringIO.StringIO(compressed_data)
gzipper = gzip.GzipFile(fileobj = compressedstream)
data = gzipper.read()
return data

36
couchpotato/core/providers/torrent/passthepopcorn/__init__.py

@ -0,0 +1,36 @@
from main import PassThePopcorn
def start():
return PassThePopcorn()
config = [{
'name': 'passthepopcorn',
'groups': [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'PassThePopcorn',
'description': 'See <a href="http://passthepopcorn.me">PassThePopcorn.me</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).',
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
}
],
}]
}]

254
couchpotato/core/providers/torrent/passthepopcorn/main.py

@ -0,0 +1,254 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
from dateutil.parser import parse
import cookielib
import htmlentitydefs
import json
import re
import time
import traceback
import urllib2
log = CPLog(__name__)
class PassThePopcorn(TorrentProvider):
urls = {
'domain': 'https://tls.passthepopcorn.me',
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/login.php',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
}
quality_search_params = {
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'},
'720p': {'resolution': '720p'},
'brrip': {'media': 'Blu-ray'},
'dvdr': {'resolution': 'anysd'},
'dvdrip': {'media': 'DVD'},
'scr': {'media': 'DVD-Screener'},
'r5': {'media': 'R5'},
'tc': {'media': 'TC'},
'ts': {'media': 'TS'},
'cam': {'media': 'CAM'}
}
post_search_filters = {
'bd50': {'Codec': ['BD50']},
'1080p': {'Resolution': ['1080p']},
'720p': {'Resolution': ['720p']},
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
'dvdr': {'Codec': ['DVD5', 'DVD9']},
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
'scr': {'Source': ['DVD-Screener']},
'r5': {'Source': ['R5']},
'tc': {'Source': ['TC']},
'ts': {'Source': ['TS']},
'cam': {'Source': ['CAM']}
}
class NotLoggedInHTTPError(urllib2.HTTPError):
def __init__(self, url, code, msg, headers, fp):
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
log.debug("302 detected; redirected to %s" % headers['Location'])
if (headers['Location'] != 'login.php'):
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
else:
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
def search(self, movie, quality):
results = []
if self.isDisabled():
return results
movie_title = getTitle(movie['library'])
quality_id = quality['identifier']
log.info('Searching for %s at quality %s' % (movie_title, quality_id))
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
'order_by': 'relevance',
'order_way': 'descending',
'searchstr': movie['library']['identifier']
})
# Do login for the cookies
if not self.login_opener and not self.login():
return results
try:
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
txt = self.urlopen(url, opener = self.login_opener)
res = json.loads(txt)
except:
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
return []
try:
if not 'Movies' in res:
log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params))
return []
authkey = res['AuthKey']
passkey = res['PassKey']
for ptpmovie in res['Movies']:
if not 'Torrents' in ptpmovie:
log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year']))
continue
log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
for torrent in ptpmovie['Torrents']:
torrent_id = tryInt(torrent['Id'])
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec'])
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
torrentdesc += ' HQ'
if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene'
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
# eliminate odd characters...
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
torrentdesc += ' (%s)' % quality_id
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc))
def extra_check(item):
return self.torrentMeetsQualitySpec(item, type)
def extra_score(item):
return 50 if torrent['GoldenPopcorn'] else 0
new = {
'id': torrent_id,
'type': 'torrent',
'provider': self.getName(),
'name': torrent_name,
'description': '',
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
'detail_url': self.urls['detail'] % torrent_id,
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
'size': tryInt(torrent['Size']) / (1024 * 1024),
'provider': self.getName(),
'seeders': tryInt(torrent['Seeders']),
'leechers': tryInt(torrent['Leechers']),
'extra_score': extra_score,
'extra_check': extra_check,
'download': self.loginDownload,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality):
results.append(new)
self.found(new)
return results
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
return []
def login(self):
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
opener.addheaders = [
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-gb,en;q=0.5'),
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
('Keep-Alive', '115'),
('Connection', 'keep-alive'),
('Cache-Control', 'max-age=0'),
]
try:
response = opener.open(self.urls['login'], self.getLoginParams())
except urllib2.URLError as e:
log.error('Login to PassThePopcorn failed: %s' % e)
return False
if response.getcode() == 200:
log.debug('Login HTTP status 200; seems successful')
self.login_opener = opener
return True
else:
log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode())
return False
def torrentMeetsQualitySpec(self, torrent, quality):
if not quality in self.post_search_filters:
return True
for field, specs in self.post_search_filters[quality].items():
matches_one = False
seen_one = False
if not field in torrent:
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality))
continue
for spec in specs:
if len(spec) > 0 and spec[0] == '!':
# a negative rule; if the field matches, return False
if torrent[field] == spec[1:]:
return False
else:
# a positive rule; if any of the possible positive values match the field, return True
seen_one = True
if torrent[field] == spec:
matches_one = True
if seen_one and not matches_one:
return False
return True
def htmlToUnicode(self, text):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
# character reference
try:
if text[:3] == "&#x":
return unichr(int(text[3:-1], 16))
else:
return unichr(int(text[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
return re.sub("&#?\w+;", fixup, u'%s' % text)
def unicodeToASCII(self, text):
import unicodedata
return ''.join(c for c in unicodedata.normalize('NFKD', text) if unicodedata.category(c) != 'Mn')
def htmlToASCII(self, text):
return self.unicodeToASCII(self.htmlToUnicode(text))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'keeplogged': '1',
'login': 'Login'
})

4
couchpotato/core/providers/torrent/publichd/__init__.py

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'PublicHD',
'description': 'Public Torrent site with only HD content.',
'description': 'Public Torrent site with only HD content. See <a href="http://publichd.eu/">PublicHD</a>',
'options': [
{
'name': 'enabled',
@ -20,4 +20,4 @@ config = [{
],
},
],
}]
}]

22
couchpotato/core/providers/torrent/publichd/main.py

@ -15,19 +15,9 @@ class PublicHD(TorrentProvider):
urls = {
'test': 'http://publichd.eu',
'download': 'http://publichd.eu/%s',
'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s',
'search': 'http://publichd.eu/index.php',
}
cat_ids = [
([9], ['bd50']),
([5], ['1080p']),
([2], ['720p']),
([15, 16], ['brrip']),
]
cat_backup_id = 0
http_time_between_calls = 0
def search(self, movie, quality):
@ -39,9 +29,8 @@ class PublicHD(TorrentProvider):
params = tryUrlencode({
'page':'torrents',
'search': getTitle(movie['library']) + ' ' + quality['identifier'],
'search': '%s %s' % (getTitle(movie['library']), movie['library']['year']),
'active': 1,
'category': self.getCatId(quality['identifier'])[0]
})
url = '%s?%s' % (self.urls['search'], params)
@ -58,7 +47,7 @@ class PublicHD(TorrentProvider):
for result in entries[2:len(entries) - 1]:
info_url = result.find(href = re.compile('torrent-details'))
download = result.find(href = re.compile('\.torrent'))
download = result.find(href = re.compile('magnet:'))
if info_url and download:
@ -67,12 +56,11 @@ class PublicHD(TorrentProvider):
new = {
'id': url['id'][0],
'name': info_url.string,
'type': 'torrent',
'type': 'torrent_magnet',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'download': self.download,
'url': self.urls['download'] % download['href'],
'url': download['href'],
'detail_url': self.urls['detail'] % url['id'][0],
'size': self.parseSize(result.find_all('td')[7].string),
'seeders': tryInt(result.find_all('td')[4].string),
@ -82,7 +70,7 @@ class PublicHD(TorrentProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)

1
couchpotato/core/providers/torrent/sceneaccess/__init__.py

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'SceneAccess',
'description': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/torrent/sceneaccess/main.py

@ -86,7 +86,7 @@ class SceneAccess(TorrentProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)

1
couchpotato/core/providers/torrent/scenehd/__init__.py

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'SceneHD',
'description': 'See <a href="http://scenehd.org">SceneHD</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/torrent/scenehd/main.py

@ -79,7 +79,7 @@ class SceneHD(TorrentProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = imdb_results, single_category = False, single = True)
imdb_results = imdb_results, single = True)
if is_correct_movie:
results.append(new)

2
couchpotato/core/providers/torrent/thepiratebay/__init__.py

@ -9,7 +9,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'ThePirateBay',
'description': 'The world\'s largest bittorrent tracker.',
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
'options': [
{
'name': 'enabled',

4
couchpotato/core/providers/torrent/thepiratebay/main.py

@ -37,6 +37,8 @@ class ThePirateBay(TorrentProvider):
'https://piratereverse.info',
'https://tpb.pirateparty.org.uk',
'https://argumentomteemigreren.nl',
'https://livepirate.com/',
'https://www.getpirate.com/',
]
def __init__(self):
@ -120,7 +122,7 @@ class ThePirateBay(TorrentProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)

1
couchpotato/core/providers/torrent/torrentleech/__init__.py

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'TorrentLeech',
'description': 'See <a href="http://torrentleech.org">TorrentLeech</a>',
'options': [
{
'name': 'enabled',

2
couchpotato/core/providers/torrent/torrentleech/main.py

@ -80,7 +80,7 @@ class TorrentLeech(TorrentProvider):
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = imdb_results, single_category = False, single = True)
imdb_results = imdb_results, single = True)
if is_correct_movie:
results.append(new)

6
couchpotato/core/providers/trailer/hdtrailers/main.py

@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits
from string import digits, ascii_letters
import re
log = CPLog(__name__)
@ -46,7 +46,7 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library'])
url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name}))
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try:
@ -100,7 +100,7 @@ class HDTrailers(TrailerProvider):
return results
def movieUrlName(self, string):
safe_chars = letters + digits + ' '
safe_chars = ascii_letters + digits + ' '
r = ''.join([char if char in safe_chars else ' ' for char in string])
name = re.sub('\s+' , '-', r).lower()

24
couchpotato/core/providers/userscript/allocine/main.py

@ -1,5 +1,9 @@
from bs4 import BeautifulSoup
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.userscript.base import UserscriptBase
import traceback
log = CPLog(__name__)
class AlloCine(UserscriptBase):
@ -15,11 +19,17 @@ class AlloCine(UserscriptBase):
except:
return
html = BeautifulSoup(data)
title = html.find('title').contents[0].strip()
split = title.split(') - ')
name = None
year = None
name = split[0][:-5].strip()
year = split[0][-4:]
try:
start = data.find('<title>')
end = data.find('</title>', start)
page_title = data[start + len('<title>'):end].strip().split('-')
name = page_title[0].strip()
year = page_title[1].strip()[-4:]
return self.search(name, year)
except:
log.error('Failed parsing page for title and year: %s', traceback.format_exc())
return self.search(name, year)

29
couchpotato/core/providers/userscript/rottentomatoes/main.py

@ -1,6 +1,10 @@
from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.userscript.base import UserscriptBase
import re
import traceback
log = CPLog(__name__)
class RottenTomatoes(UserscriptBase):
@ -16,7 +20,20 @@ class RottenTomatoes(UserscriptBase):
except:
return
html = BeautifulSoup(data)
title = html.find('span', {'itemprop':'name'}).text
info = fireEvent('scanner.name_year', title, single = True)
return self.search(info['name'], info['year'])
try:
name = None
year = None
metas = re.findall("property=\"(video:release_date|og:title)\" content=\"([^\"]*)\"", data)
for meta in metas:
mname, mvalue = meta
if mname == 'og:title':
name = mvalue.decode('unicode_escape')
elif mname == 'video:release_date':
year = mvalue[:4]
if name and year:
return self.search(name, year)
except:
log.error('Failed parsing page for title and year: %s', traceback.format_exc())

16
couchpotato/core/settings/model.py

@ -103,6 +103,22 @@ class Release(Entity):
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []):
orig_dict = super(Release, self).to_dict(deep = deep, exclude = exclude)
new_info = {}
for info in orig_dict.get('info', []):
value = info['value']
try: value = int(info['value'])
except: pass
new_info[info['identifier']] = value
orig_dict['info'] = new_info
return orig_dict
class ReleaseInfo(Entity):
"""Properties that can be bound to a file for off-line usage"""

37
couchpotato/static/scripts/couchpotato.js

@ -29,6 +29,7 @@ var CouchPotato = new Class({
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));
},
getOption: function(name){
@ -187,7 +188,7 @@ var CouchPotato = new Class({
restart: function(message, title){
var self = this;
self.blockPage(message || 'Restarting... please wait. If this takes to long, something must have gone wrong.', title);
self.blockPage(message || 'Restarting... please wait. If this takes too long, something must have gone wrong.', title);
Api.request('app.restart');
self.checkAvailable(1000);
},
@ -216,7 +217,7 @@ var CouchPotato = new Class({
Updater.check(onComplete)
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
self.blockPage('Please wait. If this takes too long, something must have gone wrong.', 'Checking for updates');
self.checkAvailable(3000);
},
@ -269,6 +270,17 @@ var CouchPotato = new Class({
createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
},
openDerefered: function(e, el){
(e).stop();
var url = 'http://www.dereferer.org/?' + el.get('href');
if(el.get('target') == '_blank' || (e.meta && Browser.Platform.mac) || (e.control && !Browser.Platform.mac))
window.open(url);
else
window.location = url;
}
});
@ -419,15 +431,18 @@ function randomString(length, extra) {
return 0;
};
Array.implement('sortBy', function(){
keyPaths.empty();
Array.each(arguments, function(argument) {
switch (typeOf(argument)) {
case "array": saveKeyPath(argument); break;
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
}
});
return this.sort(comparer);
Array.implement({
sortBy: function(){
keyPaths.empty();
Array.each(arguments, function(argument) {
switch (typeOf(argument)) {
case "array": saveKeyPath(argument); break;
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
}
});
return this.sort(comparer);
}
});
})();

11
couchpotato/static/scripts/page/about.js

@ -58,6 +58,8 @@ var AboutSettingTab = new Class({
}
}
}),
new Element('dt[text=Updater]'),
self.updater_type = new Element('dd.updater'),
new Element('dt[text=ID]'),
new Element('dd', {'text': App.getOption('pid')}),
new Element('dt[text=Directories]'),
@ -103,12 +105,8 @@ var AboutSettingTab = new Class({
),
new Element('div.donate', {
'html':
'Or, buy me a (24 pack) Pepsi, for while I\'m coding ;)' +
'<form action="https://www.paypal.com/cgi-bin/webscr" method="post">' +
'<input type="hidden" name="cmd" value="_s-xclick">' +
'<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----MIIHPwYJKoZIhvcNAQcEoIIHMDCCBywCAQExggEwMIIBLAIBADCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwDQYJKoZIhvcNAQEBBQAEgYBUq4nmDbyDV07WGd0wijGKDf/OWNA7hd2NRaxTaCVyAoaZQEGE0DQuDUHBBk7/oqWTo5Rcp1XN0A0nbYkrajWgY21lzSivGrDlWys1UjZaq0JOI89WWcy4YJMWX8chjECxicmVvk2OWgI/SOe7fhHdK4BNhQZO9ccLpfxTi2XnEDELMAkGBSsOAwIaBQAwgbwGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQI0YRtA8KWmG6AgZjKL/bDyL4JG3JN/GlKsb6863opfWLUjwJf7P7DeR10j0YZQds516TcRrSLqCSoII9KpivUUBCMknWmch8xUy4i0tyb26aNh3un7HQ6lVBQLGfnqVvKFC0iUNa6i0gTLufDKuVjzl+WkqqiOvgsg8rAE3IG2oYBCAAgzJbvyZkD4SoMr74pWAvQS19gwGG56JWNIdCy5eTXu6CCA4cwggODMIIC7KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTAeFw0wNDAyMTMxMDEzMTVaFw0zNTAyMTMxMDEzMTVaMIGOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC1BheVBhbCBJbmMuMRMwEQYDVQQLFApsaXZlX2NlcnRzMREwDwYDVQQDFAhsaXZlX2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwUdO3fxEzEtcnI7ZKZL412XvZPugoni7i7D7prCe0AtaHTc97CYgm7NsAtJyxNLixmhLV8pyIEaiHXWAh8fPKW+R017+EmXrr9EaquPmsVvTywAAE1PMNOKqo2kl4Gxiz9zZqIajOm1fZGWcGS0f5JQ2kBqNbvbg2/Za+GJ/qwUCAwEAAaOB7jCB6zAdBgNVHQ4EFgQUlp98u8ZvF71ZP1LXChvsENZklGswgbsGA1UdIwSBszCBsIAUlp98u8ZvF71ZP1LXChvsENZklGuhgZSkgZEwgY4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLUGF5UGFsIEluYy4xEzARBgNVBAsUCmxpdmVfY2VydHMxETAPBgNVBAMUCGxpdmVfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAgV86VpqAWuXvX6Oro4qJ1tYVIT5DgWpE692Ag422H7yRIr/9j/iKG4Thia/Oflx4TdL+IFJBAyPK9v6zZNZtBgPBynXb048hsP16l2vi0k5Q2JKiPDsEfBhGI+HnxLXEaUWAcVfCsQFvd2A1sxRr67ip5y2wwBelUecP3AjJ+YcxggGaMIIBlgIBATCBlDCBjjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtQYXlQYWwgSW5jLjETMBEGA1UECxQKbGl2ZV9jZXJ0czERMA8GA1UEAxQIbGl2ZV9hcGkxHDAaBgkqhkiG9w0BCQEWDXJlQHBheXBhbC5jb20CAQAwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTEwMDcyNjA4NDA0NlowIwYJKoZIhvcNAQkEMRYEFICseROR67FmINx7sa6IYP7eCVoaMA0GCSqGSIb3DQEBAQUABIGAfDx2KDyUHT6ISrTSnqtVWUHJWGjtM2T41m464maJ6nH7pEu6JZUHf53vD7Ey7d0MLFmF3IfGyIw2zAGfyEJHldeluPccFLhDmrDbRdxM0D/zwtWrYUwVXKQ4v3rskdp0avadX9ZRWrQplJkVsJDcLvRY4P/EhScBiA5ughJS7xc=-----END PKCS7-----">' +
'<input type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">' +
'</form>'
'Or support me via:' +
'<iframe src="http://couchpota.to/donate.html" style="border:none; height: 200px;" scrolling="no"></iframe>'
})
);
@ -119,6 +117,7 @@ var AboutSettingTab = new Class({
var self = this;
var date = new Date(json.version.date * 1000);
self.version_text.set('text', json.version.hash + ' ('+date.toUTCString()+')');
self.updater_type.set('text', json.version.type);
}
});

14
couchpotato/static/scripts/page/wanted.js

@ -31,7 +31,6 @@ window.addEvent('domready', function(){
'IMDB': IMDBAction
,'Trailer': TrailerAction
,'Releases': ReleaseAction
,'Edit': new Class({
Extends: MovieAction,
@ -74,20 +73,23 @@ window.addEvent('domready', function(){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
});
Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'value': profile_id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
if(self.movie.profile)
self.profile_select.set('value', profile.id ? profile.id : profile.data.id);
if(self.movie.profile && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile_id);
});
}
@ -170,7 +172,7 @@ window.addEvent('domready', function(){
(e).preventDefault();
if(!self.delete_container){
self.delete_container = new Element('div.delete_container').adopt(
self.delete_container = new Element('div.buttons.delete_container').adopt(
new Element('a.cancel', {
'text': 'Cancel',
'events': {

29
couchpotato/static/style/page/settings.css

@ -24,39 +24,36 @@
}
.page.settings .tabs a {
display: block;
padding: 11px 15px;
padding: 7px 15px;
font-weight: normal;
transition: all 0.1s ease-in-out;
transition: all 0.3s ease-in-out;
color: rgba(255, 255, 255, 0.8);
text-shadow: none;
}
.page.settings .tabs a:hover, .page.settings .tabs .active a {
.page.settings .tabs a:hover,
.page.settings .tabs .active a {
background: rgb(78, 89, 105);
font-weight: bold;
font-size: 25px;
color: #fff;
}
.page.settings .tabs > li {
border-bottom: 1px solid rgb(78, 89, 105);
}
.page.settings .tabs .subtabs {
list-style: none;
padding: 0;
overflow: hidden;
transition: all 1s ease-in-out;
max-height: 0;
margin: -5px 0 10px;
}
.page.settings .tabs > .active .subtabs {
max-height: 300px;
}
.page.settings .tabs .subtabs a {
font-size: 15px;
padding: 1px 15px;
font-size: 13px;
padding: 0 15px;
font-weight: normal;
color: rgba(255, 255, 255, 0.8);
background: rgba(78, 89, 105, 0.4);
transition: all .3s ease-in-out;
color: rgba(255, 255, 255, 0.7);
}
.page.settings .tabs .subtabs .active a {
font-weight: bold;
color: #fff;
background: rgb(78, 89, 105);
}

3
couchpotato/templates/_desktop.html

@ -43,7 +43,7 @@
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
<script type="text/javascript">
@ -51,7 +51,6 @@
new Uniform();
Api.setup({
'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }},
'url': {{ url_for('api.index')|tojson|safe }},
'path_sep': {{ sep|tojson|safe }},
'is_remote': false

Loading…
Cancel
Save