Browse Source

Merge branch 'refs/heads/develop' into desktop

Conflicts:
	version.py
tags/build/2.0.1
Ruud 13 years ago
parent
commit
37bf205d7a
  1. 6
      README.md
  2. 14
      contributing.md
  3. 1
      couchpotato/core/_base/_core/__init__.py
  4. 3
      couchpotato/core/_base/updater/__init__.py
  5. 2
      couchpotato/core/_base/updater/main.py
  6. 27
      couchpotato/core/downloaders/base.py
  7. 1
      couchpotato/core/downloaders/nzbget/__init__.py
  8. 1
      couchpotato/core/downloaders/pneumatic/__init__.py
  9. 183
      couchpotato/core/downloaders/sabnzbd/main.py
  10. 67
      couchpotato/core/event.py
  11. 14
      couchpotato/core/helpers/variable.py
  12. 2
      couchpotato/core/logger.py
  13. 6
      couchpotato/core/notifications/base.py
  14. 11
      couchpotato/core/notifications/core/main.py
  15. 8
      couchpotato/core/notifications/nmj/main.py
  16. 3
      couchpotato/core/notifications/notifymyandroid/main.py
  17. 3
      couchpotato/core/notifications/notifymywp/main.py
  18. 2
      couchpotato/core/notifications/plex/main.py
  19. 34
      couchpotato/core/notifications/prowl/main.py
  20. 14
      couchpotato/core/notifications/synoindex/main.py
  21. 21
      couchpotato/core/notifications/twitter/main.py
  22. 64
      couchpotato/core/notifications/xbmc/main.py
  23. 5
      couchpotato/core/plugins/automation/__init__.py
  24. 10
      couchpotato/core/plugins/automation/main.py
  25. 24
      couchpotato/core/plugins/base.py
  26. 25
      couchpotato/core/plugins/browser/main.py
  27. 57
      couchpotato/core/plugins/file/main.py
  28. 25
      couchpotato/core/plugins/library/main.py
  29. 160
      couchpotato/core/plugins/manage/main.py
  30. 29
      couchpotato/core/plugins/movie/main.py
  31. 63
      couchpotato/core/plugins/movie/static/list.js
  32. 86
      couchpotato/core/plugins/movie/static/movie.css
  33. 65
      couchpotato/core/plugins/movie/static/movie.js
  34. 4
      couchpotato/core/plugins/movie/static/search.js
  35. 10
      couchpotato/core/plugins/profile/main.py
  36. 19
      couchpotato/core/plugins/profile/static/profile.js
  37. 44
      couchpotato/core/plugins/quality/main.py
  38. 11
      couchpotato/core/plugins/release/main.py
  39. 171
      couchpotato/core/plugins/renamer/main.py
  40. 92
      couchpotato/core/plugins/scanner/main.py
  41. 2
      couchpotato/core/plugins/score/scores.py
  42. 141
      couchpotato/core/plugins/searcher/main.py
  43. 4
      couchpotato/core/plugins/subtitle/__init__.py
  44. 7
      couchpotato/core/plugins/subtitle/main.py
  45. 10
      couchpotato/core/plugins/trailer/__init__.py
  46. 4
      couchpotato/core/plugins/trailer/main.py
  47. 27
      couchpotato/core/plugins/wizard/static/wizard.css
  48. 478
      couchpotato/core/plugins/wizard/static/wizard.js
  49. 9
      couchpotato/core/providers/automation/base.py
  50. 1
      couchpotato/core/providers/automation/bluray/main.py
  51. 17
      couchpotato/core/providers/automation/imdb/main.py
  52. 15
      couchpotato/core/providers/automation/movies_io/main.py
  53. 28
      couchpotato/core/providers/metadata/base.py
  54. 1
      couchpotato/core/providers/movie/_modifier/main.py
  55. 62
      couchpotato/core/providers/movie/couchpotatoapi/main.py
  56. 16
      couchpotato/core/providers/movie/imdbapi/main.py
  57. 4
      couchpotato/core/providers/movie/themoviedb/main.py
  58. 4
      couchpotato/core/providers/nzb/mysterbin/__init__.py
  59. 2
      couchpotato/core/providers/nzb/newzbin/__init__.py
  60. 5
      couchpotato/core/providers/nzb/newznab/__init__.py
  61. 8
      couchpotato/core/providers/nzb/newznab/main.py
  62. 2
      couchpotato/core/providers/nzb/nzbclub/__init__.py
  63. 4
      couchpotato/core/providers/nzb/nzbindex/__init__.py
  64. 4
      couchpotato/core/providers/nzb/nzbindex/main.py
  65. 2
      couchpotato/core/providers/nzb/nzbmatrix/__init__.py
  66. 2
      couchpotato/core/providers/nzb/nzbsrus/__init__.py
  67. 3
      couchpotato/core/providers/torrent/kickasstorrents/__init__.py
  68. 13
      couchpotato/core/providers/torrent/kickasstorrents/main.py
  69. 58
      couchpotato/core/providers/torrent/passthepopcorn/__init__.py
  70. 4
      couchpotato/core/providers/torrent/publichd/__init__.py
  71. 6
      couchpotato/core/providers/torrent/publichd/main.py
  72. 2
      couchpotato/core/providers/torrent/sceneaccess/__init__.py
  73. 4
      couchpotato/core/providers/torrent/scenehd/__init__.py
  74. 10
      couchpotato/core/providers/torrent/scenehd/main.py
  75. 41
      couchpotato/core/providers/torrent/thepiratebay/__init__.py
  76. 2
      couchpotato/core/providers/torrent/torrentleech/__init__.py
  77. 17
      couchpotato/core/providers/trailer/hdtrailers/main.py
  78. 2
      couchpotato/core/settings/__init__.py
  79. 12
      couchpotato/runner.py
  80. BIN
      couchpotato/static/images/couch.png
  81. BIN
      couchpotato/static/images/emptylist.png
  82. BIN
      couchpotato/static/images/gear.png
  83. BIN
      couchpotato/static/images/homescreen.png
  84. BIN
      couchpotato/static/images/icon.attention.png
  85. BIN
      couchpotato/static/images/icon.check.png
  86. BIN
      couchpotato/static/images/icon.delete.png
  87. BIN
      couchpotato/static/images/icon.download.png
  88. BIN
      couchpotato/static/images/icon.edit.png
  89. BIN
      couchpotato/static/images/icon.files.png
  90. BIN
      couchpotato/static/images/icon.folder.gif
  91. BIN
      couchpotato/static/images/icon.imdb.png
  92. BIN
      couchpotato/static/images/icon.info.png
  93. BIN
      couchpotato/static/images/icon.rating.png
  94. BIN
      couchpotato/static/images/icon.refresh.png
  95. BIN
      couchpotato/static/images/icon.spinner.gif
  96. BIN
      couchpotato/static/images/icon.trailer.png
  97. BIN
      couchpotato/static/images/icon.undo.png
  98. BIN
      couchpotato/static/images/imdb_watchlist.png
  99. BIN
      couchpotato/static/images/right.arrow.png
  100. BIN
      couchpotato/static/images/sprite.png

6
README.md

@ -33,7 +33,7 @@ Linux (ubuntu / debian):
* 'cd' to the folder of your choosing. * 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start * Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` * To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `nano /etc/init.d/couchpotato` * Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
* Make it executable. `chmod +x /etc/init.d/couchpotato` * Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults` * Add it to defaults. `sudo update-rc.d couchpotato defaults`

14
contributing.md

@ -0,0 +1,14 @@
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
##Just make sure you think of the following things:
* Search through the existing (and closed) issues first. See if you can get your answer there.
* Double check the result manually, because it could be an external issue.
* Post logs! Without seeing what is going on, I can't reproduce the error.
* What are you settings for the specific problem
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
* Give me a short step by step of how to reproduce
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**

1
couchpotato/core/_base/_core/__init__.py

@ -27,6 +27,7 @@ config = [{
'name': 'host', 'name': 'host',
'advanced': True, 'advanced': True,
'default': '0.0.0.0', 'default': '0.0.0.0',
'hidden': True,
'label': 'IP', 'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.', 'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
}, },

3
couchpotato/core/_base/updater/__init__.py

@ -1,4 +1,6 @@
from .main import Updater from .main import Updater
from couchpotato.environment import Env
import os
def start(): def start():
return Updater() return Updater()
@ -33,6 +35,7 @@ config = [{
{ {
'name': 'git_command', 'name': 'git_command',
'default': 'git', 'default': 'git',
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
'advanced': True 'advanced': True
}, },
], ],

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

@ -406,7 +406,7 @@ class DesktopUpdater(BaseUpdater):
'last_check': self.last_check, 'last_check': self.last_check,
'update_version': self.update_version, 'update_version': self.update_version,
'version': self.getVersion(), 'version': self.getVersion(),
'branch': 'desktop_build', 'branch': self.branch,
} }
def check(self): def check(self):

27
couchpotato/core/downloaders/base.py

@ -1,10 +1,7 @@
from base64 import b32decode, b16encode 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.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
import os
import random import random
import re import re
@ -23,29 +20,17 @@ class Downloader(Plugin):
def __init__(self): def __init__(self):
addEvent('download', self.download) addEvent('download', self.download)
addEvent('download.status', self.getDownloadStatus) addEvent('download.status', self.getAllDownloadStatus)
addEvent('download.remove_failed', self.removeFailed)
def download(self, data = {}, movie = {}, manual = False, filedata = None): def download(self, data = {}, movie = {}, manual = False, filedata = None):
pass pass
def getDownloadStatus(self, data = {}, movie = {}): def getAllDownloadStatus(self):
return False return False
def createNzbName(self, data, movie): def removeFailed(self, name = {}, nzo_id = {}):
tag = self.cpTag(movie) return False
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isCorrectType(self, item_type): def isCorrectType(self, item_type):
is_correct = item_type in self.type is_correct = item_type in self.type
@ -56,7 +41,7 @@ class Downloader(Plugin):
return is_correct return is_correct
def magnetToTorrent(self, magnet_link): def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0] torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
# Convert base 32 to hex # Convert base 32 to hex
if len(torrent_hash) == 32: if len(torrent_hash) == 32:

1
couchpotato/core/downloaders/nzbget/__init__.py

@ -11,7 +11,6 @@ config = [{
'name': 'nzbget', 'name': 'nzbget',
'label': 'NZBGet', 'label': 'NZBGet',
'description': 'Send NZBs to your NZBGet installation.', 'description': 'Send NZBs to your NZBGet installation.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

1
couchpotato/core/downloaders/pneumatic/__init__.py

@ -12,7 +12,6 @@ config = [{
'name': 'pneumatic', 'name': 'pneumatic',
'label': 'Pneumatic', 'label': 'Pneumatic',
'description': 'Download the .strm file to a specific folder.', 'description': 'Download the .strm file to a specific folder.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

@ -1,9 +1,10 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import traceback from urllib2 import URLError
import json import json
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@ -36,126 +37,124 @@ class Sabnzbd(Downloader):
else: else:
params['name'] = data.get('url') params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
try: try:
if params.get('mode') is 'addfile': if params.get('mode') is 'addfile':
sab = 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:
sab = self.urlopen(url, timeout = 60, show_error = False) sab = self.urlopen(url, timeout = 60, show_error = False)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
except: except:
log.error('Failed sending release: %s', traceback.format_exc()) log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False return False
result = sab.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
log.debug("Result text from SAB: " + result[:40]) log.debug('Result text from SAB: %s', result[:40])
if result == "ok": if result[:2] == 'ok':
log.info("NZB sent to SAB successfully.") log.info('NZB sent to SAB successfully.')
return True return True
elif result == "Missing authentication":
log.error("Incorrect username/password.")
return False
else: else:
log.error("Unknown error: " + result[:40]) log.error(result[:40])
return False return False
def getDownloadStatus(self, data = {}, movie = {}): def getAllDownloadStatus(self):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')): if self.isDisabled(manual = False):
return return False
nzbname = self.createNzbName(data, movie) log.debug('Checking SABnzbd download status.')
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
# Go through Queue # Go through Queue
params = {
'apikey': self.conf('api_key'),
'mode': 'queue',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
sab = self.urlopen(url, timeout = 60, show_error = False) queue = self.call({
'mode': 'queue',
})
except: except:
log.error('Failed checking status: %s', traceback.format_exc()) log.error('Failed getting queue: %s', traceback.format_exc(1))
return False return False
# Go through history items
try: try:
history = json.loads(sab) history = self.call({
'mode': 'history',
'limit': 15,
})
except: except:
log.debug("Result text from SAB: " + sab[:40]) log.error('Failed getting history json: %s', traceback.format_exc(1))
log.error('Failed parsing json status: %s', traceback.format_exc())
return False return False
for slot in history['queue']['slots']: statuses = []
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 # Get busy releases
params = { for item in queue.get('slots', []):
'apikey': self.conf('api_key'), statuses.append({
'mode': 'history', 'id': item['nzo_id'],
'output': 'json' 'name': item['filename'],
} 'status': 'busy',
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) 'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
try: # Get old releases
sab = self.urlopen(url, timeout = 60, show_error = False) for item in history.get('slots', []):
except:
log.error('Failed getting history: %s', traceback.format_exc()) status = 'busy'
return if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
status = 'failed'
elif item['status'] == 'Completed':
status = 'completed'
statuses.append({
'id': item['nzo_id'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': 0,
})
return statuses
def removeFailed(self, item):
if not self.conf('delete_failed', default = True):
return False
log.info('%s failed downloading, deleting...', item['name'])
try: try:
history = json.loads(sab) self.call({
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': item['id']
}, use_json = False)
except: except:
log.debug("Result text from SAB: " + sab[:40]) log.error('Failed deleting: %s', traceback.format_exc(0))
log.error('Failed parsing history json: %s', traceback.format_exc()) return False
return
return True
def call(self, params, use_json = True):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
data = self.urlopen(url, timeout = 60, show_error = False)
if use_json:
d = json.loads(data)
if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
return d[params['mode']]
else:
return data
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'

67
couchpotato/core/event.py

@ -19,7 +19,7 @@ def addEvent(name, handler, priority = 100):
if events.get(name): if events.get(name):
e = events[name] e = events[name]
else: else:
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock()) e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
@ -46,49 +46,30 @@ def fireEvent(name, *args, **kwargs):
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
# Fire after event options = {
is_after_event = False 'is_after_event': False, # Fire after event
try: 'on_complete': False, # onComplete event
del kwargs['is_after_event'] 'single': False, # Return single handler
is_after_event = True 'merge': False, # Merge items
except: pass 'in_order': False, # Fire them in specific order, waits for the other to finish
}
# onComplete event
on_complete = False # Do options
try: for x in options:
on_complete = kwargs['on_complete'] try:
del kwargs['on_complete'] val = kwargs[x]
except: pass del kwargs[x]
options[x] = val
# Return single handler except: pass
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
# Merge items
in_order = False
try:
del kwargs['in_order']
in_order = True
except: pass
e = events[name] e = events[name]
if not in_order: e.lock.acquire() if not options['in_order']: e.lock.acquire()
e.asynchronous = False e.asynchronous = False
e.in_order = in_order e.in_order = options['in_order']
result = e(*args, **kwargs) result = e(*args, **kwargs)
if not in_order: e.lock.release() if not options['in_order']: e.lock.release()
if single and not merge: if options['single'] and not options['merge']:
results = None results = None
# Loop over results, stop when first not None result is found. # Loop over results, stop when first not None result is found.
@ -112,7 +93,7 @@ def fireEvent(name, *args, **kwargs):
errorHandler(r[1]) errorHandler(r[1])
# Merge # Merge
if merge and len(results) > 0: if options['merge'] and len(results) > 0:
# Dict # Dict
if type(results[0]) == dict: if type(results[0]) == dict:
merged = {} merged = {}
@ -133,11 +114,11 @@ def fireEvent(name, *args, **kwargs):
log.debug('Return modified results for %s', name) log.debug('Return modified results for %s', name)
results = modified_results results = modified_results
if not is_after_event: if not options['is_after_event']:
fireEvent('%s.after' % name, is_after_event = True) fireEvent('%s.after' % name, is_after_event = True)
if on_complete: if options['on_complete']:
on_complete() options['on_complete']()
return results return results
except KeyError, e: except KeyError, e:

14
couchpotato/core/helpers/variable.py

@ -118,8 +118,16 @@ def getTitle(library_dict):
try: try:
return library_dict['titles'][0]['title'] return library_dict['titles'][0]['title']
except: except:
log.error('Could not get title for %s', library_dict['identifier']) try:
return None for title in library_dict.titles:
if title.default:
return title.title
except:
log.error('Could not get title for %s', library_dict.identifier)
return None
log.error('Could not get title for %s', library_dict['identifier'])
return None
except: except:
log.error('Could not get title for library item: %s', library_dict) log.error('Could not get title for library item: %s', library_dict)
return None return None
@ -127,3 +135,5 @@ def getTitle(library_dict):
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','):
return [x.strip() for x in str.split(split_on)]

2
couchpotato/core/logger.py

@ -5,7 +5,7 @@ import traceback
class CPLog(object): class CPLog(object):
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
def __init__(self, context = ''): def __init__(self, context = ''):
if context.endswith('.main'): if context.endswith('.main'):

6
couchpotato/core/notifications/base.py

@ -14,7 +14,7 @@ class Notification(Plugin):
test_message = 'ZOMG Lazors Pewpewpew!' test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = [ listen_to = [
'movie.downloaded', 'movie.snatched', 'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
] ]
dont_listen_to = [] dont_listen_to = []
@ -30,10 +30,10 @@ class Notification(Plugin):
addEvent(listener, self.createNotifyHandler(listener)) addEvent(listener, self.createNotifyHandler(listener))
def createNotifyHandler(self, listener): def createNotifyHandler(self, listener):
def notify(message, data): def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return return
return self.notify(message = message, data = data, listener = listener) return self.notify(message = message, data = data if data else group, listener = listener)
return notify return notify

11
couchpotato/core/notifications/core/main.py

@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
@ -22,7 +22,7 @@ class CoreNotifier(Notification):
listeners = [] listeners = []
listen_to = [ listen_to = [
'movie.downloaded', 'movie.snatched', 'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
] ]
@ -67,7 +67,7 @@ class CoreNotifier(Notification):
ids = None ids = None
if getParam('ids'): if getParam('ids'):
ids = [x.strip() for x in getParam('ids').split(',')] ids = splitString(getParam('ids'))
db = get_session() db = get_session()
@ -79,7 +79,6 @@ class CoreNotifier(Notification):
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@ -93,7 +92,7 @@ class CoreNotifier(Notification):
q = db.query(Notif) q = db.query(Notif)
if limit_offset: if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')] splt = splitString(limit_offset)
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset) q = q.limit(limit).offset(offset)
@ -107,7 +106,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
#db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
@ -133,7 +131,6 @@ class CoreNotifier(Notification):
self.frontend(type = listener, data = data) self.frontend(type = listener, data = data)
#db.close()
return True return True
def frontend(self, type = 'notification', data = {}, message = None): def frontend(self, type = 'notification', data = {}, message = None):

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

@ -69,7 +69,7 @@ class NMJ(Notification):
'mount': mount, 'mount': mount,
}) })
def addToLibrary(self, group = {}): def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
host = self.conf('host') host = self.conf('host')
@ -114,8 +114,8 @@ class NMJ(Notification):
def failed(self): def failed(self):
return jsonified({'success': False}) return jsonified({'success': False})
def test(self): def test(self):
return jsonified({'success': self.addToLibrary()}) return jsonified({'success': self.addToLibrary()})

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

@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import pynma import pynma
@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return if self.isDisabled(): return
nma = pynma.PyNMA() nma = pynma.PyNMA()
keys = [x.strip() for x in self.conf('api_key').split(',')] keys = splitString(self.conf('api_key'))
nma.addkey(keys) nma.addkey(keys)
nma.developerkey(self.conf('dev_key')) nma.developerkey(self.conf('dev_key'))

3
couchpotato/core/notifications/notifymywp/main.py

@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP from pynmwp import PyNMWP
@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
keys = [x.strip() for x in self.conf('api_key').split(',')] keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key')) p = PyNMWP(keys, self.conf('dev_key'))
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)

2
couchpotato/core/notifications/plex/main.py

@ -17,7 +17,7 @@ class Plex(Notification):
super(Plex, self).__init__() super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}): def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
log.info('Sending notification to Plex') log.info('Sending notification to Plex')

34
couchpotato/core/notifications/prowl/main.py

@ -1,39 +1,35 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Prowl(Notification): class Prowl(Notification):
urls = {
'api': 'https://api.prowlapp.com/publicapi/add'
}
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
http_handler = HTTPSConnection('api.prowlapp.com')
data = { data = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'application': self.default_title, 'application': self.default_title,
'description': toUnicode(message), 'description': toUnicode(message),
'priority': self.conf('priority'), 'priority': self.conf('priority'),
} }
headers = {
'Content-type': 'application/x-www-form-urlencoded'
}
http_handler.request('POST', try:
'/publicapi/add', self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Prowl notifications sent.') log.info('Prowl notifications sent.')
return True return True
elif request_status == 401: except:
log.error('Prowl auth failed: %s', response.reason) log.error('Prowl failed: %s', traceback.format_exc())
return False
else: return False
log.error('Prowl notification failed.')
return False

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

@ -1,6 +1,8 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import os
import subprocess import subprocess
log = CPLog(__name__) log = CPLog(__name__)
@ -8,14 +10,17 @@ log = CPLog(__name__)
class Synoindex(Notification): class Synoindex(Notification):
index_path = '/usr/syno/bin/synoindex'
def __init__(self): def __init__(self):
super(Synoindex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}): def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')] command = [self.index_path, '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ', command) log.info('Executing synoindex command: %s ', command)
try: try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate() out = p.communicate()
@ -26,3 +31,6 @@ class Synoindex(Notification):
return False return False
return True return True
def test(self):
return jsonified({'success': os.path.isfile(self.index_path)})

21
couchpotato/core/notifications/twitter/main.py

@ -38,22 +38,33 @@ class Twitter(Notification):
direct_message = self.conf('direct_message') direct_message = self.conf('direct_message')
direct_message_users = self.conf('screen_name') direct_message_users = self.conf('screen_name')
mention = self.conf('mention') mention = self.conf('mention')
mention_tag = None
if mention: if mention:
if direct_message: if direct_message:
direct_message_users = '%s %s' % (direct_message_users, mention) direct_message_users = '%s %s' % (direct_message_users, mention)
direct_message_users = direct_message_users.replace('@',' ') direct_message_users = direct_message_users.replace('@', ' ')
direct_message_users = direct_message_users.replace(',',' ') direct_message_users = direct_message_users.replace(',', ' ')
else: else:
message = '%s @%s' % (message, mention.lstrip('@')) mention_tag = '@%s' % mention.lstrip('@')
message = '%s %s' % (message, mention_tag)
try: try:
if direct_message: if direct_message:
for user in direct_message_users.split(): for user in direct_message_users.split():
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message)) api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
else: else:
api.PostUpdate('[%s] %s' % (self.default_title, message)) update_message = '[%s] %s' % (self.default_title, message)
if len(update_message) > 140:
if mention_tag:
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
else:
api.PostUpdate(update_message[:135] + ' 1/2')
api.PostUpdate(update_message[135:] + ' 2/2')
else:
api.PostUpdate(update_message)
except Exception, e: except Exception, e:
log.error('Error sending tweet: %s', e) log.error('Error sending tweet: %s', e)
return False return False

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

@ -1,6 +1,7 @@
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64 import base64
log = CPLog(__name__) log = CPLog(__name__)
@ -8,36 +9,51 @@ log = CPLog(__name__)
class XBMC(Notification): class XBMC(Notification):
listen_to = ['movie.downloaded'] listen_to = ['renamer.after']
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
hosts = [x.strip() for x in self.conf('host').split(",")] hosts = splitString(self.conf('host'))
successful = 0 successful = 0
for host in hosts: for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host): response = self.request(host, [
successful += 1 ('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host): ('VideoLibrary.Scan', {}),
successful += 1 ])
for result in response:
if result['result'] == "OK":
successful += 1
return successful == len(hosts) * 2
def request(self, host, requests):
server = 'http://%s/jsonrpc' % host
data = []
for req in requests:
method, kwargs = req
data.append({
'method': method,
'params': kwargs,
'jsonrpc': '2.0',
'id': method,
})
data = json.dumps(data)
headers = {
'Content-Type': 'application/json',
}
return successful == len(hosts)*2 if self.conf('password'):
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
headers['Authorization'] = 'Basic %s' % base64string
def send(self, command, host): log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response))
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command)) return response
headers = {}
if self.conf('password'):
headers = {
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
}
try:
self.urlopen(url, headers = headers, show_error = False)
except:
log.error("Couldn't sent command to XBMC")
return False
log.info('XBMC notification to %s successful.', host)
return True

5
couchpotato/core/plugins/automation/__init__.py

@ -5,13 +5,12 @@ def start():
config = [{ config = [{
'name': 'automation', 'name': 'automation',
'order': 30, 'order': 101,
'groups': [ 'groups': [
{ {
'tab': 'automation', 'tab': 'automation',
'name': 'automation', 'name': 'automation',
'label': 'Automation', 'label': 'Minimal movie requirements',
'description': 'Minimal movie requirements',
'options': [ 'options': [
{ {
'name': 'year', 'name': 'year',

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

@ -18,9 +18,17 @@ class Automation(Plugin):
def addMovies(self): def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True) movies = fireEvent('automation.get_movies', merge = True)
movie_ids = []
for imdb_id in movies: for imdb_id in movies:
prop_name = 'automation.added.%s' % imdb_id prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False) added = Env.prop(prop_name, default = False)
if not added: if not added:
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False) added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
if added_movie:
movie_ids.append(added_movie['id'])
Env.prop(prop_name, True) Env.prop(prop_name, True)
for movie_id in movie_ids:
movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('searcher.single', movie_dict)

24
couchpotato/core/plugins/base.py

@ -1,7 +1,8 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss, \
toSafeString
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
@ -123,7 +124,7 @@ class Plugin(object):
try: try:
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()])) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
cookies = cookielib.CookieJar() cookies = cookielib.CookieJar()
@ -238,13 +239,30 @@ class Plugin(object):
self.setCache(cache_key, data, timeout = cache_timeout) self.setCache(cache_key, data, timeout = cache_timeout)
return data return data
except: except:
pass if not kwargs.get('show_error'):
raise
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s', cache_key) log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout) Env.get('cache').set(cache_key, value, timeout)
return value return value
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isDisabled(self): def isDisabled(self):
return not self.isEnabled() return not self.isEnabled()

25
couchpotato/core/plugins/browser/main.py

@ -27,6 +27,8 @@ class FileBrowser(Plugin):
}, },
'return': {'type': 'object', 'example': """{ 'return': {'type': 'object', 'example': """{
'is_root': bool, //is top most folder 'is_root': bool, //is top most folder
'parent': string, //parent folder of requested path
'home': string, //user home folder
'empty': bool, //directory is empty 'empty': bool, //directory is empty
'dirs': array, //directory names 'dirs': array, //directory names
}"""} }"""}
@ -64,14 +66,35 @@ class FileBrowser(Plugin):
path = getParam('path', '/') path = getParam('path', '/')
# Set proper home dir for some systems
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
except:
pass
home = os.path.expanduser('~')
if not path:
path = home
try: try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except: except:
dirs = [] dirs = []
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
parent = '/'
elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep
return jsonified({ return jsonified({
'is_root': path == '/' or not path, 'is_root': path == '/',
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent,
'home': home + os.path.sep,
'platform': os.name,
'dirs': dirs, 'dirs': dirs,
}) })

57
couchpotato/core/plugins/file/main.py

@ -2,12 +2,15 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt
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.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
import os.path import os.path
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@ -28,6 +31,52 @@ class FileManager(Plugin):
'return': {'type': 'file'} 'return': {'type': 'file'}
}) })
addApiView('file.types', self.getTypesView, docs = {
'desc': 'Return a list of all the file types and their ids.',
'return': {'type': 'object', 'example': """{
'types': [
{
"identifier": "poster_original",
"type": "image",
"id": 1,
"name": "Poster_original"
},
{
"identifier": "poster",
"type": "image",
"id": 2,
"name": "Poster"
},
etc
]
}"""}
})
addEvent('app.load', self.cleanup)
addEvent('app.load', self.init)
def init(self):
for type_tuple in Scanner.file_types.values():
self.getType(type_tuple)
def cleanup(self):
# Wait a bit after starting before cleanup
time.sleep(3)
log.debug('Cleaning up unused files')
try:
db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files:
file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
os.remove(file_path)
except:
log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''): def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir') cache_dir = Env.get('cache_dir')
@ -89,7 +138,6 @@ class FileManager(Plugin):
db.commit() db.commit()
type_dict = ft.to_dict() type_dict = ft.to_dict()
#db.close()
return type_dict return type_dict
def getTypes(self): def getTypes(self):
@ -102,5 +150,10 @@ class FileManager(Plugin):
for type_object in results: for type_object in results:
types.append(type_object.to_dict()) types.append(type_object.to_dict())
#db.close()
return types return types
def getTypesView(self):
return jsonified({
'types': self.getTypes()
})

25
couchpotato/core/plugins/library/main.py

@ -1,6 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
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, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
#db.close()
return library_dict return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', force = False):
@ -111,18 +111,20 @@ class LibraryPlugin(Plugin):
# Files # Files
images = info.get('images', []) images = info.get('images', [])
for type in images: for image_type in ['poster']:
for image in images[type]: for image in images.get(image_type, []):
if not isinstance(image, str): if not isinstance(image, (str, unicode)):
continue continue
file_path = fireEvent('file.download', url = image, single = True) file_path = fireEvent('file.download', url = image, single = True)
if file_path: if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True) file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try: try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj) library.files.append(file_obj)
db.commit() db.commit()
break
except: except:
log.debug('Failed to attach to library: %s', traceback.format_exc()) log.debug('Failed to attach to library: %s', traceback.format_exc())
@ -136,26 +138,23 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info: if not library.info:
library_dict = self.update(identifier) library_dict = self.update(identifier, force = True)
dates = library_dict.get('info', {}).get('release_dates') dates = library_dict.get('info', {}).get('release_date')
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
if dates and dates.get('expires', 0) < time.time(): if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info['release_date'] = dates library.info = mergeDicts(library.info, {'release_date': dates })
library.info = library.info
db.commit() db.commit()
dates = library.info.get('release_date', {})
#db.close()
return dates return dates
def simplifyTitle(self, title): def simplifyTitle(self, title):
title = toUnicode(title) title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#' nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title) title = simplifyString(title)

160
couchpotato/core/plugins/manage/main.py

@ -2,22 +2,32 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import getTitle, splitString
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 time import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Manage(Plugin): class Manage(Plugin):
in_progress = False
def __init__(self): def __init__(self):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2) fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
addEvent('manage.update', self.updateLibrary) addEvent('manage.update', self.updateLibrary)
# Add files after renaming
def after_rename(message = None, group = {}):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename, priority = 110)
addApiView('manage.update', self.updateLibraryView, docs = { addApiView('manage.update', self.updateLibraryView, docs = {
'desc': 'Update the library by scanning for new movies', 'desc': 'Update the library by scanning for new movies',
'params': { 'params': {
@ -25,11 +35,23 @@ class Manage(Plugin):
} }
}) })
addApiView('manage.progress', self.getProgress, docs = {
'desc': 'Get the progress of current manage update',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
if not Env.get('dev'): if not Env.get('dev'):
def updateLibrary(): def updateLibrary():
self.updateLibrary(full = False) self.updateLibrary(full = False)
addEvent('app.load', updateLibrary) addEvent('app.load', updateLibrary)
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def updateLibraryView(self): def updateLibraryView(self):
full = getParam('full', default = 1) full = getParam('full', default = 1)
@ -43,49 +65,127 @@ class Manage(Plugin):
def updateLibrary(self, full = True): def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0)) last_update = float(Env.prop('manage.last_update', default = 0))
if self.isDisabled() or (last_update > time.time() - 20): if self.in_progress:
log.info('Already updating library: %s', self.in_progress)
return
elif self.isDisabled() or (last_update > time.time() - 20):
return return
directories = self.directories() self.in_progress = {}
added_identifiers = [] fireEvent('notify.frontend', type = 'manage.updating', data = True)
for directory in directories: try:
directories = self.directories()
added_identifiers = []
# Add some progress
self.in_progress = {}
for directory in directories:
self.in_progress[os.path.normpath(directory)] = {
'total': None,
'to_go': None,
}
for directory in directories:
folder = os.path.normpath(directory)
if not os.path.isdir(folder):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', folder)
continue
log.info('Updating manage library: %s', folder)
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
onFound = self.createAddToLibrary(folder, added_identifiers)
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
# Break if CP wants to shut down
if self.shuttingDown():
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
Env.prop('manage.last_update', time.time())
except:
log.error('Failed updating library: %s', (traceback.format_exc()))
if not os.path.isdir(directory): while True and not self.shuttingDown():
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', directory)
continue
log.info('Updating manage library: %s', directory) delete_me = {}
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers:
added_identifiers.extend(identifiers)
# Break if CP wants to shut down for folder in self.in_progress:
if self.shuttingDown(): if self.in_progress[folder]['to_go'] <= 0:
delete_me[folder] = True
for delete in delete_me:
del self.in_progress[delete]
if len(self.in_progress) == 0:
break break
# If cleanup option is enabled, remove offline files from database time.sleep(1)
if self.conf('cleanup') and full and not self.shuttingDown():
fireEvent('notify.frontend', type = 'manage.updating', data = False)
self.in_progress = False
def createAddToLibrary(self, folder, added_identifiers = []):
def addToLibrary(group, total_found, to_go):
if self.in_progress[folder]['total'] is None:
self.in_progress[folder] = {
'total': total_found,
'to_go': total_found,
}
if group['library']:
identifier = group['library'].get('identifier')
added_identifiers.append(identifier)
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
# Get movies with done status return addToLibrary
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies: def createAfterUpdate(self, folder, identifier):
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
Env.prop('manage.last_update', time.time()) # Notify frontend
def afterUpdate():
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
total = self.in_progress[folder]['total']
movie_dict = fireEvent('movie.get', identifier, single = True)
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
return afterUpdate
def directories(self): def directories(self):
try: try:
return [x.strip() for x in self.conf('library', default = '').split('::')] return splitString(self.conf('library', default = ''), '::')
except: except:
return [] return []
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)

29
couchpotato/core/plugins/movie/main.py

@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb from couchpotato.core.helpers.variable import getImdb, splitString
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, LibraryTitle, Movie from couchpotato.core.settings.model import Library, LibraryTitle, Movie
@ -107,13 +107,18 @@ class MoviePlugin(Plugin):
def get(self, movie_id): def get(self, movie_id):
db = get_session() db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first()
imdb_id = getImdb(str(movie_id))
if(imdb_id):
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
else:
m = db.query(Movie).filter_by(id = movie_id).first()
results = None results = None
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
#db.close()
return results return results
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None): def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
@ -161,7 +166,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('files')) .options(joinedload_all('files'))
if limit_offset: if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')] splt = splitString(limit_offset)
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset) q2 = q2.limit(limit).offset(offset)
@ -239,7 +244,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
for id in getParam('id').split(','): for id in splitString(getParam('id')):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = id).first()
if movie: if movie:
@ -278,7 +283,7 @@ class MoviePlugin(Plugin):
'movies': movies, 'movies': movies,
}) })
def add(self, params = {}, force_readd = True, search_after = True): def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
if not params.get('identifier'): if not params.get('identifier'):
msg = 'Can\'t add movie without imdb identifier.' msg = 'Can\'t add movie without imdb identifier.'
@ -298,7 +303,7 @@ class MoviePlugin(Plugin):
pass pass
library = fireEvent('library.add', single = True, attrs = params, update_after = False) library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status # Status
status_active = fireEvent('status.add', 'active', single = True) status_active = fireEvent('status.add', 'active', single = True)
@ -381,7 +386,7 @@ class MoviePlugin(Plugin):
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = [x.strip() for x in params.get('id').split(',')] ids = splitString(params.get('id'))
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
@ -417,7 +422,7 @@ class MoviePlugin(Plugin):
params = getParams() params = getParams()
ids = [x.strip() for x in params.get('id').split(',')] ids = splitString(params.get('id'))
for movie_id in ids: for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all')) self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
@ -431,9 +436,11 @@ class MoviePlugin(Plugin):
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
if movie: if movie:
deleted = False
if delete_from == 'all': if delete_from == 'all':
db.delete(movie) db.delete(movie)
db.commit() db.commit()
deleted = True
else: else:
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
@ -456,6 +463,7 @@ class MoviePlugin(Plugin):
if total_releases == total_deleted: if total_releases == total_deleted:
db.delete(movie) db.delete(movie)
db.commit() db.commit()
deleted = True
elif new_movie_status: elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True) new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None movie.profile_id = None
@ -464,6 +472,9 @@ class MoviePlugin(Plugin):
else: else:
fireEvent('movie.restatus', movie.id, single = True) fireEvent('movie.restatus', movie.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close() #db.close()
return True return True

63
couchpotato/core/plugins/movie/static/list.js

@ -33,16 +33,34 @@ var MovieList = new Class({
); );
self.getMovies(); self.getMovies();
if(options.add_new) App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.added', self.movieAdded.bind(self)) App.addEvent('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data.id]){
self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){
movie.destroy();
delete self.movies_added[notification.data.id]
}
})
}
self.checkIfEmpty();
}, },
movieAdded: function(notification){ movieAdded: function(notification){
var self = this; var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id]) if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
self.checkIfEmpty();
}
}, },
create: function(){ create: function(){
@ -86,18 +104,19 @@ var MovieList = new Class({
Object.each(movies, function(movie){ Object.each(movies, function(movie){
self.createMovie(movie); self.createMovie(movie);
}); });
self.total_movies = total;
self.setCounter(total); self.setCounter(total);
}, },
setCounter: function(count){ setCounter: function(count){
var self = this; var self = this;
if(!self.navigation_counter) return; if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0)); self.navigation_counter.set('text', (count || 0));
}, },
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at){
@ -309,6 +328,8 @@ var MovieList = new Class({
erase_movies.each(function(movie){ erase_movies.each(function(movie){
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy()
}); });
self.calculateSelected(); self.calculateSelected();
@ -458,6 +479,8 @@ var MovieList = new Class({
self.addMovies(json.movies, json.total); self.addMovies(json.movies, json.total);
self.load_more.set('text', 'load more movies'); self.load_more.set('text', 'load more movies');
if(self.scrollspy) self.scrollspy.start(); if(self.scrollspy) self.scrollspy.start();
self.checkIfEmpty()
} }
}); });
}, },
@ -475,6 +498,28 @@ var MovieList = new Class({
}, },
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && self.total_movies == 0;
if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element);
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){ toElement: function(){
return this.el; return this.el;
} }

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

@ -24,7 +24,7 @@
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover { .movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
} }
.movies .movie_container { .movies .movie_container {
overflow: hidden; overflow: hidden;
} }
@ -313,7 +313,7 @@
padding-bottom: 4px; padding-bottom: 4px;
height: auto; height: auto;
} }
.movies .movie .trailer_container { .movies .movie .trailer_container {
width: 100%; width: 100%;
background: #000; background: #000;
@ -324,7 +324,7 @@
.movies .movie .trailer_container.hide { .movies .movie .trailer_container.hide {
height: 0 !important; height: 0 !important;
} }
.movies .movie .hide_trailer { .movies .movie .hide_trailer {
position: absolute; position: absolute;
top: 0; top: 0;
@ -340,12 +340,12 @@
.movies .movie .hide_trailer.hide { .movies .movie .hide_trailer.hide {
top: -30px; top: -30px;
} }
.movies .movie .try_container { .movies .movie .try_container {
padding: 5px 10px; padding: 5px 10px;
text-align: center; text-align: center;
} }
.movies .movie .try_container a { .movies .movie .try_container a {
margin: 0 5px; margin: 0 5px;
padding: 2px 5px; padding: 2px 5px;
@ -354,15 +354,15 @@
.movies .movie .releases .next_release { .movies .movie .releases .next_release {
border-left: 6px solid #2aa300; border-left: 6px solid #2aa300;
} }
.movies .movie .releases .next_release > :first-child { .movies .movie .releases .next_release > :first-child {
margin-left: -6px; margin-left: -6px;
} }
.movies .movie .releases .last_release { .movies .movie .releases .last_release {
border-left: 6px solid #ffa200; border-left: 6px solid #ffa200;
} }
.movies .movie .releases .last_release > :first-child { .movies .movie .releases .last_release > :first-child {
margin-left: -6px; margin-left: -6px;
} }
@ -394,8 +394,8 @@
border-radius: 0; border-radius: 0;
} }
.movies .alph_nav ul.numbers, .movies .alph_nav ul.numbers,
.movies .alph_nav .counter, .movies .alph_nav .counter,
.movies .alph_nav ul.actions { .movies .alph_nav ul.actions {
list-style: none; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
@ -518,7 +518,7 @@
padding: 3px 7px; padding: 3px 7px;
} }
.movies .alph_nav .mass_edit_form .refresh, .movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .delete { .movies .alph_nav .mass_edit_form .delete {
float: left; float: left;
padding: 8px 0 0 8px; padding: 8px 0 0 8px;
@ -534,5 +534,67 @@
} }
.movies .alph_nav .more_menu > a { .movies .alph_nav .more_menu > a {
background-position: center -157px; background-position: center -158px;
} }
.movies .empty_wanted {
background-image: url('../images/emptylist.png');
height: 750px;
width: 800px;
padding-top: 260px;
margin-top: -50px;
}
.movies .empty_manage {
text-align: center;
font-size: 25px;
line-height: 150%;
}
.movies .empty_manage .after_manage {
margin-top: 30px;
font-size: 16px;
}
.movies .progress {
border-radius: 2px;
padding: 10px;
margin: 5px 0;
text-align: left;
}
.movies .progress > div {
padding: 5px 10px;
font-size: 12px;
line-height: 12px;
text-align: left;
display: inline-block;
width: 49%;
background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%;
border-radius: 3px;
}
.movies .progress > div .folder {
display: inline-block;
padding: 5px 20px 5px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
direction: rtl;
vertical-align: middle;
}
.movies .progress > div .percentage {
font-weight: bold;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);
width: 15%;
text-align: right;
vertical-align: middle;
}

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

@ -16,14 +16,41 @@ var Movie = new Class({
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self)); self.addEvents();
},
addEvents: function(){
var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
['movie.busy', 'searcher.started'].each(function(listener){ ['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+data.id, function(notification){ App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data) if(notification.data)
self.busy(true) self.busy(true)
}); });
}) })
App.addEvent('searcher.ended.'+self.data.id, function(notification){
if(notification.data)
self.busy(false)
});
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self)
self.list.checkIfEmpty();
// Remove events
App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id);
})
}, },
busy: function(set_busy){ busy: function(set_busy){
@ -359,7 +386,7 @@ var ReleaseAction = new Class({
var status = Status.get(release.status_id); var status = Status.get(release.status_id);
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){ if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false; self.hide_on_click = false;
self.show(); self.show();
buttons_done = true; buttons_done = true;
@ -397,19 +424,11 @@ var ReleaseAction = new Class({
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;
release.status = status;
if( status.identifier == 'ignored' || status.identifier == 'failed'){
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
// Create release // 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')}),
@ -442,11 +461,27 @@ var ReleaseAction = new Class({
} }
}) })
).inject(self.release_container) ).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
}); });
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
self.trynext_container.adopt( self.trynext_container.adopt(
new Element('span.or', { new Element('span.or', {
'text': 'Download' 'text': 'This movie is snatched, if anything went wrong, download'
}), }),
self.last_release ? new Element('a.button.orange', { self.last_release ? new Element('a.button.orange', {
'text': 'the same release again', 'text': 'the same release again',
@ -455,7 +490,7 @@ var ReleaseAction = new Class({
} }
}) : null, }) : null,
self.next_release && self.last_release ? new Element('span.or', { self.next_release && self.last_release ? new Element('span.or', {
'text': 'or' 'text': ','
}) : null, }) : null,
self.next_release ? [new Element('a.button.green', { self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release', 'text': self.last_release ? 'another release' : 'the best release',

4
couchpotato/core/plugins/movie/static/search.js

@ -324,7 +324,7 @@ Block.Search.Item = new Class({
var self = this; var self = this;
if(!self.options.hasClass('set')){ if(!self.options.hasClass('set')){
if(self.info.in_library){ if(self.info.in_library){
var in_library = []; var in_library = [];
self.info.in_library.releases.each(function(release){ self.info.in_library.releases.each(function(release){
@ -339,7 +339,7 @@ Block.Search.Item = new Class({
'height': null, 'height': null,
'width': null 'width': null
}) : null, }) : null,
self.info.in_wanted ? new Element('span.in_wanted', { self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', { }) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ') 'text': 'Already in library: ' + in_library.join(', ')

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

@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
for profile in profiles: for profile in profiles:
temp.append(profile.to_dict(self.to_dict)) temp.append(profile.to_dict(self.to_dict))
#db.close()
return temp return temp
def save(self): def save(self):
@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
#db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
db = get_session() db = get_session()
default = db.query(Profile).first() default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict) default_dict = default.to_dict(self.to_dict)
#db.close()
return default_dict return default_dict
@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
order += 1 order += 1
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
except Exception, e: except Exception, e:
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)
#db.close()
return jsonified({ return jsonified({
'success': success, 'success': success,
'message': message 'message': message
@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
) )
p.types.append(profile_type) p.types.append(profile_type)
db.commit()
quality_order += 1 quality_order += 1
order += 1 order += 1
#db.close() db.commit()
return True return True

19
couchpotato/core/plugins/profile/static/profile.js

@ -86,7 +86,10 @@ var Profile = new Class({
}, },
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success){
self.data = json.profile self.data = json.profile;
self.type_container.getElement('li:first-child input[type=checkbox]')
.set('checked', true)
.getParent().addClass('checked');
} }
} }
}); });
@ -239,9 +242,17 @@ Profile.Type = new Class({
), ),
new Element('span.finish').adopt( new Element('span.finish').adopt(
self.finish = new Element('input.inlay.finish[type=checkbox]', { self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish, 'checked': data.finish !== undefined ? data.finish : 1,
'events': { 'events': {
'change': self.fireEvent.bind(self, 'change') 'change': function(e){
if(self.el == self.el.getParent().getElement(':first-child')){
self.finish_class.check();
alert('Top quality always finishes the search')
return;
}
self.fireEvent('change');
}
} }
}) })
), ),
@ -255,7 +266,7 @@ Profile.Type = new Class({
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty'); self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
new Form.Check(self.finish); self.finish_class = new Form.Check(self.finish);
}, },

44
couchpotato/core/plugins/quality/main.py

@ -9,6 +9,7 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType from couchpotato.core.settings.model import Quality, Profile, ProfileType
import os.path import os.path
import re import re
import time
log = CPLog(__name__) log = CPLog(__name__)
@ -68,7 +69,6 @@ class QualityPlugin(Plugin):
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict()) q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
temp.append(q) temp.append(q)
#db.close()
return temp return temp
def single(self, identifier = ''): def single(self, identifier = ''):
@ -80,7 +80,6 @@ class QualityPlugin(Plugin):
if quality: if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
#db.close()
return quality_dict return quality_dict
def getQuality(self, identifier): def getQuality(self, identifier):
@ -100,7 +99,6 @@ class QualityPlugin(Plugin):
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, params.get('value_type'), params.get('value'))
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@ -113,46 +111,48 @@ class QualityPlugin(Plugin):
for q in self.qualities: for q in self.qualities:
# Create quality # Create quality
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first() qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality: if not qual:
log.info('Creating quality: %s', q.get('label')) log.info('Creating quality: %s', q.get('label'))
quality = Quality() qual = Quality()
db.add(quality) qual.order = order
qual.identifier = q.get('identifier')
qual.label = toUnicode(q.get('label'))
qual.size_min, qual.size_max = q.get('size')
quality.order = order db.add(qual)
quality.identifier = q.get('identifier')
quality.label = toUnicode(q.get('label'))
quality.size_min, quality.size_max = q.get('size')
# Create single quality profile # Create single quality profile
profile = db.query(Profile).filter( prof = db.query(Profile).filter(
Profile.core == True Profile.core == True
).filter( ).filter(
Profile.types.any(quality = quality) Profile.types.any(quality = qual)
).all() ).all()
if not profile: if not prof:
log.info('Creating profile: %s', q.get('label')) log.info('Creating profile: %s', q.get('label'))
profile = Profile( prof = Profile(
core = True, core = True,
label = toUnicode(quality.label), label = toUnicode(qual.label),
order = order order = order
) )
db.add(profile) db.add(prof)
profile_type = ProfileType( profile_type = ProfileType(
quality = quality, quality = qual,
profile = profile, profile = prof,
finish = True, finish = True,
order = 0 order = 0
) )
profile.types.append(profile_type) prof.types.append(profile_type)
order += 1 order += 1
db.commit()
#db.close() db.commit()
time.sleep(0.3) # Wait a moment
return True return True
def guess(self, files, extra = {}): def guess(self, files, extra = {}):

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

@ -88,8 +88,6 @@ class Release(Plugin):
fireEvent('movie.restatus', movie.id) fireEvent('movie.restatus', movie.id)
#db.close()
return True return True
@ -108,7 +106,6 @@ class Release(Plugin):
release_id = getParam('id') release_id = getParam('id')
#db.close()
return jsonified({ return jsonified({
'success': self.delete(release_id) 'success': self.delete(release_id)
}) })
@ -152,7 +149,6 @@ class Release(Plugin):
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit() db.commit()
#db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@ -161,6 +157,7 @@ class Release(Plugin):
db = get_session() db = get_session()
id = getParam('id') id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
@ -181,14 +178,16 @@ class Release(Plugin):
'files': {} 'files': {}
}), manual = True, single = True) }), manual = True, single = True)
#db.close() if success:
rel.status_id = status_snatched.get('id')
db.commit()
return jsonified({ return jsonified({
'success': success 'success': success
}) })
else: else:
log.error('Couldn\'t find release with id: %s', id) log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({ return jsonified({
'success': False 'success': False
}) })

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

@ -3,7 +3,8 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified 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, \
getImdb
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, Release from couchpotato.core.settings.model import Library, File, Profile, Release
@ -20,6 +21,7 @@ log = CPLog(__name__)
class Renamer(Plugin): class Renamer(Plugin):
renaming_started = False renaming_started = False
checking_snatched = False
def __init__(self): def __init__(self):
@ -33,6 +35,7 @@ class Renamer(Plugin):
addEvent('app.load', self.scan) addEvent('app.load', self.scan)
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = 2)
def scanView(self): def scanView(self):
@ -48,7 +51,7 @@ class Renamer(Plugin):
return return
if self.renaming_started is True: if self.renaming_started is True:
log.info('Renamer is disabled to avoid infinite looping of the same error.') log.info('Renamer is already running, if you see this often, check the logs above for errors.')
return return
# Check to see if the "to" folder is inside the "from" folder. # Check to see if the "to" folder is inside the "from" folder.
@ -127,6 +130,8 @@ class Renamer(Plugin):
'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'), 'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'], 'imdb_id': library['identifier'],
'cd': '',
'cd_nr': '',
} }
for file_type in group['files']: for file_type in group['files']:
@ -144,7 +149,7 @@ class Renamer(Plugin):
continue continue
# Move other files # Move other files
multiple = len(group['files']['movie']) > 1 and not group['is_dvd'] multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
cd = 1 if multiple else 0 cd = 1 if multiple else 0
for current_file in sorted(list(group['files'][file_type])): for current_file in sorted(list(group['files'][file_type])):
@ -157,23 +162,19 @@ class Renamer(Plugin):
replacements['ext'] = getExt(current_file) replacements['ext'] = getExt(current_file)
# cd # # cd #
replacements['cd'] = ' cd%d' % cd if cd else '' replacements['cd'] = ' cd%d' % cd if multiple else ''
replacements['cd_nr'] = cd replacements['cd_nr'] = cd if multiple else ''
# Naming # Naming
final_folder_name = self.doReplace(folder_name, replacements) final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements) final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)] replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
# Group filename without cd extension
replacements['cd'] = ''
replacements['cd_nr'] = ''
# Meta naming # Meta naming
if file_type is 'trailer': if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements) final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
elif file_type is 'nfo': elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements) final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
# Seperator replace # Seperator replace
if separator: if separator:
@ -204,10 +205,16 @@ class Renamer(Plugin):
# Check for extra subtitle files # Check for extra subtitle files
if file_type is 'subtitle': if file_type is 'subtitle':
# rename subtitles with or without language remove_multiple = False
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name) if len(group['files']['movie']) == 1:
remove_multiple = True
sub_langs = group['subtitle_language'].get(current_file, []) sub_langs = group['subtitle_language'].get(current_file, [])
# rename subtitles with or without language
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra', extra_type = 'subtitle_extra',
replacements = replacements, replacements = replacements,
@ -215,20 +222,19 @@ class Renamer(Plugin):
file_name = file_name, file_name = file_name,
destination = destination, destination = destination,
group = group, group = group,
current_file = current_file current_file = current_file,
remove_multiple = remove_multiple,
) )
# Don't add language if multiple languages in 1 file # Don't add language if multiple languages in 1 subtitle file
if len(sub_langs) > 1: if len(sub_langs) == 1:
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
elif len(sub_langs) == 1:
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext'])) sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_files = mergeDicts(rename_files, rename_extras) rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc # Filename without cd etc
if file_type is 'movie': elif file_type is 'movie':
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
extra_type = 'movie_extra', extra_type = 'movie_extra',
replacements = replacements, replacements = replacements,
@ -240,7 +246,7 @@ class Renamer(Plugin):
) )
rename_files = mergeDicts(rename_files, rename_extras) rename_files = mergeDicts(rename_files, rename_extras)
group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)] group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
group['destination_dir'] = os.path.join(destination, final_folder_name) group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple: if multiple:
@ -375,22 +381,19 @@ class Renamer(Plugin):
except: except:
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc())) log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Search for trailers etc # Notify on download, search for trailers etc
fireEventAsync('renamer.after', group)
# Notify on download
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality']) download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group) fireEvent('renamer.after', message = download_message, group = group, in_order = True)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
#db.close()
self.renaming_started = False self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''): def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
replacements = replacements.copy()
rename_files = {} rename_files = {}
def test(s): def test(s):
@ -399,8 +402,8 @@ class Renamer(Plugin):
for extra in set(filter(test, group['files'][extra_type])): for extra in set(filter(test, group['files'][extra_type])):
replacements['ext'] = getExt(extra) replacements['ext'] = getExt(extra)
final_folder_name = self.doReplace(folder_name, replacements) final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
final_file_name = self.doReplace(file_name, replacements) final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name) rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
return rename_files return rename_files
@ -455,11 +458,16 @@ class Renamer(Plugin):
return True return True
def doReplace(self, string, replacements): def doReplace(self, string, replacements, remove_multiple = False):
''' '''
replace confignames with the real thing replace confignames with the real thing
''' '''
replacements = replacements.copy()
if remove_multiple:
replacements['cd'] = ''
replacements['cd_nr'] = ''
replaced = toUnicode(string) replaced = toUnicode(string)
for x, r in replacements.iteritems(): for x, r in replacements.iteritems():
if r is not None: if r is not None:
@ -495,6 +503,11 @@ class Renamer(Plugin):
loge('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): def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True) ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True) failed_status = fireEvent('status.get', 'failed', single = True)
@ -504,57 +517,69 @@ class Renamer(Plugin):
db = get_session() db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
if rels:
log.debug('Checking status snatched releases...')
scan_required = False scan_required = False
for rel in rels: if rels:
self.checking_snatched = True
# Get current selected title log.debug('Checking status snatched releases...')
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 statuses = fireEvent('download.status', merge = True)
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True) if not statuses:
if not downloadstatus:
log.debug('Download status functionality is not implemented for active downloaders.') log.debug('Download status functionality is not implemented for active downloaders.')
scan_required = True scan_required = True
else: else:
log.debug('Download status: %s' , downloadstatus) try:
for rel in rels:
if downloadstatus == 'failed': rel_dict = rel.to_dict({'info': {}})
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id) # Get current selected title
else: default_title = getTitle(rel.movie.library)
rel.status_id = failed_status.get('id')
db.commit() # Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.info('Download of %s failed.', item['name']) 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
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
found = False
for item in statuses:
if item['name'] == nzbname or getImdb(item['name']) == movie_dict['library']['identifier']:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
if item['status'] == 'busy':
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
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()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
found = True
break
elif downloadstatus == 'completed': if not found:
log.info('Download of %s completed!', item['name']) log.info('%s not found in downloaders', nzbname)
scan_required = True
elif downloadstatus == 'not_found': except:
log.info('%s not found in downloaders', item['name']) log.error('Failed checking for release in downloader: %s', traceback.format_exc())
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: if scan_required:
fireEvent('renamer.scan') fireEvent('renamer.scan')
self.checking_snatched = False
return True

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

@ -34,6 +34,7 @@ class Scanner(Plugin):
'subtitle_extra': ['idx'], 'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv'] 'trailer': ['mov', 'mp4', 'flv']
} }
file_types = { file_types = {
'subtitle': ('subtitle', 'subtitle'), 'subtitle': ('subtitle', 'subtitle'),
'subtitle_extra': ('subtitle', 'subtitle_extra'), 'subtitle_extra': ('subtitle', 'subtitle_extra'),
@ -42,6 +43,8 @@ class Scanner(Plugin):
'movie': ('video', 'movie'), 'movie': ('video', 'movie'),
'movie_extra': ('movie', 'movie_extra'), 'movie_extra': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'), 'backdrop': ('image', 'backdrop'),
'poster': ('image', 'poster'),
'thumbnail': ('image', 'thumbnail'),
'leftover': ('leftover', 'leftover'), 'leftover': ('leftover', 'leftover'),
} }
@ -80,55 +83,10 @@ class Scanner(Plugin):
addEvent('scanner.remove_cptag', self.removeCPTag) addEvent('scanner.remove_cptag', self.removeCPTag)
addEvent('scanner.scan', self.scan) addEvent('scanner.scan', self.scan)
addEvent('scanner.files', self.scanFilesToLibrary)
addEvent('scanner.folder', self.scanFolderToLibrary)
addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber) addEvent('scanner.partnumber', self.getPartNumber)
def after_rename(group): def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename)
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
folder = os.path.normpath(folder)
if not os.path.isdir(folder):
return
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = []
while True and not self.shuttingDown():
try:
identifier, group = groups.popitem()
except:
break
# Save to DB
if group['library']:
# Add release
fireEvent('release.add', group = group)
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
if library_item:
added_identifier.append(library_item['identifier'])
return added_identifier
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
folder = ss(os.path.normpath(folder)) folder = ss(os.path.normpath(folder))
@ -141,7 +99,8 @@ class Scanner(Plugin):
leftovers = [] leftovers = []
# Scan all files of the folder if no files are set # Scan all files of the folder if no files are set
if len(files) == 0: if not files:
check_file_date = True
try: try:
files = [] files = []
for root, dirs, walk_files in os.walk(folder): for root, dirs, walk_files in os.walk(folder):
@ -150,6 +109,7 @@ class Scanner(Plugin):
except: except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc())) log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else: else:
check_file_date = False
files = [ss(x) for x in files] files = [ss(x) for x in files]
db = get_session() db = get_session()
@ -279,8 +239,8 @@ class Scanner(Plugin):
del path_identifiers[identifier] del path_identifiers[identifier]
del delete_identifiers del delete_identifiers
# Determine file types # Make sure we remove older / still extracting files
processed_movies = {} valid_files = {}
while True and not self.shuttingDown(): while True and not self.shuttingDown():
try: try:
identifier, group = movie_files.popitem() identifier, group = movie_files.popitem()
@ -302,7 +262,7 @@ class Scanner(Plugin):
if file_too_new: if file_too_new:
break break
if file_too_new: if check_file_date and file_too_new:
try: try:
time_string = time.ctime(file_time[0]) time_string = time.ctime(file_time[0])
except: except:
@ -320,17 +280,33 @@ class Scanner(Plugin):
# Only process movies newer than x # Only process movies newer than x
if newer_than and newer_than > 0: if newer_than and newer_than > 0:
has_new_files = False
for cur_file in group['unsorted_files']: for cur_file in group['unsorted_files']:
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)] file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
if file_time[0] > time.time() or file_time[1] > time.time(): if file_time[0] > newer_than or file_time[1] > newer_than:
has_new_files = True
break break
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier)) if not has_new_files:
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
# Delete the unsorted list # Delete the unsorted list
del group['unsorted_files'] del group['unsorted_files']
continue continue
valid_files[identifier] = group
del movie_files
# Determine file types
processed_movies = {}
total_found = len(valid_files)
while True and not self.shuttingDown():
try:
identifier, group = valid_files.popitem()
except:
break
# Group extra (and easy) files first # Group extra (and easy) files first
# images = self.getImages(group['unsorted_files']) # images = self.getImages(group['unsorted_files'])
@ -392,9 +368,12 @@ class Scanner(Plugin):
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first() movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group processed_movies[identifier] = group
# Notify parent & progress on something found
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
if len(processed_movies) > 0: if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder)) log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else: else:
@ -539,7 +518,6 @@ class Scanner(Plugin):
break break
except: except:
pass pass
#db.close()
# Search based on OpenSubtitleHash # Search based on OpenSubtitleHash
if not imdb_id and not group['is_dvd']: if not imdb_id and not group['is_dvd']:

2
couchpotato/core/plugins/score/scores.py

@ -10,7 +10,7 @@ name_scores = [
# Video # Video
'x264:1', 'h264:1', 'x264:1', 'h264:1',
# Audio # Audio
'DTS:4', 'AC3:2', 'dts:4', 'ac3:2',
# Quality # Quality
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1', '720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
# Language / Subs # Language / Subs

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

@ -1,6 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle from couchpotato.core.helpers.variable import md5, getTitle
@ -36,9 +36,38 @@ class Searcher(Plugin):
}, },
}) })
addApiView('searcher.full_search', self.allMoviesView, docs = {
'desc': 'Starts a full search for all wanted movies',
})
addApiView('searcher.progress', self.getProgress, docs = {
'desc': 'Get the progress of current full search',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.allMovies, 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 allMoviesView(self):
in_progress = self.in_progress
if not in_progress:
fireEventAsync('searcher.all')
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({
'success': not in_progress
})
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def allMovies(self): def allMovies(self):
@ -54,6 +83,11 @@ class Searcher(Plugin):
Movie.status.has(identifier = 'active') Movie.status.has(identifier = 'active')
).all() ).all()
self.in_progress = {
'total': len(movies),
'to_go': len(movies),
}
for movie in movies: for movie in movies:
movie_dict = movie.to_dict({ movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
@ -65,15 +99,17 @@ class Searcher(Plugin):
try: try:
self.single(movie_dict) self.single(movie_dict)
except IndexError: except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update', movie_dict['library']['identifier'], force = True) fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except: except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
#db.close()
self.in_progress = False self.in_progress = False
def single(self, movie): def single(self, movie):
@ -144,10 +180,10 @@ class Searcher(Plugin):
status_id = available_status.get('id') status_id = available_status.get('id')
) )
db.add(rls) db.add(rls)
db.commit()
else: else:
[db.delete(info) for info in rls.info] [db.delete(old_info) for old_info in rls.info]
db.commit()
db.commit()
for info in nzb: for info in nzb:
try: try:
@ -159,10 +195,11 @@ class Searcher(Plugin):
value = toUnicode(nzb[info]) value = toUnicode(nzb[info])
) )
rls.info.append(rls_info) rls.info.append(rls_info)
db.commit()
except InterfaceError: except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id nzb['status_id'] = rls.status_id
@ -192,7 +229,6 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return ret return ret
def download(self, data, movie, manual = False): def download(self, data, movie, manual = False):
@ -210,40 +246,43 @@ class Searcher(Plugin):
if successful: if successful:
# Mark release as snatched try:
db = get_session() # Mark release as snatched
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() db = get_session()
rls.status_id = snatched_status.get('id') rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
db.commit() if rls:
rls.status_id = snatched_status.get('id')
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
#db.close()
return True return True
log.info('Tried to download, but none of the downloaders are enabled') log.info('Tried to download, but none of the downloaders are enabled')
@ -254,7 +293,7 @@ class Searcher(Plugin):
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', 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 0 < retention < nzb.get('age', 0):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False return False
@ -354,9 +393,11 @@ class Searcher(Plugin):
year_name = fireEvent('scanner.name_year', name, single = True) year_name = fireEvent('scanner.name_year', name, single = True)
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr if size > 3000: # Assume dvdr
return 'dvdr' == preferred_quality['identifier'] log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
found['dvdr'] = True
else: # Assume dvdrip else: # Assume dvdrip
return 'dvdrip' == preferred_quality['identifier'] log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
found['dvdrip'] = True
# Allow other qualities # Allow other qualities
for allowed in preferred_quality.get('allow'): for allowed in preferred_quality.get('allow'):
@ -373,12 +414,17 @@ class Searcher(Plugin):
return False return False
def correctYear(self, haystack, year, range): def correctYear(self, haystack, year, year_range):
for string in haystack: for string in haystack:
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
year_name = fireEvent('scanner.name_year', string, single = True)
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
return True return True
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
return False return False
def correctName(self, check_name, movie_name): def correctName(self, check_name, movie_name):
@ -410,6 +456,11 @@ class Searcher(Plugin):
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0): if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
return True return True
else: else:
# For movies before 1972
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if wanted_quality in pre_releases: if wanted_quality in pre_releases:
# Prerelease 1 week before theaters # Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now: if dates.get('theater') - 604800 < now:

4
couchpotato/core/plugins/subtitle/__init__.py

@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle', 'name': 'subtitle',
'label': 'Download subtitles after rename', 'label': 'Download subtitles',
'description': 'after rename',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

7
couchpotato/core/plugins/subtitle/main.py

@ -1,6 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
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, FileType from couchpotato.core.settings.model import Library, FileType
@ -13,7 +14,7 @@ log = CPLog(__name__)
class Subtitle(Plugin): class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki'] services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
def __init__(self): def __init__(self):
addEvent('renamer.before', self.searchSingle) addEvent('renamer.before', self.searchSingle)
@ -40,8 +41,6 @@ class Subtitle(Plugin):
# get subtitles for those files # get subtitles for those files
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
#db.close()
def searchSingle(self, group): def searchSingle(self, group):
if self.isDisabled(): return if self.isDisabled(): return
@ -69,4 +68,4 @@ class Subtitle(Plugin):
return False return False
def getLanguages(self): def getLanguages(self):
return [x.strip() for x in self.conf('languages').split(',')] return splitString(self.conf('languages'))

10
couchpotato/core/plugins/trailer/__init__.py

@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'subtab': 'trailer',
'name': 'trailer', 'name': 'trailer',
'label': 'Download trailer after rename', 'label': 'Download trailer',
'description': 'after rename',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@ -24,12 +24,6 @@ config = [{
'type': 'dropdown', 'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
}, },
{
'name': 'automatic',
'default': False,
'type': 'bool',
'description': 'Automaticly search & download for movies in library',
},
], ],
}, },
], ],

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

@ -12,7 +12,7 @@ class Trailer(Plugin):
def __init__(self): def __init__(self):
addEvent('renamer.after', self.searchSingle) addEvent('renamer.after', self.searchSingle)
def searchSingle(self, group): def searchSingle(self, message = None, group = {}):
if self.isDisabled() or len(group['files']['trailer']) > 0: return if self.isDisabled() or len(group['files']['trailer']) > 0: return
@ -28,6 +28,8 @@ class Trailer(Plugin):
else: else:
log.debug('Trailer already exists: %s', destination) log.debug('Trailer already exists: %s', destination)
group['renamed_files'].append(destination)
# Download first and break # Download first and break
break break

27
couchpotato/core/plugins/wizard/static/wizard.css

@ -14,12 +14,13 @@
.page.wizard .tab_wrapper { .page.wizard .tab_wrapper {
background: #5c697b; background: #5c697b;
padding: 18px 0; padding: 10px 0;
font-size: 23px; font-size: 18px;
position: fixed; position: fixed;
top: 0; top: 0;
margin: 0; margin: 0;
width: 100%; width: 100%;
min-width: 960px;
left: 0; left: 0;
z-index: 2; z-index: 2;
box-shadow: 0 0 50px rgba(0,0,0,0.55); box-shadow: 0 0 50px rgba(0,0,0,0.55);
@ -36,7 +37,7 @@
display: inline-block; display: inline-block;
} }
.page.wizard .tabs li a { .page.wizard .tabs li a {
padding: 20px 30px; padding: 20px 10px;
} }
.page.wizard .tab_wrapper .pointer { .page.wizard .tab_wrapper .pointer {
@ -45,7 +46,7 @@
border-top: 10px solid #5c697b; border-top: 10px solid #5c697b;
display: block; display: block;
position: absolute; position: absolute;
top: 60px; top: 44px;
} }
.page.wizard .tab_content { .page.wizard .tab_content {
@ -58,11 +59,25 @@
.page.wizard .wgroup_finish { .page.wizard .wgroup_finish {
height: 300px; height: 300px;
} }
.page.wizard .wgroup_finish h1 {
text-align: center;
}
.page.wizard .wgroup_finish .wizard_support,
.page.wizard .wgroup_finish .description {
font-size: 25px;
line-height: 120%;
margin: 20px 0;
text-align: center;
}
.page.wizard .button.green { .page.wizard .button.green {
padding: 20px; padding: 20px;
font-size: 25px; font-size: 25px;
margin: 10px 30px; margin: 10px 30px 80px;
display: block; display: block;
text-align: center; text-align: center;
} }
.page.wizard .tab_nzb_providers {
margin: 20px 0 0 0;
}

478
couchpotato/core/plugins/wizard/static/wizard.js

@ -1,214 +1,266 @@
Page.Wizard = new Class({ Page.Wizard = new Class({
Extends: Page.Settings, Extends: Page.Settings,
name: 'wizard', name: 'wizard',
has_tab: false, has_tab: false,
wizard_only: true, wizard_only: true,
headers: { headers: {
'welcome': { 'welcome': {
'title': 'Welcome to the new CouchPotato', 'title': 'Welcome to the new CouchPotato',
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato', 'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
'content': new Element('div', { 'content': new Element('div', {
'styles': { 'styles': {
'margin': '0 0 0 30px' 'margin': '0 0 0 30px'
} }
}).adopt( }).adopt(
new Element('div', { new Element('div', {
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.' 'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
}), }),
self.import_iframe = new Element('iframe', { self.import_iframe = new Element('iframe', {
'styles': { 'styles': {
'height': 40, 'height': 40,
'width': 300, 'width': 300,
'border': 0, 'border': 0,
'overflow': 'hidden' 'overflow': 'hidden'
} }
}) })
), ),
'event': function(){ 'event': function(){
self.import_iframe.set('src', Api.createUrl('v1.import')) self.import_iframe.set('src', Api.createUrl('v1.import'))
} }
}, },
'general': { 'general': {
'title': 'General', 'title': 'General',
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.' 'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
}, },
'downloaders': { 'downloaders': {
'title': 'What download apps are you using?', 'title': 'What download apps are you using?',
'description': 'If you don\'t have any of these listed, you have to use Blackhole. Or drop me a line, maybe I\'ll support your download app.' 'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use Blackhole.'
}, },
'providers': { 'providers': {
'title': 'Are you registered at any of these sites?', 'title': 'Are you registered at any of these sites?',
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more.' 'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.',
}, 'include': ['nzb_providers', 'torrent_providers']
'renamer': { },
'title': 'Move & rename the movies after downloading?', 'renamer': {
'description': '' 'title': 'Move & rename the movies after downloading?',
}, 'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
'finish': { },
'title': 'Finish Up', 'automation': {
'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!', 'title': 'Easily add movies to your wanted list!',
'content': new Element('div').adopt( 'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' +
new Element('a.button.green', { '<br />Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!', 'content': function(){
'events': { return App.createUserscriptButtons().setStyles({
'click': function(e){ 'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
(e).preventDefault(); })
Api.request('settings.save', { }
'data': { },
'section': 'core', 'finish': {
'name': 'show_wizard', 'title': 'Finishing Up',
'value': 0 'description': 'Are you done? Did you fill in everything as much as possible?' +
}, '<br />Be sure to check the settings to see what more CP can do!<br /><br />' +
'useSpinner': true, '<div class="wizard_support">After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. <br />Or by getting a subscription at <a href="https://usenetserver.com/partners/?a_aid=couchpotato&a_bid=3f357c6f">Usenet Server</a> or <a href="http://www.newshosting.com/partners/?a_aid=couchpotato&a_bid=a0b022df">Newshosting</a>.</div>',
'spinnerOptions': { 'content': new Element('div').adopt(
'target': self.el new Element('a.button.green', {
}, 'styles': {
'onComplete': function(){ 'margin-top': 20
window.location = App.createUrl(); },
} 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
}); 'events': {
} 'click': function(e){
} (e).preventDefault();
}) Api.request('settings.save', {
) 'data': {
} 'section': 'core',
}, 'name': 'show_wizard',
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'finish'], 'value': 0
},
open: function(action, params){ 'useSpinner': true,
var self = this; 'spinnerOptions': {
'target': self.el
if(!self.initialized){ },
App.fireEvent('unload'); 'onComplete': function(){
App.getBlock('header').hide(); window.location = App.createUrl();
}
self.parent(action, params); });
}
self.addEvent('create', function(){ }
self.order(); })
}); )
}
self.initialized = true; },
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'automation', 'finish'],
self.scroll = new Fx.Scroll(document.body, {
'transition': 'quint:in:out' open: function(action, params){
}); var self = this;
}
else if(!self.initialized){
(function(){ App.fireEvent('unload');
var sc = self.el.getElement('.wgroup_'+action); App.getBlock('header').hide();
self.scroll.start(0, sc.getCoordinates().top-80);
}).delay(1) self.parent(action, params);
},
self.addEvent('create', function(){
order: function(){ self.order();
var self = this; });
var form = self.el.getElement('.uniForm'); self.initialized = true;
var tabs = self.el.getElement('.tabs');
self.scroll = new Fx.Scroll(document.body, {
self.groups.each(function(group){ 'transition': 'quint:in:out'
if(self.headers[group]){ });
group_container = new Element('.wgroup_'+group, { }
'styles': { else
'opacity': 0.2 (function(){
}, var sc = self.el.getElement('.wgroup_'+action);
'tween': { self.scroll.start(0, sc.getCoordinates().top-80);
'duration': 350 }).delay(1)
} },
});
group_container.adopt( order: function(){
new Element('h1', { var self = this;
'text': self.headers[group].title
}), var form = self.el.getElement('.uniForm');
self.headers[group].description ? new Element('span.description', { var tabs = self.el.getElement('.tabs');
'html': self.headers[group].description
}) : null, self.groups.each(function(group, nr){
self.headers[group].content ? self.headers[group].content : null
).inject(form); if(self.headers[group]){
} group_container = new Element('.wgroup_'+group, {
'styles': {
var tab_navigation = tabs.getElement('.t_'+group); 'opacity': 0.2
if(tab_navigation && group_container){ },
tab_navigation.inject(tabs); // Tab navigation 'tween': {
self.el.getElement('.tab_'+group).inject(group_container); // Tab content 'duration': 350
if(self.headers[group]){ }
var a = tab_navigation.getElement('a'); });
a.set('text', (self.headers[group].label || group).capitalize());
var url_split = a.get('href').split('wizard')[1].split('/'); if(self.headers[group].include){
if(url_split.length > 3) self.headers[group].include.each(function(inc){
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', '')); group_container.addClass('wgroup_'+inc);
})
} }
}
else { var content = self.headers[group].content
new Element('li.t_'+group).adopt( group_container.adopt(
new Element('a', { new Element('h1', {
'href': App.createUrl('wizard/'+group), 'text': self.headers[group].title
'text': (self.headers[group].label || group).capitalize() }),
}) self.headers[group].description ? new Element('span.description', {
).inject(tabs); 'html': self.headers[group].description
} }) : null,
content ? (typeOf(content) == 'function' ? content() : content) : null
if(self.headers[group] && self.headers[group].event) ).inject(form);
self.headers[group].event.call() }
});
var tab_navigation = tabs.getElement('.t_'+group);
// Remove toggle
self.el.getElement('.advanced_toggle').destroy(); if(!tab_navigation && self.headers[group] && self.headers[group].include){
tab_navigation = []
// Hide retention self.headers[group].include.each(function(inc){
self.el.getElement('.tab_searcher').hide(); tab_navigation.include(tabs.getElement('.t_'+inc));
self.el.getElement('.t_searcher').hide(); })
}
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt( if(tab_navigation && group_container){
self.pointer = new Element('.pointer', { tabs.adopt(tab_navigation); // Tab navigation
'tween': {
'transition': 'quint:in:out' if(self.headers[group] && self.headers[group].include){
}
}) self.headers[group].include.each(function(inc){
); self.el.getElement('.tab_'+inc).inject(group_container);
})
// Add nav
var minimum = self.el.getSize().y-window.getSize().y; new Element('li.t_'+group).adopt(
self.groups.each(function(group, nr){ new Element('a', {
'href': App.createUrl('wizard/'+group),
var g = self.el.getElement('.wgroup_'+group); 'text': (self.headers[group].label || group).capitalize()
if(!g || !g.isVisible()) return; })
var t = self.el.getElement('.t_'+group); ).inject(tabs);
if(!t) return;
}
var func = function(){ else
var ct = t.getCoordinates(); self.el.getElement('.tab_'+group).inject(group_container); // Tab content
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
g.tween('opacity', 1); if(tab_navigation.getElement && self.headers[group]){
} var a = tab_navigation.getElement('a');
a.set('text', (self.headers[group].label || group).capitalize());
if(nr == 0) var url_split = a.get('href').split('wizard')[1].split('/');
func(); if(url_split.length > 3)
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
var ss = new ScrollSpy( { }
min: function(){ }
var c = g.getCoordinates(); else {
var top = c.top-(window.getSize().y/2); new Element('li.t_'+group).adopt(
return top > minimum ? minimum : top new Element('a', {
}, 'href': App.createUrl('wizard/'+group),
max: function(){ 'text': (self.headers[group].label || group).capitalize()
var c = g.getCoordinates(); })
return c.top+(c.height/2) ).inject(tabs);
}, }
onEnter: func,
onLeave: function(){ if(self.headers[group] && self.headers[group].event)
g.tween('opacity', 0.2) self.headers[group].event.call()
} });
});
}); // Remove toggle
self.el.getElement('.advanced_toggle').destroy();
}
// Hide retention
self.el.getElement('.tab_searcher').hide();
self.el.getElement('.t_searcher').hide();
self.el.getElement('.t_nzb_providers').hide();
self.el.getElement('.t_torrent_providers').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
self.groups.each(function(group, nr){
var g = self.el.getElement('.wgroup_'+group);
if(!g || !g.isVisible()) return;
var t = self.el.getElement('.t_'+group);
if(!t) return;
var func = function(){
var ct = t.getCoordinates();
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
g.tween('opacity', 1);
}
if(nr == 0)
func();
var ss = new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2);
return top > minimum ? minimum : top
},
max: function(){
var c = g.getCoordinates();
return c.top+(c.height/2)
},
onEnter: func,
onLeave: function(){
g.tween('opacity', 0.2)
}
});
});
}
}); });

9
couchpotato/core/providers/automation/base.py

@ -28,9 +28,18 @@ class Automation(Plugin):
return self.getIMDBids() return self.getIMDBids()
def search(self, name, year = None, imdb_only = False): def search(self, name, year = None, imdb_only = False):
prop_name = 'automation.cached.%s.%s' % (name, year)
cached_imdb = Env.prop(prop_name, default = False)
if cached_imdb and imdb_only:
return cached_imdb
result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True) result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True)
if len(result) > 0: if len(result) > 0:
if imdb_only and result[0].get('imdb'):
Env.prop(prop_name, result[0].get('imdb'))
return result[0].get('imdb') if imdb_only else result[0] return result[0].get('imdb') if imdb_only else result[0]
else: else:
return None return None

1
couchpotato/core/providers/automation/bluray/main.py

@ -2,7 +2,6 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, tryInt from couchpotato.core.helpers.variable import md5, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)

17
couchpotato/core/providers/automation/imdb/main.py

@ -2,9 +2,6 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb from couchpotato.core.helpers.variable import md5, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@ -34,10 +31,6 @@ class IMDB(Automation, RSS):
log.error('This isn\'t the correct url.: %s', rss_url) log.error('This isn\'t the correct url.: %s', rss_url)
continue continue
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
last_movie_added = 0
try: try:
cache_key = 'imdb.rss.%s' % md5(rss_url) cache_key = 'imdb.rss.%s' % md5(rss_url)
@ -46,20 +39,10 @@ class IMDB(Automation, RSS):
rss_movies = self.getElements(data, 'channel/item') rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies: for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
imdb = getImdb(self.getTextElement(movie, "link")) imdb = getImdb(self.getTextElement(movie, "link"))
if created > last_movie_added:
last_movie_added = created
if not imdb or created <= last_update:
continue
movies.append(imdb) movies.append(imdb)
except: except:
log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc())) log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, last_movie_added)
return movies return movies

15
couchpotato/core/providers/automation/movies_io/main.py

@ -3,10 +3,7 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
from xml.etree.ElementTree import ParseError from xml.etree.ElementTree import ParseError
import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
@ -33,10 +30,6 @@ class MoviesIO(Automation, RSS):
if not enablers[index]: if not enablers[index]:
continue continue
prop_name = 'automation.moviesio.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
last_movie_added = 0
try: try:
cache_key = 'imdb.rss.%s' % md5(rss_url) cache_key = 'imdb.rss.%s' % md5(rss_url)
@ -45,12 +38,6 @@ class MoviesIO(Automation, RSS):
rss_movies = self.getElements(data, 'channel/item') rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies: for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
if created > last_movie_added:
last_movie_added = created
if created <= last_update:
continue
nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True) nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True)
imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True) imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True)
@ -64,6 +51,4 @@ class MoviesIO(Automation, RSS):
except: except:
log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc())) log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, last_movie_added)
return movies return movies

28
couchpotato/core/providers/metadata/base.py

@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts
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
import os import os
import shutil import shutil
import traceback import traceback
@ -16,23 +17,23 @@ class MetaDataBase(Plugin):
def __init__(self): def __init__(self):
addEvent('renamer.after', self.create) addEvent('renamer.after', self.create)
def create(self, release): def create(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
log.info('Creating %s metadata.', self.getName()) log.info('Creating %s metadata.', self.getName())
# Update library to get latest info # Update library to get latest info
try: try:
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True) updated_library = fireEvent('library.update', group['library']['identifier'], force = True, single = True)
release['library'] = mergeDicts(release['library'], updated_library) group['library'] = mergeDicts(group['library'], updated_library)
except: except:
log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
root_name = self.getRootName(release) root_name = self.getRootName(group)
meta_name = os.path.basename(root_name) meta_name = os.path.basename(root_name)
root = os.path.dirname(root_name) root = os.path.dirname(root_name)
movie_info = release['library'].get('info') movie_info = group['library'].get('info')
for file_type in ['nfo', 'thumbnail', 'fanart']: for file_type in ['nfo', 'thumbnail', 'fanart']:
try: try:
@ -42,13 +43,19 @@ class MetaDataBase(Plugin):
if name and self.conf('meta_' + file_type): if name and self.conf('meta_' + file_type):
# Get file content # Get file content
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release) content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group)
if content: if content:
log.debug('Creating %s file: %s', (file_type, name)) log.debug('Creating %s file: %s', (file_type, name))
if os.path.isfile(content): if os.path.isfile(content):
shutil.copy2(content, name) shutil.copy2(content, name)
else: else:
self.createFile(name, content) self.createFile(name, content)
group['renamed_files'].append(name)
try:
os.chmod(name, Env.getPermission('file'))
except:
log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc()))
except: except:
log.error('Unable to create %s file: %s', (file_type, traceback.format_exc())) log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
@ -74,9 +81,18 @@ class MetaDataBase(Plugin):
if file_type.get('identifier') == wanted_file_type: if file_type.get('identifier') == wanted_file_type:
break break
# See if it is in current files
for cur_file in data['library'].get('files', []): for cur_file in data['library'].get('files', []):
if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')): if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
return cur_file.get('path') return cur_file.get('path')
# Download using existing info
try:
images = data['library']['info']['images'][wanted_file_type]
file_path = fireEvent('file.download', url = images[0], single = True)
return file_path
except:
pass
def getFanart(self, movie_info = {}, data = {}): def getFanart(self, movie_info = {}, data = {}):
return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original') return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original')

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

@ -70,7 +70,6 @@ class MovieResultModifier(Plugin):
except: except:
log.error('Tried getting more info on searched movies: %s', traceback.format_exc()) log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
#db.close()
return temp return temp
def checkLibrary(self, result): def checkLibrary(self, result):

62
couchpotato/core/providers/movie/couchpotatoapi/main.py

@ -1,39 +1,71 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from flask.helpers import json from flask.helpers import json
import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)
class CouchPotatoApi(MovieProvider): class CouchPotatoApi(MovieProvider):
api_url = 'http://couchpota.to/api/%s/' urls = {
'search': 'https://couchpota.to/api/search/%s/',
'info': 'https://couchpota.to/api/info/%s/',
'eta': 'https://couchpota.to/api/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/%s/%s/',
}
http_time_between_calls = 0 http_time_between_calls = 0
api_version = 1
def __init__(self): def __init__(self):
#addApiView('movie.suggest', self.suggestView) #addApiView('movie.suggest', self.suggestView)
addEvent('movie.info', self.getInfo) addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate) addEvent('movie.release_date', self.getReleaseDate)
def search(self, q, limit = 12):
cache_key = 'cpapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode(q), timeout = 3, headers = self.getRequestHeaders())
if cached:
try:
movies = json.loads(cached)
return movies
except:
log.error('Failed parsing search results: %s', traceback.format_exc())
return []
def getInfo(self, identifier = None): def getInfo(self, identifier = None):
return {
'release_date': self.getReleaseDate(identifier) if not identifier:
} return
cache_key = 'cpapi.cache.info.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = self.getRequestHeaders())
if cached:
try:
movie = json.loads(cached)
return movie
except:
log.error('Failed parsing info results: %s', traceback.format_exc())
return {}
def getReleaseDate(self, identifier = None): def getReleaseDate(self, identifier = None):
if identifier is None: return {} if identifier is None: return {}
try: try:
headers = { data = self.urlopen(self.urls['eta'] % identifier, headers = self.getRequestHeaders())
'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': 1,
}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
dates = json.loads(data) dates = json.loads(data)
log.debug('Found ETA for %s: %s', (identifier, dates)) log.debug('Found ETA for %s: %s', (identifier, dates))
return dates return dates
@ -44,7 +76,7 @@ class CouchPotatoApi(MovieProvider):
def suggest(self, movies = [], ignore = []): def suggest(self, movies = [], ignore = []):
try: try:
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/') data = self.urlopen(self.urls['suggest'] % (','.join(movies), ','.join(ignore)))
suggestions = json.loads(data) suggestions = json.loads(data)
log.info('Found Suggestions for %s', (suggestions)) log.info('Found Suggestions for %s', (suggestions))
except Exception, e: except Exception, e:
@ -62,7 +94,6 @@ class CouchPotatoApi(MovieProvider):
db = get_session() db = get_session()
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all() active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
movies = [x.library.identifier for x in active_movies] movies = [x.library.identifier for x in active_movies]
#db.close()
suggestions = self.suggest(movies, ignore) suggestions = self.suggest(movies, ignore)
@ -71,3 +102,10 @@ class CouchPotatoApi(MovieProvider):
'count': len(suggestions), 'count': len(suggestions),
'suggestions': suggestions 'suggestions': suggestions
}) })
def getRequestHeaders(self):
return {
'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': self.api_version,
'X-CP-Time': time.time(),
}

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

@ -1,6 +1,6 @@
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
import json import json
@ -27,8 +27,10 @@ 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 or (name_year and not name_year.get('name')): if not name_year or (name_year and not name_year.get('name')):
return [] name_year = {
'name': q
}
cache_key = 'imdbapi.cache.%s' % q cache_key = 'imdbapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3) cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
@ -97,10 +99,10 @@ class IMDBAPI(MovieProvider):
'released': movie.get('Released', ''), 'released': movie.get('Released', ''),
'year': year if isinstance(year, (int)) else None, 'year': year if isinstance(year, (int)) else None,
'plot': movie.get('Plot', ''), 'plot': movie.get('Plot', ''),
'genres': movie.get('Genre', '').split(','), 'genres': splitString(movie.get('Genre', '')),
'directors': movie.get('Director', '').split(','), 'directors': splitString(movie.get('Director', '')),
'writers': movie.get('Writer', '').split(','), 'writers': splitString(movie.get('Writer', '')),
'actors': movie.get('Actors', '').split(','), 'actors': splitString(movie.get('Actors', '')),
} }
except: except:
log.error('Failed parsing IMDB API json: %s', traceback.format_exc()) log.error('Failed parsing IMDB API json: %s', traceback.format_exc())

4
couchpotato/core/providers/movie/themoviedb/main.py

@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider):
def __init__(self): def __init__(self):
addEvent('movie.by_hash', self.byHash) addEvent('movie.by_hash', self.byHash)
addEvent('movie.search', self.search, priority = 1) addEvent('movie.search', self.search, priority = 2)
addEvent('movie.info', self.getInfo, priority = 1) addEvent('movie.info', self.getInfo, priority = 2)
addEvent('movie.info_by_tmdb', self.getInfoByTMDBId) addEvent('movie.info_by_tmdb', self.getInfoByTMDBId)
# Use base wrapper # Use base wrapper

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

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

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

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

5
couchpotato/core/providers/nzb/newznab/__init__.py

@ -8,9 +8,10 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'nzb_providers',
'name': 'newznab', 'name': 'newznab',
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a> and <a href="http://nzbs.org" target="_blank">nzbs.org</a>', 'order': 10,
'description': 'Enable multiple NewzNab providers such as <a href="https://nzb.su" target="_blank">NZB.su</a> and <a href="https://nzbs.org" target="_blank">nzbs.org</a>',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {

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

@ -1,7 +1,7 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
@ -161,9 +161,9 @@ class Newznab(NZBProvider, RSS):
def getHosts(self): def getHosts(self):
uses = [x.strip() for x in str(self.conf('use')).split(',')] uses = splitString(str(self.conf('use')))
hosts = [x.strip() for x in self.conf('host').split(',')] hosts = splitString(self.conf('host'))
api_keys = [x.strip() for x in self.conf('api_key').split(',')] api_keys = splitString(self.conf('api_key'))
list = [] list = []
for nr in range(len(hosts)): for nr in range(len(hosts)):

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

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

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

@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'nzb_providers',
'name': 'nzbindex', 'name': 'nzbindex',
'description': 'Free provider, less accurate. See <a href="http://www.nzbindex.nl/">NZBIndex</a>', 'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

@ -19,8 +19,8 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS): class NzbIndex(NZBProvider, RSS):
urls = { urls = {
'download': 'http://www.nzbindex.nl/download/', 'download': 'https://www.nzbindex.com/download/',
'api': 'http://www.nzbindex.nl/rss/', 'api': 'https://www.nzbindex.com/rss/',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds

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

@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'nzb_providers',
'name': 'nzbmatrix', 'name': 'nzbmatrix',
'label': 'NZBMatrix', 'label': 'NZBMatrix',
'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>', 'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>',

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

@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'nzb_providers',
'name': 'nzbsrus', 'name': 'nzbsrus',
'label': 'Nzbsrus', 'label': 'Nzbsrus',
'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>', 'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>',

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

@ -8,9 +8,10 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'torrent_providers',
'name': 'KickAssTorrents', 'name': 'KickAssTorrents',
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>', 'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

@ -1,6 +1,7 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt, getTitle
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 re import re
@ -12,9 +13,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider): class KickAssTorrents(TorrentProvider):
urls = { urls = {
'test': 'http://kat.ph/', 'test': 'https://kat.ph/',
'detail': 'http://kat.ph/%s', 'detail': 'https://kat.ph/%s',
'search': 'http://kat.ph/i%s/', 'search': 'https://kat.ph/%s-i%s/',
} }
cat_ids = [ cat_ids = [
@ -35,8 +36,10 @@ class KickAssTorrents(TorrentProvider):
if self.isDisabled(): if self.isDisabled():
return results return results
title = simplifyString(getTitle(movie['library'])).replace(' ', '-')
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['identifier'].replace('tt', ''))) data = self.getCache(cache_key, self.urls['search'] % (title, movie['library']['identifier'].replace('tt', '')))
if data: if data:
cat_ids = self.getCatId(quality['identifier']) cat_ids = self.getCatId(quality['identifier'])

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

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

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

@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'torrent_providers',
'name': 'PublicHD', 'name': 'PublicHD',
'description': 'Public Torrent site with only HD content. See <a href="http://publichd.eu/">PublicHD</a>', 'description': 'Public Torrent site with only HD content. See <a href="https://publichd.eu/">PublicHD</a>',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

@ -14,9 +14,9 @@ log = CPLog(__name__)
class PublicHD(TorrentProvider): class PublicHD(TorrentProvider):
urls = { urls = {
'test': 'http://publichd.eu', 'test': 'https://publichd.eu',
'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s', 'detail': 'https://publichd.eu/index.php?page=torrent-details&id=%s',
'search': 'http://publichd.eu/index.php', 'search': 'https://publichd.eu/index.php',
} }
http_time_between_calls = 0 http_time_between_calls = 0

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

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

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

@ -8,9 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'torrent_providers',
'name': 'SceneHD', 'name': 'SceneHD',
'description': 'See <a href="http://scenehd.org">SceneHD</a>', 'description': 'See <a href="https://scenehd.org">SceneHD</a>',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

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

@ -12,11 +12,11 @@ log = CPLog(__name__)
class SceneHD(TorrentProvider): class SceneHD(TorrentProvider):
urls = { urls = {
'test': 'http://scenehd.org/', 'test': 'https://scenehd.org/',
'login' : 'http://scenehd.org/takelogin.php', 'login' : 'https://scenehd.org/takelogin.php',
'detail': 'http://scenehd.org/details.php?id=%s', 'detail': 'https://scenehd.org/details.php?id=%s',
'search': 'http://scenehd.org/browse.php?ajax', 'search': 'https://scenehd.org/browse.php?ajax',
'download': 'http://scenehd.org/download.php?id=%s', 'download': 'https://scenehd.org/download.php?id=%s',
} }
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds

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

@ -5,23 +5,26 @@ def start():
config = [{ config = [{
'name': 'thepiratebay', 'name': 'thepiratebay',
'groups': [{ 'groups': [
'tab': 'searcher', {
'subtab': 'providers', 'tab': 'searcher',
'name': 'ThePirateBay', 'subtab': 'torrent_providers',
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>', 'name': 'ThePirateBay',
'options': [ 'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
{ 'wizard': True,
'name': 'enabled', 'options': [
'type': 'enabler', {
'default': False 'name': 'enabled',
}, 'type': 'enabler',
{ 'default': False
'name': 'domain', },
'advanced': True, {
'label': 'Proxy server', 'name': 'domain',
'description': 'Domain for requests, keep empty to let CouchPotato pick.', 'advanced': True,
} 'label': 'Proxy server',
], 'description': 'Domain for requests, keep empty to let CouchPotato pick.',
}] }
],
}
]
}] }]

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

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

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

@ -4,6 +4,7 @@ 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 digits, ascii_letters from string import digits, ascii_letters
from urllib2 import HTTPError
import re import re
log = CPLog(__name__) log = CPLog(__name__)
@ -22,7 +23,12 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library']) movie_name = getTitle(group['library'])
url = self.urls['api'] % self.movieUrlName(movie_name) url = self.urls['api'] % self.movieUrlName(movie_name)
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url) try:
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url, show_error = False)
except HTTPError:
log.debug('No page found for: %s', movie_name)
data = None
result_data = {'480p':[], '720p':[], '1080p':[]} result_data = {'480p':[], '720p':[], '1080p':[]}
if not data: if not data:
@ -47,7 +53,14 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library']) movie_name = getTitle(group['library'])
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name})) url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url) try:
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url, show_error = False)
except HTTPError:
log.debug('No alternative page found for: %s', movie_name)
data = None
if not data:
return results
try: try:
tables = SoupStrainer('div') tables = SoupStrainer('div')

2
couchpotato/core/settings/__init__.py

@ -204,7 +204,6 @@ class Settings(object):
except: except:
pass pass
#db.close()
return prop return prop
def setProperty(self, identifier, value = ''): def setProperty(self, identifier, value = ''):
@ -221,4 +220,3 @@ class Settings(object):
p.value = toUnicode(value) p.value = toUnicode(value)
db.commit() db.commit()
#db.close()

12
couchpotato/runner.py

@ -170,18 +170,17 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
log.warning('%s %s %s line:%s', (category, message, filename, lineno)) log.warning('%s %s %s line:%s', (category, message, filename, lineno))
warnings.showwarning = customwarn warnings.showwarning = customwarn
# Check if database exists
db = Env.get('db_path')
db_exists = os.path.isfile(db_path)
# Load configs & plugins # Load configs & plugins
loader = Env.get('loader') loader = Env.get('loader')
loader.preload(root = base_path) loader.preload(root = base_path)
loader.run() loader.run()
# Load migrations # Load migrations
initialize = True if db_exists:
db = Env.get('db_path')
if os.path.isfile(db_path):
initialize = False
from migrate.versioning.api import version_control, db_version, version, upgrade from migrate.versioning.api import version_control, db_version, version, upgrade
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
@ -201,7 +200,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from couchpotato.core.settings.model import setup from couchpotato.core.settings.model import setup
setup() setup()
if initialize: # Fill database with needed stuff
if not db_exists:
fireEvent('app.initialize', in_order = True) fireEvent('app.initialize', in_order = True)
# Create app # Create app

BIN
couchpotato/static/images/couch.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
couchpotato/static/images/emptylist.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
couchpotato/static/images/gear.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
couchpotato/static/images/homescreen.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
couchpotato/static/images/icon.attention.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 573 B

BIN
couchpotato/static/images/icon.check.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 352 B

BIN
couchpotato/static/images/icon.delete.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 220 B

BIN
couchpotato/static/images/icon.download.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

After

Width:  |  Height:  |  Size: 306 B

BIN
couchpotato/static/images/icon.edit.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 511 B

BIN
couchpotato/static/images/icon.files.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 763 B

BIN
couchpotato/static/images/icon.folder.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 690 B

BIN
couchpotato/static/images/icon.imdb.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 152 B

BIN
couchpotato/static/images/icon.info.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 724 B

BIN
couchpotato/static/images/icon.rating.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 239 B

BIN
couchpotato/static/images/icon.refresh.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 B

After

Width:  |  Height:  |  Size: 568 B

BIN
couchpotato/static/images/icon.spinner.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
couchpotato/static/images/icon.trailer.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 435 B

BIN
couchpotato/static/images/icon.undo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 585 B

BIN
couchpotato/static/images/imdb_watchlist.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
couchpotato/static/images/right.arrow.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 151 B

BIN
couchpotato/static/images/sprite.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save