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 *.pyc
/data/ /data/
/_source/ /_source/
.project
.pydevproject

6
CouchPotato.py

@ -9,7 +9,7 @@ import socket
import subprocess import subprocess
import sys import sys
import traceback import traceback
import time
# Root path # Root path
base_path = dirname(os.path.abspath(__file__)) base_path = dirname(os.path.abspath(__file__))
@ -96,6 +96,10 @@ class Loader(object):
except: except:
self.log.critical(traceback.format_exc()) 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:] args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
subprocess.Popen(args) subprocess.Popen(args)
except: 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 ## 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 * 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. * 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`. * 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): if not os.path.isdir(dirname):
self.makeDir(dirname) self.makeDir(dirname)
os.rename(fromfile, tofile) shutil.move(fromfile, tofile)
try: try:
existing_files.remove(tofile) existing_files.remove(tofile)
except ValueError: 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.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
import random
import re
log = CPLog(__name__) log = CPLog(__name__)
@ -12,14 +15,25 @@ class Downloader(Plugin):
type = [] type = []
torrent_sources = [
'http://torrage.com/torrent/%s.torrent',
'http://torrage.ws/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent',
]
def __init__(self): def __init__(self):
addEvent('download', self.download) 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 pass
def createNzbName(self, data, movie): 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): def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie)) name = os.path.join(self.createNzbName(data, movie))
@ -41,6 +55,26 @@ class Downloader(Plugin):
return is_correct 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): def isDisabled(self, manual):
return not self.isEnabled(manual) return not self.isEnabled(manual)

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

@ -8,10 +8,12 @@ log = CPLog(__name__)
class Blackhole(Downloader): class Blackhole(Downloader):
type = ['nzb', 'torrent'] type = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, manual = False, filedata = None): 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 return
directory = self.conf('directory') directory = self.conf('directory')
@ -20,8 +22,16 @@ class Blackhole(Downloader):
else: else:
try: try:
if not filedata or len(filedata) < 50: if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available!') try:
return False 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)) fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))

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

@ -35,11 +35,17 @@ config = [{
}, },
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': False,
'type': 'bool', 'type': 'bool',
'advanced': True, 'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', '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.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import traceback import traceback
import json
log = CPLog(__name__) log = CPLog(__name__)
@ -39,14 +40,14 @@ class Sabnzbd(Downloader):
try: try:
if params.get('mode') is 'addfile': 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: else:
data = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error('Failed sending release: %s', traceback.format_exc())
return False return False
result = data.strip() result = sab.strip()
if not result: if not result:
log.error("SABnzbd didn't return anything.") log.error("SABnzbd didn't return anything.")
return False return False
@ -61,3 +62,100 @@ class Sabnzbd(Downloader):
else: else:
log.error("Unknown error: " + result[:40]) log.error("Unknown error: " + result[:40])
return False 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 string import ascii_letters, digits
from urllib import quote_plus from urllib import quote_plus
import re import re
import traceback
import unicodedata import unicodedata
log = CPLog(__name__) log = CPLog(__name__)
@ -30,8 +31,8 @@ def toUnicode(original, *args):
return ek(original, *args) return ek(original, *args)
except: except:
raise raise
except UnicodeDecodeError: except:
log.error('Unable to decode value: %s... ', repr(original)[:20]) log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
ascii_text = str(original).encode('string_escape') ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text) return toUnicode(ascii_text)

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

@ -216,7 +216,7 @@ var NotificationBase = new Class({
}, },
testButtonName: function(fieldset){ 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; return 'Test '+name;
} }

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

@ -19,7 +19,7 @@ class NMJ(Notification):
def __init__(self): def __init__(self):
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig) addApiView('notify.nmj.auto_config', self.autoConfig)
def autoConfig(self): def autoConfig(self):
@ -76,7 +76,7 @@ class NMJ(Notification):
mount = self.conf('mount') mount = self.conf('mount')
database = self.conf('database') database = self.conf('database')
if self.mount: if mount:
log.debug('Try to mount network drive via url: %s', (mount)) log.debug('Try to mount network drive via url: %s', (mount))
try: try:
data = self.urlopen(mount) data = self.urlopen(mount)
@ -114,3 +114,8 @@ class NMJ(Notification):
def failed(self): def failed(self):
return jsonified({'success': False}) 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.addkey(keys)
nma.developerkey(self.conf('dev_key')) 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 # as it seems to be part of the message now
self.event = message.split(' ')[0] self.event = message.split(' ')[0]
response = nma.push(
response = nma.push(self.default_title, self.event , message, self.conf('priority'), batch_mode = len(keys) > 1) application = self.default_title,
event = self.event,
description = message,
priority = self.conf('priority'),
batch_mode = len(keys) > 1
)
successful = 0
for key in keys: for key in keys:
if not response[str(key)]['code'] == u'200': if not response[str(key)]['code'] == u'200':
log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) 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): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
for host in [x.strip() for x in self.conf('host').split(",")]: hosts = [x.strip() for x in self.conf('host').split(",")]
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host) successful = 0
self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host) for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
return True successful += 1
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
successful += 1
return successful == len(hosts)*2
def send(self, command, host): def send(self, command, host):

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

@ -340,6 +340,32 @@
.movies .movie .hide_trailer.hide { .movies .movie .hide_trailer.hide {
top: -30px; 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 { .movies .load_more {
display: block; display: block;

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

@ -140,28 +140,33 @@ var Movie = new Class({
self.profile.getTypes().each(function(type){ self.profile.getTypes().each(function(type){
var q = self.addQuality(type.quality_id || type.get('quality_id')); 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.addClass('finish');
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
}
}); });
// Add done releases // 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), var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id); status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'done')) if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id) var q = self.addQuality(release.quality_id)
if (status && q)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(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){ Object.each(self.options.actions, function(action, key){
self.actions.adopt( self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
self.action[key.toLowerCase()] = new self.options.actions[key](self) if(action.el)
) self.actions.adopt(action)
}); });
if(!self.data.library.rating) if(!self.data.library.rating)
@ -175,7 +180,8 @@ var Movie = new Class({
var q = Quality.getQuality(quality_id); var q = Quality.getQuality(quality_id);
return new Element('span', { return new Element('span', {
'text': q.label, 'text': q.label,
'class': 'q_'+q.identifier + ' q_id' + q.id 'class': 'q_'+q.identifier + ' q_id' + q.id,
'title': ''
}).inject(self.quality); }).inject(self.quality);
}, },
@ -280,6 +286,31 @@ var MovieAction = new Class({
this.el.removeClass('disable') 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(){ toElement: function(){
return this.el || null return this.el || null
} }
@ -298,19 +329,11 @@ var IMDBAction = new Class({
self.el = new Element('a.imdb', { self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(), 'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'events': { 'href': 'http://www.imdb.com/title/'+self.id+'/',
'click': self.gotoIMDB.bind(self) 'target': '_blank'
}
}); });
if(!self.id) self.disable(); 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({ var ReleaseAction = new Class({
Extends: MovieAction, Extends: MovieAction,
id: null,
create: function(){ create: function(){
var self = this; var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.releases.icon.download', { self.el = new Element('a.releases.icon.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': { '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){ show: function(e){
var self = this; var self = this;
(e).preventDefault(); if(e)
(e).preventDefault();
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( 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'); ).inject(self.movie, 'top');
// Header // Header
@ -354,29 +392,35 @@ var ReleaseAction = new Class({
new Element('span.provider', {'text': 'Provider'}) new Element('span.provider', {'text': 'Provider'})
).inject(self.release_container) ).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), var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {}, quality = Quality.getProfile(release.quality_id) || {},
info = release.info; info = release.info;
try { if( status.identifier == 'ignored' || status.identifier == 'failed'){
var details_url = info.filter(function(item){ return item.identifier == 'detail_url' }).pick().value; self.last_release = release;
} catch(e){} }
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
// Create release
new Element('div', { 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 'id': 'release_'+release.id
}).adopt( }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), 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.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
new Element('span.quality', {'text': quality.label || 'n/a'}), new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
new Element('span.size', {'text': (self.get(release, 'size'))}), 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.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', {'text': self.get(release, 'provider')}), new Element('span.provider', {'text': self.get(release, 'provider')}),
details_url ? new Element('a.info.icon', { release.info['detail_url'] ? new Element('a.info.icon', {
'href': details_url, 'href': release.info['detail_url'],
'target': '_blank' 'target': '_blank'
}) : null, }) : null,
new Element('a.download.icon', { new Element('a.download.icon', {
@ -400,17 +444,37 @@ var ReleaseAction = new Class({
).inject(self.release_container) ).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); self.movie.slide('in', self.options_container);
}, },
get: function(release, type){ get: function(release, type){
var self = this; return release.info[type] || 'n/a'
return (release.info.filter(function(info){
return type == info.identifier
}).pick() || {}).value || 'n/a'
}, },
download: function(release){ 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 { .movie_result .info h2 span {
padding: 0 5px; padding: 0 5px;
content: ")";
} }
.movie_result .info h2 span:before { 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 # Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) 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({ success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},

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

@ -74,6 +74,22 @@ config = [{
'default': False, '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', 'name': 'move_leftover',
'type': 'bool', 'type': 'bool',
'description': 'Move all leftover file after renaming, to the movie folder.', 'description': 'Move all leftover file after renaming, to the movie folder.',
@ -86,15 +102,6 @@ config = [{
'label': 'Separator', 'label': 'Separator',
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', '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', '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.helpers.variable import getExt, mergeDicts, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin 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 from couchpotato.environment import Env
import errno
import os import os
import re import re
import shutil import shutil
import traceback import traceback
import errno
log = CPLog(__name__) log = CPLog(__name__)
@ -28,9 +28,11 @@ class Renamer(Plugin):
}) })
addEvent('renamer.scan', self.scan) addEvent('renamer.scan', self.scan)
addEvent('renamer.check_snatched', self.checkSnatched)
addEvent('app.load', self.scan) 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): def scanView(self):
@ -314,6 +316,7 @@ class Renamer(Plugin):
break break
# Remove files # Remove files
delete_folders = []
for src in remove_files: for src in remove_files:
if isinstance(src, File): if isinstance(src, File):
@ -327,10 +330,19 @@ class Renamer(Plugin):
try: try:
if os.path.isfile(src): if os.path.isfile(src):
os.remove(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: except:
log.error('Failed removing %s: %s', (src, traceback.format_exc())) log.error('Failed removing %s: %s', (src, traceback.format_exc()))
self.tagDir(group, 'failed_remove') 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 # Rename all files marked
group['renamed_files'] = [] group['renamed_files'] = []
for src in rename_files: for src in rename_files:
@ -464,8 +476,9 @@ class Renamer(Plugin):
def replaceDoubles(self, string): def replaceDoubles(self, string):
return string.replace(' ', ' ').replace(' .', '.') 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 root, dirs, files in os.walk(folder):
for dir_name in dirs: for dir_name in dirs:
@ -474,9 +487,74 @@ class Renamer(Plugin):
try: try:
os.rmdir(full_path) os.rmdir(full_path)
except: 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: try:
os.rmdir(folder) os.rmdir(folder)
except: 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 break
if file_too_new: 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 # Delete the unsorted list
del group['unsorted_files'] del group['unsorted_files']

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

@ -1,7 +1,9 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode 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.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
@ -21,15 +23,24 @@ class Searcher(Plugin):
in_progress = False in_progress = False
def __init__(self): def __init__(self):
addEvent('searcher.all', self.all_movies) addEvent('searcher.all', self.allMovies)
addEvent('searcher.single', self.single) addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie) addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download) 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 # 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: if self.in_progress:
log.info('Search already in progress') log.info('Search already in progress')
@ -138,7 +149,7 @@ class Searcher(Plugin):
for info in nzb: for info in nzb:
try: try:
if not isinstance(nzb[info], (str, unicode, int, long)): if not isinstance(nzb[info], (str, unicode, int, long, float)):
continue continue
rls_info = ReleaseInfo( rls_info = ReleaseInfo(
@ -239,7 +250,6 @@ class Searcher(Plugin):
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', False)
single_category = kwargs.get('single_category', False)
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0): 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) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # 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'])) log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False return False
@ -317,14 +327,10 @@ class Searcher(Plugin):
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True 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'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False 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'] name = nzb['name']
size = nzb.get('size', 0) size = nzb.get('size', 0)
@ -355,9 +361,6 @@ class Searcher(Plugin):
if found.get(allowed): if found.get(allowed):
del found[allowed] del found[allowed]
if (len(found) == 0 and single_category):
return False
return not (found.get(preferred_quality['identifier']) and len(found) == 1) return not (found.get(preferred_quality['identifier']) and len(found) == 1)
def checkIMDB(self, haystack, imdbId): def checkIMDB(self, haystack, imdbId):
@ -398,19 +401,6 @@ class Searcher(Plugin):
return False 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): def couldBeReleased(self, wanted_quality, dates, pre_releases):
now = int(time.time()) now = int(time.time())
@ -439,3 +429,37 @@ class Searcher(Plugin):
return False 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', 'downloaded': 'Downloaded',
'wanted': 'Wanted', 'wanted': 'Wanted',
'snatched': 'Snatched', 'snatched': 'Snatched',
'failed': 'Failed',
'deleted': 'Deleted', 'deleted': 'Deleted',
'ignored': 'Ignored', 'ignored': 'Ignored',
} }

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

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

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

@ -1,6 +1,7 @@
// ==UserScript== // ==UserScript==
// @name CouchPotato UserScript // @name CouchPotato UserScript
// @description Add movies like a real CouchPotato // @description Add movies like a real CouchPotato
// @grant none
// @version {{version}} // @version {{version}}
// @match {{host}}* // @match {{host}}*
@ -44,21 +45,19 @@ function create() {
return A; return A;
} }
if (typeof GM_addStyle == 'undefined'){ var addStyle = function(css) {
GM_addStyle = function(css) { var head = document.getElementsByTagName('head')[0],
var head = document.getElementsByTagName('head')[0], style = document.createElement('style');
style = document.createElement('style'); if (!head)
if (!head) return;
return;
style.type = 'text/css'; style.type = 'text/css';
style.textContent = css; style.textContent = css;
head.appendChild(style); head.appendChild(style);
}
} }
// Styles // 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 { 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.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; } \ #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) order.append(imdb)
if item.get('via_imdb'): if item.get('via_imdb'):
if order.index(imdb): if order.count(imdb):
order.remove(imdb) order.remove(imdb)
order.insert(0, 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) 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 [] return []
cache_key = 'imdbapi.cache.%s' % q cache_key = 'imdbapi.cache.%s' % q

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

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'Mysterbin', 'name': 'Mysterbin',
'description': '', 'description': 'Free provider, less accurate. See <a href="http://www.mysterbin.com/">Mysterbin</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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) new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single = True)
if is_correct_movie: if is_correct_movie:
results.append(new) results.append(new)
self.found(new) self.found(new)

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

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'newzbin', 'name': 'newzbin',
'description': 'See <a href="https://www.newzbin2.es/">Newzbin</a>',
'wizard': True, 'wizard': True,
'options': [ '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) 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]) 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 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 = [] results = []
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) 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: if not for_feed:
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True) new['score'] = fireEvent('score.calculate', new, movie, single = True)

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

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'NZBClub', 'name': 'NZBClub',
'description': '', 'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True) new['score'] = fireEvent('score.calculate', new, movie, single = True)

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

@ -10,7 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'nzbindex', 'name': 'nzbindex',
'description': 'Free provider, but less accurate.', 'description': 'Free provider, less accurate. See <a href="http://www.nzbindex.nl/">NZBIndex</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True) new['score'] = fireEvent('score.calculate', new, movie, single = True)

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

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

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

@ -22,7 +22,7 @@ class NZBMatrix(NZBProvider, RSS):
cat_ids = [ cat_ids = [
([50], ['bd50']), ([50], ['bd50']),
([42, 53], ['720p', '1080p']), ([42, 53], ['720p', '1080p']),
([2], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2, 9], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([54], ['brrip']), ([54], ['brrip']),
([1], ['dvdr']), ([1], ['dvdr']),
] ]
@ -49,7 +49,6 @@ class NZBMatrix(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments) url = "%s?%s" % (self.urls['search'], arguments)
cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids) 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()}) data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
if data: if data:
@ -86,7 +85,7 @@ class NZBMatrix(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True) 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', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'KickAssTorrents', 'name': 'KickAssTorrents',
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import StringIO
import gzip
import re import re
import traceback import traceback
@ -14,10 +12,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider): class KickAssTorrents(TorrentProvider):
urls = { urls = {
'test': 'http://www.kat.ph/', 'test': 'http://kat.ph/',
'detail': 'http://www.kat.ph/%s-t%s.html', 'detail': 'http://kat.ph/%s',
'search': 'http://www.kat.ph/i%s/', 'search': 'http://kat.ph/i%s/',
'download': 'http://torcache.net/',
} }
cat_ids = [ cat_ids = [
@ -60,11 +57,10 @@ class KickAssTorrents(TorrentProvider):
continue continue
new = { new = {
'type': 'torrent', 'type': 'torrent_magnet',
'check_nzb': False, 'check_nzb': False,
'description': '', 'description': '',
'provider': self.getName(), 'provider': self.getName(),
'download': self.download,
'score': 0, 'score': 0,
} }
@ -77,9 +73,8 @@ class KickAssTorrents(TorrentProvider):
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1] link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
new['id'] = temp.get('id')[-8:] new['id'] = temp.get('id')[-8:]
new['name'] = link.text new['name'] = link.text
new['url'] = td.find_all('a', 'idownload')[1]['href'] new['url'] = td.find('a', 'imagnet')['href']
if new['url'][:2] == '//': new['detail_url'] = self.urls['detail'] % link['href'][1:]
new['url'] = 'http:%s' % new['url']
new['score'] = 20 if td.find('a', 'iverif') else 0 new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size': elif column_name is 'size':
new['size'] = self.parseSize(td.text) new['size'] = self.parseSize(td.text)
@ -95,7 +90,7 @@ class KickAssTorrents(TorrentProvider):
new['score'] += fireEvent('score.calculate', new, movie, single = True) new['score'] += fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = False, single = True) imdb_results = True, single = True)
if is_correct_movie: if is_correct_movie:
results.append(new) results.append(new)
self.found(new) self.found(new)
@ -129,13 +124,3 @@ class KickAssTorrents(TorrentProvider):
age += tryInt(nr) * mult age += tryInt(nr) * mult
return tryInt(age) 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', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'PublicHD', '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': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@ -20,4 +20,4 @@ config = [{
], ],
}, },
], ],
}] }]

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

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

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

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'SceneAccess', 'name': 'SceneAccess',
'description': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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) new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, 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: if is_correct_movie:
results.append(new) results.append(new)

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

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'SceneHD', 'name': 'SceneHD',
'description': 'See <a href="http://scenehd.org">SceneHD</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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) new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, 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: if is_correct_movie:
results.append(new) results.append(new)

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

@ -9,7 +9,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'ThePirateBay', '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': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

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

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

@ -10,6 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': 'TorrentLeech', 'name': 'TorrentLeech',
'description': 'See <a href="http://torrentleech.org">TorrentLeech</a>',
'options': [ 'options': [
{ {
'name': 'enabled', '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) new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, 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: if is_correct_movie:
results.append(new) 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.helpers.variable import mergeDicts, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits from string import digits, ascii_letters
import re import re
log = CPLog(__name__) log = CPLog(__name__)
@ -46,7 +46,7 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library']) 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) data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try: try:
@ -100,7 +100,7 @@ class HDTrailers(TrailerProvider):
return results return results
def movieUrlName(self, string): 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]) r = ''.join([char if char in safe_chars else ' ' for char in string])
name = re.sub('\s+' , '-', r).lower() 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 from couchpotato.core.providers.userscript.base import UserscriptBase
import traceback
log = CPLog(__name__)
class AlloCine(UserscriptBase): class AlloCine(UserscriptBase):
@ -15,11 +19,17 @@ class AlloCine(UserscriptBase):
except: except:
return return
html = BeautifulSoup(data) name = None
title = html.find('title').contents[0].strip() year = None
split = title.split(') - ')
name = split[0][:-5].strip() try:
year = split[0][-4:] 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.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.providers.userscript.base import UserscriptBase from couchpotato.core.providers.userscript.base import UserscriptBase
import re
import traceback
log = CPLog(__name__)
class RottenTomatoes(UserscriptBase): class RottenTomatoes(UserscriptBase):
@ -16,7 +20,20 @@ class RottenTomatoes(UserscriptBase):
except: except:
return return
html = BeautifulSoup(data) try:
title = html.find('span', {'itemprop':'name'}).text name = None
info = fireEvent('scanner.name_year', title, single = True) year = None
return self.search(info['name'], info['year']) 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) files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan') 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): class ReleaseInfo(Entity):
"""Properties that can be bound to a file for off-line usage""" """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)); 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^=/]:not([target]))', self.pushState.bind(self));
self.c.addEvent('click:relay(a[href^=http])', self.openDerefered.bind(self));
}, },
getOption: function(name){ getOption: function(name){
@ -187,7 +188,7 @@ var CouchPotato = new Class({
restart: function(message, title){ restart: function(message, title){
var self = this; 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'); Api.request('app.restart');
self.checkAvailable(1000); self.checkAvailable(1000);
}, },
@ -216,7 +217,7 @@ var CouchPotato = new Class({
Updater.check(onComplete) 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); self.checkAvailable(3000);
}, },
@ -269,6 +270,17 @@ var CouchPotato = new Class({
createUrl: function(action, params){ createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(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; return 0;
}; };
Array.implement('sortBy', function(){ Array.implement({
keyPaths.empty(); sortBy: function(){
Array.each(arguments, function(argument) { keyPaths.empty();
switch (typeOf(argument)) {
case "array": saveKeyPath(argument); break; Array.each(arguments, function(argument) {
case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break; switch (typeOf(argument)) {
} case "array": saveKeyPath(argument); break;
}); case "string": saveKeyPath(argument.match(/[+-]|[^.]+/g)); break;
return this.sort(comparer); }
});
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('dt[text=ID]'),
new Element('dd', {'text': App.getOption('pid')}), new Element('dd', {'text': App.getOption('pid')}),
new Element('dt[text=Directories]'), new Element('dt[text=Directories]'),
@ -103,12 +105,8 @@ var AboutSettingTab = new Class({
), ),
new Element('div.donate', { new Element('div.donate', {
'html': 'html':
'Or, buy me a (24 pack) Pepsi, for while I\'m coding ;)' + 'Or support me via:' +
'<form action="https://www.paypal.com/cgi-bin/webscr" method="post">' + '<iframe src="http://couchpota.to/donate.html" style="border:none; height: 200px;" scrolling="no"></iframe>'
'<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>'
}) })
); );
@ -119,6 +117,7 @@ var AboutSettingTab = new Class({
var self = this; var self = this;
var date = new Date(json.version.date * 1000); var date = new Date(json.version.date * 1000);
self.version_text.set('text', json.version.hash + ' ('+date.toUTCString()+')'); 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 'IMDB': IMDBAction
,'Trailer': TrailerAction ,'Trailer': TrailerAction
,'Releases': ReleaseAction ,'Releases': ReleaseAction
,'Edit': new Class({ ,'Edit': new Class({
Extends: MovieAction, Extends: MovieAction,
@ -74,20 +73,23 @@ window.addEvent('domready', function(){
new Element('option', { new Element('option', {
'text': alt.title 'text': alt.title
}).inject(self.title_select); }).inject(self.title_select);
if(alt['default']) if(alt['default'])
self.title_select.set('value', alt.title); self.title_select.set('value', alt.title);
}); });
Quality.getActiveProfiles().each(function(profile){ Quality.getActiveProfiles().each(function(profile){
var profile_id = profile.id ? profile.id : profile.data.id;
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile_id,
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select); }).inject(self.profile_select);
if(self.movie.profile) if(self.movie.profile && self.movie.profile.data.id == profile_id)
self.profile_select.set('value', profile.id ? profile.id : profile.data.id); self.profile_select.set('value', profile_id);
}); });
} }
@ -170,7 +172,7 @@ window.addEvent('domready', function(){
(e).preventDefault(); (e).preventDefault();
if(!self.delete_container){ 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', { new Element('a.cancel', {
'text': 'Cancel', 'text': 'Cancel',
'events': { 'events': {

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

@ -24,39 +24,36 @@
} }
.page.settings .tabs a { .page.settings .tabs a {
display: block; display: block;
padding: 11px 15px; padding: 7px 15px;
font-weight: normal; font-weight: normal;
transition: all 0.1s ease-in-out; transition: all 0.3s ease-in-out;
color: rgba(255, 255, 255, 0.8); 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); background: rgb(78, 89, 105);
font-weight: bold;
font-size: 25px;
color: #fff; color: #fff;
} }
.page.settings .tabs > li {
border-bottom: 1px solid rgb(78, 89, 105);
}
.page.settings .tabs .subtabs { .page.settings .tabs .subtabs {
list-style: none; list-style: none;
padding: 0; padding: 0;
overflow: hidden; margin: -5px 0 10px;
transition: all 1s ease-in-out;
max-height: 0;
} }
.page.settings .tabs > .active .subtabs {
max-height: 300px;
}
.page.settings .tabs .subtabs a { .page.settings .tabs .subtabs a {
font-size: 15px; font-size: 13px;
padding: 1px 15px; padding: 0 15px;
font-weight: normal; font-weight: normal;
color: rgba(255, 255, 255, 0.8); transition: all .3s ease-in-out;
background: rgba(78, 89, 105, 0.4); color: rgba(255, 255, 255, 0.7);
} }
.page.settings .tabs .subtabs .active a { .page.settings .tabs .subtabs .active a {
font-weight: bold;
color: #fff; color: #fff;
background: rgb(78, 89, 105); 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 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') }}" /> <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" src="https://www.youtube.com/player_api" defer="defer"></script>
<script type="text/javascript"> <script type="text/javascript">
@ -51,7 +51,6 @@
new Uniform(); new Uniform();
Api.setup({ Api.setup({
'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }},
'url': {{ url_for('api.index')|tojson|safe }}, 'url': {{ url_for('api.index')|tojson|safe }},
'path_sep': {{ sep|tojson|safe }}, 'path_sep': {{ sep|tojson|safe }},
'is_remote': false 'is_remote': false

Loading…
Cancel
Save